@botbotgo/agent-harness 0.0.188 → 0.0.189

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
@@ -217,6 +217,25 @@ Real products need a runtime that can answer harder questions:
217
217
  - It lets YAML own assembly and operating policy while code keeps a small, stable surface
218
218
  - It goes deep on runtime concerns that upstream libraries do not fully productize
219
219
 
220
+ ## What To Sell First
221
+
222
+ The product story should stay scenario-shaped instead of drifting back to a generic "multi-agent runtime" pitch.
223
+
224
+ - Enterprise internal agent runtime: approvals, restart-safe recovery, operator evidence, and policy-owned MCP access.
225
+ - Code modernization runtime: long-running coding flows, approval checkpoints, resumable runs, and exported evidence packages.
226
+ - Protocol bridge runtime: ACP, A2A, AG-UI, and runtime MCP on one stable control plane instead of bespoke per-surface glue.
227
+
228
+ Typical runtime governance defaults now look like:
229
+
230
+ ```yaml
231
+ governance:
232
+ remoteMcp:
233
+ denyTrustTiers: ["untrusted"]
234
+ denyTenantScopes: ["cross-tenant"]
235
+ denyPromptInjectionRisks: ["high"]
236
+ requireApprovalTransports: ["websocket"]
237
+ ```
238
+
220
239
  ## When To Use It
221
240
 
222
241
  Use `agent-harness` when:
package/README.zh.md CHANGED
@@ -214,6 +214,25 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正更
214
214
  - 复杂装配与运行策略交给 YAML,代码面保持小而稳
215
215
  - 在上游库未充分产品化的运行时问题上做深做透
216
216
 
217
+ ## 先卖哪三类场景
218
+
219
+ 产品叙事应该保持场景化,不要再漂回“通用 multi-agent runtime”。
220
+
221
+ - 企业内部 agent 运行时:审批、重启恢复、operator 证据链,以及由策略持有的 MCP 访问控制。
222
+ - 代码现代化运行时:长链路 coding flow、审批检查点、可恢复 runs,以及可导出的运行证据包。
223
+ - 协议桥接运行时:ACP、A2A、AG-UI 与 runtime MCP 共用一套稳定控制面,而不是每个对外接面各写一层胶水。
224
+
225
+ 现在推荐的 runtime 治理默认值大致是:
226
+
227
+ ```yaml
228
+ governance:
229
+ remoteMcp:
230
+ denyTrustTiers: ["untrusted"]
231
+ denyTenantScopes: ["cross-tenant"]
232
+ denyPromptInjectionRisks: ["high"]
233
+ requireApprovalTransports: ["websocket"]
234
+ ```
235
+
217
236
  ## 什么时候该用
218
237
 
219
238
  下面这些场景适合用 `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.188";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.187";
1
+ export const AGENT_HARNESS_VERSION = "0.0.188";
@@ -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,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.188",
3
+ "version": "0.0.189",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",