@botbotgo/agent-harness 0.0.269 → 0.0.270

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
@@ -147,6 +147,7 @@ The public API spans a full product runtime—persistent records, memory and evi
147
147
  - **Runtime memory and evidence:** `memorize`, `recall`, `listMemories`, memory policy hooks, `listArtifacts`, `getArtifact`, `exportEvaluationBundle`, `replayEvaluationBundle`, and request/session evidence export helpers.
148
148
  - **Protocol and transport surfaces:** `createAcpServer`, `createAcpStdioClient`, `serveAcpStdio`, `serveAcpHttp`, `serveA2aHttp`, `serveAgUiHttp`, and `createRuntimeMcpServer` / `serveRuntimeMcpOverStdio`.
149
149
  - **Governed workspace runtime:** YAML-owned routing, concurrency, maintenance, MCP policy, runtime governance bundles, and approval defaults for sensitive memory or write-like MCP side effects.
150
+ - **Policy-shaped approvals:** governed tools can stay on manual review, auto-approve, or auto-reject / deny-and-continue modes while the runtime keeps one inspectable governance decision surface.
150
151
 
151
152
  If you integrate external clients, treat `deepagents-acp` as the primary protocol direction: clients connect through that surface while `agent-harness` keeps persistence, recovery, approvals, and operator control on the runtime side.
152
153
  Keep the standard stack split explicit: MCP connects agents to resources and tools, A2A bridges task exchange between agent platforms, ACP is the client-to-runtime boundary, AG-UI is the UI event surface, and runtime MCP exposes the operator control plane.
@@ -1059,12 +1060,12 @@ ACP transport notes:
1059
1060
  - `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
1060
1061
  - ACP transport validation now covers the reference-client core flow: capability discovery, request submit, session lookup, request lookup, invalid-JSON handling, notification calls without response ids, stdio JSON-RPC, and HTTP plus SSE runtime notifications.
1061
1062
  - For the thinnest editor or CLI starter, begin with `agent-harness acp serve --workspace . --transport stdio` and mirror the `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` wire shape. Applications that want an in-process reference client can use `createAcpStdioClient(...)` to issue JSON-RPC requests and route runtime notifications without hand-rolling line parsing.
1062
- - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetExtendedAgentCard`, and task push-notification config methods onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, sets `capabilities.streaming = true` plus `capabilities.pushNotifications = true`, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams an initial `{ task }` snapshot plus later `{ statusUpdate }` payloads over SSE for v1 streaming methods, and can send best-effort webhook task snapshots for configured push notification receivers.
1063
+ - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and A2A v1.0 PascalCase methods such as `SendMessage`, `SendStreamingMessage`, `GetTask`, `ListTasks`, `CancelTask`, `SubscribeToTask`, `GetAgentCard`, `GetExtendedAgentCard`, and task push-notification config methods onto the existing session/request runtime surface. The bridge now advertises both `1.0` and `0.3` JSON-RPC interfaces, answers `HEAD` / `OPTIONS` discovery on the agent-card path, sets supported-version discovery headers, validates `A2A-Version`, records `A2A-Extensions` into runtime invocation metadata, publishes `TASK_STATE_*` statuses plus the `{ task }` `SendMessage` wrapper, streams an initial `{ task }` snapshot plus later `{ statusUpdate }` payloads over SSE for v1 streaming methods, and can send best-effort webhook task snapshots for configured push notification receivers.
1063
1064
  - `serveAgUiHttp(runtime)` exposes an AG-UI-compatible HTTP SSE bridge that projects runtime lifecycle, text output, upstream thinking, step progress, and tool calls onto `RUN_*`, `TEXT_MESSAGE_*`, `THINKING_TEXT_MESSAGE_*`, `STEP_*`, and `TOOL_CALL_*` events for UI clients.
1064
1065
  - `createRuntimeMcpServer(runtime)` and `serveRuntimeMcpOverStdio(runtime)` expose the persisted runtime control surface itself as MCP tools, including sessions, requests, approvals, artifacts, events, and package export helpers.
1065
1066
  - `listRequestEvents(...)`, `listRequestTraceItems(...)`, and `exportRequestPackage(...)` are the request-first inspection helpers.
1066
1067
  - `exportRequestPackage(...)` and `exportSessionPackage(...)` package stable runtime records, transcript, approvals, events, artifacts, and governance evidence for operator tooling without reaching into persistence internals.
1067
- - `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. MCP server catalogs can also declare trust tier, access mode, tenant scope, approval policy, prompt-injection risk, and OAuth scope metadata so governance bundles capture why one remote tool is treated as high-risk.
1068
+ - `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. MCP server catalogs can also declare trust tier, access mode, tenant scope, approval policy, prompt-injection risk, and OAuth scope metadata so governance bundles capture why one remote tool is treated as high-risk. Tool policy overrides can also set `decisionMode: manual | auto-approve | auto-reject | deny-and-continue` so operator evidence and execution behavior stay aligned.
1068
1069
  - Protocol responsibilities stay split on purpose: ACP is the primary editor/client runtime boundary, A2A is the streaming-capable agent-platform bridge with polling compatibility, AG-UI is the UI event surface, and runtime MCP is the operator-facing control plane exported as MCP tools.
1069
1070
  - `runtime/default.observability.tracing` can now describe exporter metadata such as OTLP endpoints and propagation mode, so frozen runtime snapshots keep trace-correlation plus operator-visible export context without exposing backend-private span internals.
1070
1071
  - `agent-harness runtime overview`, `agent-harness runtime health`, `agent-harness runtime approvals list|watch`, `agent-harness runtime runs list|tail`, and `agent-harness runtime export request|session` provide a thin operator CLI over persisted runtime health, queue pressure, governance risk, approval queues, active run state, and audit-ready evidence packages.
package/README.zh.md CHANGED
@@ -143,6 +143,7 @@ try {
143
143
  - **运行时 memory 与证据能力:** `memorize`、`recall`、`listMemories`、memory policy hooks、`listArtifacts`、`getArtifact`、`exportEvaluationBundle`、`replayEvaluationBundle`,以及 request / session 级证据导出辅助函数。
144
144
  - **协议与传输层:** `createAcpServer`、`createAcpStdioClient`、`serveAcpStdio`、`serveAcpHttp`、`serveA2aHttp`、`serveAgUiHttp`,以及 `createRuntimeMcpServer` / `serveRuntimeMcpOverStdio`。
145
145
  - **受治理的工作区运行时:** 由 YAML 持有的路由、并发、维护、MCP 策略、runtime governance bundles,以及针对敏感 memory 或写类 MCP 副作用的默认审批门槛。
146
+ - **策略化审批:** 受治理工具现在既可以走人工审批,也可以走 `auto-approve`、`auto-reject` 或 `deny-and-continue`,同时继续保留统一可检查的治理决策面。
146
147
 
147
148
  若你的产品需要对接外部客户端,可从本节理解边界:`deepagents-acp` 是主要的外部协议接入方向;持久化、恢复、审批与运行控制仍由 `agent-harness` 在运行时侧承担。
148
149
  协议栈分工也要保持明确:MCP 连接资源和工具,A2A 承接 agent 平台之间的 task exchange,ACP 是 client-to-runtime 边界,AG-UI 是 UI 事件面,runtime MCP 则把 operator control plane 以 MCP tools 暴露出去。
@@ -1016,12 +1017,12 @@ ACP transport 说明:
1016
1017
  - `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程界面或独立控制面接入。
1017
1018
  - ACP transport 现已覆盖核心参考客户端流程验证:capability discovery、request submit、session lookup、request lookup、invalid JSON 处理、无 id notification 不返回响应,以及 stdio JSON-RPC 与 HTTP + SSE runtime notifications。
1018
1019
  - 如果要从最薄的一层 editor / CLI starter 开始,优先用 `agent-harness acp serve --workspace . --transport stdio`,并直接参考 `examples/protocol-hello-world/app/acp-stdio-hello-world.mjs` 的 wire shape。需要在应用内使用 reference client 时,可直接用 `createAcpStdioClient(...)` 发起 JSON-RPC 请求并分流 runtime notifications,避免每个 sidecar 自己重写 line parsing。
1019
- - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetExtendedAgentCard` 与 task push-notification config 这类 A2A v1.0 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、把 `capabilities.streaming` `capabilities.pushNotifications` 都设为 `true`、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper、在 v1 streaming 方法上先通过 SSE 输出 `{ task }` 初始快照,再输出 `{ statusUpdate }` 增量状态,并可向已配置的 webhook receiver 发送 best-effort task push notifications。
1020
+ - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`SendStreamingMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask`、`GetAgentCard`、`GetExtendedAgentCard` 与 task push-notification config 这类 A2A v1.0 方法,并统一映射到现有 session/request 运行记录。bridge 现在会同时声明 `1.0` 与 `0.3` 两个 JSON-RPC interface、在 agent card 路径上响应 `HEAD` / `OPTIONS` discovery、写出支持版本的 discovery headers、校验 `A2A-Version`、把 `A2A-Extensions` 记录进 runtime invocation metadata、发布 `TASK_STATE_*` 状态与 `SendMessage` 的 `{ task }` wrapper、在 v1 streaming 方法上先通过 SSE 输出 `{ task }` 初始快照,再输出 `{ statusUpdate }` 增量状态,并可向已配置的 webhook receiver 发送 best-effort task push notifications。
1020
1021
  - `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
1021
1022
  - `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
1022
1023
  - `listRequestEvents(...)`、`listRequestTraceItems(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
1023
1024
  - `exportRequestPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events、artifacts 与 governance evidence 一起打包给管理工具,而不必直接访问 persistence 内部实现。
1024
- - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。MCP server catalog 也可以声明 trust tier、access mode、tenant scope、approval policy、prompt-injection risk 与 OAuth scope 元数据,让治理快照能解释为什么某个远端工具被视为高风险。
1025
+ - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。MCP server catalog 也可以声明 trust tier、access mode、tenant scope、approval policy、prompt-injection risk 与 OAuth scope 元数据,让治理快照能解释为什么某个远端工具被视为高风险。tool policy override 也可以声明 `decisionMode: manual | auto-approve | auto-reject | deny-and-continue`,让治理快照与实际执行路径保持一致。
1025
1026
  - 协议分工要继续保持清晰:ACP 是 editor / client 的主运行时边界,A2A 是支持 streaming 且兼容轮询的 agent-platform bridge,AG-UI 是 UI 事件面,runtime MCP 是以 MCP tools 暴露的 operator control plane。
1026
1027
  - `runtime/default.observability.tracing` 现在可描述 OTLP endpoint 和 propagation mode 这类 exporter 元数据,使冻结的 runtime snapshot 在保留 trace correlation 的同时,也能保留有用的导出上下文,而不暴露 backend 私有 span 细节。
1027
1028
  - `agent-harness runtime overview`、`agent-harness runtime health`、`agent-harness runtime approvals list|watch`、`agent-harness runtime runs list|tail` 与 `agent-harness runtime export request|session` 提供了一层轻量 CLI,可直接查看 runtime health、queue pressure、governance risk、审批队列、运行状态与可审计证据包。
@@ -119,6 +119,8 @@ export type RuntimeGovernanceDiagnostics = {
119
119
  runsWithGovernance: number;
120
120
  highRiskTools: number;
121
121
  approvalRequiredTools: number;
122
+ autoApprovedTools: number;
123
+ autoRejectedTools: number;
122
124
  untrustedMcpTools: number;
123
125
  crossTenantTools: number;
124
126
  writeAccessMcpTools: number;
@@ -169,6 +171,7 @@ export type RuntimeSnapshotTracing = {
169
171
  metadata?: Record<string, unknown>;
170
172
  };
171
173
  export type RuntimeGovernanceRiskLevel = "low" | "medium" | "high";
174
+ export type RuntimeGovernanceDecisionMode = "none" | "manual" | "auto-approve" | "auto-reject" | "deny-and-continue";
172
175
  export type RuntimeGovernanceToolPolicy = {
173
176
  toolName: string;
174
177
  toolId: string;
@@ -185,6 +188,7 @@ export type RuntimeGovernanceToolPolicy = {
185
188
  risk: RuntimeGovernanceRiskLevel;
186
189
  requiresApproval: boolean;
187
190
  approvalPolicy: "explicit-hitl" | "runtime-default" | "none";
191
+ decisionMode: RuntimeGovernanceDecisionMode;
188
192
  hasInputSchema: boolean;
189
193
  inputRiskHints: string[];
190
194
  };
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.268";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.269";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.268";
1
+ export const AGENT_HARNESS_VERSION = "0.0.269";
@@ -11,6 +11,10 @@ function writeJson(response, statusCode, payload) {
11
11
  response.setHeader("content-type", "application/json; charset=utf-8");
12
12
  response.end(JSON.stringify(payload));
13
13
  }
14
+ function writeA2aDiscoveryHeaders(response) {
15
+ response.setHeader("A2A-Version", "1.0");
16
+ response.setHeader("A2A-Supported-Versions", SUPPORTED_A2A_VERSIONS.join(", "));
17
+ }
14
18
  function writeSseHeaders(response) {
15
19
  response.statusCode = 200;
16
20
  response.setHeader("content-type", "text/event-stream; charset=utf-8");
@@ -913,6 +917,19 @@ export async function serveA2aOverHttp(runtime, options = {}) {
913
917
  });
914
918
  return;
915
919
  }
920
+ writeA2aDiscoveryHeaders(response);
921
+ if (request.method === "OPTIONS" && (requestUrl.pathname === rpcPath || requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
922
+ response.statusCode = 204;
923
+ response.setHeader("Allow", "GET,HEAD,POST,OPTIONS");
924
+ response.end();
925
+ return;
926
+ }
927
+ if (request.method === "HEAD" && (requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
928
+ response.statusCode = 200;
929
+ response.setHeader("content-type", "application/json; charset=utf-8");
930
+ response.end();
931
+ return;
932
+ }
916
933
  if (request.method === "GET" && (requestUrl.pathname === agentCardPath || requestUrl.pathname === "/.well-known/agent.json")) {
917
934
  writeJson(response, 200, buildAgentCard(runtime, buildCardOptions()));
918
935
  return;
@@ -950,6 +967,10 @@ export async function serveA2aOverHttp(runtime, options = {}) {
950
967
  writeJson(response, 200, toSuccess(payload.id ?? null, toSendMessageResult(payload.method, task)));
951
968
  return;
952
969
  }
970
+ if (payload.method === "GetAgentCard") {
971
+ writeJson(response, 200, toSuccess(payload.id ?? null, buildAgentCard(runtime, buildCardOptions())));
972
+ return;
973
+ }
953
974
  if (payload.method === "message/stream" || payload.method === "SendStreamingMessage") {
954
975
  if (!acceptsSse(request)) {
955
976
  writeJson(response, 406, toError(payload.id ?? null, -32004, "A2A streaming requires `Accept: text/event-stream`."));
@@ -1,7 +1,9 @@
1
1
  import type { CompiledTool } from "../../../contracts/types.js";
2
2
  import type { CompiledAgentBinding } from "../../../contracts/types.js";
3
+ import type { RuntimeGovernanceDecisionMode } from "../../../contracts/types.js";
3
4
  type InterruptFn = <I = unknown, R = unknown>(value: I) => R;
5
+ export declare function resolveToolApprovalDecisionMode(compiledTool: CompiledTool, binding?: CompiledAgentBinding): RuntimeGovernanceDecisionMode;
4
6
  export declare function toolRequiresRuntimeApproval(compiledTool: CompiledTool): boolean;
5
- export declare function wrapToolForHumanInTheLoop<T>(resolvedTool: T, compiledTool: CompiledTool, interruptFn?: InterruptFn): T;
7
+ export declare function wrapToolForHumanInTheLoop<T>(resolvedTool: T, compiledTool: CompiledTool, binding?: CompiledAgentBinding, interruptFn?: InterruptFn): T;
6
8
  export declare function wrapToolForExecution<T>(resolvedTool: T, compiledTool: CompiledTool, binding?: CompiledAgentBinding, interruptFn?: InterruptFn): T;
7
9
  export {};
@@ -14,6 +14,16 @@ function asRecord(value) {
14
14
  function asString(value) {
15
15
  return typeof value === "string" && value.trim().length > 0 ? value.trim() : undefined;
16
16
  }
17
+ function readApprovalDecisionMode(value) {
18
+ if (value === "none" ||
19
+ value === "manual" ||
20
+ value === "auto-approve" ||
21
+ value === "auto-reject" ||
22
+ value === "deny-and-continue") {
23
+ return value;
24
+ }
25
+ return undefined;
26
+ }
17
27
  function readApprovalOverride(compiledTool) {
18
28
  if (compiledTool.config?.requireApproval === true) {
19
29
  return true;
@@ -59,11 +69,62 @@ function requiresApprovalForHighRiskMcpWrite(compiledTool) {
59
69
  .join(" ");
60
70
  return WRITE_LIKE_REMOTE_TOOL_PATTERN.test(targetText);
61
71
  }
62
- export function toolRequiresRuntimeApproval(compiledTool) {
72
+ function matchesGovernanceToolPolicy(rule, compiledTool) {
73
+ const match = asRecord(rule.match) ?? rule;
74
+ const toolName = asString(match.toolName);
75
+ const category = asString(match.category);
76
+ const toolType = asString(match.toolType);
77
+ const compiledCategory = compiledTool.type === "mcp"
78
+ ? "mcp"
79
+ : compiledTool.type === "backend"
80
+ ? "backend"
81
+ : compiledTool.type === "provider"
82
+ ? "provider-native"
83
+ : "local";
84
+ return (!toolName || toolName === compiledTool.name)
85
+ && (!category || category === compiledCategory)
86
+ && (!toolType || toolType === compiledTool.type);
87
+ }
88
+ function readToolApprovalModeFromGovernance(compiledTool, binding) {
89
+ const governance = asRecord(binding?.harnessRuntime.governance);
90
+ const overrides = Array.isArray(governance?.toolPolicies) ? governance.toolPolicies : [];
91
+ for (const rule of overrides) {
92
+ const typedRule = asRecord(rule);
93
+ if (!typedRule || !matchesGovernanceToolPolicy(typedRule, compiledTool)) {
94
+ continue;
95
+ }
96
+ const overrideMode = readApprovalDecisionMode(typedRule.decisionMode ?? typedRule.approvalMode);
97
+ if (overrideMode) {
98
+ return overrideMode;
99
+ }
100
+ }
101
+ return undefined;
102
+ }
103
+ function readToolApprovalModeFromConfig(compiledTool) {
104
+ const approval = asRecord(compiledTool.config?.approval);
105
+ return readApprovalDecisionMode(approval?.mode ??
106
+ approval?.decisionMode ??
107
+ compiledTool.config?.approvalMode);
108
+ }
109
+ export function resolveToolApprovalDecisionMode(compiledTool, binding) {
63
110
  if (compiledTool.hitl?.enabled === true) {
64
- return true;
111
+ return "manual";
112
+ }
113
+ const governanceMode = readToolApprovalModeFromGovernance(compiledTool, binding);
114
+ if (governanceMode) {
115
+ return governanceMode;
116
+ }
117
+ const configuredMode = readToolApprovalModeFromConfig(compiledTool);
118
+ if (configuredMode) {
119
+ return configuredMode;
65
120
  }
66
- return readApprovalOverride(compiledTool) || requiresApprovalForSensitiveMemoryWrite(compiledTool) || requiresApprovalForHighRiskMcpWrite(compiledTool);
121
+ if (readApprovalOverride(compiledTool) || requiresApprovalForSensitiveMemoryWrite(compiledTool) || requiresApprovalForHighRiskMcpWrite(compiledTool)) {
122
+ return "manual";
123
+ }
124
+ return "none";
125
+ }
126
+ export function toolRequiresRuntimeApproval(compiledTool) {
127
+ return resolveToolApprovalDecisionMode(compiledTool) === "manual";
67
128
  }
68
129
  function toolApprovalReason(compiledTool) {
69
130
  if (compiledTool.hitl?.enabled === true) {
@@ -101,17 +162,25 @@ function resolveApprovedInput(originalInput, resumeValue) {
101
162
  }
102
163
  return originalInput;
103
164
  }
104
- export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, interruptFn = interrupt) {
105
- if (!toolRequiresRuntimeApproval(compiledTool) || typeof resolvedTool !== "object" || resolvedTool === null) {
165
+ export function wrapToolForHumanInTheLoop(resolvedTool, compiledTool, binding, interruptFn = interrupt) {
166
+ const decisionMode = resolveToolApprovalDecisionMode(compiledTool, binding);
167
+ if (decisionMode === "none" || typeof resolvedTool !== "object" || resolvedTool === null) {
106
168
  return resolvedTool;
107
169
  }
108
170
  const target = resolvedTool;
109
171
  const runWithApproval = async (input, config, invokeOriginal) => {
172
+ if (decisionMode === "auto-approve") {
173
+ return invokeOriginal(input, config);
174
+ }
175
+ if (decisionMode === "auto-reject" || decisionMode === "deny-and-continue") {
176
+ return "Tool execution denied by runtime policy.";
177
+ }
110
178
  const resumed = interruptFn({
111
179
  toolName: compiledTool.name,
112
180
  toolId: compiledTool.id,
113
181
  allowedDecisions: compiledTool.hitl?.allow ?? ["approve", "edit", "reject"],
114
182
  inputPreview: toInputPreview(input),
183
+ decisionMode,
115
184
  ...(toolApprovalReason(compiledTool) ? { approvalReason: toolApprovalReason(compiledTool) } : {}),
116
185
  });
117
186
  const approvedInput = resolveApprovedInput(input, resumed);
@@ -238,5 +307,5 @@ export function wrapToolForExecution(resolvedTool, compiledTool, binding, interr
238
307
  },
239
308
  })
240
309
  : resolvedTool;
241
- return wrapToolWithDedupe(wrapToolForHumanInTheLoop(guardedTool, compiledTool, interruptFn), compiledTool);
310
+ return wrapToolWithDedupe(wrapToolForHumanInTheLoop(guardedTool, compiledTool, binding, interruptFn), compiledTool);
242
311
  }
@@ -1,5 +1,5 @@
1
1
  import { getBindingPrimaryTools } from "../../support/compiled-binding.js";
2
- import { toolRequiresRuntimeApproval } from "../../adapter/tool/tool-hitl.js";
2
+ import { resolveToolApprovalDecisionMode } from "../../adapter/tool/tool-hitl.js";
3
3
  import { compiledToolHasInputSchema } from "../tool-schema.js";
4
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;
5
5
  function inputHints(binding, tool) {
@@ -64,6 +64,11 @@ function readRisk(value) {
64
64
  function readApprovalPolicy(value) {
65
65
  return value === "explicit-hitl" || value === "runtime-default" || value === "none" ? value : undefined;
66
66
  }
67
+ function readDecisionMode(value) {
68
+ return value === "none" || value === "manual" || value === "auto-approve" || value === "auto-reject" || value === "deny-and-continue"
69
+ ? value
70
+ : undefined;
71
+ }
67
72
  function normalizeServerRef(value) {
68
73
  if (typeof value !== "string" || value.trim().length === 0) {
69
74
  return undefined;
@@ -123,18 +128,32 @@ function applyGovernanceOverrides(binding, policies) {
123
128
  const overrideRisk = readRisk(typedRule.risk);
124
129
  const overrideApprovalPolicy = readApprovalPolicy(typedRule.approvalPolicy);
125
130
  const overrideRequiresApproval = typeof typedRule.requiresApproval === "boolean" ? typedRule.requiresApproval : undefined;
131
+ const overrideDecisionMode = readDecisionMode(typedRule.decisionMode ?? typedRule.approvalMode);
126
132
  if (overrideRisk) {
127
133
  merged.risk = overrideRisk;
128
134
  }
129
135
  if (overrideRequiresApproval !== undefined) {
130
136
  merged.requiresApproval = overrideRequiresApproval;
131
137
  }
138
+ if (overrideDecisionMode) {
139
+ merged.decisionMode = overrideDecisionMode;
140
+ merged.requiresApproval = overrideDecisionMode === "manual";
141
+ merged.approvalPolicy =
142
+ overrideDecisionMode === "none"
143
+ ? "none"
144
+ : merged.approvalPolicy === "explicit-hitl"
145
+ ? "explicit-hitl"
146
+ : "runtime-default";
147
+ }
132
148
  if (overrideApprovalPolicy) {
133
149
  merged.approvalPolicy = overrideApprovalPolicy;
134
150
  }
135
151
  else if (overrideRequiresApproval === true && merged.approvalPolicy === "none") {
136
152
  merged.approvalPolicy = "runtime-default";
137
153
  }
154
+ else if (overrideRequiresApproval === false && !overrideDecisionMode) {
155
+ merged.approvalPolicy = "none";
156
+ }
138
157
  const extraHints = readStringArray(typedRule.inputRiskHints);
139
158
  if (extraHints.length > 0) {
140
159
  merged.inputRiskHints = Array.from(new Set([...merged.inputRiskHints, ...extraHints]));
@@ -160,6 +179,9 @@ function applyRemoteMcpGovernance(binding, policies) {
160
179
  const transport = merged.mcpTransport;
161
180
  if (transport && requireApprovalTransports.has(transport)) {
162
181
  merged.requiresApproval = true;
182
+ if (merged.decisionMode === "none") {
183
+ merged.decisionMode = "manual";
184
+ }
163
185
  if (merged.approvalPolicy === "none") {
164
186
  merged.approvalPolicy = "runtime-default";
165
187
  }
@@ -179,11 +201,15 @@ export function buildRuntimeGovernanceBundles(binding) {
179
201
  const toolPolicies = applyGovernanceOverrides(binding, applyRemoteMcpGovernance(binding, getBindingPrimaryTools(binding).map((tool) => {
180
202
  const remoteMcp = readRemoteMcpMetadata(tool);
181
203
  const writeLikeRemoteMcp = tool.type === "mcp" && (remoteMcp.access === "read-write" || WRITE_LIKE_PATTERN.test(`${tool.name} ${tool.description}`));
182
- const requiresApproval = toolRequiresRuntimeApproval(tool) ||
204
+ const derivedDecisionMode = resolveToolApprovalDecisionMode(tool, binding);
205
+ const requiresApproval = derivedDecisionMode === "manual" ||
183
206
  remoteMcp.trustTier === "untrusted" ||
184
207
  remoteMcp.approvalPolicy === "always" ||
185
208
  (remoteMcp.approvalPolicy === "write" && writeLikeRemoteMcp) ||
186
209
  remoteMcp.tenantScope === "cross-tenant";
210
+ const decisionMode = requiresApproval
211
+ ? (derivedDecisionMode === "none" ? "manual" : derivedDecisionMode)
212
+ : derivedDecisionMode;
187
213
  const approvalReason = remoteMcp.trustTier === "untrusted"
188
214
  ? "untrusted-mcp-server"
189
215
  : remoteMcp.tenantScope === "cross-tenant"
@@ -230,7 +256,8 @@ export function buildRuntimeGovernanceBundles(binding) {
230
256
  promptInjectionRisk: remoteMcp.promptInjectionRisk,
231
257
  }),
232
258
  requiresApproval,
233
- approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : requiresApproval ? "runtime-default" : "none",
259
+ approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : decisionMode === "none" ? "none" : "runtime-default",
260
+ decisionMode,
234
261
  hasInputSchema: compiledToolHasInputSchema(tool),
235
262
  inputRiskHints: Array.from(new Set(inputRiskHints)),
236
263
  };
@@ -241,7 +268,9 @@ export function buildRuntimeGovernanceBundles(binding) {
241
268
  return [{
242
269
  bundleId: `governance/${binding.agent.id}`,
243
270
  title: "Runtime tool governance",
244
- summary: `${toolPolicies.filter((tool) => tool.requiresApproval).length} of ${toolPolicies.length} tool(s) require approval`,
271
+ summary: `${toolPolicies.filter((tool) => tool.requiresApproval).length} of ${toolPolicies.length} tool(s) require approval; ` +
272
+ `auto-approved=${toolPolicies.filter((tool) => tool.decisionMode === "auto-approve").length}; ` +
273
+ `auto-rejected=${toolPolicies.filter((tool) => tool.decisionMode === "auto-reject" || tool.decisionMode === "deny-and-continue").length}`,
245
274
  toolPolicies,
246
275
  }];
247
276
  }
@@ -29,6 +29,8 @@ function projectGovernanceDiagnostics(runs) {
29
29
  const toolPolicies = collectGovernanceToolPolicies(runs);
30
30
  const highRiskTools = toolPolicies.filter((tool) => tool.risk === "high").length;
31
31
  const approvalRequiredTools = toolPolicies.filter((tool) => tool.requiresApproval).length;
32
+ const autoApprovedTools = toolPolicies.filter((tool) => tool.decisionMode === "auto-approve").length;
33
+ const autoRejectedTools = toolPolicies.filter((tool) => tool.decisionMode === "auto-reject" || tool.decisionMode === "deny-and-continue").length;
32
34
  const untrustedMcpTools = toolPolicies.filter((tool) => tool.mcpTrustTier === "untrusted").length;
33
35
  const crossTenantTools = toolPolicies.filter((tool) => tool.tenantScope === "cross-tenant").length;
34
36
  const writeAccessMcpTools = toolPolicies.filter((tool) => tool.category === "mcp" && tool.mcpAccess === "read-write").length;
@@ -36,11 +38,15 @@ function projectGovernanceDiagnostics(runs) {
36
38
  runsWithGovernance: runs.filter((run) => (run.runtimeSnapshot?.governance?.bundles?.length ?? 0) > 0).length,
37
39
  highRiskTools,
38
40
  approvalRequiredTools,
41
+ autoApprovedTools,
42
+ autoRejectedTools,
39
43
  untrustedMcpTools,
40
44
  crossTenantTools,
41
45
  writeAccessMcpTools,
42
46
  summary: `highRisk=${highRiskTools} ` +
43
47
  `approvalRequired=${approvalRequiredTools} ` +
48
+ `autoApproved=${autoApprovedTools} ` +
49
+ `autoRejected=${autoRejectedTools} ` +
44
50
  `untrusted=${untrustedMcpTools} ` +
45
51
  `crossTenant=${crossTenantTools}`,
46
52
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.269",
3
+ "version": "0.0.270",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "license": "MIT",
6
6
  "type": "module",