@botbotgo/agent-harness 0.0.187 → 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:
@@ -953,10 +972,12 @@ ACP transport notes:
953
972
 
954
973
  - `serveAcpStdio(runtime)` exposes newline-delimited JSON-RPC over stdio for local IDE, CLI, or subprocess clients.
955
974
  - `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
956
- - `serveA2aHttp(runtime)` exposes a minimal A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping `message/send`, `tasks/get`, and `tasks/cancel` onto the existing session/request runtime surface.
975
+ - `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and newer aliases such as `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, and `SubscribeToTask` onto the existing session/request runtime surface.
957
976
  - `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.
958
977
  - `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.
959
978
  - `listRequestEvents(...)` and `exportRequestPackage(...)` are the request-first inspection helpers.
960
979
  - `exportRequestPackage(...)` and `exportSessionPackage(...)` package stable runtime records, transcript, approvals, events, and artifacts for operator tooling without reaching into persistence internals.
961
- - `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.
980
+ - `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.
981
+ - `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.
982
+ - `agent-harness runtime health`, `agent-harness runtime approvals list|watch`, and `agent-harness runtime runs list|tail` provide a thin operator CLI over persisted runtime health, approval queues, and active run state.
962
983
  - detailed A2A adapter guidance lives in [`docs/a2a-bridge.md`](docs/a2a-bridge.md)
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`:
@@ -912,10 +931,12 @@ ACP transport 说明:
912
931
 
913
932
  - `serveAcpStdio(runtime)` 提供基于 stdio 的 newline-delimited JSON-RPC,适合本地 IDE、CLI 或子进程客户端。
914
933
  - `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程 operator surface 或独立控制面接入。
915
- - `serveA2aHttp(runtime)` 提供最小可用的 A2A HTTP JSON-RPC bridge 与 agent card discovery,把 `message/send`、`tasks/get`、`tasks/cancel` 映射到现有 session/request runtime surface。
934
+ - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask` 这类更新的方法别名,并统一映射到现有 session/request runtime surface。
916
935
  - `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
917
936
  - `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
918
937
  - `listRequestEvents(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
919
938
  - `exportRequestPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events 和 artifacts 打包给 operator tooling,而不必直接访问 persistence 内部实现。
920
- - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。
939
+ - `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 元数据,让治理快照能解释为什么某个远端工具被视为高风险。
940
+ - `runtime/default.observability.tracing` 现在可描述 OTLP endpoint 和 propagation mode 这类 exporter 元数据,使冻结的 runtime snapshot 在保留 trace correlation 的同时,也能保留对 operator 有意义的导出上下文,而不暴露 backend 私有 span 细节。
941
+ - `agent-harness runtime health`、`agent-harness runtime approvals list|watch` 与 `agent-harness runtime runs list|tail` 提供了一层轻量 operator CLI,可直接查看 runtime health、审批队列和运行状态。
921
942
  - 更详细的 A2A 适配层开发说明见 [`docs/a2a-bridge.md`](docs/a2a-bridge.md)
package/dist/cli.js CHANGED
@@ -14,6 +14,11 @@ function renderUsage() {
14
14
  agent-harness acp serve [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>]
15
15
  agent-harness a2a serve [--workspace <path>] [--host <hostname>] [--port <port>]
16
16
  agent-harness ag-ui serve [--workspace <path>] [--host <hostname>] [--port <port>]
17
+ agent-harness runtime health [--workspace <path>] [--json]
18
+ agent-harness runtime approvals list [--workspace <path>] [--status <pending|approved|edited|rejected|expired>] [--json]
19
+ agent-harness runtime approvals watch [--workspace <path>] [--status <pending|approved|edited|rejected|expired>] [--poll-ms <ms>] [--once] [--json]
20
+ agent-harness runtime runs list [--workspace <path>] [--agent <agentId>] [--thread <threadId>] [--state <state>] [--json]
21
+ agent-harness runtime runs tail [--workspace <path>] [--agent <agentId>] [--thread <threadId>] [--state <state>] [--poll-ms <ms>] [--once] [--json]
17
22
  agent-harness runtime-mcp serve [--workspace <path>]
18
23
  `;
19
24
  }
@@ -151,6 +156,152 @@ function parseHttpServeOptions(args, serviceLabel = "HTTP") {
151
156
  }
152
157
  return { workspaceRoot, hostname, port };
153
158
  }
159
+ function parseRuntimeInspectOptions(args) {
160
+ let workspaceRoot;
161
+ let json = false;
162
+ let once = false;
163
+ let pollMs = 1000;
164
+ let status;
165
+ let state;
166
+ let agentId;
167
+ let threadId;
168
+ for (let index = 0; index < args.length; index += 1) {
169
+ const arg = args[index];
170
+ if (arg === "--workspace" || arg === "--status" || arg === "--state" || arg === "--agent" || arg === "--thread" || arg === "--poll-ms") {
171
+ const value = args[index + 1];
172
+ if (!value) {
173
+ return { json, once, pollMs, status, state, agentId, threadId, error: `Missing value for ${arg}` };
174
+ }
175
+ if (arg === "--workspace") {
176
+ workspaceRoot = value;
177
+ }
178
+ else if (arg === "--status") {
179
+ status = value;
180
+ }
181
+ else if (arg === "--state") {
182
+ state = value;
183
+ }
184
+ else if (arg === "--agent") {
185
+ agentId = value;
186
+ }
187
+ else if (arg === "--thread") {
188
+ threadId = value;
189
+ }
190
+ else {
191
+ const parsedPoll = Number.parseInt(value, 10);
192
+ if (!Number.isFinite(parsedPoll) || parsedPoll <= 0) {
193
+ return { json, once, pollMs, status, state, agentId, threadId, error: `Invalid poll interval: ${value}` };
194
+ }
195
+ pollMs = parsedPoll;
196
+ }
197
+ index += 1;
198
+ continue;
199
+ }
200
+ if (arg === "--json") {
201
+ json = true;
202
+ continue;
203
+ }
204
+ if (arg === "--once") {
205
+ once = true;
206
+ continue;
207
+ }
208
+ return { workspaceRoot, json, once, pollMs, status, state, agentId, threadId, error: `Unknown option: ${arg}` };
209
+ }
210
+ return { workspaceRoot, json, once, pollMs, status, state, agentId, threadId };
211
+ }
212
+ function renderJson(value) {
213
+ return `${JSON.stringify(value, null, 2)}\n`;
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
+ }
264
+ async function sleep(ms) {
265
+ await new Promise((resolve) => {
266
+ setTimeout(resolve, ms);
267
+ });
268
+ }
269
+ function renderApprovalList(approvals) {
270
+ if (approvals.length === 0) {
271
+ return "No approvals matched.\n";
272
+ }
273
+ return approvals.map((approval) => {
274
+ const status = typeof approval.status === "string" ? approval.status : "unknown";
275
+ const toolName = typeof approval.toolName === "string" ? approval.toolName : "unknown_tool";
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}` : "";
279
+ const reason = typeof approval.approvalReason === "string" ? ` reason=${approval.approvalReason}` : "";
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}`;
285
+ }).join("\n") + "\n";
286
+ }
287
+ function renderRunList(runs) {
288
+ if (runs.length === 0) {
289
+ return "No runs matched.\n";
290
+ }
291
+ return runs.map((run) => {
292
+ const runId = typeof run.runId === "string" ? run.runId : "unknown";
293
+ const threadId = typeof run.threadId === "string" ? run.threadId : "unknown";
294
+ const agentId = typeof run.agentId === "string" ? run.agentId : "unknown";
295
+ const state = typeof run.state === "string" ? run.state : "unknown";
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}`;
303
+ }).join("\n") + "\n";
304
+ }
154
305
  export async function runCli(argv, io = {}, deps = {}) {
155
306
  const cwd = io.cwd ?? process.cwd();
156
307
  const stdout = io.stdout ?? ((message) => process.stdout.write(message));
@@ -320,6 +471,75 @@ export async function runCli(argv, io = {}, deps = {}) {
320
471
  return 1;
321
472
  }
322
473
  }
474
+ if (command === "runtime") {
475
+ const [subcommand, possibleNestedCommand, ...remainingArgs] = [projectName, ...rest];
476
+ if (!subcommand) {
477
+ stderr(renderUsage());
478
+ return 1;
479
+ }
480
+ const nestedCommand = (subcommand === "approvals" || subcommand === "runs") && possibleNestedCommand
481
+ ? possibleNestedCommand
482
+ : undefined;
483
+ const subcommandArgs = nestedCommand ? remainingArgs : [possibleNestedCommand, ...remainingArgs].filter((item) => typeof item === "string");
484
+ const parsed = parseRuntimeInspectOptions(subcommandArgs);
485
+ if (parsed.error) {
486
+ stderr(`${parsed.error}\n`);
487
+ stderr(renderUsage());
488
+ return 1;
489
+ }
490
+ try {
491
+ const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
492
+ const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
493
+ if (subcommand === "health") {
494
+ const snapshot = await runtime.getHealth();
495
+ stdout(parsed.json ? renderJson(snapshot) : renderHealthSnapshot(snapshot, workspacePath));
496
+ await runtime.stop();
497
+ return 0;
498
+ }
499
+ if (subcommand === "approvals" && (nestedCommand === "list" || nestedCommand === "watch")) {
500
+ const renderApprovals = async () => {
501
+ const approvals = await runtime.listApprovals(parsed.status ? { status: parsed.status } : undefined);
502
+ stdout(parsed.json ? renderJson(approvals) : renderApprovalList(approvals));
503
+ };
504
+ await renderApprovals();
505
+ if (nestedCommand === "watch" && !parsed.once) {
506
+ for (;;) {
507
+ await sleep(parsed.pollMs);
508
+ await renderApprovals();
509
+ }
510
+ }
511
+ await runtime.stop();
512
+ return 0;
513
+ }
514
+ if (subcommand === "runs" && (nestedCommand === "list" || nestedCommand === "tail")) {
515
+ const renderRuns = async () => {
516
+ const runs = await runtime.listRuns({
517
+ ...(parsed.agentId ? { agentId: parsed.agentId } : {}),
518
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
519
+ ...(parsed.state ? { state: parsed.state } : {}),
520
+ });
521
+ stdout(parsed.json ? renderJson(runs) : renderRunList(runs));
522
+ };
523
+ await renderRuns();
524
+ if (nestedCommand === "tail" && !parsed.once) {
525
+ for (;;) {
526
+ await sleep(parsed.pollMs);
527
+ await renderRuns();
528
+ }
529
+ }
530
+ await runtime.stop();
531
+ return 0;
532
+ }
533
+ await runtime.stop();
534
+ stderr(renderUsage());
535
+ return 1;
536
+ }
537
+ catch (error) {
538
+ const message = error instanceof Error ? error.message : String(error);
539
+ stderr(`${message}\n`);
540
+ return 1;
541
+ }
542
+ }
323
543
  stderr(renderUsage());
324
544
  return 1;
325
545
  }
@@ -150,3 +150,38 @@ spec:
150
150
  - socket hang up
151
151
  - econnreset
152
152
  - timed out
153
+
154
+ # agent-harness feature: runtime-owned governance defaults for remote MCP access and approval escalation.
155
+ # Keep transport-specific and trust-specific policy here rather than scattering it across app code.
156
+ governance:
157
+ remoteMcp:
158
+ denyTrustTiers:
159
+ - untrusted
160
+ denyTenantScopes:
161
+ - cross-tenant
162
+ denyPromptInjectionRisks:
163
+ - high
164
+ requireApprovalTransports:
165
+ - websocket
166
+ riskByTransport:
167
+ http: medium
168
+ sse: medium
169
+ websocket: high
170
+ inputRiskHintsByTransport:
171
+ http: ["remote-http"]
172
+ sse: ["remote-sse"]
173
+ websocket: ["remote-websocket"]
174
+
175
+ # agent-harness feature: runtime observability defaults for health and tracing/export alignment.
176
+ # These settings are runtime-owned metadata for operator tooling and downstream trace/export integration.
177
+ observability:
178
+ health:
179
+ enabled: true
180
+ evaluateIntervalSeconds: 30
181
+ emitEvents: true
182
+ tracing:
183
+ enabled: true
184
+ propagation: w3c-tracecontext
185
+ exporters:
186
+ - type: otlp-http
187
+ endpoint: https://otel.example.com/v1/traces
@@ -106,6 +106,12 @@ export type RuntimeGovernanceToolPolicy = {
106
106
  category: "local" | "backend" | "mcp" | "provider-native";
107
107
  mcpServerRef?: string;
108
108
  mcpTransport?: string;
109
+ mcpTrustTier?: "trusted" | "reviewed" | "untrusted";
110
+ mcpAccess?: "read-only" | "read-write";
111
+ tenantScope?: "workspace" | "project" | "tenant" | "cross-tenant";
112
+ approvalReason?: string;
113
+ promptInjectionRisk?: RuntimeGovernanceRiskLevel;
114
+ oauthScopes?: string[];
109
115
  risk: RuntimeGovernanceRiskLevel;
110
116
  requiresApproval: boolean;
111
117
  approvalPolicy: "explicit-hitl" | "runtime-default" | "none";
@@ -72,6 +72,16 @@ export type ParsedMcpServerObject = {
72
72
  cwd?: string;
73
73
  token?: string;
74
74
  headers?: Record<string, string>;
75
+ trustTier?: "trusted" | "reviewed" | "untrusted";
76
+ access?: "read-only" | "read-write";
77
+ tenantScope?: "workspace" | "project" | "tenant" | "cross-tenant";
78
+ approvalPolicy?: "always" | "write" | "never";
79
+ promptInjectionRisk?: "low" | "medium" | "high";
80
+ oauth?: {
81
+ provider?: string;
82
+ scopes?: string[];
83
+ };
84
+ labels?: string[];
75
85
  sourcePath: string;
76
86
  };
77
87
  export type ParsedToolObject = {
@@ -239,6 +249,7 @@ export type CompiledAgentBinding = {
239
249
  capabilities?: RuntimeCapabilities;
240
250
  resilience?: Record<string, unknown>;
241
251
  governance?: Record<string, unknown>;
252
+ observability?: Record<string, unknown>;
242
253
  deepagent?: {
243
254
  description?: string;
244
255
  passthrough?: Record<string, unknown>;
@@ -1 +1 @@
1
- export declare const AGENT_HARNESS_VERSION = "0.0.186";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.188";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.186";
1
+ export const AGENT_HARNESS_VERSION = "0.0.188";
@@ -17,6 +17,7 @@ export type A2aAgentCard = {
17
17
  name: string;
18
18
  description: string;
19
19
  version: string;
20
+ protocolVersion?: string;
20
21
  url: string;
21
22
  preferredTransport: "JSONRPC";
22
23
  provider?: {
@@ -24,6 +25,9 @@ export type A2aAgentCard = {
24
25
  url?: string;
25
26
  };
26
27
  documentationUrl?: string;
28
+ defaultAgentId?: string;
29
+ supportsAuthenticatedExtendedCard?: boolean;
30
+ supportedInterfaces?: string[];
27
31
  capabilities: {
28
32
  streaming: boolean;
29
33
  pushNotifications: boolean;
@@ -43,6 +47,7 @@ export type A2aTaskState = "submitted" | "working" | "input-required" | "complet
43
47
  type A2aTaskStatus = {
44
48
  state: A2aTaskState;
45
49
  timestamp: string;
50
+ messageId?: string;
46
51
  message?: {
47
52
  role: "agent";
48
53
  parts: Array<{
@@ -97,6 +97,22 @@ function parseTaskLocatorParams(params) {
97
97
  }
98
98
  return { taskId };
99
99
  }
100
+ function parseTaskListParams(params) {
101
+ if (!params || typeof params !== "object" || Array.isArray(params)) {
102
+ return { limit: 20 };
103
+ }
104
+ const typed = params;
105
+ const limitValue = typeof typed.limit === "number" && Number.isFinite(typed.limit)
106
+ ? Math.max(1, Math.min(100, Math.trunc(typed.limit)))
107
+ : 20;
108
+ return {
109
+ ...(typeof typed.agentId === "string" && typed.agentId.trim().length > 0 ? { agentId: typed.agentId.trim() } : {}),
110
+ ...(typeof typed.contextId === "string" && typed.contextId.trim().length > 0 ? { contextId: typed.contextId.trim() } : {}),
111
+ ...(typeof typed.state === "string" && typed.state.trim().length > 0 ? { state: typed.state.trim() } : {}),
112
+ ...(typeof typed.cursor === "string" && typed.cursor.trim().length > 0 ? { cursor: typed.cursor.trim() } : {}),
113
+ limit: limitValue,
114
+ };
115
+ }
100
116
  function mapRunState(state) {
101
117
  switch (state) {
102
118
  case "queued":
@@ -134,6 +150,63 @@ function contentToText(content) {
134
150
  .trim();
135
151
  return text.length > 0 ? text : undefined;
136
152
  }
153
+ function toSessionRecord(session) {
154
+ if (!session) {
155
+ return null;
156
+ }
157
+ return {
158
+ sessionId: session.threadId,
159
+ entryAgentId: session.entryAgentId,
160
+ currentAgentId: session.currentAgentId,
161
+ currentState: session.currentState,
162
+ latestRequestId: session.latestRunId,
163
+ createdAt: session.createdAt,
164
+ updatedAt: session.updatedAt,
165
+ messages: session.messages,
166
+ requests: session.runs.map((run) => ({
167
+ requestId: run.runId,
168
+ sessionId: run.threadId,
169
+ agentId: run.agentId,
170
+ executionMode: run.executionMode,
171
+ adapterKind: run.adapterKind,
172
+ createdAt: run.createdAt,
173
+ updatedAt: run.updatedAt,
174
+ state: run.state,
175
+ checkpointRef: run.checkpointRef,
176
+ resumable: run.resumable,
177
+ startedAt: run.startedAt,
178
+ endedAt: run.endedAt,
179
+ lastActivityAt: run.lastActivityAt,
180
+ currentAgentId: run.currentAgentId,
181
+ delegationChain: run.delegationChain,
182
+ runtimeSnapshot: run.runtimeSnapshot,
183
+ })),
184
+ pendingDecision: session.pendingDecision,
185
+ };
186
+ }
187
+ function toRequestRecord(request) {
188
+ if (!request) {
189
+ return null;
190
+ }
191
+ return {
192
+ requestId: request.runId,
193
+ sessionId: request.threadId,
194
+ agentId: request.agentId,
195
+ executionMode: request.executionMode,
196
+ adapterKind: request.adapterKind,
197
+ createdAt: request.createdAt,
198
+ updatedAt: request.updatedAt,
199
+ state: request.state,
200
+ checkpointRef: request.checkpointRef,
201
+ resumable: request.resumable,
202
+ startedAt: request.startedAt,
203
+ endedAt: request.endedAt,
204
+ lastActivityAt: request.lastActivityAt,
205
+ currentAgentId: request.currentAgentId,
206
+ delegationChain: request.delegationChain,
207
+ runtimeSnapshot: request.runtimeSnapshot,
208
+ };
209
+ }
137
210
  function buildTaskFromSessionAndRequest(session, request, approvals, output, failureMessage) {
138
211
  if (!session || !request) {
139
212
  return null;
@@ -193,52 +266,22 @@ async function buildTaskFromRuntime(runtime, requestId) {
193
266
  }
194
267
  const session = await runtime.getThread(request.threadId);
195
268
  const approvals = await runtime.listApprovals({ threadId: request.threadId, runId: request.runId });
196
- return buildTaskFromSessionAndRequest(session ? {
197
- sessionId: session.threadId,
198
- entryAgentId: session.entryAgentId,
199
- currentAgentId: session.currentAgentId,
200
- currentState: session.currentState,
201
- latestRequestId: session.latestRunId,
202
- createdAt: session.createdAt,
203
- updatedAt: session.updatedAt,
204
- messages: session.messages,
205
- requests: session.runs.map((run) => ({
206
- requestId: run.runId,
207
- sessionId: run.threadId,
208
- agentId: run.agentId,
209
- executionMode: run.executionMode,
210
- adapterKind: run.adapterKind,
211
- createdAt: run.createdAt,
212
- updatedAt: run.updatedAt,
213
- state: run.state,
214
- checkpointRef: run.checkpointRef,
215
- resumable: run.resumable,
216
- startedAt: run.startedAt,
217
- endedAt: run.endedAt,
218
- lastActivityAt: run.lastActivityAt,
219
- currentAgentId: run.currentAgentId,
220
- delegationChain: run.delegationChain,
221
- runtimeSnapshot: run.runtimeSnapshot,
222
- })),
223
- pendingDecision: session.pendingDecision,
224
- } : null, {
225
- requestId: request.runId,
226
- sessionId: request.threadId,
227
- agentId: request.agentId,
228
- executionMode: request.executionMode,
229
- adapterKind: request.adapterKind,
230
- createdAt: request.createdAt,
231
- updatedAt: request.updatedAt,
232
- state: request.state,
233
- checkpointRef: request.checkpointRef,
234
- resumable: request.resumable,
235
- startedAt: request.startedAt,
236
- endedAt: request.endedAt,
237
- lastActivityAt: request.lastActivityAt,
238
- currentAgentId: request.currentAgentId,
239
- delegationChain: request.delegationChain,
240
- runtimeSnapshot: request.runtimeSnapshot,
241
- }, approvals);
269
+ return buildTaskFromSessionAndRequest(toSessionRecord(session), toRequestRecord(request), approvals);
270
+ }
271
+ async function listTasksFromRuntime(runtime, params) {
272
+ const runs = await runtime.listRuns({
273
+ ...(params.agentId ? { agentId: params.agentId } : {}),
274
+ ...(params.contextId ? { threadId: params.contextId } : {}),
275
+ ...(params.state ? { state: params.state } : {}),
276
+ });
277
+ const startIndex = params.cursor ? Number.parseInt(Buffer.from(params.cursor, "base64url").toString("utf8"), 10) || 0 : 0;
278
+ const page = runs.slice(startIndex, startIndex + params.limit);
279
+ const tasks = (await Promise.all(page.map((run) => buildTaskFromRuntime(runtime, run.runId)))).filter((task) => Boolean(task));
280
+ const nextIndex = startIndex + page.length;
281
+ return {
282
+ tasks,
283
+ ...(nextIndex < runs.length ? { nextCursor: Buffer.from(String(nextIndex), "utf8").toString("base64url") } : {}),
284
+ };
242
285
  }
243
286
  function buildAgentCard(runtime, options) {
244
287
  const inventory = runtime.describeWorkspaceInventory();
@@ -253,8 +296,23 @@ function buildAgentCard(runtime, options) {
253
296
  name: options.agentName,
254
297
  description: options.agentDescription,
255
298
  version: "0.1.0",
299
+ protocolVersion: "1.0",
256
300
  url: options.rpcUrl,
257
301
  preferredTransport: "JSONRPC",
302
+ supportsAuthenticatedExtendedCard: false,
303
+ supportedInterfaces: [
304
+ "message/send",
305
+ "tasks/send",
306
+ "tasks/get",
307
+ "tasks/list",
308
+ "tasks/cancel",
309
+ "tasks/subscribe",
310
+ "SendMessage",
311
+ "GetTask",
312
+ "ListTasks",
313
+ "CancelTask",
314
+ "SubscribeToTask",
315
+ ],
258
316
  capabilities: {
259
317
  streaming: false,
260
318
  pushNotifications: false,
@@ -302,7 +360,7 @@ export async function serveA2aOverHttp(runtime, options = {}) {
302
360
  return;
303
361
  }
304
362
  try {
305
- if (payload.method === "message/send" || payload.method === "tasks/send") {
363
+ if (payload.method === "message/send" || payload.method === "tasks/send" || payload.method === "SendMessage") {
306
364
  const parsed = parseMessageSendParams(payload.params);
307
365
  const result = await runtime.run({
308
366
  agentId: parsed.agentId ?? options.defaultAgentId,
@@ -313,56 +371,11 @@ export async function serveA2aOverHttp(runtime, options = {}) {
313
371
  const session = await runtime.getThread(result.threadId);
314
372
  const requestRecord = await runtime.getRun(result.runId);
315
373
  const approvals = await runtime.listApprovals({ threadId: result.threadId, runId: result.runId });
316
- const task = buildTaskFromSessionAndRequest(session ? {
317
- sessionId: session.threadId,
318
- entryAgentId: session.entryAgentId,
319
- currentAgentId: session.currentAgentId,
320
- currentState: session.currentState,
321
- latestRequestId: session.latestRunId,
322
- createdAt: session.createdAt,
323
- updatedAt: session.updatedAt,
324
- messages: session.messages,
325
- requests: session.runs.map((run) => ({
326
- requestId: run.runId,
327
- sessionId: run.threadId,
328
- agentId: run.agentId,
329
- executionMode: run.executionMode,
330
- adapterKind: run.adapterKind,
331
- createdAt: run.createdAt,
332
- updatedAt: run.updatedAt,
333
- state: run.state,
334
- checkpointRef: run.checkpointRef,
335
- resumable: run.resumable,
336
- startedAt: run.startedAt,
337
- endedAt: run.endedAt,
338
- lastActivityAt: run.lastActivityAt,
339
- currentAgentId: run.currentAgentId,
340
- delegationChain: run.delegationChain,
341
- runtimeSnapshot: run.runtimeSnapshot,
342
- })),
343
- pendingDecision: session.pendingDecision,
344
- } : null, requestRecord ? {
345
- requestId: requestRecord.runId,
346
- sessionId: requestRecord.threadId,
347
- agentId: requestRecord.agentId,
348
- executionMode: requestRecord.executionMode,
349
- adapterKind: requestRecord.adapterKind,
350
- createdAt: requestRecord.createdAt,
351
- updatedAt: requestRecord.updatedAt,
352
- state: requestRecord.state,
353
- checkpointRef: requestRecord.checkpointRef,
354
- resumable: requestRecord.resumable,
355
- startedAt: requestRecord.startedAt,
356
- endedAt: requestRecord.endedAt,
357
- lastActivityAt: requestRecord.lastActivityAt,
358
- currentAgentId: requestRecord.currentAgentId,
359
- delegationChain: requestRecord.delegationChain,
360
- runtimeSnapshot: requestRecord.runtimeSnapshot,
361
- } : null, approvals, result.output);
374
+ const task = buildTaskFromSessionAndRequest(toSessionRecord(session), toRequestRecord(requestRecord), approvals, result.output);
362
375
  writeJson(response, 200, toSuccess(payload.id ?? null, task));
363
376
  return;
364
377
  }
365
- if (payload.method === "tasks/get") {
378
+ if (payload.method === "tasks/get" || payload.method === "GetTask") {
366
379
  const { taskId } = parseTaskLocatorParams(payload.params);
367
380
  const task = await buildTaskFromRuntime(runtime, taskId);
368
381
  if (!task) {
@@ -372,13 +385,35 @@ export async function serveA2aOverHttp(runtime, options = {}) {
372
385
  writeJson(response, 200, toSuccess(payload.id ?? null, task));
373
386
  return;
374
387
  }
375
- if (payload.method === "tasks/cancel") {
388
+ if (payload.method === "tasks/list" || payload.method === "ListTasks") {
389
+ const taskList = await listTasksFromRuntime(runtime, parseTaskListParams(payload.params));
390
+ writeJson(response, 200, toSuccess(payload.id ?? null, taskList));
391
+ return;
392
+ }
393
+ if (payload.method === "tasks/cancel" || payload.method === "CancelTask") {
376
394
  const { taskId } = parseTaskLocatorParams(payload.params);
377
395
  const result = await runtime.cancelRun({ runId: taskId, reason: "Cancelled via A2A bridge." });
378
396
  const task = await buildTaskFromRuntime(runtime, result.runId);
379
397
  writeJson(response, 200, toSuccess(payload.id ?? null, task));
380
398
  return;
381
399
  }
400
+ if (payload.method === "tasks/subscribe" || payload.method === "SubscribeToTask") {
401
+ const { taskId } = parseTaskLocatorParams(payload.params);
402
+ const task = await buildTaskFromRuntime(runtime, taskId);
403
+ if (!task) {
404
+ writeJson(response, 200, toError(payload.id ?? null, -32004, "Task not found."));
405
+ return;
406
+ }
407
+ writeJson(response, 200, toSuccess(payload.id ?? null, {
408
+ task,
409
+ streamable: false,
410
+ pushNotifications: false,
411
+ nextPollAfterMs: task.status.state === "completed" || task.status.state === "failed" || task.status.state === "canceled"
412
+ ? 0
413
+ : 1_000,
414
+ }));
415
+ return;
416
+ }
382
417
  writeJson(response, 200, toError(payload.id ?? null, -32601, `Unknown A2A method: ${payload.method}`));
383
418
  }
384
419
  catch (error) {
@@ -9,6 +9,16 @@ export type McpServerConfig = {
9
9
  url?: string;
10
10
  token?: string;
11
11
  headers?: Record<string, string>;
12
+ trustTier?: "trusted" | "reviewed" | "untrusted";
13
+ access?: "read-only" | "read-write";
14
+ tenantScope?: "workspace" | "project" | "tenant" | "cross-tenant";
15
+ approvalPolicy?: "always" | "write" | "never";
16
+ promptInjectionRisk?: "low" | "medium" | "high";
17
+ oauth?: {
18
+ provider?: string;
19
+ scopes?: string[];
20
+ };
21
+ labels?: string[];
12
22
  };
13
23
  export type McpToolDescriptor = {
14
24
  name: string;
@@ -14,6 +14,13 @@ function readStringRecord(value) {
14
14
  const entries = Object.entries(value).filter((entry) => typeof entry[1] === "string");
15
15
  return entries.length > 0 ? Object.fromEntries(entries) : undefined;
16
16
  }
17
+ function readStringArray(value) {
18
+ if (!Array.isArray(value)) {
19
+ return undefined;
20
+ }
21
+ const entries = value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim());
22
+ return entries.length > 0 ? entries : undefined;
23
+ }
17
24
  function normalizeMcpTransport(value) {
18
25
  if (value === "stdio" || value === "http" || value === "sse" || value === "websocket") {
19
26
  return value;
@@ -40,6 +47,13 @@ export function readMcpServerConfig(workspace, tool) {
40
47
  url: server.url,
41
48
  token: server.token,
42
49
  headers: server.headers,
50
+ trustTier: server.trustTier,
51
+ access: server.access,
52
+ tenantScope: server.tenantScope,
53
+ approvalPolicy: server.approvalPolicy,
54
+ promptInjectionRisk: server.promptInjectionRisk,
55
+ oauth: server.oauth,
56
+ labels: server.labels,
43
57
  };
44
58
  }
45
59
  const config = typeof tool.config === "object" && tool.config
@@ -60,6 +74,31 @@ export function readMcpServerConfig(workspace, tool) {
60
74
  url: typeof mcpServer.url === "string" ? mcpServer.url.trim() : undefined,
61
75
  token: typeof mcpServer.token === "string" ? mcpServer.token : undefined,
62
76
  headers: readStringRecord(mcpServer.headers),
77
+ trustTier: mcpServer.trustTier === "trusted" || mcpServer.trustTier === "reviewed" || mcpServer.trustTier === "untrusted"
78
+ ? mcpServer.trustTier
79
+ : undefined,
80
+ access: mcpServer.access === "read-only" || mcpServer.access === "read-write" ? mcpServer.access : undefined,
81
+ tenantScope: mcpServer.tenantScope === "workspace" ||
82
+ mcpServer.tenantScope === "project" ||
83
+ mcpServer.tenantScope === "tenant" ||
84
+ mcpServer.tenantScope === "cross-tenant"
85
+ ? mcpServer.tenantScope
86
+ : undefined,
87
+ approvalPolicy: mcpServer.approvalPolicy === "always" || mcpServer.approvalPolicy === "write" || mcpServer.approvalPolicy === "never"
88
+ ? mcpServer.approvalPolicy
89
+ : undefined,
90
+ promptInjectionRisk: mcpServer.promptInjectionRisk === "low" || mcpServer.promptInjectionRisk === "medium" || mcpServer.promptInjectionRisk === "high"
91
+ ? mcpServer.promptInjectionRisk
92
+ : undefined,
93
+ oauth: typeof mcpServer.oauth === "object" && mcpServer.oauth
94
+ ? {
95
+ provider: typeof mcpServer.oauth.provider === "string"
96
+ ? mcpServer.oauth.provider
97
+ : undefined,
98
+ scopes: readStringArray(mcpServer.oauth.scopes),
99
+ }
100
+ : undefined,
101
+ labels: readStringArray(mcpServer.labels),
63
102
  };
64
103
  }
65
104
  function createMcpCacheKey(config) {
@@ -72,6 +111,13 @@ function createMcpCacheKey(config) {
72
111
  url: config.url ?? "",
73
112
  token: config.token ?? "",
74
113
  headers: config.headers ?? {},
114
+ trustTier: config.trustTier ?? "",
115
+ access: config.access ?? "",
116
+ tenantScope: config.tenantScope ?? "",
117
+ approvalPolicy: config.approvalPolicy ?? "",
118
+ promptInjectionRisk: config.promptInjectionRisk ?? "",
119
+ oauth: config.oauth ?? {},
120
+ labels: config.labels ?? [],
75
121
  });
76
122
  }
77
123
  async function createConnectedMcpClient(config) {
@@ -23,6 +23,12 @@ function classifyRisk(policy) {
23
23
  if (policy.requiresApproval) {
24
24
  return "high";
25
25
  }
26
+ if (policy.mcpTrustTier === "untrusted" || policy.tenantScope === "cross-tenant" || policy.promptInjectionRisk === "high") {
27
+ return "high";
28
+ }
29
+ if (policy.mcpAccess === "read-write" || policy.promptInjectionRisk === "medium") {
30
+ return "medium";
31
+ }
26
32
  const target = `${policy.toolName} ${policy.description}`;
27
33
  if (policy.toolType === "mcp" && WRITE_LIKE_PATTERN.test(target)) {
28
34
  return "high";
@@ -69,12 +75,27 @@ function readRemoteMcpMetadata(tool) {
69
75
  const config = asObject(tool.config);
70
76
  const mcpReference = asObject(config?.mcp);
71
77
  const inlineServer = asObject(config?.mcpServer);
78
+ const oauth = asObject(inlineServer?.oauth);
72
79
  const transport = typeof inlineServer?.transport === "string" && inlineServer.transport.trim().length > 0
73
80
  ? inlineServer.transport.trim()
74
81
  : undefined;
82
+ const readEnum = (value, allowed) => typeof value === "string" && allowed.includes(value) ? value : undefined;
83
+ const oauthScopes = readStringArray(oauth?.scopes);
75
84
  return {
76
85
  ...(normalizeServerRef(mcpReference?.serverRef) ? { serverRef: normalizeServerRef(mcpReference?.serverRef) } : {}),
77
86
  ...(transport ? { transport } : {}),
87
+ ...(readEnum(inlineServer?.trustTier, ["trusted", "reviewed", "untrusted"]) ? { trustTier: readEnum(inlineServer?.trustTier, ["trusted", "reviewed", "untrusted"]) } : {}),
88
+ ...(readEnum(inlineServer?.access, ["read-only", "read-write"]) ? { access: readEnum(inlineServer?.access, ["read-only", "read-write"]) } : {}),
89
+ ...(readEnum(inlineServer?.tenantScope, ["workspace", "project", "tenant", "cross-tenant"])
90
+ ? { tenantScope: readEnum(inlineServer?.tenantScope, ["workspace", "project", "tenant", "cross-tenant"]) }
91
+ : {}),
92
+ ...(readEnum(inlineServer?.approvalPolicy, ["always", "write", "never"])
93
+ ? { approvalPolicy: readEnum(inlineServer?.approvalPolicy, ["always", "write", "never"]) }
94
+ : {}),
95
+ ...(readEnum(inlineServer?.promptInjectionRisk, ["low", "medium", "high"])
96
+ ? { promptInjectionRisk: readEnum(inlineServer?.promptInjectionRisk, ["low", "medium", "high"]) }
97
+ : {}),
98
+ ...(oauthScopes.length > 0 ? { oauthScopes } : {}),
78
99
  };
79
100
  }
80
101
  function matchesToolPolicy(rule, policy) {
@@ -156,8 +177,34 @@ function applyRemoteMcpGovernance(binding, policies) {
156
177
  }
157
178
  export function buildRuntimeGovernanceBundles(binding) {
158
179
  const toolPolicies = applyGovernanceOverrides(binding, applyRemoteMcpGovernance(binding, getBindingPrimaryTools(binding).map((tool) => {
159
- const requiresApproval = toolRequiresRuntimeApproval(tool);
160
180
  const remoteMcp = readRemoteMcpMetadata(tool);
181
+ const writeLikeRemoteMcp = tool.type === "mcp" && (remoteMcp.access === "read-write" || WRITE_LIKE_PATTERN.test(`${tool.name} ${tool.description}`));
182
+ const requiresApproval = toolRequiresRuntimeApproval(tool) ||
183
+ remoteMcp.trustTier === "untrusted" ||
184
+ remoteMcp.approvalPolicy === "always" ||
185
+ (remoteMcp.approvalPolicy === "write" && writeLikeRemoteMcp) ||
186
+ remoteMcp.tenantScope === "cross-tenant";
187
+ const approvalReason = remoteMcp.trustTier === "untrusted"
188
+ ? "untrusted-mcp-server"
189
+ : remoteMcp.tenantScope === "cross-tenant"
190
+ ? "cross-tenant-mcp-access"
191
+ : remoteMcp.approvalPolicy === "always"
192
+ ? "remote-mcp-approval-policy"
193
+ : remoteMcp.approvalPolicy === "write" && writeLikeRemoteMcp
194
+ ? "high-risk-mcp-write"
195
+ : requiresApproval && tool.type === "mcp"
196
+ ? "high-risk-mcp-write"
197
+ : undefined;
198
+ const inputRiskHints = inputHints(binding, tool);
199
+ if (remoteMcp.access === "read-write") {
200
+ inputRiskHints.push("remote-write-access");
201
+ }
202
+ if (remoteMcp.tenantScope === "cross-tenant") {
203
+ inputRiskHints.push("cross-tenant-scope");
204
+ }
205
+ if (remoteMcp.promptInjectionRisk) {
206
+ inputRiskHints.push(`prompt-injection-${remoteMcp.promptInjectionRisk}`);
207
+ }
161
208
  return {
162
209
  toolName: tool.name,
163
210
  toolId: tool.id,
@@ -165,17 +212,27 @@ export function buildRuntimeGovernanceBundles(binding) {
165
212
  category: toCategory(tool.type),
166
213
  ...(remoteMcp.serverRef ? { mcpServerRef: remoteMcp.serverRef } : {}),
167
214
  ...(remoteMcp.transport ? { mcpTransport: remoteMcp.transport } : {}),
215
+ ...(remoteMcp.trustTier ? { mcpTrustTier: remoteMcp.trustTier } : {}),
216
+ ...(remoteMcp.access ? { mcpAccess: remoteMcp.access } : {}),
217
+ ...(remoteMcp.tenantScope ? { tenantScope: remoteMcp.tenantScope } : {}),
218
+ ...(remoteMcp.promptInjectionRisk ? { promptInjectionRisk: remoteMcp.promptInjectionRisk } : {}),
219
+ ...(remoteMcp.oauthScopes && remoteMcp.oauthScopes.length > 0 ? { oauthScopes: remoteMcp.oauthScopes } : {}),
220
+ ...(approvalReason ? { approvalReason } : {}),
168
221
  risk: classifyRisk({
169
222
  toolType: tool.type,
170
223
  requiresApproval,
171
224
  toolName: tool.name,
172
225
  description: tool.description,
173
226
  config: tool.config,
227
+ mcpTrustTier: remoteMcp.trustTier,
228
+ mcpAccess: remoteMcp.access,
229
+ tenantScope: remoteMcp.tenantScope,
230
+ promptInjectionRisk: remoteMcp.promptInjectionRisk,
174
231
  }),
175
232
  requiresApproval,
176
233
  approvalPolicy: tool.hitl?.enabled === true ? "explicit-hitl" : requiresApproval ? "runtime-default" : "none",
177
234
  hasInputSchema: compiledToolHasInputSchema(tool),
178
- inputRiskHints: inputHints(binding, tool),
235
+ inputRiskHints: Array.from(new Set(inputRiskHints)),
179
236
  };
180
237
  })));
181
238
  if (toolPolicies.length === 0) {
@@ -20,8 +20,28 @@ function readTracingConfig(binding) {
20
20
  }
21
21
  function buildRuntimeSnapshotTracing(binding, runId) {
22
22
  const tracing = readTracingConfig(binding);
23
+ const observability = asObject(binding.harnessRuntime.observability);
24
+ const runtimeTracing = asObject(observability?.tracing);
25
+ const exporters = Array.isArray(runtimeTracing?.exporters)
26
+ ? runtimeTracing.exporters
27
+ .filter((item) => typeof item === "object" && item !== null && !Array.isArray(item))
28
+ .map((item) => ({
29
+ type: typeof item.type === "string" ? item.type : "custom",
30
+ ...(typeof item.endpoint === "string" ? { endpoint: item.endpoint } : {}),
31
+ }))
32
+ : [];
23
33
  if (!tracing) {
24
- return undefined;
34
+ if (!runtimeTracing || runId === undefined || runtimeTracing.enabled === false) {
35
+ return undefined;
36
+ }
37
+ return {
38
+ enabled: true,
39
+ correlationId: runId,
40
+ metadata: {
41
+ ...(typeof runtimeTracing.propagation === "string" ? { propagation: runtimeTracing.propagation } : {}),
42
+ ...(exporters.length > 0 ? { exporters } : {}),
43
+ },
44
+ };
25
45
  }
26
46
  const enabled = tracing.enabled !== false;
27
47
  if (!enabled || !runId) {
@@ -33,7 +53,15 @@ function buildRuntimeSnapshotTracing(binding, runId) {
33
53
  enabled: true,
34
54
  correlationId: runId,
35
55
  ...(tags.length > 0 ? { tags } : {}),
36
- ...(metadata && Object.keys(metadata).length > 0 ? { metadata: { ...metadata } } : {}),
56
+ ...((metadata && Object.keys(metadata).length > 0) || exporters.length > 0 || typeof runtimeTracing?.propagation === "string"
57
+ ? {
58
+ metadata: {
59
+ ...(metadata ? { ...metadata } : {}),
60
+ ...(typeof runtimeTracing?.propagation === "string" ? { propagation: runtimeTracing.propagation } : {}),
61
+ ...(exporters.length > 0 ? { exporters } : {}),
62
+ },
63
+ }
64
+ : {}),
37
65
  };
38
66
  }
39
67
  export function buildRunRuntimeSnapshot(binding, options) {
@@ -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(", ")}`);
@@ -341,6 +341,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
341
341
  ? asObject(runtimeDefaults?.filesystem)
342
342
  : undefined;
343
343
  const runtimeGovernanceDefaults = asObject(runtimeDefaults?.governance);
344
+ const runtimeObservabilityDefaults = asObject(runtimeDefaults?.observability);
344
345
  const compiledFilesystemConfig = agent.executionMode === "langchain-v1"
345
346
  ? mergeConfigObjects(runtimeFilesystemDefaults, getAgentExecutionObject(agent, "filesystem", { executionMode: "langchain-v1" }))
346
347
  : undefined;
@@ -357,6 +358,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
357
358
  capabilities: inferAgentCapabilities(agent),
358
359
  resilience,
359
360
  ...(runtimeGovernanceDefaults ? { governance: runtimeGovernanceDefaults } : {}),
361
+ ...(runtimeObservabilityDefaults ? { observability: runtimeObservabilityDefaults } : {}),
360
362
  ...(agent.executionMode === "deepagent"
361
363
  ? {
362
364
  deepagent: {
@@ -117,6 +117,7 @@ export function parseMcpServerObject(object) {
117
117
  const value = object.value;
118
118
  const env = asObject(value.env);
119
119
  const headers = asObject(value.headers);
120
+ const oauth = asObject(value.oauth);
120
121
  const transport = String(value.transport ??
121
122
  (typeof value.url === "string" && value.url.trim() ? "http" : undefined) ??
122
123
  (typeof value.command === "string" && value.command.trim() ? "stdio" : undefined) ??
@@ -131,6 +132,29 @@ export function parseMcpServerObject(object) {
131
132
  cwd: typeof value.cwd === "string" ? value.cwd : undefined,
132
133
  token: typeof value.token === "string" ? value.token : undefined,
133
134
  headers: headers ? Object.fromEntries(Object.entries(headers).filter((entry) => typeof entry[1] === "string")) : undefined,
135
+ trustTier: value.trustTier === "trusted" || value.trustTier === "reviewed" || value.trustTier === "untrusted"
136
+ ? value.trustTier
137
+ : undefined,
138
+ access: value.access === "read-only" || value.access === "read-write" ? value.access : undefined,
139
+ tenantScope: value.tenantScope === "workspace" ||
140
+ value.tenantScope === "project" ||
141
+ value.tenantScope === "tenant" ||
142
+ value.tenantScope === "cross-tenant"
143
+ ? value.tenantScope
144
+ : undefined,
145
+ approvalPolicy: value.approvalPolicy === "always" || value.approvalPolicy === "write" || value.approvalPolicy === "never"
146
+ ? value.approvalPolicy
147
+ : undefined,
148
+ promptInjectionRisk: value.promptInjectionRisk === "low" || value.promptInjectionRisk === "medium" || value.promptInjectionRisk === "high"
149
+ ? value.promptInjectionRisk
150
+ : undefined,
151
+ oauth: oauth
152
+ ? {
153
+ provider: typeof oauth.provider === "string" ? oauth.provider : undefined,
154
+ scopes: asStringArray(oauth.scopes),
155
+ }
156
+ : undefined,
157
+ labels: asStringArray(value.labels),
134
158
  sourcePath: object.sourcePath,
135
159
  };
136
160
  }
@@ -15,6 +15,13 @@ function toMcpServerConfig(server) {
15
15
  url: server.url,
16
16
  token: server.token,
17
17
  headers: server.headers,
18
+ trustTier: server.trustTier,
19
+ access: server.access,
20
+ tenantScope: server.tenantScope,
21
+ approvalPolicy: server.approvalPolicy,
22
+ promptInjectionRisk: server.promptInjectionRisk,
23
+ oauth: server.oauth,
24
+ labels: server.labels,
18
25
  };
19
26
  }
20
27
  function readStringArray(value) {
@@ -108,6 +115,29 @@ function mergeReferencedMcpServer(referencedServer, item, agentId, name, sourceP
108
115
  ...(referencedServer.headers ?? {}),
109
116
  ...(readStringRecord(item.headers) ?? {}),
110
117
  },
118
+ trustTier: item.trustTier === "trusted" || item.trustTier === "reviewed" || item.trustTier === "untrusted"
119
+ ? item.trustTier
120
+ : referencedServer.trustTier,
121
+ access: item.access === "read-only" || item.access === "read-write" ? item.access : referencedServer.access,
122
+ tenantScope: item.tenantScope === "workspace" ||
123
+ item.tenantScope === "project" ||
124
+ item.tenantScope === "tenant" ||
125
+ item.tenantScope === "cross-tenant"
126
+ ? item.tenantScope
127
+ : referencedServer.tenantScope,
128
+ approvalPolicy: item.approvalPolicy === "always" || item.approvalPolicy === "write" || item.approvalPolicy === "never"
129
+ ? item.approvalPolicy
130
+ : referencedServer.approvalPolicy,
131
+ promptInjectionRisk: item.promptInjectionRisk === "low" || item.promptInjectionRisk === "medium" || item.promptInjectionRisk === "high"
132
+ ? item.promptInjectionRisk
133
+ : referencedServer.promptInjectionRisk,
134
+ oauth: asObject(item.oauth)
135
+ ? {
136
+ provider: typeof asObject(item.oauth)?.provider === "string" ? asObject(item.oauth)?.provider : referencedServer.oauth?.provider,
137
+ scopes: readStringArray(asObject(item.oauth)?.scopes) || referencedServer.oauth?.scopes,
138
+ }
139
+ : referencedServer.oauth,
140
+ labels: readStringArray(item.labels).length > 0 ? readStringArray(item.labels) : referencedServer.labels,
111
141
  sourcePath,
112
142
  };
113
143
  }
@@ -220,6 +250,16 @@ export async function hydrateAgentMcpTools(agents, mcpServers, tools) {
220
250
  mcp: {
221
251
  serverRef: `mcp/${serverId}`,
222
252
  },
253
+ mcpServer: {
254
+ transport: parsedServer.transport,
255
+ ...(parsedServer.trustTier ? { trustTier: parsedServer.trustTier } : {}),
256
+ ...(parsedServer.access ? { access: parsedServer.access } : {}),
257
+ ...(parsedServer.tenantScope ? { tenantScope: parsedServer.tenantScope } : {}),
258
+ ...(parsedServer.approvalPolicy ? { approvalPolicy: parsedServer.approvalPolicy } : {}),
259
+ ...(parsedServer.promptInjectionRisk ? { promptInjectionRisk: parsedServer.promptInjectionRisk } : {}),
260
+ ...(parsedServer.oauth ? { oauth: parsedServer.oauth } : {}),
261
+ ...(parsedServer.labels && parsedServer.labels.length > 0 ? { labels: parsedServer.labels } : {}),
262
+ },
223
263
  },
224
264
  mcpRef: remoteTool.name,
225
265
  bundleRefs: [],
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@botbotgo/agent-harness",
3
- "version": "0.0.187",
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",