@botbotgo/agent-harness 0.0.187 → 0.0.189
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -2
- package/README.zh.md +23 -2
- package/dist/cli.js +220 -0
- package/dist/config/runtime/workspace.yaml +35 -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/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/runtime/harness/system/policy-engine.js +53 -4
- 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
|
@@ -217,6 +217,25 @@ Real products need a runtime that can answer harder questions:
|
|
|
217
217
|
- It lets YAML own assembly and operating policy while code keeps a small, stable surface
|
|
218
218
|
- It goes deep on runtime concerns that upstream libraries do not fully productize
|
|
219
219
|
|
|
220
|
+
## What To Sell First
|
|
221
|
+
|
|
222
|
+
The product story should stay scenario-shaped instead of drifting back to a generic "multi-agent runtime" pitch.
|
|
223
|
+
|
|
224
|
+
- Enterprise internal agent runtime: approvals, restart-safe recovery, operator evidence, and policy-owned MCP access.
|
|
225
|
+
- Code modernization runtime: long-running coding flows, approval checkpoints, resumable runs, and exported evidence packages.
|
|
226
|
+
- Protocol bridge runtime: ACP, A2A, AG-UI, and runtime MCP on one stable control plane instead of bespoke per-surface glue.
|
|
227
|
+
|
|
228
|
+
Typical runtime governance defaults now look like:
|
|
229
|
+
|
|
230
|
+
```yaml
|
|
231
|
+
governance:
|
|
232
|
+
remoteMcp:
|
|
233
|
+
denyTrustTiers: ["untrusted"]
|
|
234
|
+
denyTenantScopes: ["cross-tenant"]
|
|
235
|
+
denyPromptInjectionRisks: ["high"]
|
|
236
|
+
requireApprovalTransports: ["websocket"]
|
|
237
|
+
```
|
|
238
|
+
|
|
220
239
|
## When To Use It
|
|
221
240
|
|
|
222
241
|
Use `agent-harness` when:
|
|
@@ -953,10 +972,12 @@ ACP transport notes:
|
|
|
953
972
|
|
|
954
973
|
- `serveAcpStdio(runtime)` exposes newline-delimited JSON-RPC over stdio for local IDE, CLI, or subprocess clients.
|
|
955
974
|
- `serveAcpHttp(runtime)` exposes JSON-RPC over HTTP plus SSE runtime events so remote operator surfaces can connect without importing the runtime in-process.
|
|
956
|
-
- `serveA2aHttp(runtime)` exposes
|
|
975
|
+
- `serveA2aHttp(runtime)` exposes an A2A-compatible HTTP JSON-RPC bridge plus agent card discovery, mapping both existing methods such as `message/send` and newer aliases such as `SendMessage`, `GetTask`, `ListTasks`, `CancelTask`, and `SubscribeToTask` onto the existing session/request runtime surface.
|
|
957
976
|
- `serveAgUiHttp(runtime)` exposes an AG-UI-compatible HTTP SSE bridge that projects runtime lifecycle, text output, upstream thinking, step progress, and tool calls onto `RUN_*`, `TEXT_MESSAGE_*`, `THINKING_TEXT_MESSAGE_*`, `STEP_*`, and `TOOL_CALL_*` events for UI clients.
|
|
958
977
|
- `createRuntimeMcpServer(runtime)` and `serveRuntimeMcpOverStdio(runtime)` expose the persisted runtime control surface itself as MCP tools, including sessions, requests, approvals, artifacts, events, and package export helpers.
|
|
959
978
|
- `listRequestEvents(...)` and `exportRequestPackage(...)` are the request-first inspection helpers.
|
|
960
979
|
- `exportRequestPackage(...)` and `exportSessionPackage(...)` package stable runtime records, transcript, approvals, events, and artifacts for operator tooling without reaching into persistence internals.
|
|
961
|
-
- `runtime/default.governance.remoteMcp` can now deny or allow specific MCP servers, raise approval requirements by transport, and stamp transport-based risk tiers into runtime governance bundles.
|
|
980
|
+
- `runtime/default.governance.remoteMcp` can now deny or allow specific MCP servers, raise approval requirements by transport, and stamp transport-based risk tiers into runtime governance bundles. MCP server catalogs can also declare trust tier, access mode, tenant scope, approval policy, prompt-injection risk, and OAuth scope metadata so governance bundles capture why one remote tool is treated as high-risk.
|
|
981
|
+
- `runtime/default.observability.tracing` can now describe exporter metadata such as OTLP endpoints and propagation mode, so frozen runtime snapshots keep trace-correlation plus operator-visible export context without exposing backend-private span internals.
|
|
982
|
+
- `agent-harness runtime health`, `agent-harness runtime approvals list|watch`, and `agent-harness runtime runs list|tail` provide a thin operator CLI over persisted runtime health, approval queues, and active run state.
|
|
962
983
|
- detailed A2A adapter guidance lives in [`docs/a2a-bridge.md`](docs/a2a-bridge.md)
|
package/README.zh.md
CHANGED
|
@@ -214,6 +214,25 @@ AI 让 agent 逻辑、工具调用和工作流代码更容易生成,真正更
|
|
|
214
214
|
- 复杂装配与运行策略交给 YAML,代码面保持小而稳
|
|
215
215
|
- 在上游库未充分产品化的运行时问题上做深做透
|
|
216
216
|
|
|
217
|
+
## 先卖哪三类场景
|
|
218
|
+
|
|
219
|
+
产品叙事应该保持场景化,不要再漂回“通用 multi-agent runtime”。
|
|
220
|
+
|
|
221
|
+
- 企业内部 agent 运行时:审批、重启恢复、operator 证据链,以及由策略持有的 MCP 访问控制。
|
|
222
|
+
- 代码现代化运行时:长链路 coding flow、审批检查点、可恢复 runs,以及可导出的运行证据包。
|
|
223
|
+
- 协议桥接运行时:ACP、A2A、AG-UI 与 runtime MCP 共用一套稳定控制面,而不是每个对外接面各写一层胶水。
|
|
224
|
+
|
|
225
|
+
现在推荐的 runtime 治理默认值大致是:
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
governance:
|
|
229
|
+
remoteMcp:
|
|
230
|
+
denyTrustTiers: ["untrusted"]
|
|
231
|
+
denyTenantScopes: ["cross-tenant"]
|
|
232
|
+
denyPromptInjectionRisks: ["high"]
|
|
233
|
+
requireApprovalTransports: ["websocket"]
|
|
234
|
+
```
|
|
235
|
+
|
|
217
236
|
## 什么时候该用
|
|
218
237
|
|
|
219
238
|
下面这些场景适合用 `agent-harness`:
|
|
@@ -912,10 +931,12 @@ ACP transport 说明:
|
|
|
912
931
|
|
|
913
932
|
- `serveAcpStdio(runtime)` 提供基于 stdio 的 newline-delimited JSON-RPC,适合本地 IDE、CLI 或子进程客户端。
|
|
914
933
|
- `serveAcpHttp(runtime)` 提供基于 HTTP 的 JSON-RPC 与 SSE runtime events,适合远程 operator surface 或独立控制面接入。
|
|
915
|
-
- `serveA2aHttp(runtime)`
|
|
934
|
+
- `serveA2aHttp(runtime)` 提供 A2A HTTP JSON-RPC bridge 与 agent card discovery,同时兼容 `message/send` 这类旧方法,以及 `SendMessage`、`GetTask`、`ListTasks`、`CancelTask`、`SubscribeToTask` 这类更新的方法别名,并统一映射到现有 session/request runtime surface。
|
|
916
935
|
- `serveAgUiHttp(runtime)` 提供 AG-UI HTTP SSE bridge,把 runtime 生命周期、文本输出、upstream thinking、step 进度与 tool call 投影成 `RUN_*`、`TEXT_MESSAGE_*`、`THINKING_TEXT_MESSAGE_*`、`STEP_*` 与 `TOOL_CALL_*` 事件,便于 UI 客户端直接接入。
|
|
917
936
|
- `createRuntimeMcpServer(runtime)` 与 `serveRuntimeMcpOverStdio(runtime)` 会把持久化 runtime 控制面本身暴露成 MCP tools,包括 sessions、requests、approvals、artifacts、events 与 package export helpers。
|
|
918
937
|
- `listRequestEvents(...)` 与 `exportRequestPackage(...)` 是 request-first 的检查 helper。
|
|
919
938
|
- `exportRequestPackage(...)` 与 `exportSessionPackage(...)` 可把稳定 runtime 记录、transcript、approvals、events 和 artifacts 打包给 operator tooling,而不必直接访问 persistence 内部实现。
|
|
920
|
-
- `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。
|
|
939
|
+
- `runtime/default.governance.remoteMcp` 现在可以按 MCP server 或 transport 做 allow/deny、审批升级,并把 transport 风险等级写进 runtime governance bundles。MCP server catalog 也可以声明 trust tier、access mode、tenant scope、approval policy、prompt-injection risk 与 OAuth scope 元数据,让治理快照能解释为什么某个远端工具被视为高风险。
|
|
940
|
+
- `runtime/default.observability.tracing` 现在可描述 OTLP endpoint 和 propagation mode 这类 exporter 元数据,使冻结的 runtime snapshot 在保留 trace correlation 的同时,也能保留对 operator 有意义的导出上下文,而不暴露 backend 私有 span 细节。
|
|
941
|
+
- `agent-harness runtime health`、`agent-harness runtime approvals list|watch` 与 `agent-harness runtime runs list|tail` 提供了一层轻量 operator CLI,可直接查看 runtime health、审批队列和运行状态。
|
|
921
942
|
- 更详细的 A2A 适配层开发说明见 [`docs/a2a-bridge.md`](docs/a2a-bridge.md)
|
package/dist/cli.js
CHANGED
|
@@ -14,6 +14,11 @@ function renderUsage() {
|
|
|
14
14
|
agent-harness acp serve [--workspace <path>] [--transport stdio|http] [--host <hostname>] [--port <port>]
|
|
15
15
|
agent-harness a2a serve [--workspace <path>] [--host <hostname>] [--port <port>]
|
|
16
16
|
agent-harness ag-ui serve [--workspace <path>] [--host <hostname>] [--port <port>]
|
|
17
|
+
agent-harness runtime health [--workspace <path>] [--json]
|
|
18
|
+
agent-harness runtime approvals list [--workspace <path>] [--status <pending|approved|edited|rejected|expired>] [--json]
|
|
19
|
+
agent-harness runtime approvals watch [--workspace <path>] [--status <pending|approved|edited|rejected|expired>] [--poll-ms <ms>] [--once] [--json]
|
|
20
|
+
agent-harness runtime runs list [--workspace <path>] [--agent <agentId>] [--thread <threadId>] [--state <state>] [--json]
|
|
21
|
+
agent-harness runtime runs tail [--workspace <path>] [--agent <agentId>] [--thread <threadId>] [--state <state>] [--poll-ms <ms>] [--once] [--json]
|
|
17
22
|
agent-harness runtime-mcp serve [--workspace <path>]
|
|
18
23
|
`;
|
|
19
24
|
}
|
|
@@ -151,6 +156,152 @@ function parseHttpServeOptions(args, serviceLabel = "HTTP") {
|
|
|
151
156
|
}
|
|
152
157
|
return { workspaceRoot, hostname, port };
|
|
153
158
|
}
|
|
159
|
+
function parseRuntimeInspectOptions(args) {
|
|
160
|
+
let workspaceRoot;
|
|
161
|
+
let json = false;
|
|
162
|
+
let once = false;
|
|
163
|
+
let pollMs = 1000;
|
|
164
|
+
let status;
|
|
165
|
+
let state;
|
|
166
|
+
let agentId;
|
|
167
|
+
let threadId;
|
|
168
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
169
|
+
const arg = args[index];
|
|
170
|
+
if (arg === "--workspace" || arg === "--status" || arg === "--state" || arg === "--agent" || arg === "--thread" || arg === "--poll-ms") {
|
|
171
|
+
const value = args[index + 1];
|
|
172
|
+
if (!value) {
|
|
173
|
+
return { json, once, pollMs, status, state, agentId, threadId, error: `Missing value for ${arg}` };
|
|
174
|
+
}
|
|
175
|
+
if (arg === "--workspace") {
|
|
176
|
+
workspaceRoot = value;
|
|
177
|
+
}
|
|
178
|
+
else if (arg === "--status") {
|
|
179
|
+
status = value;
|
|
180
|
+
}
|
|
181
|
+
else if (arg === "--state") {
|
|
182
|
+
state = value;
|
|
183
|
+
}
|
|
184
|
+
else if (arg === "--agent") {
|
|
185
|
+
agentId = value;
|
|
186
|
+
}
|
|
187
|
+
else if (arg === "--thread") {
|
|
188
|
+
threadId = value;
|
|
189
|
+
}
|
|
190
|
+
else {
|
|
191
|
+
const parsedPoll = Number.parseInt(value, 10);
|
|
192
|
+
if (!Number.isFinite(parsedPoll) || parsedPoll <= 0) {
|
|
193
|
+
return { json, once, pollMs, status, state, agentId, threadId, error: `Invalid poll interval: ${value}` };
|
|
194
|
+
}
|
|
195
|
+
pollMs = parsedPoll;
|
|
196
|
+
}
|
|
197
|
+
index += 1;
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
if (arg === "--json") {
|
|
201
|
+
json = true;
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
if (arg === "--once") {
|
|
205
|
+
once = true;
|
|
206
|
+
continue;
|
|
207
|
+
}
|
|
208
|
+
return { workspaceRoot, json, once, pollMs, status, state, agentId, threadId, error: `Unknown option: ${arg}` };
|
|
209
|
+
}
|
|
210
|
+
return { workspaceRoot, json, once, pollMs, status, state, agentId, threadId };
|
|
211
|
+
}
|
|
212
|
+
function renderJson(value) {
|
|
213
|
+
return `${JSON.stringify(value, null, 2)}\n`;
|
|
214
|
+
}
|
|
215
|
+
function isObject(value) {
|
|
216
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
217
|
+
}
|
|
218
|
+
function formatTimestamp(value) {
|
|
219
|
+
return typeof value === "string" && value.trim().length > 0 ? value.trim() : null;
|
|
220
|
+
}
|
|
221
|
+
function renderHealthSnapshot(snapshot, workspacePath) {
|
|
222
|
+
const lines = [];
|
|
223
|
+
const status = typeof snapshot.status === "string" ? snapshot.status : "unknown";
|
|
224
|
+
lines.push(`Runtime health ${workspacePath}: ${status}`);
|
|
225
|
+
const checks = isObject(snapshot.checks) ? snapshot.checks : {};
|
|
226
|
+
const checkEntries = Object.entries(checks)
|
|
227
|
+
.filter(([, value]) => isObject(value))
|
|
228
|
+
.map(([name, value]) => {
|
|
229
|
+
const check = value;
|
|
230
|
+
const reason = typeof check.reason === "string" && check.reason.trim().length > 0 ? ` (${check.reason})` : "";
|
|
231
|
+
return ` - ${name}: ${typeof check.status === "string" ? check.status : "unknown"}${reason}`;
|
|
232
|
+
});
|
|
233
|
+
if (checkEntries.length > 0) {
|
|
234
|
+
lines.push("Checks:");
|
|
235
|
+
lines.push(...checkEntries);
|
|
236
|
+
}
|
|
237
|
+
const stats = isObject(snapshot.stats) ? snapshot.stats : {};
|
|
238
|
+
const statEntries = [
|
|
239
|
+
["activeRunSlots", stats.activeRunSlots],
|
|
240
|
+
["pendingRunSlots", stats.pendingRunSlots],
|
|
241
|
+
["pendingApprovals", stats.pendingApprovals],
|
|
242
|
+
["stuckRuns", stats.stuckRuns],
|
|
243
|
+
["llmSuccessRate1m", stats.llmSuccessRate1m],
|
|
244
|
+
["llmP95LatencyMs1m", stats.llmP95LatencyMs1m],
|
|
245
|
+
]
|
|
246
|
+
.filter(([, value]) => typeof value === "number")
|
|
247
|
+
.map(([name, value]) => ` - ${name}: ${value}`);
|
|
248
|
+
if (statEntries.length > 0) {
|
|
249
|
+
lines.push("Stats:");
|
|
250
|
+
lines.push(...statEntries);
|
|
251
|
+
}
|
|
252
|
+
const symptoms = Array.isArray(snapshot.symptoms) ? snapshot.symptoms.filter(isObject) : [];
|
|
253
|
+
if (symptoms.length > 0) {
|
|
254
|
+
lines.push("Symptoms:");
|
|
255
|
+
lines.push(...symptoms.map((symptom) => {
|
|
256
|
+
const code = typeof symptom.code === "string" ? symptom.code : "unknown";
|
|
257
|
+
const severity = typeof symptom.severity === "string" ? symptom.severity : "unknown";
|
|
258
|
+
const message = typeof symptom.message === "string" ? symptom.message : "";
|
|
259
|
+
return ` - ${code}: ${severity}${message ? ` (${message})` : ""}`;
|
|
260
|
+
}));
|
|
261
|
+
}
|
|
262
|
+
return `${lines.join("\n")}\n`;
|
|
263
|
+
}
|
|
264
|
+
async function sleep(ms) {
|
|
265
|
+
await new Promise((resolve) => {
|
|
266
|
+
setTimeout(resolve, ms);
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function renderApprovalList(approvals) {
|
|
270
|
+
if (approvals.length === 0) {
|
|
271
|
+
return "No approvals matched.\n";
|
|
272
|
+
}
|
|
273
|
+
return approvals.map((approval) => {
|
|
274
|
+
const status = typeof approval.status === "string" ? approval.status : "unknown";
|
|
275
|
+
const toolName = typeof approval.toolName === "string" ? approval.toolName : "unknown_tool";
|
|
276
|
+
const approvalId = typeof approval.approvalId === "string" ? approval.approvalId : "unknown";
|
|
277
|
+
const threadId = typeof approval.threadId === "string" ? ` thread=${approval.threadId}` : "";
|
|
278
|
+
const runId = typeof approval.runId === "string" ? ` run=${approval.runId}` : "";
|
|
279
|
+
const reason = typeof approval.approvalReason === "string" ? ` reason=${approval.approvalReason}` : "";
|
|
280
|
+
const requestedAt = formatTimestamp(approval.requestedAt);
|
|
281
|
+
const resolvedAt = formatTimestamp(approval.resolvedAt);
|
|
282
|
+
const requested = requestedAt ? ` requested=${requestedAt}` : "";
|
|
283
|
+
const resolved = resolvedAt ? ` resolved=${resolvedAt}` : "";
|
|
284
|
+
return `${approvalId} status=${status} tool=${toolName}${threadId}${runId}${reason}${requested}${resolved}`;
|
|
285
|
+
}).join("\n") + "\n";
|
|
286
|
+
}
|
|
287
|
+
function renderRunList(runs) {
|
|
288
|
+
if (runs.length === 0) {
|
|
289
|
+
return "No runs matched.\n";
|
|
290
|
+
}
|
|
291
|
+
return runs.map((run) => {
|
|
292
|
+
const runId = typeof run.runId === "string" ? run.runId : "unknown";
|
|
293
|
+
const threadId = typeof run.threadId === "string" ? run.threadId : "unknown";
|
|
294
|
+
const agentId = typeof run.agentId === "string" ? run.agentId : "unknown";
|
|
295
|
+
const state = typeof run.state === "string" ? run.state : "unknown";
|
|
296
|
+
const currentAgent = typeof run.currentAgentId === "string" ? ` current=${run.currentAgentId}` : "";
|
|
297
|
+
const resumable = typeof run.resumable === "boolean" ? ` resumable=${run.resumable}` : "";
|
|
298
|
+
const updatedAt = formatTimestamp(run.updatedAt);
|
|
299
|
+
const lastActivityAt = formatTimestamp(run.lastActivityAt);
|
|
300
|
+
const updated = updatedAt ? ` updated=${updatedAt}` : "";
|
|
301
|
+
const lastActivity = lastActivityAt ? ` activity=${lastActivityAt}` : "";
|
|
302
|
+
return `${runId} thread=${threadId} agent=${agentId}${currentAgent} state=${state}${resumable}${updated}${lastActivity}`;
|
|
303
|
+
}).join("\n") + "\n";
|
|
304
|
+
}
|
|
154
305
|
export async function runCli(argv, io = {}, deps = {}) {
|
|
155
306
|
const cwd = io.cwd ?? process.cwd();
|
|
156
307
|
const stdout = io.stdout ?? ((message) => process.stdout.write(message));
|
|
@@ -320,6 +471,75 @@ export async function runCli(argv, io = {}, deps = {}) {
|
|
|
320
471
|
return 1;
|
|
321
472
|
}
|
|
322
473
|
}
|
|
474
|
+
if (command === "runtime") {
|
|
475
|
+
const [subcommand, possibleNestedCommand, ...remainingArgs] = [projectName, ...rest];
|
|
476
|
+
if (!subcommand) {
|
|
477
|
+
stderr(renderUsage());
|
|
478
|
+
return 1;
|
|
479
|
+
}
|
|
480
|
+
const nestedCommand = (subcommand === "approvals" || subcommand === "runs") && possibleNestedCommand
|
|
481
|
+
? possibleNestedCommand
|
|
482
|
+
: undefined;
|
|
483
|
+
const subcommandArgs = nestedCommand ? remainingArgs : [possibleNestedCommand, ...remainingArgs].filter((item) => typeof item === "string");
|
|
484
|
+
const parsed = parseRuntimeInspectOptions(subcommandArgs);
|
|
485
|
+
if (parsed.error) {
|
|
486
|
+
stderr(`${parsed.error}\n`);
|
|
487
|
+
stderr(renderUsage());
|
|
488
|
+
return 1;
|
|
489
|
+
}
|
|
490
|
+
try {
|
|
491
|
+
const runtime = await createHarness(path.resolve(cwd, parsed.workspaceRoot ?? "."));
|
|
492
|
+
const workspacePath = path.resolve(cwd, parsed.workspaceRoot ?? ".");
|
|
493
|
+
if (subcommand === "health") {
|
|
494
|
+
const snapshot = await runtime.getHealth();
|
|
495
|
+
stdout(parsed.json ? renderJson(snapshot) : renderHealthSnapshot(snapshot, workspacePath));
|
|
496
|
+
await runtime.stop();
|
|
497
|
+
return 0;
|
|
498
|
+
}
|
|
499
|
+
if (subcommand === "approvals" && (nestedCommand === "list" || nestedCommand === "watch")) {
|
|
500
|
+
const renderApprovals = async () => {
|
|
501
|
+
const approvals = await runtime.listApprovals(parsed.status ? { status: parsed.status } : undefined);
|
|
502
|
+
stdout(parsed.json ? renderJson(approvals) : renderApprovalList(approvals));
|
|
503
|
+
};
|
|
504
|
+
await renderApprovals();
|
|
505
|
+
if (nestedCommand === "watch" && !parsed.once) {
|
|
506
|
+
for (;;) {
|
|
507
|
+
await sleep(parsed.pollMs);
|
|
508
|
+
await renderApprovals();
|
|
509
|
+
}
|
|
510
|
+
}
|
|
511
|
+
await runtime.stop();
|
|
512
|
+
return 0;
|
|
513
|
+
}
|
|
514
|
+
if (subcommand === "runs" && (nestedCommand === "list" || nestedCommand === "tail")) {
|
|
515
|
+
const renderRuns = async () => {
|
|
516
|
+
const runs = await runtime.listRuns({
|
|
517
|
+
...(parsed.agentId ? { agentId: parsed.agentId } : {}),
|
|
518
|
+
...(parsed.threadId ? { threadId: parsed.threadId } : {}),
|
|
519
|
+
...(parsed.state ? { state: parsed.state } : {}),
|
|
520
|
+
});
|
|
521
|
+
stdout(parsed.json ? renderJson(runs) : renderRunList(runs));
|
|
522
|
+
};
|
|
523
|
+
await renderRuns();
|
|
524
|
+
if (nestedCommand === "tail" && !parsed.once) {
|
|
525
|
+
for (;;) {
|
|
526
|
+
await sleep(parsed.pollMs);
|
|
527
|
+
await renderRuns();
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
await runtime.stop();
|
|
531
|
+
return 0;
|
|
532
|
+
}
|
|
533
|
+
await runtime.stop();
|
|
534
|
+
stderr(renderUsage());
|
|
535
|
+
return 1;
|
|
536
|
+
}
|
|
537
|
+
catch (error) {
|
|
538
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
539
|
+
stderr(`${message}\n`);
|
|
540
|
+
return 1;
|
|
541
|
+
}
|
|
542
|
+
}
|
|
323
543
|
stderr(renderUsage());
|
|
324
544
|
return 1;
|
|
325
545
|
}
|
|
@@ -150,3 +150,38 @@ spec:
|
|
|
150
150
|
- socket hang up
|
|
151
151
|
- econnreset
|
|
152
152
|
- timed out
|
|
153
|
+
|
|
154
|
+
# agent-harness feature: runtime-owned governance defaults for remote MCP access and approval escalation.
|
|
155
|
+
# Keep transport-specific and trust-specific policy here rather than scattering it across app code.
|
|
156
|
+
governance:
|
|
157
|
+
remoteMcp:
|
|
158
|
+
denyTrustTiers:
|
|
159
|
+
- untrusted
|
|
160
|
+
denyTenantScopes:
|
|
161
|
+
- cross-tenant
|
|
162
|
+
denyPromptInjectionRisks:
|
|
163
|
+
- high
|
|
164
|
+
requireApprovalTransports:
|
|
165
|
+
- websocket
|
|
166
|
+
riskByTransport:
|
|
167
|
+
http: medium
|
|
168
|
+
sse: medium
|
|
169
|
+
websocket: high
|
|
170
|
+
inputRiskHintsByTransport:
|
|
171
|
+
http: ["remote-http"]
|
|
172
|
+
sse: ["remote-sse"]
|
|
173
|
+
websocket: ["remote-websocket"]
|
|
174
|
+
|
|
175
|
+
# agent-harness feature: runtime observability defaults for health and tracing/export alignment.
|
|
176
|
+
# These settings are runtime-owned metadata for operator tooling and downstream trace/export integration.
|
|
177
|
+
observability:
|
|
178
|
+
health:
|
|
179
|
+
enabled: true
|
|
180
|
+
evaluateIntervalSeconds: 30
|
|
181
|
+
emitEvents: true
|
|
182
|
+
tracing:
|
|
183
|
+
enabled: true
|
|
184
|
+
propagation: w3c-tracecontext
|
|
185
|
+
exporters:
|
|
186
|
+
- type: otlp-http
|
|
187
|
+
endpoint: https://otel.example.com/v1/traces
|
|
@@ -106,6 +106,12 @@ export type RuntimeGovernanceToolPolicy = {
|
|
|
106
106
|
category: "local" | "backend" | "mcp" | "provider-native";
|
|
107
107
|
mcpServerRef?: string;
|
|
108
108
|
mcpTransport?: string;
|
|
109
|
+
mcpTrustTier?: "trusted" | "reviewed" | "untrusted";
|
|
110
|
+
mcpAccess?: "read-only" | "read-write";
|
|
111
|
+
tenantScope?: "workspace" | "project" | "tenant" | "cross-tenant";
|
|
112
|
+
approvalReason?: string;
|
|
113
|
+
promptInjectionRisk?: RuntimeGovernanceRiskLevel;
|
|
114
|
+
oauthScopes?: string[];
|
|
109
115
|
risk: RuntimeGovernanceRiskLevel;
|
|
110
116
|
requiresApproval: boolean;
|
|
111
117
|
approvalPolicy: "explicit-hitl" | "runtime-default" | "none";
|
|
@@ -72,6 +72,16 @@ export type ParsedMcpServerObject = {
|
|
|
72
72
|
cwd?: string;
|
|
73
73
|
token?: string;
|
|
74
74
|
headers?: Record<string, string>;
|
|
75
|
+
trustTier?: "trusted" | "reviewed" | "untrusted";
|
|
76
|
+
access?: "read-only" | "read-write";
|
|
77
|
+
tenantScope?: "workspace" | "project" | "tenant" | "cross-tenant";
|
|
78
|
+
approvalPolicy?: "always" | "write" | "never";
|
|
79
|
+
promptInjectionRisk?: "low" | "medium" | "high";
|
|
80
|
+
oauth?: {
|
|
81
|
+
provider?: string;
|
|
82
|
+
scopes?: string[];
|
|
83
|
+
};
|
|
84
|
+
labels?: string[];
|
|
75
85
|
sourcePath: string;
|
|
76
86
|
};
|
|
77
87
|
export type ParsedToolObject = {
|
|
@@ -239,6 +249,7 @@ export type CompiledAgentBinding = {
|
|
|
239
249
|
capabilities?: RuntimeCapabilities;
|
|
240
250
|
resilience?: Record<string, unknown>;
|
|
241
251
|
governance?: Record<string, unknown>;
|
|
252
|
+
observability?: Record<string, unknown>;
|
|
242
253
|
deepagent?: {
|
|
243
254
|
description?: string;
|
|
244
255
|
passthrough?: Record<string, unknown>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export declare const AGENT_HARNESS_VERSION = "0.0.188";
|
package/dist/package-version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const AGENT_HARNESS_VERSION = "0.0.
|
|
1
|
+
export const AGENT_HARNESS_VERSION = "0.0.188";
|
|
@@ -17,6 +17,7 @@ export type A2aAgentCard = {
|
|
|
17
17
|
name: string;
|
|
18
18
|
description: string;
|
|
19
19
|
version: string;
|
|
20
|
+
protocolVersion?: string;
|
|
20
21
|
url: string;
|
|
21
22
|
preferredTransport: "JSONRPC";
|
|
22
23
|
provider?: {
|
|
@@ -24,6 +25,9 @@ export type A2aAgentCard = {
|
|
|
24
25
|
url?: string;
|
|
25
26
|
};
|
|
26
27
|
documentationUrl?: string;
|
|
28
|
+
defaultAgentId?: string;
|
|
29
|
+
supportsAuthenticatedExtendedCard?: boolean;
|
|
30
|
+
supportedInterfaces?: string[];
|
|
27
31
|
capabilities: {
|
|
28
32
|
streaming: boolean;
|
|
29
33
|
pushNotifications: boolean;
|
|
@@ -43,6 +47,7 @@ export type A2aTaskState = "submitted" | "working" | "input-required" | "complet
|
|
|
43
47
|
type A2aTaskStatus = {
|
|
44
48
|
state: A2aTaskState;
|
|
45
49
|
timestamp: string;
|
|
50
|
+
messageId?: string;
|
|
46
51
|
message?: {
|
|
47
52
|
role: "agent";
|
|
48
53
|
parts: Array<{
|
|
@@ -97,6 +97,22 @@ function parseTaskLocatorParams(params) {
|
|
|
97
97
|
}
|
|
98
98
|
return { taskId };
|
|
99
99
|
}
|
|
100
|
+
function parseTaskListParams(params) {
|
|
101
|
+
if (!params || typeof params !== "object" || Array.isArray(params)) {
|
|
102
|
+
return { limit: 20 };
|
|
103
|
+
}
|
|
104
|
+
const typed = params;
|
|
105
|
+
const limitValue = typeof typed.limit === "number" && Number.isFinite(typed.limit)
|
|
106
|
+
? Math.max(1, Math.min(100, Math.trunc(typed.limit)))
|
|
107
|
+
: 20;
|
|
108
|
+
return {
|
|
109
|
+
...(typeof typed.agentId === "string" && typed.agentId.trim().length > 0 ? { agentId: typed.agentId.trim() } : {}),
|
|
110
|
+
...(typeof typed.contextId === "string" && typed.contextId.trim().length > 0 ? { contextId: typed.contextId.trim() } : {}),
|
|
111
|
+
...(typeof typed.state === "string" && typed.state.trim().length > 0 ? { state: typed.state.trim() } : {}),
|
|
112
|
+
...(typeof typed.cursor === "string" && typed.cursor.trim().length > 0 ? { cursor: typed.cursor.trim() } : {}),
|
|
113
|
+
limit: limitValue,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
100
116
|
function mapRunState(state) {
|
|
101
117
|
switch (state) {
|
|
102
118
|
case "queued":
|
|
@@ -134,6 +150,63 @@ function contentToText(content) {
|
|
|
134
150
|
.trim();
|
|
135
151
|
return text.length > 0 ? text : undefined;
|
|
136
152
|
}
|
|
153
|
+
function toSessionRecord(session) {
|
|
154
|
+
if (!session) {
|
|
155
|
+
return null;
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
sessionId: session.threadId,
|
|
159
|
+
entryAgentId: session.entryAgentId,
|
|
160
|
+
currentAgentId: session.currentAgentId,
|
|
161
|
+
currentState: session.currentState,
|
|
162
|
+
latestRequestId: session.latestRunId,
|
|
163
|
+
createdAt: session.createdAt,
|
|
164
|
+
updatedAt: session.updatedAt,
|
|
165
|
+
messages: session.messages,
|
|
166
|
+
requests: session.runs.map((run) => ({
|
|
167
|
+
requestId: run.runId,
|
|
168
|
+
sessionId: run.threadId,
|
|
169
|
+
agentId: run.agentId,
|
|
170
|
+
executionMode: run.executionMode,
|
|
171
|
+
adapterKind: run.adapterKind,
|
|
172
|
+
createdAt: run.createdAt,
|
|
173
|
+
updatedAt: run.updatedAt,
|
|
174
|
+
state: run.state,
|
|
175
|
+
checkpointRef: run.checkpointRef,
|
|
176
|
+
resumable: run.resumable,
|
|
177
|
+
startedAt: run.startedAt,
|
|
178
|
+
endedAt: run.endedAt,
|
|
179
|
+
lastActivityAt: run.lastActivityAt,
|
|
180
|
+
currentAgentId: run.currentAgentId,
|
|
181
|
+
delegationChain: run.delegationChain,
|
|
182
|
+
runtimeSnapshot: run.runtimeSnapshot,
|
|
183
|
+
})),
|
|
184
|
+
pendingDecision: session.pendingDecision,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function toRequestRecord(request) {
|
|
188
|
+
if (!request) {
|
|
189
|
+
return null;
|
|
190
|
+
}
|
|
191
|
+
return {
|
|
192
|
+
requestId: request.runId,
|
|
193
|
+
sessionId: request.threadId,
|
|
194
|
+
agentId: request.agentId,
|
|
195
|
+
executionMode: request.executionMode,
|
|
196
|
+
adapterKind: request.adapterKind,
|
|
197
|
+
createdAt: request.createdAt,
|
|
198
|
+
updatedAt: request.updatedAt,
|
|
199
|
+
state: request.state,
|
|
200
|
+
checkpointRef: request.checkpointRef,
|
|
201
|
+
resumable: request.resumable,
|
|
202
|
+
startedAt: request.startedAt,
|
|
203
|
+
endedAt: request.endedAt,
|
|
204
|
+
lastActivityAt: request.lastActivityAt,
|
|
205
|
+
currentAgentId: request.currentAgentId,
|
|
206
|
+
delegationChain: request.delegationChain,
|
|
207
|
+
runtimeSnapshot: request.runtimeSnapshot,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
137
210
|
function buildTaskFromSessionAndRequest(session, request, approvals, output, failureMessage) {
|
|
138
211
|
if (!session || !request) {
|
|
139
212
|
return null;
|
|
@@ -193,52 +266,22 @@ async function buildTaskFromRuntime(runtime, requestId) {
|
|
|
193
266
|
}
|
|
194
267
|
const session = await runtime.getThread(request.threadId);
|
|
195
268
|
const approvals = await runtime.listApprovals({ threadId: request.threadId, runId: request.runId });
|
|
196
|
-
return buildTaskFromSessionAndRequest(session
|
|
197
|
-
|
|
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) {
|
|
@@ -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) {
|
|
@@ -49,6 +49,17 @@ export class PolicyEngine {
|
|
|
49
49
|
const trimmed = value.trim();
|
|
50
50
|
return trimmed.startsWith("mcp/") ? trimmed : `mcp/${trimmed}`;
|
|
51
51
|
};
|
|
52
|
+
const readStringSet = (value) => new Set(Array.isArray(value)
|
|
53
|
+
? value.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
54
|
+
: []);
|
|
55
|
+
const riskRank = {
|
|
56
|
+
low: 0,
|
|
57
|
+
medium: 1,
|
|
58
|
+
high: 2,
|
|
59
|
+
};
|
|
60
|
+
const maxPromptInjectionRisk = remoteMcp.maxPromptInjectionRisk === "low" || remoteMcp.maxPromptInjectionRisk === "medium" || remoteMcp.maxPromptInjectionRisk === "high"
|
|
61
|
+
? remoteMcp.maxPromptInjectionRisk
|
|
62
|
+
: undefined;
|
|
52
63
|
const allowServerRefs = new Set(Array.isArray(remoteMcp.allowServerRefs)
|
|
53
64
|
? remoteMcp.allowServerRefs
|
|
54
65
|
.map((item) => normalizeServerRef(item))
|
|
@@ -59,9 +70,14 @@ export class PolicyEngine {
|
|
|
59
70
|
.map((item) => normalizeServerRef(item))
|
|
60
71
|
.filter((item) => Boolean(item))
|
|
61
72
|
: []);
|
|
62
|
-
const denyTransports =
|
|
63
|
-
|
|
64
|
-
|
|
73
|
+
const denyTransports = readStringSet(remoteMcp.denyTransports);
|
|
74
|
+
const allowTrustTiers = readStringSet(remoteMcp.allowTrustTiers);
|
|
75
|
+
const denyTrustTiers = readStringSet(remoteMcp.denyTrustTiers);
|
|
76
|
+
const allowTenantScopes = readStringSet(remoteMcp.allowTenantScopes);
|
|
77
|
+
const denyTenantScopes = readStringSet(remoteMcp.denyTenantScopes);
|
|
78
|
+
const denyPromptInjectionRisks = readStringSet(remoteMcp.denyPromptInjectionRisks);
|
|
79
|
+
const allowOauthScopes = readStringSet(remoteMcp.allowOauthScopes);
|
|
80
|
+
const denyOauthScopes = readStringSet(remoteMcp.denyOauthScopes);
|
|
65
81
|
const tools = binding.execution?.params?.tools ?? binding.langchainAgentParams?.tools ?? binding.deepAgentParams?.tools ?? [];
|
|
66
82
|
const deniedRemoteTools = tools.flatMap((tool) => {
|
|
67
83
|
if (tool.type !== "mcp") {
|
|
@@ -80,13 +96,43 @@ export class PolicyEngine {
|
|
|
80
96
|
const transport = typeof inlineMcpServer?.transport === "string" && inlineMcpServer.transport.trim().length > 0
|
|
81
97
|
? inlineMcpServer.transport.trim()
|
|
82
98
|
: undefined;
|
|
99
|
+
const trustTier = inlineMcpServer?.trustTier === "trusted" || inlineMcpServer?.trustTier === "reviewed" || inlineMcpServer?.trustTier === "untrusted"
|
|
100
|
+
? inlineMcpServer.trustTier
|
|
101
|
+
: undefined;
|
|
102
|
+
const tenantScope = inlineMcpServer?.tenantScope === "workspace" ||
|
|
103
|
+
inlineMcpServer?.tenantScope === "project" ||
|
|
104
|
+
inlineMcpServer?.tenantScope === "tenant" ||
|
|
105
|
+
inlineMcpServer?.tenantScope === "cross-tenant"
|
|
106
|
+
? inlineMcpServer.tenantScope
|
|
107
|
+
: undefined;
|
|
108
|
+
const promptInjectionRisk = inlineMcpServer?.promptInjectionRisk === "low" ||
|
|
109
|
+
inlineMcpServer?.promptInjectionRisk === "medium" ||
|
|
110
|
+
inlineMcpServer?.promptInjectionRisk === "high"
|
|
111
|
+
? inlineMcpServer.promptInjectionRisk
|
|
112
|
+
: undefined;
|
|
113
|
+
const oauth = typeof inlineMcpServer?.oauth === "object" && inlineMcpServer.oauth && !Array.isArray(inlineMcpServer.oauth)
|
|
114
|
+
? inlineMcpServer.oauth
|
|
115
|
+
: undefined;
|
|
116
|
+
const oauthScopes = Array.isArray(oauth?.scopes)
|
|
117
|
+
? oauth.scopes.filter((item) => typeof item === "string" && item.trim().length > 0).map((item) => item.trim())
|
|
118
|
+
: [];
|
|
83
119
|
const serverDenied = serverRef ? denyServerRefs.has(serverRef) || (allowServerRefs.size > 0 && !allowServerRefs.has(serverRef)) : false;
|
|
84
120
|
const transportDenied = transport ? denyTransports.has(transport) : false;
|
|
85
|
-
|
|
121
|
+
const trustDenied = trustTier ? denyTrustTiers.has(trustTier) || (allowTrustTiers.size > 0 && !allowTrustTiers.has(trustTier)) : false;
|
|
122
|
+
const tenantDenied = tenantScope ? denyTenantScopes.has(tenantScope) || (allowTenantScopes.size > 0 && !allowTenantScopes.has(tenantScope)) : false;
|
|
123
|
+
const promptRiskDenied = (promptInjectionRisk ? denyPromptInjectionRisks.has(promptInjectionRisk) : false)
|
|
124
|
+
|| (promptInjectionRisk && maxPromptInjectionRisk ? riskRank[promptInjectionRisk] > riskRank[maxPromptInjectionRisk] : false);
|
|
125
|
+
const oauthDenied = oauthScopes.some((scope) => denyOauthScopes.has(scope))
|
|
126
|
+
|| (allowOauthScopes.size > 0 && oauthScopes.some((scope) => !allowOauthScopes.has(scope)));
|
|
127
|
+
return serverDenied || transportDenied || trustDenied || tenantDenied || promptRiskDenied || oauthDenied
|
|
86
128
|
? [{
|
|
87
129
|
toolName: tool.name,
|
|
88
130
|
...(serverRef ? { serverRef } : {}),
|
|
89
131
|
...(transport ? { transport } : {}),
|
|
132
|
+
...(trustTier ? { trustTier } : {}),
|
|
133
|
+
...(tenantScope ? { tenantScope } : {}),
|
|
134
|
+
...(promptInjectionRisk ? { promptInjectionRisk } : {}),
|
|
135
|
+
...(oauthScopes.length > 0 ? { oauthScopes } : {}),
|
|
90
136
|
}]
|
|
91
137
|
: [];
|
|
92
138
|
});
|
|
@@ -102,6 +148,9 @@ export class PolicyEngine {
|
|
|
102
148
|
if (tool.transport) {
|
|
103
149
|
return `${tool.toolName} (${tool.transport})`;
|
|
104
150
|
}
|
|
151
|
+
if (tool.trustTier) {
|
|
152
|
+
return `${tool.toolName} (${tool.trustTier})`;
|
|
153
|
+
}
|
|
105
154
|
return tool.toolName;
|
|
106
155
|
});
|
|
107
156
|
reasons.push(`runtime governance denied remote MCP access: ${details.join(", ")}`);
|
|
@@ -341,6 +341,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
|
|
|
341
341
|
? asObject(runtimeDefaults?.filesystem)
|
|
342
342
|
: undefined;
|
|
343
343
|
const runtimeGovernanceDefaults = asObject(runtimeDefaults?.governance);
|
|
344
|
+
const runtimeObservabilityDefaults = asObject(runtimeDefaults?.observability);
|
|
344
345
|
const compiledFilesystemConfig = agent.executionMode === "langchain-v1"
|
|
345
346
|
? mergeConfigObjects(runtimeFilesystemDefaults, getAgentExecutionObject(agent, "filesystem", { executionMode: "langchain-v1" }))
|
|
346
347
|
: undefined;
|
|
@@ -357,6 +358,7 @@ export function compileBinding(workspaceRoot, agent, agents, referencedSubagentI
|
|
|
357
358
|
capabilities: inferAgentCapabilities(agent),
|
|
358
359
|
resilience,
|
|
359
360
|
...(runtimeGovernanceDefaults ? { governance: runtimeGovernanceDefaults } : {}),
|
|
361
|
+
...(runtimeObservabilityDefaults ? { observability: runtimeObservabilityDefaults } : {}),
|
|
360
362
|
...(agent.executionMode === "deepagent"
|
|
361
363
|
? {
|
|
362
364
|
deepagent: {
|
|
@@ -117,6 +117,7 @@ export function parseMcpServerObject(object) {
|
|
|
117
117
|
const value = object.value;
|
|
118
118
|
const env = asObject(value.env);
|
|
119
119
|
const headers = asObject(value.headers);
|
|
120
|
+
const oauth = asObject(value.oauth);
|
|
120
121
|
const transport = String(value.transport ??
|
|
121
122
|
(typeof value.url === "string" && value.url.trim() ? "http" : undefined) ??
|
|
122
123
|
(typeof value.command === "string" && value.command.trim() ? "stdio" : undefined) ??
|
|
@@ -131,6 +132,29 @@ export function parseMcpServerObject(object) {
|
|
|
131
132
|
cwd: typeof value.cwd === "string" ? value.cwd : undefined,
|
|
132
133
|
token: typeof value.token === "string" ? value.token : undefined,
|
|
133
134
|
headers: headers ? Object.fromEntries(Object.entries(headers).filter((entry) => typeof entry[1] === "string")) : undefined,
|
|
135
|
+
trustTier: value.trustTier === "trusted" || value.trustTier === "reviewed" || value.trustTier === "untrusted"
|
|
136
|
+
? value.trustTier
|
|
137
|
+
: undefined,
|
|
138
|
+
access: value.access === "read-only" || value.access === "read-write" ? value.access : undefined,
|
|
139
|
+
tenantScope: value.tenantScope === "workspace" ||
|
|
140
|
+
value.tenantScope === "project" ||
|
|
141
|
+
value.tenantScope === "tenant" ||
|
|
142
|
+
value.tenantScope === "cross-tenant"
|
|
143
|
+
? value.tenantScope
|
|
144
|
+
: undefined,
|
|
145
|
+
approvalPolicy: value.approvalPolicy === "always" || value.approvalPolicy === "write" || value.approvalPolicy === "never"
|
|
146
|
+
? value.approvalPolicy
|
|
147
|
+
: undefined,
|
|
148
|
+
promptInjectionRisk: value.promptInjectionRisk === "low" || value.promptInjectionRisk === "medium" || value.promptInjectionRisk === "high"
|
|
149
|
+
? value.promptInjectionRisk
|
|
150
|
+
: undefined,
|
|
151
|
+
oauth: oauth
|
|
152
|
+
? {
|
|
153
|
+
provider: typeof oauth.provider === "string" ? oauth.provider : undefined,
|
|
154
|
+
scopes: asStringArray(oauth.scopes),
|
|
155
|
+
}
|
|
156
|
+
: undefined,
|
|
157
|
+
labels: asStringArray(value.labels),
|
|
134
158
|
sourcePath: object.sourcePath,
|
|
135
159
|
};
|
|
136
160
|
}
|
|
@@ -15,6 +15,13 @@ function toMcpServerConfig(server) {
|
|
|
15
15
|
url: server.url,
|
|
16
16
|
token: server.token,
|
|
17
17
|
headers: server.headers,
|
|
18
|
+
trustTier: server.trustTier,
|
|
19
|
+
access: server.access,
|
|
20
|
+
tenantScope: server.tenantScope,
|
|
21
|
+
approvalPolicy: server.approvalPolicy,
|
|
22
|
+
promptInjectionRisk: server.promptInjectionRisk,
|
|
23
|
+
oauth: server.oauth,
|
|
24
|
+
labels: server.labels,
|
|
18
25
|
};
|
|
19
26
|
}
|
|
20
27
|
function readStringArray(value) {
|
|
@@ -108,6 +115,29 @@ function mergeReferencedMcpServer(referencedServer, item, agentId, name, sourceP
|
|
|
108
115
|
...(referencedServer.headers ?? {}),
|
|
109
116
|
...(readStringRecord(item.headers) ?? {}),
|
|
110
117
|
},
|
|
118
|
+
trustTier: item.trustTier === "trusted" || item.trustTier === "reviewed" || item.trustTier === "untrusted"
|
|
119
|
+
? item.trustTier
|
|
120
|
+
: referencedServer.trustTier,
|
|
121
|
+
access: item.access === "read-only" || item.access === "read-write" ? item.access : referencedServer.access,
|
|
122
|
+
tenantScope: item.tenantScope === "workspace" ||
|
|
123
|
+
item.tenantScope === "project" ||
|
|
124
|
+
item.tenantScope === "tenant" ||
|
|
125
|
+
item.tenantScope === "cross-tenant"
|
|
126
|
+
? item.tenantScope
|
|
127
|
+
: referencedServer.tenantScope,
|
|
128
|
+
approvalPolicy: item.approvalPolicy === "always" || item.approvalPolicy === "write" || item.approvalPolicy === "never"
|
|
129
|
+
? item.approvalPolicy
|
|
130
|
+
: referencedServer.approvalPolicy,
|
|
131
|
+
promptInjectionRisk: item.promptInjectionRisk === "low" || item.promptInjectionRisk === "medium" || item.promptInjectionRisk === "high"
|
|
132
|
+
? item.promptInjectionRisk
|
|
133
|
+
: referencedServer.promptInjectionRisk,
|
|
134
|
+
oauth: asObject(item.oauth)
|
|
135
|
+
? {
|
|
136
|
+
provider: typeof asObject(item.oauth)?.provider === "string" ? asObject(item.oauth)?.provider : referencedServer.oauth?.provider,
|
|
137
|
+
scopes: readStringArray(asObject(item.oauth)?.scopes) || referencedServer.oauth?.scopes,
|
|
138
|
+
}
|
|
139
|
+
: referencedServer.oauth,
|
|
140
|
+
labels: readStringArray(item.labels).length > 0 ? readStringArray(item.labels) : referencedServer.labels,
|
|
111
141
|
sourcePath,
|
|
112
142
|
};
|
|
113
143
|
}
|
|
@@ -220,6 +250,16 @@ export async function hydrateAgentMcpTools(agents, mcpServers, tools) {
|
|
|
220
250
|
mcp: {
|
|
221
251
|
serverRef: `mcp/${serverId}`,
|
|
222
252
|
},
|
|
253
|
+
mcpServer: {
|
|
254
|
+
transport: parsedServer.transport,
|
|
255
|
+
...(parsedServer.trustTier ? { trustTier: parsedServer.trustTier } : {}),
|
|
256
|
+
...(parsedServer.access ? { access: parsedServer.access } : {}),
|
|
257
|
+
...(parsedServer.tenantScope ? { tenantScope: parsedServer.tenantScope } : {}),
|
|
258
|
+
...(parsedServer.approvalPolicy ? { approvalPolicy: parsedServer.approvalPolicy } : {}),
|
|
259
|
+
...(parsedServer.promptInjectionRisk ? { promptInjectionRisk: parsedServer.promptInjectionRisk } : {}),
|
|
260
|
+
...(parsedServer.oauth ? { oauth: parsedServer.oauth } : {}),
|
|
261
|
+
...(parsedServer.labels && parsedServer.labels.length > 0 ? { labels: parsedServer.labels } : {}),
|
|
262
|
+
},
|
|
223
263
|
},
|
|
224
264
|
mcpRef: remoteTool.name,
|
|
225
265
|
bundleRefs: [],
|