@frontmcp/plugin-skilled-openapi 1.5.0 → 1.5.2
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 +91 -25
- package/esm/package.json +5 -5
- package/index.js +89 -23
- package/package.json +5 -5
- package/skilled-openapi.plugin.d.ts.map +1 -1
- package/sync/bundle-sync.service.d.ts +41 -0
- package/sync/bundle-sync.service.d.ts.map +1 -1
- package/tools/load-skill.tool.d.ts.map +1 -1
- package/tools/run-workflow.tool.d.ts.map +1 -1
- package/tools/search-skill.tool.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -15,12 +15,12 @@ import {
|
|
|
15
15
|
createBundleSource
|
|
16
16
|
} from "@frontmcp/adapters/skills";
|
|
17
17
|
import {
|
|
18
|
+
buildSkillsCatalogSummary,
|
|
18
19
|
DynamicPlugin,
|
|
19
20
|
FrontMcpLogger,
|
|
20
21
|
ListToolsHook,
|
|
21
22
|
Plugin,
|
|
22
|
-
ScopeEntry as ScopeEntry3
|
|
23
|
-
buildSkillsCatalogSummary
|
|
23
|
+
ScopeEntry as ScopeEntry3
|
|
24
24
|
} from "@frontmcp/sdk";
|
|
25
25
|
|
|
26
26
|
// plugins/plugin-skilled-openapi/src/executor/credential-resolver.ts
|
|
@@ -350,6 +350,78 @@ var BundleSyncService = class {
|
|
|
350
350
|
logger;
|
|
351
351
|
operationToolFactory;
|
|
352
352
|
skillUnregisterByBundleId = /* @__PURE__ */ new Map();
|
|
353
|
+
/** The configured bundle source; wired by the DI factory via {@link attachSource}. */
|
|
354
|
+
source;
|
|
355
|
+
readyPromise;
|
|
356
|
+
/**
|
|
357
|
+
* Wired by the plugin's DI factory. Stores the bundle source so {@link
|
|
358
|
+
* ensureReady} can start it and await the first apply lazily, inside the
|
|
359
|
+
* request that needs the catalog. Does NOT start the source — see the context
|
|
360
|
+
* note on {@link bootstrap}.
|
|
361
|
+
*/
|
|
362
|
+
attachSource(source) {
|
|
363
|
+
this.source = source;
|
|
364
|
+
}
|
|
365
|
+
/**
|
|
366
|
+
* Ensure the source has started and its FIRST bundle has been applied, so
|
|
367
|
+
* `scope.skills` and the hidden-op registry are populated before a meta-tool
|
|
368
|
+
* (search_skill / load_skill / run_workflow) reads them. Memoized — the
|
|
369
|
+
* underlying start + apply runs at most once per service instance.
|
|
370
|
+
*/
|
|
371
|
+
async ensureReady() {
|
|
372
|
+
if (!this.source) return;
|
|
373
|
+
this.readyPromise ??= this.bootstrap(this.source);
|
|
374
|
+
return this.readyPromise;
|
|
375
|
+
}
|
|
376
|
+
/**
|
|
377
|
+
* Start the source and await its FIRST bundle apply.
|
|
378
|
+
*
|
|
379
|
+
* CRITICAL — every promise here (the `apply()`, the `firstApply` latch, and
|
|
380
|
+
* the `onChange` wiring) is created inside the FIRST caller's context, which
|
|
381
|
+
* is a request context (the meta-tools / list-tools hook call `ensureReady()`
|
|
382
|
+
* while handling a request). On a stateless V8-isolate runtime (Cloudflare
|
|
383
|
+
* Workers) this is mandatory two ways over:
|
|
384
|
+
* 1. workerd cancels promise continuations that cross a request boundary, so
|
|
385
|
+
* a `firstApply` created in the scope-init/factory context would never
|
|
386
|
+
* resolve when awaited from a later request — it must be created here.
|
|
387
|
+
* 2. There is no background event loop, so a deferred/fire-and-forget apply
|
|
388
|
+
* (the old `setImmediate(source.start())`) can never complete after the
|
|
389
|
+
* response is sent — the awaiting tool drives it to completion inline.
|
|
390
|
+
* A failed first sync is swallowed (logged) so it degrades to "no skills"
|
|
391
|
+
* instead of hanging or 500-ing every call.
|
|
392
|
+
*/
|
|
393
|
+
async bootstrap(source) {
|
|
394
|
+
let resolveFirst;
|
|
395
|
+
let firstSettled = false;
|
|
396
|
+
const firstApply = new Promise((resolve) => {
|
|
397
|
+
resolveFirst = resolve;
|
|
398
|
+
});
|
|
399
|
+
let applyTail = Promise.resolve();
|
|
400
|
+
source.onChange((bundle) => {
|
|
401
|
+
applyTail = applyTail.then(async () => {
|
|
402
|
+
try {
|
|
403
|
+
const result = await this.apply(bundle);
|
|
404
|
+
if (!result.applied) {
|
|
405
|
+
this.logger.warn(`[bundle-sync] bundle ${bundle.bundleId}@${bundle.version} not applied: ${result.reason}`);
|
|
406
|
+
}
|
|
407
|
+
} catch (e) {
|
|
408
|
+
this.logger.error(
|
|
409
|
+
`[bundle-sync] bundle ${bundle.bundleId}@${bundle.version} apply threw: ${e.message}`
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
if (!firstSettled) {
|
|
414
|
+
firstSettled = true;
|
|
415
|
+
void applyTail.then(() => resolveFirst());
|
|
416
|
+
}
|
|
417
|
+
});
|
|
418
|
+
try {
|
|
419
|
+
await source.start();
|
|
420
|
+
if (firstSettled) await firstApply;
|
|
421
|
+
} catch (e) {
|
|
422
|
+
this.logger.error(`[skilled-openapi] initial bundle sync failed: ${e.message}`);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
353
425
|
/**
|
|
354
426
|
* Validate signature and apply the bundle. Returns a structured result;
|
|
355
427
|
* never throws on validation/registration failure (caller logs + ignores).
|
|
@@ -612,7 +684,7 @@ var loadSkillOutputSchema = {
|
|
|
612
684
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
|
|
613
685
|
var LoadSkillTool = class extends ToolContext {
|
|
614
686
|
async execute(input) {
|
|
615
|
-
this.get(BundleSyncService);
|
|
687
|
+
await this.get(BundleSyncService).ensureReady();
|
|
616
688
|
const scope = this.get(ScopeEntry);
|
|
617
689
|
const skillRegistry = scope.skills;
|
|
618
690
|
if (!skillRegistry) {
|
|
@@ -1411,7 +1483,7 @@ var MAX_TOOL_CALLS = 25;
|
|
|
1411
1483
|
var TIMEOUT_MS = 8e3;
|
|
1412
1484
|
var RunWorkflowTool = class extends ToolContext2 {
|
|
1413
1485
|
async execute(input) {
|
|
1414
|
-
this.get(BundleSyncService);
|
|
1486
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1415
1487
|
const config = this.get(SkilledOpenApiConfig);
|
|
1416
1488
|
const hiddenOps = this.get(HiddenOpRegistry);
|
|
1417
1489
|
const guard = this.get(AuthorityGuard);
|
|
@@ -1438,7 +1510,9 @@ var RunWorkflowTool = class extends ToolContext2 {
|
|
|
1438
1510
|
const toolHandler = async (actionId, actionInput) => {
|
|
1439
1511
|
const entry = hiddenOps.getByActionId(actionId);
|
|
1440
1512
|
if (!entry) {
|
|
1441
|
-
throw new Error(
|
|
1513
|
+
throw new Error(
|
|
1514
|
+
`unknown action "${actionId}" \u2014 run search_skill then load_skill to discover available actions`
|
|
1515
|
+
);
|
|
1442
1516
|
}
|
|
1443
1517
|
const res = await executeSkillAction({ entry, input: actionInput ?? {}, authInfo: this.authInfo, deps });
|
|
1444
1518
|
if (!res.ok) {
|
|
@@ -1450,7 +1524,10 @@ var RunWorkflowTool = class extends ToolContext2 {
|
|
|
1450
1524
|
try {
|
|
1451
1525
|
transformed = transformAgentScript(input.script, { transformLoops: false });
|
|
1452
1526
|
} catch (error) {
|
|
1453
|
-
return {
|
|
1527
|
+
return {
|
|
1528
|
+
success: false,
|
|
1529
|
+
error: `AgentScript rejected: ${error instanceof Error ? error.message : String(error)}`
|
|
1530
|
+
};
|
|
1454
1531
|
}
|
|
1455
1532
|
const adapter = new InterpreterAdapter({ maxSteps: MAX_STEPS });
|
|
1456
1533
|
const context = {
|
|
@@ -1486,9 +1563,6 @@ RunWorkflowTool = __decorateClass([
|
|
|
1486
1563
|
})
|
|
1487
1564
|
], RunWorkflowTool);
|
|
1488
1565
|
|
|
1489
|
-
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1490
|
-
import { ScopeEntry as ScopeEntry2, Tool as Tool3, ToolContext as ToolContext3 } from "@frontmcp/sdk";
|
|
1491
|
-
|
|
1492
1566
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.schema.ts
|
|
1493
1567
|
import { z as z5 } from "@frontmcp/lazy-zod";
|
|
1494
1568
|
var searchSkillDescription = `Search the available skills by free-form query.
|
|
@@ -1530,9 +1604,10 @@ var searchSkillOutputSchema = {
|
|
|
1530
1604
|
};
|
|
1531
1605
|
|
|
1532
1606
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1607
|
+
import { ScopeEntry as ScopeEntry2, Tool as Tool3, ToolContext as ToolContext3 } from "@frontmcp/sdk";
|
|
1533
1608
|
var SearchSkillTool = class extends ToolContext3 {
|
|
1534
1609
|
async execute(input) {
|
|
1535
|
-
this.get(BundleSyncService);
|
|
1610
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1536
1611
|
const scope = this.get(ScopeEntry2);
|
|
1537
1612
|
const skillRegistry = scope.skills;
|
|
1538
1613
|
if (!skillRegistry || !skillRegistry.hasAny()) {
|
|
@@ -1620,7 +1695,7 @@ var SkilledOpenApiPlugin = class extends DynamicPlugin {
|
|
|
1620
1695
|
const target = tools.find((item) => item.tool?.metadata?.name === "search_skill");
|
|
1621
1696
|
if (!target) return;
|
|
1622
1697
|
try {
|
|
1623
|
-
this.get(BundleSyncService);
|
|
1698
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1624
1699
|
} catch {
|
|
1625
1700
|
}
|
|
1626
1701
|
const scope = this.get(ScopeEntry3);
|
|
@@ -1746,21 +1821,12 @@ ${catalog}` : searchSkillDescription;
|
|
|
1746
1821
|
logger.warn(`runtime-deps attach threw: ${e.message}`);
|
|
1747
1822
|
}
|
|
1748
1823
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
}
|
|
1754
|
-
}).catch((e) => {
|
|
1755
|
-
logger.error(`bundle ${bundle.bundleId}@${bundle.version} apply threw: ${e.message}`);
|
|
1824
|
+
sync.attachSource(source);
|
|
1825
|
+
if (!runtimeDeps?.disablePolling) {
|
|
1826
|
+
setImmediate(() => {
|
|
1827
|
+
void sync.ensureReady();
|
|
1756
1828
|
});
|
|
1757
|
-
}
|
|
1758
|
-
const startedSource = source;
|
|
1759
|
-
setImmediate(() => {
|
|
1760
|
-
startedSource.start().catch((e) => {
|
|
1761
|
-
logger.error(`bundle source failed to start: ${e.message}`);
|
|
1762
|
-
});
|
|
1763
|
-
});
|
|
1829
|
+
}
|
|
1764
1830
|
return sync;
|
|
1765
1831
|
}
|
|
1766
1832
|
}
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-skilled-openapi",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.5.
|
|
52
|
-
"@frontmcp/auth": "1.5.
|
|
53
|
-
"@frontmcp/sdk": "1.5.
|
|
51
|
+
"@frontmcp/adapters": "1.5.2",
|
|
52
|
+
"@frontmcp/auth": "1.5.2",
|
|
53
|
+
"@frontmcp/sdk": "1.5.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@enclave-vm/ast": "*",
|
|
57
57
|
"@enclave-vm/core": "*",
|
|
58
|
-
"@frontmcp/lazy-zod": "1.5.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.5.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@enclave-vm/ast": {
|
package/index.js
CHANGED
|
@@ -359,6 +359,78 @@ var BundleSyncService = class {
|
|
|
359
359
|
logger;
|
|
360
360
|
operationToolFactory;
|
|
361
361
|
skillUnregisterByBundleId = /* @__PURE__ */ new Map();
|
|
362
|
+
/** The configured bundle source; wired by the DI factory via {@link attachSource}. */
|
|
363
|
+
source;
|
|
364
|
+
readyPromise;
|
|
365
|
+
/**
|
|
366
|
+
* Wired by the plugin's DI factory. Stores the bundle source so {@link
|
|
367
|
+
* ensureReady} can start it and await the first apply lazily, inside the
|
|
368
|
+
* request that needs the catalog. Does NOT start the source — see the context
|
|
369
|
+
* note on {@link bootstrap}.
|
|
370
|
+
*/
|
|
371
|
+
attachSource(source) {
|
|
372
|
+
this.source = source;
|
|
373
|
+
}
|
|
374
|
+
/**
|
|
375
|
+
* Ensure the source has started and its FIRST bundle has been applied, so
|
|
376
|
+
* `scope.skills` and the hidden-op registry are populated before a meta-tool
|
|
377
|
+
* (search_skill / load_skill / run_workflow) reads them. Memoized — the
|
|
378
|
+
* underlying start + apply runs at most once per service instance.
|
|
379
|
+
*/
|
|
380
|
+
async ensureReady() {
|
|
381
|
+
if (!this.source) return;
|
|
382
|
+
this.readyPromise ??= this.bootstrap(this.source);
|
|
383
|
+
return this.readyPromise;
|
|
384
|
+
}
|
|
385
|
+
/**
|
|
386
|
+
* Start the source and await its FIRST bundle apply.
|
|
387
|
+
*
|
|
388
|
+
* CRITICAL — every promise here (the `apply()`, the `firstApply` latch, and
|
|
389
|
+
* the `onChange` wiring) is created inside the FIRST caller's context, which
|
|
390
|
+
* is a request context (the meta-tools / list-tools hook call `ensureReady()`
|
|
391
|
+
* while handling a request). On a stateless V8-isolate runtime (Cloudflare
|
|
392
|
+
* Workers) this is mandatory two ways over:
|
|
393
|
+
* 1. workerd cancels promise continuations that cross a request boundary, so
|
|
394
|
+
* a `firstApply` created in the scope-init/factory context would never
|
|
395
|
+
* resolve when awaited from a later request — it must be created here.
|
|
396
|
+
* 2. There is no background event loop, so a deferred/fire-and-forget apply
|
|
397
|
+
* (the old `setImmediate(source.start())`) can never complete after the
|
|
398
|
+
* response is sent — the awaiting tool drives it to completion inline.
|
|
399
|
+
* A failed first sync is swallowed (logged) so it degrades to "no skills"
|
|
400
|
+
* instead of hanging or 500-ing every call.
|
|
401
|
+
*/
|
|
402
|
+
async bootstrap(source) {
|
|
403
|
+
let resolveFirst;
|
|
404
|
+
let firstSettled = false;
|
|
405
|
+
const firstApply = new Promise((resolve) => {
|
|
406
|
+
resolveFirst = resolve;
|
|
407
|
+
});
|
|
408
|
+
let applyTail = Promise.resolve();
|
|
409
|
+
source.onChange((bundle) => {
|
|
410
|
+
applyTail = applyTail.then(async () => {
|
|
411
|
+
try {
|
|
412
|
+
const result = await this.apply(bundle);
|
|
413
|
+
if (!result.applied) {
|
|
414
|
+
this.logger.warn(`[bundle-sync] bundle ${bundle.bundleId}@${bundle.version} not applied: ${result.reason}`);
|
|
415
|
+
}
|
|
416
|
+
} catch (e) {
|
|
417
|
+
this.logger.error(
|
|
418
|
+
`[bundle-sync] bundle ${bundle.bundleId}@${bundle.version} apply threw: ${e.message}`
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
if (!firstSettled) {
|
|
423
|
+
firstSettled = true;
|
|
424
|
+
void applyTail.then(() => resolveFirst());
|
|
425
|
+
}
|
|
426
|
+
});
|
|
427
|
+
try {
|
|
428
|
+
await source.start();
|
|
429
|
+
if (firstSettled) await firstApply;
|
|
430
|
+
} catch (e) {
|
|
431
|
+
this.logger.error(`[skilled-openapi] initial bundle sync failed: ${e.message}`);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
362
434
|
/**
|
|
363
435
|
* Validate signature and apply the bundle. Returns a structured result;
|
|
364
436
|
* never throws on validation/registration failure (caller logs + ignores).
|
|
@@ -621,7 +693,7 @@ var loadSkillOutputSchema = {
|
|
|
621
693
|
// plugins/plugin-skilled-openapi/src/tools/load-skill.tool.ts
|
|
622
694
|
var LoadSkillTool = class extends import_sdk.ToolContext {
|
|
623
695
|
async execute(input) {
|
|
624
|
-
this.get(BundleSyncService);
|
|
696
|
+
await this.get(BundleSyncService).ensureReady();
|
|
625
697
|
const scope = this.get(import_sdk.ScopeEntry);
|
|
626
698
|
const skillRegistry = scope.skills;
|
|
627
699
|
if (!skillRegistry) {
|
|
@@ -1414,7 +1486,7 @@ var MAX_TOOL_CALLS = 25;
|
|
|
1414
1486
|
var TIMEOUT_MS = 8e3;
|
|
1415
1487
|
var RunWorkflowTool = class extends import_sdk3.ToolContext {
|
|
1416
1488
|
async execute(input) {
|
|
1417
|
-
this.get(BundleSyncService);
|
|
1489
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1418
1490
|
const config = this.get(SkilledOpenApiConfig);
|
|
1419
1491
|
const hiddenOps = this.get(HiddenOpRegistry);
|
|
1420
1492
|
const guard = this.get(AuthorityGuard);
|
|
@@ -1441,7 +1513,9 @@ var RunWorkflowTool = class extends import_sdk3.ToolContext {
|
|
|
1441
1513
|
const toolHandler = async (actionId, actionInput) => {
|
|
1442
1514
|
const entry = hiddenOps.getByActionId(actionId);
|
|
1443
1515
|
if (!entry) {
|
|
1444
|
-
throw new Error(
|
|
1516
|
+
throw new Error(
|
|
1517
|
+
`unknown action "${actionId}" \u2014 run search_skill then load_skill to discover available actions`
|
|
1518
|
+
);
|
|
1445
1519
|
}
|
|
1446
1520
|
const res = await executeSkillAction({ entry, input: actionInput ?? {}, authInfo: this.authInfo, deps });
|
|
1447
1521
|
if (!res.ok) {
|
|
@@ -1453,7 +1527,10 @@ var RunWorkflowTool = class extends import_sdk3.ToolContext {
|
|
|
1453
1527
|
try {
|
|
1454
1528
|
transformed = transformAgentScript(input.script, { transformLoops: false });
|
|
1455
1529
|
} catch (error) {
|
|
1456
|
-
return {
|
|
1530
|
+
return {
|
|
1531
|
+
success: false,
|
|
1532
|
+
error: `AgentScript rejected: ${error instanceof Error ? error.message : String(error)}`
|
|
1533
|
+
};
|
|
1457
1534
|
}
|
|
1458
1535
|
const adapter = new InterpreterAdapter({ maxSteps: MAX_STEPS });
|
|
1459
1536
|
const context = {
|
|
@@ -1489,9 +1566,6 @@ RunWorkflowTool = __decorateClass([
|
|
|
1489
1566
|
})
|
|
1490
1567
|
], RunWorkflowTool);
|
|
1491
1568
|
|
|
1492
|
-
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1493
|
-
var import_sdk4 = require("@frontmcp/sdk");
|
|
1494
|
-
|
|
1495
1569
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.schema.ts
|
|
1496
1570
|
var import_lazy_zod5 = require("@frontmcp/lazy-zod");
|
|
1497
1571
|
var searchSkillDescription = `Search the available skills by free-form query.
|
|
@@ -1533,9 +1607,10 @@ var searchSkillOutputSchema = {
|
|
|
1533
1607
|
};
|
|
1534
1608
|
|
|
1535
1609
|
// plugins/plugin-skilled-openapi/src/tools/search-skill.tool.ts
|
|
1610
|
+
var import_sdk4 = require("@frontmcp/sdk");
|
|
1536
1611
|
var SearchSkillTool = class extends import_sdk4.ToolContext {
|
|
1537
1612
|
async execute(input) {
|
|
1538
|
-
this.get(BundleSyncService);
|
|
1613
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1539
1614
|
const scope = this.get(import_sdk4.ScopeEntry);
|
|
1540
1615
|
const skillRegistry = scope.skills;
|
|
1541
1616
|
if (!skillRegistry || !skillRegistry.hasAny()) {
|
|
@@ -1623,7 +1698,7 @@ var SkilledOpenApiPlugin = class extends import_sdk5.DynamicPlugin {
|
|
|
1623
1698
|
const target = tools.find((item) => item.tool?.metadata?.name === "search_skill");
|
|
1624
1699
|
if (!target) return;
|
|
1625
1700
|
try {
|
|
1626
|
-
this.get(BundleSyncService);
|
|
1701
|
+
await this.get(BundleSyncService).ensureReady();
|
|
1627
1702
|
} catch {
|
|
1628
1703
|
}
|
|
1629
1704
|
const scope = this.get(import_sdk5.ScopeEntry);
|
|
@@ -1749,21 +1824,12 @@ ${catalog}` : searchSkillDescription;
|
|
|
1749
1824
|
logger.warn(`runtime-deps attach threw: ${e.message}`);
|
|
1750
1825
|
}
|
|
1751
1826
|
}
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
}
|
|
1757
|
-
}).catch((e) => {
|
|
1758
|
-
logger.error(`bundle ${bundle.bundleId}@${bundle.version} apply threw: ${e.message}`);
|
|
1827
|
+
sync.attachSource(source);
|
|
1828
|
+
if (!runtimeDeps?.disablePolling) {
|
|
1829
|
+
setImmediate(() => {
|
|
1830
|
+
void sync.ensureReady();
|
|
1759
1831
|
});
|
|
1760
|
-
}
|
|
1761
|
-
const startedSource = source;
|
|
1762
|
-
setImmediate(() => {
|
|
1763
|
-
startedSource.start().catch((e) => {
|
|
1764
|
-
logger.error(`bundle source failed to start: ${e.message}`);
|
|
1765
|
-
});
|
|
1766
|
-
});
|
|
1832
|
+
}
|
|
1767
1833
|
return sync;
|
|
1768
1834
|
}
|
|
1769
1835
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-skilled-openapi",
|
|
3
|
-
"version": "1.5.
|
|
3
|
+
"version": "1.5.2",
|
|
4
4
|
"description": "FrontMCP plugin: serve a customer's OpenAPI spec as skill bundles (signed, OpenAPI-Overlay-based) with hidden per-operation tools mediated by three meta-tools (search_skill / load_skill / run_workflow — the last runs an enclave-sandboxed AgentScript that orchestrates the loaded skills' operations).",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -48,14 +48,14 @@
|
|
|
48
48
|
}
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
|
-
"@frontmcp/adapters": "1.5.
|
|
52
|
-
"@frontmcp/auth": "1.5.
|
|
53
|
-
"@frontmcp/sdk": "1.5.
|
|
51
|
+
"@frontmcp/adapters": "1.5.2",
|
|
52
|
+
"@frontmcp/auth": "1.5.2",
|
|
53
|
+
"@frontmcp/sdk": "1.5.2"
|
|
54
54
|
},
|
|
55
55
|
"peerDependencies": {
|
|
56
56
|
"@enclave-vm/ast": "*",
|
|
57
57
|
"@enclave-vm/core": "*",
|
|
58
|
-
"@frontmcp/lazy-zod": "1.5.
|
|
58
|
+
"@frontmcp/lazy-zod": "1.5.2"
|
|
59
59
|
},
|
|
60
60
|
"peerDependenciesMeta": {
|
|
61
61
|
"@enclave-vm/ast": {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skilled-openapi.plugin.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAIrB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,
|
|
1
|
+
{"version":3,"file":"skilled-openapi.plugin.d.ts","sourceRoot":"","sources":["../src/skilled-openapi.plugin.ts"],"names":[],"mappings":"AAEA,OAAO,EAGL,KAAK,gBAAgB,EAIrB,KAAK,iBAAiB,EACvB,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAEL,aAAa,EAKb,KAAK,SAAS,EAEd,KAAK,YAAY,EAClB,MAAM,eAAe,CAAC;AAMvB,OAAO,EAEL,KAAK,2BAA2B,EAChC,KAAK,gCAAgC,EACtC,MAAM,yBAAyB,CAAC;AAmBjC;;;;;;;;;;GAUG;AACH,eAAO,MAAM,kCAAkC,eAAsD,CAAC;AAEtG;;;;;GAKG;AACH,MAAM,WAAW,yBAA0B,SAAQ,gBAAgB;IACjE;;;;OAIG;IACH,MAAM,CAAC,CAAC,MAAM,EAAE,iBAAiB,GAAG,IAAI,CAAC;CAC1C;AAqDD;;;;;;;;;;;;GAYG;AAQH,MAAM,CAAC,OAAO,OAAO,oBAAqB,SAAQ,aAAa,CAC7D,2BAA2B,EAC3B,gCAAgC,CACjC;IACC,OAAO,EAAE,2BAA2B,CAAC;IACrC,OAAO,CAAC,YAAY,CAAC,CAAiB;gBAE1B,OAAO,EAAE,gCAAgC;IAMrD,OAAO,CAAC,SAAS;IAOjB;;;;;;;;;;OAUG;IAEG,gCAAgC,CAAC,OAAO,EAAE,SAAS,CAAC,kBAAkB,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC;IAuB7F,OAAO,CAAC,oBAAoB;WAaZ,gBAAgB,CAAC,OAAO,EAAE,gCAAgC,GAAG,YAAY,EAAE;CA0J5F"}
|
|
@@ -46,6 +46,11 @@ export interface BundleApplyResult {
|
|
|
46
46
|
* Listeners can subscribe via the underlying BundleStore for downstream
|
|
47
47
|
* effects (audit log, OTel attributes, /healthz updates).
|
|
48
48
|
*/
|
|
49
|
+
/** Minimal slice of a bundle source that {@link BundleSyncService} drives. */
|
|
50
|
+
interface StartableBundleSource {
|
|
51
|
+
start(): Promise<void>;
|
|
52
|
+
onChange(listener: (bundle: ResolvedBundle) => void): () => void;
|
|
53
|
+
}
|
|
49
54
|
export declare class BundleSyncService {
|
|
50
55
|
private readonly skillRegistry;
|
|
51
56
|
private readonly hiddenOps;
|
|
@@ -61,6 +66,9 @@ export declare class BundleSyncService {
|
|
|
61
66
|
*/
|
|
62
67
|
private readonly operationToolFactory?;
|
|
63
68
|
private skillUnregisterByBundleId;
|
|
69
|
+
/** The configured bundle source; wired by the DI factory via {@link attachSource}. */
|
|
70
|
+
private source?;
|
|
71
|
+
private readyPromise?;
|
|
64
72
|
constructor(skillRegistry: SkillRegistryInterface, hiddenOps: HiddenOpRegistry, bundleStore: BundleStore, options: BundleSyncOptions, logger: FrontMcpLogger,
|
|
65
73
|
/**
|
|
66
74
|
* Optional factory that registers each operation as an internal SDK tool.
|
|
@@ -70,6 +78,38 @@ export declare class BundleSyncService {
|
|
|
70
78
|
* tool registry (e.g. unit tests for the sync service in isolation).
|
|
71
79
|
*/
|
|
72
80
|
operationToolFactory?: OperationToolFactory | undefined);
|
|
81
|
+
/**
|
|
82
|
+
* Wired by the plugin's DI factory. Stores the bundle source so {@link
|
|
83
|
+
* ensureReady} can start it and await the first apply lazily, inside the
|
|
84
|
+
* request that needs the catalog. Does NOT start the source — see the context
|
|
85
|
+
* note on {@link bootstrap}.
|
|
86
|
+
*/
|
|
87
|
+
attachSource(source: StartableBundleSource): void;
|
|
88
|
+
/**
|
|
89
|
+
* Ensure the source has started and its FIRST bundle has been applied, so
|
|
90
|
+
* `scope.skills` and the hidden-op registry are populated before a meta-tool
|
|
91
|
+
* (search_skill / load_skill / run_workflow) reads them. Memoized — the
|
|
92
|
+
* underlying start + apply runs at most once per service instance.
|
|
93
|
+
*/
|
|
94
|
+
ensureReady(): Promise<void>;
|
|
95
|
+
/**
|
|
96
|
+
* Start the source and await its FIRST bundle apply.
|
|
97
|
+
*
|
|
98
|
+
* CRITICAL — every promise here (the `apply()`, the `firstApply` latch, and
|
|
99
|
+
* the `onChange` wiring) is created inside the FIRST caller's context, which
|
|
100
|
+
* is a request context (the meta-tools / list-tools hook call `ensureReady()`
|
|
101
|
+
* while handling a request). On a stateless V8-isolate runtime (Cloudflare
|
|
102
|
+
* Workers) this is mandatory two ways over:
|
|
103
|
+
* 1. workerd cancels promise continuations that cross a request boundary, so
|
|
104
|
+
* a `firstApply` created in the scope-init/factory context would never
|
|
105
|
+
* resolve when awaited from a later request — it must be created here.
|
|
106
|
+
* 2. There is no background event loop, so a deferred/fire-and-forget apply
|
|
107
|
+
* (the old `setImmediate(source.start())`) can never complete after the
|
|
108
|
+
* response is sent — the awaiting tool drives it to completion inline.
|
|
109
|
+
* A failed first sync is swallowed (logged) so it degrades to "no skills"
|
|
110
|
+
* instead of hanging or 500-ing every call.
|
|
111
|
+
*/
|
|
112
|
+
private bootstrap;
|
|
73
113
|
/**
|
|
74
114
|
* Validate signature and apply the bundle. Returns a structured result;
|
|
75
115
|
* never throws on validation/registration failure (caller logs + ignores).
|
|
@@ -84,4 +124,5 @@ export declare class BundleSyncService {
|
|
|
84
124
|
private toSkillContent;
|
|
85
125
|
private rebuildHiddenOps;
|
|
86
126
|
}
|
|
127
|
+
export {};
|
|
87
128
|
//# sourceMappingURL=bundle-sync.service.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"bundle-sync.service.d.ts","sourceRoot":"","sources":["../../src/sync/bundle-sync.service.ts"],"names":[],"mappings":"AAEA,OAAO,EASL,KAAK,UAAU,EACf,KAAK,WAAW,EAEhB,KAAK,cAAc,EAEnB,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,KAAK,cAAc,EAAqB,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAEpG,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC;;;;;;;;OAQG;IACH,+BAA+B,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,qBAAa,iBAAiB;
|
|
1
|
+
{"version":3,"file":"bundle-sync.service.d.ts","sourceRoot":"","sources":["../../src/sync/bundle-sync.service.ts"],"names":[],"mappings":"AAEA,OAAO,EASL,KAAK,UAAU,EACf,KAAK,WAAW,EAEhB,KAAK,cAAc,EAEnB,KAAK,wBAAwB,EAC9B,MAAM,2BAA2B,CAAC;AACnC,OAAO,EAAE,KAAK,cAAc,EAAqB,KAAK,sBAAsB,EAAE,MAAM,eAAe,CAAC;AAEpG,OAAO,EAAsB,KAAK,gBAAgB,EAAE,MAAM,gCAAgC,CAAC;AAC3F,OAAO,EAAE,KAAK,YAAY,EAAE,MAAM,0BAA0B,CAAC;AAC7D,OAAO,EAAE,KAAK,oBAAoB,EAAE,MAAM,iCAAiC,CAAC;AAE5E,MAAM,WAAW,iBAAiB;IAChC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,WAAW,EAAE,YAAY,EAAE,CAAC;IAC5B;;;;;;;OAOG;IACH,SAAS,CAAC,EAAE,wBAAwB,CAAC;IACrC;;;;;;;;OAQG;IACH,+BAA+B,EAAE,OAAO,CAAC;CAC1C;AAED,MAAM,WAAW,iBAAiB;IAChC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,IAAI,CAAC,EAAE,UAAU,CAAC;CACnB;AAED;;;;;;;;;;;;GAYG;AACH,8EAA8E;AAC9E,UAAU,qBAAqB;IAC7B,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACvB,QAAQ,CAAC,QAAQ,EAAE,CAAC,MAAM,EAAE,cAAc,KAAK,IAAI,GAAG,MAAM,IAAI,CAAC;CAClE;AAED,qBAAa,iBAAiB;IAQ1B,OAAO,CAAC,QAAQ,CAAC,aAAa;IAC9B,OAAO,CAAC,QAAQ,CAAC,SAAS;IAC1B,OAAO,CAAC,QAAQ,CAAC,WAAW;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB;;;;;;OAMG;IACH,OAAO,CAAC,QAAQ,CAAC,oBAAoB,CAAC;IAnBxC,OAAO,CAAC,yBAAyB,CAA0C;IAE3E,sFAAsF;IACtF,OAAO,CAAC,MAAM,CAAC,CAAwB;IACvC,OAAO,CAAC,YAAY,CAAC,CAAgB;gBAGlB,aAAa,EAAE,sBAAsB,EACrC,SAAS,EAAE,gBAAgB,EAC3B,WAAW,EAAE,WAAW,EACxB,OAAO,EAAE,iBAAiB,EAC1B,MAAM,EAAE,cAAc;IACvC;;;;;;OAMG;IACc,oBAAoB,CAAC,EAAE,oBAAoB,YAAA;IAG9D;;;;;OAKG;IACH,YAAY,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI;IAIjD;;;;;OAKG;IACG,WAAW,IAAI,OAAO,CAAC,IAAI,CAAC;IAMlC;;;;;;;;;;;;;;;;OAgBG;YACW,SAAS;IA+CvB;;;;OAIG;IACG,KAAK,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,iBAAiB,CAAC;IA0O/D;;;;OAIG;IACH,OAAO,CAAC,cAAc;IAatB,OAAO,CAAC,gBAAgB;CAwCzB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"load-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/load-skill.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsD,WAAW,EAAE,MAAM,eAAe,CAAC;AAGhG,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;
|
|
1
|
+
{"version":3,"file":"load-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/load-skill.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAsD,WAAW,EAAE,MAAM,eAAe,CAAC;AAGhG,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;CAgC/D"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"run-workflow.tool.d.ts","sourceRoot":"","sources":["../../src/tools/run-workflow.tool.ts"],"names":[],"mappings":"AAUA,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAOlD,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAkB/B,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,WAAW;IAChD,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"run-workflow.tool.d.ts","sourceRoot":"","sources":["../../src/tools/run-workflow.tool.ts"],"names":[],"mappings":"AAUA,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAOlD,OAAO,EAIL,KAAK,gBAAgB,EACrB,KAAK,iBAAiB,EACvB,MAAM,uBAAuB,CAAC;AAkB/B,MAAM,CAAC,OAAO,OAAO,eAAgB,SAAQ,WAAW;IAChD,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,iBAAiB,CAAC;CA0FnE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"search-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/search-skill.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9D,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;
|
|
1
|
+
{"version":3,"file":"search-skill.tool.d.ts","sourceRoot":"","sources":["../../src/tools/search-skill.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAoB,WAAW,EAAE,MAAM,eAAe,CAAC;AAG9D,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;CAkCnE"}
|