@agentwares/agentguard 0.1.5 → 0.1.6
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/CHANGELOG.md +69 -0
- package/README.md +69 -19
- package/dist/cli.js +417 -56
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +9 -1
- package/dist/index.js +819 -458
- package/dist/index.js.map +1 -1
- package/llms.txt +4 -0
- package/package.json +7 -2
- package/server.json +3 -3
package/dist/index.js
CHANGED
|
@@ -845,6 +845,7 @@ var ProxyRuntime = class {
|
|
|
845
845
|
guard;
|
|
846
846
|
agent;
|
|
847
847
|
defaultRunId;
|
|
848
|
+
configured;
|
|
848
849
|
sessions = /* @__PURE__ */ new Set();
|
|
849
850
|
connections = /* @__PURE__ */ new Map();
|
|
850
851
|
exposed = [];
|
|
@@ -861,6 +862,7 @@ var ProxyRuntime = class {
|
|
|
861
862
|
this.guard = opts.guard;
|
|
862
863
|
this.agent = opts.agent;
|
|
863
864
|
this.defaultRunId = opts.defaultRunId;
|
|
865
|
+
this.configured = opts.configured ?? true;
|
|
864
866
|
this.log = opts.log ?? ((line) => process.stderr.write(`[agentguard] ${line}
|
|
865
867
|
`));
|
|
866
868
|
this.connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
|
|
@@ -1328,251 +1330,24 @@ async function withTimeout(promise, ms, what) {
|
|
|
1328
1330
|
}
|
|
1329
1331
|
|
|
1330
1332
|
// src/proxy/server.ts
|
|
1331
|
-
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1333
|
+
import { McpServer as McpServer3 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1332
1334
|
import {
|
|
1333
|
-
CallToolRequestSchema,
|
|
1335
|
+
CallToolRequestSchema as CallToolRequestSchema2,
|
|
1334
1336
|
CompleteRequestSchema,
|
|
1335
1337
|
GetPromptRequestSchema,
|
|
1336
1338
|
ListPromptsRequestSchema,
|
|
1337
1339
|
ListResourceTemplatesRequestSchema,
|
|
1338
1340
|
ListResourcesRequestSchema,
|
|
1339
|
-
ListToolsRequestSchema,
|
|
1341
|
+
ListToolsRequestSchema as ListToolsRequestSchema2,
|
|
1340
1342
|
ReadResourceRequestSchema,
|
|
1341
1343
|
SetLevelRequestSchema,
|
|
1342
|
-
SubscribeRequestSchema,
|
|
1343
|
-
UnsubscribeRequestSchema
|
|
1344
|
+
SubscribeRequestSchema as SubscribeRequestSchema2,
|
|
1345
|
+
UnsubscribeRequestSchema as UnsubscribeRequestSchema2
|
|
1344
1346
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1345
1347
|
import {
|
|
1346
1348
|
keyFromHeaders,
|
|
1347
1349
|
resolveAgent
|
|
1348
1350
|
} from "@agentwares/agentguard-core";
|
|
1349
|
-
var PROXY_VERSION = version();
|
|
1350
|
-
function headerValue(extra, name) {
|
|
1351
|
-
const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
|
|
1352
|
-
if (Array.isArray(raw)) return raw[0];
|
|
1353
|
-
return typeof raw === "string" ? raw : void 0;
|
|
1354
|
-
}
|
|
1355
|
-
function headersOf(extra) {
|
|
1356
|
-
const out = {};
|
|
1357
|
-
for (const [k, v] of Object.entries(extra.requestInfo?.headers ?? {}))
|
|
1358
|
-
out[k.toLowerCase()] = Array.isArray(v) ? v[0] : v;
|
|
1359
|
-
return out;
|
|
1360
|
-
}
|
|
1361
|
-
function runIdFor(extra, meta, fallback) {
|
|
1362
|
-
const header = headerValue(extra, "x-run-id");
|
|
1363
|
-
if (header && header.trim()) return header.trim().slice(0, 128);
|
|
1364
|
-
const fromMeta = meta?.runId ?? meta?.["agentguard/runId"] ?? meta?.run_id;
|
|
1365
|
-
if (typeof fromMeta === "string" && fromMeta.trim()) return fromMeta.trim().slice(0, 128);
|
|
1366
|
-
if (extra.sessionId) return `sess_${extra.sessionId.slice(0, 12)}`;
|
|
1367
|
-
return fallback;
|
|
1368
|
-
}
|
|
1369
|
-
function toCallToolResult(result, tool) {
|
|
1370
|
-
const meta = {
|
|
1371
|
-
agentguard: {
|
|
1372
|
-
class: result.decision.classification.class,
|
|
1373
|
-
verb: result.decision.classification.verb,
|
|
1374
|
-
mode: result.decision.mode,
|
|
1375
|
-
outcome: result.outcome,
|
|
1376
|
-
dryRun: result.faked,
|
|
1377
|
-
seq: result.entry.seq,
|
|
1378
|
-
run_id: result.entry.run_id
|
|
1379
|
-
}
|
|
1380
|
-
};
|
|
1381
|
-
const hasOutputSchema = Boolean(tool?.tool.outputSchema);
|
|
1382
|
-
if (result.error) {
|
|
1383
|
-
const upstream2 = result.value;
|
|
1384
|
-
if (upstream2 && upstream2.content) {
|
|
1385
|
-
return { ...upstream2, isError: true, _meta: { ...upstream2._meta ?? {}, ...meta } };
|
|
1386
|
-
}
|
|
1387
|
-
const body = result.error;
|
|
1388
|
-
return {
|
|
1389
|
-
isError: true,
|
|
1390
|
-
content: [{ type: "text", text: JSON.stringify(body) }],
|
|
1391
|
-
...hasOutputSchema ? {} : { structuredContent: body },
|
|
1392
|
-
_meta: { ...meta, httpStatus: httpStatusFor(body.code) }
|
|
1393
|
-
};
|
|
1394
|
-
}
|
|
1395
|
-
if (result.faked) {
|
|
1396
|
-
const value = result.value;
|
|
1397
|
-
const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
1398
|
-
return {
|
|
1399
|
-
content: [
|
|
1400
|
-
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
|
|
1401
|
-
],
|
|
1402
|
-
...isObject ? { structuredContent: value } : {},
|
|
1403
|
-
_meta: meta
|
|
1404
|
-
};
|
|
1405
|
-
}
|
|
1406
|
-
const upstream = result.value;
|
|
1407
|
-
return { ...upstream, _meta: { ...upstream._meta ?? {}, ...meta } };
|
|
1408
|
-
}
|
|
1409
|
-
function httpStatusFor(code) {
|
|
1410
|
-
switch (code) {
|
|
1411
|
-
case "KILLED":
|
|
1412
|
-
case "TOOL_DENIED":
|
|
1413
|
-
case "APPROVAL_DENIED":
|
|
1414
|
-
return 403;
|
|
1415
|
-
case "APPROVAL_REQUIRED":
|
|
1416
|
-
return 202;
|
|
1417
|
-
case "CAP_EXCEEDED":
|
|
1418
|
-
case "LOOP_DETECTED":
|
|
1419
|
-
return 429;
|
|
1420
|
-
case "UNKNOWN_TOOL":
|
|
1421
|
-
return 404;
|
|
1422
|
-
case "UPSTREAM_ERROR":
|
|
1423
|
-
return 502;
|
|
1424
|
-
default:
|
|
1425
|
-
return 500;
|
|
1426
|
-
}
|
|
1427
|
-
}
|
|
1428
|
-
function annotateTool(t, mode) {
|
|
1429
|
-
return {
|
|
1430
|
-
...t.tool,
|
|
1431
|
-
name: t.name,
|
|
1432
|
-
_meta: {
|
|
1433
|
-
...t.tool._meta ?? {},
|
|
1434
|
-
agentguard: {
|
|
1435
|
-
upstream: t.upstream,
|
|
1436
|
-
class: t.classification.class,
|
|
1437
|
-
verb: t.classification.verb,
|
|
1438
|
-
mode
|
|
1439
|
-
}
|
|
1440
|
-
}
|
|
1441
|
-
};
|
|
1442
|
-
}
|
|
1443
|
-
function killedError(state) {
|
|
1444
|
-
return Object.assign(
|
|
1445
|
-
new Error(`agentguard kill switch is on (${state.source ?? "?"}: ${state.reason ?? ""})`),
|
|
1446
|
-
{ code: -32e3 }
|
|
1447
|
-
);
|
|
1448
|
-
}
|
|
1449
|
-
function createDownstreamServer(runtime, opts = {}) {
|
|
1450
|
-
const server = new McpServer(
|
|
1451
|
-
{ name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
|
|
1452
|
-
{
|
|
1453
|
-
capabilities: {
|
|
1454
|
-
tools: { listChanged: true },
|
|
1455
|
-
resources: { subscribe: true, listChanged: true },
|
|
1456
|
-
prompts: { listChanged: true },
|
|
1457
|
-
logging: {},
|
|
1458
|
-
completions: {}
|
|
1459
|
-
},
|
|
1460
|
-
instructions: `Tool calls pass through agentguard (${runtime.policy.mode} mode). Errors carry { code, cause, fix, retryable }; follow "fix". CAP_EXCEEDED, LOOP_DETECTED and KILLED are not retryable: stop and report to the user. APPROVAL_REQUIRED: relay the approval command to the user, then retry the identical call once.`
|
|
1461
|
-
}
|
|
1462
|
-
);
|
|
1463
|
-
const session = {
|
|
1464
|
-
id: crypto.randomUUID(),
|
|
1465
|
-
server,
|
|
1466
|
-
lastActive: Date.now(),
|
|
1467
|
-
inflight: /* @__PURE__ */ new Map()
|
|
1468
|
-
};
|
|
1469
|
-
runtime.attach(session);
|
|
1470
|
-
const previousOnClose = server.server.onclose;
|
|
1471
|
-
server.server.onclose = () => {
|
|
1472
|
-
previousOnClose?.();
|
|
1473
|
-
runtime.detach(session);
|
|
1474
|
-
};
|
|
1475
|
-
const touch = () => {
|
|
1476
|
-
session.lastActive = Date.now();
|
|
1477
|
-
};
|
|
1478
|
-
const agentFor = async (extra) => {
|
|
1479
|
-
const key = keyFromHeaders(headersOf(extra));
|
|
1480
|
-
if (!key) return { agent: void 0 };
|
|
1481
|
-
const resolved = await resolveAgent(runtime.policy, { key });
|
|
1482
|
-
if (resolved.error) {
|
|
1483
|
-
return {
|
|
1484
|
-
agent: null,
|
|
1485
|
-
error: {
|
|
1486
|
-
code: "TOOL_DENIED",
|
|
1487
|
-
cause: "the agent key in Authorization / X-Agentguard-Key is not in agentguard.yaml",
|
|
1488
|
-
fix: "use a key created with `agentguard key create <agent>`, or omit the key to use the global policy",
|
|
1489
|
-
retryable: false
|
|
1490
|
-
}
|
|
1491
|
-
};
|
|
1492
|
-
}
|
|
1493
|
-
return { agent: resolved.agent };
|
|
1494
|
-
};
|
|
1495
|
-
const low = server.server;
|
|
1496
|
-
low.setRequestHandler(ListToolsRequestSchema, async (_req, extra) => {
|
|
1497
|
-
touch();
|
|
1498
|
-
const { agent } = await agentFor(extra);
|
|
1499
|
-
const scope = agent === void 0 ? runtime.agent : agent;
|
|
1500
|
-
return {
|
|
1501
|
-
tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
|
|
1502
|
-
};
|
|
1503
|
-
});
|
|
1504
|
-
low.setRequestHandler(CallToolRequestSchema, async (req, extra) => {
|
|
1505
|
-
touch();
|
|
1506
|
-
const { agent, error } = await agentFor(extra);
|
|
1507
|
-
const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
|
|
1508
|
-
if (error) {
|
|
1509
|
-
return {
|
|
1510
|
-
isError: true,
|
|
1511
|
-
content: [{ type: "text", text: JSON.stringify(error) }],
|
|
1512
|
-
structuredContent: error,
|
|
1513
|
-
_meta: { httpStatus: 401 }
|
|
1514
|
-
};
|
|
1515
|
-
}
|
|
1516
|
-
const progressToken = req.params._meta?.progressToken;
|
|
1517
|
-
const { result, tool } = await runtime.callTool(req.params.name, req.params.arguments ?? {}, {
|
|
1518
|
-
runId,
|
|
1519
|
-
agent,
|
|
1520
|
-
sessionId: extra.sessionId ?? session.id,
|
|
1521
|
-
session,
|
|
1522
|
-
requestId: extra.requestId,
|
|
1523
|
-
onprogress: progressToken === void 0 ? void 0 : (p) => {
|
|
1524
|
-
void extra.sendNotification({
|
|
1525
|
-
method: "notifications/progress",
|
|
1526
|
-
params: { progressToken, ...p }
|
|
1527
|
-
});
|
|
1528
|
-
}
|
|
1529
|
-
});
|
|
1530
|
-
return toCallToolResult(result, tool);
|
|
1531
|
-
});
|
|
1532
|
-
const guardRead = async () => {
|
|
1533
|
-
const state = await runtime.guard.killState();
|
|
1534
|
-
if (state.killed) throw killedError(state);
|
|
1535
|
-
};
|
|
1536
|
-
low.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
1537
|
-
touch();
|
|
1538
|
-
return runtime.listResources();
|
|
1539
|
-
});
|
|
1540
|
-
low.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
1541
|
-
touch();
|
|
1542
|
-
return runtime.listResourceTemplates();
|
|
1543
|
-
});
|
|
1544
|
-
low.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
1545
|
-
touch();
|
|
1546
|
-
await guardRead();
|
|
1547
|
-
return runtime.readResource(req.params.uri);
|
|
1548
|
-
});
|
|
1549
|
-
low.setRequestHandler(SubscribeRequestSchema, async (req) => {
|
|
1550
|
-
await runtime.subscribe(req.params.uri, true);
|
|
1551
|
-
return {};
|
|
1552
|
-
});
|
|
1553
|
-
low.setRequestHandler(UnsubscribeRequestSchema, async (req) => {
|
|
1554
|
-
await runtime.subscribe(req.params.uri, false);
|
|
1555
|
-
return {};
|
|
1556
|
-
});
|
|
1557
|
-
low.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
1558
|
-
touch();
|
|
1559
|
-
return runtime.listPrompts();
|
|
1560
|
-
});
|
|
1561
|
-
low.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
1562
|
-
touch();
|
|
1563
|
-
await guardRead();
|
|
1564
|
-
return runtime.getPrompt(req.params.name, req.params.arguments);
|
|
1565
|
-
});
|
|
1566
|
-
low.setRequestHandler(CompleteRequestSchema, async (req) => {
|
|
1567
|
-
touch();
|
|
1568
|
-
return runtime.complete(req.params);
|
|
1569
|
-
});
|
|
1570
|
-
low.setRequestHandler(SetLevelRequestSchema, async (req) => {
|
|
1571
|
-
await runtime.setLevel(req.params.level);
|
|
1572
|
-
return {};
|
|
1573
|
-
});
|
|
1574
|
-
return server;
|
|
1575
|
-
}
|
|
1576
1351
|
|
|
1577
1352
|
// ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
|
|
1578
1353
|
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
@@ -1580,8 +1355,8 @@ import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
1580
1355
|
import {
|
|
1581
1356
|
CreateMessageResultSchema,
|
|
1582
1357
|
ElicitResultSchema,
|
|
1583
|
-
SubscribeRequestSchema
|
|
1584
|
-
UnsubscribeRequestSchema
|
|
1358
|
+
SubscribeRequestSchema,
|
|
1359
|
+
UnsubscribeRequestSchema
|
|
1585
1360
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1586
1361
|
import { z } from "zod";
|
|
1587
1362
|
|
|
@@ -1691,11 +1466,11 @@ var StubVerifier = class {
|
|
|
1691
1466
|
// ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
|
|
1692
1467
|
import { z as z2 } from "zod";
|
|
1693
1468
|
import { z as z3 } from "zod";
|
|
1694
|
-
import { McpServer
|
|
1469
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1695
1470
|
import {
|
|
1696
|
-
CallToolRequestSchema
|
|
1471
|
+
CallToolRequestSchema,
|
|
1697
1472
|
ErrorCode,
|
|
1698
|
-
ListToolsRequestSchema
|
|
1473
|
+
ListToolsRequestSchema,
|
|
1699
1474
|
McpError
|
|
1700
1475
|
} from "@modelcontextprotocol/sdk/types.js";
|
|
1701
1476
|
import { z as z4 } from "zod";
|
|
@@ -1798,6 +1573,10 @@ function errorResult(err, opts = {}) {
|
|
|
1798
1573
|
_meta: meta
|
|
1799
1574
|
};
|
|
1800
1575
|
}
|
|
1576
|
+
function createToolContext(init = {}) {
|
|
1577
|
+
const { headers, ...rest } = init;
|
|
1578
|
+
return { headers: headers ?? new Headers(), ...rest };
|
|
1579
|
+
}
|
|
1801
1580
|
var TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
1802
1581
|
var MIN_NAMESPACE_LENGTH = 3;
|
|
1803
1582
|
var MIN_DESCRIPTION_LENGTH = 20;
|
|
@@ -1824,7 +1603,7 @@ function isCallToolResult(value) {
|
|
|
1824
1603
|
(block) => isPlainObject(block) && typeof block.type === "string"
|
|
1825
1604
|
);
|
|
1826
1605
|
}
|
|
1827
|
-
function
|
|
1606
|
+
function toCallToolResult(value) {
|
|
1828
1607
|
if (isCallToolResult(value)) return value;
|
|
1829
1608
|
const text = typeof value === "string" ? value : JSON.stringify(value ?? null, null, 2);
|
|
1830
1609
|
const result = { content: [{ type: "text", text }] };
|
|
@@ -1890,9 +1669,9 @@ function defineTool(opts) {
|
|
|
1890
1669
|
details: { issues: checked.error.issues }
|
|
1891
1670
|
});
|
|
1892
1671
|
}
|
|
1893
|
-
return
|
|
1672
|
+
return toCallToolResult(checked.data);
|
|
1894
1673
|
}
|
|
1895
|
-
return
|
|
1674
|
+
return toCallToolResult(value);
|
|
1896
1675
|
} catch (err) {
|
|
1897
1676
|
return fail2(err);
|
|
1898
1677
|
}
|
|
@@ -2279,7 +2058,7 @@ function createMcpServer(opts) {
|
|
|
2279
2058
|
if (byName.has(tool.name)) throw new Error(`duplicate tool name: ${tool.name}`);
|
|
2280
2059
|
byName.set(tool.name, tool);
|
|
2281
2060
|
}
|
|
2282
|
-
const server = new
|
|
2061
|
+
const server = new McpServer(
|
|
2283
2062
|
{ name: opts.name, version: opts.version, title: opts.title, websiteUrl: opts.websiteUrl },
|
|
2284
2063
|
{ instructions: opts.instructions, capabilities: { logging: {}, ...opts.capabilities } }
|
|
2285
2064
|
);
|
|
@@ -2300,7 +2079,7 @@ function createMcpServer(opts) {
|
|
|
2300
2079
|
);
|
|
2301
2080
|
}
|
|
2302
2081
|
if (byName.size > 0) {
|
|
2303
|
-
server.server.setRequestHandler(
|
|
2082
|
+
server.server.setRequestHandler(ListToolsRequestSchema, async () => {
|
|
2304
2083
|
const enabled = [...byName.values()].filter(
|
|
2305
2084
|
(tool) => registered.get(tool.name)?.enabled !== false
|
|
2306
2085
|
);
|
|
@@ -2312,7 +2091,7 @@ function createMcpServer(opts) {
|
|
|
2312
2091
|
}))
|
|
2313
2092
|
};
|
|
2314
2093
|
});
|
|
2315
|
-
server.server.setRequestHandler(
|
|
2094
|
+
server.server.setRequestHandler(CallToolRequestSchema, async (request, extra) => {
|
|
2316
2095
|
const name = request.params.name;
|
|
2317
2096
|
const tool = byName.get(name);
|
|
2318
2097
|
const entry = registered.get(name);
|
|
@@ -2514,152 +2293,667 @@ function createStatefulHandler({ factory, cors, transportBase }, options) {
|
|
|
2514
2293
|
previousOnClose?.();
|
|
2515
2294
|
if (transport.sessionId) sessions.delete(transport.sessionId);
|
|
2516
2295
|
};
|
|
2517
|
-
const response = withCors(await transport.handleRequest(request), cors);
|
|
2518
|
-
if (!transport.sessionId) server.close().catch(() => void 0);
|
|
2519
|
-
return response;
|
|
2520
|
-
} catch (err) {
|
|
2521
|
-
server.close().catch(() => void 0);
|
|
2522
|
-
return errorResponse(err, cors);
|
|
2296
|
+
const response = withCors(await transport.handleRequest(request), cors);
|
|
2297
|
+
if (!transport.sessionId) server.close().catch(() => void 0);
|
|
2298
|
+
return response;
|
|
2299
|
+
} catch (err) {
|
|
2300
|
+
server.close().catch(() => void 0);
|
|
2301
|
+
return errorResponse(err, cors);
|
|
2302
|
+
}
|
|
2303
|
+
};
|
|
2304
|
+
}
|
|
2305
|
+
function createHttpHandler(opts, transportOptions = {}) {
|
|
2306
|
+
const { factory, transport: t } = splitOptions(opts, transportOptions);
|
|
2307
|
+
const cors = corsHeaders(t.cors);
|
|
2308
|
+
const sessions = t.sessions ? typeof t.sessions === "object" ? t.sessions : {} : void 0;
|
|
2309
|
+
const context = {
|
|
2310
|
+
factory,
|
|
2311
|
+
cors,
|
|
2312
|
+
transportBase: {
|
|
2313
|
+
enableJsonResponse: t.jsonResponse ?? sessions === void 0,
|
|
2314
|
+
allowedHosts: t.allowedHosts,
|
|
2315
|
+
allowedOrigins: t.allowedOrigins,
|
|
2316
|
+
enableDnsRebindingProtection: t.enableDnsRebindingProtection
|
|
2317
|
+
}
|
|
2318
|
+
};
|
|
2319
|
+
const handler = sessions ? createStatefulHandler(context, sessions) : createStatelessHandler(context);
|
|
2320
|
+
const allow = sessions ? "GET, POST, DELETE, OPTIONS" : "POST, OPTIONS";
|
|
2321
|
+
return async (request) => {
|
|
2322
|
+
if (request.method === "OPTIONS") {
|
|
2323
|
+
return new Response(null, { status: 204, headers: { Allow: allow, ...cors } });
|
|
2324
|
+
}
|
|
2325
|
+
return handler(request);
|
|
2326
|
+
};
|
|
2327
|
+
}
|
|
2328
|
+
function localhostHosts(port) {
|
|
2329
|
+
const hosts = ["localhost", "127.0.0.1", "[::1]"];
|
|
2330
|
+
return [...hosts, ...hosts.map((h) => `${h}:${port}`)];
|
|
2331
|
+
}
|
|
2332
|
+
function handleHealth(info) {
|
|
2333
|
+
const tools = typeof info.tools === "number" ? info.tools : info.tools.length;
|
|
2334
|
+
return jsonResponse({ ok: true, name: info.name, version: info.version, tools }, 200);
|
|
2335
|
+
}
|
|
2336
|
+
async function serveNodeHttp(opts) {
|
|
2337
|
+
const http = await import("http");
|
|
2338
|
+
const host = opts.host ?? "127.0.0.1";
|
|
2339
|
+
const path2 = opts.path ?? "/mcp";
|
|
2340
|
+
const server = http.createServer((req, res) => {
|
|
2341
|
+
void (async () => {
|
|
2342
|
+
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
|
|
2343
|
+
let response;
|
|
2344
|
+
if (url.pathname === path2) {
|
|
2345
|
+
const controller = new AbortController();
|
|
2346
|
+
res.on("close", () => {
|
|
2347
|
+
if (!res.writableFinished) controller.abort();
|
|
2348
|
+
});
|
|
2349
|
+
const headers = new Headers();
|
|
2350
|
+
for (const [key, value] of Object.entries(req.headers)) {
|
|
2351
|
+
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
2352
|
+
else if (typeof value === "string") headers.set(key, value);
|
|
2353
|
+
}
|
|
2354
|
+
const method = req.method ?? "GET";
|
|
2355
|
+
let body;
|
|
2356
|
+
if (method !== "GET" && method !== "HEAD") {
|
|
2357
|
+
const chunks = [];
|
|
2358
|
+
for await (const chunk of req) chunks.push(chunk);
|
|
2359
|
+
body = new Uint8Array(Buffer.concat(chunks));
|
|
2360
|
+
}
|
|
2361
|
+
response = await opts.handler(
|
|
2362
|
+
new Request(url, { method, headers, body, signal: controller.signal })
|
|
2363
|
+
);
|
|
2364
|
+
} else if (url.pathname === "/health" && opts.health) {
|
|
2365
|
+
response = opts.health();
|
|
2366
|
+
} else {
|
|
2367
|
+
response = new Response(
|
|
2368
|
+
JSON.stringify({
|
|
2369
|
+
code: "NOT_FOUND",
|
|
2370
|
+
cause: `no route for ${url.pathname}`,
|
|
2371
|
+
fix: `POST JSON-RPC to ${path2}`,
|
|
2372
|
+
retryable: false
|
|
2373
|
+
}),
|
|
2374
|
+
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
2375
|
+
);
|
|
2376
|
+
}
|
|
2377
|
+
const outHeaders = {};
|
|
2378
|
+
response.headers.forEach((value, key) => {
|
|
2379
|
+
outHeaders[key] = value;
|
|
2380
|
+
});
|
|
2381
|
+
res.writeHead(response.status, outHeaders);
|
|
2382
|
+
if (!response.body) {
|
|
2383
|
+
res.end();
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2386
|
+
const reader = response.body.getReader();
|
|
2387
|
+
try {
|
|
2388
|
+
for (; ; ) {
|
|
2389
|
+
const { done, value } = await reader.read();
|
|
2390
|
+
if (done) break;
|
|
2391
|
+
res.write(value);
|
|
2392
|
+
}
|
|
2393
|
+
} catch {
|
|
2394
|
+
}
|
|
2395
|
+
res.end();
|
|
2396
|
+
})().catch((err) => {
|
|
2397
|
+
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
|
2398
|
+
res.end(
|
|
2399
|
+
JSON.stringify({
|
|
2400
|
+
code: "INTERNAL",
|
|
2401
|
+
cause: err instanceof Error ? err.message : String(err),
|
|
2402
|
+
fix: "retry; if it persists, report the request id",
|
|
2403
|
+
retryable: false
|
|
2404
|
+
})
|
|
2405
|
+
);
|
|
2406
|
+
});
|
|
2407
|
+
});
|
|
2408
|
+
await new Promise((resolve5, reject) => {
|
|
2409
|
+
server.once("error", reject);
|
|
2410
|
+
server.listen(opts.port ?? 8765, host, () => resolve5());
|
|
2411
|
+
});
|
|
2412
|
+
const address = server.address();
|
|
2413
|
+
const port = typeof address === "object" && address ? address.port : opts.port ?? 8765;
|
|
2414
|
+
return {
|
|
2415
|
+
port,
|
|
2416
|
+
url: `http://${host}:${port}${path2}`,
|
|
2417
|
+
close: () => new Promise((resolve5, reject) => {
|
|
2418
|
+
server.closeAllConnections();
|
|
2419
|
+
server.close((err) => err ? reject(err) : resolve5());
|
|
2420
|
+
})
|
|
2421
|
+
};
|
|
2422
|
+
}
|
|
2423
|
+
|
|
2424
|
+
// ../../packages/mcp-kit/dist/index.js
|
|
2425
|
+
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2426
|
+
async function serveStdio(serverOrOpts) {
|
|
2427
|
+
const server = serverOrOpts instanceof McpServer2 ? serverOrOpts : createMcpServer(serverOrOpts);
|
|
2428
|
+
const { StdioServerTransport } = await import("@modelcontextprotocol/sdk/server/stdio.js");
|
|
2429
|
+
const transport = new StdioServerTransport();
|
|
2430
|
+
await server.connect(transport);
|
|
2431
|
+
await new Promise((resolve5) => {
|
|
2432
|
+
const previous = transport.onclose;
|
|
2433
|
+
transport.onclose = () => {
|
|
2434
|
+
previous?.();
|
|
2435
|
+
resolve5();
|
|
2436
|
+
};
|
|
2437
|
+
});
|
|
2438
|
+
}
|
|
2439
|
+
|
|
2440
|
+
// src/proxy/own-tools.ts
|
|
2441
|
+
import {
|
|
2442
|
+
buildReport,
|
|
2443
|
+
latestRunId,
|
|
2444
|
+
listRuns,
|
|
2445
|
+
parseAuditJsonl,
|
|
2446
|
+
renderReportMarkdown,
|
|
2447
|
+
verifyChain
|
|
2448
|
+
} from "@agentwares/agentguard-core";
|
|
2449
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2450
|
+
import { z as z5 } from "zod";
|
|
2451
|
+
var INIT_COMMAND = "npx -y @agentwares/agentguard init";
|
|
2452
|
+
var READ_ONLY = {
|
|
2453
|
+
readOnlyHint: true,
|
|
2454
|
+
destructiveHint: false,
|
|
2455
|
+
idempotentHint: true,
|
|
2456
|
+
openWorldHint: false
|
|
2457
|
+
};
|
|
2458
|
+
var runIdInput = z5.string().min(1).max(128).optional().describe(
|
|
2459
|
+
"Run to read, as it appears in the audit log (for example run_20261007093015_ab12, or the X-Run-Id an HTTP client sent). Omit it for the most recent run."
|
|
2460
|
+
);
|
|
2461
|
+
var counters = z5.record(z5.string(), z5.number()).describe(
|
|
2462
|
+
"counter name (tool_calls, writes, deletes, emails, spend_usd or a custom one) \u2192 value"
|
|
2463
|
+
);
|
|
2464
|
+
function readAudit(path2) {
|
|
2465
|
+
if (!existsSync(path2)) return { exists: false, entries: [] };
|
|
2466
|
+
try {
|
|
2467
|
+
return { exists: true, entries: parseAuditJsonl(readFileSync2(path2, "utf8")) };
|
|
2468
|
+
} catch (err) {
|
|
2469
|
+
return { exists: true, entries: [], error: err instanceof Error ? err.message : String(err) };
|
|
2470
|
+
}
|
|
2471
|
+
}
|
|
2472
|
+
function unreadable(path2, error) {
|
|
2473
|
+
return toolError({
|
|
2474
|
+
code: "AUDIT_UNREADABLE",
|
|
2475
|
+
cause: `the audit log at ${path2} could not be read: ${error}`,
|
|
2476
|
+
fix: "call agentguard_verify_audit_log to see where it breaks; a hand-edited or truncated line has to be restored from a copy",
|
|
2477
|
+
retryable: false
|
|
2478
|
+
});
|
|
2479
|
+
}
|
|
2480
|
+
function numbers(caps) {
|
|
2481
|
+
const out = {};
|
|
2482
|
+
for (const [k, v] of Object.entries(caps)) if (typeof v === "number") out[k] = v;
|
|
2483
|
+
return out;
|
|
2484
|
+
}
|
|
2485
|
+
function nextStep(runtime) {
|
|
2486
|
+
if (!runtime.configured)
|
|
2487
|
+
return `agentguard is guarding nothing: no policy file at ${runtime.loaded.path}. Run \`${INIT_COMMAND}\` in the project directory: it writes agentguard.yaml in dry-run mode and routes the project's MCP servers through agentguard (backup kept, \`--undo\` restores). Then restart the MCP client.`;
|
|
2488
|
+
if (runtime.policy.upstreams.length === 0)
|
|
2489
|
+
return `${runtime.loaded.path} lists no upstream servers, so agentguard is guarding nothing. Add the MCP servers your agent uses under \`upstreams:\` in that file (it has commented examples), then restart the MCP client.`;
|
|
2490
|
+
return "agentguard is running; run your agent, then read agentguard_get_report.";
|
|
2491
|
+
}
|
|
2492
|
+
var StatusOutput = z5.object({
|
|
2493
|
+
configured: z5.boolean().describe(
|
|
2494
|
+
"true when a policy file was found and loaded; false means agentguard guards nothing"
|
|
2495
|
+
),
|
|
2496
|
+
policy_path: z5.string().describe("absolute path of the policy file read, or looked for"),
|
|
2497
|
+
mode: z5.enum(["dry-run", "enforce"]).describe("dry-run fakes writes and spends and records them; enforce lets allowed calls run"),
|
|
2498
|
+
upstreams: z5.array(
|
|
2499
|
+
z5.object({
|
|
2500
|
+
name: z5.string().describe("upstream name from the policy"),
|
|
2501
|
+
connected: z5.boolean().describe("whether agentguard is connected to it"),
|
|
2502
|
+
tools: z5.number().int().describe("tools it exposes"),
|
|
2503
|
+
transport: z5.enum(["stdio", "http"]).describe("how agentguard reaches it"),
|
|
2504
|
+
error: z5.string().optional().describe("why it is not connected")
|
|
2505
|
+
})
|
|
2506
|
+
).describe("the MCP servers this proxy fronts; empty means none are configured"),
|
|
2507
|
+
run_id: z5.string().nullable().describe(
|
|
2508
|
+
"run the usage is for: the requested run_id, else the latest run in the log, else null"
|
|
2509
|
+
),
|
|
2510
|
+
caps: z5.object({ per_run: counters, per_day: counters }).describe("limits from the policy; a counter that is absent has no cap"),
|
|
2511
|
+
used: z5.object({ per_run: counters, per_day: counters }).describe("what that run, and today, have used against the caps"),
|
|
2512
|
+
remaining: z5.record(
|
|
2513
|
+
z5.string(),
|
|
2514
|
+
z5.object({ per_run: z5.number().optional(), per_day: z5.number().optional() })
|
|
2515
|
+
).describe("budget left per capped counter, per run and per day"),
|
|
2516
|
+
kill_switch: z5.object({
|
|
2517
|
+
on: z5.boolean().describe("true halts every tool call with KILLED"),
|
|
2518
|
+
reason: z5.string().optional().describe("why it was pulled"),
|
|
2519
|
+
source: z5.string().optional().describe("file, env or memory")
|
|
2520
|
+
}).describe("the kill switch state"),
|
|
2521
|
+
pending_approvals: z5.number().int().describe("destructive calls waiting for a human decision"),
|
|
2522
|
+
audit_log: z5.object({
|
|
2523
|
+
path: z5.string().describe("absolute path of the hash-chained JSONL audit log"),
|
|
2524
|
+
entries: z5.number().int().describe("entries in it; 0 when it does not exist yet")
|
|
2525
|
+
}).describe("where every tool call is recorded"),
|
|
2526
|
+
next_step: z5.string().describe("the one thing to do next, with the command when there is one")
|
|
2527
|
+
});
|
|
2528
|
+
var ReportOutput = z5.object({
|
|
2529
|
+
configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
|
|
2530
|
+
audit_log_path: z5.string().describe("absolute path of the audit log the report was built from"),
|
|
2531
|
+
run_id: z5.string().nullable().describe("the run reported on; null when the log has no runs"),
|
|
2532
|
+
runs_in_log: z5.number().int().describe("how many runs the audit log holds"),
|
|
2533
|
+
tool_calls: z5.number().int().describe("tool calls in this run"),
|
|
2534
|
+
would_have: z5.object({
|
|
2535
|
+
creates: z5.number().int(),
|
|
2536
|
+
updates: z5.number().int(),
|
|
2537
|
+
deletes: z5.number().int(),
|
|
2538
|
+
sends: z5.number().int(),
|
|
2539
|
+
executes: z5.number().int(),
|
|
2540
|
+
spends: z5.number().int(),
|
|
2541
|
+
spend_usd: z5.number()
|
|
2542
|
+
}).describe("what dry-run mode faked: what the run would have done had the calls executed"),
|
|
2543
|
+
did: z5.object({
|
|
2544
|
+
writes: z5.number().int(),
|
|
2545
|
+
deletes: z5.number().int(),
|
|
2546
|
+
sends: z5.number().int(),
|
|
2547
|
+
spend_usd: z5.number()
|
|
2548
|
+
}).describe("what actually executed upstream"),
|
|
2549
|
+
halts: z5.array(
|
|
2550
|
+
z5.object({
|
|
2551
|
+
seq: z5.number().int().describe("audit entry number"),
|
|
2552
|
+
ts: z5.string().describe("ISO time of the call"),
|
|
2553
|
+
code: z5.string().describe("CAP_EXCEEDED, LOOP_DETECTED, KILLED, APPROVAL_REQUIRED, \u2026"),
|
|
2554
|
+
tool: z5.string().describe("the tool that was stopped"),
|
|
2555
|
+
cause: z5.string().describe("why")
|
|
2556
|
+
})
|
|
2557
|
+
).describe("every call agentguard stopped, in order"),
|
|
2558
|
+
top_tools: z5.array(z5.object({ tool: z5.string(), calls: z5.number().int(), class: z5.string() })).describe("the most-called tools with their class (read, write, spend)"),
|
|
2559
|
+
markdown: z5.string().describe("the same report `agentguard report` prints, as Markdown")
|
|
2560
|
+
});
|
|
2561
|
+
var VerifyOutput = z5.object({
|
|
2562
|
+
configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
|
|
2563
|
+
path: z5.string().describe("absolute path of the audit log checked"),
|
|
2564
|
+
exists: z5.boolean().describe("false when no call has been recorded yet; an absent log verifies"),
|
|
2565
|
+
ok: z5.boolean().describe("true when every entry's hash and prev_hash recompute"),
|
|
2566
|
+
entries: z5.number().int().describe("entries checked"),
|
|
2567
|
+
head_hash: z5.string().nullable().describe(
|
|
2568
|
+
"hash of the last entry; record it with the count to detect entries cut from the end"
|
|
2569
|
+
),
|
|
2570
|
+
broken_at: z5.number().int().nullable().describe("seq of the first entry that does not verify"),
|
|
2571
|
+
reason: z5.string().nullable().describe("why verification failed")
|
|
2572
|
+
});
|
|
2573
|
+
function createOwnTools(runtime) {
|
|
2574
|
+
const status = defineTool({
|
|
2575
|
+
name: "agentguard_get_status",
|
|
2576
|
+
title: "agentguard status",
|
|
2577
|
+
description: "Report what agentguard is guarding right now: the policy file it loaded (or that none was found), dry-run or enforce mode, each upstream MCP server and whether it connected, a run's cap usage and remaining budget, the kill switch, and pending approvals. Call it first to learn whether agentguard is set up; next_step says what to do when it is not. For what a run did, use agentguard_get_report; to check the log was not edited, agentguard_verify_audit_log. Reads local files only and changes nothing.",
|
|
2578
|
+
input: z5.strictObject({ run_id: runIdInput }),
|
|
2579
|
+
output: StatusOutput,
|
|
2580
|
+
annotations: { title: "agentguard status", ...READ_ONLY },
|
|
2581
|
+
async handler({ run_id }) {
|
|
2582
|
+
const { loaded, guard } = runtime;
|
|
2583
|
+
const audit = readAudit(loaded.auditPath);
|
|
2584
|
+
if (audit.error) throw unreadable(loaded.auditPath, audit.error);
|
|
2585
|
+
const runId = run_id ?? latestRunId(audit.entries) ?? null;
|
|
2586
|
+
const state = await guard.status(runId ?? "(none yet)");
|
|
2587
|
+
const pending = await guard.approvals.list("pending");
|
|
2588
|
+
return {
|
|
2589
|
+
configured: runtime.configured,
|
|
2590
|
+
policy_path: loaded.path,
|
|
2591
|
+
mode: runtime.policy.mode,
|
|
2592
|
+
upstreams: runtime.upstreamStatus(),
|
|
2593
|
+
run_id: runId,
|
|
2594
|
+
caps: { per_run: numbers(state.caps.per_run), per_day: numbers(state.caps.per_day) },
|
|
2595
|
+
used: state.usage,
|
|
2596
|
+
remaining: state.remaining,
|
|
2597
|
+
kill_switch: {
|
|
2598
|
+
on: state.killed.killed,
|
|
2599
|
+
...state.killed.reason ? { reason: state.killed.reason } : {},
|
|
2600
|
+
...state.killed.source ? { source: state.killed.source } : {}
|
|
2601
|
+
},
|
|
2602
|
+
pending_approvals: pending.length,
|
|
2603
|
+
audit_log: { path: loaded.auditPath, entries: audit.entries.length },
|
|
2604
|
+
next_step: nextStep(runtime)
|
|
2605
|
+
};
|
|
2606
|
+
}
|
|
2607
|
+
});
|
|
2608
|
+
const report = defineTool({
|
|
2609
|
+
name: "agentguard_get_report",
|
|
2610
|
+
title: "agentguard run report",
|
|
2611
|
+
description: "Summarize one run from agentguard's audit log: how many tool calls, what dry-run mode faked (records it would have created, updated or deleted, messages sent, dollars spent), what really ran upstream, and every call agentguard halted with its code and cause. Defaults to the most recent run. Use it after an agent run, before switching a policy from dry-run to enforce; for live caps and the kill switch use agentguard_get_status. Read-only: reads the audit file and does not record this call.",
|
|
2612
|
+
input: z5.strictObject({ run_id: runIdInput }),
|
|
2613
|
+
output: ReportOutput,
|
|
2614
|
+
annotations: { title: "agentguard run report", ...READ_ONLY },
|
|
2615
|
+
handler({ run_id }) {
|
|
2616
|
+
const { loaded } = runtime;
|
|
2617
|
+
const audit = readAudit(loaded.auditPath);
|
|
2618
|
+
if (audit.error) throw unreadable(loaded.auditPath, audit.error);
|
|
2619
|
+
const runs = listRuns(audit.entries);
|
|
2620
|
+
if (run_id && !runs.some((r) => r.runId === run_id)) {
|
|
2621
|
+
throw toolError({
|
|
2622
|
+
code: "NOT_FOUND",
|
|
2623
|
+
cause: `no run "${run_id}" in the audit log at ${loaded.auditPath}`,
|
|
2624
|
+
fix: `call again without run_id for the latest run${runs.length ? `; runs in the log: ${runs.slice(0, 5).map((r) => r.runId).join(", ")}` : "; the log has no runs yet"}`,
|
|
2625
|
+
retryable: false
|
|
2626
|
+
});
|
|
2627
|
+
}
|
|
2628
|
+
const runId = run_id ?? latestRunId(audit.entries) ?? null;
|
|
2629
|
+
const built = buildReport(audit.entries, { runId: runId ?? void 0 });
|
|
2630
|
+
const markdown = runId ? renderReportMarkdown(built) : `# agentguard report
|
|
2631
|
+
|
|
2632
|
+
No runs recorded in ${loaded.auditPath} yet. ${nextStep(runtime)}`;
|
|
2633
|
+
return {
|
|
2634
|
+
configured: runtime.configured,
|
|
2635
|
+
audit_log_path: loaded.auditPath,
|
|
2636
|
+
run_id: runId,
|
|
2637
|
+
runs_in_log: runs.length,
|
|
2638
|
+
tool_calls: runId ? audit.entries.filter((e) => e.run_id === runId).length : 0,
|
|
2639
|
+
would_have: {
|
|
2640
|
+
creates: built.wouldHave.creates,
|
|
2641
|
+
updates: built.wouldHave.updates,
|
|
2642
|
+
deletes: built.wouldHave.deletes,
|
|
2643
|
+
sends: built.wouldHave.sends,
|
|
2644
|
+
executes: built.wouldHave.executes,
|
|
2645
|
+
spends: built.wouldHave.spends,
|
|
2646
|
+
spend_usd: built.wouldHave.spendUsd
|
|
2647
|
+
},
|
|
2648
|
+
did: {
|
|
2649
|
+
writes: built.did.writes,
|
|
2650
|
+
deletes: built.did.deletes,
|
|
2651
|
+
sends: built.did.sends,
|
|
2652
|
+
spend_usd: built.did.spendUsd
|
|
2653
|
+
},
|
|
2654
|
+
halts: built.halts,
|
|
2655
|
+
top_tools: built.topTools,
|
|
2656
|
+
markdown
|
|
2657
|
+
};
|
|
2658
|
+
}
|
|
2659
|
+
});
|
|
2660
|
+
const verify = defineTool({
|
|
2661
|
+
name: "agentguard_verify_audit_log",
|
|
2662
|
+
title: "Verify agentguard audit log",
|
|
2663
|
+
description: "Recompute the hash chain of agentguard's audit log (JSONL, one entry per tool call) and say whether any entry was edited, removed from the middle or reordered, naming the first broken entry. Returns the entry count and head hash; record both elsewhere, because entries cut from the end of the file still verify. Use it before trusting agentguard_get_report. Takes no arguments: it checks the log the loaded policy names. Read-only; a log that does not exist yet verifies as empty.",
|
|
2664
|
+
input: z5.strictObject({}),
|
|
2665
|
+
output: VerifyOutput,
|
|
2666
|
+
annotations: { title: "Verify agentguard audit log", ...READ_ONLY },
|
|
2667
|
+
async handler() {
|
|
2668
|
+
const { loaded } = runtime;
|
|
2669
|
+
const audit = readAudit(loaded.auditPath);
|
|
2670
|
+
if (audit.error) {
|
|
2671
|
+
return {
|
|
2672
|
+
configured: runtime.configured,
|
|
2673
|
+
path: loaded.auditPath,
|
|
2674
|
+
exists: true,
|
|
2675
|
+
ok: false,
|
|
2676
|
+
entries: 0,
|
|
2677
|
+
head_hash: null,
|
|
2678
|
+
broken_at: null,
|
|
2679
|
+
reason: audit.error
|
|
2680
|
+
};
|
|
2681
|
+
}
|
|
2682
|
+
const result = await verifyChain(audit.entries);
|
|
2683
|
+
return {
|
|
2684
|
+
configured: runtime.configured,
|
|
2685
|
+
path: loaded.auditPath,
|
|
2686
|
+
exists: audit.exists,
|
|
2687
|
+
ok: result.ok,
|
|
2688
|
+
entries: result.entries,
|
|
2689
|
+
// an empty chain's head is the genesis hash, which is not an entry anyone can record
|
|
2690
|
+
head_hash: result.entries > 0 ? result.head ?? null : null,
|
|
2691
|
+
broken_at: result.brokenAt ?? null,
|
|
2692
|
+
reason: result.reason ?? null
|
|
2693
|
+
};
|
|
2694
|
+
}
|
|
2695
|
+
});
|
|
2696
|
+
return [status, report, verify];
|
|
2697
|
+
}
|
|
2698
|
+
function ownToolsActive(runtime) {
|
|
2699
|
+
return runtime.upstreamStatus().length === 0;
|
|
2700
|
+
}
|
|
2701
|
+
function ownToolList(tools) {
|
|
2702
|
+
return listToolManifest(tools);
|
|
2703
|
+
}
|
|
2704
|
+
async function callOwnTool(tools, name, args, requestId) {
|
|
2705
|
+
const tool = tools.find((t) => t.name === name);
|
|
2706
|
+
if (!tool) return void 0;
|
|
2707
|
+
return tool.invoke(args, createToolContext({ requestId }));
|
|
2708
|
+
}
|
|
2709
|
+
|
|
2710
|
+
// src/proxy/server.ts
|
|
2711
|
+
var PROXY_VERSION = version();
|
|
2712
|
+
function headerValue(extra, name) {
|
|
2713
|
+
const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
|
|
2714
|
+
if (Array.isArray(raw)) return raw[0];
|
|
2715
|
+
return typeof raw === "string" ? raw : void 0;
|
|
2716
|
+
}
|
|
2717
|
+
function headersOf(extra) {
|
|
2718
|
+
const out = {};
|
|
2719
|
+
for (const [k, v] of Object.entries(extra.requestInfo?.headers ?? {}))
|
|
2720
|
+
out[k.toLowerCase()] = Array.isArray(v) ? v[0] : v;
|
|
2721
|
+
return out;
|
|
2722
|
+
}
|
|
2723
|
+
function runIdFor(extra, meta, fallback) {
|
|
2724
|
+
const header = headerValue(extra, "x-run-id");
|
|
2725
|
+
if (header && header.trim()) return header.trim().slice(0, 128);
|
|
2726
|
+
const fromMeta = meta?.runId ?? meta?.["agentguard/runId"] ?? meta?.run_id;
|
|
2727
|
+
if (typeof fromMeta === "string" && fromMeta.trim()) return fromMeta.trim().slice(0, 128);
|
|
2728
|
+
if (extra.sessionId) return `sess_${extra.sessionId.slice(0, 12)}`;
|
|
2729
|
+
return fallback;
|
|
2730
|
+
}
|
|
2731
|
+
function toCallToolResult2(result, tool) {
|
|
2732
|
+
const meta = {
|
|
2733
|
+
agentguard: {
|
|
2734
|
+
class: result.decision.classification.class,
|
|
2735
|
+
verb: result.decision.classification.verb,
|
|
2736
|
+
mode: result.decision.mode,
|
|
2737
|
+
outcome: result.outcome,
|
|
2738
|
+
dryRun: result.faked,
|
|
2739
|
+
seq: result.entry.seq,
|
|
2740
|
+
run_id: result.entry.run_id
|
|
2741
|
+
}
|
|
2742
|
+
};
|
|
2743
|
+
const hasOutputSchema = Boolean(tool?.tool.outputSchema);
|
|
2744
|
+
if (result.error) {
|
|
2745
|
+
const upstream2 = result.value;
|
|
2746
|
+
if (upstream2 && upstream2.content) {
|
|
2747
|
+
return { ...upstream2, isError: true, _meta: { ...upstream2._meta ?? {}, ...meta } };
|
|
2748
|
+
}
|
|
2749
|
+
const body = result.error;
|
|
2750
|
+
return {
|
|
2751
|
+
isError: true,
|
|
2752
|
+
content: [{ type: "text", text: JSON.stringify(body) }],
|
|
2753
|
+
...hasOutputSchema ? {} : { structuredContent: body },
|
|
2754
|
+
_meta: { ...meta, httpStatus: httpStatusFor(body.code) }
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
if (result.faked) {
|
|
2758
|
+
const value = result.value;
|
|
2759
|
+
const isObject = value !== null && typeof value === "object" && !Array.isArray(value);
|
|
2760
|
+
return {
|
|
2761
|
+
content: [
|
|
2762
|
+
{ type: "text", text: typeof value === "string" ? value : JSON.stringify(value, null, 2) }
|
|
2763
|
+
],
|
|
2764
|
+
...isObject ? { structuredContent: value } : {},
|
|
2765
|
+
_meta: meta
|
|
2766
|
+
};
|
|
2767
|
+
}
|
|
2768
|
+
const upstream = result.value;
|
|
2769
|
+
return { ...upstream, _meta: { ...upstream._meta ?? {}, ...meta } };
|
|
2770
|
+
}
|
|
2771
|
+
function httpStatusFor(code) {
|
|
2772
|
+
switch (code) {
|
|
2773
|
+
case "KILLED":
|
|
2774
|
+
case "TOOL_DENIED":
|
|
2775
|
+
case "APPROVAL_DENIED":
|
|
2776
|
+
return 403;
|
|
2777
|
+
case "APPROVAL_REQUIRED":
|
|
2778
|
+
return 202;
|
|
2779
|
+
case "CAP_EXCEEDED":
|
|
2780
|
+
case "LOOP_DETECTED":
|
|
2781
|
+
return 429;
|
|
2782
|
+
case "UNKNOWN_TOOL":
|
|
2783
|
+
return 404;
|
|
2784
|
+
case "UPSTREAM_ERROR":
|
|
2785
|
+
return 502;
|
|
2786
|
+
default:
|
|
2787
|
+
return 500;
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
function annotateTool(t, mode) {
|
|
2791
|
+
return {
|
|
2792
|
+
...t.tool,
|
|
2793
|
+
name: t.name,
|
|
2794
|
+
_meta: {
|
|
2795
|
+
...t.tool._meta ?? {},
|
|
2796
|
+
agentguard: {
|
|
2797
|
+
upstream: t.upstream,
|
|
2798
|
+
class: t.classification.class,
|
|
2799
|
+
verb: t.classification.verb,
|
|
2800
|
+
mode
|
|
2801
|
+
}
|
|
2802
|
+
}
|
|
2803
|
+
};
|
|
2804
|
+
}
|
|
2805
|
+
function killedError(state) {
|
|
2806
|
+
return Object.assign(
|
|
2807
|
+
new Error(`agentguard kill switch is on (${state.source ?? "?"}: ${state.reason ?? ""})`),
|
|
2808
|
+
{ code: -32e3 }
|
|
2809
|
+
);
|
|
2810
|
+
}
|
|
2811
|
+
function serverInstructions(runtime) {
|
|
2812
|
+
if (ownToolsActive(runtime))
|
|
2813
|
+
return `agentguard is an MCP policy proxy, and this one fronts no upstream MCP servers${runtime.configured ? "" : ` (no policy file at ${runtime.loaded.path})`}, so it serves only its own read-only tools: agentguard_get_status (what it guards, caps, kill switch), agentguard_get_report (what a run did or would have done) and agentguard_verify_audit_log. To guard the user's MCP servers, run \`${INIT_COMMAND}\` in the project directory and restart the client.`;
|
|
2814
|
+
return `Tool calls pass through agentguard (${runtime.policy.mode} mode). Errors carry { code, cause, fix, retryable }; follow "fix". CAP_EXCEEDED, LOOP_DETECTED and KILLED are not retryable: stop and report to the user. APPROVAL_REQUIRED: relay the approval command to the user, then retry the identical call once.`;
|
|
2815
|
+
}
|
|
2816
|
+
function createDownstreamServer(runtime, opts = {}) {
|
|
2817
|
+
const server = new McpServer3(
|
|
2818
|
+
{ name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
|
|
2819
|
+
{
|
|
2820
|
+
capabilities: {
|
|
2821
|
+
tools: { listChanged: true },
|
|
2822
|
+
resources: { subscribe: true, listChanged: true },
|
|
2823
|
+
prompts: { listChanged: true },
|
|
2824
|
+
logging: {},
|
|
2825
|
+
completions: {}
|
|
2826
|
+
},
|
|
2827
|
+
instructions: serverInstructions(runtime)
|
|
2828
|
+
}
|
|
2829
|
+
);
|
|
2830
|
+
const ownTools = createOwnTools(runtime);
|
|
2831
|
+
const session = {
|
|
2832
|
+
id: crypto.randomUUID(),
|
|
2833
|
+
server,
|
|
2834
|
+
lastActive: Date.now(),
|
|
2835
|
+
inflight: /* @__PURE__ */ new Map()
|
|
2836
|
+
};
|
|
2837
|
+
runtime.attach(session);
|
|
2838
|
+
const previousOnClose = server.server.onclose;
|
|
2839
|
+
server.server.onclose = () => {
|
|
2840
|
+
previousOnClose?.();
|
|
2841
|
+
runtime.detach(session);
|
|
2842
|
+
};
|
|
2843
|
+
const touch = () => {
|
|
2844
|
+
session.lastActive = Date.now();
|
|
2845
|
+
};
|
|
2846
|
+
const agentFor = async (extra) => {
|
|
2847
|
+
const key = keyFromHeaders(headersOf(extra));
|
|
2848
|
+
if (!key) return { agent: void 0 };
|
|
2849
|
+
const resolved = await resolveAgent(runtime.policy, { key });
|
|
2850
|
+
if (resolved.error) {
|
|
2851
|
+
return {
|
|
2852
|
+
agent: null,
|
|
2853
|
+
error: {
|
|
2854
|
+
code: "TOOL_DENIED",
|
|
2855
|
+
cause: "the agent key in Authorization / X-Agentguard-Key is not in agentguard.yaml",
|
|
2856
|
+
fix: "use a key created with `agentguard key create <agent>`, or omit the key to use the global policy",
|
|
2857
|
+
retryable: false
|
|
2858
|
+
}
|
|
2859
|
+
};
|
|
2523
2860
|
}
|
|
2861
|
+
return { agent: resolved.agent };
|
|
2524
2862
|
};
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
2532
|
-
|
|
2533
|
-
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2863
|
+
const low = server.server;
|
|
2864
|
+
low.setRequestHandler(ListToolsRequestSchema2, async (_req, extra) => {
|
|
2865
|
+
touch();
|
|
2866
|
+
const { agent } = await agentFor(extra);
|
|
2867
|
+
const scope = agent === void 0 ? runtime.agent : agent;
|
|
2868
|
+
if (ownToolsActive(runtime)) return { tools: ownToolList(ownTools) };
|
|
2869
|
+
return {
|
|
2870
|
+
tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
|
|
2871
|
+
};
|
|
2872
|
+
});
|
|
2873
|
+
low.setRequestHandler(CallToolRequestSchema2, async (req, extra) => {
|
|
2874
|
+
touch();
|
|
2875
|
+
if (ownToolsActive(runtime)) {
|
|
2876
|
+
const own = await callOwnTool(
|
|
2877
|
+
ownTools,
|
|
2878
|
+
req.params.name,
|
|
2879
|
+
req.params.arguments ?? {},
|
|
2880
|
+
String(extra.requestId)
|
|
2881
|
+
);
|
|
2882
|
+
if (own) return own;
|
|
2538
2883
|
}
|
|
2539
|
-
|
|
2540
|
-
|
|
2541
|
-
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2884
|
+
const { agent, error } = await agentFor(extra);
|
|
2885
|
+
const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
|
|
2886
|
+
if (error) {
|
|
2887
|
+
return {
|
|
2888
|
+
isError: true,
|
|
2889
|
+
content: [{ type: "text", text: JSON.stringify(error) }],
|
|
2890
|
+
structuredContent: error,
|
|
2891
|
+
_meta: { httpStatus: 401 }
|
|
2892
|
+
};
|
|
2545
2893
|
}
|
|
2546
|
-
|
|
2547
|
-
|
|
2548
|
-
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
}
|
|
2557
|
-
async function serveNodeHttp(opts) {
|
|
2558
|
-
const http = await import("http");
|
|
2559
|
-
const host = opts.host ?? "127.0.0.1";
|
|
2560
|
-
const path2 = opts.path ?? "/mcp";
|
|
2561
|
-
const server = http.createServer((req, res) => {
|
|
2562
|
-
void (async () => {
|
|
2563
|
-
const url = new URL(req.url ?? "/", `http://${req.headers.host ?? host}`);
|
|
2564
|
-
let response;
|
|
2565
|
-
if (url.pathname === path2) {
|
|
2566
|
-
const controller = new AbortController();
|
|
2567
|
-
res.on("close", () => {
|
|
2568
|
-
if (!res.writableFinished) controller.abort();
|
|
2894
|
+
const progressToken = req.params._meta?.progressToken;
|
|
2895
|
+
const { result, tool } = await runtime.callTool(req.params.name, req.params.arguments ?? {}, {
|
|
2896
|
+
runId,
|
|
2897
|
+
agent,
|
|
2898
|
+
sessionId: extra.sessionId ?? session.id,
|
|
2899
|
+
session,
|
|
2900
|
+
requestId: extra.requestId,
|
|
2901
|
+
onprogress: progressToken === void 0 ? void 0 : (p) => {
|
|
2902
|
+
void extra.sendNotification({
|
|
2903
|
+
method: "notifications/progress",
|
|
2904
|
+
params: { progressToken, ...p }
|
|
2569
2905
|
});
|
|
2570
|
-
const headers = new Headers();
|
|
2571
|
-
for (const [key, value] of Object.entries(req.headers)) {
|
|
2572
|
-
if (Array.isArray(value)) for (const item of value) headers.append(key, item);
|
|
2573
|
-
else if (typeof value === "string") headers.set(key, value);
|
|
2574
|
-
}
|
|
2575
|
-
const method = req.method ?? "GET";
|
|
2576
|
-
let body;
|
|
2577
|
-
if (method !== "GET" && method !== "HEAD") {
|
|
2578
|
-
const chunks = [];
|
|
2579
|
-
for await (const chunk of req) chunks.push(chunk);
|
|
2580
|
-
body = new Uint8Array(Buffer.concat(chunks));
|
|
2581
|
-
}
|
|
2582
|
-
response = await opts.handler(
|
|
2583
|
-
new Request(url, { method, headers, body, signal: controller.signal })
|
|
2584
|
-
);
|
|
2585
|
-
} else if (url.pathname === "/health" && opts.health) {
|
|
2586
|
-
response = opts.health();
|
|
2587
|
-
} else {
|
|
2588
|
-
response = new Response(
|
|
2589
|
-
JSON.stringify({
|
|
2590
|
-
code: "NOT_FOUND",
|
|
2591
|
-
cause: `no route for ${url.pathname}`,
|
|
2592
|
-
fix: `POST JSON-RPC to ${path2}`,
|
|
2593
|
-
retryable: false
|
|
2594
|
-
}),
|
|
2595
|
-
{ status: 404, headers: { "Content-Type": "application/json" } }
|
|
2596
|
-
);
|
|
2597
|
-
}
|
|
2598
|
-
const outHeaders = {};
|
|
2599
|
-
response.headers.forEach((value, key) => {
|
|
2600
|
-
outHeaders[key] = value;
|
|
2601
|
-
});
|
|
2602
|
-
res.writeHead(response.status, outHeaders);
|
|
2603
|
-
if (!response.body) {
|
|
2604
|
-
res.end();
|
|
2605
|
-
return;
|
|
2606
|
-
}
|
|
2607
|
-
const reader = response.body.getReader();
|
|
2608
|
-
try {
|
|
2609
|
-
for (; ; ) {
|
|
2610
|
-
const { done, value } = await reader.read();
|
|
2611
|
-
if (done) break;
|
|
2612
|
-
res.write(value);
|
|
2613
|
-
}
|
|
2614
|
-
} catch {
|
|
2615
2906
|
}
|
|
2616
|
-
res.end();
|
|
2617
|
-
})().catch((err) => {
|
|
2618
|
-
if (!res.headersSent) res.writeHead(500, { "Content-Type": "application/json" });
|
|
2619
|
-
res.end(
|
|
2620
|
-
JSON.stringify({
|
|
2621
|
-
code: "INTERNAL",
|
|
2622
|
-
cause: err instanceof Error ? err.message : String(err),
|
|
2623
|
-
fix: "retry; if it persists, report the request id",
|
|
2624
|
-
retryable: false
|
|
2625
|
-
})
|
|
2626
|
-
);
|
|
2627
2907
|
});
|
|
2908
|
+
return toCallToolResult2(result, tool);
|
|
2628
2909
|
});
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
|
|
2632
|
-
});
|
|
2633
|
-
const address = server.address();
|
|
2634
|
-
const port = typeof address === "object" && address ? address.port : opts.port ?? 8765;
|
|
2635
|
-
return {
|
|
2636
|
-
port,
|
|
2637
|
-
url: `http://${host}:${port}${path2}`,
|
|
2638
|
-
close: () => new Promise((resolve5, reject) => {
|
|
2639
|
-
server.closeAllConnections();
|
|
2640
|
-
server.close((err) => err ? reject(err) : resolve5());
|
|
2641
|
-
})
|
|
2910
|
+
const guardRead = async () => {
|
|
2911
|
+
const state = await runtime.guard.killState();
|
|
2912
|
+
if (state.killed) throw killedError(state);
|
|
2642
2913
|
};
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
async
|
|
2648
|
-
|
|
2649
|
-
|
|
2650
|
-
|
|
2651
|
-
|
|
2652
|
-
|
|
2653
|
-
|
|
2654
|
-
|
|
2655
|
-
|
|
2656
|
-
|
|
2657
|
-
|
|
2914
|
+
low.setRequestHandler(ListResourcesRequestSchema, async () => {
|
|
2915
|
+
touch();
|
|
2916
|
+
return runtime.listResources();
|
|
2917
|
+
});
|
|
2918
|
+
low.setRequestHandler(ListResourceTemplatesRequestSchema, async () => {
|
|
2919
|
+
touch();
|
|
2920
|
+
return runtime.listResourceTemplates();
|
|
2921
|
+
});
|
|
2922
|
+
low.setRequestHandler(ReadResourceRequestSchema, async (req) => {
|
|
2923
|
+
touch();
|
|
2924
|
+
await guardRead();
|
|
2925
|
+
return runtime.readResource(req.params.uri);
|
|
2926
|
+
});
|
|
2927
|
+
low.setRequestHandler(SubscribeRequestSchema2, async (req) => {
|
|
2928
|
+
await runtime.subscribe(req.params.uri, true);
|
|
2929
|
+
return {};
|
|
2930
|
+
});
|
|
2931
|
+
low.setRequestHandler(UnsubscribeRequestSchema2, async (req) => {
|
|
2932
|
+
await runtime.subscribe(req.params.uri, false);
|
|
2933
|
+
return {};
|
|
2934
|
+
});
|
|
2935
|
+
low.setRequestHandler(ListPromptsRequestSchema, async () => {
|
|
2936
|
+
touch();
|
|
2937
|
+
return runtime.listPrompts();
|
|
2938
|
+
});
|
|
2939
|
+
low.setRequestHandler(GetPromptRequestSchema, async (req) => {
|
|
2940
|
+
touch();
|
|
2941
|
+
await guardRead();
|
|
2942
|
+
return runtime.getPrompt(req.params.name, req.params.arguments);
|
|
2943
|
+
});
|
|
2944
|
+
low.setRequestHandler(CompleteRequestSchema, async (req) => {
|
|
2945
|
+
touch();
|
|
2946
|
+
return runtime.complete(req.params);
|
|
2947
|
+
});
|
|
2948
|
+
low.setRequestHandler(SetLevelRequestSchema, async (req) => {
|
|
2949
|
+
await runtime.setLevel(req.params.level);
|
|
2950
|
+
return {};
|
|
2658
2951
|
});
|
|
2952
|
+
return server;
|
|
2659
2953
|
}
|
|
2660
2954
|
|
|
2661
2955
|
// src/proxy/http.ts
|
|
2662
|
-
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
|
|
2956
|
+
import { writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
2663
2957
|
import { createServer } from "http";
|
|
2664
2958
|
import { join } from "path";
|
|
2665
2959
|
import { FileKillSwitch } from "@agentwares/agentguard-core/node";
|
|
@@ -2857,7 +3151,7 @@ async function startHttpProxy(opts) {
|
|
|
2857
3151
|
port,
|
|
2858
3152
|
token,
|
|
2859
3153
|
close: () => new Promise((resolve5, reject) => {
|
|
2860
|
-
if (opts.registerFile !== false &&
|
|
3154
|
+
if (opts.registerFile !== false && existsSync2(regFile)) {
|
|
2861
3155
|
try {
|
|
2862
3156
|
unlinkSync(regFile);
|
|
2863
3157
|
} catch {
|
|
@@ -2878,7 +3172,7 @@ function unauthorized() {
|
|
|
2878
3172
|
}
|
|
2879
3173
|
|
|
2880
3174
|
// src/context.ts
|
|
2881
|
-
import { existsSync as
|
|
3175
|
+
import { existsSync as existsSync3 } from "fs";
|
|
2882
3176
|
import { resolve } from "path";
|
|
2883
3177
|
import { Guard } from "@agentwares/agentguard-core";
|
|
2884
3178
|
import {
|
|
@@ -2888,7 +3182,7 @@ import {
|
|
|
2888
3182
|
FileStateStore,
|
|
2889
3183
|
loadPolicyFile
|
|
2890
3184
|
} from "@agentwares/agentguard-core/node";
|
|
2891
|
-
import { existsSync as exists, readFileSync as
|
|
3185
|
+
import { existsSync as exists, readFileSync as readFileSync3 } from "fs";
|
|
2892
3186
|
|
|
2893
3187
|
// ../../node_modules/.pnpm/postal-mime@2.7.5/node_modules/postal-mime/src/decode-strings.js
|
|
2894
3188
|
var textEncoder = new TextEncoder();
|
|
@@ -3272,7 +3566,7 @@ function readRegistration(loaded) {
|
|
|
3272
3566
|
const file = registrationPath(loaded.stateDir);
|
|
3273
3567
|
if (!exists(file)) return void 0;
|
|
3274
3568
|
try {
|
|
3275
|
-
const reg = JSON.parse(
|
|
3569
|
+
const reg = JSON.parse(readFileSync3(file, "utf8"));
|
|
3276
3570
|
return reg;
|
|
3277
3571
|
} catch {
|
|
3278
3572
|
return void 0;
|
|
@@ -3288,7 +3582,7 @@ function describeError(err) {
|
|
|
3288
3582
|
}
|
|
3289
3583
|
|
|
3290
3584
|
// src/configs.ts
|
|
3291
|
-
import { copyFileSync, existsSync as
|
|
3585
|
+
import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
3292
3586
|
import { homedir as homedir2, platform } from "os";
|
|
3293
3587
|
import { join as join2, resolve as resolve2 } from "path";
|
|
3294
3588
|
function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.env) {
|
|
@@ -3321,6 +3615,12 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
|
|
|
3321
3615
|
serversKey: "servers"
|
|
3322
3616
|
},
|
|
3323
3617
|
{ kind: "generic", label: "mcp.json", path: join2(cwd, "mcp.json"), serversKey: "mcpServers" },
|
|
3618
|
+
{
|
|
3619
|
+
kind: "gemini-project",
|
|
3620
|
+
label: "Gemini CLI (project .gemini/settings.json)",
|
|
3621
|
+
path: join2(cwd, ".gemini", "settings.json"),
|
|
3622
|
+
serversKey: "mcpServers"
|
|
3623
|
+
},
|
|
3324
3624
|
{ kind: "claude-desktop", label: "Claude Desktop", path: desktop, serversKey: "mcpServers" },
|
|
3325
3625
|
{
|
|
3326
3626
|
kind: "cursor-user",
|
|
@@ -3339,11 +3639,20 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
|
|
|
3339
3639
|
label: "Claude Code (user ~/.claude.json)",
|
|
3340
3640
|
path: join2(home, ".claude.json"),
|
|
3341
3641
|
serversKey: "mcpServers"
|
|
3642
|
+
},
|
|
3643
|
+
{
|
|
3644
|
+
kind: "gemini-user",
|
|
3645
|
+
label: "Gemini CLI (user ~/.gemini/settings.json)",
|
|
3646
|
+
path: join2(home, ".gemini", "settings.json"),
|
|
3647
|
+
serversKey: "mcpServers"
|
|
3342
3648
|
}
|
|
3343
3649
|
];
|
|
3344
3650
|
}
|
|
3651
|
+
function isGeminiConfig(file) {
|
|
3652
|
+
return file.kind === "gemini-project" || file.kind === "gemini-user" || /[/\\]\.gemini[/\\]settings\.json$/.test(file.path);
|
|
3653
|
+
}
|
|
3345
3654
|
function detectClientConfigs(cwd, home, env) {
|
|
3346
|
-
return candidateConfigs(cwd, home, env).filter((c) =>
|
|
3655
|
+
return candidateConfigs(cwd, home, env).filter((c) => existsSync4(c.path));
|
|
3347
3656
|
}
|
|
3348
3657
|
function describeConfigPath(path2) {
|
|
3349
3658
|
const abs = resolve2(path2);
|
|
@@ -3351,7 +3660,7 @@ function describeConfigPath(path2) {
|
|
|
3351
3660
|
if (known) return known;
|
|
3352
3661
|
let serversKey = "mcpServers";
|
|
3353
3662
|
try {
|
|
3354
|
-
const parsed = JSON.parse(
|
|
3663
|
+
const parsed = JSON.parse(readFileSync4(abs, "utf8"));
|
|
3355
3664
|
if (parsed.servers && !parsed.mcpServers) serversKey = "servers";
|
|
3356
3665
|
} catch {
|
|
3357
3666
|
}
|
|
@@ -3363,7 +3672,7 @@ function describeConfigPath(path2) {
|
|
|
3363
3672
|
};
|
|
3364
3673
|
}
|
|
3365
3674
|
function readServers(file) {
|
|
3366
|
-
const raw = JSON.parse(
|
|
3675
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
3367
3676
|
const servers = raw[file.serversKey];
|
|
3368
3677
|
if (!servers || typeof servers !== "object") return {};
|
|
3369
3678
|
return servers;
|
|
@@ -3382,10 +3691,11 @@ function serversToUpstreams(servers) {
|
|
|
3382
3691
|
continue;
|
|
3383
3692
|
}
|
|
3384
3693
|
const name = rawName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[^a-zA-Z0-9]+/, "") || "upstream";
|
|
3385
|
-
|
|
3694
|
+
const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
|
|
3695
|
+
if (url) {
|
|
3386
3696
|
upstreams.push({
|
|
3387
3697
|
name,
|
|
3388
|
-
url
|
|
3698
|
+
url,
|
|
3389
3699
|
headers: entry.headers ?? {},
|
|
3390
3700
|
args: [],
|
|
3391
3701
|
env: {},
|
|
@@ -3420,34 +3730,34 @@ function proxyEntry(file, policyPath, opts = {}) {
|
|
|
3420
3730
|
return entry;
|
|
3421
3731
|
}
|
|
3422
3732
|
function rewriteClientConfig(file, policyPath, opts = {}) {
|
|
3423
|
-
const raw = JSON.parse(
|
|
3733
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
3424
3734
|
const servers = raw[file.serversKey] ?? {};
|
|
3425
3735
|
const replaced = Object.keys(servers).filter((n) => !isProxyEntry(servers[n]));
|
|
3426
3736
|
const backup = backupPath(file);
|
|
3427
|
-
if (!
|
|
3737
|
+
if (!existsSync4(backup)) copyFileSync(file.path, backup);
|
|
3428
3738
|
raw[file.serversKey] = { [PROXY_SERVER_NAME]: proxyEntry(file, policyPath, opts) };
|
|
3429
3739
|
writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
|
|
3430
3740
|
return { backup, replaced };
|
|
3431
3741
|
}
|
|
3432
3742
|
function restoreClientConfig(file) {
|
|
3433
3743
|
const backup = backupPath(file);
|
|
3434
|
-
if (!
|
|
3744
|
+
if (!existsSync4(backup)) return false;
|
|
3435
3745
|
copyFileSync(backup, file.path);
|
|
3436
3746
|
return true;
|
|
3437
3747
|
}
|
|
3438
3748
|
function mergeServerIntoConfig(file, name, entry) {
|
|
3439
|
-
const raw = JSON.parse(
|
|
3749
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
3440
3750
|
const servers = raw[file.serversKey] ?? {};
|
|
3441
3751
|
const replaced = name in servers;
|
|
3442
3752
|
const backup = backupPath(file);
|
|
3443
|
-
if (!
|
|
3753
|
+
if (!existsSync4(backup)) copyFileSync(file.path, backup);
|
|
3444
3754
|
raw[file.serversKey] = { ...servers, [name]: entry };
|
|
3445
3755
|
writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
|
|
3446
3756
|
return { backup, replaced };
|
|
3447
3757
|
}
|
|
3448
3758
|
|
|
3449
3759
|
// src/commands/init.ts
|
|
3450
|
-
import { existsSync as
|
|
3760
|
+
import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
|
|
3451
3761
|
import { relative, resolve as resolve3 } from "path";
|
|
3452
3762
|
import YAML from "yaml";
|
|
3453
3763
|
|
|
@@ -3534,6 +3844,10 @@ async function toolsCommand(args, io) {
|
|
|
3534
3844
|
io.err(
|
|
3535
3845
|
`${u.connected ? "ok " : "down"} ${u.name}: ${u.connected ? `${u.tools} tools` : u.error}`
|
|
3536
3846
|
);
|
|
3847
|
+
if (result.upstreams.length === 0)
|
|
3848
|
+
io.err(
|
|
3849
|
+
"no upstreams in the policy: an MCP client sees agentguard's own read-only tools instead (agentguard_get_status, agentguard_get_report, agentguard_verify_audit_log)"
|
|
3850
|
+
);
|
|
3537
3851
|
for (const line of formatToolTable(result.tools)) io.out(line);
|
|
3538
3852
|
const counts = result.tools.reduce(
|
|
3539
3853
|
(acc, t) => ({ ...acc, [t.classification.class]: (acc[t.classification.class] ?? 0) + 1 }),
|
|
@@ -3701,7 +4015,7 @@ async function initCommand(args, io) {
|
|
|
3701
4015
|
`found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to route it through agentguard`
|
|
3702
4016
|
);
|
|
3703
4017
|
}
|
|
3704
|
-
if (!chosen.every((c) =>
|
|
4018
|
+
if (!chosen.every((c) => existsSync5(c.path))) {
|
|
3705
4019
|
io.err(`no MCP config at ${chosen.map((c) => c.path).join(", ")}`);
|
|
3706
4020
|
return 1;
|
|
3707
4021
|
}
|
|
@@ -3716,11 +4030,11 @@ async function initCommand(args, io) {
|
|
|
3716
4030
|
}
|
|
3717
4031
|
if (chosen.length === 0)
|
|
3718
4032
|
io.out(
|
|
3719
|
-
"found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf) \u2014 writing a policy with no upstreams"
|
|
4033
|
+
"found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf, Gemini CLI) \u2014 writing a policy with no upstreams"
|
|
3720
4034
|
);
|
|
3721
4035
|
const { upstreams, skipped } = serversToUpstreams(servers);
|
|
3722
4036
|
for (const s of skipped) io.err(`skipped ${s.name}: ${s.reason}`);
|
|
3723
|
-
if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 &&
|
|
4037
|
+
if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 && existsSync5(policyPath)) {
|
|
3724
4038
|
io.out(
|
|
3725
4039
|
`already initialized: ${rel(policyPath)} exists and ${chosen.map((c) => rel(c.path)).join(", ")} already points at agentguard`
|
|
3726
4040
|
);
|
|
@@ -3747,7 +4061,7 @@ async function initCommand(args, io) {
|
|
|
3747
4061
|
}
|
|
3748
4062
|
}
|
|
3749
4063
|
const mode = flagString(args, "mode") ?? "dry-run";
|
|
3750
|
-
if (
|
|
4064
|
+
if (existsSync5(policyPath) && !flagBool(args, "force")) {
|
|
3751
4065
|
io.out(`kept ${rel(policyPath)} (exists; pass --force to regenerate)`);
|
|
3752
4066
|
} else {
|
|
3753
4067
|
writeFileSync3(policyPath, renderStarterPolicy({ upstreams, tools: probed, mode, probeErrors }));
|
|
@@ -3875,7 +4189,7 @@ async function permissionDiffCommand(args, io) {
|
|
|
3875
4189
|
}
|
|
3876
4190
|
|
|
3877
4191
|
// src/fixtures/crm-server.ts
|
|
3878
|
-
import { z as
|
|
4192
|
+
import { z as z6 } from "zod";
|
|
3879
4193
|
function seedState() {
|
|
3880
4194
|
const contacts = /* @__PURE__ */ new Map();
|
|
3881
4195
|
for (const [id, name, email] of [
|
|
@@ -3890,18 +4204,18 @@ function seedState() {
|
|
|
3890
4204
|
var counter = 0;
|
|
3891
4205
|
var nextId = (prefix) => `${prefix}_${(++counter).toString(36).padStart(4, "0")}`;
|
|
3892
4206
|
function crmTools(state) {
|
|
3893
|
-
const contactSchema =
|
|
3894
|
-
id:
|
|
3895
|
-
name:
|
|
3896
|
-
email:
|
|
3897
|
-
updated_at:
|
|
4207
|
+
const contactSchema = z6.object({
|
|
4208
|
+
id: z6.string(),
|
|
4209
|
+
name: z6.string(),
|
|
4210
|
+
email: z6.string(),
|
|
4211
|
+
updated_at: z6.string()
|
|
3898
4212
|
});
|
|
3899
4213
|
return [
|
|
3900
4214
|
defineTool({
|
|
3901
4215
|
name: "crm_list_contacts",
|
|
3902
4216
|
description: "List every contact in the CRM. Read-only; returns id, name, email and updated_at.",
|
|
3903
|
-
input:
|
|
3904
|
-
output:
|
|
4217
|
+
input: z6.object({ limit: z6.number().int().min(1).max(500).default(100) }),
|
|
4218
|
+
output: z6.object({ contacts: z6.array(contactSchema), total: z6.number() }),
|
|
3905
4219
|
annotations: { readOnlyHint: true },
|
|
3906
4220
|
handler: ({ limit }) => ({
|
|
3907
4221
|
contacts: [...state.contacts.values()].slice(0, limit),
|
|
@@ -3911,7 +4225,7 @@ function crmTools(state) {
|
|
|
3911
4225
|
defineTool({
|
|
3912
4226
|
name: "crm_get_contact",
|
|
3913
4227
|
description: "Fetch one contact by id. Read-only; NOT_FOUND when the id does not exist.",
|
|
3914
|
-
input:
|
|
4228
|
+
input: z6.object({ id: z6.string() }),
|
|
3915
4229
|
output: contactSchema,
|
|
3916
4230
|
annotations: { readOnlyHint: true },
|
|
3917
4231
|
handler: ({ id }) => {
|
|
@@ -3928,7 +4242,7 @@ function crmTools(state) {
|
|
|
3928
4242
|
defineTool({
|
|
3929
4243
|
name: "crm_search",
|
|
3930
4244
|
description: "Search contacts by name or email substring. Returns matching contacts (no annotations on purpose).",
|
|
3931
|
-
input:
|
|
4245
|
+
input: z6.object({ query: z6.string() }),
|
|
3932
4246
|
handler: ({ query }) => ({
|
|
3933
4247
|
contacts: [...state.contacts.values()].filter(
|
|
3934
4248
|
(c) => `${c.name} ${c.email}`.toLowerCase().includes(query.toLowerCase())
|
|
@@ -3938,8 +4252,8 @@ function crmTools(state) {
|
|
|
3938
4252
|
defineTool({
|
|
3939
4253
|
name: "crm_create_contact",
|
|
3940
4254
|
description: "Create a contact. Returns the new id and created_at timestamp.",
|
|
3941
|
-
input:
|
|
3942
|
-
output:
|
|
4255
|
+
input: z6.object({ name: z6.string(), email: z6.string() }),
|
|
4256
|
+
output: z6.object({ id: z6.string(), created_at: z6.string() }),
|
|
3943
4257
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
3944
4258
|
handler: ({ name, email }) => {
|
|
3945
4259
|
const id = nextId("c");
|
|
@@ -3952,11 +4266,11 @@ function crmTools(state) {
|
|
|
3952
4266
|
defineTool({
|
|
3953
4267
|
name: "crm_update_contact",
|
|
3954
4268
|
description: "Update fields on a contact. Returns the id and updated_at; NOT_FOUND for unknown ids.",
|
|
3955
|
-
input:
|
|
3956
|
-
id:
|
|
3957
|
-
fields:
|
|
4269
|
+
input: z6.object({
|
|
4270
|
+
id: z6.string(),
|
|
4271
|
+
fields: z6.object({ name: z6.string().optional(), email: z6.string().optional() })
|
|
3958
4272
|
}),
|
|
3959
|
-
output:
|
|
4273
|
+
output: z6.object({ id: z6.string(), updated_at: z6.string() }),
|
|
3960
4274
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: true },
|
|
3961
4275
|
handler: ({ id, fields }) => {
|
|
3962
4276
|
const c = state.contacts.get(id);
|
|
@@ -3974,8 +4288,8 @@ function crmTools(state) {
|
|
|
3974
4288
|
defineTool({
|
|
3975
4289
|
name: "crm_delete_contact",
|
|
3976
4290
|
description: "Permanently delete a contact. Irreversible. Returns { deleted, id }.",
|
|
3977
|
-
input:
|
|
3978
|
-
output:
|
|
4291
|
+
input: z6.object({ id: z6.string() }),
|
|
4292
|
+
output: z6.object({ deleted: z6.boolean(), id: z6.string() }),
|
|
3979
4293
|
annotations: { readOnlyHint: false, destructiveHint: true },
|
|
3980
4294
|
handler: ({ id }) => {
|
|
3981
4295
|
const existed = state.contacts.delete(id);
|
|
@@ -3986,8 +4300,8 @@ function crmTools(state) {
|
|
|
3986
4300
|
defineTool({
|
|
3987
4301
|
name: "crm_send_email",
|
|
3988
4302
|
description: "Send an email to a contact through the CRM. Returns the message id and sent_at.",
|
|
3989
|
-
input:
|
|
3990
|
-
output:
|
|
4303
|
+
input: z6.object({ to: z6.string(), subject: z6.string(), body: z6.string() }),
|
|
4304
|
+
output: z6.object({ message_id: z6.string(), sent_at: z6.string() }),
|
|
3991
4305
|
handler: ({ to, subject }) => {
|
|
3992
4306
|
state.emails.push({ to, subject });
|
|
3993
4307
|
state.mutations.push({ tool: "crm_send_email", args: { to, subject } });
|
|
@@ -3997,12 +4311,12 @@ function crmTools(state) {
|
|
|
3997
4311
|
defineTool({
|
|
3998
4312
|
name: "crm_charge_card",
|
|
3999
4313
|
description: "Charge a customer's card on file (amount in cents). Returns the charge id and amount_usd.",
|
|
4000
|
-
input:
|
|
4001
|
-
customer_id:
|
|
4002
|
-
amount_cents:
|
|
4003
|
-
currency:
|
|
4314
|
+
input: z6.object({
|
|
4315
|
+
customer_id: z6.string(),
|
|
4316
|
+
amount_cents: z6.number().int().positive(),
|
|
4317
|
+
currency: z6.string().default("usd")
|
|
4004
4318
|
}),
|
|
4005
|
-
output:
|
|
4319
|
+
output: z6.object({ charge_id: z6.string(), amount_usd: z6.number() }),
|
|
4006
4320
|
handler: ({ customer_id, amount_cents }) => {
|
|
4007
4321
|
const amount_usd = amount_cents / 100;
|
|
4008
4322
|
state.charges.push({ customer_id, amount_usd });
|
|
@@ -4013,7 +4327,7 @@ function crmTools(state) {
|
|
|
4013
4327
|
defineTool({
|
|
4014
4328
|
name: "crm_frobnicate",
|
|
4015
4329
|
description: "A tool with no annotations and no recognizable verb \u2014 exists to test `unknown` handling.",
|
|
4016
|
-
input:
|
|
4330
|
+
input: z6.object({}),
|
|
4017
4331
|
handler: () => {
|
|
4018
4332
|
state.mutations.push({ tool: "crm_frobnicate", args: {} });
|
|
4019
4333
|
return { frobnicated: true };
|
|
@@ -4022,7 +4336,7 @@ function crmTools(state) {
|
|
|
4022
4336
|
defineTool({
|
|
4023
4337
|
name: "crm_fail",
|
|
4024
4338
|
description: "Always returns a structured UPSTREAM_ERROR result, for error-path tests.",
|
|
4025
|
-
input:
|
|
4339
|
+
input: z6.object({}),
|
|
4026
4340
|
annotations: { readOnlyHint: true },
|
|
4027
4341
|
handler: () => {
|
|
4028
4342
|
throw toolError("UPSTREAM_ERROR", "the CRM is having a bad day", "retry in a minute", {
|
|
@@ -4179,7 +4493,7 @@ ${steps.length} calls; halts: ${halts.join(", ") || "none"}. Now run: agentguard
|
|
|
4179
4493
|
}
|
|
4180
4494
|
|
|
4181
4495
|
// src/commands/connect.ts
|
|
4182
|
-
import { existsSync as
|
|
4496
|
+
import { existsSync as existsSync6 } from "fs";
|
|
4183
4497
|
import { relative as relative2 } from "path";
|
|
4184
4498
|
var DEFAULT_PROXY_URL = "https://agentwares-agentguard-proxy.vercel.app";
|
|
4185
4499
|
function fail(e) {
|
|
@@ -4257,11 +4571,12 @@ async function connectCommand(args, io, fetchImpl = fetch) {
|
|
|
4257
4571
|
return 1;
|
|
4258
4572
|
}
|
|
4259
4573
|
for (const c of chosen) {
|
|
4260
|
-
if (!
|
|
4574
|
+
if (!existsSync6(c.path)) {
|
|
4261
4575
|
io.err(`no MCP config at ${c.path}`);
|
|
4262
4576
|
return 1;
|
|
4263
4577
|
}
|
|
4264
|
-
const {
|
|
4578
|
+
const forClient = isGeminiConfig(c) ? { httpUrl: entry.url, ...entry.headers ? { headers: entry.headers } : {} } : entry;
|
|
4579
|
+
const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, forClient);
|
|
4265
4580
|
io.out(
|
|
4266
4581
|
`${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
|
|
4267
4582
|
);
|
|
@@ -4376,13 +4691,13 @@ async function approvalsCommand(args, io) {
|
|
|
4376
4691
|
}
|
|
4377
4692
|
|
|
4378
4693
|
// src/commands/keys.ts
|
|
4379
|
-
import { readFileSync as
|
|
4694
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
4380
4695
|
import YAML2, { isMap, isSeq, YAMLSeq } from "yaml";
|
|
4381
4696
|
import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
|
|
4382
4697
|
async function keyCommand(args, io) {
|
|
4383
4698
|
const [sub, name] = args.positionals;
|
|
4384
4699
|
const path2 = policyPathFrom(args, io);
|
|
4385
|
-
const doc = YAML2.parseDocument(
|
|
4700
|
+
const doc = YAML2.parseDocument(readFileSync5(path2, "utf8"));
|
|
4386
4701
|
let agents = doc.get("agents");
|
|
4387
4702
|
if (!isSeq(agents)) {
|
|
4388
4703
|
agents = new YAMLSeq();
|
|
@@ -4465,14 +4780,65 @@ async function keyCommand(args, io) {
|
|
|
4465
4780
|
}
|
|
4466
4781
|
|
|
4467
4782
|
// src/commands/proxy.ts
|
|
4468
|
-
import {
|
|
4783
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4784
|
+
import { dirname } from "path";
|
|
4785
|
+
import { Guard as Guard3, defaultPolicy, resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
|
|
4786
|
+
import { FileKillSwitch as FileKillSwitch3, resolvePath } from "@agentwares/agentguard-core/node";
|
|
4469
4787
|
function newRunId(now = /* @__PURE__ */ new Date()) {
|
|
4470
4788
|
return `run_${now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4471
4789
|
}
|
|
4790
|
+
function unconfiguredPolicy(args, io) {
|
|
4791
|
+
if (flagString(args, "config") !== void 0 || io.env.AGENTGUARD_CONFIG) return void 0;
|
|
4792
|
+
const path2 = policyPathFrom(args, io);
|
|
4793
|
+
if (existsSync7(path2)) return void 0;
|
|
4794
|
+
const policy = defaultPolicy();
|
|
4795
|
+
const baseDir = dirname(path2);
|
|
4796
|
+
return {
|
|
4797
|
+
policy,
|
|
4798
|
+
path: path2,
|
|
4799
|
+
baseDir,
|
|
4800
|
+
stateDir: resolvePath(policy.state.dir, baseDir),
|
|
4801
|
+
auditPath: resolvePath(policy.audit.path, baseDir),
|
|
4802
|
+
killPath: resolvePath(policy.kill.file, baseDir)
|
|
4803
|
+
};
|
|
4804
|
+
}
|
|
4805
|
+
async function serveOverStdio(runtime) {
|
|
4806
|
+
const server = createDownstreamServer(runtime);
|
|
4807
|
+
const stop = async () => {
|
|
4808
|
+
await runtime.close();
|
|
4809
|
+
};
|
|
4810
|
+
process.once("SIGINT", () => void stop().then(() => process.exit(0)));
|
|
4811
|
+
process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
|
|
4812
|
+
await serveStdio(server);
|
|
4813
|
+
await stop();
|
|
4814
|
+
return 0;
|
|
4815
|
+
}
|
|
4472
4816
|
async function proxyCommand(args, io) {
|
|
4473
|
-
const loaded = loadPolicyFor(args, io);
|
|
4474
4817
|
const log = (line) => io.err(`[agentguard] ${line}`);
|
|
4475
4818
|
const modeOverride = flagString(args, "mode");
|
|
4819
|
+
const http = flagBool(args, "http") === true;
|
|
4820
|
+
const unconfigured = http ? void 0 : unconfiguredPolicy(args, io);
|
|
4821
|
+
if (unconfigured) {
|
|
4822
|
+
if (modeOverride === "dry-run" || modeOverride === "enforce")
|
|
4823
|
+
unconfigured.policy.mode = modeOverride;
|
|
4824
|
+
const runtime2 = new ProxyRuntime({
|
|
4825
|
+
loaded: unconfigured,
|
|
4826
|
+
guard: new Guard3({
|
|
4827
|
+
policy: unconfigured.policy,
|
|
4828
|
+
env: io.env,
|
|
4829
|
+
kill: new FileKillSwitch3(unconfigured.killPath)
|
|
4830
|
+
}),
|
|
4831
|
+
configured: false,
|
|
4832
|
+
defaultRunId: flagString(args, "run-id") ?? newRunId(),
|
|
4833
|
+
log
|
|
4834
|
+
});
|
|
4835
|
+
await runtime2.start();
|
|
4836
|
+
log(
|
|
4837
|
+
`no policy file at ${unconfigured.path}: serving agentguard's own read-only tools only. To guard your MCP servers, run \`${INIT_COMMAND}\` in the project directory.`
|
|
4838
|
+
);
|
|
4839
|
+
return serveOverStdio(runtime2);
|
|
4840
|
+
}
|
|
4841
|
+
const loaded = loadPolicyFor(args, io);
|
|
4476
4842
|
if (modeOverride === "dry-run" || modeOverride === "enforce") loaded.policy.mode = modeOverride;
|
|
4477
4843
|
const agentName = flagString(args, "agent");
|
|
4478
4844
|
let agent = void 0;
|
|
@@ -4486,7 +4852,6 @@ async function proxyCommand(args, io) {
|
|
|
4486
4852
|
}
|
|
4487
4853
|
agent = resolved.agent;
|
|
4488
4854
|
}
|
|
4489
|
-
const http = flagBool(args, "http") === true;
|
|
4490
4855
|
let approvalBase;
|
|
4491
4856
|
const fileGuard = createFileGuard(loaded, {
|
|
4492
4857
|
log,
|
|
@@ -4512,59 +4877,55 @@ async function proxyCommand(args, io) {
|
|
|
4512
4877
|
log(
|
|
4513
4878
|
`kill switch is ON (${killed.reason}); every call returns KILLED until \`agentguard resume\``
|
|
4514
4879
|
);
|
|
4880
|
+
if (runtime.upstreamStatus().length === 0)
|
|
4881
|
+
log(
|
|
4882
|
+
`${loaded.path} lists no upstreams: serving agentguard's own read-only tools only. Add servers under \`upstreams:\` to guard them.`
|
|
4883
|
+
);
|
|
4515
4884
|
if (http) {
|
|
4516
|
-
const
|
|
4885
|
+
const server = await startHttpProxy({
|
|
4517
4886
|
runtime,
|
|
4518
4887
|
port: flagNumber(args, "port") ?? 8788,
|
|
4519
4888
|
host: flagString(args, "host") ?? "127.0.0.1",
|
|
4520
4889
|
token: io.env.AGENTGUARD_HTTP_TOKEN
|
|
4521
4890
|
});
|
|
4522
|
-
approvalBase = { baseUrl:
|
|
4891
|
+
approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
|
|
4523
4892
|
log(
|
|
4524
|
-
`Streamable HTTP at ${
|
|
4893
|
+
`Streamable HTTP at ${server.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
|
|
4525
4894
|
);
|
|
4526
4895
|
log(
|
|
4527
|
-
`point your agent at ${
|
|
4896
|
+
`point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
|
|
4528
4897
|
);
|
|
4529
|
-
const
|
|
4530
|
-
await
|
|
4898
|
+
const stop = async () => {
|
|
4899
|
+
await server.close();
|
|
4531
4900
|
await runtime.close();
|
|
4532
4901
|
process.exit(0);
|
|
4533
4902
|
};
|
|
4534
|
-
process.once("SIGINT", () => void
|
|
4535
|
-
process.once("SIGTERM", () => void
|
|
4903
|
+
process.once("SIGINT", () => void stop());
|
|
4904
|
+
process.once("SIGTERM", () => void stop());
|
|
4536
4905
|
await new Promise(() => void 0);
|
|
4537
4906
|
}
|
|
4538
|
-
|
|
4539
|
-
const stop = async () => {
|
|
4540
|
-
await runtime.close();
|
|
4541
|
-
};
|
|
4542
|
-
process.once("SIGINT", () => void stop().then(() => process.exit(0)));
|
|
4543
|
-
process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
|
|
4544
|
-
await serveStdio(server);
|
|
4545
|
-
await stop();
|
|
4546
|
-
return 0;
|
|
4907
|
+
return serveOverStdio(runtime);
|
|
4547
4908
|
}
|
|
4548
4909
|
|
|
4549
4910
|
// src/commands/report.ts
|
|
4550
4911
|
import { resolve as resolve4 } from "path";
|
|
4551
4912
|
import {
|
|
4552
|
-
buildReport,
|
|
4553
|
-
latestRunId,
|
|
4554
|
-
listRuns,
|
|
4913
|
+
buildReport as buildReport2,
|
|
4914
|
+
latestRunId as latestRunId2,
|
|
4915
|
+
listRuns as listRuns2,
|
|
4555
4916
|
renderMutationDiff,
|
|
4556
|
-
renderReportMarkdown
|
|
4917
|
+
renderReportMarkdown as renderReportMarkdown2
|
|
4557
4918
|
} from "@agentwares/agentguard-core";
|
|
4558
4919
|
import { readAuditFile, verifyAuditFile } from "@agentwares/agentguard-core/node";
|
|
4559
4920
|
function pickRun(args, entries) {
|
|
4560
4921
|
if (flagBool(args, "all")) return void 0;
|
|
4561
|
-
return flagString(args, "run") ??
|
|
4922
|
+
return flagString(args, "run") ?? latestRunId2(entries);
|
|
4562
4923
|
}
|
|
4563
4924
|
async function reportCommand(args, io) {
|
|
4564
4925
|
const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
|
|
4565
4926
|
const entries = readAuditFile(loaded.auditPath);
|
|
4566
4927
|
const runId = pickRun(args, entries);
|
|
4567
|
-
const report =
|
|
4928
|
+
const report = buildReport2(entries, { runId });
|
|
4568
4929
|
if (flagBool(args, "json")) {
|
|
4569
4930
|
io.out(
|
|
4570
4931
|
JSON.stringify(
|
|
@@ -4583,12 +4944,12 @@ async function reportCommand(args, io) {
|
|
|
4583
4944
|
);
|
|
4584
4945
|
return 0;
|
|
4585
4946
|
}
|
|
4586
|
-
io.out(
|
|
4947
|
+
io.out(renderReportMarkdown2(report));
|
|
4587
4948
|
const chain = await verifyAuditFile(loaded.auditPath);
|
|
4588
4949
|
io.out(
|
|
4589
4950
|
chain.ok ? `Audit chain: ${chain.entries} entries, verified (${loaded.auditPath}).` : `Audit chain BROKEN at entry ${chain.brokenAt}: ${chain.reason}`
|
|
4590
4951
|
);
|
|
4591
|
-
if (runId &&
|
|
4952
|
+
if (runId && listRuns2(entries).length > 1) io.out(`Other runs: agentguard report --all`);
|
|
4592
4953
|
return 0;
|
|
4593
4954
|
}
|
|
4594
4955
|
async function diffCommand(args, io) {
|
|
@@ -4615,7 +4976,7 @@ async function statusCommand(args, io) {
|
|
|
4615
4976
|
const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
|
|
4616
4977
|
const { guard, kill } = createFileGuard(loaded, { alerts: false, env: io.env });
|
|
4617
4978
|
const entries = readAuditFile(loaded.auditPath);
|
|
4618
|
-
const runId = flagString(args, "run") ??
|
|
4979
|
+
const runId = flagString(args, "run") ?? latestRunId2(entries) ?? "(none yet)";
|
|
4619
4980
|
const status = await guard.status(runId);
|
|
4620
4981
|
const killed = kill.check();
|
|
4621
4982
|
const reg = readRegistration(loaded);
|
|
@@ -4784,6 +5145,6 @@ export {
|
|
|
4784
5145
|
seedState,
|
|
4785
5146
|
serversToUpstreams,
|
|
4786
5147
|
startHttpProxy,
|
|
4787
|
-
toCallToolResult
|
|
5148
|
+
toCallToolResult2 as toCallToolResult
|
|
4788
5149
|
};
|
|
4789
5150
|
//# sourceMappingURL=index.js.map
|