@botbotgo/agent-harness 0.0.188 → 0.0.190

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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 The botbotgo Team
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
22
+
package/README.md CHANGED
@@ -31,6 +31,11 @@
31
31
  (<code>docs/development/</code>, English / 中文)
32
32
  </p>
33
33
 
34
+ <p align="center">
35
+ <a href="./LICENSE">License</a> · <a href="./CONTRIBUTING.md">Contributing</a> ·
36
+ <a href="./SECURITY.md">Security</a> · <a href="./CODE_OF_CONDUCT.md">Code of Conduct</a>
37
+ </p>
38
+
34
39
  <p align="center">
35
40
  <em
36
41
  >We specialize in AI solutions. If you have a product idea you want to ship,
@@ -217,6 +222,25 @@ Real products need a runtime that can answer harder questions:
217
222
  - It lets YAML own assembly and operating policy while code keeps a small, stable surface
218
223
  - It goes deep on runtime concerns that upstream libraries do not fully productize
219
224
 
225
+ ## What To Sell First
226
+
227
+ The product story should stay scenario-shaped instead of drifting back to a generic "multi-agent runtime" pitch.
228
+
229
+ - Enterprise internal agent runtime: approvals, restart-safe recovery, operator evidence, and policy-owned MCP access.
230
+ - Code modernization runtime: long-running coding flows, approval checkpoints, resumable runs, and exported evidence packages.
231
+ - Protocol bridge runtime: ACP, A2A, AG-UI, and runtime MCP on one stable control plane instead of bespoke per-surface glue.
232
+
233
+ Typical runtime governance defaults now look like:
234
+
235
+ ```yaml
236
+ governance:
237
+ remoteMcp:
238
+ denyTrustTiers: ["untrusted"]
239
+ denyTenantScopes: ["cross-tenant"]
240
+ denyPromptInjectionRisks: ["high"]
241
+ requireApprovalTransports: ["websocket"]
242
+ ```
243
+
220
244
  ## When To Use It
221
245
 
222
246
  Use `agent-harness` when:
package/README.zh.md CHANGED
@@ -31,6 +31,11 @@
31
31
  (多页面静态文档位于 <code>docs/development/</code>,支持 English / 中文)
32
32
  </p>
33
33
 
34
+ <p align="center">
35
+ <a href="./LICENSE">许可证</a> · <a href="./CONTRIBUTING.md">贡献指南</a> ·
36
+ <a href="./SECURITY.md">安全说明</a> · <a href="./CODE_OF_CONDUCT.md">行为准则</a>
37
+ </p>
38
+
34
39
  <p align="center">
35
40
  <em
36
41
  >我们专注于 AI 解决方案。若有希望落地的产品想法,欢迎来信
@@ -214,6 +219,25 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正更
214
219
  - 复杂装配与运行策略交给 YAML,代码面保持小而稳
215
220
  - 在上游库未充分产品化的运行时问题上做深做透
216
221
 
222
+ ## 先卖哪三类场景
223
+
224
+ 产品叙事应该保持场景化,不要再漂回“通用 multi-agent runtime”。
225
+
226
+ - 企业内部 agent 运行时:审批、重启恢复、operator 证据链,以及由策略持有的 MCP 访问控制。
227
+ - 代码现代化运行时:长链路 coding flow、审批检查点、可恢复 runs,以及可导出的运行证据包。
228
+ - 协议桥接运行时:ACP、A2A、AG-UI 与 runtime MCP 共用一套稳定控制面,而不是每个对外接面各写一层胶水。
229
+
230
+ 现在推荐的 runtime 治理默认值大致是:
231
+
232
+ ```yaml
233
+ governance:
234
+ remoteMcp:
235
+ denyTrustTiers: ["untrusted"]
236
+ denyTenantScopes: ["cross-tenant"]
237
+ denyPromptInjectionRisks: ["high"]
238
+ requireApprovalTransports: ["websocket"]
239
+ ```
240
+
217
241
  ## 什么时候该用
218
242
 
219
243
  下面这些场景适合用 `agent-harness`:
package/dist/cli.js CHANGED
@@ -212,6 +212,55 @@ function parseRuntimeInspectOptions(args) {
212
212
  function renderJson(value) {
213
213
  return `${JSON.stringify(value, null, 2)}\n`;
214
214
  }
215
+ function isObject(value) {
216
+ return typeof value === "object" && value !== null && !Array.isArray(value);
217
+ }
218
+ function formatTimestamp(value) {
219
+ return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
220
+ }
221
+ function renderHealthSnapshot(snapshot, workspacePath) {
222
+ const lines = [];
223
+ const status = typeof snapshot.status === "string" ? snapshot.status : "unknown";
224
+ lines.push(`Runtime health ${workspacePath}: ${status}`);
225
+ const checks = isObject(snapshot.checks) ? snapshot.checks : {};
226
+ const checkEntries = Object.entries(checks)
227
+ .filter(([, value]) => isObject(value))
228
+ .map(([name, value]) => {
229
+ const check = value;
230
+ const reason = typeof check.reason === "string" && check.reason.trim().length > 0 ? ` (${check.reason})` : "";
231
+ return ` - ${name}: ${typeof check.status === "string" ? check.status : "unknown"}${reason}`;
232
+ });
233
+ if (checkEntries.length > 0) {
234
+ lines.push("Checks:");
235
+ lines.push(...checkEntries);
236
+ }
237
+ const stats = isObject(snapshot.stats) ? snapshot.stats : {};
238
+ const statEntries = [
239
+ ["activeRunSlots", stats.activeRunSlots],
240
+ ["pendingRunSlots", stats.pendingRunSlots],
241
+ ["pendingApprovals", stats.pendingApprovals],
242
+ ["stuckRuns", stats.stuckRuns],
243
+ ["llmSuccessRate1m", stats.llmSuccessRate1m],
244
+ ["llmP95LatencyMs1m", stats.llmP95LatencyMs1m],
245
+ ]
246
+ .filter(([, value]) => typeof value === "number")
247
+ .map(([name, value]) => ` - ${name}: ${value}`);
248
+ if (statEntries.length > 0) {
249
+ lines.push("Stats:");
250
+ lines.push(...statEntries);
251
+ }
252
+ const symptoms = Array.isArray(snapshot.symptoms) ? snapshot.symptoms.filter(isObject) : [];
253
+ if (symptoms.length > 0) {
254
+ lines.push("Symptoms:");
255
+ lines.push(...symptoms.map((symptom) => {
256
+ const code = typeof symptom.code === "string" ? symptom.code : "unknown";
257
+ const severity = typeof symptom.severity === "string" ? symptom.severity : "unknown";
258
+ const message = typeof symptom.message === "string" ? symptom.message : "";
259
+ return ` - ${code}: ${severity}${message ? ` (${message})` : ""}`;
260
+ }));
261
+ }
262
+ return `${lines.join("\n")}\n`;
263
+ }
215
264
  async function sleep(ms) {
216
265
  await new Promise((resolve) => {
217
266
  setTimeout(resolve, ms);
@@ -225,8 +274,14 @@ function renderApprovalList(approvals) {
225
274
  const status = typeof approval.status === "string" ? approval.status : "unknown";
226
275
  const toolName = typeof approval.toolName === "string" ? approval.toolName : "unknown_tool";
227
276
  const approvalId = typeof approval.approvalId === "string" ? approval.approvalId : "unknown";
277
+ const threadId = typeof approval.threadId === "string" ? ` thread=${approval.threadId}` : "";
278
+ const runId = typeof approval.runId === "string" ? ` run=${approval.runId}` : "";
228
279
  const reason = typeof approval.approvalReason === "string" ? ` reason=${approval.approvalReason}` : "";
229
- return `${approvalId} status=${status} tool=${toolName}${reason}`;
280
+ const requestedAt = formatTimestamp(approval.requestedAt);
281
+ const resolvedAt = formatTimestamp(approval.resolvedAt);
282
+ const requested = requestedAt ? ` requested=${requestedAt}` : "";
283
+ const resolved = resolvedAt ? ` resolved=${resolvedAt}` : "";
284
+ return `${approvalId} status=${status} tool=${toolName}${threadId}${runId}${reason}${requested}${resolved}`;
230
285
  }).join("\n") + "\n";
231
286
  }
232
287
  function renderRunList(runs) {
@@ -238,7 +293,13 @@ function renderRunList(runs) {
238
293
  const threadId = typeof run.threadId === "string" ? run.threadId : "unknown";
239
294
  const agentId = typeof run.agentId === "string" ? run.agentId : "unknown";
240
295
  const state = typeof run.state === "string" ? run.state : "unknown";
241
- return `${runId} thread=${threadId} agent=${agentId} state=${state}`;
296
+ const currentAgent = typeof run.currentAgentId === "string" ? ` current=${run.currentAgentId}` : "";
297
+ const resumable = typeof run.resumable === "boolean" ? ` resumable=${run.resumable}` : "";
298
+ const updatedAt = formatTimestamp(run.updatedAt);
299
+ const lastActivityAt = formatTimestamp(run.lastActivityAt);
300
+ const updated = updatedAt ? ` updated=${updatedAt}` : "";
301
+ const lastActivity = lastActivityAt ? ` activity=${lastActivityAt}` : "";
302
+ return `${runId} thread=${threadId} agent=${agentId}${currentAgent} state=${state}${resumable}${updated}${lastActivity}`;
242
303
  }).join("\n") + "\n";
243
304
  }
244
305
  export async function runCli(argv, io = {}, deps = {}) {
@@ -431,10 +492,7 @@ export async function runCli(argv, io = {}, deps = {}) {
431
492
  const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
432
493
  if (subcommand === "health") {
433
494
  const snapshot = await runtime.getHealth();
434
- stdout(parsed.json ? renderJson(snapshot) : `Runtime health ${workspacePath}: ${snapshot.status}\n`);
435
- if (!parsed.json) {
436
- stdout(renderJson(snapshot));
437
- }
495
+ stdout(parsed.json ? renderJson(snapshot) : renderHealthSnapshot(snapshot, workspacePath));
438
496
  await runtime.stop();
439
497
  return 0;
440
498
  }
@@ -155,6 +155,12 @@ spec:
155
155
  # Keep transport-specific and trust-specific policy here rather than scattering it across app code.
156
156
  governance:
157
157
  remoteMcp:
158
+ denyTrustTiers:
159
+ - untrusted
160
+ denyTenantScopes:
161
+ - cross-tenant
162
+ denyPromptInjectionRisks:
163
+ - high
158
164
  requireApprovalTransports:
159
165
  - websocket
160
166
  riskByTransport:
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.187";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.189";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.187";
1
+ export const AGENT_HARNESS_VERSION = "0.0.189";
@@ -49,6 +49,17 @@ export class PolicyEngine {
49
49
  const trimmed = value.trim();
50
50
  return trimmed.startsWith("mcp/") ? trimmed : `mcp/${trimmed}`;
51
51
  };
52
+ const readStringSet = (value) => new Set(Array.isArray(value)
53
+ ? value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
54
+ : []);
55
+ const riskRank = {
56
+ low: 0,
57
+ medium: 1,
58
+ high: 2,
59
+ };
60
+ const maxPromptInjectionRisk = remoteMcp.maxPromptInjectionRisk === "low" || remoteMcp.maxPromptInjectionRisk === "medium" || remoteMcp.maxPromptInjectionRisk === "high"
61
+ ? remoteMcp.maxPromptInjectionRisk
62
+ : undefined;
52
63
  const allowServerRefs = new Set(Array.isArray(remoteMcp.allowServerRefs)
53
64
  ? remoteMcp.allowServerRefs
54
65
  .map((item) => normalizeServerRef(item))
@@ -59,9 +70,14 @@ export class PolicyEngine {
59
70
  .map((item) => normalizeServerRef(item))
60
71
  .filter((item) => Boolean(item))
61
72
  : []);
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
- : []);
73
+ const denyTransports = readStringSet(remoteMcp.denyTransports);
74
+ const allowTrustTiers = readStringSet(remoteMcp.allowTrustTiers);
75
+ const denyTrustTiers = readStringSet(remoteMcp.denyTrustTiers);
76
+ const allowTenantScopes = readStringSet(remoteMcp.allowTenantScopes);
77
+ const denyTenantScopes = readStringSet(remoteMcp.denyTenantScopes);
78
+ const denyPromptInjectionRisks = readStringSet(remoteMcp.denyPromptInjectionRisks);
79
+ const allowOauthScopes = readStringSet(remoteMcp.allowOauthScopes);
80
+ const denyOauthScopes = readStringSet(remoteMcp.denyOauthScopes);
65
81
  const tools = binding.execution?.params?.tools ?? binding.langchainAgentParams?.tools ?? binding.deepAgentParams?.tools ?? [];
66
82
  const deniedRemoteTools = tools.flatMap((tool) => {
67
83
  if (tool.type !== "mcp") {
@@ -80,13 +96,43 @@ export class PolicyEngine {
80
96
  const transport = typeof inlineMcpServer?.transport === "string" && inlineMcpServer.transport.trim().length > 0
81
97
  ? inlineMcpServer.transport.trim()
82
98
  : undefined;
99
+ const trustTier = inlineMcpServer?.trustTier === "trusted" || inlineMcpServer?.trustTier === "reviewed" || inlineMcpServer?.trustTier === "untrusted"
100
+ ? inlineMcpServer.trustTier
101
+ : undefined;
102
+ const tenantScope = inlineMcpServer?.tenantScope === "workspace" ||
103
+ inlineMcpServer?.tenantScope === "project" ||
104
+ inlineMcpServer?.tenantScope === "tenant" ||
105
+ inlineMcpServer?.tenantScope === "cross-tenant"
106
+ ? inlineMcpServer.tenantScope
107
+ : undefined;
108
+ const promptInjectionRisk = inlineMcpServer?.promptInjectionRisk === "low" ||
109
+ inlineMcpServer?.promptInjectionRisk === "medium" ||
110
+ inlineMcpServer?.promptInjectionRisk === "high"
111
+ ? inlineMcpServer.promptInjectionRisk
112
+ : undefined;
113
+ const oauth = typeof inlineMcpServer?.oauth === "object" && inlineMcpServer.oauth && !Array.isArray(inlineMcpServer.oauth)
114
+ ? inlineMcpServer.oauth
115
+ : undefined;
116
+ const oauthScopes = Array.isArray(oauth?.scopes)
117
+ ? oauth.scopes.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
118
+ : [];
83
119
  const serverDenied = serverRef ? denyServerRefs.has(serverRef) || (allowServerRefs.size > 0 && !allowServerRefs.has(serverRef)) : false;
84
120
  const transportDenied = transport ? denyTransports.has(transport) : false;
85
- return serverDenied || transportDenied
121
+ const trustDenied = trustTier ? denyTrustTiers.has(trustTier) || (allowTrustTiers.size > 0 && !allowTrustTiers.has(trustTier)) : false;
122
+ const tenantDenied = tenantScope ? denyTenantScopes.has(tenantScope) || (allowTenantScopes.size > 0 && !allowTenantScopes.has(tenantScope)) : false;
123
+ const promptRiskDenied = (promptInjectionRisk ? denyPromptInjectionRisks.has(promptInjectionRisk) : false)
124
+ || (promptInjectionRisk && maxPromptInjectionRisk ? riskRank[promptInjectionRisk] > riskRank[maxPromptInjectionRisk] : false);
125
+ const oauthDenied = oauthScopes.some((scope) => denyOauthScopes.has(scope))
126
+ || (allowOauthScopes.size > 0 && oauthScopes.some((scope) => !allowOauthScopes.has(scope)));
127
+ return serverDenied || transportDenied || trustDenied || tenantDenied || promptRiskDenied || oauthDenied
86
128
  ? [{
87
129
  toolName: tool.name,
88
130
  ...(serverRef ? { serverRef } : {}),
89
131
  ...(transport ? { transport } : {}),
132
+ ...(trustTier ? { trustTier } : {}),
133
+ ...(tenantScope ? { tenantScope } : {}),
134
+ ...(promptInjectionRisk ? { promptInjectionRisk } : {}),
135
+ ...(oauthScopes.length > 0 ? { oauthScopes } : {}),
90
136
  }]
91
137
  : [];
92
138
  });
@@ -102,6 +148,9 @@ export class PolicyEngine {
102
148
  if (tool.transport) {
103
149
  return `${tool.toolName} (${tool.transport})`;
104
150
  }
151
+ if (tool.trustTier) {
152
+ return `${tool.toolName} (${tool.trustTier})`;
153
+ }
105
154
  return tool.toolName;
106
155
  });
107
156
  reasons.push(`runtime governance denied remote MCP access: ${details.join(", ")}`);
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.188",
3
+ "version": "0.0.190",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
+ "license": "MIT",
5
6
  "type": "module",
6
7
  "packageManager": "npm@10.9.2",
7
8
  "main": "./dist/index.js",