@frontmcp/plugin-skilled-openapi 1.7.2 → 1.8.1

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/esm/index.mjs CHANGED
@@ -734,6 +734,7 @@ import {
734
734
  buildRequest,
735
735
  parseResponse
736
736
  } from "@frontmcp/adapters/openapi";
737
+ import { isRedirectResponse } from "@frontmcp/utils";
737
738
 
738
739
  // plugins/plugin-skilled-openapi/src/executor/host-concurrency.ts
739
740
  var gates = /* @__PURE__ */ new Map();
@@ -767,68 +768,28 @@ async function withHostConcurrency(host, limit, fn) {
767
768
  }
768
769
 
769
770
  // plugins/plugin-skilled-openapi/src/executor/ssrf-guard.ts
770
- var PRIVATE_IPV4_BLOCKS = [
771
- // RFC 1918
772
- { net: ipv4ToInt("10.0.0.0"), mask: 4278190080 },
773
- { net: ipv4ToInt("172.16.0.0"), mask: 4293918720 },
774
- { net: ipv4ToInt("192.168.0.0"), mask: 4294901760 },
775
- // Loopback
776
- { net: ipv4ToInt("127.0.0.0"), mask: 4278190080 },
777
- // Carrier-grade NAT (RFC 6598) — routable-looking but private; a common
778
- // SSRF blind spot (SECURITY-REVIEW B4).
779
- { net: ipv4ToInt("100.64.0.0"), mask: 4290772992 }
780
- ];
781
- var ALWAYS_FORBIDDEN_IPV4_BLOCKS = [
782
- // Link-local incl. AWS/GCP/Azure metadata 169.254.169.254
783
- { net: ipv4ToInt("169.254.0.0"), mask: 4294901760 },
784
- // "this host on this network" / unspecified
785
- { net: ipv4ToInt("0.0.0.0"), mask: 4278190080 }
786
- ];
787
- function ipv4ToInt(ip) {
788
- const parts = ip.split(".").map((n) => parseInt(n, 10));
789
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return 0;
790
- const [a, b, c, d] = parts;
791
- return a * 16777216 + (b << 16 >>> 0) + (c << 8 >>> 0) + d >>> 0;
792
- }
793
- function isValidIPv4(ip) {
794
- if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) return false;
795
- return ip.split(".").every((p) => {
796
- const n = Number(p);
797
- return n >= 0 && n <= 255;
798
- });
799
- }
800
- function matchesBlock(ip, blocks) {
801
- const v = ipv4ToInt(ip);
802
- return blocks.some((b) => (v & b.mask) >>> 0 === b.net);
803
- }
804
- function isPrivateIPv4(ip) {
805
- if (!isValidIPv4(ip)) return false;
806
- return matchesBlock(ip, PRIVATE_IPV4_BLOCKS) || matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
807
- }
808
- function isAlwaysForbiddenIPv4(ip) {
809
- if (!isValidIPv4(ip)) return false;
810
- return matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
811
- }
812
- function isPrivateIPv6(ip) {
813
- const lower = ip.toLowerCase();
814
- if (lower === "::1" || lower === "::") return true;
815
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
816
- if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
817
- if (lower.startsWith("::ffff:")) {
818
- const v4 = lower.slice("::ffff:".length);
819
- return isPrivateIPv4(v4);
820
- }
821
- return false;
822
- }
823
- function isAlwaysForbiddenIPv6(ip) {
824
- const lower = ip.toLowerCase();
825
- if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
826
- if (lower === "fd00:ec2::254") return true;
827
- if (lower === "::") return true;
828
- if (lower.startsWith("::ffff:")) {
829
- return isAlwaysForbiddenIPv4(lower.slice("::ffff:".length));
830
- }
831
- return false;
771
+ import { classifyIpAddress, parseIpv4 } from "@frontmcp/utils";
772
+ var ALWAYS_FORBIDDEN_RANGES = /* @__PURE__ */ new Set([
773
+ "link-local",
774
+ "cloud-metadata",
775
+ "unspecified",
776
+ "this-network",
777
+ "local-use-nat64"
778
+ ]);
779
+ function findAddressViolation(address, outbound) {
780
+ const classification = classifyIpAddress(address);
781
+ if (!classification) {
782
+ return { description: `malformed IP address ${address}`, alwaysForbidden: true };
783
+ }
784
+ if (classification.range === "public") return void 0;
785
+ const alwaysForbidden = ALWAYS_FORBIDDEN_RANGES.has(classification.range);
786
+ if (!alwaysForbidden && outbound.allowPrivateNetworks) return void 0;
787
+ const category = alwaysForbidden ? "metadata/link-local" : "private/loopback";
788
+ const embedded = classification.embeddedIpv4 ? `embeds ${classification.embeddedIpv4}, ` : "";
789
+ return {
790
+ description: `${category} IPv${classification.family} ${address} (${embedded}in ${classification.cidr})`,
791
+ alwaysForbidden
792
+ };
832
793
  }
833
794
  var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
834
795
  "metadata.google.internal",
@@ -839,11 +800,8 @@ var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
839
800
  "metadata.goog"
840
801
  ]);
841
802
  function classifyHostLiteral(hostname) {
842
- let h = hostname;
843
- if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
844
- if (isValidIPv4(h)) return { kind: "v4", ip: h };
845
- if (h.includes(":")) return { kind: "v6", ip: h };
846
- return { kind: "name", ip: h };
803
+ const address = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
804
+ return { isIpLiteral: parseIpv4(address) !== void 0 || address.includes(":"), address };
847
805
  }
848
806
  async function checkOutboundUrl(target, allowedHosts, outbound) {
849
807
  let url;
@@ -863,50 +821,27 @@ async function checkOutboundUrl(target, allowedHosts, outbound) {
863
821
  return { ok: false, reason: `hostname "${hostname}" is not in the bundle's declared services` };
864
822
  }
865
823
  const literal = classifyHostLiteral(hostname);
866
- if (literal.kind === "v4") {
867
- if (isAlwaysForbiddenIPv4(literal.ip)) {
868
- return { ok: false, reason: `metadata/link-local IPv4 ${literal.ip} is always blocked` };
869
- }
870
- if (!outbound.allowPrivateNetworks && isPrivateIPv4(literal.ip)) {
871
- return { ok: false, reason: `private/loopback IPv4 ${literal.ip} is blocked` };
872
- }
873
- return { ok: true };
874
- }
875
- if (literal.kind === "v6") {
876
- if (isAlwaysForbiddenIPv6(literal.ip)) {
877
- return { ok: false, reason: `metadata/link-local IPv6 ${literal.ip} is always blocked` };
878
- }
879
- if (!outbound.allowPrivateNetworks && isPrivateIPv6(literal.ip)) {
880
- return { ok: false, reason: `private IPv6 ${literal.ip} is blocked` };
824
+ if (literal.isIpLiteral) {
825
+ const violation = findAddressViolation(literal.address, outbound);
826
+ if (violation) {
827
+ return { ok: false, reason: `${violation.description} is ${violation.alwaysForbidden ? "always " : ""}blocked` };
881
828
  }
882
829
  return { ok: true };
883
830
  }
884
831
  let addresses;
885
832
  try {
886
833
  const { promises: dns } = await import("node:dns");
887
- addresses = await dns.lookup(literal.ip, { all: true });
834
+ addresses = await dns.lookup(literal.address, { all: true });
888
835
  } catch (e) {
889
836
  if (!outbound.allowPrivateNetworks) {
890
837
  return { ok: false, reason: `DNS resolution failed for "${hostname}": ${e.message}` };
891
838
  }
892
839
  return { ok: true };
893
840
  }
894
- for (const a of addresses) {
895
- if (a.family === 4) {
896
- if (isAlwaysForbiddenIPv4(a.address)) {
897
- return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv4 ${a.address}` };
898
- }
899
- if (!outbound.allowPrivateNetworks && isPrivateIPv4(a.address)) {
900
- return { ok: false, reason: `host "${hostname}" resolved to private/loopback IPv4 ${a.address}` };
901
- }
902
- }
903
- if (a.family === 6) {
904
- if (isAlwaysForbiddenIPv6(a.address)) {
905
- return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv6 ${a.address}` };
906
- }
907
- if (!outbound.allowPrivateNetworks && isPrivateIPv6(a.address)) {
908
- return { ok: false, reason: `host "${hostname}" resolved to private IPv6 ${a.address}` };
909
- }
841
+ for (const { address } of addresses) {
842
+ const violation = findAddressViolation(address, outbound);
843
+ if (violation) {
844
+ return { ok: false, reason: `host "${hostname}" resolved to ${violation.description}` };
910
845
  }
911
846
  }
912
847
  return { ok: true };
@@ -1061,7 +996,7 @@ async function executeOperation(args) {
1061
996
  // here so we can refuse it instead of following blindly.
1062
997
  redirect: "manual"
1063
998
  });
1064
- if (response.status >= 300 && response.status < 400) {
999
+ if (isRedirectResponse(response)) {
1065
1000
  return failure(
1066
1001
  response.status,
1067
1002
  `upstream returned a redirect (${response.status}); not followed to protect injected credentials`
@@ -1699,7 +1634,9 @@ var SkilledOpenApiPlugin = class extends DynamicPlugin {
1699
1634
  } catch {
1700
1635
  }
1701
1636
  const scope = this.get(ScopeEntry3);
1702
- const catalog = buildSkillsCatalogSummary(scope.skills);
1637
+ const catalog = buildSkillsCatalogSummary(scope.skills, {
1638
+ mcpResources: scope.metadata?.skillsConfig?.mcpResources
1639
+ });
1703
1640
  const description = catalog ? `${searchSkillDescription}
1704
1641
 
1705
1642
  ---
package/esm/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-skilled-openapi",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
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,14 +48,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@frontmcp/adapters": "1.7.2",
52
- "@frontmcp/auth": "1.7.2",
53
- "@frontmcp/sdk": "1.7.2"
51
+ "@frontmcp/adapters": "1.8.1",
52
+ "@frontmcp/auth": "1.8.1",
53
+ "@frontmcp/sdk": "1.8.1",
54
+ "@frontmcp/utils": "1.8.1"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "@enclave-vm/ast": "*",
57
58
  "@enclave-vm/core": "*",
58
- "@frontmcp/lazy-zod": "1.7.2"
59
+ "@frontmcp/lazy-zod": "1.8.1"
59
60
  },
60
61
  "peerDependenciesMeta": {
61
62
  "@enclave-vm/ast": {
@@ -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;AAEpD,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,CA8H3B"}
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"}
@@ -7,7 +7,7 @@ export declare function isPrivateIPv4(ip: string): boolean;
7
7
  /** Metadata / link-local IPv4 — blocked even when `allowPrivateNetworks` is on. */
8
8
  export declare function isAlwaysForbiddenIPv4(ip: string): boolean;
9
9
  export declare function isPrivateIPv6(ip: string): boolean;
10
- /** Metadata / link-local IPv6 — blocked even when `allowPrivateNetworks` is on. */
10
+ /** Metadata / link-local IPv6 (including IPv4-carrying forms) — blocked even when `allowPrivateNetworks` is on. */
11
11
  export declare function isAlwaysForbiddenIPv6(ip: string): boolean;
12
12
  /**
13
13
  * Validate an outbound URL against the allowlist + scheme rules + IP blocklist.
@@ -1 +1 @@
1
- {"version":3,"file":"ssrf-guard.d.ts","sourceRoot":"","sources":["../../src/executor/ssrf-guard.ts"],"names":[],"mappings":"AA2BA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAkDD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAOjD;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAWjD;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAazD;AAyBD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EACjC,QAAQ,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CAkF1B"}
1
+ {"version":3,"file":"ssrf-guard.d.ts","sourceRoot":"","sources":["../../src/executor/ssrf-guard.ts"],"names":[],"mappings":"AAoCA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,0BAA0B,CAAC;AAEhE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,OAAO,CAAC;IACZ,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAwBD,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAIjD;AAED,mFAAmF;AACnF,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGzD;AAED,wBAAgB,aAAa,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGjD;AAED,mHAAmH;AACnH,wBAAgB,qBAAqB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAGzD;AAyCD;;;;;;GAMG;AACH,wBAAsB,gBAAgB,CACpC,MAAM,EAAE,MAAM,EACd,YAAY,EAAE,WAAW,CAAC,MAAM,CAAC,EACjC,QAAQ,EAAE,eAAe,GACxB,OAAO,CAAC,eAAe,CAAC,CA2D1B"}
package/index.js CHANGED
@@ -737,6 +737,7 @@ var import_sdk2 = require("@frontmcp/sdk");
737
737
 
738
738
  // plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
739
739
  var import_openapi = require("@frontmcp/adapters/openapi");
740
+ var import_utils2 = require("@frontmcp/utils");
740
741
 
741
742
  // plugins/plugin-skilled-openapi/src/executor/host-concurrency.ts
742
743
  var gates = /* @__PURE__ */ new Map();
@@ -770,68 +771,28 @@ async function withHostConcurrency(host, limit, fn) {
770
771
  }
771
772
 
772
773
  // plugins/plugin-skilled-openapi/src/executor/ssrf-guard.ts
773
- var PRIVATE_IPV4_BLOCKS = [
774
- // RFC 1918
775
- { net: ipv4ToInt("10.0.0.0"), mask: 4278190080 },
776
- { net: ipv4ToInt("172.16.0.0"), mask: 4293918720 },
777
- { net: ipv4ToInt("192.168.0.0"), mask: 4294901760 },
778
- // Loopback
779
- { net: ipv4ToInt("127.0.0.0"), mask: 4278190080 },
780
- // Carrier-grade NAT (RFC 6598) — routable-looking but private; a common
781
- // SSRF blind spot (SECURITY-REVIEW B4).
782
- { net: ipv4ToInt("100.64.0.0"), mask: 4290772992 }
783
- ];
784
- var ALWAYS_FORBIDDEN_IPV4_BLOCKS = [
785
- // Link-local incl. AWS/GCP/Azure metadata 169.254.169.254
786
- { net: ipv4ToInt("169.254.0.0"), mask: 4294901760 },
787
- // "this host on this network" / unspecified
788
- { net: ipv4ToInt("0.0.0.0"), mask: 4278190080 }
789
- ];
790
- function ipv4ToInt(ip) {
791
- const parts = ip.split(".").map((n) => parseInt(n, 10));
792
- if (parts.length !== 4 || parts.some((p) => Number.isNaN(p))) return 0;
793
- const [a, b, c, d] = parts;
794
- return a * 16777216 + (b << 16 >>> 0) + (c << 8 >>> 0) + d >>> 0;
795
- }
796
- function isValidIPv4(ip) {
797
- if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) return false;
798
- return ip.split(".").every((p) => {
799
- const n = Number(p);
800
- return n >= 0 && n <= 255;
801
- });
802
- }
803
- function matchesBlock(ip, blocks) {
804
- const v = ipv4ToInt(ip);
805
- return blocks.some((b) => (v & b.mask) >>> 0 === b.net);
806
- }
807
- function isPrivateIPv4(ip) {
808
- if (!isValidIPv4(ip)) return false;
809
- return matchesBlock(ip, PRIVATE_IPV4_BLOCKS) || matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
810
- }
811
- function isAlwaysForbiddenIPv4(ip) {
812
- if (!isValidIPv4(ip)) return false;
813
- return matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
814
- }
815
- function isPrivateIPv6(ip) {
816
- const lower = ip.toLowerCase();
817
- if (lower === "::1" || lower === "::") return true;
818
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
819
- if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
820
- if (lower.startsWith("::ffff:")) {
821
- const v4 = lower.slice("::ffff:".length);
822
- return isPrivateIPv4(v4);
823
- }
824
- return false;
825
- }
826
- function isAlwaysForbiddenIPv6(ip) {
827
- const lower = ip.toLowerCase();
828
- if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
829
- if (lower === "fd00:ec2::254") return true;
830
- if (lower === "::") return true;
831
- if (lower.startsWith("::ffff:")) {
832
- return isAlwaysForbiddenIPv4(lower.slice("::ffff:".length));
833
- }
834
- return false;
774
+ var import_utils = require("@frontmcp/utils");
775
+ var ALWAYS_FORBIDDEN_RANGES = /* @__PURE__ */ new Set([
776
+ "link-local",
777
+ "cloud-metadata",
778
+ "unspecified",
779
+ "this-network",
780
+ "local-use-nat64"
781
+ ]);
782
+ function findAddressViolation(address, outbound) {
783
+ const classification = (0, import_utils.classifyIpAddress)(address);
784
+ if (!classification) {
785
+ return { description: `malformed IP address ${address}`, alwaysForbidden: true };
786
+ }
787
+ if (classification.range === "public") return void 0;
788
+ const alwaysForbidden = ALWAYS_FORBIDDEN_RANGES.has(classification.range);
789
+ if (!alwaysForbidden && outbound.allowPrivateNetworks) return void 0;
790
+ const category = alwaysForbidden ? "metadata/link-local" : "private/loopback";
791
+ const embedded = classification.embeddedIpv4 ? `embeds ${classification.embeddedIpv4}, ` : "";
792
+ return {
793
+ description: `${category} IPv${classification.family} ${address} (${embedded}in ${classification.cidr})`,
794
+ alwaysForbidden
795
+ };
835
796
  }
836
797
  var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
837
798
  "metadata.google.internal",
@@ -842,11 +803,8 @@ var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
842
803
  "metadata.goog"
843
804
  ]);
844
805
  function classifyHostLiteral(hostname) {
845
- let h = hostname;
846
- if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
847
- if (isValidIPv4(h)) return { kind: "v4", ip: h };
848
- if (h.includes(":")) return { kind: "v6", ip: h };
849
- return { kind: "name", ip: h };
806
+ const address = hostname.startsWith("[") && hostname.endsWith("]") ? hostname.slice(1, -1) : hostname;
807
+ return { isIpLiteral: (0, import_utils.parseIpv4)(address) !== void 0 || address.includes(":"), address };
850
808
  }
851
809
  async function checkOutboundUrl(target, allowedHosts, outbound) {
852
810
  let url;
@@ -866,50 +824,27 @@ async function checkOutboundUrl(target, allowedHosts, outbound) {
866
824
  return { ok: false, reason: `hostname "${hostname}" is not in the bundle's declared services` };
867
825
  }
868
826
  const literal = classifyHostLiteral(hostname);
869
- if (literal.kind === "v4") {
870
- if (isAlwaysForbiddenIPv4(literal.ip)) {
871
- return { ok: false, reason: `metadata/link-local IPv4 ${literal.ip} is always blocked` };
872
- }
873
- if (!outbound.allowPrivateNetworks && isPrivateIPv4(literal.ip)) {
874
- return { ok: false, reason: `private/loopback IPv4 ${literal.ip} is blocked` };
875
- }
876
- return { ok: true };
877
- }
878
- if (literal.kind === "v6") {
879
- if (isAlwaysForbiddenIPv6(literal.ip)) {
880
- return { ok: false, reason: `metadata/link-local IPv6 ${literal.ip} is always blocked` };
881
- }
882
- if (!outbound.allowPrivateNetworks && isPrivateIPv6(literal.ip)) {
883
- return { ok: false, reason: `private IPv6 ${literal.ip} is blocked` };
827
+ if (literal.isIpLiteral) {
828
+ const violation = findAddressViolation(literal.address, outbound);
829
+ if (violation) {
830
+ return { ok: false, reason: `${violation.description} is ${violation.alwaysForbidden ? "always " : ""}blocked` };
884
831
  }
885
832
  return { ok: true };
886
833
  }
887
834
  let addresses;
888
835
  try {
889
836
  const { promises: dns } = await import("node:dns");
890
- addresses = await dns.lookup(literal.ip, { all: true });
837
+ addresses = await dns.lookup(literal.address, { all: true });
891
838
  } catch (e) {
892
839
  if (!outbound.allowPrivateNetworks) {
893
840
  return { ok: false, reason: `DNS resolution failed for "${hostname}": ${e.message}` };
894
841
  }
895
842
  return { ok: true };
896
843
  }
897
- for (const a of addresses) {
898
- if (a.family === 4) {
899
- if (isAlwaysForbiddenIPv4(a.address)) {
900
- return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv4 ${a.address}` };
901
- }
902
- if (!outbound.allowPrivateNetworks && isPrivateIPv4(a.address)) {
903
- return { ok: false, reason: `host "${hostname}" resolved to private/loopback IPv4 ${a.address}` };
904
- }
905
- }
906
- if (a.family === 6) {
907
- if (isAlwaysForbiddenIPv6(a.address)) {
908
- return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv6 ${a.address}` };
909
- }
910
- if (!outbound.allowPrivateNetworks && isPrivateIPv6(a.address)) {
911
- return { ok: false, reason: `host "${hostname}" resolved to private IPv6 ${a.address}` };
912
- }
844
+ for (const { address } of addresses) {
845
+ const violation = findAddressViolation(address, outbound);
846
+ if (violation) {
847
+ return { ok: false, reason: `host "${hostname}" resolved to ${violation.description}` };
913
848
  }
914
849
  }
915
850
  return { ok: true };
@@ -1064,7 +999,7 @@ async function executeOperation(args) {
1064
999
  // here so we can refuse it instead of following blindly.
1065
1000
  redirect: "manual"
1066
1001
  });
1067
- if (response.status >= 300 && response.status < 400) {
1002
+ if ((0, import_utils2.isRedirectResponse)(response)) {
1068
1003
  return failure(
1069
1004
  response.status,
1070
1005
  `upstream returned a redirect (${response.status}); not followed to protect injected credentials`
@@ -1702,7 +1637,9 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
1702
1637
  } catch {
1703
1638
  }
1704
1639
  const scope = this.get(import_sdk5.ScopeEntry);
1705
- const catalog = (0, import_sdk5.buildSkillsCatalogSummary)(scope.skills);
1640
+ const catalog = (0, import_sdk5.buildSkillsCatalogSummary)(scope.skills, {
1641
+ mcpResources: scope.metadata?.skillsConfig?.mcpResources
1642
+ });
1706
1643
  const description = catalog ? `${searchSkillDescription}
1707
1644
 
1708
1645
  ---
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frontmcp/plugin-skilled-openapi",
3
- "version": "1.7.2",
3
+ "version": "1.8.1",
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,14 +48,15 @@
48
48
  }
49
49
  },
50
50
  "dependencies": {
51
- "@frontmcp/adapters": "1.7.2",
52
- "@frontmcp/auth": "1.7.2",
53
- "@frontmcp/sdk": "1.7.2"
51
+ "@frontmcp/adapters": "1.8.1",
52
+ "@frontmcp/auth": "1.8.1",
53
+ "@frontmcp/sdk": "1.8.1",
54
+ "@frontmcp/utils": "1.8.1"
54
55
  },
55
56
  "peerDependencies": {
56
57
  "@enclave-vm/ast": "*",
57
58
  "@enclave-vm/core": "*",
58
- "@frontmcp/lazy-zod": "1.7.2"
59
+ "@frontmcp/lazy-zod": "1.8.1"
59
60
  },
60
61
  "peerDependenciesMeta": {
61
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,EAKb,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAMvB,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,yBAAyB,CAAC;AAmBjC;;;;;;;;;;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;AAQH,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;IAuB7F,OAAO,CAAC,oBAAoB;WAaZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CA0J5F"}
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,EAKb,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAMvB,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,yBAAyB,CAAC;AAmBjC;;;;;;;;;;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;AAQH,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;IAyB7F,OAAO,CAAC,oBAAoB;WAaZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CA0J5F"}