@frontmcp/plugin-skilled-openapi 1.8.0 → 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 +38 -101
- package/esm/package.json +6 -5
- package/executor/openapi-runtime.d.ts.map +1 -1
- package/executor/ssrf-guard.d.ts +1 -1
- package/executor/ssrf-guard.d.ts.map +1 -1
- package/index.js +38 -101
- package/package.json +6 -5
- package/skilled-openapi.plugin.d.ts.map +1 -1
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
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
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
|
-
|
|
843
|
-
|
|
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.
|
|
867
|
-
|
|
868
|
-
|
|
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.
|
|
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
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
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
|
|
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.8.
|
|
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.8.
|
|
52
|
-
"@frontmcp/auth": "1.8.
|
|
53
|
-
"@frontmcp/sdk": "1.8.
|
|
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.8.
|
|
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;
|
|
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"}
|
package/executor/ssrf-guard.d.ts
CHANGED
|
@@ -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":"
|
|
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
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
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
|
-
|
|
846
|
-
|
|
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.
|
|
870
|
-
|
|
871
|
-
|
|
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.
|
|
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
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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 (
|
|
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.8.
|
|
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.8.
|
|
52
|
-
"@frontmcp/auth": "1.8.
|
|
53
|
-
"@frontmcp/sdk": "1.8.
|
|
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.8.
|
|
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;
|
|
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"}
|