@frontmcp/plugin-skilled-openapi 1.8.2 → 1.8.4
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 +258 -16
- package/esm/package.json +6 -6
- package/index.js +285 -45
- package/package.json +6 -6
- package/security/authority-guard.d.ts +34 -1
- package/security/authority-guard.d.ts.map +1 -1
- package/security/skill-visibility.d.ts +46 -0
- package/security/skill-visibility.d.ts.map +1 -0
- package/skilled-openapi.plugin.d.ts +10 -0
- package/skilled-openapi.plugin.d.ts.map +1 -1
- package/tools/load-skill.tool.d.ts.map +1 -1
- package/tools/search-skill.tool.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -11,12 +11,14 @@ var __decorateClass = (decorators, target, key2, kind) => {
|
|
|
11
11
|
|
|
12
12
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
|
|
13
13
|
import {
|
|
14
|
-
BundleStore,
|
|
14
|
+
BundleStore as BundleStore3,
|
|
15
15
|
createBundleSource
|
|
16
16
|
} from "@frontmcp/adapters/skills";
|
|
17
17
|
import {
|
|
18
18
|
buildSkillsCatalogSummary,
|
|
19
19
|
DynamicPlugin,
|
|
20
|
+
FlowHooksOf,
|
|
21
|
+
FrontMcpContextStorage,
|
|
20
22
|
FrontMcpLogger,
|
|
21
23
|
ListToolsHook,
|
|
22
24
|
Plugin,
|
|
@@ -122,16 +124,61 @@ import {
|
|
|
122
124
|
AuthoritiesEvaluatorRegistry,
|
|
123
125
|
AuthoritiesProfileRegistry
|
|
124
126
|
} from "@frontmcp/auth";
|
|
127
|
+
function policyDependsOnInput(value) {
|
|
128
|
+
if (typeof value === "string") return value === "input" || value.startsWith("input.");
|
|
129
|
+
if (Array.isArray(value)) return value.some(policyDependsOnInput);
|
|
130
|
+
if (value !== null && typeof value === "object") {
|
|
131
|
+
for (const [key2, nested] of Object.entries(value)) {
|
|
132
|
+
if (key2 === "fromInput" || key2 === "custom" || key2 === "guards") return true;
|
|
133
|
+
if (policyDependsOnInput(key2) || policyDependsOnInput(nested)) return true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
function judgeablePart(policy, engine) {
|
|
139
|
+
if (policy === void 0 || policy === null) return void 0;
|
|
140
|
+
const profileDependsOnInput = (name) => typeof name === "string" && policyDependsOnInput(engine.resolveProfile(name));
|
|
141
|
+
const value = policy;
|
|
142
|
+
if (typeof value === "string") return profileDependsOnInput(value) ? void 0 : policy;
|
|
143
|
+
if (Array.isArray(value)) {
|
|
144
|
+
const names = value.filter((name) => !profileDependsOnInput(name));
|
|
145
|
+
return names.length > 0 ? names : void 0;
|
|
146
|
+
}
|
|
147
|
+
return policyDependsOnInput(policy) ? void 0 : policy;
|
|
148
|
+
}
|
|
125
149
|
var AuthorityGuard = class {
|
|
126
150
|
engine;
|
|
127
151
|
contextBuilder;
|
|
128
152
|
logger;
|
|
153
|
+
serverAuthorities;
|
|
129
154
|
constructor(opts = {}) {
|
|
130
155
|
const profiles = opts.profiles ?? new AuthoritiesProfileRegistry();
|
|
131
156
|
const evaluators = opts.evaluators ?? new AuthoritiesEvaluatorRegistry();
|
|
132
157
|
this.engine = new AuthoritiesEngine(profiles, evaluators);
|
|
133
158
|
this.contextBuilder = new AuthoritiesContextBuilder();
|
|
134
159
|
this.logger = opts.logger;
|
|
160
|
+
this.serverAuthorities = opts.serverAuthorities;
|
|
161
|
+
}
|
|
162
|
+
/** The server's engine and context builder when it configures authorities, else the guard's own. */
|
|
163
|
+
authorities() {
|
|
164
|
+
return this.serverAuthorities?.() ?? { engine: this.engine, contextBuilder: this.contextBuilder };
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Whether the caller may be shown a skill or action (in `search_skill`,
|
|
168
|
+
* `load_skill`, the catalog and the SDK skill surfaces), judged without the
|
|
169
|
+
* action's input. A rule that depends on the input can't be judged yet and
|
|
170
|
+
* doesn't hide anything; the call is still checked by {@link check}. Every other
|
|
171
|
+
* rule must grant, and a policy-less action under `unprotectedOps: 'deny'` must
|
|
172
|
+
* be public, exactly as at call time.
|
|
173
|
+
*/
|
|
174
|
+
async canDiscover(args) {
|
|
175
|
+
const { engine } = this.authorities();
|
|
176
|
+
const hasPolicy = [args.skillPolicy, args.policy].some((p) => p !== void 0 && p !== null);
|
|
177
|
+
const skillPolicy = judgeablePart(args.skillPolicy, engine);
|
|
178
|
+
const policy = judgeablePart(args.policy, engine);
|
|
179
|
+
if (hasPolicy && skillPolicy === void 0 && policy === void 0) return true;
|
|
180
|
+
const result = await this.check({ ...args, skillPolicy, policy, input: {} });
|
|
181
|
+
return result.granted;
|
|
135
182
|
}
|
|
136
183
|
async check(args) {
|
|
137
184
|
const { policy, skillPolicy, isPublic, authInfo, input, env } = args;
|
|
@@ -149,10 +196,11 @@ var AuthorityGuard = class {
|
|
|
149
196
|
return { granted: true, evaluatedPolicies: [] };
|
|
150
197
|
}
|
|
151
198
|
try {
|
|
152
|
-
const
|
|
199
|
+
const { engine, contextBuilder } = this.authorities();
|
|
200
|
+
const ctx = contextBuilder.build(authInfo, input, env);
|
|
153
201
|
const evaluatedPolicies = [];
|
|
154
202
|
for (const p of policies) {
|
|
155
|
-
const res = await
|
|
203
|
+
const res = await engine.evaluate(p, ctx);
|
|
156
204
|
evaluatedPolicies.push(...res.evaluatedPolicies ?? []);
|
|
157
205
|
if (!res.granted) {
|
|
158
206
|
return { ...res, evaluatedPolicies };
|
|
@@ -180,6 +228,112 @@ function normalizeCaughtMessage(e) {
|
|
|
180
228
|
return String(e ?? "authority evaluation threw");
|
|
181
229
|
}
|
|
182
230
|
|
|
231
|
+
// plugins/plugin-skilled-openapi/src/security/skill-visibility.ts
|
|
232
|
+
import { filterServableSkills } from "@frontmcp/sdk";
|
|
233
|
+
function bundleSkillPolicies(bundle) {
|
|
234
|
+
const policies = /* @__PURE__ */ new Map();
|
|
235
|
+
for (const skill of bundle?.skills ?? []) {
|
|
236
|
+
if (skill.requiredAuthorities !== void 0 && skill.requiredAuthorities !== null) {
|
|
237
|
+
policies.set(skill.id, skill.requiredAuthorities);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return policies;
|
|
241
|
+
}
|
|
242
|
+
var SkillVisibility = class {
|
|
243
|
+
constructor(deps) {
|
|
244
|
+
this.deps = deps;
|
|
245
|
+
this.policies = bundleSkillPolicies(deps.bundle);
|
|
246
|
+
this.authInfo = deps.authInfo ?? {};
|
|
247
|
+
}
|
|
248
|
+
deps;
|
|
249
|
+
policies;
|
|
250
|
+
authInfo;
|
|
251
|
+
/** Whether the caller satisfies the bundle skill's own `requiredAuthorities` (true for other skills). */
|
|
252
|
+
async passesBundleSkillRule(skillId) {
|
|
253
|
+
const skillPolicy = this.policies.get(skillId);
|
|
254
|
+
if (skillPolicy === void 0) return true;
|
|
255
|
+
return this.deps.guard.canDiscover({ skillPolicy, authInfo: this.authInfo });
|
|
256
|
+
}
|
|
257
|
+
/**
|
|
258
|
+
* Keep the items whose skill the caller may see: the bundle skill's rule, the
|
|
259
|
+
* `skills:filter` flow and the server's `@Skill({ authorities })` rule. An item
|
|
260
|
+
* whose id names no registered skill (an external skill) is judged by the bundle
|
|
261
|
+
* rule alone, as the SDK's own surfaces keep such results.
|
|
262
|
+
*/
|
|
263
|
+
async filterVisible(items, idOf) {
|
|
264
|
+
if (items.length === 0) return [];
|
|
265
|
+
const entries = items.map((item) => this.resolveEntry(idOf(item)));
|
|
266
|
+
const registered = [...new Set(entries.filter((entry) => entry !== void 0))];
|
|
267
|
+
const visibleEntries = new Set(await this.filterVisibleEntries(registered));
|
|
268
|
+
const visible = [];
|
|
269
|
+
for (const [index, item] of items.entries()) {
|
|
270
|
+
const entry = entries[index];
|
|
271
|
+
if (entry ? !visibleEntries.has(entry) : !await this.passesBundleSkillRule(idOf(item))) continue;
|
|
272
|
+
visible.push(item);
|
|
273
|
+
}
|
|
274
|
+
return visible;
|
|
275
|
+
}
|
|
276
|
+
/** The registered skills the caller may see, in their order (see {@link filterVisible}). */
|
|
277
|
+
async filterVisibleEntries(entries) {
|
|
278
|
+
if (entries.length === 0) return [];
|
|
279
|
+
const servable = new Set(await filterServableSkills(this.deps.scope, entries));
|
|
280
|
+
const visible = [];
|
|
281
|
+
for (const entry of entries) {
|
|
282
|
+
if (!servable.has(entry) || !await this.passesSkillAuthorities(entry)) continue;
|
|
283
|
+
if (!await this.passesBundleSkillRule(entry.metadata.id ?? entry.name)) continue;
|
|
284
|
+
visible.push(entry);
|
|
285
|
+
}
|
|
286
|
+
return visible;
|
|
287
|
+
}
|
|
288
|
+
/** Whether the caller may see the skill named by `skillId` (see {@link filterVisible}). */
|
|
289
|
+
async isVisible(skillId) {
|
|
290
|
+
return (await this.filterVisible([skillId], (id) => id)).length === 1;
|
|
291
|
+
}
|
|
292
|
+
/**
|
|
293
|
+
* The actions of a bundle skill the caller may be shown: an action whose rules
|
|
294
|
+
* refuse the caller whatever the input (or that `unprotectedOps: 'deny'` blocks)
|
|
295
|
+
* is left out. Rules that depend on the input keep the action listed; the call is
|
|
296
|
+
* checked again when it runs.
|
|
297
|
+
*/
|
|
298
|
+
async visibleActions(skillId, actions, hiddenOps) {
|
|
299
|
+
const visible = [];
|
|
300
|
+
for (const action of actions) {
|
|
301
|
+
const entry = hiddenOps.get(skillId, action.actionId);
|
|
302
|
+
if (!entry) continue;
|
|
303
|
+
const allowed = await this.deps.guard.canDiscover({
|
|
304
|
+
policy: entry.op.requiredAuthorities,
|
|
305
|
+
skillPolicy: entry.skillRequiredAuthorities,
|
|
306
|
+
isPublic: entry.op.public,
|
|
307
|
+
unprotectedOps: this.deps.unprotectedOps,
|
|
308
|
+
authInfo: this.authInfo
|
|
309
|
+
});
|
|
310
|
+
if (allowed) visible.push(action);
|
|
311
|
+
}
|
|
312
|
+
return visible;
|
|
313
|
+
}
|
|
314
|
+
/** The server's `@Skill({ authorities })` rule, evaluated as the SDK's skill surfaces do. */
|
|
315
|
+
async passesSkillAuthorities(entry) {
|
|
316
|
+
const { authoritiesEngine: engine, authoritiesContextBuilder: contextBuilder } = this.deps.scope;
|
|
317
|
+
const authorities = entry.metadata["authorities"];
|
|
318
|
+
if (!engine || !contextBuilder || !authorities) return true;
|
|
319
|
+
try {
|
|
320
|
+
const result = await engine.evaluate(
|
|
321
|
+
authorities,
|
|
322
|
+
contextBuilder.build(this.authInfo)
|
|
323
|
+
);
|
|
324
|
+
return result.granted;
|
|
325
|
+
} catch {
|
|
326
|
+
return false;
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
/** The registered skill an id names, looked up the way the SDK's skill surfaces do. */
|
|
330
|
+
resolveEntry(id) {
|
|
331
|
+
const registry = this.deps.scope.skills;
|
|
332
|
+
if (!registry) return void 0;
|
|
333
|
+
return registry.findByName(id) ?? (id.includes(":") ? registry.findByQualifiedName(id) : void 0) ?? registry.getSkills(true).find((skill) => (skill.metadata.id ?? skill.name) === id || skill.metadata.name === id);
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
|
|
183
337
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.symbols.ts
|
|
184
338
|
var SkilledOpenApiConfig = class {
|
|
185
339
|
constructor(options) {
|
|
@@ -642,6 +796,7 @@ function collectPathsWithGet(entries) {
|
|
|
642
796
|
}
|
|
643
797
|
|
|
644
798
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
|
|
799
|
+
import { BundleStore } from "@frontmcp/adapters/skills";
|
|
645
800
|
import { InternalMcpError, PublicMcpError, ScopeEntry, Tool, ToolContext } from "@frontmcp/sdk";
|
|
646
801
|
|
|
647
802
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
|
|
@@ -690,11 +845,25 @@ var LoadSkillTool = class extends ToolContext {
|
|
|
690
845
|
if (!skillRegistry) {
|
|
691
846
|
throw new InternalMcpError("SkillRegistry is not available on the active scope", "SKILL_REGISTRY_UNAVAILABLE");
|
|
692
847
|
}
|
|
848
|
+
const notFound = () => (
|
|
849
|
+
// Caller-visible: skill id was not registered, or the caller may not see it
|
|
850
|
+
// (the same answer, so a refused skill's existence isn't revealed). Map to a
|
|
851
|
+
// 404 with a stable code so MCP clients can branch on it.
|
|
852
|
+
new PublicMcpError(`Skill "${input.skillId}" not found`, "SKILL_NOT_FOUND", 404)
|
|
853
|
+
);
|
|
693
854
|
const result = await skillRegistry.loadSkill(input.skillId);
|
|
694
|
-
if (!result)
|
|
695
|
-
|
|
696
|
-
|
|
855
|
+
if (!result) throw notFound();
|
|
856
|
+
const config = this.get(SkilledOpenApiConfig);
|
|
857
|
+
const visibility = new SkillVisibility({
|
|
858
|
+
scope,
|
|
859
|
+
guard: this.get(AuthorityGuard),
|
|
860
|
+
bundle: this.get(BundleStore).current(),
|
|
861
|
+
unprotectedOps: config.unprotectedOps,
|
|
862
|
+
authInfo: this.authInfo
|
|
863
|
+
});
|
|
697
864
|
const skill = result.skill;
|
|
865
|
+
if (!await visibility.isVisible(input.skillId) || !await visibility.isVisible(skill.id)) throw notFound();
|
|
866
|
+
const actions = skill.actions ? await visibility.visibleActions(skill.id, skill.actions, this.get(HiddenOpRegistry)) : void 0;
|
|
698
867
|
return {
|
|
699
868
|
skill: {
|
|
700
869
|
id: skill.id,
|
|
@@ -702,7 +871,7 @@ var LoadSkillTool = class extends ToolContext {
|
|
|
702
871
|
description: skill.description,
|
|
703
872
|
instructions: skill.instructions,
|
|
704
873
|
...skill.bundleVersion !== void 0 && { bundleVersion: skill.bundleVersion },
|
|
705
|
-
...
|
|
874
|
+
...actions ? { actions } : {}
|
|
706
875
|
},
|
|
707
876
|
isComplete: result.isComplete,
|
|
708
877
|
...result.warning !== void 0 && { warning: result.warning }
|
|
@@ -1539,6 +1708,7 @@ var searchSkillOutputSchema = {
|
|
|
1539
1708
|
};
|
|
1540
1709
|
|
|
1541
1710
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1711
|
+
import { BundleStore as BundleStore2 } from "@frontmcp/adapters/skills";
|
|
1542
1712
|
import { ScopeEntry as ScopeEntry2, Tool as Tool3, ToolContext as ToolContext3 } from "@frontmcp/sdk";
|
|
1543
1713
|
var SearchSkillTool = class extends ToolContext3 {
|
|
1544
1714
|
async execute(input) {
|
|
@@ -1550,14 +1720,23 @@ var SearchSkillTool = class extends ToolContext3 {
|
|
|
1550
1720
|
}
|
|
1551
1721
|
const limit = input.limit ?? 20;
|
|
1552
1722
|
const tags = input.tags;
|
|
1553
|
-
const
|
|
1554
|
-
|
|
1723
|
+
const found = await skillRegistry.search(input.query, {
|
|
1724
|
+
// Over-fetch: skills the caller may not see are removed below.
|
|
1725
|
+
topK: limit * 4,
|
|
1555
1726
|
...tags ? { tags } : {},
|
|
1556
1727
|
// Anti-query demotion (honored when the skill index supports it) — surfaces
|
|
1557
1728
|
// what the user wants while pushing down what they explicitly do not.
|
|
1558
1729
|
...input.notQuery !== void 0 ? { negativeQuery: input.notQuery } : {},
|
|
1559
1730
|
...input.notWeight !== void 0 ? { negativeWeight: input.notWeight } : {}
|
|
1560
1731
|
});
|
|
1732
|
+
const visibility = new SkillVisibility({
|
|
1733
|
+
scope,
|
|
1734
|
+
guard: this.get(AuthorityGuard),
|
|
1735
|
+
bundle: this.get(BundleStore2).current(),
|
|
1736
|
+
unprotectedOps: this.get(SkilledOpenApiConfig).unprotectedOps,
|
|
1737
|
+
authInfo: this.authInfo
|
|
1738
|
+
});
|
|
1739
|
+
const results = (await visibility.filterVisible(found, (r) => r.metadata.id ?? r.metadata.name)).slice(0, limit);
|
|
1561
1740
|
return {
|
|
1562
1741
|
skills: results.map((r) => ({
|
|
1563
1742
|
skillId: r.metadata.id ?? r.metadata.name,
|
|
@@ -1584,6 +1763,7 @@ SearchSkillTool = __decorateClass([
|
|
|
1584
1763
|
|
|
1585
1764
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
|
|
1586
1765
|
var TELEMETRY_FACTORY_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-factory");
|
|
1766
|
+
var FilterSkillsHook = FlowHooksOf("skills:filter");
|
|
1587
1767
|
var SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:skilled-openapi:runtime-deps");
|
|
1588
1768
|
function resolveRuntimeDeps(scope) {
|
|
1589
1769
|
const providers = scope?.providers;
|
|
@@ -1634,15 +1814,61 @@ var SkilledOpenApiPlugin = class extends DynamicPlugin {
|
|
|
1634
1814
|
} catch {
|
|
1635
1815
|
}
|
|
1636
1816
|
const scope = this.get(ScopeEntry3);
|
|
1817
|
+
const visible = await this.skillVisibility(scope, flowCtx.state.authInfo).filterVisibleEntries(
|
|
1818
|
+
scope.skills?.getSkills({ visibility: "mcp" }) ?? []
|
|
1819
|
+
);
|
|
1637
1820
|
const catalog = buildSkillsCatalogSummary(scope.skills, {
|
|
1638
|
-
mcpResources: scope.metadata?.skillsConfig?.mcpResources
|
|
1821
|
+
mcpResources: scope.metadata?.skillsConfig?.mcpResources,
|
|
1822
|
+
skills: visible
|
|
1639
1823
|
});
|
|
1640
1824
|
const description = catalog ? `${searchSkillDescription}
|
|
1641
1825
|
|
|
1642
1826
|
---
|
|
1643
1827
|
|
|
1644
1828
|
${catalog}` : searchSkillDescription;
|
|
1645
|
-
target.tool
|
|
1829
|
+
const perRequestTool = Object.create(target.tool, {
|
|
1830
|
+
metadata: { value: { ...target.tool.metadata, description }, enumerable: true }
|
|
1831
|
+
});
|
|
1832
|
+
flowCtx.state.set(
|
|
1833
|
+
"tools",
|
|
1834
|
+
tools.map((item) => item === target ? { ...item, tool: perRequestTool } : item)
|
|
1835
|
+
);
|
|
1836
|
+
}
|
|
1837
|
+
async hideBundleSkillsFromCaller(flowCtx) {
|
|
1838
|
+
const { skills } = flowCtx.state;
|
|
1839
|
+
if (!skills || skills.length === 0) return;
|
|
1840
|
+
const bundle = this.get(BundleStore3).current();
|
|
1841
|
+
const policies = bundleSkillPolicies(bundle);
|
|
1842
|
+
if (policies.size === 0) return;
|
|
1843
|
+
const visibility = new SkillVisibility({
|
|
1844
|
+
scope: this.get(ScopeEntry3),
|
|
1845
|
+
guard: this.get(AuthorityGuard),
|
|
1846
|
+
bundle,
|
|
1847
|
+
unprotectedOps: this.options.unprotectedOps,
|
|
1848
|
+
authInfo: this.currentAuthInfo()
|
|
1849
|
+
});
|
|
1850
|
+
const kept = [];
|
|
1851
|
+
for (const skill of skills) {
|
|
1852
|
+
if (await visibility.passesBundleSkillRule(skill.metadata.id ?? skill.name)) kept.push(skill);
|
|
1853
|
+
}
|
|
1854
|
+
if (kept.length !== skills.length) flowCtx.state.set("skills", kept);
|
|
1855
|
+
}
|
|
1856
|
+
skillVisibility(scope, authInfo) {
|
|
1857
|
+
return new SkillVisibility({
|
|
1858
|
+
scope,
|
|
1859
|
+
guard: this.get(AuthorityGuard),
|
|
1860
|
+
bundle: this.get(BundleStore3).current(),
|
|
1861
|
+
unprotectedOps: this.options.unprotectedOps,
|
|
1862
|
+
authInfo
|
|
1863
|
+
});
|
|
1864
|
+
}
|
|
1865
|
+
/** The AuthInfo of the request being served; an anonymous caller when there is none. */
|
|
1866
|
+
currentAuthInfo() {
|
|
1867
|
+
try {
|
|
1868
|
+
return this.get(FrontMcpContextStorage)?.getStore()?.authInfo ?? {};
|
|
1869
|
+
} catch {
|
|
1870
|
+
return {};
|
|
1871
|
+
}
|
|
1646
1872
|
}
|
|
1647
1873
|
warnIfInsecureConfig() {
|
|
1648
1874
|
if (this.options.dev) {
|
|
@@ -1664,7 +1890,7 @@ ${catalog}` : searchSkillDescription;
|
|
|
1664
1890
|
{ name: "skilled-openapi:hidden-ops", provide: HiddenOpRegistry, useValue: new HiddenOpRegistry() },
|
|
1665
1891
|
{
|
|
1666
1892
|
name: "skilled-openapi:bundle-store",
|
|
1667
|
-
provide:
|
|
1893
|
+
provide: BundleStore3,
|
|
1668
1894
|
// Resolve TelemetryAccessor from the scope's provider registry so the
|
|
1669
1895
|
// bundle-pulls counter and `skill.bundle.swap` span actually export.
|
|
1670
1896
|
// The accessor is structurally compatible with `BundleStoreTelemetry`.
|
|
@@ -1672,7 +1898,7 @@ ${catalog}` : searchSkillDescription;
|
|
|
1672
1898
|
// we resolve `undefined` and the BundleStore falls through its zero-cost
|
|
1673
1899
|
// no-telemetry path.
|
|
1674
1900
|
inject: () => [ScopeEntry3],
|
|
1675
|
-
useFactory: (scope) => new
|
|
1901
|
+
useFactory: (scope) => new BundleStore3({ telemetry: resolveBundleTelemetry(scope) })
|
|
1676
1902
|
},
|
|
1677
1903
|
{
|
|
1678
1904
|
name: "skilled-openapi:credential-resolver",
|
|
@@ -1683,12 +1909,22 @@ ${catalog}` : searchSkillDescription;
|
|
|
1683
1909
|
name: "skilled-openapi:authority-guard",
|
|
1684
1910
|
provide: AuthorityGuard,
|
|
1685
1911
|
inject: () => [ScopeEntry3],
|
|
1686
|
-
useFactory: (scope) => new AuthorityGuard({
|
|
1912
|
+
useFactory: (scope) => new AuthorityGuard({
|
|
1913
|
+
logger: scope.logger.child("skilled-openapi:authority"),
|
|
1914
|
+
// Bundle rules follow the server's authorities settings (claimsMapping,
|
|
1915
|
+
// claimsResolver, relationshipResolver, evaluators) when it has them. Read per check:
|
|
1916
|
+
// the server may register its engine after this provider is built.
|
|
1917
|
+
serverAuthorities: () => {
|
|
1918
|
+
const engine = scope.authoritiesEngine;
|
|
1919
|
+
const contextBuilder = scope.authoritiesContextBuilder;
|
|
1920
|
+
return engine && contextBuilder ? { engine, contextBuilder } : void 0;
|
|
1921
|
+
}
|
|
1922
|
+
})
|
|
1687
1923
|
},
|
|
1688
1924
|
{
|
|
1689
1925
|
name: "skilled-openapi:bundle-sync",
|
|
1690
1926
|
provide: BundleSyncService,
|
|
1691
|
-
inject: () => [ScopeEntry3, HiddenOpRegistry,
|
|
1927
|
+
inject: () => [ScopeEntry3, HiddenOpRegistry, BundleStore3],
|
|
1692
1928
|
useFactory: async (scope, hiddenOps, bundleStore) => {
|
|
1693
1929
|
const logger = scope.logger.child("skilled-openapi:sync");
|
|
1694
1930
|
const lazySkillRegistry = new Proxy({}, {
|
|
@@ -1773,12 +2009,18 @@ ${catalog}` : searchSkillDescription;
|
|
|
1773
2009
|
__decorateClass([
|
|
1774
2010
|
ListToolsHook.Did("findTools", { priority: 50 })
|
|
1775
2011
|
], SkilledOpenApiPlugin.prototype, "injectSkillCatalogIntoSearchTool", 1);
|
|
2012
|
+
__decorateClass([
|
|
2013
|
+
FilterSkillsHook.Did("filterSkills", { priority: 50 })
|
|
2014
|
+
], SkilledOpenApiPlugin.prototype, "hideBundleSkillsFromCaller", 1);
|
|
1776
2015
|
SkilledOpenApiPlugin = __decorateClass([
|
|
1777
2016
|
Plugin({
|
|
1778
2017
|
name: "skilled-openapi",
|
|
1779
2018
|
description: "Serve a customer's OpenAPI spec as signed skill bundles with hidden per-operation tools mediated by 3 meta-tools.",
|
|
1780
2019
|
providers: [],
|
|
1781
|
-
tools: [SearchSkillTool, LoadSkillTool, RunWorkflowTool]
|
|
2020
|
+
tools: [SearchSkillTool, LoadSkillTool, RunWorkflowTool],
|
|
2021
|
+
// The bundle's skills are registered after startup: serve the skills capability and methods from
|
|
2022
|
+
// startup, answering with the skills loaded so far.
|
|
2023
|
+
dynamicSkills: true
|
|
1782
2024
|
})
|
|
1783
2025
|
], SkilledOpenApiPlugin);
|
|
1784
2026
|
|
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.4",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.8.
|
|
52
|
-
"@frontmcp/auth": "1.8.
|
|
53
|
-
"@frontmcp/sdk": "1.8.
|
|
54
|
-
"@frontmcp/utils": "1.8.
|
|
51
|
+
"@frontmcp/adapters": "1.8.4",
|
|
52
|
+
"@frontmcp/auth": "1.8.4",
|
|
53
|
+
"@frontmcp/sdk": "1.8.4",
|
|
54
|
+
"@frontmcp/utils": "1.8.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@enclave-vm/ast": "*",
|
|
58
58
|
"@enclave-vm/core": "*",
|
|
59
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
59
|
+
"@frontmcp/lazy-zod": "1.8.4"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@enclave-vm/ast": {
|
package/index.js
CHANGED
|
@@ -48,8 +48,8 @@ __export(index_exports, {
|
|
|
48
48
|
module.exports = __toCommonJS(index_exports);
|
|
49
49
|
|
|
50
50
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
|
|
51
|
-
var
|
|
52
|
-
var
|
|
51
|
+
var import_skills8 = require("@frontmcp/adapters/skills");
|
|
52
|
+
var import_sdk6 = require("@frontmcp/sdk");
|
|
53
53
|
|
|
54
54
|
// plugins/plugin-skilled-openapi/src/executor/credential-resolver.ts
|
|
55
55
|
var MemoryCredentialResolver = class {
|
|
@@ -145,16 +145,61 @@ var HiddenOpRegistry = class {
|
|
|
145
145
|
|
|
146
146
|
// plugins/plugin-skilled-openapi/src/security/authority-guard.ts
|
|
147
147
|
var import_auth = require("@frontmcp/auth");
|
|
148
|
+
function policyDependsOnInput(value) {
|
|
149
|
+
if (typeof value === "string") return value === "input" || value.startsWith("input.");
|
|
150
|
+
if (Array.isArray(value)) return value.some(policyDependsOnInput);
|
|
151
|
+
if (value !== null && typeof value === "object") {
|
|
152
|
+
for (const [key2, nested] of Object.entries(value)) {
|
|
153
|
+
if (key2 === "fromInput" || key2 === "custom" || key2 === "guards") return true;
|
|
154
|
+
if (policyDependsOnInput(key2) || policyDependsOnInput(nested)) return true;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
function judgeablePart(policy, engine) {
|
|
160
|
+
if (policy === void 0 || policy === null) return void 0;
|
|
161
|
+
const profileDependsOnInput = (name) => typeof name === "string" && policyDependsOnInput(engine.resolveProfile(name));
|
|
162
|
+
const value = policy;
|
|
163
|
+
if (typeof value === "string") return profileDependsOnInput(value) ? void 0 : policy;
|
|
164
|
+
if (Array.isArray(value)) {
|
|
165
|
+
const names = value.filter((name) => !profileDependsOnInput(name));
|
|
166
|
+
return names.length > 0 ? names : void 0;
|
|
167
|
+
}
|
|
168
|
+
return policyDependsOnInput(policy) ? void 0 : policy;
|
|
169
|
+
}
|
|
148
170
|
var AuthorityGuard = class {
|
|
149
171
|
engine;
|
|
150
172
|
contextBuilder;
|
|
151
173
|
logger;
|
|
174
|
+
serverAuthorities;
|
|
152
175
|
constructor(opts = {}) {
|
|
153
176
|
const profiles = opts.profiles ?? new import_auth.AuthoritiesProfileRegistry();
|
|
154
177
|
const evaluators = opts.evaluators ?? new import_auth.AuthoritiesEvaluatorRegistry();
|
|
155
178
|
this.engine = new import_auth.AuthoritiesEngine(profiles, evaluators);
|
|
156
179
|
this.contextBuilder = new import_auth.AuthoritiesContextBuilder();
|
|
157
180
|
this.logger = opts.logger;
|
|
181
|
+
this.serverAuthorities = opts.serverAuthorities;
|
|
182
|
+
}
|
|
183
|
+
/** The server's engine and context builder when it configures authorities, else the guard's own. */
|
|
184
|
+
authorities() {
|
|
185
|
+
return this.serverAuthorities?.() ?? { engine: this.engine, contextBuilder: this.contextBuilder };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Whether the caller may be shown a skill or action (in `search_skill`,
|
|
189
|
+
* `load_skill`, the catalog and the SDK skill surfaces), judged without the
|
|
190
|
+
* action's input. A rule that depends on the input can't be judged yet and
|
|
191
|
+
* doesn't hide anything; the call is still checked by {@link check}. Every other
|
|
192
|
+
* rule must grant, and a policy-less action under `unprotectedOps: 'deny'` must
|
|
193
|
+
* be public, exactly as at call time.
|
|
194
|
+
*/
|
|
195
|
+
async canDiscover(args) {
|
|
196
|
+
const { engine } = this.authorities();
|
|
197
|
+
const hasPolicy = [args.skillPolicy, args.policy].some((p) => p !== void 0 && p !== null);
|
|
198
|
+
const skillPolicy = judgeablePart(args.skillPolicy, engine);
|
|
199
|
+
const policy = judgeablePart(args.policy, engine);
|
|
200
|
+
if (hasPolicy && skillPolicy === void 0 && policy === void 0) return true;
|
|
201
|
+
const result = await this.check({ ...args, skillPolicy, policy, input: {} });
|
|
202
|
+
return result.granted;
|
|
158
203
|
}
|
|
159
204
|
async check(args) {
|
|
160
205
|
const { policy, skillPolicy, isPublic, authInfo, input, env } = args;
|
|
@@ -172,10 +217,11 @@ var AuthorityGuard = class {
|
|
|
172
217
|
return { granted: true, evaluatedPolicies: [] };
|
|
173
218
|
}
|
|
174
219
|
try {
|
|
175
|
-
const
|
|
220
|
+
const { engine, contextBuilder } = this.authorities();
|
|
221
|
+
const ctx = contextBuilder.build(authInfo, input, env);
|
|
176
222
|
const evaluatedPolicies = [];
|
|
177
223
|
for (const p of policies) {
|
|
178
|
-
const res = await
|
|
224
|
+
const res = await engine.evaluate(p, ctx);
|
|
179
225
|
evaluatedPolicies.push(...res.evaluatedPolicies ?? []);
|
|
180
226
|
if (!res.granted) {
|
|
181
227
|
return { ...res, evaluatedPolicies };
|
|
@@ -203,6 +249,112 @@ function normalizeCaughtMessage(e) {
|
|
|
203
249
|
return String(e ?? "authority evaluation threw");
|
|
204
250
|
}
|
|
205
251
|
|
|
252
|
+
// plugins/plugin-skilled-openapi/src/security/skill-visibility.ts
|
|
253
|
+
var import_sdk = require("@frontmcp/sdk");
|
|
254
|
+
function bundleSkillPolicies(bundle) {
|
|
255
|
+
const policies = /* @__PURE__ */ new Map();
|
|
256
|
+
for (const skill of bundle?.skills ?? []) {
|
|
257
|
+
if (skill.requiredAuthorities !== void 0 && skill.requiredAuthorities !== null) {
|
|
258
|
+
policies.set(skill.id, skill.requiredAuthorities);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return policies;
|
|
262
|
+
}
|
|
263
|
+
var SkillVisibility = class {
|
|
264
|
+
constructor(deps) {
|
|
265
|
+
this.deps = deps;
|
|
266
|
+
this.policies = bundleSkillPolicies(deps.bundle);
|
|
267
|
+
this.authInfo = deps.authInfo ?? {};
|
|
268
|
+
}
|
|
269
|
+
deps;
|
|
270
|
+
policies;
|
|
271
|
+
authInfo;
|
|
272
|
+
/** Whether the caller satisfies the bundle skill's own `requiredAuthorities` (true for other skills). */
|
|
273
|
+
async passesBundleSkillRule(skillId) {
|
|
274
|
+
const skillPolicy = this.policies.get(skillId);
|
|
275
|
+
if (skillPolicy === void 0) return true;
|
|
276
|
+
return this.deps.guard.canDiscover({ skillPolicy, authInfo: this.authInfo });
|
|
277
|
+
}
|
|
278
|
+
/**
|
|
279
|
+
* Keep the items whose skill the caller may see: the bundle skill's rule, the
|
|
280
|
+
* `skills:filter` flow and the server's `@Skill({ authorities })` rule. An item
|
|
281
|
+
* whose id names no registered skill (an external skill) is judged by the bundle
|
|
282
|
+
* rule alone, as the SDK's own surfaces keep such results.
|
|
283
|
+
*/
|
|
284
|
+
async filterVisible(items, idOf) {
|
|
285
|
+
if (items.length === 0) return [];
|
|
286
|
+
const entries = items.map((item) => this.resolveEntry(idOf(item)));
|
|
287
|
+
const registered = [...new Set(entries.filter((entry) => entry !== void 0))];
|
|
288
|
+
const visibleEntries = new Set(await this.filterVisibleEntries(registered));
|
|
289
|
+
const visible = [];
|
|
290
|
+
for (const [index, item] of items.entries()) {
|
|
291
|
+
const entry = entries[index];
|
|
292
|
+
if (entry ? !visibleEntries.has(entry) : !await this.passesBundleSkillRule(idOf(item))) continue;
|
|
293
|
+
visible.push(item);
|
|
294
|
+
}
|
|
295
|
+
return visible;
|
|
296
|
+
}
|
|
297
|
+
/** The registered skills the caller may see, in their order (see {@link filterVisible}). */
|
|
298
|
+
async filterVisibleEntries(entries) {
|
|
299
|
+
if (entries.length === 0) return [];
|
|
300
|
+
const servable = new Set(await (0, import_sdk.filterServableSkills)(this.deps.scope, entries));
|
|
301
|
+
const visible = [];
|
|
302
|
+
for (const entry of entries) {
|
|
303
|
+
if (!servable.has(entry) || !await this.passesSkillAuthorities(entry)) continue;
|
|
304
|
+
if (!await this.passesBundleSkillRule(entry.metadata.id ?? entry.name)) continue;
|
|
305
|
+
visible.push(entry);
|
|
306
|
+
}
|
|
307
|
+
return visible;
|
|
308
|
+
}
|
|
309
|
+
/** Whether the caller may see the skill named by `skillId` (see {@link filterVisible}). */
|
|
310
|
+
async isVisible(skillId) {
|
|
311
|
+
return (await this.filterVisible([skillId], (id) => id)).length === 1;
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The actions of a bundle skill the caller may be shown: an action whose rules
|
|
315
|
+
* refuse the caller whatever the input (or that `unprotectedOps: 'deny'` blocks)
|
|
316
|
+
* is left out. Rules that depend on the input keep the action listed; the call is
|
|
317
|
+
* checked again when it runs.
|
|
318
|
+
*/
|
|
319
|
+
async visibleActions(skillId, actions, hiddenOps) {
|
|
320
|
+
const visible = [];
|
|
321
|
+
for (const action of actions) {
|
|
322
|
+
const entry = hiddenOps.get(skillId, action.actionId);
|
|
323
|
+
if (!entry) continue;
|
|
324
|
+
const allowed = await this.deps.guard.canDiscover({
|
|
325
|
+
policy: entry.op.requiredAuthorities,
|
|
326
|
+
skillPolicy: entry.skillRequiredAuthorities,
|
|
327
|
+
isPublic: entry.op.public,
|
|
328
|
+
unprotectedOps: this.deps.unprotectedOps,
|
|
329
|
+
authInfo: this.authInfo
|
|
330
|
+
});
|
|
331
|
+
if (allowed) visible.push(action);
|
|
332
|
+
}
|
|
333
|
+
return visible;
|
|
334
|
+
}
|
|
335
|
+
/** The server's `@Skill({ authorities })` rule, evaluated as the SDK's skill surfaces do. */
|
|
336
|
+
async passesSkillAuthorities(entry) {
|
|
337
|
+
const { authoritiesEngine: engine, authoritiesContextBuilder: contextBuilder } = this.deps.scope;
|
|
338
|
+
const authorities = entry.metadata["authorities"];
|
|
339
|
+
if (!engine || !contextBuilder || !authorities) return true;
|
|
340
|
+
try {
|
|
341
|
+
const result = await engine.evaluate(
|
|
342
|
+
authorities,
|
|
343
|
+
contextBuilder.build(this.authInfo)
|
|
344
|
+
);
|
|
345
|
+
return result.granted;
|
|
346
|
+
} catch {
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
/** The registered skill an id names, looked up the way the SDK's skill surfaces do. */
|
|
351
|
+
resolveEntry(id) {
|
|
352
|
+
const registry = this.deps.scope.skills;
|
|
353
|
+
if (!registry) return void 0;
|
|
354
|
+
return registry.findByName(id) ?? (id.includes(":") ? registry.findByQualifiedName(id) : void 0) ?? registry.getSkills(true).find((skill) => (skill.metadata.id ?? skill.name) === id || skill.metadata.name === id);
|
|
355
|
+
}
|
|
356
|
+
};
|
|
357
|
+
|
|
206
358
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.symbols.ts
|
|
207
359
|
var SkilledOpenApiConfig = class {
|
|
208
360
|
constructor(options) {
|
|
@@ -651,7 +803,8 @@ function collectPathsWithGet(entries) {
|
|
|
651
803
|
}
|
|
652
804
|
|
|
653
805
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
|
|
654
|
-
var
|
|
806
|
+
var import_skills4 = require("@frontmcp/adapters/skills");
|
|
807
|
+
var import_sdk2 = require("@frontmcp/sdk");
|
|
655
808
|
|
|
656
809
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.schema.ts
|
|
657
810
|
var import_lazy_zod2 = require("@frontmcp/lazy-zod");
|
|
@@ -691,19 +844,33 @@ var loadSkillOutputSchema = {
|
|
|
691
844
|
};
|
|
692
845
|
|
|
693
846
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
|
|
694
|
-
var LoadSkillTool = class extends
|
|
847
|
+
var LoadSkillTool = class extends import_sdk2.ToolContext {
|
|
695
848
|
async execute(input) {
|
|
696
849
|
await this.get(BundleSyncService).ensureReady();
|
|
697
|
-
const scope = this.get(
|
|
850
|
+
const scope = this.get(import_sdk2.ScopeEntry);
|
|
698
851
|
const skillRegistry = scope.skills;
|
|
699
852
|
if (!skillRegistry) {
|
|
700
|
-
throw new
|
|
853
|
+
throw new import_sdk2.InternalMcpError("SkillRegistry is not available on the active scope", "SKILL_REGISTRY_UNAVAILABLE");
|
|
701
854
|
}
|
|
855
|
+
const notFound = () => (
|
|
856
|
+
// Caller-visible: skill id was not registered, or the caller may not see it
|
|
857
|
+
// (the same answer, so a refused skill's existence isn't revealed). Map to a
|
|
858
|
+
// 404 with a stable code so MCP clients can branch on it.
|
|
859
|
+
new import_sdk2.PublicMcpError(`Skill "${input.skillId}" not found`, "SKILL_NOT_FOUND", 404)
|
|
860
|
+
);
|
|
702
861
|
const result = await skillRegistry.loadSkill(input.skillId);
|
|
703
|
-
if (!result)
|
|
704
|
-
|
|
705
|
-
|
|
862
|
+
if (!result) throw notFound();
|
|
863
|
+
const config = this.get(SkilledOpenApiConfig);
|
|
864
|
+
const visibility = new SkillVisibility({
|
|
865
|
+
scope,
|
|
866
|
+
guard: this.get(AuthorityGuard),
|
|
867
|
+
bundle: this.get(import_skills4.BundleStore).current(),
|
|
868
|
+
unprotectedOps: config.unprotectedOps,
|
|
869
|
+
authInfo: this.authInfo
|
|
870
|
+
});
|
|
706
871
|
const skill = result.skill;
|
|
872
|
+
if (!await visibility.isVisible(input.skillId) || !await visibility.isVisible(skill.id)) throw notFound();
|
|
873
|
+
const actions = skill.actions ? await visibility.visibleActions(skill.id, skill.actions, this.get(HiddenOpRegistry)) : void 0;
|
|
707
874
|
return {
|
|
708
875
|
skill: {
|
|
709
876
|
id: skill.id,
|
|
@@ -711,7 +878,7 @@ var LoadSkillTool = class extends import_sdk.ToolContext {
|
|
|
711
878
|
description: skill.description,
|
|
712
879
|
instructions: skill.instructions,
|
|
713
880
|
...skill.bundleVersion !== void 0 && { bundleVersion: skill.bundleVersion },
|
|
714
|
-
...
|
|
881
|
+
...actions ? { actions } : {}
|
|
715
882
|
},
|
|
716
883
|
isComplete: result.isComplete,
|
|
717
884
|
...result.warning !== void 0 && { warning: result.warning }
|
|
@@ -719,7 +886,7 @@ var LoadSkillTool = class extends import_sdk.ToolContext {
|
|
|
719
886
|
}
|
|
720
887
|
};
|
|
721
888
|
LoadSkillTool = __decorateClass([
|
|
722
|
-
(0,
|
|
889
|
+
(0, import_sdk2.Tool)({
|
|
723
890
|
name: "load_skill",
|
|
724
891
|
description: loadSkillDescription,
|
|
725
892
|
inputSchema: loadSkillInputSchema,
|
|
@@ -732,8 +899,8 @@ LoadSkillTool = __decorateClass([
|
|
|
732
899
|
], LoadSkillTool);
|
|
733
900
|
|
|
734
901
|
// plugins/plugin-skilled-openapi/src/tools/operation-tool.factory.ts
|
|
735
|
-
var
|
|
736
|
-
var
|
|
902
|
+
var import_skills5 = require("@frontmcp/adapters/skills");
|
|
903
|
+
var import_sdk3 = require("@frontmcp/sdk");
|
|
737
904
|
|
|
738
905
|
// plugins/plugin-skilled-openapi/src/executor/openapi-runtime.ts
|
|
739
906
|
var import_openapi = require("@frontmcp/adapters/openapi");
|
|
@@ -1147,16 +1314,16 @@ var OperationToolFactory = class {
|
|
|
1147
1314
|
const executor = this.makeExecutor(entry);
|
|
1148
1315
|
const metadata = buildOperationMetadata(entry);
|
|
1149
1316
|
const record = {
|
|
1150
|
-
kind:
|
|
1317
|
+
kind: import_sdk3.ToolKind.FUNCTION,
|
|
1151
1318
|
provide: executor,
|
|
1152
1319
|
metadata
|
|
1153
1320
|
};
|
|
1154
|
-
const instance = new
|
|
1321
|
+
const instance = new import_sdk3.ToolInstance(record, this.deps.providers, OPERATION_TOOL_OWNER);
|
|
1155
1322
|
this.deps.toolRegistry.registerToolInstance(instance);
|
|
1156
1323
|
this.registered.set(key2, executor);
|
|
1157
1324
|
if (this.deps.classificationRegistry && ctx?.pathsWithGet) {
|
|
1158
1325
|
try {
|
|
1159
|
-
const classification = (0,
|
|
1326
|
+
const classification = (0, import_skills5.classifyOne)(
|
|
1160
1327
|
entry.bundleId,
|
|
1161
1328
|
{
|
|
1162
1329
|
operationId: entry.op.operationId,
|
|
@@ -1270,8 +1437,8 @@ var OperationToolFactory = class {
|
|
|
1270
1437
|
};
|
|
1271
1438
|
|
|
1272
1439
|
// plugins/plugin-skilled-openapi/src/tools/run-workflow.tool.ts
|
|
1273
|
-
var
|
|
1274
|
-
var
|
|
1440
|
+
var import_skills6 = require("@frontmcp/adapters/skills");
|
|
1441
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
1275
1442
|
|
|
1276
1443
|
// plugins/plugin-skilled-openapi/src/executor/execute-skill-action.ts
|
|
1277
1444
|
function formatZodIssues(issues) {
|
|
@@ -1419,7 +1586,7 @@ var runWorkflowOutputSchema = {
|
|
|
1419
1586
|
var MAX_STEPS = 2e6;
|
|
1420
1587
|
var MAX_TOOL_CALLS = 25;
|
|
1421
1588
|
var TIMEOUT_MS = 8e3;
|
|
1422
|
-
var RunWorkflowTool = class extends
|
|
1589
|
+
var RunWorkflowTool = class extends import_sdk4.ToolContext {
|
|
1423
1590
|
async execute(input) {
|
|
1424
1591
|
await this.get(BundleSyncService).ensureReady();
|
|
1425
1592
|
const config = this.get(SkilledOpenApiConfig);
|
|
@@ -1437,7 +1604,7 @@ var RunWorkflowTool = class extends import_sdk3.ToolContext {
|
|
|
1437
1604
|
error: "workflow execution unavailable: the @enclave-vm sandbox is not installed. Install @enclave-vm/core and @enclave-vm/ast to enable run_workflow."
|
|
1438
1605
|
};
|
|
1439
1606
|
}
|
|
1440
|
-
const auditWriter = this.tryGet(
|
|
1607
|
+
const auditWriter = this.tryGet(import_skills6.SkillAuditWriterToken);
|
|
1441
1608
|
const deps = {
|
|
1442
1609
|
config: { outbound: config.outbound, unprotectedOps: config.unprotectedOps },
|
|
1443
1610
|
resolver: { resolve: (ref, opts) => resolver.resolve(ref, opts) },
|
|
@@ -1488,7 +1655,7 @@ var RunWorkflowTool = class extends import_sdk3.ToolContext {
|
|
|
1488
1655
|
}
|
|
1489
1656
|
};
|
|
1490
1657
|
RunWorkflowTool = __decorateClass([
|
|
1491
|
-
(0,
|
|
1658
|
+
(0, import_sdk4.Tool)({
|
|
1492
1659
|
name: "run_workflow",
|
|
1493
1660
|
description: runWorkflowDescription,
|
|
1494
1661
|
inputSchema: runWorkflowInputSchema,
|
|
@@ -1542,25 +1709,35 @@ var searchSkillOutputSchema = {
|
|
|
1542
1709
|
};
|
|
1543
1710
|
|
|
1544
1711
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1545
|
-
var
|
|
1546
|
-
var
|
|
1712
|
+
var import_skills7 = require("@frontmcp/adapters/skills");
|
|
1713
|
+
var import_sdk5 = require("@frontmcp/sdk");
|
|
1714
|
+
var SearchSkillTool = class extends import_sdk5.ToolContext {
|
|
1547
1715
|
async execute(input) {
|
|
1548
1716
|
await this.get(BundleSyncService).ensureReady();
|
|
1549
|
-
const scope = this.get(
|
|
1717
|
+
const scope = this.get(import_sdk5.ScopeEntry);
|
|
1550
1718
|
const skillRegistry = scope.skills;
|
|
1551
1719
|
if (!skillRegistry || !skillRegistry.hasAny()) {
|
|
1552
1720
|
return { skills: [] };
|
|
1553
1721
|
}
|
|
1554
1722
|
const limit = input.limit ?? 20;
|
|
1555
1723
|
const tags = input.tags;
|
|
1556
|
-
const
|
|
1557
|
-
|
|
1724
|
+
const found = await skillRegistry.search(input.query, {
|
|
1725
|
+
// Over-fetch: skills the caller may not see are removed below.
|
|
1726
|
+
topK: limit * 4,
|
|
1558
1727
|
...tags ? { tags } : {},
|
|
1559
1728
|
// Anti-query demotion (honored when the skill index supports it) — surfaces
|
|
1560
1729
|
// what the user wants while pushing down what they explicitly do not.
|
|
1561
1730
|
...input.notQuery !== void 0 ? { negativeQuery: input.notQuery } : {},
|
|
1562
1731
|
...input.notWeight !== void 0 ? { negativeWeight: input.notWeight } : {}
|
|
1563
1732
|
});
|
|
1733
|
+
const visibility = new SkillVisibility({
|
|
1734
|
+
scope,
|
|
1735
|
+
guard: this.get(AuthorityGuard),
|
|
1736
|
+
bundle: this.get(import_skills7.BundleStore).current(),
|
|
1737
|
+
unprotectedOps: this.get(SkilledOpenApiConfig).unprotectedOps,
|
|
1738
|
+
authInfo: this.authInfo
|
|
1739
|
+
});
|
|
1740
|
+
const results = (await visibility.filterVisible(found, (r) => r.metadata.id ?? r.metadata.name)).slice(0, limit);
|
|
1564
1741
|
return {
|
|
1565
1742
|
skills: results.map((r) => ({
|
|
1566
1743
|
skillId: r.metadata.id ?? r.metadata.name,
|
|
@@ -1573,7 +1750,7 @@ var SearchSkillTool = class extends import_sdk4.ToolContext {
|
|
|
1573
1750
|
}
|
|
1574
1751
|
};
|
|
1575
1752
|
SearchSkillTool = __decorateClass([
|
|
1576
|
-
(0,
|
|
1753
|
+
(0, import_sdk5.Tool)({
|
|
1577
1754
|
name: "search_skill",
|
|
1578
1755
|
description: searchSkillDescription,
|
|
1579
1756
|
inputSchema: searchSkillInputSchema,
|
|
@@ -1587,6 +1764,7 @@ SearchSkillTool = __decorateClass([
|
|
|
1587
1764
|
|
|
1588
1765
|
// plugins/plugin-skilled-openapi/src/skilled-openapi.plugin.ts
|
|
1589
1766
|
var TELEMETRY_FACTORY_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:observability:telemetry-factory");
|
|
1767
|
+
var FilterSkillsHook = (0, import_sdk6.FlowHooksOf)("skills:filter");
|
|
1590
1768
|
var SKILLED_OPENAPI_RUNTIME_DEPS_TOKEN = /* @__PURE__ */ Symbol.for("frontmcp:skilled-openapi:runtime-deps");
|
|
1591
1769
|
function resolveRuntimeDeps(scope) {
|
|
1592
1770
|
const providers = scope?.providers;
|
|
@@ -1613,7 +1791,7 @@ function resolveBundleTelemetry(scope) {
|
|
|
1613
1791
|
startSpan: (name, attributes) => factory.startSpan(name, attributes)
|
|
1614
1792
|
};
|
|
1615
1793
|
}
|
|
1616
|
-
var SkilledOpenApiPlugin = class extends
|
|
1794
|
+
var SkilledOpenApiPlugin = class extends import_sdk6.DynamicPlugin {
|
|
1617
1795
|
options;
|
|
1618
1796
|
cachedLogger;
|
|
1619
1797
|
constructor(options) {
|
|
@@ -1623,7 +1801,7 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
|
|
|
1623
1801
|
}
|
|
1624
1802
|
getLogger() {
|
|
1625
1803
|
if (!this.cachedLogger) {
|
|
1626
|
-
this.cachedLogger = this.get(
|
|
1804
|
+
this.cachedLogger = this.get(import_sdk6.FrontMcpLogger).child("skilled-openapi");
|
|
1627
1805
|
}
|
|
1628
1806
|
return this.cachedLogger;
|
|
1629
1807
|
}
|
|
@@ -1636,16 +1814,62 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
|
|
|
1636
1814
|
await this.get(BundleSyncService).ensureReady();
|
|
1637
1815
|
} catch {
|
|
1638
1816
|
}
|
|
1639
|
-
const scope = this.get(
|
|
1640
|
-
const
|
|
1641
|
-
|
|
1817
|
+
const scope = this.get(import_sdk6.ScopeEntry);
|
|
1818
|
+
const visible = await this.skillVisibility(scope, flowCtx.state.authInfo).filterVisibleEntries(
|
|
1819
|
+
scope.skills?.getSkills({ visibility: "mcp" }) ?? []
|
|
1820
|
+
);
|
|
1821
|
+
const catalog = (0, import_sdk6.buildSkillsCatalogSummary)(scope.skills, {
|
|
1822
|
+
mcpResources: scope.metadata?.skillsConfig?.mcpResources,
|
|
1823
|
+
skills: visible
|
|
1642
1824
|
});
|
|
1643
1825
|
const description = catalog ? `${searchSkillDescription}
|
|
1644
1826
|
|
|
1645
1827
|
---
|
|
1646
1828
|
|
|
1647
1829
|
${catalog}` : searchSkillDescription;
|
|
1648
|
-
target.tool
|
|
1830
|
+
const perRequestTool = Object.create(target.tool, {
|
|
1831
|
+
metadata: { value: { ...target.tool.metadata, description }, enumerable: true }
|
|
1832
|
+
});
|
|
1833
|
+
flowCtx.state.set(
|
|
1834
|
+
"tools",
|
|
1835
|
+
tools.map((item) => item === target ? { ...item, tool: perRequestTool } : item)
|
|
1836
|
+
);
|
|
1837
|
+
}
|
|
1838
|
+
async hideBundleSkillsFromCaller(flowCtx) {
|
|
1839
|
+
const { skills } = flowCtx.state;
|
|
1840
|
+
if (!skills || skills.length === 0) return;
|
|
1841
|
+
const bundle = this.get(import_skills8.BundleStore).current();
|
|
1842
|
+
const policies = bundleSkillPolicies(bundle);
|
|
1843
|
+
if (policies.size === 0) return;
|
|
1844
|
+
const visibility = new SkillVisibility({
|
|
1845
|
+
scope: this.get(import_sdk6.ScopeEntry),
|
|
1846
|
+
guard: this.get(AuthorityGuard),
|
|
1847
|
+
bundle,
|
|
1848
|
+
unprotectedOps: this.options.unprotectedOps,
|
|
1849
|
+
authInfo: this.currentAuthInfo()
|
|
1850
|
+
});
|
|
1851
|
+
const kept = [];
|
|
1852
|
+
for (const skill of skills) {
|
|
1853
|
+
if (await visibility.passesBundleSkillRule(skill.metadata.id ?? skill.name)) kept.push(skill);
|
|
1854
|
+
}
|
|
1855
|
+
if (kept.length !== skills.length) flowCtx.state.set("skills", kept);
|
|
1856
|
+
}
|
|
1857
|
+
skillVisibility(scope, authInfo) {
|
|
1858
|
+
return new SkillVisibility({
|
|
1859
|
+
scope,
|
|
1860
|
+
guard: this.get(AuthorityGuard),
|
|
1861
|
+
bundle: this.get(import_skills8.BundleStore).current(),
|
|
1862
|
+
unprotectedOps: this.options.unprotectedOps,
|
|
1863
|
+
authInfo
|
|
1864
|
+
});
|
|
1865
|
+
}
|
|
1866
|
+
/** The AuthInfo of the request being served; an anonymous caller when there is none. */
|
|
1867
|
+
currentAuthInfo() {
|
|
1868
|
+
try {
|
|
1869
|
+
return this.get(import_sdk6.FrontMcpContextStorage)?.getStore()?.authInfo ?? {};
|
|
1870
|
+
} catch {
|
|
1871
|
+
return {};
|
|
1872
|
+
}
|
|
1649
1873
|
}
|
|
1650
1874
|
warnIfInsecureConfig() {
|
|
1651
1875
|
if (this.options.dev) {
|
|
@@ -1667,15 +1891,15 @@ ${catalog}` : searchSkillDescription;
|
|
|
1667
1891
|
{ name: "skilled-openapi:hidden-ops", provide: HiddenOpRegistry, useValue: new HiddenOpRegistry() },
|
|
1668
1892
|
{
|
|
1669
1893
|
name: "skilled-openapi:bundle-store",
|
|
1670
|
-
provide:
|
|
1894
|
+
provide: import_skills8.BundleStore,
|
|
1671
1895
|
// Resolve TelemetryAccessor from the scope's provider registry so the
|
|
1672
1896
|
// bundle-pulls counter and `skill.bundle.swap` span actually export.
|
|
1673
1897
|
// The accessor is structurally compatible with `BundleStoreTelemetry`.
|
|
1674
1898
|
// ObservabilityPlugin is an optional peer dep — when it isn't installed
|
|
1675
1899
|
// we resolve `undefined` and the BundleStore falls through its zero-cost
|
|
1676
1900
|
// no-telemetry path.
|
|
1677
|
-
inject: () => [
|
|
1678
|
-
useFactory: (scope) => new
|
|
1901
|
+
inject: () => [import_sdk6.ScopeEntry],
|
|
1902
|
+
useFactory: (scope) => new import_skills8.BundleStore({ telemetry: resolveBundleTelemetry(scope) })
|
|
1679
1903
|
},
|
|
1680
1904
|
{
|
|
1681
1905
|
name: "skilled-openapi:credential-resolver",
|
|
@@ -1685,13 +1909,23 @@ ${catalog}` : searchSkillDescription;
|
|
|
1685
1909
|
{
|
|
1686
1910
|
name: "skilled-openapi:authority-guard",
|
|
1687
1911
|
provide: AuthorityGuard,
|
|
1688
|
-
inject: () => [
|
|
1689
|
-
useFactory: (scope) => new AuthorityGuard({
|
|
1912
|
+
inject: () => [import_sdk6.ScopeEntry],
|
|
1913
|
+
useFactory: (scope) => new AuthorityGuard({
|
|
1914
|
+
logger: scope.logger.child("skilled-openapi:authority"),
|
|
1915
|
+
// Bundle rules follow the server's authorities settings (claimsMapping,
|
|
1916
|
+
// claimsResolver, relationshipResolver, evaluators) when it has them. Read per check:
|
|
1917
|
+
// the server may register its engine after this provider is built.
|
|
1918
|
+
serverAuthorities: () => {
|
|
1919
|
+
const engine = scope.authoritiesEngine;
|
|
1920
|
+
const contextBuilder = scope.authoritiesContextBuilder;
|
|
1921
|
+
return engine && contextBuilder ? { engine, contextBuilder } : void 0;
|
|
1922
|
+
}
|
|
1923
|
+
})
|
|
1690
1924
|
},
|
|
1691
1925
|
{
|
|
1692
1926
|
name: "skilled-openapi:bundle-sync",
|
|
1693
1927
|
provide: BundleSyncService,
|
|
1694
|
-
inject: () => [
|
|
1928
|
+
inject: () => [import_sdk6.ScopeEntry, HiddenOpRegistry, import_skills8.BundleStore],
|
|
1695
1929
|
useFactory: async (scope, hiddenOps, bundleStore) => {
|
|
1696
1930
|
const logger = scope.logger.child("skilled-openapi:sync");
|
|
1697
1931
|
const lazySkillRegistry = new Proxy({}, {
|
|
@@ -1749,7 +1983,7 @@ ${catalog}` : searchSkillDescription;
|
|
|
1749
1983
|
const runtimeDeps = resolveRuntimeDeps(scope);
|
|
1750
1984
|
let source;
|
|
1751
1985
|
try {
|
|
1752
|
-
source = (0,
|
|
1986
|
+
source = (0, import_skills8.createBundleSource)(parsed.source, parsed.bundleCacheDir, logger, runtimeDeps);
|
|
1753
1987
|
} catch (e) {
|
|
1754
1988
|
logger.error(`failed to construct bundle source: ${e.message}`);
|
|
1755
1989
|
return sync;
|
|
@@ -1774,14 +2008,20 @@ ${catalog}` : searchSkillDescription;
|
|
|
1774
2008
|
}
|
|
1775
2009
|
};
|
|
1776
2010
|
__decorateClass([
|
|
1777
|
-
|
|
2011
|
+
import_sdk6.ListToolsHook.Did("findTools", { priority: 50 })
|
|
1778
2012
|
], SkilledOpenApiPlugin.prototype, "injectSkillCatalogIntoSearchTool", 1);
|
|
2013
|
+
__decorateClass([
|
|
2014
|
+
FilterSkillsHook.Did("filterSkills", { priority: 50 })
|
|
2015
|
+
], SkilledOpenApiPlugin.prototype, "hideBundleSkillsFromCaller", 1);
|
|
1779
2016
|
SkilledOpenApiPlugin = __decorateClass([
|
|
1780
|
-
(0,
|
|
2017
|
+
(0, import_sdk6.Plugin)({
|
|
1781
2018
|
name: "skilled-openapi",
|
|
1782
2019
|
description: "Serve a customer's OpenAPI spec as signed skill bundles with hidden per-operation tools mediated by 3 meta-tools.",
|
|
1783
2020
|
providers: [],
|
|
1784
|
-
tools: [SearchSkillTool, LoadSkillTool, RunWorkflowTool]
|
|
2021
|
+
tools: [SearchSkillTool, LoadSkillTool, RunWorkflowTool],
|
|
2022
|
+
// The bundle's skills are registered after startup: serve the skills capability and methods from
|
|
2023
|
+
// startup, answering with the skills loaded so far.
|
|
2024
|
+
dynamicSkills: true
|
|
1785
2025
|
})
|
|
1786
2026
|
], SkilledOpenApiPlugin);
|
|
1787
2027
|
|
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.4",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,15 +48,15 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.8.
|
|
52
|
-
"@frontmcp/auth": "1.8.
|
|
53
|
-
"@frontmcp/sdk": "1.8.
|
|
54
|
-
"@frontmcp/utils": "1.8.
|
|
51
|
+
"@frontmcp/adapters": "1.8.4",
|
|
52
|
+
"@frontmcp/auth": "1.8.4",
|
|
53
|
+
"@frontmcp/sdk": "1.8.4",
|
|
54
|
+
"@frontmcp/utils": "1.8.4"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
57
|
"@enclave-vm/ast": "*",
|
|
58
58
|
"@enclave-vm/core": "*",
|
|
59
|
-
"@frontmcp/lazy-zod": "1.8.
|
|
59
|
+
"@frontmcp/lazy-zod": "1.8.4"
|
|
60
60
|
},
|
|
61
61
|
"peerDependenciesMeta": {
|
|
62
62
|
"@enclave-vm/ast": {
|
|
@@ -1,8 +1,23 @@
|
|
|
1
1
|
import type { AuthoritiesPolicy } from '@frontmcp/adapters/skills';
|
|
2
|
-
import { AuthoritiesEvaluatorRegistry, AuthoritiesProfileRegistry, type AuthoritiesResult } from '@frontmcp/auth';
|
|
2
|
+
import { AuthoritiesContextBuilder, AuthoritiesEngine, AuthoritiesEvaluatorRegistry, AuthoritiesProfileRegistry, type AuthoritiesResult } from '@frontmcp/auth';
|
|
3
3
|
import type { FrontMcpLogger } from '@frontmcp/sdk';
|
|
4
4
|
/** How an op with NO authorities policy at all is treated by {@link AuthorityGuard}. */
|
|
5
5
|
export type UnprotectedOpsPolicy = 'allow' | 'deny';
|
|
6
|
+
/** The server's authorities engine and context builder, when the server configures authorities. */
|
|
7
|
+
export interface ServerAuthorities {
|
|
8
|
+
engine: AuthoritiesEngine;
|
|
9
|
+
contextBuilder: AuthoritiesContextBuilder;
|
|
10
|
+
}
|
|
11
|
+
/** What {@link AuthorityGuard.canDiscover} needs: the rules of an action, without its input. */
|
|
12
|
+
export type DiscoveryCheckArgs = Omit<AuthorityCheckArgs, 'input' | 'env' | 'policy'> & {
|
|
13
|
+
policy?: AuthoritiesPolicy;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Whether a policy reads the action's input (or runs code the guard can't see
|
|
17
|
+
* into): `input.*` paths or keys, `fromInput` references, `custom` evaluators and
|
|
18
|
+
* `guards`. Such a policy can't be judged before the action is called.
|
|
19
|
+
*/
|
|
20
|
+
export declare function policyDependsOnInput(value: unknown): boolean;
|
|
6
21
|
export interface AuthorityCheckArgs {
|
|
7
22
|
/** Op-level required-authorities policy from the bundle (`OperationDescriptor.requiredAuthorities`). */
|
|
8
23
|
policy: AuthoritiesPolicy | undefined;
|
|
@@ -54,11 +69,29 @@ export declare class AuthorityGuard {
|
|
|
54
69
|
private readonly engine;
|
|
55
70
|
private readonly contextBuilder;
|
|
56
71
|
private readonly logger;
|
|
72
|
+
private readonly serverAuthorities;
|
|
57
73
|
constructor(opts?: {
|
|
58
74
|
profiles?: AuthoritiesProfileRegistry;
|
|
59
75
|
evaluators?: AuthoritiesEvaluatorRegistry;
|
|
60
76
|
logger?: FrontMcpLogger;
|
|
77
|
+
/**
|
|
78
|
+
* The server's authorities, read on every check (the server may register
|
|
79
|
+
* them after this guard is built). When it returns an engine and context
|
|
80
|
+
* builder, they replace the guard's own defaults.
|
|
81
|
+
*/
|
|
82
|
+
serverAuthorities?: () => ServerAuthorities | undefined;
|
|
61
83
|
});
|
|
84
|
+
/** The server's engine and context builder when it configures authorities, else the guard's own. */
|
|
85
|
+
private authorities;
|
|
86
|
+
/**
|
|
87
|
+
* Whether the caller may be shown a skill or action (in `search_skill`,
|
|
88
|
+
* `load_skill`, the catalog and the SDK skill surfaces), judged without the
|
|
89
|
+
* action's input. A rule that depends on the input can't be judged yet and
|
|
90
|
+
* doesn't hide anything; the call is still checked by {@link check}. Every other
|
|
91
|
+
* rule must grant, and a policy-less action under `unprotectedOps: 'deny'` must
|
|
92
|
+
* be public, exactly as at call time.
|
|
93
|
+
*/
|
|
94
|
+
canDiscover(args: DiscoveryCheckArgs): Promise<boolean>;
|
|
62
95
|
check(args: AuthorityCheckArgs): Promise<AuthoritiesResult>;
|
|
63
96
|
}
|
|
64
97
|
//# sourceMappingURL=authority-guard.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"authority-guard.d.ts","sourceRoot":"","sources":["../../src/security/authority-guard.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"authority-guard.d.ts","sourceRoot":"","sources":["../../src/security/authority-guard.ts"],"names":[],"mappings":"AAWA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,2BAA2B,CAAC;AACnE,OAAO,EACL,yBAAyB,EACzB,iBAAiB,EACjB,4BAA4B,EAC5B,0BAA0B,EAG1B,KAAK,iBAAiB,EACvB,MAAM,gBAAgB,CAAC;AACxB,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,eAAe,CAAC;AAEpD,wFAAwF;AACxF,MAAM,MAAM,oBAAoB,GAAG,OAAO,GAAG,MAAM,CAAC;AAEpD,mGAAmG;AACnG,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE,iBAAiB,CAAC;IAC1B,cAAc,EAAE,yBAAyB,CAAC;CAC3C;AAED,gGAAgG;AAChG,MAAM,MAAM,kBAAkB,GAAG,IAAI,CAAC,kBAAkB,EAAE,OAAO,GAAG,KAAK,GAAG,QAAQ,CAAC,GAAG;IACtF,MAAM,CAAC,EAAE,iBAAiB,CAAC;CAC5B,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,KAAK,EAAE,OAAO,GAAG,OAAO,CAU5D;AAwBD,MAAM,WAAW,kBAAkB;IACjC,wGAAwG;IACxG,MAAM,EAAE,iBAAiB,GAAG,SAAS,CAAC;IACtC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,iBAAiB,GAAG,SAAS,CAAC;IAC5C;;;;;OAKG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,oBAAoB,CAAC;IACtC,kFAAkF;IAClF,QAAQ,EAAE,OAAO,CAAC;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QAAC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACvF,4DAA4D;IAC5D,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAC/B,oEAAoE;IACpE,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAC/B;AAED;;;;;;;;;;GAUG;AACH,qBAAa,cAAc;IACzB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAoB;IAC3C,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA4B;IAC3D,OAAO,CAAC,QAAQ,CAAC,MAAM,CAA6B;IACpD,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAoD;gBAGpF,IAAI,GAAE;QACJ,QAAQ,CAAC,EAAE,0BAA0B,CAAC;QACtC,UAAU,CAAC,EAAE,4BAA4B,CAAC;QAC1C,MAAM,CAAC,EAAE,cAAc,CAAC;QACxB;;;;WAIG;QACH,iBAAiB,CAAC,EAAE,MAAM,iBAAiB,GAAG,SAAS,CAAC;KACpD;IAUR,oGAAoG;IACpG,OAAO,CAAC,WAAW;IAInB;;;;;;;OAOG;IACG,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,OAAO,CAAC;IAWvD,KAAK,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CAuDlE"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AuthoritiesPolicy, ResolvedBundle } from '@frontmcp/adapters/skills';
|
|
2
|
+
import { type ScopeEntry, type SkillAction, type SkillEntry } from '@frontmcp/sdk';
|
|
3
|
+
import type { HiddenOpRegistry } from '../registry/hidden-op.registry';
|
|
4
|
+
import type { AuthorityGuard, UnprotectedOpsPolicy } from './authority-guard';
|
|
5
|
+
export interface SkillVisibilityDeps {
|
|
6
|
+
scope: ScopeEntry;
|
|
7
|
+
guard: AuthorityGuard;
|
|
8
|
+
/** The active bundle, whose skill-level `requiredAuthorities` gate its skills. */
|
|
9
|
+
bundle: ResolvedBundle | undefined;
|
|
10
|
+
unprotectedOps: UnprotectedOpsPolicy;
|
|
11
|
+
/** The caller's AuthInfo. */
|
|
12
|
+
authInfo: unknown;
|
|
13
|
+
}
|
|
14
|
+
/** The skill-level rule of each skill in `bundle` that has one. */
|
|
15
|
+
export declare function bundleSkillPolicies(bundle: ResolvedBundle | undefined): Map<string, AuthoritiesPolicy>;
|
|
16
|
+
export declare class SkillVisibility {
|
|
17
|
+
private readonly deps;
|
|
18
|
+
private readonly policies;
|
|
19
|
+
private readonly authInfo;
|
|
20
|
+
constructor(deps: SkillVisibilityDeps);
|
|
21
|
+
/** Whether the caller satisfies the bundle skill's own `requiredAuthorities` (true for other skills). */
|
|
22
|
+
passesBundleSkillRule(skillId: string): Promise<boolean>;
|
|
23
|
+
/**
|
|
24
|
+
* Keep the items whose skill the caller may see: the bundle skill's rule, the
|
|
25
|
+
* `skills:filter` flow and the server's `@Skill({ authorities })` rule. An item
|
|
26
|
+
* whose id names no registered skill (an external skill) is judged by the bundle
|
|
27
|
+
* rule alone, as the SDK's own surfaces keep such results.
|
|
28
|
+
*/
|
|
29
|
+
filterVisible<T>(items: readonly T[], idOf: (item: T) => string): Promise<T[]>;
|
|
30
|
+
/** The registered skills the caller may see, in their order (see {@link filterVisible}). */
|
|
31
|
+
filterVisibleEntries<T extends SkillEntry>(entries: readonly T[]): Promise<T[]>;
|
|
32
|
+
/** Whether the caller may see the skill named by `skillId` (see {@link filterVisible}). */
|
|
33
|
+
isVisible(skillId: string): Promise<boolean>;
|
|
34
|
+
/**
|
|
35
|
+
* The actions of a bundle skill the caller may be shown: an action whose rules
|
|
36
|
+
* refuse the caller whatever the input (or that `unprotectedOps: 'deny'` blocks)
|
|
37
|
+
* is left out. Rules that depend on the input keep the action listed; the call is
|
|
38
|
+
* checked again when it runs.
|
|
39
|
+
*/
|
|
40
|
+
visibleActions(skillId: string, actions: readonly SkillAction[], hiddenOps: HiddenOpRegistry): Promise<SkillAction[]>;
|
|
41
|
+
/** The server's `@Skill({ authorities })` rule, evaluated as the SDK's skill surfaces do. */
|
|
42
|
+
private passesSkillAuthorities;
|
|
43
|
+
/** The registered skill an id names, looked up the way the SDK's skill surfaces do. */
|
|
44
|
+
private resolveEntry;
|
|
45
|
+
}
|
|
46
|
+
//# sourceMappingURL=skill-visibility.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skill-visibility.d.ts","sourceRoot":"","sources":["../../src/security/skill-visibility.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAEnF,OAAO,EAAwB,KAAK,UAAU,EAAE,KAAK,WAAW,EAAE,KAAK,UAAU,EAAE,MAAM,eAAe,CAAC;AAEzG,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AACvE,OAAO,KAAK,EAAE,cAAc,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAI9E,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,UAAU,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,kFAAkF;IAClF,MAAM,EAAE,cAAc,GAAG,SAAS,CAAC;IACnC,cAAc,EAAE,oBAAoB,CAAC;IACrC,6BAA6B;IAC7B,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,mEAAmE;AACnE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,cAAc,GAAG,SAAS,GAAG,GAAG,CAAC,MAAM,EAAE,iBAAiB,CAAC,CAQtG;AAED,qBAAa,eAAe;IAId,OAAO,CAAC,QAAQ,CAAC,IAAI;IAHjC,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiC;IAC1D,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAiB;gBAEb,IAAI,EAAE,mBAAmB;IAKtD,yGAAyG;IACnG,qBAAqB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAM9D;;;;;OAKG;IACG,aAAa,CAAC,CAAC,EAAE,KAAK,EAAE,SAAS,CAAC,EAAE,EAAE,IAAI,EAAE,CAAC,IAAI,EAAE,CAAC,KAAK,MAAM,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAepF,4FAA4F;IACtF,oBAAoB,CAAC,CAAC,SAAS,UAAU,EAAE,OAAO,EAAE,SAAS,CAAC,EAAE,GAAG,OAAO,CAAC,CAAC,EAAE,CAAC;IAYrF,2FAA2F;IACrF,SAAS,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAIlD;;;;;OAKG;IACG,cAAc,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,SAAS,WAAW,EAAE,EAAE,SAAS,EAAE,gBAAgB;IAiBlG,6FAA6F;YAC/E,sBAAsB;IAepC,uFAAuF;IACvF,OAAO,CAAC,YAAY;CASrB"}
|
|
@@ -57,6 +57,16 @@ export default class SkilledOpenApiPlugin extends DynamicPlugin<SkilledOpenApiPl
|
|
|
57
57
|
* from the static base each time, never appended onto a prior catalog.
|
|
58
58
|
*/
|
|
59
59
|
injectSkillCatalogIntoSearchTool(flowCtx: FlowCtxOf<'tools:list-tools'>): Promise<void>;
|
|
60
|
+
/**
|
|
61
|
+
* Hide a bundle skill from a caller who doesn't satisfy its `requiredAuthorities`
|
|
62
|
+
* on every SDK skill surface (skills/list, skills/search, skills/load, the
|
|
63
|
+
* `skill://` resources, the HTTP /skills API): a skill this flow drops is absent
|
|
64
|
+
* there and not found when named. The plugin's own meta-tools apply the same rule.
|
|
65
|
+
*/
|
|
66
|
+
hideBundleSkillsFromCaller(flowCtx: FlowCtxOf<'skills:filter'>): Promise<void>;
|
|
67
|
+
private skillVisibility;
|
|
68
|
+
/** The AuthInfo of the request being served; an anonymous caller when there is none. */
|
|
69
|
+
private currentAuthInfo;
|
|
60
70
|
private warnIfInsecureConfig;
|
|
61
71
|
static dynamicProviders(options: SkilledOpenApiPluginOptionsInput): ProviderType[];
|
|
62
72
|
}
|
|
@@ -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,
|
|
1
|
+
{"version":3,"file":"skilled-openapi.plugin.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAIrB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,aAAa,EAOb,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAOvB,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,yBAAyB,CAAC;AAsBjC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,eAAsD,CAAC;AAEtG;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE;;;;OAIG;IACH,MAAM,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC1C;AAqDD;;;;;;;;;;;;GAYG;AAWH,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,aAAa,CAC7D,2BAA2B,EAC3B,gCAAgC,CACjC;IACC,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,YAAY,CAAC,CAAiB;gBAE1B,OAAO,EAAE,gCAAgC;IAMrD,OAAO,CAAC,SAAS;IAOjB;;;;;;;;;;OAUG;IAEG,gCAAgC,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqC7F;;;;;OAKG;IAEG,0BAA0B,CAAC,OAAO,EAAE,SAAS,CAAC,eAAe,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAqBpF,OAAO,CAAC,eAAe;IAUvB,wFAAwF;IACxF,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,oBAAoB;WAaZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CAoK5F"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/load-skill.tool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"load-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/load-skill.tool.ts"],"names":[],"mappings":"AAGA,OAAO,EAAsD,WAAW,EAAE,MAAM,eAAe,CAAC;AAOhG,OAAO,EAIL,KAAK,cAAc,EACnB,KAAK,eAAe,EACrB,MAAM,qBAAqB,CAAC;AAY7B,MAAM,CAAC,OAAO,OAAO,aAAc,SAAQ,WAAW;IAC9C,OAAO,CAAC,KAAK,EAAE,cAAc,GAAG,OAAO,CAAC,eAAe,CAAC;CAkD/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/search-skill.tool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"search-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/search-skill.tool.ts"],"names":[],"mappings":"AAGA,OAAO,EAAoB,WAAW,EAAE,MAAM,eAAe,CAAC;AAM9D,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAY/B,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,WAAW;IAChD,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA8CnE"}
|