@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 +5 -3
- package/README.zh.md +5 -3
- package/dist/cli.js +162 -0
- package/dist/config/runtime/workspace.yaml +29 -0
- package/dist/contracts/runtime.d.ts +6 -0
- package/dist/contracts/workspace.d.ts +11 -0
- package/dist/package-version.d.ts +1 -1
- package/dist/package-version.js +1 -1
- package/dist/protocol/a2a/http.d.ts +5 -0
- package/dist/protocol/a2a/http.js +130 -95
- package/dist/protocol/ag-ui/http.d.ts +41 -0
- package/dist/protocol/ag-ui/http.js +181 -7
- package/dist/resource/mcp-tool-support.d.ts +10 -0
- package/dist/resource/mcp-tool-support.js +46 -0
- package/dist/runtime/harness/run/governance.js +59 -2
- package/dist/runtime/harness/run/inspection.js +30 -2
- package/dist/workspace/agent-binding-compiler.js +2 -0
- package/dist/workspace/resource-compilers.js +24 -0
- package/dist/workspace/tool-hydration.js +40 -0
- package/package.json +1 -1
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
|
|
957
|
-
- `serveAgUiHttp(runtime)` exposes
|
|
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)`
|
|
916
|
-
- `serveAgUiHttp(runtime)`
|
|
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.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.187";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
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
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
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/
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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: [],
|