@botbotgo/agent-harness 0.0.186 → 0.0.188

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
@@ -953,10 +953,12 @@ ACP transport notes:
953
953
 
954
954
  - `serveAcpStdio(runtime)` exposes newline-delimited JSON-RPC over stdio for local IDE, CLI, or subprocess clients.
955
955
  - `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.
957
- - `serveAgUiHttp(runtime)` exposes a minimal AG-UI-compatible HTTP SSE bridge that projects `run + output.delta + final result` onto `RUN_STARTED`, `TEXT_MESSAGE_*`, and `RUN_FINISHED` events for UI clients.
956
+ - `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
+ - `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
958
  - `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
959
  - `listRequestEvents(...)` and `exportRequestPackage(...)` are the request-first inspection helpers.
960
960
  - `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.
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. 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.
962
+ - `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.
963
+ - `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
964
  - detailed A2A adapter guidance lives in [`docs/a2a-bridge.md`](docs/a2a-bridge.md)
package/README.zh.md CHANGED
@@ -912,10 +912,12 @@ ACP transport 说明:
912
912
 
913
913
  - `serveAcpStdio(runtime)` 提供基于 stdio 的 newline-delimited JSON-RPC,适合本地 IDE、CLI 或子进程客户端。
914
914
  - `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。
916
- - `serveAgUiHttp(runtime)` 提供最小可用的 AG-UI HTTP SSE bridge,把现有 `run + output.delta + final result` 投影成 `RUN_STARTED`、`TEXT_MESSAGE_*` 与 `RUN_FINISHED` 事件,便于 UI 客户端直接接入。
915
+ - `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask` 这类更新的方法别名,并统一映射到现有 session/request runtime surface。
916
+ - `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
917
917
  - `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
918
918
  - `listRequestEvents(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
919
919
  - `exportRequestPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events 和 artifacts 打包给 operator tooling,而不必直接访问 persistence 内部实现。
920
- - `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。
920
+ - `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 元数据,让治理快照能解释为什么某个远端工具被视为高风险。
921
+ - `runtime/default.observability.tracing` 现在可描述 OTLP endpoint 和 propagation mode 这类 exporter 元数据,使冻结的 runtime snapshot 在保留 trace correlation 的同时,也能保留对 operator 有意义的导出上下文,而不暴露 backend 私有 span 细节。
922
+ - `agent-harness runtime health`、`agent-harness runtime approvals list|watch` 与 `agent-harness runtime runs list|tail` 提供了一层轻量 operator CLI,可直接查看 runtime health、审批队列和运行状态。
921
923
  - 更详细的 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,91 @@ 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
+ async function sleep(ms) {
216
+ await new Promise((resolve) => {
217
+ setTimeout(resolve, ms);
218
+ });
219
+ }
220
+ function renderApprovalList(approvals) {
221
+ if (approvals.length === 0) {
222
+ return "No approvals matched.\n";
223
+ }
224
+ return approvals.map((approval) => {
225
+ const status = typeof approval.status === "string" ? approval.status : "unknown";
226
+ const toolName = typeof approval.toolName === "string" ? approval.toolName : "unknown_tool";
227
+ const approvalId = typeof approval.approvalId === "string" ? approval.approvalId : "unknown";
228
+ const reason = typeof approval.approvalReason === "string" ? ` reason=${approval.approvalReason}` : "";
229
+ return `${approvalId} status=${status} tool=${toolName}${reason}`;
230
+ }).join("\n") + "\n";
231
+ }
232
+ function renderRunList(runs) {
233
+ if (runs.length === 0) {
234
+ return "No runs matched.\n";
235
+ }
236
+ return runs.map((run) => {
237
+ const runId = typeof run.runId === "string" ? run.runId : "unknown";
238
+ const threadId = typeof run.threadId === "string" ? run.threadId : "unknown";
239
+ const agentId = typeof run.agentId === "string" ? run.agentId : "unknown";
240
+ const state = typeof run.state === "string" ? run.state : "unknown";
241
+ return `${runId} thread=${threadId} agent=${agentId} state=${state}`;
242
+ }).join("\n") + "\n";
243
+ }
154
244
  export async function runCli(argv, io = {}, deps = {}) {
155
245
  const cwd = io.cwd ?? process.cwd();
156
246
  const stdout = io.stdout ?? ((message) => process.stdout.write(message));
@@ -320,6 +410,78 @@ export async function runCli(argv, io = {}, deps = {}) {
320
410
  return 1;
321
411
  }
322
412
  }
413
+ if (command === "runtime") {
414
+ const [subcommand, possibleNestedCommand, ...remainingArgs] = [projectName, ...rest];
415
+ if (!subcommand) {
416
+ stderr(renderUsage());
417
+ return 1;
418
+ }
419
+ const nestedCommand = (subcommand === "approvals" || subcommand === "runs") && possibleNestedCommand
420
+ ? possibleNestedCommand
421
+ : undefined;
422
+ const subcommandArgs = nestedCommand ? remainingArgs : [possibleNestedCommand, ...remainingArgs].filter((item) => typeof item === "string");
423
+ const parsed = parseRuntimeInspectOptions(subcommandArgs);
424
+ if (parsed.error) {
425
+ stderr(`${parsed.error}\n`);
426
+ stderr(renderUsage());
427
+ return 1;
428
+ }
429
+ try {
430
+ const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
431
+ const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
432
+ if (subcommand === "health") {
433
+ 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
+ }
438
+ await runtime.stop();
439
+ return 0;
440
+ }
441
+ if (subcommand === "approvals" && (nestedCommand === "list" || nestedCommand === "watch")) {
442
+ const renderApprovals = async () => {
443
+ const approvals = await runtime.listApprovals(parsed.status ? { status: parsed.status } : undefined);
444
+ stdout(parsed.json ? renderJson(approvals) : renderApprovalList(approvals));
445
+ };
446
+ await renderApprovals();
447
+ if (nestedCommand === "watch" && !parsed.once) {
448
+ for (;;) {
449
+ await sleep(parsed.pollMs);
450
+ await renderApprovals();
451
+ }
452
+ }
453
+ await runtime.stop();
454
+ return 0;
455
+ }
456
+ if (subcommand === "runs" && (nestedCommand === "list" || nestedCommand === "tail")) {
457
+ const renderRuns = async () => {
458
+ const runs = await runtime.listRuns({
459
+ ...(parsed.agentId ? { agentId: parsed.agentId } : {}),
460
+ ...(parsed.threadId ? { threadId: parsed.threadId } : {}),
461
+ ...(parsed.state ? { state: parsed.state } : {}),
462
+ });
463
+ stdout(parsed.json ? renderJson(runs) : renderRunList(runs));
464
+ };
465
+ await renderRuns();
466
+ if (nestedCommand === "tail" && !parsed.once) {
467
+ for (;;) {
468
+ await sleep(parsed.pollMs);
469
+ await renderRuns();
470
+ }
471
+ }
472
+ await runtime.stop();
473
+ return 0;
474
+ }
475
+ await runtime.stop();
476
+ stderr(renderUsage());
477
+ return 1;
478
+ }
479
+ catch (error) {
480
+ const message = error instanceof Error ? error.message : String(error);
481
+ stderr(`${message}\n`);
482
+ return 1;
483
+ }
484
+ }
323
485
  stderr(renderUsage());
324
486
  return 1;
325
487
  }
@@ -150,3 +150,32 @@ 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
+ requireApprovalTransports:
159
+ - websocket
160
+ riskByTransport:
161
+ http: medium
162
+ sse: medium
163
+ websocket: high
164
+ inputRiskHintsByTransport:
165
+ http: ["remote-http"]
166
+ sse: ["remote-sse"]
167
+ websocket: ["remote-websocket"]
168
+
169
+ # agent-harness feature: runtime observability defaults for health and tracing/export alignment.
170
+ # These settings are runtime-owned metadata for operator tooling and downstream trace/export integration.
171
+ observability:
172
+ health:
173
+ enabled: true
174
+ evaluateIntervalSeconds: 30
175
+ emitEvents: true
176
+ tracing:
177
+ enabled: true
178
+ propagation: w3c-tracecontext
179
+ exporters:
180
+ - type: otlp-http
181
+ 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.185";
1
+ export declare const AGENT_HARNESS_VERSION = "0.0.187";
@@ -1 +1 @@
1
- export const AGENT_HARNESS_VERSION = "0.0.185";
1
+ export const AGENT_HARNESS_VERSION = "0.0.187";
@@ -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) {
@@ -20,6 +20,28 @@ export type AgUiEvent = (AgUiBaseEvent & {
20
20
  threadId: string;
21
21
  runId: string;
22
22
  input?: MessageContent;
23
+ }) | (AgUiBaseEvent & {
24
+ type: "STEP_STARTED";
25
+ stepId: string;
26
+ title: string;
27
+ category: "llm" | "tool" | "skill" | "memory" | "chain" | "approval";
28
+ }) | (AgUiBaseEvent & {
29
+ type: "STEP_FINISHED";
30
+ stepId: string;
31
+ title: string;
32
+ category: "llm" | "tool" | "skill" | "memory" | "chain" | "approval";
33
+ status: "completed" | "failed";
34
+ }) | (AgUiBaseEvent & {
35
+ type: "THINKING_TEXT_MESSAGE_START";
36
+ messageId: string;
37
+ role: "assistant";
38
+ }) | (AgUiBaseEvent & {
39
+ type: "THINKING_TEXT_MESSAGE_CONTENT";
40
+ messageId: string;
41
+ delta: string;
42
+ }) | (AgUiBaseEvent & {
43
+ type: "THINKING_TEXT_MESSAGE_END";
44
+ messageId: string;
23
45
  }) | (AgUiBaseEvent & {
24
46
  type: "TEXT_MESSAGE_START";
25
47
  messageId: string;
@@ -31,6 +53,25 @@ export type AgUiEvent = (AgUiBaseEvent & {
31
53
  }) | (AgUiBaseEvent & {
32
54
  type: "TEXT_MESSAGE_END";
33
55
  messageId: string;
56
+ }) | (AgUiBaseEvent & {
57
+ type: "TOOL_CALL_START";
58
+ toolCallId: string;
59
+ toolName: string;
60
+ }) | (AgUiBaseEvent & {
61
+ type: "TOOL_CALL_ARGS";
62
+ toolCallId: string;
63
+ toolName: string;
64
+ args: unknown;
65
+ }) | (AgUiBaseEvent & {
66
+ type: "TOOL_CALL_RESULT";
67
+ toolCallId: string;
68
+ toolName: string;
69
+ result: unknown;
70
+ isError?: boolean;
71
+ }) | (AgUiBaseEvent & {
72
+ type: "TOOL_CALL_END";
73
+ toolCallId: string;
74
+ toolName: string;
34
75
  }) | (AgUiBaseEvent & {
35
76
  type: "RUN_FINISHED";
36
77
  threadId: string;
@@ -1,5 +1,6 @@
1
1
  import { createServer } from "node:http";
2
2
  import { createPersistentId } from "../../utils/id.js";
3
+ import { createUpstreamTimelineReducer } from "../../upstream-events.js";
3
4
  function normalizePath(value, fallback) {
4
5
  const source = typeof value === "string" && value.trim().length > 0 ? value.trim() : fallback;
5
6
  return source.startsWith("/") ? source : `/${source}`;
@@ -55,6 +56,63 @@ function toRunStarted(event, input) {
55
56
  input,
56
57
  };
57
58
  }
59
+ function asObject(value) {
60
+ return typeof value === "object" && value !== null ? value : null;
61
+ }
62
+ function readUpstreamEventName(event) {
63
+ return typeof asObject(event)?.event === "string" ? String(asObject(event)?.event) : "";
64
+ }
65
+ function readUpstreamToolName(event) {
66
+ return typeof asObject(event)?.name === "string" ? String(asObject(event)?.name) : "";
67
+ }
68
+ function readUpstreamRunType(event) {
69
+ return typeof asObject(event)?.run_type === "string" ? String(asObject(event)?.run_type) : "";
70
+ }
71
+ function isToolStartEvent(event) {
72
+ const eventName = readUpstreamEventName(event);
73
+ return eventName === "on_tool_start" || (eventName === "on_chain_start" && readUpstreamRunType(event) === "tool");
74
+ }
75
+ function isToolTerminalEvent(event) {
76
+ const eventName = readUpstreamEventName(event);
77
+ return eventName === "on_tool_end"
78
+ || eventName === "on_tool_error"
79
+ || ((eventName === "on_chain_end" || eventName === "on_chain_error") && readUpstreamRunType(event) === "tool");
80
+ }
81
+ function readToolArgs(event) {
82
+ const typed = asObject(event);
83
+ const data = asObject(typed?.data);
84
+ if (!data || !("input" in data)) {
85
+ return undefined;
86
+ }
87
+ return data.input;
88
+ }
89
+ function createToolCallState() {
90
+ const activeToolCallIds = new Map();
91
+ return {
92
+ start(toolName) {
93
+ const toolCallId = `tool-${createPersistentId()}`;
94
+ const activeIds = activeToolCallIds.get(toolName) ?? [];
95
+ activeIds.push(toolCallId);
96
+ activeToolCallIds.set(toolName, activeIds);
97
+ return toolCallId;
98
+ },
99
+ peek(toolName) {
100
+ const activeIds = activeToolCallIds.get(toolName);
101
+ return activeIds?.at(-1);
102
+ },
103
+ finish(toolName) {
104
+ const activeIds = activeToolCallIds.get(toolName);
105
+ if (!activeIds || activeIds.length === 0) {
106
+ return undefined;
107
+ }
108
+ const toolCallId = activeIds.pop();
109
+ if (!activeIds.length) {
110
+ activeToolCallIds.delete(toolName);
111
+ }
112
+ return toolCallId;
113
+ },
114
+ };
115
+ }
58
116
  export async function serveAgUiOverHttp(runtime, options = {}) {
59
117
  const hostname = options.hostname?.trim() || "127.0.0.1";
60
118
  const port = typeof options.port === "number" && Number.isFinite(options.port) ? options.port : 0;
@@ -74,7 +132,11 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
74
132
  let runId;
75
133
  let threadId;
76
134
  let messageId;
135
+ let thinkingMessageId;
77
136
  let textMessageStarted = false;
137
+ let thinkingMessageStarted = false;
138
+ const toolCalls = createToolCallState();
139
+ const upstreamReducer = createUpstreamTimelineReducer();
78
140
  const ensureTextStart = async () => {
79
141
  if (textMessageStarted) {
80
142
  return;
@@ -88,6 +150,37 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
88
150
  role: "assistant",
89
151
  });
90
152
  };
153
+ const ensureThinkingTextStart = async () => {
154
+ if (thinkingMessageStarted) {
155
+ return;
156
+ }
157
+ thinkingMessageId = thinkingMessageId ?? `thinking-${createPersistentId()}`;
158
+ thinkingMessageStarted = true;
159
+ await writeSseEvent(response, {
160
+ type: "THINKING_TEXT_MESSAGE_START",
161
+ timestamp: createTimestamp(),
162
+ messageId: thinkingMessageId,
163
+ role: "assistant",
164
+ });
165
+ };
166
+ const closeOpenMessages = async () => {
167
+ if (thinkingMessageStarted) {
168
+ await writeSseEvent(response, {
169
+ type: "THINKING_TEXT_MESSAGE_END",
170
+ timestamp: createTimestamp(),
171
+ messageId: thinkingMessageId,
172
+ });
173
+ thinkingMessageStarted = false;
174
+ }
175
+ if (textMessageStarted) {
176
+ await writeSseEvent(response, {
177
+ type: "TEXT_MESSAGE_END",
178
+ timestamp: createTimestamp(),
179
+ messageId: messageId,
180
+ });
181
+ textMessageStarted = false;
182
+ }
183
+ };
91
184
  try {
92
185
  const body = await readRequestBody(request);
93
186
  const input = parseRunInput(JSON.parse(body));
@@ -120,6 +213,92 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
120
213
  });
121
214
  }
122
215
  },
216
+ onUpstreamEvent: async (event) => {
217
+ const eventName = readUpstreamEventName(event);
218
+ const toolName = readUpstreamToolName(event);
219
+ if (isToolStartEvent(event) && toolName) {
220
+ const toolCallId = toolCalls.start(toolName);
221
+ await writeSseEvent(response, {
222
+ type: "TOOL_CALL_START",
223
+ timestamp: createTimestamp(),
224
+ toolCallId,
225
+ toolName,
226
+ });
227
+ const args = readToolArgs(event);
228
+ if (args !== undefined) {
229
+ await writeSseEvent(response, {
230
+ type: "TOOL_CALL_ARGS",
231
+ timestamp: createTimestamp(),
232
+ toolCallId,
233
+ toolName,
234
+ args,
235
+ });
236
+ }
237
+ }
238
+ const projections = upstreamReducer.consume(event);
239
+ for (const projection of projections) {
240
+ if (projection.type === "thinking") {
241
+ if (!projection.text) {
242
+ continue;
243
+ }
244
+ await ensureThinkingTextStart();
245
+ await writeSseEvent(response, {
246
+ type: "THINKING_TEXT_MESSAGE_CONTENT",
247
+ timestamp: createTimestamp(),
248
+ messageId: thinkingMessageId,
249
+ delta: projection.text,
250
+ });
251
+ continue;
252
+ }
253
+ if (projection.type === "step") {
254
+ if (projection.status === "started") {
255
+ await writeSseEvent(response, {
256
+ type: "STEP_STARTED",
257
+ timestamp: createTimestamp(),
258
+ stepId: projection.key,
259
+ title: projection.step,
260
+ category: projection.category,
261
+ });
262
+ continue;
263
+ }
264
+ await writeSseEvent(response, {
265
+ type: "STEP_FINISHED",
266
+ timestamp: createTimestamp(),
267
+ stepId: projection.key,
268
+ title: projection.step,
269
+ category: projection.category,
270
+ status: projection.status,
271
+ });
272
+ continue;
273
+ }
274
+ const toolCallId = toolCalls.peek(projection.toolName) ?? toolCalls.start(projection.toolName);
275
+ await writeSseEvent(response, {
276
+ type: "TOOL_CALL_RESULT",
277
+ timestamp: createTimestamp(),
278
+ toolCallId,
279
+ toolName: projection.toolName,
280
+ result: projection.output,
281
+ ...(projection.isError !== undefined ? { isError: projection.isError } : {}),
282
+ });
283
+ }
284
+ if (isToolTerminalEvent(event) && toolName) {
285
+ const toolCallId = toolCalls.finish(toolName) ?? `tool-${createPersistentId()}`;
286
+ await writeSseEvent(response, {
287
+ type: "TOOL_CALL_END",
288
+ timestamp: createTimestamp(),
289
+ toolCallId,
290
+ toolName,
291
+ });
292
+ }
293
+ if (eventName === "on_chat_model_end" && thinkingMessageStarted) {
294
+ await writeSseEvent(response, {
295
+ type: "THINKING_TEXT_MESSAGE_END",
296
+ timestamp: createTimestamp(),
297
+ messageId: thinkingMessageId,
298
+ });
299
+ thinkingMessageStarted = false;
300
+ }
301
+ },
123
302
  },
124
303
  });
125
304
  runId = runId ?? result.runId;
@@ -133,13 +312,7 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
133
312
  delta: result.output,
134
313
  });
135
314
  }
136
- if (textMessageStarted) {
137
- await writeSseEvent(response, {
138
- type: "TEXT_MESSAGE_END",
139
- timestamp: createTimestamp(),
140
- messageId: messageId,
141
- });
142
- }
315
+ await closeOpenMessages();
143
316
  await writeSseEvent(response, {
144
317
  type: "RUN_FINISHED",
145
318
  timestamp: createTimestamp(),
@@ -151,6 +324,7 @@ export async function serveAgUiOverHttp(runtime, options = {}) {
151
324
  });
152
325
  }
153
326
  catch (error) {
327
+ await closeOpenMessages();
154
328
  await writeSseEvent(response, {
155
329
  type: "RUN_ERROR",
156
330
  timestamp: createTimestamp(),
@@ -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) {
@@ -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.186",
3
+ "version": "0.0.188",
4
4
  "description": "Workspace runtime for multi-agent applications",
5
5
  "type": "module",
6
6
  "packageManager": "npm@10.9.2",