@frontmcp/plugin-codecall 1.7.2 → 1.8.0
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 +143 -0
- package/esm/package.json +5 -5
- package/index.js +143 -0
- package/package.json +5 -5
- package/security/codecall-tool-policy.d.ts +89 -0
- package/security/codecall-tool-policy.d.ts.map +1 -0
- package/security/index.d.ts +1 -0
- package/security/index.d.ts.map +1 -1
- package/security/tool-access-control.service.d.ts +12 -2
- package/security/tool-access-control.service.d.ts.map +1 -1
- package/tools/execute.tool.d.ts +9 -0
- package/tools/execute.tool.d.ts.map +1 -1
- package/tools/invoke.tool.d.ts +2 -0
- package/tools/invoke.tool.d.ts.map +1 -1
package/esm/index.mjs
CHANGED
|
@@ -1882,6 +1882,125 @@ SearchKnowledgeTool = __decorateClass([
|
|
|
1882
1882
|
import { toJSONSchema, z as z6, ZodType } from "@frontmcp/lazy-zod";
|
|
1883
1883
|
import { Tool as Tool4, ToolContext as ToolContext4 } from "@frontmcp/sdk";
|
|
1884
1884
|
|
|
1885
|
+
// plugins/plugin-codecall/src/security/codecall-tool-policy.ts
|
|
1886
|
+
var BLOCKED_NAMESPACE_PATTERNS = Object.freeze([/^system:/, /^internal:/, /^__/]);
|
|
1887
|
+
var ALLOWED = { allowed: true };
|
|
1888
|
+
function deny(reason) {
|
|
1889
|
+
return { allowed: false, reason };
|
|
1890
|
+
}
|
|
1891
|
+
function checkCodeCallToolPolicy(tool, config) {
|
|
1892
|
+
const { name } = tool;
|
|
1893
|
+
const names = [name, ...tool.aliases ?? []].filter((candidate) => !!candidate);
|
|
1894
|
+
if (names.some((candidate) => candidate.startsWith("codecall:"))) {
|
|
1895
|
+
return deny("CodeCall meta-tools are not callable from CodeCall");
|
|
1896
|
+
}
|
|
1897
|
+
for (const candidate of names) {
|
|
1898
|
+
if (BLOCKED_NAMESPACE_PATTERNS.some((pattern) => pattern.test(candidate))) {
|
|
1899
|
+
return deny(`Tool "${candidate}" is in a namespace CodeCall never calls`);
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
if (tool.hideFromDiscovery === true) {
|
|
1903
|
+
return deny(`Tool "${name}" is hidden from discovery`);
|
|
1904
|
+
}
|
|
1905
|
+
const enabled = tool.codecall?.enabledInCodeCall;
|
|
1906
|
+
switch (config.mode) {
|
|
1907
|
+
case "codecall_only":
|
|
1908
|
+
case "metadata_driven":
|
|
1909
|
+
if (enabled === false) {
|
|
1910
|
+
return deny(`Tool "${name}" sets enabledInCodeCall: false`);
|
|
1911
|
+
}
|
|
1912
|
+
break;
|
|
1913
|
+
case "codecall_opt_in":
|
|
1914
|
+
if (enabled !== true) {
|
|
1915
|
+
return deny(`Tool "${name}" has not opted in to CodeCall`);
|
|
1916
|
+
}
|
|
1917
|
+
break;
|
|
1918
|
+
default:
|
|
1919
|
+
return deny(`Unknown CodeCall mode: ${String(config.mode)}`);
|
|
1920
|
+
}
|
|
1921
|
+
if (config.includeTools) {
|
|
1922
|
+
const included = config.includeTools({
|
|
1923
|
+
name,
|
|
1924
|
+
appId: tool.appId,
|
|
1925
|
+
source: tool.codecall?.source,
|
|
1926
|
+
description: tool.description,
|
|
1927
|
+
tags: tool.codecall?.tags ?? tool.tags
|
|
1928
|
+
});
|
|
1929
|
+
if (!included) {
|
|
1930
|
+
return deny(`Tool "${name}" is excluded by the includeTools filter`);
|
|
1931
|
+
}
|
|
1932
|
+
}
|
|
1933
|
+
return ALLOWED;
|
|
1934
|
+
}
|
|
1935
|
+
function denyUnknownTool(name) {
|
|
1936
|
+
return deny(`Tool "${name}" is not available through CodeCall`);
|
|
1937
|
+
}
|
|
1938
|
+
function resolveCodeCallTool(scope, name) {
|
|
1939
|
+
const typedScope = scope;
|
|
1940
|
+
const alias = /[-_]/.test(name) ? name.includes("_") ? name.replace(/_/g, "-") : name.replace(/-/g, "_") : void 0;
|
|
1941
|
+
const candidates = alias ? [name, alias] : [name];
|
|
1942
|
+
const matches = (entry) => candidates.includes(entry.fullName) || candidates.includes(entry.name);
|
|
1943
|
+
const local = typedScope.tools?.getTools(true)?.find(matches);
|
|
1944
|
+
if (local) return local;
|
|
1945
|
+
const registries = typedScope.providers?.getRegistries?.("AppRegistry") ?? [];
|
|
1946
|
+
for (const registry of registries) {
|
|
1947
|
+
for (const app of registry.getApps()) {
|
|
1948
|
+
if (!app.isRemote) continue;
|
|
1949
|
+
const remote = app.tools.getTools(true).find(matches);
|
|
1950
|
+
if (remote) return remote;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
return void 0;
|
|
1954
|
+
}
|
|
1955
|
+
function checkDirectCallPolicy(tool, directCalls) {
|
|
1956
|
+
if (!directCalls) return ALLOWED;
|
|
1957
|
+
if (directCalls.enabled === false) {
|
|
1958
|
+
return deny("Direct tool invocation is disabled");
|
|
1959
|
+
}
|
|
1960
|
+
if (directCalls.allowedTools && !directCalls.allowedTools.includes(tool.name)) {
|
|
1961
|
+
return deny(`Tool "${tool.name}" is not in the direct-call allowlist`);
|
|
1962
|
+
}
|
|
1963
|
+
if (directCalls.filter) {
|
|
1964
|
+
const permitted = directCalls.filter({
|
|
1965
|
+
name: tool.name,
|
|
1966
|
+
appId: tool.appId,
|
|
1967
|
+
source: tool.codecall?.source,
|
|
1968
|
+
tags: tool.codecall?.tags ?? tool.tags
|
|
1969
|
+
});
|
|
1970
|
+
if (!permitted) {
|
|
1971
|
+
return deny(`Tool "${tool.name}" is excluded by the directCalls filter`);
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
return ALLOWED;
|
|
1975
|
+
}
|
|
1976
|
+
function checkCodeCallToolAccess(scope, config, name, options = {}) {
|
|
1977
|
+
const entry = resolveCodeCallTool(scope, name);
|
|
1978
|
+
if (!entry) return denyUnknownTool(name);
|
|
1979
|
+
const metadata = entry.metadata;
|
|
1980
|
+
const owner = entry.owner;
|
|
1981
|
+
const policyTool = {
|
|
1982
|
+
// The qualified name is the subject: `call-tool.flow.ts` dispatches `fullName`, so the
|
|
1983
|
+
// policy has to judge the same string the flow will run.
|
|
1984
|
+
name: entry.fullName || entry.name,
|
|
1985
|
+
aliases: [entry.name, name],
|
|
1986
|
+
appId: owner?.kind === "app" ? owner.id : void 0,
|
|
1987
|
+
description: metadata?.description,
|
|
1988
|
+
tags: metadata?.tags,
|
|
1989
|
+
hideFromDiscovery: metadata?.hideFromDiscovery,
|
|
1990
|
+
codecall: metadata?.codecall
|
|
1991
|
+
};
|
|
1992
|
+
const baseDecision = checkCodeCallToolPolicy(policyTool, {
|
|
1993
|
+
// An absent mode means an unparsed/partial config, not a hostile one: fall back to the
|
|
1994
|
+
// schema's own documented default rather than denying every call. An unrecognised mode
|
|
1995
|
+
// still fails closed inside the policy's switch.
|
|
1996
|
+
mode: config.get("mode") ?? "codecall_only",
|
|
1997
|
+
includeTools: config.get("includeTools")
|
|
1998
|
+
});
|
|
1999
|
+
if (!baseDecision.allowed) return baseDecision;
|
|
2000
|
+
if (!options.directCall) return ALLOWED;
|
|
2001
|
+
return checkDirectCallPolicy(policyTool, config.get("directCalls"));
|
|
2002
|
+
}
|
|
2003
|
+
|
|
1885
2004
|
// plugins/plugin-codecall/src/errors/tool-call.errors.ts
|
|
1886
2005
|
var TOOL_CALL_ERROR_CODES = {
|
|
1887
2006
|
NOT_FOUND: "NOT_FOUND",
|
|
@@ -2667,6 +2786,17 @@ function getErrorCode(error) {
|
|
|
2667
2786
|
return TOOL_CALL_ERROR_CODES.EXECUTION;
|
|
2668
2787
|
}
|
|
2669
2788
|
var ExecuteTool = class extends ToolContext5 {
|
|
2789
|
+
/**
|
|
2790
|
+
* Resolve the server-side CodeCall policy for a tool name (GHSA-6w3j-82v5-6qrr).
|
|
2791
|
+
*
|
|
2792
|
+
* Shares its decision with discovery and with `codecall:invoke`, so the three cannot
|
|
2793
|
+
* diverge. A name matching no registered tool is denied rather than passed through —
|
|
2794
|
+
* otherwise it would skip every metadata-driven check and reach `tools:call-tool`
|
|
2795
|
+
* unexamined.
|
|
2796
|
+
*/
|
|
2797
|
+
checkToolPolicy(name) {
|
|
2798
|
+
return checkCodeCallToolAccess(this.scope, this.get(CodeCallConfig), name);
|
|
2799
|
+
}
|
|
2670
2800
|
async execute(input) {
|
|
2671
2801
|
const { script, allowedTools } = input;
|
|
2672
2802
|
const allowedToolSet = allowedTools ? new Set(allowedTools) : null;
|
|
@@ -2674,6 +2804,14 @@ var ExecuteTool = class extends ToolContext5 {
|
|
|
2674
2804
|
callTool: async (name, toolInput, options) => {
|
|
2675
2805
|
const throwOnError = options?.throwOnError !== false;
|
|
2676
2806
|
assertNotSelfReference(name);
|
|
2807
|
+
const decision = this.checkToolPolicy(name);
|
|
2808
|
+
if (!decision.allowed) {
|
|
2809
|
+
const error = createToolCallError(TOOL_CALL_ERROR_CODES.ACCESS_DENIED, name, decision.reason);
|
|
2810
|
+
if (throwOnError) {
|
|
2811
|
+
throw error;
|
|
2812
|
+
}
|
|
2813
|
+
return { success: false, error };
|
|
2814
|
+
}
|
|
2677
2815
|
if (allowedToolSet && !allowedToolSet.has(name)) {
|
|
2678
2816
|
const error = createToolCallError(TOOL_CALL_ERROR_CODES.ACCESS_DENIED, name);
|
|
2679
2817
|
if (throwOnError) {
|
|
@@ -2718,6 +2856,7 @@ var ExecuteTool = class extends ToolContext5 {
|
|
|
2718
2856
|
getTool: (name) => {
|
|
2719
2857
|
try {
|
|
2720
2858
|
if (isBlockedSelfReference(name)) return void 0;
|
|
2859
|
+
if (!this.checkToolPolicy(name).allowed) return void 0;
|
|
2721
2860
|
if (allowedToolSet && !allowedToolSet.has(name)) return void 0;
|
|
2722
2861
|
const tools = this.scope.tools.getTools(true);
|
|
2723
2862
|
const tool = tools.find((t) => t.name === name || t.fullName === name);
|
|
@@ -2892,6 +3031,10 @@ var InvokeTool = class extends ToolContext6 {
|
|
|
2892
3031
|
`Tool "${toolName}" cannot be invoked directly. CodeCall tools are internal and not accessible via codecall:invoke.`
|
|
2893
3032
|
);
|
|
2894
3033
|
}
|
|
3034
|
+
const decision = checkCodeCallToolAccess(this.scope, this.get(CodeCallConfig), toolName, { directCall: true });
|
|
3035
|
+
if (!decision.allowed) {
|
|
3036
|
+
return buildErrorResult(`Tool "${toolName}" is not available. Use codecall:search to discover available tools.`);
|
|
3037
|
+
}
|
|
2895
3038
|
const request = {
|
|
2896
3039
|
method: "tools/call",
|
|
2897
3040
|
params: {
|
package/esm/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-codecall",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "CodeCall plugin for FrontMCP - AgentScript-based meta-tools for orchestrating MCP tools",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@enclave-vm/core": "^2.15.2",
|
|
52
|
-
"@frontmcp/protocol": "1.
|
|
53
|
-
"@frontmcp/sdk": "1.
|
|
52
|
+
"@frontmcp/protocol": "1.8.0",
|
|
53
|
+
"@frontmcp/sdk": "1.8.0",
|
|
54
54
|
"vectoriadb": "^2.3.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@frontmcp/lazy-zod": "1.
|
|
58
|
-
"@frontmcp/plugin-cache": "1.
|
|
57
|
+
"@frontmcp/lazy-zod": "1.8.0",
|
|
58
|
+
"@frontmcp/plugin-cache": "1.8.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"reflect-metadata": "^0.2.2"
|
package/index.js
CHANGED
|
@@ -1919,6 +1919,125 @@ SearchKnowledgeTool = __decorateClass([
|
|
|
1919
1919
|
var import_lazy_zod6 = require("@frontmcp/lazy-zod");
|
|
1920
1920
|
var import_sdk6 = require("@frontmcp/sdk");
|
|
1921
1921
|
|
|
1922
|
+
// plugins/plugin-codecall/src/security/codecall-tool-policy.ts
|
|
1923
|
+
var BLOCKED_NAMESPACE_PATTERNS = Object.freeze([/^system:/, /^internal:/, /^__/]);
|
|
1924
|
+
var ALLOWED = { allowed: true };
|
|
1925
|
+
function deny(reason) {
|
|
1926
|
+
return { allowed: false, reason };
|
|
1927
|
+
}
|
|
1928
|
+
function checkCodeCallToolPolicy(tool, config) {
|
|
1929
|
+
const { name } = tool;
|
|
1930
|
+
const names = [name, ...tool.aliases ?? []].filter((candidate) => !!candidate);
|
|
1931
|
+
if (names.some((candidate) => candidate.startsWith("codecall:"))) {
|
|
1932
|
+
return deny("CodeCall meta-tools are not callable from CodeCall");
|
|
1933
|
+
}
|
|
1934
|
+
for (const candidate of names) {
|
|
1935
|
+
if (BLOCKED_NAMESPACE_PATTERNS.some((pattern) => pattern.test(candidate))) {
|
|
1936
|
+
return deny(`Tool "${candidate}" is in a namespace CodeCall never calls`);
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
if (tool.hideFromDiscovery === true) {
|
|
1940
|
+
return deny(`Tool "${name}" is hidden from discovery`);
|
|
1941
|
+
}
|
|
1942
|
+
const enabled = tool.codecall?.enabledInCodeCall;
|
|
1943
|
+
switch (config.mode) {
|
|
1944
|
+
case "codecall_only":
|
|
1945
|
+
case "metadata_driven":
|
|
1946
|
+
if (enabled === false) {
|
|
1947
|
+
return deny(`Tool "${name}" sets enabledInCodeCall: false`);
|
|
1948
|
+
}
|
|
1949
|
+
break;
|
|
1950
|
+
case "codecall_opt_in":
|
|
1951
|
+
if (enabled !== true) {
|
|
1952
|
+
return deny(`Tool "${name}" has not opted in to CodeCall`);
|
|
1953
|
+
}
|
|
1954
|
+
break;
|
|
1955
|
+
default:
|
|
1956
|
+
return deny(`Unknown CodeCall mode: ${String(config.mode)}`);
|
|
1957
|
+
}
|
|
1958
|
+
if (config.includeTools) {
|
|
1959
|
+
const included = config.includeTools({
|
|
1960
|
+
name,
|
|
1961
|
+
appId: tool.appId,
|
|
1962
|
+
source: tool.codecall?.source,
|
|
1963
|
+
description: tool.description,
|
|
1964
|
+
tags: tool.codecall?.tags ?? tool.tags
|
|
1965
|
+
});
|
|
1966
|
+
if (!included) {
|
|
1967
|
+
return deny(`Tool "${name}" is excluded by the includeTools filter`);
|
|
1968
|
+
}
|
|
1969
|
+
}
|
|
1970
|
+
return ALLOWED;
|
|
1971
|
+
}
|
|
1972
|
+
function denyUnknownTool(name) {
|
|
1973
|
+
return deny(`Tool "${name}" is not available through CodeCall`);
|
|
1974
|
+
}
|
|
1975
|
+
function resolveCodeCallTool(scope, name) {
|
|
1976
|
+
const typedScope = scope;
|
|
1977
|
+
const alias = /[-_]/.test(name) ? name.includes("_") ? name.replace(/_/g, "-") : name.replace(/-/g, "_") : void 0;
|
|
1978
|
+
const candidates = alias ? [name, alias] : [name];
|
|
1979
|
+
const matches = (entry) => candidates.includes(entry.fullName) || candidates.includes(entry.name);
|
|
1980
|
+
const local = typedScope.tools?.getTools(true)?.find(matches);
|
|
1981
|
+
if (local) return local;
|
|
1982
|
+
const registries = typedScope.providers?.getRegistries?.("AppRegistry") ?? [];
|
|
1983
|
+
for (const registry of registries) {
|
|
1984
|
+
for (const app of registry.getApps()) {
|
|
1985
|
+
if (!app.isRemote) continue;
|
|
1986
|
+
const remote = app.tools.getTools(true).find(matches);
|
|
1987
|
+
if (remote) return remote;
|
|
1988
|
+
}
|
|
1989
|
+
}
|
|
1990
|
+
return void 0;
|
|
1991
|
+
}
|
|
1992
|
+
function checkDirectCallPolicy(tool, directCalls) {
|
|
1993
|
+
if (!directCalls) return ALLOWED;
|
|
1994
|
+
if (directCalls.enabled === false) {
|
|
1995
|
+
return deny("Direct tool invocation is disabled");
|
|
1996
|
+
}
|
|
1997
|
+
if (directCalls.allowedTools && !directCalls.allowedTools.includes(tool.name)) {
|
|
1998
|
+
return deny(`Tool "${tool.name}" is not in the direct-call allowlist`);
|
|
1999
|
+
}
|
|
2000
|
+
if (directCalls.filter) {
|
|
2001
|
+
const permitted = directCalls.filter({
|
|
2002
|
+
name: tool.name,
|
|
2003
|
+
appId: tool.appId,
|
|
2004
|
+
source: tool.codecall?.source,
|
|
2005
|
+
tags: tool.codecall?.tags ?? tool.tags
|
|
2006
|
+
});
|
|
2007
|
+
if (!permitted) {
|
|
2008
|
+
return deny(`Tool "${tool.name}" is excluded by the directCalls filter`);
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
return ALLOWED;
|
|
2012
|
+
}
|
|
2013
|
+
function checkCodeCallToolAccess(scope, config, name, options = {}) {
|
|
2014
|
+
const entry = resolveCodeCallTool(scope, name);
|
|
2015
|
+
if (!entry) return denyUnknownTool(name);
|
|
2016
|
+
const metadata = entry.metadata;
|
|
2017
|
+
const owner = entry.owner;
|
|
2018
|
+
const policyTool = {
|
|
2019
|
+
// The qualified name is the subject: `call-tool.flow.ts` dispatches `fullName`, so the
|
|
2020
|
+
// policy has to judge the same string the flow will run.
|
|
2021
|
+
name: entry.fullName || entry.name,
|
|
2022
|
+
aliases: [entry.name, name],
|
|
2023
|
+
appId: owner?.kind === "app" ? owner.id : void 0,
|
|
2024
|
+
description: metadata?.description,
|
|
2025
|
+
tags: metadata?.tags,
|
|
2026
|
+
hideFromDiscovery: metadata?.hideFromDiscovery,
|
|
2027
|
+
codecall: metadata?.codecall
|
|
2028
|
+
};
|
|
2029
|
+
const baseDecision = checkCodeCallToolPolicy(policyTool, {
|
|
2030
|
+
// An absent mode means an unparsed/partial config, not a hostile one: fall back to the
|
|
2031
|
+
// schema's own documented default rather than denying every call. An unrecognised mode
|
|
2032
|
+
// still fails closed inside the policy's switch.
|
|
2033
|
+
mode: config.get("mode") ?? "codecall_only",
|
|
2034
|
+
includeTools: config.get("includeTools")
|
|
2035
|
+
});
|
|
2036
|
+
if (!baseDecision.allowed) return baseDecision;
|
|
2037
|
+
if (!options.directCall) return ALLOWED;
|
|
2038
|
+
return checkDirectCallPolicy(policyTool, config.get("directCalls"));
|
|
2039
|
+
}
|
|
2040
|
+
|
|
1922
2041
|
// plugins/plugin-codecall/src/errors/tool-call.errors.ts
|
|
1923
2042
|
var TOOL_CALL_ERROR_CODES = {
|
|
1924
2043
|
NOT_FOUND: "NOT_FOUND",
|
|
@@ -2704,6 +2823,17 @@ function getErrorCode(error) {
|
|
|
2704
2823
|
return TOOL_CALL_ERROR_CODES.EXECUTION;
|
|
2705
2824
|
}
|
|
2706
2825
|
var ExecuteTool = class extends import_sdk7.ToolContext {
|
|
2826
|
+
/**
|
|
2827
|
+
* Resolve the server-side CodeCall policy for a tool name (GHSA-6w3j-82v5-6qrr).
|
|
2828
|
+
*
|
|
2829
|
+
* Shares its decision with discovery and with `codecall:invoke`, so the three cannot
|
|
2830
|
+
* diverge. A name matching no registered tool is denied rather than passed through —
|
|
2831
|
+
* otherwise it would skip every metadata-driven check and reach `tools:call-tool`
|
|
2832
|
+
* unexamined.
|
|
2833
|
+
*/
|
|
2834
|
+
checkToolPolicy(name) {
|
|
2835
|
+
return checkCodeCallToolAccess(this.scope, this.get(CodeCallConfig), name);
|
|
2836
|
+
}
|
|
2707
2837
|
async execute(input) {
|
|
2708
2838
|
const { script, allowedTools } = input;
|
|
2709
2839
|
const allowedToolSet = allowedTools ? new Set(allowedTools) : null;
|
|
@@ -2711,6 +2841,14 @@ var ExecuteTool = class extends import_sdk7.ToolContext {
|
|
|
2711
2841
|
callTool: async (name, toolInput, options) => {
|
|
2712
2842
|
const throwOnError = options?.throwOnError !== false;
|
|
2713
2843
|
assertNotSelfReference(name);
|
|
2844
|
+
const decision = this.checkToolPolicy(name);
|
|
2845
|
+
if (!decision.allowed) {
|
|
2846
|
+
const error = createToolCallError(TOOL_CALL_ERROR_CODES.ACCESS_DENIED, name, decision.reason);
|
|
2847
|
+
if (throwOnError) {
|
|
2848
|
+
throw error;
|
|
2849
|
+
}
|
|
2850
|
+
return { success: false, error };
|
|
2851
|
+
}
|
|
2714
2852
|
if (allowedToolSet && !allowedToolSet.has(name)) {
|
|
2715
2853
|
const error = createToolCallError(TOOL_CALL_ERROR_CODES.ACCESS_DENIED, name);
|
|
2716
2854
|
if (throwOnError) {
|
|
@@ -2755,6 +2893,7 @@ var ExecuteTool = class extends import_sdk7.ToolContext {
|
|
|
2755
2893
|
getTool: (name) => {
|
|
2756
2894
|
try {
|
|
2757
2895
|
if (isBlockedSelfReference(name)) return void 0;
|
|
2896
|
+
if (!this.checkToolPolicy(name).allowed) return void 0;
|
|
2758
2897
|
if (allowedToolSet && !allowedToolSet.has(name)) return void 0;
|
|
2759
2898
|
const tools = this.scope.tools.getTools(true);
|
|
2760
2899
|
const tool = tools.find((t) => t.name === name || t.fullName === name);
|
|
@@ -2929,6 +3068,10 @@ var InvokeTool = class extends import_sdk8.ToolContext {
|
|
|
2929
3068
|
`Tool "${toolName}" cannot be invoked directly. CodeCall tools are internal and not accessible via codecall:invoke.`
|
|
2930
3069
|
);
|
|
2931
3070
|
}
|
|
3071
|
+
const decision = checkCodeCallToolAccess(this.scope, this.get(CodeCallConfig), toolName, { directCall: true });
|
|
3072
|
+
if (!decision.allowed) {
|
|
3073
|
+
return buildErrorResult(`Tool "${toolName}" is not available. Use codecall:search to discover available tools.`);
|
|
3074
|
+
}
|
|
2932
3075
|
const request = {
|
|
2933
3076
|
method: "tools/call",
|
|
2934
3077
|
params: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frontmcp/plugin-codecall",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.0",
|
|
4
4
|
"description": "CodeCall plugin for FrontMCP - AgentScript-based meta-tools for orchestrating MCP tools",
|
|
5
5
|
"author": "AgentFront <info@agentfront.dev>",
|
|
6
6
|
"license": "Apache-2.0",
|
|
@@ -49,13 +49,13 @@
|
|
|
49
49
|
},
|
|
50
50
|
"dependencies": {
|
|
51
51
|
"@enclave-vm/core": "^2.15.2",
|
|
52
|
-
"@frontmcp/protocol": "1.
|
|
53
|
-
"@frontmcp/sdk": "1.
|
|
52
|
+
"@frontmcp/protocol": "1.8.0",
|
|
53
|
+
"@frontmcp/sdk": "1.8.0",
|
|
54
54
|
"vectoriadb": "^2.3.2"
|
|
55
55
|
},
|
|
56
56
|
"peerDependencies": {
|
|
57
|
-
"@frontmcp/lazy-zod": "1.
|
|
58
|
-
"@frontmcp/plugin-cache": "1.
|
|
57
|
+
"@frontmcp/lazy-zod": "1.8.0",
|
|
58
|
+
"@frontmcp/plugin-cache": "1.8.0"
|
|
59
59
|
},
|
|
60
60
|
"devDependencies": {
|
|
61
61
|
"reflect-metadata": "^0.2.2"
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import type { CodeCallMode, CodeCallToolMetadata, DirectCallsFilterFn, IncludeToolsFilterFn } from '../codecall.types';
|
|
2
|
+
/** The subset of a ToolEntry this decision needs, so callers need not pass the whole entry. */
|
|
3
|
+
export interface CodeCallPolicyTool {
|
|
4
|
+
/** Canonical, most-qualified name — `fullName` where the entry has one. */
|
|
5
|
+
name: string;
|
|
6
|
+
/**
|
|
7
|
+
* Every other name this tool answers to, including the name the caller asked for.
|
|
8
|
+
*
|
|
9
|
+
* The namespace rules run over all of them. A tool can carry a bare `name` and a qualified
|
|
10
|
+
* `fullName` (`wipeConfig` / `system:wipeConfig`), and the flow dispatches the qualified
|
|
11
|
+
* one — so judging a single spelling lets the other one through.
|
|
12
|
+
*/
|
|
13
|
+
aliases?: string[];
|
|
14
|
+
appId?: string;
|
|
15
|
+
description?: string;
|
|
16
|
+
tags?: string[];
|
|
17
|
+
hideFromDiscovery?: boolean;
|
|
18
|
+
codecall?: CodeCallToolMetadata;
|
|
19
|
+
}
|
|
20
|
+
export interface CodeCallPolicyConfig {
|
|
21
|
+
mode: CodeCallMode;
|
|
22
|
+
includeTools?: IncludeToolsFilterFn;
|
|
23
|
+
}
|
|
24
|
+
/** The `directCalls` options, which narrow `codecall:invoke` beyond the base policy. */
|
|
25
|
+
export interface CodeCallDirectCallsConfig {
|
|
26
|
+
enabled: boolean;
|
|
27
|
+
allowedTools?: string[];
|
|
28
|
+
filter?: DirectCallsFilterFn;
|
|
29
|
+
}
|
|
30
|
+
export type CodeCallPolicyDecision = {
|
|
31
|
+
allowed: true;
|
|
32
|
+
} | {
|
|
33
|
+
allowed: false;
|
|
34
|
+
reason: string;
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Decide whether CodeCall may reach `tool`.
|
|
38
|
+
*
|
|
39
|
+
* Discovery and execution both call this. A tool the policy denies must be absent from
|
|
40
|
+
* search results AND rejected at `callTool` — hiding it from search is not authorization.
|
|
41
|
+
*/
|
|
42
|
+
export declare function checkCodeCallToolPolicy(tool: CodeCallPolicyTool, config: CodeCallPolicyConfig): CodeCallPolicyDecision;
|
|
43
|
+
/**
|
|
44
|
+
* A tool the policy cannot evaluate is denied.
|
|
45
|
+
*
|
|
46
|
+
* `callTool` receives a bare string, so a name matching no resolvable tool would otherwise
|
|
47
|
+
* skip every metadata-driven check above and fall straight through to `tools:call-tool`.
|
|
48
|
+
*/
|
|
49
|
+
export declare function denyUnknownTool(name: string): CodeCallPolicyDecision;
|
|
50
|
+
interface ResolvableEntry {
|
|
51
|
+
name: string;
|
|
52
|
+
fullName: string;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Resolve a tool name exactly as `call-tool.flow.ts` `findTool` does.
|
|
56
|
+
*
|
|
57
|
+
* MUST mirror that stage. The policy has to evaluate the same entry the flow will execute:
|
|
58
|
+
* resolve more narrowly and legitimate remote-app tools get denied; resolve more loosely and
|
|
59
|
+
* a tool reachable only through the flow's fallback escapes the policy entirely. The flow
|
|
60
|
+
* applies three steps, and so does this —
|
|
61
|
+
* 1. the scope registry,
|
|
62
|
+
* 2. a hyphen <-> underscore alias (issue #408),
|
|
63
|
+
* 3. remote app registries, for tools whose subscription has not yet propagated.
|
|
64
|
+
*/
|
|
65
|
+
export declare function resolveCodeCallTool<T extends ResolvableEntry>(scope: unknown, name: string): T | undefined;
|
|
66
|
+
/**
|
|
67
|
+
* Apply the `directCalls` options to a `codecall:invoke` request.
|
|
68
|
+
*
|
|
69
|
+
* These options were declared in the plugin schema and read by nothing, so `codecall:invoke`
|
|
70
|
+
* proxied the entire registry regardless of how an operator configured it. They narrow the
|
|
71
|
+
* base policy; they never widen it.
|
|
72
|
+
*/
|
|
73
|
+
export declare function checkDirectCallPolicy(tool: CodeCallPolicyTool, directCalls: CodeCallDirectCallsConfig | undefined): CodeCallPolicyDecision;
|
|
74
|
+
interface PolicyConfigReader {
|
|
75
|
+
get(key: string): unknown;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Resolve a tool name and decide whether CodeCall may reach it.
|
|
79
|
+
*
|
|
80
|
+
* The one entry point for both meta-tools: `codecall:execute` calls it for every `callTool`
|
|
81
|
+
* and `getTool`, `codecall:invoke` for its single target with `directCall: true`. Sharing it
|
|
82
|
+
* is the point — the advisory existed because execution and discovery each had their own
|
|
83
|
+
* answer.
|
|
84
|
+
*/
|
|
85
|
+
export declare function checkCodeCallToolAccess(scope: unknown, config: PolicyConfigReader, name: string, options?: {
|
|
86
|
+
directCall?: boolean;
|
|
87
|
+
}): CodeCallPolicyDecision;
|
|
88
|
+
export {};
|
|
89
|
+
//# sourceMappingURL=codecall-tool-policy.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"codecall-tool-policy.d.ts","sourceRoot":"","sources":["../../src/security/codecall-tool-policy.ts"],"names":[],"mappings":"AAYA,OAAO,KAAK,EAAE,YAAY,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAC;AAKvH,+FAA+F;AAC/F,MAAM,WAAW,kBAAkB;IACjC,2EAA2E;IAC3E,IAAI,EAAE,MAAM,CAAC;IACb;;;;;;OAMG;IACH,OAAO,CAAC,EAAE,MAAM,EAAE,CAAC;IACnB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;IAChB,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,EAAE,oBAAoB,CAAC;CACjC;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,EAAE,YAAY,CAAC;IACnB,YAAY,CAAC,EAAE,oBAAoB,CAAC;CACrC;AAED,wFAAwF;AACxF,MAAM,WAAW,yBAAyB;IACxC,OAAO,EAAE,OAAO,CAAC;IACjB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,MAAM,CAAC,EAAE,mBAAmB,CAAC;CAC9B;AAED,MAAM,MAAM,sBAAsB,GAAG;IAAE,OAAO,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC;AAQ5F;;;;;GAKG;AACH,wBAAgB,uBAAuB,CACrC,IAAI,EAAE,kBAAkB,EACxB,MAAM,EAAE,oBAAoB,GAC3B,sBAAsB,CAwDxB;AAED;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,GAAG,sBAAsB,CAEpE;AAED,UAAU,eAAe;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;CAClB;AAWD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,SAAS,eAAe,EAAE,KAAK,EAAE,OAAO,EAAE,IAAI,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAwB1G;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,EACxB,WAAW,EAAE,yBAAyB,GAAG,SAAS,GACjD,sBAAsB,CAyBxB;AAED,UAAU,kBAAkB;IAC1B,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC;CAC3B;AAED;;;;;;;GAOG;AACH,wBAAgB,uBAAuB,CACrC,KAAK,EAAE,OAAO,EACd,MAAM,EAAE,kBAAkB,EAC1B,IAAI,EAAE,MAAM,EACZ,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,OAAO,CAAA;CAAO,GACrC,sBAAsB,CAmCxB"}
|
package/security/index.d.ts
CHANGED
package/security/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAEA,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/security/index.ts"],"names":[],"mappings":"AAEA,cAAc,wBAAwB,CAAC;AACvC,cAAc,wBAAwB,CAAC;AACvC,cAAc,+BAA+B,CAAC"}
|
|
@@ -58,8 +58,18 @@ export interface ToolAccessDecision {
|
|
|
58
58
|
/**
|
|
59
59
|
* Tool Access Control Service
|
|
60
60
|
*
|
|
61
|
-
*
|
|
62
|
-
*
|
|
61
|
+
* NOT ON THE ENFORCEMENT PATH. Nothing constructs this service: the plugin declares
|
|
62
|
+
* `providers: []`, no `ToolAccessPolicy` can be supplied through the plugin options, and
|
|
63
|
+
* neither `codecall:execute` nor `codecall:invoke` consults it. Its unit tests pass by
|
|
64
|
+
* constructing it directly, which is exactly how GHSA-6w3j-82v5-6qrr stayed invisible — a
|
|
65
|
+
* green suite for a control that never ran.
|
|
66
|
+
*
|
|
67
|
+
* The policy CodeCall actually enforces lives in `codecall-tool-policy.ts`
|
|
68
|
+
* (`checkCodeCallToolAccess`), called by both meta-tools. This class is kept only so the
|
|
69
|
+
* exported types do not disappear from the package's public surface in a security patch;
|
|
70
|
+
* wiring it in, or removing it, is a follow-up with its own BC note.
|
|
71
|
+
*
|
|
72
|
+
* The richer model it offers, unused today:
|
|
63
73
|
*
|
|
64
74
|
* 1. Self-reference blocking (handled separately in self-reference-guard.ts)
|
|
65
75
|
* 2. Default blacklist (always enforced)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tool-access-control.service.d.ts","sourceRoot":"","sources":["../../src/security/tool-access-control.service.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"tool-access-control.service.d.ts","sourceRoot":"","sources":["../../src/security/tool-access-control.service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAGnD;;;;;GAKG;AACH,MAAM,MAAM,cAAc,GAAG,WAAW,GAAG,WAAW,GAAG,SAAS,CAAC;AAEnE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,EAAE,CAAC;IAErB;;OAEG;IACH,QAAQ,CAAC,EAAE;QACT,KAAK,CAAC,EAAE,MAAM,EAAE,CAAC;QACjB,IAAI,CAAC,EAAE,MAAM,EAAE,CAAC;KACjB,CAAC;IAEF;;;OAGG;IACH,SAAS,CAAC,EAAE,CAAC,OAAO,EAAE,iBAAiB,KAAK,OAAO,CAAC,kBAAkB,CAAC,CAAC;CACzE;AAED;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,QAAQ,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAeD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,qBAIa,wBAAwB;IACnC,OAAO,CAAC,QAAQ,CAAC,MAAM,CAAmB;IAC1C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAc;IAC3C,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAW;IACzC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAW;gBAE5B,MAAM,CAAC,EAAE,gBAAgB;IAWrC;;;;;;OAMG;IACG,WAAW,CAAC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,OAAO,CAAC,iBAAiB,CAAC,GAAG,OAAO,CAAC,kBAAkB,CAAC;YAsCxF,kBAAkB;YAqBlB,kBAAkB;YAelB,gBAAgB;IAS9B;;OAEG;IACH,OAAO,CAAC,OAAO;IAmBf;;;;;OAKG;IACH,OAAO,CAAC,WAAW;IAenB;;OAEG;IACH,SAAS,IAAI,QAAQ,CAAC,gBAAgB,CAAC;CAGxC;AAED,eAAe,wBAAwB,CAAC"}
|
package/tools/execute.tool.d.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { ToolContext } from '@frontmcp/sdk';
|
|
2
2
|
import { type CodeCallExecuteResult, type ExecuteToolInput } from './execute.schema';
|
|
3
3
|
export default class ExecuteTool extends ToolContext {
|
|
4
|
+
/**
|
|
5
|
+
* Resolve the server-side CodeCall policy for a tool name (GHSA-6w3j-82v5-6qrr).
|
|
6
|
+
*
|
|
7
|
+
* Shares its decision with discovery and with `codecall:invoke`, so the three cannot
|
|
8
|
+
* diverge. A name matching no registered tool is denied rather than passed through —
|
|
9
|
+
* otherwise it would skip every metadata-driven check and reach `tools:call-tool`
|
|
10
|
+
* unexamined.
|
|
11
|
+
*/
|
|
12
|
+
private checkToolPolicy;
|
|
4
13
|
execute(input: ExecuteToolInput): Promise<CodeCallExecuteResult>;
|
|
5
14
|
}
|
|
6
15
|
//# sourceMappingURL=execute.tool.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"execute.tool.d.ts","sourceRoot":"","sources":["../../src/tools/execute.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;
|
|
1
|
+
{"version":3,"file":"execute.tool.d.ts","sourceRoot":"","sources":["../../src/tools/execute.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAmBlD,OAAO,EAIL,KAAK,qBAAqB,EAC1B,KAAK,gBAAgB,EACtB,MAAM,kBAAkB,CAAC;AAyC1B,MAAM,CAAC,OAAO,OAAO,WAAY,SAAQ,WAAW;IAClD;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;IAIjB,OAAO,CAAC,KAAK,EAAE,gBAAgB,GAAG,OAAO,CAAC,qBAAqB,CAAC;CA4QvE"}
|
package/tools/invoke.tool.d.ts
CHANGED
|
@@ -6,6 +6,8 @@ import { InvokeToolInput, InvokeToolOutput } from './invoke.schema';
|
|
|
6
6
|
*
|
|
7
7
|
* Security Considerations:
|
|
8
8
|
* - Self-reference blocking: Cannot invoke codecall:* tools
|
|
9
|
+
* - Shares the CodeCall access policy with `codecall:execute`, so a tool withheld from
|
|
10
|
+
* scripts is not reachable by naming it here instead (GHSA-6w3j-82v5-6qrr)
|
|
9
11
|
* - All middleware (auth, PII, rate limiting) applies via normal tool execution
|
|
10
12
|
*/
|
|
11
13
|
export default class InvokeTool extends ToolContext {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"invoke.tool.d.ts","sourceRoot":"","sources":["../../src/tools/invoke.tool.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"invoke.tool.d.ts","sourceRoot":"","sources":["../../src/tools/invoke.tool.ts"],"names":[],"mappings":"AAEA,OAAO,EAAQ,WAAW,EAAE,MAAM,eAAe,CAAC;AAIlD,OAAO,EAEL,eAAe,EAEf,gBAAgB,EAEjB,MAAM,iBAAiB,CAAC;AAYzB;;;;;;;;;GASG;AAeH,MAAM,CAAC,OAAO,OAAO,UAAW,SAAQ,WAAW;IAC3C,OAAO,CAAC,KAAK,EAAE,eAAe,GAAG,OAAO,CAAC,gBAAgB,CAAC;CA4CjE"}
|