@botbotgo/agent-harness 0.0.161 → 0.0.163

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/README.md CHANGED
@@ -578,6 +578,7 @@ Example workspaces:
578
578
 
579
579
  Workspace-local tool modules in `resources/tools/` should be exported with `tool({...})`.
580
580
  Any other local module shape is not supported, and unsupported shapes are rejected at load time.
581
+ When a local function tool declares its schema in the module, runtime governance treats that module-defined schema as the source of truth; duplicating it into YAML `inputSchema.ref` is optional rather than required for schema-bound metadata.
581
582
  When local tools use Zod-authored schemas, keep the workspace or isolated `resources` package on `zod@^4` so raw-shape validators and runtime parsing stay on one supported major version.
582
583
 
583
584
  Default wiring guidance:
@@ -911,3 +912,4 @@ ACP transport notes:
911
912
  - `serveAcpStdio(runtime)` exposes newline-delimited JSON-RPC over stdio for local IDE, CLI, or subprocess clients.
912
913
  - `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
913
914
  - `exportRunPackage(...)` and `exportSessionPackage(...)` package stable runtime records, transcript, approvals, events, and artifacts for operator tooling without reaching into persistence internals.
915
+ - `runtime/default.governance.remoteMcp` can now deny or allow specific MCP servers, raise approval requirements by transport, and stamp transport-based risk tiers into runtime governance bundles.
package/README.zh.md CHANGED
@@ -542,6 +542,7 @@ await stop(runtime);
542
542
 
543
543
  `resources/tools/` 下的工作区本地工具模块应统一用 `tool({...})` 导出。
544
544
  不支持历史/兼容写法,任何不带该导出形式的工具模块都会在工作区加载时被拒绝。
545
+ 本地 function tool 如果在模块里声明了 schema,runtime governance 会直接把该模块 schema 视为事实来源;不需要为了 schema-bound 元数据再额外复制一份 YAML `inputSchema.ref`。
545
546
  若本地工具使用 Zod schema,请让工作区或隔离的 `resources` 包统一使用 `zod@^4`,避免 raw shape validator 与 runtime 解析落在不同 major 版本上。
546
547
 
547
548
  主要有三层配置:
@@ -870,3 +871,4 @@ ACP transport 说明:
870
871
  - `serveAcpStdio(runtime)` 提供基于 stdio 的 newline-delimited JSON-RPC,适合本地 IDE、CLI 或子进程客户端。
871
872
  - `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程 operator surface 或独立控制面接入。
872
873
  - `exportRunPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events 和 artifacts 打包给 operator tooling,而不必直接访问 persistence 内部实现。
874
+ - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。
@@ -102,6 +102,8 @@ export type RuntimeGovernanceToolPolicy = {
102
102
  toolId: string;
103
103
  toolType: string;
104
104
  category: "local" | "backend" | "mcp" | "provider-native";
105
+ mcpServerRef?: string;
106
+ mcpTransport?: string;
105
107
  risk: RuntimeGovernanceRiskLevel;
106
108
  requiresApproval: boolean;
107
109
  approvalPolicy: "explicit-hitl" | "runtime-default" | "none";
@@ -83,6 +83,7 @@ export type ParsedToolObject = {
83
83
  config?: Record<string, unknown>;
84
84
  subprocess?: boolean;
85
85
  inputSchemaRef?: string;
86
+ hasModuleSchema?: boolean;
86
87
  embeddingModelRef?: string;
87
88
  backendOperation?: string;
88
89
  mcpRef?: string;
@@ -132,6 +133,7 @@ export type CompiledTool = {
132
133
  config?: Record<string, unknown>;
133
134
  subprocess?: boolean;
134
135
  inputSchemaRef?: string;
136
+ hasModuleSchema?: boolean;
135
137
  embeddingModelRef?: string;
136
138
  backendOperation?: string;
137
139
  mcpRef?: string;
@@ -124,6 +124,7 @@ registerToolKind({
124
124
  config: tool.config,
125
125
  subprocess: tool.subprocess,
126
126
  inputSchemaRef: tool.inputSchemaRef,
127
+ hasModuleSchema: tool.hasModuleSchema,
127
128
  embeddingModelRef: tool.embeddingModelRef,
128
129
  bundleRefs: [],
129
130
  hitl: tool.hitl
@@ -158,6 +159,7 @@ registerToolKind({
158
159
  config: tool.config,
159
160
  subprocess: tool.subprocess,
160
161
  inputSchemaRef: tool.inputSchemaRef,
162
+ hasModuleSchema: tool.hasModuleSchema,
161
163
  embeddingModelRef: tool.embeddingModelRef,
162
164
  backendOperation: tool.backendOperation,
163
165
  bundleRefs: [],
@@ -193,6 +195,7 @@ registerToolKind({
193
195
  config: tool.config,
194
196
  subprocess: tool.subprocess,
195
197
  inputSchemaRef: tool.inputSchemaRef,
198
+ hasModuleSchema: tool.hasModuleSchema,
196
199
  embeddingModelRef: tool.embeddingModelRef,
197
200
  mcpRef: tool.mcpRef,
198
201
  bundleRefs: [],
@@ -234,6 +237,7 @@ registerToolKind({
234
237
  config: tool.config,
235
238
  subprocess: tool.subprocess,
236
239
  inputSchemaRef: tool.inputSchemaRef,
240
+ hasModuleSchema: tool.hasModuleSchema,
237
241
  embeddingModelRef: tool.embeddingModelRef,
238
242
  bundleRefs: [],
239
243
  hitl: tool.hitl
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.160";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.162";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.160";
1
+ export const AGENT_HARNESS_VERSION = "0.0.162";
@@ -1,6 +1,7 @@
1
1
  import { resolveBindingTimeout, resolveProviderRetryPolicy, resolveStreamIdleTimeout } from "../adapter/resilience.js";
2
2
  import { toolRequiresRuntimeApproval } from "../adapter/tool/tool-hitl.js";
3
3
  import { getBindingPrimaryTools } from "../support/compiled-binding.js";
4
+ import { compiledToolHasInputSchema } from "./tool-schema.js";
4
5
  export function getWorkspaceBinding(workspace, agentId) {
5
6
  return workspace.bindings.get(agentId);
6
7
  }
@@ -29,7 +30,7 @@ export function projectBindingToolExecutionPolicy(binding) {
29
30
  toolId: tool.id,
30
31
  name: tool.name,
31
32
  retryable: tool.retryable === true,
32
- hasInputSchema: typeof tool.inputSchemaRef === "string" && tool.inputSchemaRef.trim().length > 0,
33
+ hasInputSchema: compiledToolHasInputSchema(tool),
33
34
  requiresApproval: toolRequiresRuntimeApproval(tool),
34
35
  }));
35
36
  const retryableToolCount = projectedTools.filter((tool) => tool.retryable).length;
@@ -1,5 +1,6 @@
1
1
  import { getBindingPrimaryTools } from "../../support/compiled-binding.js";
2
2
  import { toolRequiresRuntimeApproval } from "../../adapter/tool/tool-hitl.js";
3
+ import { compiledToolHasInputSchema } from "../tool-schema.js";
3
4
  const WRITE_LIKE_PATTERN = /\b(write|edit|delete|create|update|append|insert|push|commit|publish|send|post|apply|merge|sync|upload|save)\b/i;
4
5
  function inputHints(binding, tool) {
5
6
  const hints = new Set();
@@ -57,6 +58,25 @@ function readRisk(value) {
57
58
  function readApprovalPolicy(value) {
58
59
  return value === "explicit-hitl" || value === "runtime-default" || value === "none" ? value : undefined;
59
60
  }
61
+ function normalizeServerRef(value) {
62
+ if (typeof value !== "string" || value.trim().length === 0) {
63
+ return undefined;
64
+ }
65
+ const trimmed = value.trim();
66
+ return trimmed.startsWith("mcp/") ? trimmed : `mcp/${trimmed}`;
67
+ }
68
+ function readRemoteMcpMetadata(tool) {
69
+ const config = asObject(tool.config);
70
+ const mcpReference = asObject(config?.mcp);
71
+ const inlineServer = asObject(config?.mcpServer);
72
+ const transport = typeof inlineServer?.transport === "string" && inlineServer.transport.trim().length > 0
73
+ ? inlineServer.transport.trim()
74
+ : undefined;
75
+ return {
76
+ ...(normalizeServerRef(mcpReference?.serverRef) ? { serverRef: normalizeServerRef(mcpReference?.serverRef) } : {}),
77
+ ...(transport ? { transport } : {}),
78
+ };
79
+ }
60
80
  function matchesToolPolicy(rule, policy) {
61
81
  const match = asObject(rule.match) ?? rule;
62
82
  const toolName = typeof match.toolName === "string" ? match.toolName.trim() : undefined;
@@ -102,14 +122,49 @@ function applyGovernanceOverrides(binding, policies) {
102
122
  return merged;
103
123
  });
104
124
  }
125
+ function applyRemoteMcpGovernance(binding, policies) {
126
+ const governance = asObject(binding.harnessRuntime.governance);
127
+ const remoteMcp = asObject(governance?.remoteMcp);
128
+ if (!remoteMcp) {
129
+ return policies;
130
+ }
131
+ const requireApprovalTransports = new Set(readStringArray(remoteMcp.requireApprovalTransports));
132
+ const riskByTransport = asObject(remoteMcp.riskByTransport);
133
+ const inputRiskHintsByTransport = asObject(remoteMcp.inputRiskHintsByTransport);
134
+ return policies.map((policy) => {
135
+ if (policy.category !== "mcp") {
136
+ return policy;
137
+ }
138
+ const merged = { ...policy };
139
+ const transport = merged.mcpTransport;
140
+ if (transport && requireApprovalTransports.has(transport)) {
141
+ merged.requiresApproval = true;
142
+ if (merged.approvalPolicy === "none") {
143
+ merged.approvalPolicy = "runtime-default";
144
+ }
145
+ }
146
+ const transportRisk = transport ? readRisk(riskByTransport?.[transport]) : undefined;
147
+ if (transportRisk) {
148
+ merged.risk = transportRisk;
149
+ }
150
+ const transportHints = transport ? readStringArray(inputRiskHintsByTransport?.[transport]) : [];
151
+ if (transportHints.length > 0) {
152
+ merged.inputRiskHints = Array.from(new Set([...merged.inputRiskHints, ...transportHints]));
153
+ }
154
+ return merged;
155
+ });
156
+ }
105
157
  export function buildRuntimeGovernanceBundles(binding) {
106
- const toolPolicies = applyGovernanceOverrides(binding, getBindingPrimaryTools(binding).map((tool) => {
158
+ const toolPolicies = applyGovernanceOverrides(binding, applyRemoteMcpGovernance(binding, getBindingPrimaryTools(binding).map((tool) => {
107
159
  const requiresApproval = toolRequiresRuntimeApproval(tool);
160
+ const remoteMcp = readRemoteMcpMetadata(tool);
108
161
  return {
109
162
  toolName: tool.name,
110
163
  toolId: tool.id,
111
164
  toolType: tool.type,
112
165
  category: toCategory(tool.type),
166
+ ...(remoteMcp.serverRef ? { mcpServerRef: remoteMcp.serverRef } : {}),
167
+ ...(remoteMcp.transport ? { mcpTransport: remoteMcp.transport } : {}),
113
168
  risk: classifyRisk({
114
169
  toolType: tool.type,
115
170
  requiresApproval,
@@ -119,10 +174,10 @@ export function buildRuntimeGovernanceBundles(binding) {
119
174
  }),
120
175
  requiresApproval,
121
176
  approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : requiresApproval ? "runtime-default" : "none",
122
- hasInputSchema: typeof tool.inputSchemaRef === "string" && tool.inputSchemaRef.trim().length > 0,
177
+ hasInputSchema: compiledToolHasInputSchema(tool),
123
178
  inputRiskHints: inputHints(binding, tool),
124
179
  };
125
- }));
180
+ })));
126
181
  if (toolPolicies.length === 0) {
127
182
  return [];
128
183
  }
@@ -12,6 +12,9 @@ export class PolicyEngine {
12
12
  const governance = typeof binding.harnessRuntime.governance === "object" && binding.harnessRuntime.governance
13
13
  ? binding.harnessRuntime.governance
14
14
  : undefined;
15
+ const remoteMcp = typeof governance?.remoteMcp === "object" && governance.remoteMcp
16
+ ? governance.remoteMcp
17
+ : undefined;
15
18
  const denyConfig = typeof governance?.deny === "object" && governance.deny
16
19
  ? governance.deny
17
20
  : undefined;
@@ -38,6 +41,72 @@ export class PolicyEngine {
38
41
  reasons.push(`runtime governance denied tool access: ${blocked.map((tool) => tool.name).join(", ")}`);
39
42
  }
40
43
  }
44
+ if (remoteMcp) {
45
+ const normalizeServerRef = (value) => {
46
+ if (typeof value !== "string" || value.trim().length === 0) {
47
+ return undefined;
48
+ }
49
+ const trimmed = value.trim();
50
+ return trimmed.startsWith("mcp/") ? trimmed : `mcp/${trimmed}`;
51
+ };
52
+ const allowServerRefs = new Set(Array.isArray(remoteMcp.allowServerRefs)
53
+ ? remoteMcp.allowServerRefs
54
+ .map((item) => normalizeServerRef(item))
55
+ .filter((item) => Boolean(item))
56
+ : []);
57
+ const denyServerRefs = new Set(Array.isArray(remoteMcp.denyServerRefs)
58
+ ? remoteMcp.denyServerRefs
59
+ .map((item) => normalizeServerRef(item))
60
+ .filter((item) => Boolean(item))
61
+ : []);
62
+ const denyTransports = new Set(Array.isArray(remoteMcp.denyTransports)
63
+ ? remoteMcp.denyTransports.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
64
+ : []);
65
+ const tools = binding.execution?.params?.tools ?? binding.langchainAgentParams?.tools ?? binding.deepAgentParams?.tools ?? [];
66
+ const deniedRemoteTools = tools.flatMap((tool) => {
67
+ if (tool.type !== "mcp") {
68
+ return [];
69
+ }
70
+ const config = typeof tool.config === "object" && tool.config && !Array.isArray(tool.config)
71
+ ? tool.config
72
+ : undefined;
73
+ const mcpRef = typeof config?.mcp === "object" && config.mcp && !Array.isArray(config.mcp)
74
+ ? config.mcp
75
+ : undefined;
76
+ const inlineMcpServer = typeof config?.mcpServer === "object" && config.mcpServer && !Array.isArray(config.mcpServer)
77
+ ? config.mcpServer
78
+ : undefined;
79
+ const serverRef = normalizeServerRef(mcpRef?.serverRef);
80
+ const transport = typeof inlineMcpServer?.transport === "string" && inlineMcpServer.transport.trim().length > 0
81
+ ? inlineMcpServer.transport.trim()
82
+ : undefined;
83
+ const serverDenied = serverRef ? denyServerRefs.has(serverRef) || (allowServerRefs.size > 0 && !allowServerRefs.has(serverRef)) : false;
84
+ const transportDenied = transport ? denyTransports.has(transport) : false;
85
+ return serverDenied || transportDenied
86
+ ? [{
87
+ toolName: tool.name,
88
+ ...(serverRef ? { serverRef } : {}),
89
+ ...(transport ? { transport } : {}),
90
+ }]
91
+ : [];
92
+ });
93
+ if (deniedRemoteTools.length > 0) {
94
+ allowed = false;
95
+ const details = deniedRemoteTools.map((tool) => {
96
+ if (tool.serverRef && tool.transport) {
97
+ return `${tool.toolName} (${tool.serverRef}, ${tool.transport})`;
98
+ }
99
+ if (tool.serverRef) {
100
+ return `${tool.toolName} (${tool.serverRef})`;
101
+ }
102
+ if (tool.transport) {
103
+ return `${tool.toolName} (${tool.transport})`;
104
+ }
105
+ return tool.toolName;
106
+ });
107
+ reasons.push(`runtime governance denied remote MCP access: ${details.join(", ")}`);
108
+ }
109
+ }
41
110
  for (const evaluator of getPolicyEvaluators()) {
42
111
  const decision = evaluator.evaluate(binding);
43
112
  if (!decision) {
@@ -0,0 +1,2 @@
1
+ import type { CompiledTool } from "../../contracts/types.js";
2
+ export declare function compiledToolHasInputSchema(tool: Pick<CompiledTool, "inputSchemaRef" | "hasModuleSchema">): boolean;
@@ -0,0 +1,3 @@
1
+ export function compiledToolHasInputSchema(tool) {
2
+ return (typeof tool.inputSchemaRef === "string" && tool.inputSchemaRef.trim().length > 0) || tool.hasModuleSchema === true;
3
+ }
@@ -4,6 +4,7 @@ export type LoadedToolModule = {
4
4
  implementationName: string;
5
5
  invoke: (input: unknown, context?: Record<string, unknown>) => Promise<unknown> | unknown;
6
6
  schema: ReturnType<typeof normalizeToolSchema>;
7
+ hasModuleSchema: boolean;
7
8
  description: string;
8
9
  retryable?: boolean;
9
10
  memory?: {
@@ -20,6 +20,7 @@ function loadToolObjectDefinition(imported, exportName) {
20
20
  implementationName: definition.name?.trim() || exportName,
21
21
  invoke: definition.invoke,
22
22
  schema: normalizeToolSchema(definition.schema),
23
+ hasModuleSchema: true,
23
24
  description: definition.description.trim(),
24
25
  retryable: definition.retryable === true,
25
26
  ...(definition.memory ? { memory: { ...definition.memory } } : {}),
@@ -4,7 +4,7 @@ import { readdir, readFile } from "node:fs/promises";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { resolveIsolatedResourceModulePath } from "../resource/isolation.js";
6
6
  import { isExternalSourceLocator, resolveResourcePackageRoot } from "../resource/sources.js";
7
- import { discoverToolModuleDefinitions, isSupportedToolModulePath } from "../tool-modules.js";
7
+ import { discoverToolModuleDefinitions, isSupportedToolModulePath, loadToolModuleDefinition } from "../tool-modules.js";
8
8
  import { fileExists } from "../utils/fs.js";
9
9
  import { readNamedYamlItems, readYamlItems, } from "./yaml-object-reader.js";
10
10
  export { normalizeYamlItem, readYamlItems } from "./yaml-object-reader.js";
@@ -633,12 +633,22 @@ async function readModuleToolItems(root) {
633
633
  if (inferredType === "function" && !discoveredPath) {
634
634
  throw new Error(`Module tool ${workspaceObject.id} must define implementation.path or provide index.mjs|index.js|index.cjs`);
635
635
  }
636
+ const implementationName = typeof normalizedItem.implementationName === "string" && normalizedItem.implementationName.trim().length > 0
637
+ ? normalizedItem.implementationName.trim()
638
+ : typeof implementation?.export === "string" && implementation.export.trim().length > 0
639
+ ? implementation.export.trim()
640
+ : workspaceObject.id;
641
+ const schemaMetadata = inferredType === "function" && discoveredPath
642
+ ? await readModuleToolSchemaMetadata({
643
+ sourcePath: discoveredPath,
644
+ implementationName,
645
+ })
646
+ : { hasModuleSchema: false };
636
647
  records.push({
637
648
  item: {
638
649
  ...normalizedItem,
639
- ...(typeof implementation?.export === "string" && !normalizedItem.implementationName
640
- ? { implementationName: implementation.export }
641
- : {}),
650
+ ...(typeof implementation?.export === "string" && !normalizedItem.implementationName ? { implementationName } : {}),
651
+ ...(schemaMetadata.hasModuleSchema ? { hasModuleSchema: true } : {}),
642
652
  },
643
653
  sourcePath: discoveredPath ?? sourcePath,
644
654
  });
@@ -646,6 +656,32 @@ async function readModuleToolItems(root) {
646
656
  }
647
657
  return records;
648
658
  }
659
+ function findToolPackageRoot(startPath) {
660
+ let current = path.dirname(startPath);
661
+ for (;;) {
662
+ const packageJsonPath = path.join(current, "package.json");
663
+ if (existsSync(packageJsonPath)) {
664
+ return current;
665
+ }
666
+ const parent = path.dirname(current);
667
+ if (parent === current) {
668
+ return path.dirname(startPath);
669
+ }
670
+ current = parent;
671
+ }
672
+ }
673
+ async function readModuleToolSchemaMetadata(input) {
674
+ if (!isSupportedToolModulePath(input.sourcePath)) {
675
+ return { hasModuleSchema: false };
676
+ }
677
+ const packageRoot = findToolPackageRoot(input.sourcePath);
678
+ const isolatedSourcePath = await resolveIsolatedResourceModulePath(packageRoot, input.sourcePath);
679
+ const imported = await import(pathToFileURL(isolatedSourcePath).href);
680
+ const definition = loadToolModuleDefinition(imported, input.implementationName);
681
+ return {
682
+ hasModuleSchema: definition.hasModuleSchema,
683
+ };
684
+ }
649
685
  async function loadModuleObjectsForRoot(root, mergedObjects) {
650
686
  for (const { item, sourcePath } of await readModuleToolItems(root)) {
651
687
  const workspaceObject = parseWorkspaceObject(item, sourcePath);
@@ -728,6 +764,7 @@ export async function readToolModuleItems(root) {
728
764
  name: definition.implementationName,
729
765
  description: definition.description,
730
766
  implementationName: definition.implementationName,
767
+ hasModuleSchema: definition.hasModuleSchema,
731
768
  ...(definition.retryable !== undefined ? { retryable: definition.retryable } : {}),
732
769
  ...(definition.memory ? { config: { memory: definition.memory } } : {}),
733
770
  },
@@ -269,6 +269,7 @@ export function parseToolObject(object) {
269
269
  : undefined),
270
270
  subprocess: value.subprocess === true,
271
271
  inputSchemaRef: typeof asObject(value.inputSchema)?.ref === "string" ? String(asObject(value.inputSchema)?.ref) : undefined,
272
+ hasModuleSchema: value.hasModuleSchema === true,
272
273
  embeddingModelRef: typeof value.embeddingModelRef === "string"
273
274
  ? value.embeddingModelRef
274
275
  : typeof value.embeddingModel === "string"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.161",
3
+ "version": "0.0.163",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",