@frontmcp/plugin-skilled-openapi 1.4.0 → 1.5.0-rc.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.
Files changed (39) hide show
  1. package/README.md +123 -17
  2. package/esm/index.mjs +640 -433
  3. package/esm/package.json +19 -8
  4. package/executor/execute-skill-action.d.ts +44 -0
  5. package/executor/execute-skill-action.d.ts.map +1 -0
  6. package/executor/host-concurrency.d.ts +9 -0
  7. package/executor/host-concurrency.d.ts.map +1 -0
  8. package/executor/openapi-runtime.d.ts.map +1 -1
  9. package/executor/ssrf-guard.d.ts +4 -0
  10. package/executor/ssrf-guard.d.ts.map +1 -1
  11. package/index.d.ts +1 -0
  12. package/index.d.ts.map +1 -1
  13. package/index.js +643 -421
  14. package/package.json +19 -8
  15. package/registry/hidden-op.registry.d.ts +15 -1
  16. package/registry/hidden-op.registry.d.ts.map +1 -1
  17. package/security/authority-guard.d.ts +35 -5
  18. package/security/authority-guard.d.ts.map +1 -1
  19. package/skilled-openapi.plugin.d.ts +41 -2
  20. package/skilled-openapi.plugin.d.ts.map +1 -1
  21. package/skilled-openapi.symbols.d.ts +3 -0
  22. package/skilled-openapi.symbols.d.ts.map +1 -1
  23. package/skilled-openapi.types.d.ts +22 -0
  24. package/skilled-openapi.types.d.ts.map +1 -1
  25. package/sync/bundle-sync.service.d.ts +1 -1
  26. package/sync/bundle-sync.service.d.ts.map +1 -1
  27. package/tools/operation-tool.factory.d.ts +5 -4
  28. package/tools/operation-tool.factory.d.ts.map +1 -1
  29. package/tools/run-workflow.schema.d.ts +28 -0
  30. package/tools/run-workflow.schema.d.ts.map +1 -0
  31. package/tools/run-workflow.tool.d.ts +6 -0
  32. package/tools/run-workflow.tool.d.ts.map +1 -0
  33. package/tools/search-skill.schema.d.ts +5 -1
  34. package/tools/search-skill.schema.d.ts.map +1 -1
  35. package/tools/search-skill.tool.d.ts.map +1 -1
  36. package/tools/execute-action.schema.d.ts +0 -26
  37. package/tools/execute-action.schema.d.ts.map +0 -1
  38. package/tools/execute-action.tool.d.ts +0 -6
  39. package/tools/execute-action.tool.d.ts.map +0 -1
package/index.js CHANGED
@@ -1,7 +1,9 @@
1
1
  "use strict";
2
+ var __create = Object.create;
2
3
  var __defProp = Object.defineProperty;
3
4
  var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
5
  var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
5
7
  var __hasOwnProp = Object.prototype.hasOwnProperty;
6
8
  var __export = (target, all) => {
7
9
  for (var name in all)
@@ -15,6 +17,14 @@ var __copyProps = (to, from, except, desc) => {
15
17
  }
16
18
  return to;
17
19
  };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
18
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
29
  var __decorateClass = (decorators, target, key2, kind) => {
20
30
  var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc(target, key2) : target;
@@ -28,6 +38,7 @@ var __decorateClass = (decorators, target, key2, kind) => {
28
38
  // plugins/plugin-skilled-openapi/src/index.ts
29
39
  var index_exports = {};
30
40
  __export(index_exports, {
41
+ SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN: () => SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN,
31
42
  SkilledOpenApiConfig: () => SkilledOpenApiConfig,
32
43
  SkilledOpenApiCredentialResolver: () => SkilledOpenApiCredentialResolver,
33
44
  SkilledOpenApiPlugin: () => SkilledOpenApiPlugin,
@@ -86,6 +97,19 @@ var HiddenOpRegistry = class {
86
97
  get(skillId, actionId) {
87
98
  return this.entries.get(key(skillId, actionId));
88
99
  }
100
+ /**
101
+ * Resolve an operation by actionId (operationId) ALONE — used by the enclave
102
+ * codecall (`run_workflow`), where a sandboxed script calls
103
+ * `callTool(actionId, input)` without naming the owning skill. operationIds
104
+ * are unique within a bundle, so the first match is authoritative; returns
105
+ * undefined when no loaded skill exposes the action.
106
+ */
107
+ getByActionId(actionId) {
108
+ for (const entry of this.entries.values()) {
109
+ if (entry.op.operationId === actionId) return entry;
110
+ }
111
+ return void 0;
112
+ }
89
113
  /** Set or replace the entry for `(skillId, actionId)`. */
90
114
  set(entry) {
91
115
  this.entries.set(key(entry.skillId, entry.op.operationId), entry);
@@ -133,13 +157,31 @@ var AuthorityGuard = class {
133
157
  this.logger = opts.logger;
134
158
  }
135
159
  async check(args) {
136
- const { policy, authInfo, input, env } = args;
137
- if (policy === void 0) {
160
+ const { policy, skillPolicy, isPublic, authInfo, input, env } = args;
161
+ const unprotectedOps = args.unprotectedOps ?? "allow";
162
+ const policies = [skillPolicy, policy].filter((p) => p !== void 0 && p !== null);
163
+ if (policies.length === 0) {
164
+ if (unprotectedOps === "deny" && isPublic !== true) {
165
+ return {
166
+ granted: false,
167
+ deniedBy: "unprotected_operation_denied",
168
+ message: 'operation has no required-authorities policy and is not marked public; blocked by unprotectedOps:"deny" (set the op `public: true` or attach a policy)',
169
+ evaluatedPolicies: []
170
+ };
171
+ }
138
172
  return { granted: true, evaluatedPolicies: [] };
139
173
  }
140
174
  try {
141
175
  const ctx = this.contextBuilder.build(authInfo, input, env);
142
- return await this.engine.evaluate(policy, ctx);
176
+ const evaluatedPolicies = [];
177
+ for (const p of policies) {
178
+ const res = await this.engine.evaluate(p, ctx);
179
+ evaluatedPolicies.push(...res.evaluatedPolicies ?? []);
180
+ if (!res.granted) {
181
+ return { ...res, evaluatedPolicies };
182
+ }
183
+ }
184
+ return { granted: true, evaluatedPolicies };
143
185
  } catch (e) {
144
186
  const message = normalizeCaughtMessage(e);
145
187
  this.logger?.error(`[skilled-openapi:authority] evaluation failed: ${message}`);
@@ -166,9 +208,14 @@ var SkilledOpenApiConfig = class {
166
208
  constructor(options) {
167
209
  this.options = options;
168
210
  }
211
+ options;
169
212
  get outbound() {
170
213
  return this.options.outbound;
171
214
  }
215
+ /** Default-deny policy for ops with no required-authorities (C1/C3). */
216
+ get unprotectedOps() {
217
+ return this.options.unprotectedOps;
218
+ }
172
219
  };
173
220
  var SkilledOpenApiCredentialResolver = class {
174
221
  };
@@ -225,6 +272,20 @@ var skilledOpenApiPluginOptionsObjectSchema = import_lazy_zod.z.object({
225
272
  dev: import_lazy_zod.z.boolean().default(false),
226
273
  /** Outbound HTTP / SSRF defenses for the executor. */
227
274
  outbound: outboundOptionsSchema,
275
+ /**
276
+ * How to treat operations that carry NO required-authorities policy (neither
277
+ * skill-level nor op-level) on the execution surface (`run_workflow` and the
278
+ * internal per-op tools):
279
+ *
280
+ * - `'allow'` (default, backward compatible): policy-less ops are callable —
281
+ * origin trust comes from the signed bundle. Appropriate when every op in
282
+ * the bundle is genuinely public (e.g. a public REST API).
283
+ * - `'deny'`: default-deny the execution surface — a policy-less op is blocked
284
+ * unless the bundle explicitly marks it `public: true`. **Recommended for
285
+ * production**: it ensures a single missing `requiredAuthorities` line can't
286
+ * silently expose a protected op (closes SECURITY-REVIEW C1/C3).
287
+ */
288
+ unprotectedOps: import_lazy_zod.z.enum(["allow", "deny"]).default("allow"),
228
289
  /**
229
290
  * Source-conflict policy when more than one source registers a skill with
230
291
  * the same id. Default: locally-pinned static beats npm beats saas.
@@ -291,6 +352,12 @@ var BundleSyncService = class {
291
352
  this.logger = logger;
292
353
  this.operationToolFactory = operationToolFactory;
293
354
  }
355
+ skillRegistry;
356
+ hiddenOps;
357
+ bundleStore;
358
+ options;
359
+ logger;
360
+ operationToolFactory;
294
361
  skillUnregisterByBundleId = /* @__PURE__ */ new Map();
295
362
  /**
296
363
  * Validate signature and apply the bundle. Returns a structured result;
@@ -485,6 +552,11 @@ var BundleSyncService = class {
485
552
  }
486
553
  const entry = {
487
554
  skillId: skill.id,
555
+ // Pin the skill-level policy so it is AND-ed with the op-level policy
556
+ // at execution time (C2 — previously this was silently dropped).
557
+ ...skill.requiredAuthorities !== void 0 && {
558
+ skillRequiredAuthorities: skill.requiredAuthorities
559
+ },
488
560
  op,
489
561
  service,
490
562
  authBinding,
@@ -506,15 +578,126 @@ function collectPathsWithGet(entries) {
506
578
  return set;
507
579
  }
508
580
 
509
- // plugins/plugin-skilled-openapi/src/tools/execute-action.tool.ts
510
- var import_skills4 = require("@frontmcp/adapters/skills");
581
+ // plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
511
582
  var import_sdk = require("@frontmcp/sdk");
512
583
 
584
+ // plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
585
+ var import_lazy_zod2 = require("@frontmcp/lazy-zod");
586
+ var loadSkillDescription = `Load the full instructions and executable actions for a specific skill.
587
+
588
+ Use this AFTER \`search_skill\` once you've identified the right skill for the
589
+ user's task. The returned object contains:
590
+ - \`instructions\`: markdown the LLM should read carefully before invoking
591
+ - \`actions\`: each action's input/output JSON Schema and required authorities
592
+ - \`bundleVersion\`: changes when the bundle is hot-swapped (use it to detect drift)
593
+
594
+ INPUT: { skillId }
595
+ OUTPUT: { skill: { id, name, description, instructions, actions[] }, isComplete }`;
596
+ var loadSkillInputSchema = {
597
+ skillId: import_lazy_zod2.z.string().min(1).max(256).describe("Stable skill identifier (returned by search_skill)")
598
+ };
599
+ var loadSkillOutputSchema = {
600
+ skill: import_lazy_zod2.z.object({
601
+ id: import_lazy_zod2.z.string(),
602
+ name: import_lazy_zod2.z.string(),
603
+ description: import_lazy_zod2.z.string(),
604
+ instructions: import_lazy_zod2.z.string(),
605
+ bundleVersion: import_lazy_zod2.z.string().optional(),
606
+ actions: import_lazy_zod2.z.array(
607
+ import_lazy_zod2.z.object({
608
+ actionId: import_lazy_zod2.z.string(),
609
+ summary: import_lazy_zod2.z.string(),
610
+ description: import_lazy_zod2.z.string().optional(),
611
+ inputJsonSchema: import_lazy_zod2.z.record(import_lazy_zod2.z.string(), import_lazy_zod2.z.unknown()),
612
+ outputJsonSchema: import_lazy_zod2.z.record(import_lazy_zod2.z.string(), import_lazy_zod2.z.unknown()),
613
+ requiredAuthorities: import_lazy_zod2.z.record(import_lazy_zod2.z.string(), import_lazy_zod2.z.unknown()).optional()
614
+ })
615
+ ).optional()
616
+ }),
617
+ isComplete: import_lazy_zod2.z.boolean(),
618
+ warning: import_lazy_zod2.z.string().optional()
619
+ };
620
+
621
+ // plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
622
+ var LoadSkillTool = class extends import_sdk.ToolContext {
623
+ async execute(input) {
624
+ this.get(BundleSyncService);
625
+ const scope = this.get(import_sdk.ScopeEntry);
626
+ const skillRegistry = scope.skills;
627
+ if (!skillRegistry) {
628
+ throw new import_sdk.InternalMcpError("SkillRegistry is not available on the active scope", "SKILL_REGISTRY_UNAVAILABLE");
629
+ }
630
+ const result = await skillRegistry.loadSkill(input.skillId);
631
+ if (!result) {
632
+ throw new import_sdk.PublicMcpError(`Skill "${input.skillId}" not found`, "SKILL_NOT_FOUND", 404);
633
+ }
634
+ const skill = result.skill;
635
+ return {
636
+ skill: {
637
+ id: skill.id,
638
+ name: skill.name,
639
+ description: skill.description,
640
+ instructions: skill.instructions,
641
+ ...skill.bundleVersion !== void 0 && { bundleVersion: skill.bundleVersion },
642
+ ...skill.actions ? { actions: skill.actions } : {}
643
+ },
644
+ isComplete: result.isComplete,
645
+ ...result.warning !== void 0 && { warning: result.warning }
646
+ };
647
+ }
648
+ };
649
+ LoadSkillTool = __decorateClass([
650
+ (0, import_sdk.Tool)({
651
+ name: "load_skill",
652
+ description: loadSkillDescription,
653
+ inputSchema: loadSkillInputSchema,
654
+ outputSchema: loadSkillOutputSchema,
655
+ annotations: {
656
+ readOnlyHint: true,
657
+ openWorldHint: false
658
+ }
659
+ })
660
+ ], LoadSkillTool);
661
+
662
+ // plugins/plugin-skilled-openapi/src/tools/operation-tool.factory.ts
663
+ var import_skills4 = require("@frontmcp/adapters/skills");
664
+ var import_sdk2 = require("@frontmcp/sdk");
665
+
513
666
  // plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
514
667
  var import_openapi = require("@frontmcp/adapters/openapi");
515
668
 
669
+ // plugins/plugin-skilled-openapi/src/executor/host-concurrency.ts
670
+ var gates = /* @__PURE__ */ new Map();
671
+ async function withHostConcurrency(host, limit, fn) {
672
+ if (!Number.isFinite(limit) || limit <= 0) {
673
+ return fn();
674
+ }
675
+ let gate = gates.get(host);
676
+ if (!gate) {
677
+ gate = { active: 0, queue: [] };
678
+ gates.set(host, gate);
679
+ }
680
+ if (gate.active >= limit) {
681
+ await new Promise((resolve) => gate.queue.push(resolve));
682
+ } else {
683
+ gate.active += 1;
684
+ }
685
+ try {
686
+ return await fn();
687
+ } finally {
688
+ const next = gate.queue.shift();
689
+ if (next) {
690
+ next();
691
+ } else {
692
+ gate.active -= 1;
693
+ if (gate.active <= 0 && gate.queue.length === 0) {
694
+ gates.delete(host);
695
+ }
696
+ }
697
+ }
698
+ }
699
+
516
700
  // plugins/plugin-skilled-openapi/src/executor/ssrf-guard.ts
517
- var import_node_dns = require("node:dns");
518
701
  var PRIVATE_IPV4_BLOCKS = [
519
702
  // RFC 1918
520
703
  { net: ipv4ToInt("10.0.0.0"), mask: 4278190080 },
@@ -522,9 +705,14 @@ var PRIVATE_IPV4_BLOCKS = [
522
705
  { net: ipv4ToInt("192.168.0.0"), mask: 4294901760 },
523
706
  // Loopback
524
707
  { net: ipv4ToInt("127.0.0.0"), mask: 4278190080 },
708
+ // Carrier-grade NAT (RFC 6598) — routable-looking but private; a common
709
+ // SSRF blind spot (SECURITY-REVIEW B4).
710
+ { net: ipv4ToInt("100.64.0.0"), mask: 4290772992 }
711
+ ];
712
+ var ALWAYS_FORBIDDEN_IPV4_BLOCKS = [
525
713
  // Link-local incl. AWS/GCP/Azure metadata 169.254.169.254
526
714
  { net: ipv4ToInt("169.254.0.0"), mask: 4294901760 },
527
- // 0.0.0.0/8
715
+ // "this host on this network" / unspecified
528
716
  { net: ipv4ToInt("0.0.0.0"), mask: 4278190080 }
529
717
  ];
530
718
  function ipv4ToInt(ip) {
@@ -533,25 +721,61 @@ function ipv4ToInt(ip) {
533
721
  const [a, b, c, d] = parts;
534
722
  return a * 16777216 + (b << 16 >>> 0) + (c << 8 >>> 0) + d >>> 0;
535
723
  }
536
- function isPrivateIPv4(ip) {
537
- if (!/^\d+\.\d+\.\d+\.\d+$/.test(ip)) return false;
538
- const parts = ip.split(".").map((n) => parseInt(n, 10));
539
- if (parts.some((p) => Number.isNaN(p) || p < 0 || p > 255)) return false;
724
+ function isValidIPv4(ip) {
725
+ if (!/^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$/.test(ip)) return false;
726
+ return ip.split(".").every((p) => {
727
+ const n = Number(p);
728
+ return n >= 0 && n <= 255;
729
+ });
730
+ }
731
+ function matchesBlock(ip, blocks) {
540
732
  const v = ipv4ToInt(ip);
541
- return PRIVATE_IPV4_BLOCKS.some((b) => (v & b.mask) >>> 0 === b.net);
733
+ return blocks.some((b) => (v & b.mask) >>> 0 === b.net);
734
+ }
735
+ function isPrivateIPv4(ip) {
736
+ if (!isValidIPv4(ip)) return false;
737
+ return matchesBlock(ip, PRIVATE_IPV4_BLOCKS) || matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
738
+ }
739
+ function isAlwaysForbiddenIPv4(ip) {
740
+ if (!isValidIPv4(ip)) return false;
741
+ return matchesBlock(ip, ALWAYS_FORBIDDEN_IPV4_BLOCKS);
542
742
  }
543
743
  function isPrivateIPv6(ip) {
544
744
  const lower = ip.toLowerCase();
545
745
  if (lower === "::1" || lower === "::") return true;
546
746
  if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
547
- if (lower.startsWith("fe80:")) return true;
747
+ if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
548
748
  if (lower.startsWith("::ffff:")) {
549
749
  const v4 = lower.slice("::ffff:".length);
550
750
  return isPrivateIPv4(v4);
551
751
  }
552
752
  return false;
553
753
  }
554
- var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set(["metadata.google.internal", "metadata.azure.com", "metadata.aws.com"]);
754
+ function isAlwaysForbiddenIPv6(ip) {
755
+ const lower = ip.toLowerCase();
756
+ if (lower.startsWith("fe80:") || lower.startsWith("fe80::")) return true;
757
+ if (lower === "fd00:ec2::254") return true;
758
+ if (lower === "::") return true;
759
+ if (lower.startsWith("::ffff:")) {
760
+ return isAlwaysForbiddenIPv4(lower.slice("::ffff:".length));
761
+ }
762
+ return false;
763
+ }
764
+ var FORBIDDEN_METADATA_HOSTS = /* @__PURE__ */ new Set([
765
+ "metadata.google.internal",
766
+ "metadata.azure.com",
767
+ "metadata.aws.com",
768
+ // GCP legacy + Alibaba/OpenStack-style metadata hostnames.
769
+ "metadata",
770
+ "metadata.goog"
771
+ ]);
772
+ function classifyHostLiteral(hostname) {
773
+ let h = hostname;
774
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
775
+ if (isValidIPv4(h)) return { kind: "v4", ip: h };
776
+ if (h.includes(":")) return { kind: "v6", ip: h };
777
+ return { kind: "name", ip: h };
778
+ }
555
779
  async function checkOutboundUrl(target, allowedHosts, outbound) {
556
780
  let url;
557
781
  try {
@@ -569,21 +793,51 @@ async function checkOutboundUrl(target, allowedHosts, outbound) {
569
793
  if (!allowedHosts.has(hostname)) {
570
794
  return { ok: false, reason: `hostname "${hostname}" is not in the bundle's declared services` };
571
795
  }
572
- if (outbound.allowPrivateNetworks) {
796
+ const literal = classifyHostLiteral(hostname);
797
+ if (literal.kind === "v4") {
798
+ if (isAlwaysForbiddenIPv4(literal.ip)) {
799
+ return { ok: false, reason: `metadata/link-local IPv4 ${literal.ip} is always blocked` };
800
+ }
801
+ if (!outbound.allowPrivateNetworks && isPrivateIPv4(literal.ip)) {
802
+ return { ok: false, reason: `private/loopback IPv4 ${literal.ip} is blocked` };
803
+ }
804
+ return { ok: true };
805
+ }
806
+ if (literal.kind === "v6") {
807
+ if (isAlwaysForbiddenIPv6(literal.ip)) {
808
+ return { ok: false, reason: `metadata/link-local IPv6 ${literal.ip} is always blocked` };
809
+ }
810
+ if (!outbound.allowPrivateNetworks && isPrivateIPv6(literal.ip)) {
811
+ return { ok: false, reason: `private IPv6 ${literal.ip} is blocked` };
812
+ }
573
813
  return { ok: true };
574
814
  }
575
815
  let addresses;
576
816
  try {
577
- addresses = await import_node_dns.promises.lookup(hostname, { all: true });
817
+ const { promises: dns } = await import("node:dns");
818
+ addresses = await dns.lookup(literal.ip, { all: true });
578
819
  } catch (e) {
579
- return { ok: false, reason: `DNS resolution failed for "${hostname}": ${e.message}` };
820
+ if (!outbound.allowPrivateNetworks) {
821
+ return { ok: false, reason: `DNS resolution failed for "${hostname}": ${e.message}` };
822
+ }
823
+ return { ok: true };
580
824
  }
581
825
  for (const a of addresses) {
582
- if (a.family === 4 && isPrivateIPv4(a.address)) {
583
- return { ok: false, reason: `host "${hostname}" resolved to private/loopback IPv4 ${a.address}` };
826
+ if (a.family === 4) {
827
+ if (isAlwaysForbiddenIPv4(a.address)) {
828
+ return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv4 ${a.address}` };
829
+ }
830
+ if (!outbound.allowPrivateNetworks && isPrivateIPv4(a.address)) {
831
+ return { ok: false, reason: `host "${hostname}" resolved to private/loopback IPv4 ${a.address}` };
832
+ }
584
833
  }
585
- if (a.family === 6 && isPrivateIPv6(a.address)) {
586
- return { ok: false, reason: `host "${hostname}" resolved to private IPv6 ${a.address}` };
834
+ if (a.family === 6) {
835
+ if (isAlwaysForbiddenIPv6(a.address)) {
836
+ return { ok: false, reason: `host "${hostname}" resolved to metadata/link-local IPv6 ${a.address}` };
837
+ }
838
+ if (!outbound.allowPrivateNetworks && isPrivateIPv6(a.address)) {
839
+ return { ok: false, reason: `host "${hostname}" resolved to private IPv6 ${a.address}` };
840
+ }
587
841
  }
588
842
  }
589
843
  return { ok: true };
@@ -668,7 +922,7 @@ function toMcpOpenAPITool(entry) {
668
922
  };
669
923
  }
670
924
  async function resolveSecurity(tool, ctx) {
671
- const { SecurityResolver } = require("mcp-from-openapi");
925
+ const { SecurityResolver } = await import("mcp-from-openapi");
672
926
  const resolver = new SecurityResolver();
673
927
  return resolver.resolve(tool.mapper, ctx);
674
928
  }
@@ -711,79 +965,103 @@ async function executeOperation(args) {
711
965
  }
712
966
  const timeoutMs = entry.op.timeoutMs ?? outbound.defaultTimeoutMs;
713
967
  const maxBytes = entry.op.maxResponseBytes ?? outbound.defaultMaxResponseBytes;
714
- const ac = new AbortController();
715
- const timer = setTimeout(() => ac.abort(), timeoutMs);
716
- timer.unref?.();
717
- try {
718
- const response = await fetchImpl(req.url, {
719
- method: entry.op.httpMethod,
720
- headers: req.headers,
721
- body: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
722
- signal: ac.signal
723
- });
724
- const contentType = response.headers.get("content-type") ?? void 0;
725
- const reader = response.body?.getReader();
726
- let received = 0;
727
- const chunks = [];
728
- if (reader) {
729
- for (; ; ) {
730
- const { value, done } = await reader.read();
731
- if (done) break;
732
- if (value) {
733
- received += value.byteLength;
734
- if (received > maxBytes) {
735
- return failure(response.status, `response exceeded maxResponseBytes (${maxBytes})`);
968
+ const host = (() => {
969
+ try {
970
+ return new URL(req.url).hostname.toLowerCase();
971
+ } catch {
972
+ return req.url;
973
+ }
974
+ })();
975
+ return withHostConcurrency(host, outbound.maxConcurrencyPerHost, async () => {
976
+ const ac = new AbortController();
977
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
978
+ timer.unref?.();
979
+ try {
980
+ const response = await fetchImpl(req.url, {
981
+ method: entry.op.httpMethod,
982
+ headers: req.headers,
983
+ body: req.body !== void 0 ? JSON.stringify(req.body) : void 0,
984
+ signal: ac.signal,
985
+ // SECURITY: never auto-follow redirects. `fetch` defaults to
986
+ // `redirect: 'follow'`, which re-sends the request — INCLUDING the
987
+ // vault-injected `Authorization`/api-key headers — to the redirect
988
+ // target, even cross-origin. An allowlisted-but-compromised (or
989
+ // open-redirecting) upstream could thus exfiltrate the credential to an
990
+ // attacker host, and the SSRF allowlist (validated only on the initial
991
+ // URL) would never re-evaluate the hop. `manual` makes the 3xx visible
992
+ // here so we can refuse it instead of following blindly.
993
+ redirect: "manual"
994
+ });
995
+ if (response.status >= 300 && response.status < 400) {
996
+ return failure(
997
+ response.status,
998
+ `upstream returned a redirect (${response.status}); not followed to protect injected credentials`
999
+ );
1000
+ }
1001
+ const contentType = response.headers.get("content-type") ?? void 0;
1002
+ const reader = response.body?.getReader();
1003
+ let received = 0;
1004
+ const chunks = [];
1005
+ if (reader) {
1006
+ for (; ; ) {
1007
+ const { value, done } = await reader.read();
1008
+ if (done) break;
1009
+ if (value) {
1010
+ received += value.byteLength;
1011
+ if (received > maxBytes) {
1012
+ return failure(response.status, `response exceeded maxResponseBytes (${maxBytes})`);
1013
+ }
1014
+ chunks.push(value);
736
1015
  }
737
- chunks.push(value);
738
1016
  }
739
1017
  }
1018
+ const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)));
1019
+ const synthetic = new Response(buf, {
1020
+ status: response.status,
1021
+ headers: response.headers
1022
+ });
1023
+ const parsed = await (0, import_openapi.parseResponse)(synthetic);
1024
+ void logger;
1025
+ return {
1026
+ ok: response.ok,
1027
+ status: response.status,
1028
+ contentType,
1029
+ data: parsed.data,
1030
+ responseBytes: received
1031
+ };
1032
+ } catch (e) {
1033
+ const err = e;
1034
+ return failure(0, err.name === "AbortError" ? `timeout after ${timeoutMs}ms` : err.message);
1035
+ } finally {
1036
+ clearTimeout(timer);
740
1037
  }
741
- const buf = Buffer.concat(chunks.map((c) => Buffer.from(c)));
742
- const synthetic = new Response(buf, {
743
- status: response.status,
744
- headers: response.headers
745
- });
746
- const parsed = await (0, import_openapi.parseResponse)(synthetic);
747
- void logger;
748
- return {
749
- ok: response.ok,
750
- status: response.status,
751
- contentType,
752
- data: parsed.data,
753
- responseBytes: received
754
- };
755
- } catch (e) {
756
- const err = e;
757
- return failure(0, err.name === "AbortError" ? `timeout after ${timeoutMs}ms` : err.message);
758
- } finally {
759
- clearTimeout(timer);
760
- }
1038
+ });
761
1039
  }
762
1040
  function failure(status, error) {
763
1041
  return { ok: false, status, data: null, error, responseBytes: 0 };
764
1042
  }
765
1043
 
766
1044
  // plugins/plugin-skilled-openapi/src/executor/schema-cache.ts
767
- var import_lazy_zod2 = require("@frontmcp/lazy-zod");
1045
+ var import_lazy_zod3 = require("@frontmcp/lazy-zod");
768
1046
  var compiled = /* @__PURE__ */ new Map();
769
1047
  var cacheKey = (bundleVersion, opId) => `${bundleVersion}\0${opId}`;
770
1048
  function compileOne(jsonSchema) {
771
1049
  if (!jsonSchema || typeof jsonSchema !== "object") {
772
- return { schema: import_lazy_zod2.z.looseObject({}), failed: true };
1050
+ return { schema: import_lazy_zod3.z.looseObject({}), failed: true };
773
1051
  }
774
1052
  try {
775
- const zodSchema = import_lazy_zod2.z.fromJSONSchema(
1053
+ const zodSchema = import_lazy_zod3.z.fromJSONSchema(
776
1054
  jsonSchema,
777
1055
  {
778
1056
  defaultTarget: "draft-2020-12"
779
1057
  }
780
1058
  );
781
1059
  if (typeof zodSchema?.parse !== "function") {
782
- return { schema: import_lazy_zod2.z.looseObject({}), failed: true };
1060
+ return { schema: import_lazy_zod3.z.looseObject({}), failed: true };
783
1061
  }
784
1062
  return { schema: zodSchema, failed: false };
785
1063
  } catch {
786
- return { schema: import_lazy_zod2.z.looseObject({}), failed: true };
1064
+ return { schema: import_lazy_zod3.z.looseObject({}), failed: true };
787
1065
  }
788
1066
  }
789
1067
  function getCompiledOpSchemas(args) {
@@ -802,345 +1080,7 @@ function getCompiledOpSchemas(args) {
802
1080
  return value;
803
1081
  }
804
1082
 
805
- // plugins/plugin-skilled-openapi/src/tools/execute-action.schema.ts
806
- var import_lazy_zod3 = require("@frontmcp/lazy-zod");
807
- var executeActionDescription = `Execute one action of a previously loaded skill.
808
-
809
- Pipeline:
810
- 1. Resolve (skillId, actionId) \u2192 bundled OpenAPI operation
811
- 2. Authorize: caller's authInfo is checked against the action's required authorities (if any)
812
- 3. Validate: the input is validated against the action's inputJsonSchema by the underlying executor
813
- 4. Outbound: an HTTPS request is built and sent to the service the action belongs to,
814
- with credentials injected from the configured vault (never echoed back to you)
815
- 5. Response: the response body is validated against outputJsonSchema and returned in
816
- a structured envelope. Failures (auth, schema, network) are returned as ok:false
817
- with a structured error string \u2014 they DO NOT throw.
818
-
819
- INPUT: { skillId, actionId, input }
820
- OUTPUT: { ok, status, data?, contentType?, error? }`;
821
- var executeActionInputSchema = {
822
- skillId: import_lazy_zod3.z.string().min(1).max(256).describe("Skill that owns the action"),
823
- actionId: import_lazy_zod3.z.string().min(1).max(256).describe("Action id (operationId) within the skill"),
824
- input: import_lazy_zod3.z.record(import_lazy_zod3.z.string(), import_lazy_zod3.z.unknown()).optional().describe("Flat input object; keys correspond to the action inputJsonSchema properties")
825
- };
826
- var executeActionOutputSchema = {
827
- ok: import_lazy_zod3.z.boolean(),
828
- status: import_lazy_zod3.z.number().int(),
829
- data: import_lazy_zod3.z.unknown().optional(),
830
- contentType: import_lazy_zod3.z.string().optional(),
831
- error: import_lazy_zod3.z.string().optional()
832
- };
833
-
834
- // plugins/plugin-skilled-openapi/src/tools/execute-action.tool.ts
835
- var ExecuteActionTool = class extends import_sdk.ToolContext {
836
- async execute(input) {
837
- this.get(BundleSyncService);
838
- const config = this.get(SkilledOpenApiConfig);
839
- const hiddenOps = this.get(HiddenOpRegistry);
840
- const bundleStore = this.get(import_skills4.BundleStore);
841
- const guard = this.get(AuthorityGuard);
842
- const resolver = this.get(SkilledOpenApiCredentialResolver);
843
- const auditWriter = this.tryGet(import_skills4.SkillAuditWriterToken);
844
- const auditSubject = this.authInfo?.user?.sub ?? "anonymous";
845
- const detachAudit = (op, phase) => {
846
- op.catch((error) => {
847
- this.logger.warn(
848
- `[skill-audit] detached ${phase} write failed: ${error instanceof Error ? error.message : String(error)}`
849
- );
850
- });
851
- };
852
- const TOTAL_STEPS = 5;
853
- const tick = (step, message) => this.progress(step, TOTAL_STEPS, message);
854
- const TELEMETRY_ACCESSOR_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-accessor");
855
- const tel = this.tryGet(TELEMETRY_ACCESSOR_TOKEN);
856
- const phaseEvent = (phase, attrs) => {
857
- tel?.addEvent("skill_action.phase", {
858
- phase,
859
- skillId: input.skillId,
860
- actionId: input.actionId,
861
- ...attrs ?? {}
862
- });
863
- };
864
- await tick(1, "resolve-action");
865
- phaseEvent("resolve-action");
866
- const entry = hiddenOps.get(input.skillId, input.actionId);
867
- if (!entry) {
868
- return {
869
- ok: false,
870
- status: 0,
871
- error: `unknown action "${input.skillId}/${input.actionId}" \u2014 search_skill / load_skill first`
872
- };
873
- }
874
- const pinned = entry;
875
- const bundleId = pinned.bundleId;
876
- const bundle = bundleStore.current();
877
- await tick(2, "authority-check");
878
- phaseEvent("authority-check", { bundleVersion: pinned.bundleVersion });
879
- const policy = pinned.op.requiredAuthorities;
880
- const authResult = await guard.check({
881
- policy,
882
- authInfo: this.authInfo ?? {},
883
- input: input.input ?? {}
884
- });
885
- if (!authResult.granted) {
886
- if (auditWriter) {
887
- detachAudit(
888
- auditWriter.writeAuthorityFail(
889
- {
890
- subject: auditSubject,
891
- skillId: input.skillId,
892
- actionId: input.actionId,
893
- bundleId,
894
- bundleVersion: pinned.bundleVersion,
895
- input: input.input ?? {}
896
- },
897
- { reason: authResult.deniedBy ?? "policy not satisfied" }
898
- ),
899
- "authority-check-fail"
900
- );
901
- }
902
- return {
903
- ok: false,
904
- status: 0,
905
- error: `authority denied: ${authResult.deniedBy ?? "policy not satisfied"}`
906
- };
907
- }
908
- if (auditWriter) {
909
- detachAudit(
910
- auditWriter.writeAuthorityPass({
911
- subject: auditSubject,
912
- skillId: input.skillId,
913
- actionId: input.actionId,
914
- bundleId,
915
- bundleVersion: pinned.bundleVersion,
916
- input: input.input ?? {}
917
- }),
918
- "authority-check-pass"
919
- );
920
- }
921
- const schemas = getCompiledOpSchemas({
922
- bundleVersion: pinned.bundleVersion,
923
- operationId: pinned.op.operationId,
924
- inputSchema: pinned.op.inputSchema,
925
- outputSchema: pinned.op.outputSchema
926
- });
927
- await tick(3, "input-validate");
928
- phaseEvent("input-validate", { bundleVersion: pinned.bundleVersion });
929
- const inputParse = schemas.input.safeParse(input.input ?? {});
930
- if (!inputParse.success) {
931
- return {
932
- ok: false,
933
- status: 0,
934
- error: `input validation failed: ${formatZodIssues(inputParse.error.issues)}`
935
- };
936
- }
937
- const allowedHosts = /* @__PURE__ */ new Set();
938
- try {
939
- allowedHosts.add(new URL(pinned.service.baseUrl).hostname.toLowerCase());
940
- } catch {
941
- }
942
- if (bundle) {
943
- for (const svc of bundle.services) {
944
- try {
945
- allowedHosts.add(new URL(svc.baseUrl).hostname.toLowerCase());
946
- } catch {
947
- }
948
- }
949
- }
950
- const deps = {
951
- outbound: config.outbound,
952
- resolver: { resolve: (ref, opts) => resolver.resolve(ref, opts) },
953
- allowedHosts,
954
- logger: this.logger
955
- };
956
- await tick(4, "http-call");
957
- phaseEvent("http-call", { bundleVersion: pinned.bundleVersion });
958
- let result;
959
- try {
960
- result = await executeOperation({
961
- entry: pinned,
962
- bundleId,
963
- input: inputParse.data,
964
- deps
965
- });
966
- } catch (e) {
967
- if (auditWriter) {
968
- detachAudit(
969
- auditWriter.writeHttpCallFailure(
970
- {
971
- subject: auditSubject,
972
- skillId: input.skillId,
973
- actionId: input.actionId,
974
- bundleId,
975
- bundleVersion: pinned.bundleVersion,
976
- input: input.input ?? {}
977
- },
978
- { status: 0, error: e }
979
- ),
980
- "http-call-failure"
981
- );
982
- }
983
- throw e;
984
- }
985
- if (auditWriter) {
986
- const auditCtx = {
987
- subject: auditSubject,
988
- skillId: input.skillId,
989
- actionId: input.actionId,
990
- bundleId,
991
- bundleVersion: pinned.bundleVersion,
992
- input: input.input ?? {}
993
- };
994
- if (result.ok) {
995
- detachAudit(
996
- auditWriter.writeHttpCallSuccess(auditCtx, {
997
- status: result.status,
998
- output: result.data ?? null
999
- }),
1000
- "http-call-success"
1001
- );
1002
- } else {
1003
- detachAudit(
1004
- auditWriter.writeHttpCallFailure(auditCtx, {
1005
- status: result.status,
1006
- error: result.error ?? `http call failed with status ${result.status}`
1007
- }),
1008
- "http-call-failure"
1009
- );
1010
- }
1011
- }
1012
- const isJsonResponse = (result.contentType ?? "").toLowerCase().includes("application/json");
1013
- if (result.ok && isJsonResponse && result.data !== void 0 && result.data !== null) {
1014
- const outputParse = schemas.output.safeParse(result.data);
1015
- if (!outputParse.success) {
1016
- return {
1017
- ok: false,
1018
- status: result.status,
1019
- ...result.contentType ? { contentType: result.contentType } : {},
1020
- error: `upstream response failed output schema: ${formatZodIssues(outputParse.error.issues)}`
1021
- };
1022
- }
1023
- }
1024
- await tick(5, "done");
1025
- phaseEvent("done", { bundleVersion: pinned.bundleVersion });
1026
- tel?.setAttributes({
1027
- "skill_action.status": result.status,
1028
- "skill_action.ok": result.ok,
1029
- "skill_action.skill_id": input.skillId,
1030
- "skill_action.action_id": input.actionId,
1031
- "skill_action.bundle_version": pinned.bundleVersion
1032
- });
1033
- return {
1034
- ok: result.ok,
1035
- status: result.status,
1036
- ...result.data !== void 0 && result.data !== null ? { data: result.data } : {},
1037
- ...result.contentType ? { contentType: result.contentType } : {},
1038
- ...result.error ? { error: result.error } : {}
1039
- };
1040
- }
1041
- };
1042
- ExecuteActionTool = __decorateClass([
1043
- (0, import_sdk.Tool)({
1044
- name: "execute_action",
1045
- description: executeActionDescription,
1046
- inputSchema: executeActionInputSchema,
1047
- outputSchema: executeActionOutputSchema,
1048
- annotations: {
1049
- readOnlyHint: false,
1050
- destructiveHint: true,
1051
- openWorldHint: true
1052
- }
1053
- })
1054
- ], ExecuteActionTool);
1055
- function formatZodIssues(issues) {
1056
- if (!issues.length) return "unspecified validation error";
1057
- return issues.slice(0, 3).map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1058
- }
1059
-
1060
- // plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
1061
- var import_sdk2 = require("@frontmcp/sdk");
1062
-
1063
- // plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
1064
- var import_lazy_zod4 = require("@frontmcp/lazy-zod");
1065
- var loadSkillDescription = `Load the full instructions and executable actions for a specific skill.
1066
-
1067
- Use this AFTER \`search_skill\` once you've identified the right skill for the
1068
- user's task. The returned object contains:
1069
- - \`instructions\`: markdown the LLM should read carefully before invoking
1070
- - \`actions\`: each action's input/output JSON Schema and required authorities
1071
- - \`bundleVersion\`: changes when the bundle is hot-swapped (use it to detect drift)
1072
-
1073
- INPUT: { skillId }
1074
- OUTPUT: { skill: { id, name, description, instructions, actions[] }, isComplete }`;
1075
- var loadSkillInputSchema = {
1076
- skillId: import_lazy_zod4.z.string().min(1).max(256).describe("Stable skill identifier (returned by search_skill)")
1077
- };
1078
- var loadSkillOutputSchema = {
1079
- skill: import_lazy_zod4.z.object({
1080
- id: import_lazy_zod4.z.string(),
1081
- name: import_lazy_zod4.z.string(),
1082
- description: import_lazy_zod4.z.string(),
1083
- instructions: import_lazy_zod4.z.string(),
1084
- bundleVersion: import_lazy_zod4.z.string().optional(),
1085
- actions: import_lazy_zod4.z.array(
1086
- import_lazy_zod4.z.object({
1087
- actionId: import_lazy_zod4.z.string(),
1088
- summary: import_lazy_zod4.z.string(),
1089
- description: import_lazy_zod4.z.string().optional(),
1090
- inputJsonSchema: import_lazy_zod4.z.record(import_lazy_zod4.z.string(), import_lazy_zod4.z.unknown()),
1091
- outputJsonSchema: import_lazy_zod4.z.record(import_lazy_zod4.z.string(), import_lazy_zod4.z.unknown()),
1092
- requiredAuthorities: import_lazy_zod4.z.record(import_lazy_zod4.z.string(), import_lazy_zod4.z.unknown()).optional()
1093
- })
1094
- ).optional()
1095
- }),
1096
- isComplete: import_lazy_zod4.z.boolean(),
1097
- warning: import_lazy_zod4.z.string().optional()
1098
- };
1099
-
1100
- // plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
1101
- var LoadSkillTool = class extends import_sdk2.ToolContext {
1102
- async execute(input) {
1103
- this.get(BundleSyncService);
1104
- const scope = this.get(import_sdk2.ScopeEntry);
1105
- const skillRegistry = scope.skills;
1106
- if (!skillRegistry) {
1107
- throw new import_sdk2.InternalMcpError("SkillRegistry is not available on the active scope", "SKILL_REGISTRY_UNAVAILABLE");
1108
- }
1109
- const result = await skillRegistry.loadSkill(input.skillId);
1110
- if (!result) {
1111
- throw new import_sdk2.PublicMcpError(`Skill "${input.skillId}" not found`, "SKILL_NOT_FOUND", 404);
1112
- }
1113
- const skill = result.skill;
1114
- return {
1115
- skill: {
1116
- id: skill.id,
1117
- name: skill.name,
1118
- description: skill.description,
1119
- instructions: skill.instructions,
1120
- ...skill.bundleVersion !== void 0 && { bundleVersion: skill.bundleVersion },
1121
- ...skill.actions ? { actions: skill.actions } : {}
1122
- },
1123
- isComplete: result.isComplete,
1124
- ...result.warning !== void 0 && { warning: result.warning }
1125
- };
1126
- }
1127
- };
1128
- LoadSkillTool = __decorateClass([
1129
- (0, import_sdk2.Tool)({
1130
- name: "load_skill",
1131
- description: loadSkillDescription,
1132
- inputSchema: loadSkillInputSchema,
1133
- outputSchema: loadSkillOutputSchema,
1134
- annotations: {
1135
- readOnlyHint: true,
1136
- openWorldHint: false
1137
- }
1138
- })
1139
- ], LoadSkillTool);
1140
-
1141
1083
  // plugins/plugin-skilled-openapi/src/tools/operation-tool.factory.ts
1142
- var import_skills5 = require("@frontmcp/adapters/skills");
1143
- var import_sdk3 = require("@frontmcp/sdk");
1144
1084
  var OPERATION_TOOL_OWNER_TOKEN = /* @__PURE__ */ Symbol.for("skilled-openapi:operation-tool-owner");
1145
1085
  var OPERATION_TOOL_OWNER = {
1146
1086
  kind: "plugin",
@@ -1177,6 +1117,7 @@ var OperationToolFactory = class {
1177
1117
  constructor(deps) {
1178
1118
  this.deps = deps;
1179
1119
  }
1120
+ deps;
1180
1121
  /** Map (bundleId|opId) → the executor function that doubles as registry token. */
1181
1122
  registered = /* @__PURE__ */ new Map();
1182
1123
  /** Tool names this factory has pushed into the classification registry. */
@@ -1199,16 +1140,16 @@ var OperationToolFactory = class {
1199
1140
  const executor = this.makeExecutor(entry);
1200
1141
  const metadata = buildOperationMetadata(entry);
1201
1142
  const record = {
1202
- kind: import_sdk3.ToolKind.FUNCTION,
1143
+ kind: import_sdk2.ToolKind.FUNCTION,
1203
1144
  provide: executor,
1204
1145
  metadata
1205
1146
  };
1206
- const instance = new import_sdk3.ToolInstance(record, this.deps.providers, OPERATION_TOOL_OWNER);
1147
+ const instance = new import_sdk2.ToolInstance(record, this.deps.providers, OPERATION_TOOL_OWNER);
1207
1148
  this.deps.toolRegistry.registerToolInstance(instance);
1208
1149
  this.registered.set(key2, executor);
1209
1150
  if (this.deps.classificationRegistry && ctx?.pathsWithGet) {
1210
1151
  try {
1211
- const classification = (0, import_skills5.classifyOne)(
1152
+ const classification = (0, import_skills4.classifyOne)(
1212
1153
  entry.bundleId,
1213
1154
  {
1214
1155
  operationId: entry.op.operationId,
@@ -1253,19 +1194,22 @@ var OperationToolFactory = class {
1253
1194
  return this.registered.size;
1254
1195
  }
1255
1196
  /**
1256
- * Build the actual function executor for one op. Mirrors `ExecuteActionTool.execute`:
1197
+ * Build the actual function executor for one op. Mirrors the skill-action
1198
+ * executor (`executeSkillAction`):
1257
1199
  * authority check + input validation + `executeOperation` — so callers
1258
1200
  * reaching the op via `callTool` get the same security gates as callers
1259
- * going through `execute_action`.
1201
+ * going through `run_workflow`.
1260
1202
  */
1261
1203
  makeExecutor(entry) {
1262
1204
  return async (input, ctx) => {
1263
1205
  const config = ctx.get(SkilledOpenApiConfig);
1264
1206
  const guard = ctx.get(AuthorityGuard);
1265
1207
  const resolver = ctx.get(SkilledOpenApiCredentialResolver);
1266
- const policy = entry.op.requiredAuthorities;
1267
1208
  const authResult = await guard.check({
1268
- policy,
1209
+ policy: entry.op.requiredAuthorities,
1210
+ skillPolicy: entry.skillRequiredAuthorities,
1211
+ isPublic: entry.op.public,
1212
+ unprotectedOps: config.unprotectedOps,
1269
1213
  authInfo: ctx.authInfo ?? {},
1270
1214
  input: input ?? {}
1271
1215
  });
@@ -1318,6 +1262,233 @@ var OperationToolFactory = class {
1318
1262
  }
1319
1263
  };
1320
1264
 
1265
+ // plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
1266
+ var import_skills5 = require("@frontmcp/adapters/skills");
1267
+ var import_sdk3 = require("@frontmcp/sdk");
1268
+
1269
+ // plugins/plugin-skilled-openapi/src/executor/execute-skill-action.ts
1270
+ function formatZodIssues(issues) {
1271
+ if (!issues.length) return "unspecified validation error";
1272
+ return issues.slice(0, 3).map((i) => `${i.path.join(".") || "<root>"}: ${i.message}`).join("; ");
1273
+ }
1274
+ async function executeSkillAction(args) {
1275
+ const { entry, input, authInfo, deps } = args;
1276
+ const { config, resolver, guard, logger, audit } = deps;
1277
+ const auditCtx = {
1278
+ subject: audit?.subject ?? "anonymous",
1279
+ skillId: entry.skillId,
1280
+ actionId: entry.op.operationId,
1281
+ bundleId: entry.bundleId,
1282
+ bundleVersion: entry.bundleVersion,
1283
+ input: input ?? {}
1284
+ };
1285
+ const detach = (op, phase) => {
1286
+ op?.catch((e) => logger.warn(`[skill-audit] ${phase} write failed: ${e instanceof Error ? e.message : String(e)}`));
1287
+ };
1288
+ const authResult = await guard.check({
1289
+ policy: entry.op.requiredAuthorities,
1290
+ skillPolicy: entry.skillRequiredAuthorities,
1291
+ isPublic: entry.op.public,
1292
+ unprotectedOps: config.unprotectedOps,
1293
+ authInfo: authInfo ?? {},
1294
+ input: input ?? {}
1295
+ });
1296
+ if (!authResult.granted) {
1297
+ detach(
1298
+ audit?.writer.writeAuthorityFail(auditCtx, { reason: authResult.deniedBy ?? "policy not satisfied" }),
1299
+ "authority-fail"
1300
+ );
1301
+ return { ok: false, status: 0, error: `authority denied: ${authResult.deniedBy ?? "policy not satisfied"}` };
1302
+ }
1303
+ detach(audit?.writer.writeAuthorityPass(auditCtx), "authority-pass");
1304
+ const schemas = getCompiledOpSchemas({
1305
+ bundleVersion: entry.bundleVersion,
1306
+ operationId: entry.op.operationId,
1307
+ inputSchema: entry.op.inputSchema,
1308
+ outputSchema: entry.op.outputSchema
1309
+ });
1310
+ const inputParse = schemas.input.safeParse(input ?? {});
1311
+ if (!inputParse.success) {
1312
+ return { ok: false, status: 0, error: `input validation failed: ${formatZodIssues(inputParse.error.issues)}` };
1313
+ }
1314
+ const allowedHosts = /* @__PURE__ */ new Set();
1315
+ try {
1316
+ allowedHosts.add(new URL(entry.service.baseUrl).hostname.toLowerCase());
1317
+ } catch {
1318
+ }
1319
+ const runtimeDeps = {
1320
+ outbound: config.outbound,
1321
+ resolver: { resolve: (ref, opts) => resolver.resolve(ref, opts) },
1322
+ allowedHosts,
1323
+ logger
1324
+ };
1325
+ let result;
1326
+ try {
1327
+ result = await executeOperation({
1328
+ entry,
1329
+ bundleId: entry.bundleId,
1330
+ input: inputParse.data,
1331
+ deps: runtimeDeps
1332
+ });
1333
+ } catch (e) {
1334
+ detach(audit?.writer.writeHttpCallFailure(auditCtx, { status: 0, error: e }), "http-call-failure");
1335
+ throw e;
1336
+ }
1337
+ if (result.ok) {
1338
+ detach(
1339
+ audit?.writer.writeHttpCallSuccess(auditCtx, { status: result.status, output: result.data ?? null }),
1340
+ "http-call-success"
1341
+ );
1342
+ } else {
1343
+ detach(
1344
+ audit?.writer.writeHttpCallFailure(auditCtx, {
1345
+ status: result.status,
1346
+ error: result.error ?? `http call failed with status ${result.status}`
1347
+ }),
1348
+ "http-call-failure"
1349
+ );
1350
+ }
1351
+ const isJsonResponse = (result.contentType ?? "").toLowerCase().includes("application/json");
1352
+ if (result.ok && isJsonResponse && result.data !== void 0 && result.data !== null) {
1353
+ const outputParse = schemas.output.safeParse(result.data);
1354
+ if (!outputParse.success) {
1355
+ return {
1356
+ ok: false,
1357
+ status: result.status,
1358
+ ...result.contentType ? { contentType: result.contentType } : {},
1359
+ error: `upstream response failed output schema: ${formatZodIssues(outputParse.error.issues)}`
1360
+ };
1361
+ }
1362
+ }
1363
+ return {
1364
+ ok: result.ok,
1365
+ status: result.status,
1366
+ ...result.data !== void 0 && result.data !== null ? { data: result.data } : {},
1367
+ ...result.contentType ? { contentType: result.contentType } : {},
1368
+ ...result.error ? { error: result.error } : {}
1369
+ };
1370
+ }
1371
+
1372
+ // plugins/plugin-skilled-openapi/src/tools/run-workflow.schema.ts
1373
+ var import_lazy_zod4 = require("@frontmcp/lazy-zod");
1374
+ var runWorkflowDescription = `Execute a task by running a short AgentScript program in a secure sandbox (enclave).
1375
+
1376
+ This is the EXECUTE step of the skill mechanism. After \`search_skill\` (to find a
1377
+ skill) and \`load_skill\` (to read its instructions + the actions it offers), write
1378
+ a program that calls those actions to accomplish the user's task \u2014 chaining
1379
+ several calls in ONE round-trip, with each call authorized, schema-validated, and
1380
+ sent to the real upstream API (credentials injected from the vault, never echoed).
1381
+
1382
+ Inside the script:
1383
+ - \`await callTool(actionId, input)\` invokes a loaded skill's action (the
1384
+ \`actionId\` is what \`load_skill\` lists under \`actions[]\`). It returns the
1385
+ action's response \`data\`, or throws if the action is unknown / unauthorized /
1386
+ fails validation.
1387
+ - \`Math\` and \`JSON\` are available. There is NO host access and NO network
1388
+ except through \`callTool\`.
1389
+ - The script's final \`return <value>\` is the result.
1390
+
1391
+ Example (after loading a skill exposing getTodo + getUser):
1392
+ const t = await callTool("getTodo", { id: 1 });
1393
+ const u = await callTool("getUser", { id: t.userId });
1394
+ return { todo: t.title, owner: u.name };
1395
+
1396
+ OUTPUT: { success, value?, error?, stats }`;
1397
+ var runWorkflowInputSchema = {
1398
+ script: import_lazy_zod4.z.string().min(1).max(2e4).describe("AgentScript source. Use `await callTool(actionId, input)` for loaded skill actions; end with `return <value>`.")
1399
+ };
1400
+ var runWorkflowOutputSchema = {
1401
+ success: import_lazy_zod4.z.boolean(),
1402
+ value: import_lazy_zod4.z.unknown().optional(),
1403
+ error: import_lazy_zod4.z.string().optional(),
1404
+ stats: import_lazy_zod4.z.object({
1405
+ durationMs: import_lazy_zod4.z.number(),
1406
+ toolCalls: import_lazy_zod4.z.number(),
1407
+ steps: import_lazy_zod4.z.number()
1408
+ }).optional()
1409
+ };
1410
+
1411
+ // plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
1412
+ var MAX_STEPS = 2e6;
1413
+ var MAX_TOOL_CALLS = 25;
1414
+ var TIMEOUT_MS = 8e3;
1415
+ var RunWorkflowTool = class extends import_sdk3.ToolContext {
1416
+ async execute(input) {
1417
+ this.get(BundleSyncService);
1418
+ const config = this.get(SkilledOpenApiConfig);
1419
+ const hiddenOps = this.get(HiddenOpRegistry);
1420
+ const guard = this.get(AuthorityGuard);
1421
+ const resolver = this.get(SkilledOpenApiCredentialResolver);
1422
+ let transformAgentScript;
1423
+ let InterpreterAdapter;
1424
+ try {
1425
+ ({ transformAgentScript } = await import("@enclave-vm/ast"));
1426
+ ({ InterpreterAdapter } = await import("@enclave-vm/core/worker"));
1427
+ } catch {
1428
+ return {
1429
+ success: false,
1430
+ error: "workflow execution unavailable: the @enclave-vm sandbox is not installed. Install @enclave-vm/core and @enclave-vm/ast to enable run_workflow."
1431
+ };
1432
+ }
1433
+ const auditWriter = this.tryGet(import_skills5.SkillAuditWriterToken);
1434
+ const deps = {
1435
+ config: { outbound: config.outbound, unprotectedOps: config.unprotectedOps },
1436
+ resolver: { resolve: (ref, opts) => resolver.resolve(ref, opts) },
1437
+ guard,
1438
+ logger: this.logger,
1439
+ ...auditWriter ? { audit: { writer: auditWriter, subject: this.authInfo?.user?.sub ?? "anonymous" } } : {}
1440
+ };
1441
+ const toolHandler = async (actionId, actionInput) => {
1442
+ const entry = hiddenOps.getByActionId(actionId);
1443
+ if (!entry) {
1444
+ throw new Error(`unknown action "${actionId}" \u2014 run search_skill then load_skill to discover available actions`);
1445
+ }
1446
+ const res = await executeSkillAction({ entry, input: actionInput ?? {}, authInfo: this.authInfo, deps });
1447
+ if (!res.ok) {
1448
+ throw new Error(res.error ?? `action "${actionId}" failed with status ${res.status}`);
1449
+ }
1450
+ return res.data;
1451
+ };
1452
+ let transformed;
1453
+ try {
1454
+ transformed = transformAgentScript(input.script, { transformLoops: false });
1455
+ } catch (error) {
1456
+ return { success: false, error: `AgentScript rejected: ${error instanceof Error ? error.message : String(error)}` };
1457
+ }
1458
+ const adapter = new InterpreterAdapter({ maxSteps: MAX_STEPS });
1459
+ const context = {
1460
+ config: { maxToolCalls: MAX_TOOL_CALLS, timeout: TIMEOUT_MS },
1461
+ stats: { duration: 0, toolCallCount: 0, iterationCount: 0, startTime: 0 },
1462
+ abortController: new AbortController(),
1463
+ aborted: false,
1464
+ toolHandler
1465
+ };
1466
+ const result = await adapter.execute(transformed, context);
1467
+ const stats = {
1468
+ durationMs: result.stats.duration,
1469
+ toolCalls: result.stats.toolCallCount,
1470
+ steps: result.stats.iterationCount
1471
+ };
1472
+ if (!result.success) {
1473
+ return { success: false, error: result.error?.message ?? "workflow failed", stats };
1474
+ }
1475
+ return { success: true, value: result.value, stats };
1476
+ }
1477
+ };
1478
+ RunWorkflowTool = __decorateClass([
1479
+ (0, import_sdk3.Tool)({
1480
+ name: "run_workflow",
1481
+ description: runWorkflowDescription,
1482
+ inputSchema: runWorkflowInputSchema,
1483
+ outputSchema: runWorkflowOutputSchema,
1484
+ annotations: {
1485
+ readOnlyHint: false,
1486
+ destructiveHint: true,
1487
+ openWorldHint: true
1488
+ }
1489
+ })
1490
+ ], RunWorkflowTool);
1491
+
1321
1492
  // plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
1322
1493
  var import_sdk4 = require("@frontmcp/sdk");
1323
1494
 
@@ -1329,19 +1500,25 @@ A "skill" is a curated bundle of REST operations exposed to you behind a single
1329
1500
  named capability \u2014 instead of seeing each individual API endpoint, you see one
1330
1501
  skill that knows how to do something useful (e.g. "billing", "customers"). Use
1331
1502
  this tool first to discover what skills exist for the user's request, then call
1332
- \`load_skill\` to read its instructions + the actions it offers, and \`execute_action\`
1333
- to actually invoke one.
1503
+ \`load_skill\` to read its instructions + the actions it offers, and \`run_workflow\`
1504
+ to execute \u2014 a short sandboxed script that calls the skill's actions via
1505
+ \`callTool(actionId, input)\` (chaining several in one round-trip).
1334
1506
 
1335
1507
  INPUT:
1336
1508
  - query: short natural-language description of what you want to do
1337
1509
  - limit?: max results (default 20, max 50)
1338
1510
  - tags?: filter to skills carrying these tags
1511
+ - notQuery?: anti-query \u2014 describe what you do NOT want; matching skills are
1512
+ demoted (e.g. query "rate limiting", notQuery "enforcement" to prefer guidance
1513
+ over enforcement skills)
1339
1514
 
1340
1515
  OUTPUT: { skills: Array<{ skillId, name, description, score }> }`;
1341
1516
  var searchSkillInputSchema = {
1342
1517
  query: import_lazy_zod5.z.string().min(1).max(2048).describe("Natural-language search query"),
1343
1518
  limit: import_lazy_zod5.z.number().int().positive().max(50).optional().describe("Max results (default 20)"),
1344
- tags: import_lazy_zod5.z.array(import_lazy_zod5.z.string().min(1).max(64)).max(16).optional().describe("Filter by tags")
1519
+ tags: import_lazy_zod5.z.array(import_lazy_zod5.z.string().min(1).max(64)).max(16).optional().describe("Filter by tags"),
1520
+ notQuery: import_lazy_zod5.z.union([import_lazy_zod5.z.string().min(1).max(2048), import_lazy_zod5.z.array(import_lazy_zod5.z.string().min(1).max(2048)).max(8)]).optional().describe("Anti-query: skills matching this are demoted in ranking"),
1521
+ notWeight: import_lazy_zod5.z.number().positive().max(10).optional().describe("Strength of the anti-query demotion (default 1)")
1345
1522
  };
1346
1523
  var searchSkillOutputSchema = {
1347
1524
  skills: import_lazy_zod5.z.array(
@@ -1368,7 +1545,11 @@ var SearchSkillTool = class extends import_sdk4.ToolContext {
1368
1545
  const tags = input.tags;
1369
1546
  const results = await skillRegistry.search(input.query, {
1370
1547
  topK: limit,
1371
- ...tags ? { tags } : {}
1548
+ ...tags ? { tags } : {},
1549
+ // Anti-query demotion (honored when the skill index supports it) — surfaces
1550
+ // what the user wants while pushing down what they explicitly do not.
1551
+ ...input.notQuery !== void 0 ? { negativeQuery: input.notQuery } : {},
1552
+ ...input.notWeight !== void 0 ? { negativeWeight: input.notWeight } : {}
1372
1553
  });
1373
1554
  return {
1374
1555
  skills: results.map((r) => ({
@@ -1396,6 +1577,17 @@ SearchSkillTool = __decorateClass([
1396
1577
 
1397
1578
  // plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
1398
1579
  var TELEMETRY_FACTORY_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-factory");
1580
+ var SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:skilled-openapi:runtime-deps");
1581
+ function resolveRuntimeDeps(scope) {
1582
+ const providers = scope?.providers;
1583
+ if (!providers || typeof providers.get !== "function") return void 0;
1584
+ try {
1585
+ const deps = providers.get(SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN);
1586
+ return deps && typeof deps === "object" ? deps : void 0;
1587
+ } catch {
1588
+ return void 0;
1589
+ }
1590
+ }
1399
1591
  function resolveBundleTelemetry(scope) {
1400
1592
  const providers = scope?.providers;
1401
1593
  if (!providers || typeof providers.get !== "function") return void 0;
@@ -1425,6 +1617,24 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
1425
1617
  }
1426
1618
  return this.cachedLogger;
1427
1619
  }
1620
+ async injectSkillCatalogIntoSearchTool(flowCtx) {
1621
+ const { tools } = flowCtx.state;
1622
+ if (!tools || tools.length === 0) return;
1623
+ const target = tools.find((item) => item.tool?.metadata?.name === "search_skill");
1624
+ if (!target) return;
1625
+ try {
1626
+ this.get(BundleSyncService);
1627
+ } catch {
1628
+ }
1629
+ const scope = this.get(import_sdk5.ScopeEntry);
1630
+ const catalog = (0, import_sdk5.buildSkillsCatalogSummary)(scope.skills);
1631
+ const description = catalog ? `${searchSkillDescription}
1632
+
1633
+ ---
1634
+
1635
+ ${catalog}` : searchSkillDescription;
1636
+ target.tool.metadata.description = description;
1637
+ }
1428
1638
  warnIfInsecureConfig() {
1429
1639
  if (this.options.dev) {
1430
1640
  console.warn(
@@ -1524,13 +1734,21 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
1524
1734
  logger,
1525
1735
  opToolFactory
1526
1736
  );
1737
+ const runtimeDeps = resolveRuntimeDeps(scope);
1527
1738
  let source;
1528
1739
  try {
1529
- source = (0, import_skills6.createBundleSource)(parsed.source, parsed.bundleCacheDir, logger);
1740
+ source = (0, import_skills6.createBundleSource)(parsed.source, parsed.bundleCacheDir, logger, runtimeDeps);
1530
1741
  } catch (e) {
1531
1742
  logger.error(`failed to construct bundle source: ${e.message}`);
1532
1743
  return sync;
1533
1744
  }
1745
+ if (runtimeDeps?.attach) {
1746
+ try {
1747
+ runtimeDeps.attach(source);
1748
+ } catch (e) {
1749
+ logger.warn(`runtime-deps attach threw: ${e.message}`);
1750
+ }
1751
+ }
1534
1752
  source.onChange((bundle) => {
1535
1753
  void sync.apply(bundle).then((result) => {
1536
1754
  if (!result.applied) {
@@ -1552,12 +1770,15 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
1552
1770
  ];
1553
1771
  }
1554
1772
  };
1773
+ __decorateClass([
1774
+ import_sdk5.ListToolsHook.Did("findTools", { priority: 50 })
1775
+ ], SkilledOpenApiPlugin.prototype, "injectSkillCatalogIntoSearchTool", 1);
1555
1776
  SkilledOpenApiPlugin = __decorateClass([
1556
1777
  (0, import_sdk5.Plugin)({
1557
1778
  name: "skilled-openapi",
1558
1779
  description: "Serve a customer's OpenAPI spec as signed skill bundles with hidden per-operation tools mediated by 3 meta-tools.",
1559
1780
  providers: [],
1560
- tools: [SearchSkillTool, LoadSkillTool, ExecuteActionTool]
1781
+ tools: [SearchSkillTool, LoadSkillTool, RunWorkflowTool]
1561
1782
  })
1562
1783
  ], SkilledOpenApiPlugin);
1563
1784
 
@@ -1565,6 +1786,7 @@ SkilledOpenApiPlugin = __decorateClass([
1565
1786
  var index_default = SkilledOpenApiPlugin;
1566
1787
  // Annotate the CommonJS export names for ESM import in node:
1567
1788
  0 && (module.exports = {
1789
+ SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN,
1568
1790
  SkilledOpenApiConfig,
1569
1791
  SkilledOpenApiCredentialResolver,
1570
1792
  SkilledOpenApiPlugin,