@agentwares/agentguard 0.1.5 → 0.1.7
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 +83 -0
- package/README.md +69 -19
- package/dist/cli.js +476 -65
- package/dist/cli.js.map +1 -1
- package/dist/index.d.ts +16 -1
- package/dist/index.js +2197 -1784
- package/dist/index.js.map +1 -1
- package/llms.txt +4 -0
- package/package.json +7 -2
- package/server.json +3 -3
package/dist/cli.js
CHANGED
|
@@ -925,7 +925,7 @@ function version() {
|
|
|
925
925
|
}
|
|
926
926
|
|
|
927
927
|
// src/context.ts
|
|
928
|
-
import { existsSync as
|
|
928
|
+
import { existsSync as existsSync3 } from "fs";
|
|
929
929
|
import { resolve } from "path";
|
|
930
930
|
import { Guard } from "@agentwares/agentguard-core";
|
|
931
931
|
import {
|
|
@@ -935,7 +935,7 @@ import {
|
|
|
935
935
|
FileStateStore,
|
|
936
936
|
loadPolicyFile
|
|
937
937
|
} from "@agentwares/agentguard-core/node";
|
|
938
|
-
import { existsSync as exists, readFileSync as
|
|
938
|
+
import { existsSync as exists, readFileSync as readFileSync3 } from "fs";
|
|
939
939
|
|
|
940
940
|
// ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
|
|
941
941
|
import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
|
|
@@ -1161,6 +1161,10 @@ function errorResult(err, opts = {}) {
|
|
|
1161
1161
|
_meta: meta
|
|
1162
1162
|
};
|
|
1163
1163
|
}
|
|
1164
|
+
function createToolContext(init = {}) {
|
|
1165
|
+
const { headers, ...rest } = init;
|
|
1166
|
+
return { headers: headers ?? new Headers(), ...rest };
|
|
1167
|
+
}
|
|
1164
1168
|
var TOOL_NAME_PATTERN = /^[a-z][a-z0-9]*(_[a-z0-9]+)+$/;
|
|
1165
1169
|
var MIN_NAMESPACE_LENGTH = 3;
|
|
1166
1170
|
var MIN_DESCRIPTION_LENGTH = 20;
|
|
@@ -1914,6 +1918,48 @@ function localhostHosts(port) {
|
|
|
1914
1918
|
return [...hosts, ...hosts.map((h) => `${h}:${port}`)];
|
|
1915
1919
|
}
|
|
1916
1920
|
|
|
1921
|
+
// ../../packages/mcp-kit/dist/chunk-M567BWMA.js
|
|
1922
|
+
var LIST_MAX_PAGES = 20;
|
|
1923
|
+
var LIST_MAX_ITEMS = 2e3;
|
|
1924
|
+
async function readAllPages(fetchPage, opts = {}) {
|
|
1925
|
+
const maxPages = opts.maxPages ?? LIST_MAX_PAGES;
|
|
1926
|
+
const maxItems = opts.maxItems ?? LIST_MAX_ITEMS;
|
|
1927
|
+
const method = opts.method ?? "tools/list";
|
|
1928
|
+
const items = [];
|
|
1929
|
+
const followed = /* @__PURE__ */ new Set();
|
|
1930
|
+
let cursor;
|
|
1931
|
+
for (let page = 1; ; page++) {
|
|
1932
|
+
const res = await fetchPage(cursor, page);
|
|
1933
|
+
if (Array.isArray(res.items)) items.push(...res.items);
|
|
1934
|
+
const next = res.nextCursor;
|
|
1935
|
+
const more = typeof next === "string" && next !== "";
|
|
1936
|
+
if (items.length > maxItems) {
|
|
1937
|
+
return {
|
|
1938
|
+
items,
|
|
1939
|
+
pages: page,
|
|
1940
|
+
truncated: {
|
|
1941
|
+
ceiling: "items",
|
|
1942
|
+
reason: `${method} listed more than ${maxItems} items in ${page} page${page === 1 ? "" : "s"}${more ? " and had more" : ""}; at most ${maxItems} are read`
|
|
1943
|
+
}
|
|
1944
|
+
};
|
|
1945
|
+
}
|
|
1946
|
+
if (!more) return { items, pages: page };
|
|
1947
|
+
if (followed.has(next)) return { items, pages: page, repeatedCursor: true };
|
|
1948
|
+
if (page >= maxPages) {
|
|
1949
|
+
return {
|
|
1950
|
+
items,
|
|
1951
|
+
pages: page,
|
|
1952
|
+
truncated: {
|
|
1953
|
+
ceiling: "pages",
|
|
1954
|
+
reason: `${method} had more than ${maxPages} pages (${items.length} items read); at most ${maxPages} pages are read`
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
}
|
|
1958
|
+
followed.add(next);
|
|
1959
|
+
cursor = next;
|
|
1960
|
+
}
|
|
1961
|
+
}
|
|
1962
|
+
|
|
1917
1963
|
// ../../packages/mcp-kit/dist/index.js
|
|
1918
1964
|
import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
1919
1965
|
async function serveStdio(serverOrOpts) {
|
|
@@ -1931,7 +1977,7 @@ async function serveStdio(serverOrOpts) {
|
|
|
1931
1977
|
}
|
|
1932
1978
|
|
|
1933
1979
|
// src/proxy/http.ts
|
|
1934
|
-
import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
|
|
1980
|
+
import { writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
|
|
1935
1981
|
import { createServer } from "http";
|
|
1936
1982
|
import { join } from "path";
|
|
1937
1983
|
import { FileKillSwitch } from "@agentwares/agentguard-core/node";
|
|
@@ -1955,6 +2001,278 @@ import {
|
|
|
1955
2001
|
keyFromHeaders,
|
|
1956
2002
|
resolveAgent
|
|
1957
2003
|
} from "@agentwares/agentguard-core";
|
|
2004
|
+
|
|
2005
|
+
// src/proxy/own-tools.ts
|
|
2006
|
+
import {
|
|
2007
|
+
buildReport,
|
|
2008
|
+
latestRunId,
|
|
2009
|
+
listRuns,
|
|
2010
|
+
parseAuditJsonl,
|
|
2011
|
+
renderReportMarkdown,
|
|
2012
|
+
verifyChain
|
|
2013
|
+
} from "@agentwares/agentguard-core";
|
|
2014
|
+
import { existsSync, readFileSync as readFileSync2 } from "fs";
|
|
2015
|
+
import { z as z5 } from "zod";
|
|
2016
|
+
var INIT_COMMAND = "npx -y @agentwares/agentguard init";
|
|
2017
|
+
var READ_ONLY = {
|
|
2018
|
+
readOnlyHint: true,
|
|
2019
|
+
destructiveHint: false,
|
|
2020
|
+
idempotentHint: true,
|
|
2021
|
+
openWorldHint: false
|
|
2022
|
+
};
|
|
2023
|
+
var runIdInput = z5.string().min(1).max(128).optional().describe(
|
|
2024
|
+
"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."
|
|
2025
|
+
);
|
|
2026
|
+
var counters = z5.record(z5.string(), z5.number()).describe(
|
|
2027
|
+
"counter name (tool_calls, writes, deletes, emails, spend_usd or a custom one) \u2192 value"
|
|
2028
|
+
);
|
|
2029
|
+
function readAudit(path) {
|
|
2030
|
+
if (!existsSync(path)) return { exists: false, entries: [] };
|
|
2031
|
+
try {
|
|
2032
|
+
return { exists: true, entries: parseAuditJsonl(readFileSync2(path, "utf8")) };
|
|
2033
|
+
} catch (err) {
|
|
2034
|
+
return { exists: true, entries: [], error: err instanceof Error ? err.message : String(err) };
|
|
2035
|
+
}
|
|
2036
|
+
}
|
|
2037
|
+
function unreadable(path, error) {
|
|
2038
|
+
return toolError({
|
|
2039
|
+
code: "AUDIT_UNREADABLE",
|
|
2040
|
+
cause: `the audit log at ${path} could not be read: ${error}`,
|
|
2041
|
+
fix: "call agentguard_verify_audit_log to see where it breaks; a hand-edited or truncated line has to be restored from a copy",
|
|
2042
|
+
retryable: false
|
|
2043
|
+
});
|
|
2044
|
+
}
|
|
2045
|
+
function numbers(caps) {
|
|
2046
|
+
const out = {};
|
|
2047
|
+
for (const [k, v] of Object.entries(caps)) if (typeof v === "number") out[k] = v;
|
|
2048
|
+
return out;
|
|
2049
|
+
}
|
|
2050
|
+
function nextStep(runtime) {
|
|
2051
|
+
if (!runtime.configured)
|
|
2052
|
+
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.`;
|
|
2053
|
+
if (runtime.policy.upstreams.length === 0)
|
|
2054
|
+
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.`;
|
|
2055
|
+
return "agentguard is running; run your agent, then read agentguard_get_report.";
|
|
2056
|
+
}
|
|
2057
|
+
var StatusOutput = z5.object({
|
|
2058
|
+
configured: z5.boolean().describe(
|
|
2059
|
+
"true when a policy file was found and loaded; false means agentguard guards nothing"
|
|
2060
|
+
),
|
|
2061
|
+
policy_path: z5.string().describe("absolute path of the policy file read, or looked for"),
|
|
2062
|
+
mode: z5.enum(["dry-run", "enforce"]).describe("dry-run fakes writes and spends and records them; enforce lets allowed calls run"),
|
|
2063
|
+
upstreams: z5.array(
|
|
2064
|
+
z5.object({
|
|
2065
|
+
name: z5.string().describe("upstream name from the policy"),
|
|
2066
|
+
connected: z5.boolean().describe("whether agentguard is connected to it"),
|
|
2067
|
+
tools: z5.number().int().describe("tools it exposes"),
|
|
2068
|
+
transport: z5.enum(["stdio", "http"]).describe("how agentguard reaches it"),
|
|
2069
|
+
error: z5.string().optional().describe("why it is not connected")
|
|
2070
|
+
})
|
|
2071
|
+
).describe("the MCP servers this proxy fronts; empty means none are configured"),
|
|
2072
|
+
run_id: z5.string().nullable().describe(
|
|
2073
|
+
"run the usage is for: the requested run_id, else the latest run in the log, else null"
|
|
2074
|
+
),
|
|
2075
|
+
caps: z5.object({ per_run: counters, per_day: counters }).describe("limits from the policy; a counter that is absent has no cap"),
|
|
2076
|
+
used: z5.object({ per_run: counters, per_day: counters }).describe("what that run, and today, have used against the caps"),
|
|
2077
|
+
remaining: z5.record(
|
|
2078
|
+
z5.string(),
|
|
2079
|
+
z5.object({ per_run: z5.number().optional(), per_day: z5.number().optional() })
|
|
2080
|
+
).describe("budget left per capped counter, per run and per day"),
|
|
2081
|
+
kill_switch: z5.object({
|
|
2082
|
+
on: z5.boolean().describe("true halts every tool call with KILLED"),
|
|
2083
|
+
reason: z5.string().optional().describe("why it was pulled"),
|
|
2084
|
+
source: z5.string().optional().describe("file, env or memory")
|
|
2085
|
+
}).describe("the kill switch state"),
|
|
2086
|
+
pending_approvals: z5.number().int().describe("destructive calls waiting for a human decision"),
|
|
2087
|
+
audit_log: z5.object({
|
|
2088
|
+
path: z5.string().describe("absolute path of the hash-chained JSONL audit log"),
|
|
2089
|
+
entries: z5.number().int().describe("entries in it; 0 when it does not exist yet")
|
|
2090
|
+
}).describe("where every tool call is recorded"),
|
|
2091
|
+
next_step: z5.string().describe("the one thing to do next, with the command when there is one")
|
|
2092
|
+
});
|
|
2093
|
+
var ReportOutput = z5.object({
|
|
2094
|
+
configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
|
|
2095
|
+
audit_log_path: z5.string().describe("absolute path of the audit log the report was built from"),
|
|
2096
|
+
run_id: z5.string().nullable().describe("the run reported on; null when the log has no runs"),
|
|
2097
|
+
runs_in_log: z5.number().int().describe("how many runs the audit log holds"),
|
|
2098
|
+
tool_calls: z5.number().int().describe("tool calls in this run"),
|
|
2099
|
+
would_have: z5.object({
|
|
2100
|
+
creates: z5.number().int(),
|
|
2101
|
+
updates: z5.number().int(),
|
|
2102
|
+
deletes: z5.number().int(),
|
|
2103
|
+
sends: z5.number().int(),
|
|
2104
|
+
executes: z5.number().int(),
|
|
2105
|
+
spends: z5.number().int(),
|
|
2106
|
+
spend_usd: z5.number()
|
|
2107
|
+
}).describe("what dry-run mode faked: what the run would have done had the calls executed"),
|
|
2108
|
+
did: z5.object({
|
|
2109
|
+
writes: z5.number().int(),
|
|
2110
|
+
deletes: z5.number().int(),
|
|
2111
|
+
sends: z5.number().int(),
|
|
2112
|
+
spend_usd: z5.number()
|
|
2113
|
+
}).describe("what actually executed upstream"),
|
|
2114
|
+
halts: z5.array(
|
|
2115
|
+
z5.object({
|
|
2116
|
+
seq: z5.number().int().describe("audit entry number"),
|
|
2117
|
+
ts: z5.string().describe("ISO time of the call"),
|
|
2118
|
+
code: z5.string().describe("CAP_EXCEEDED, LOOP_DETECTED, KILLED, APPROVAL_REQUIRED, \u2026"),
|
|
2119
|
+
tool: z5.string().describe("the tool that was stopped"),
|
|
2120
|
+
cause: z5.string().describe("why")
|
|
2121
|
+
})
|
|
2122
|
+
).describe("every call agentguard stopped, in order"),
|
|
2123
|
+
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)"),
|
|
2124
|
+
markdown: z5.string().describe("the same report `agentguard report` prints, as Markdown")
|
|
2125
|
+
});
|
|
2126
|
+
var VerifyOutput = z5.object({
|
|
2127
|
+
configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
|
|
2128
|
+
path: z5.string().describe("absolute path of the audit log checked"),
|
|
2129
|
+
exists: z5.boolean().describe("false when no call has been recorded yet; an absent log verifies"),
|
|
2130
|
+
ok: z5.boolean().describe("true when every entry's hash and prev_hash recompute"),
|
|
2131
|
+
entries: z5.number().int().describe("entries checked"),
|
|
2132
|
+
head_hash: z5.string().nullable().describe(
|
|
2133
|
+
"hash of the last entry; record it with the count to detect entries cut from the end"
|
|
2134
|
+
),
|
|
2135
|
+
broken_at: z5.number().int().nullable().describe("seq of the first entry that does not verify"),
|
|
2136
|
+
reason: z5.string().nullable().describe("why verification failed")
|
|
2137
|
+
});
|
|
2138
|
+
function createOwnTools(runtime) {
|
|
2139
|
+
const status = defineTool({
|
|
2140
|
+
name: "agentguard_get_status",
|
|
2141
|
+
title: "agentguard status",
|
|
2142
|
+
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.",
|
|
2143
|
+
input: z5.strictObject({ run_id: runIdInput }),
|
|
2144
|
+
output: StatusOutput,
|
|
2145
|
+
annotations: { title: "agentguard status", ...READ_ONLY },
|
|
2146
|
+
async handler({ run_id }) {
|
|
2147
|
+
const { loaded, guard } = runtime;
|
|
2148
|
+
const audit = readAudit(loaded.auditPath);
|
|
2149
|
+
if (audit.error) throw unreadable(loaded.auditPath, audit.error);
|
|
2150
|
+
const runId = run_id ?? latestRunId(audit.entries) ?? null;
|
|
2151
|
+
const state = await guard.status(runId ?? "(none yet)");
|
|
2152
|
+
const pending = await guard.approvals.list("pending");
|
|
2153
|
+
return {
|
|
2154
|
+
configured: runtime.configured,
|
|
2155
|
+
policy_path: loaded.path,
|
|
2156
|
+
mode: runtime.policy.mode,
|
|
2157
|
+
upstreams: runtime.upstreamStatus(),
|
|
2158
|
+
run_id: runId,
|
|
2159
|
+
caps: { per_run: numbers(state.caps.per_run), per_day: numbers(state.caps.per_day) },
|
|
2160
|
+
used: state.usage,
|
|
2161
|
+
remaining: state.remaining,
|
|
2162
|
+
kill_switch: {
|
|
2163
|
+
on: state.killed.killed,
|
|
2164
|
+
...state.killed.reason ? { reason: state.killed.reason } : {},
|
|
2165
|
+
...state.killed.source ? { source: state.killed.source } : {}
|
|
2166
|
+
},
|
|
2167
|
+
pending_approvals: pending.length,
|
|
2168
|
+
audit_log: { path: loaded.auditPath, entries: audit.entries.length },
|
|
2169
|
+
next_step: nextStep(runtime)
|
|
2170
|
+
};
|
|
2171
|
+
}
|
|
2172
|
+
});
|
|
2173
|
+
const report = defineTool({
|
|
2174
|
+
name: "agentguard_get_report",
|
|
2175
|
+
title: "agentguard run report",
|
|
2176
|
+
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.",
|
|
2177
|
+
input: z5.strictObject({ run_id: runIdInput }),
|
|
2178
|
+
output: ReportOutput,
|
|
2179
|
+
annotations: { title: "agentguard run report", ...READ_ONLY },
|
|
2180
|
+
handler({ run_id }) {
|
|
2181
|
+
const { loaded } = runtime;
|
|
2182
|
+
const audit = readAudit(loaded.auditPath);
|
|
2183
|
+
if (audit.error) throw unreadable(loaded.auditPath, audit.error);
|
|
2184
|
+
const runs = listRuns(audit.entries);
|
|
2185
|
+
if (run_id && !runs.some((r) => r.runId === run_id)) {
|
|
2186
|
+
throw toolError({
|
|
2187
|
+
code: "NOT_FOUND",
|
|
2188
|
+
cause: `no run "${run_id}" in the audit log at ${loaded.auditPath}`,
|
|
2189
|
+
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"}`,
|
|
2190
|
+
retryable: false
|
|
2191
|
+
});
|
|
2192
|
+
}
|
|
2193
|
+
const runId = run_id ?? latestRunId(audit.entries) ?? null;
|
|
2194
|
+
const built = buildReport(audit.entries, { runId: runId ?? void 0 });
|
|
2195
|
+
const markdown = runId ? renderReportMarkdown(built) : `# agentguard report
|
|
2196
|
+
|
|
2197
|
+
No runs recorded in ${loaded.auditPath} yet. ${nextStep(runtime)}`;
|
|
2198
|
+
return {
|
|
2199
|
+
configured: runtime.configured,
|
|
2200
|
+
audit_log_path: loaded.auditPath,
|
|
2201
|
+
run_id: runId,
|
|
2202
|
+
runs_in_log: runs.length,
|
|
2203
|
+
tool_calls: runId ? audit.entries.filter((e) => e.run_id === runId).length : 0,
|
|
2204
|
+
would_have: {
|
|
2205
|
+
creates: built.wouldHave.creates,
|
|
2206
|
+
updates: built.wouldHave.updates,
|
|
2207
|
+
deletes: built.wouldHave.deletes,
|
|
2208
|
+
sends: built.wouldHave.sends,
|
|
2209
|
+
executes: built.wouldHave.executes,
|
|
2210
|
+
spends: built.wouldHave.spends,
|
|
2211
|
+
spend_usd: built.wouldHave.spendUsd
|
|
2212
|
+
},
|
|
2213
|
+
did: {
|
|
2214
|
+
writes: built.did.writes,
|
|
2215
|
+
deletes: built.did.deletes,
|
|
2216
|
+
sends: built.did.sends,
|
|
2217
|
+
spend_usd: built.did.spendUsd
|
|
2218
|
+
},
|
|
2219
|
+
halts: built.halts,
|
|
2220
|
+
top_tools: built.topTools,
|
|
2221
|
+
markdown
|
|
2222
|
+
};
|
|
2223
|
+
}
|
|
2224
|
+
});
|
|
2225
|
+
const verify = defineTool({
|
|
2226
|
+
name: "agentguard_verify_audit_log",
|
|
2227
|
+
title: "Verify agentguard audit log",
|
|
2228
|
+
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.",
|
|
2229
|
+
input: z5.strictObject({}),
|
|
2230
|
+
output: VerifyOutput,
|
|
2231
|
+
annotations: { title: "Verify agentguard audit log", ...READ_ONLY },
|
|
2232
|
+
async handler() {
|
|
2233
|
+
const { loaded } = runtime;
|
|
2234
|
+
const audit = readAudit(loaded.auditPath);
|
|
2235
|
+
if (audit.error) {
|
|
2236
|
+
return {
|
|
2237
|
+
configured: runtime.configured,
|
|
2238
|
+
path: loaded.auditPath,
|
|
2239
|
+
exists: true,
|
|
2240
|
+
ok: false,
|
|
2241
|
+
entries: 0,
|
|
2242
|
+
head_hash: null,
|
|
2243
|
+
broken_at: null,
|
|
2244
|
+
reason: audit.error
|
|
2245
|
+
};
|
|
2246
|
+
}
|
|
2247
|
+
const result = await verifyChain(audit.entries);
|
|
2248
|
+
return {
|
|
2249
|
+
configured: runtime.configured,
|
|
2250
|
+
path: loaded.auditPath,
|
|
2251
|
+
exists: audit.exists,
|
|
2252
|
+
ok: result.ok,
|
|
2253
|
+
entries: result.entries,
|
|
2254
|
+
// an empty chain's head is the genesis hash, which is not an entry anyone can record
|
|
2255
|
+
head_hash: result.entries > 0 ? result.head ?? null : null,
|
|
2256
|
+
broken_at: result.brokenAt ?? null,
|
|
2257
|
+
reason: result.reason ?? null
|
|
2258
|
+
};
|
|
2259
|
+
}
|
|
2260
|
+
});
|
|
2261
|
+
return [status, report, verify];
|
|
2262
|
+
}
|
|
2263
|
+
function ownToolsActive(runtime) {
|
|
2264
|
+
return runtime.upstreamStatus().length === 0;
|
|
2265
|
+
}
|
|
2266
|
+
function ownToolList(tools) {
|
|
2267
|
+
return listToolManifest(tools);
|
|
2268
|
+
}
|
|
2269
|
+
async function callOwnTool(tools, name, args, requestId) {
|
|
2270
|
+
const tool = tools.find((t) => t.name === name);
|
|
2271
|
+
if (!tool) return void 0;
|
|
2272
|
+
return tool.invoke(args, createToolContext({ requestId }));
|
|
2273
|
+
}
|
|
2274
|
+
|
|
2275
|
+
// src/proxy/server.ts
|
|
1958
2276
|
var PROXY_VERSION = version();
|
|
1959
2277
|
function headerValue(extra, name) {
|
|
1960
2278
|
const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
|
|
@@ -2055,6 +2373,11 @@ function killedError(state) {
|
|
|
2055
2373
|
{ code: -32e3 }
|
|
2056
2374
|
);
|
|
2057
2375
|
}
|
|
2376
|
+
function serverInstructions(runtime) {
|
|
2377
|
+
if (ownToolsActive(runtime))
|
|
2378
|
+
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.`;
|
|
2379
|
+
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.`;
|
|
2380
|
+
}
|
|
2058
2381
|
function createDownstreamServer(runtime, opts = {}) {
|
|
2059
2382
|
const server = new McpServer3(
|
|
2060
2383
|
{ name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
|
|
@@ -2066,9 +2389,10 @@ function createDownstreamServer(runtime, opts = {}) {
|
|
|
2066
2389
|
logging: {},
|
|
2067
2390
|
completions: {}
|
|
2068
2391
|
},
|
|
2069
|
-
instructions:
|
|
2392
|
+
instructions: serverInstructions(runtime)
|
|
2070
2393
|
}
|
|
2071
2394
|
);
|
|
2395
|
+
const ownTools = createOwnTools(runtime);
|
|
2072
2396
|
const session = {
|
|
2073
2397
|
id: crypto.randomUUID(),
|
|
2074
2398
|
server,
|
|
@@ -2106,12 +2430,22 @@ function createDownstreamServer(runtime, opts = {}) {
|
|
|
2106
2430
|
touch();
|
|
2107
2431
|
const { agent } = await agentFor(extra);
|
|
2108
2432
|
const scope = agent === void 0 ? runtime.agent : agent;
|
|
2433
|
+
if (ownToolsActive(runtime)) return { tools: ownToolList(ownTools) };
|
|
2109
2434
|
return {
|
|
2110
2435
|
tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
|
|
2111
2436
|
};
|
|
2112
2437
|
});
|
|
2113
2438
|
low.setRequestHandler(CallToolRequestSchema2, async (req, extra) => {
|
|
2114
2439
|
touch();
|
|
2440
|
+
if (ownToolsActive(runtime)) {
|
|
2441
|
+
const own = await callOwnTool(
|
|
2442
|
+
ownTools,
|
|
2443
|
+
req.params.name,
|
|
2444
|
+
req.params.arguments ?? {},
|
|
2445
|
+
String(extra.requestId)
|
|
2446
|
+
);
|
|
2447
|
+
if (own) return own;
|
|
2448
|
+
}
|
|
2115
2449
|
const { agent, error } = await agentFor(extra);
|
|
2116
2450
|
const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
|
|
2117
2451
|
if (error) {
|
|
@@ -2378,7 +2712,7 @@ async function startHttpProxy(opts) {
|
|
|
2378
2712
|
port,
|
|
2379
2713
|
token,
|
|
2380
2714
|
close: () => new Promise((resolve5, reject) => {
|
|
2381
|
-
if (opts.registerFile !== false &&
|
|
2715
|
+
if (opts.registerFile !== false && existsSync2(regFile)) {
|
|
2382
2716
|
try {
|
|
2383
2717
|
unlinkSync(regFile);
|
|
2384
2718
|
} catch {
|
|
@@ -2674,7 +3008,7 @@ function readRegistration(loaded) {
|
|
|
2674
3008
|
const file = registrationPath(loaded.stateDir);
|
|
2675
3009
|
if (!exists(file)) return void 0;
|
|
2676
3010
|
try {
|
|
2677
|
-
const reg = JSON.parse(
|
|
3011
|
+
const reg = JSON.parse(readFileSync3(file, "utf8"));
|
|
2678
3012
|
return reg;
|
|
2679
3013
|
} catch {
|
|
2680
3014
|
return void 0;
|
|
@@ -2690,11 +3024,11 @@ function describeError(err) {
|
|
|
2690
3024
|
}
|
|
2691
3025
|
|
|
2692
3026
|
// src/commands/connect.ts
|
|
2693
|
-
import { existsSync as
|
|
3027
|
+
import { existsSync as existsSync5 } from "fs";
|
|
2694
3028
|
import { relative } from "path";
|
|
2695
3029
|
|
|
2696
3030
|
// src/configs.ts
|
|
2697
|
-
import { copyFileSync, existsSync as
|
|
3031
|
+
import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
|
|
2698
3032
|
import { homedir as homedir2, platform } from "os";
|
|
2699
3033
|
import { join as join2, resolve as resolve2 } from "path";
|
|
2700
3034
|
function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.env) {
|
|
@@ -2727,6 +3061,12 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
|
|
|
2727
3061
|
serversKey: "servers"
|
|
2728
3062
|
},
|
|
2729
3063
|
{ kind: "generic", label: "mcp.json", path: join2(cwd, "mcp.json"), serversKey: "mcpServers" },
|
|
3064
|
+
{
|
|
3065
|
+
kind: "gemini-project",
|
|
3066
|
+
label: "Gemini CLI (project .gemini/settings.json)",
|
|
3067
|
+
path: join2(cwd, ".gemini", "settings.json"),
|
|
3068
|
+
serversKey: "mcpServers"
|
|
3069
|
+
},
|
|
2730
3070
|
{ kind: "claude-desktop", label: "Claude Desktop", path: desktop, serversKey: "mcpServers" },
|
|
2731
3071
|
{
|
|
2732
3072
|
kind: "cursor-user",
|
|
@@ -2745,11 +3085,20 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
|
|
|
2745
3085
|
label: "Claude Code (user ~/.claude.json)",
|
|
2746
3086
|
path: join2(home, ".claude.json"),
|
|
2747
3087
|
serversKey: "mcpServers"
|
|
3088
|
+
},
|
|
3089
|
+
{
|
|
3090
|
+
kind: "gemini-user",
|
|
3091
|
+
label: "Gemini CLI (user ~/.gemini/settings.json)",
|
|
3092
|
+
path: join2(home, ".gemini", "settings.json"),
|
|
3093
|
+
serversKey: "mcpServers"
|
|
2748
3094
|
}
|
|
2749
3095
|
];
|
|
2750
3096
|
}
|
|
3097
|
+
function isGeminiConfig(file) {
|
|
3098
|
+
return file.kind === "gemini-project" || file.kind === "gemini-user" || /[/\\]\.gemini[/\\]settings\.json$/.test(file.path);
|
|
3099
|
+
}
|
|
2751
3100
|
function detectClientConfigs(cwd, home, env) {
|
|
2752
|
-
return candidateConfigs(cwd, home, env).filter((c) =>
|
|
3101
|
+
return candidateConfigs(cwd, home, env).filter((c) => existsSync4(c.path));
|
|
2753
3102
|
}
|
|
2754
3103
|
function describeConfigPath(path) {
|
|
2755
3104
|
const abs = resolve2(path);
|
|
@@ -2757,7 +3106,7 @@ function describeConfigPath(path) {
|
|
|
2757
3106
|
if (known) return known;
|
|
2758
3107
|
let serversKey = "mcpServers";
|
|
2759
3108
|
try {
|
|
2760
|
-
const parsed = JSON.parse(
|
|
3109
|
+
const parsed = JSON.parse(readFileSync4(abs, "utf8"));
|
|
2761
3110
|
if (parsed.servers && !parsed.mcpServers) serversKey = "servers";
|
|
2762
3111
|
} catch {
|
|
2763
3112
|
}
|
|
@@ -2769,7 +3118,7 @@ function describeConfigPath(path) {
|
|
|
2769
3118
|
};
|
|
2770
3119
|
}
|
|
2771
3120
|
function readServers(file) {
|
|
2772
|
-
const raw = JSON.parse(
|
|
3121
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
2773
3122
|
const servers = raw[file.serversKey];
|
|
2774
3123
|
if (!servers || typeof servers !== "object") return {};
|
|
2775
3124
|
return servers;
|
|
@@ -2788,10 +3137,11 @@ function serversToUpstreams(servers) {
|
|
|
2788
3137
|
continue;
|
|
2789
3138
|
}
|
|
2790
3139
|
const name = rawName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[^a-zA-Z0-9]+/, "") || "upstream";
|
|
2791
|
-
|
|
3140
|
+
const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
|
|
3141
|
+
if (url) {
|
|
2792
3142
|
upstreams.push({
|
|
2793
3143
|
name,
|
|
2794
|
-
url
|
|
3144
|
+
url,
|
|
2795
3145
|
headers: entry.headers ?? {},
|
|
2796
3146
|
args: [],
|
|
2797
3147
|
env: {},
|
|
@@ -2826,27 +3176,27 @@ function proxyEntry(file, policyPath, opts = {}) {
|
|
|
2826
3176
|
return entry;
|
|
2827
3177
|
}
|
|
2828
3178
|
function rewriteClientConfig(file, policyPath, opts = {}) {
|
|
2829
|
-
const raw = JSON.parse(
|
|
3179
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
2830
3180
|
const servers = raw[file.serversKey] ?? {};
|
|
2831
3181
|
const replaced = Object.keys(servers).filter((n) => !isProxyEntry(servers[n]));
|
|
2832
3182
|
const backup = backupPath(file);
|
|
2833
|
-
if (!
|
|
3183
|
+
if (!existsSync4(backup)) copyFileSync(file.path, backup);
|
|
2834
3184
|
raw[file.serversKey] = { [PROXY_SERVER_NAME]: proxyEntry(file, policyPath, opts) };
|
|
2835
3185
|
writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
|
|
2836
3186
|
return { backup, replaced };
|
|
2837
3187
|
}
|
|
2838
3188
|
function restoreClientConfig(file) {
|
|
2839
3189
|
const backup = backupPath(file);
|
|
2840
|
-
if (!
|
|
3190
|
+
if (!existsSync4(backup)) return false;
|
|
2841
3191
|
copyFileSync(backup, file.path);
|
|
2842
3192
|
return true;
|
|
2843
3193
|
}
|
|
2844
3194
|
function mergeServerIntoConfig(file, name, entry) {
|
|
2845
|
-
const raw = JSON.parse(
|
|
3195
|
+
const raw = JSON.parse(readFileSync4(file.path, "utf8"));
|
|
2846
3196
|
const servers = raw[file.serversKey] ?? {};
|
|
2847
3197
|
const replaced = name in servers;
|
|
2848
3198
|
const backup = backupPath(file);
|
|
2849
|
-
if (!
|
|
3199
|
+
if (!existsSync4(backup)) copyFileSync(file.path, backup);
|
|
2850
3200
|
raw[file.serversKey] = { ...servers, [name]: entry };
|
|
2851
3201
|
writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
|
|
2852
3202
|
return { backup, replaced };
|
|
@@ -2929,11 +3279,12 @@ async function connectCommand(args, io, fetchImpl = fetch) {
|
|
|
2929
3279
|
return 1;
|
|
2930
3280
|
}
|
|
2931
3281
|
for (const c of chosen) {
|
|
2932
|
-
if (!
|
|
3282
|
+
if (!existsSync5(c.path)) {
|
|
2933
3283
|
io.err(`no MCP config at ${c.path}`);
|
|
2934
3284
|
return 1;
|
|
2935
3285
|
}
|
|
2936
|
-
const {
|
|
3286
|
+
const forClient = isGeminiConfig(c) ? { httpUrl: entry.url, ...entry.headers ? { headers: entry.headers } : {} } : entry;
|
|
3287
|
+
const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, forClient);
|
|
2937
3288
|
io.out(
|
|
2938
3289
|
`${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
|
|
2939
3290
|
);
|
|
@@ -3048,7 +3399,7 @@ async function approvalsCommand(args, io) {
|
|
|
3048
3399
|
}
|
|
3049
3400
|
|
|
3050
3401
|
// src/commands/init.ts
|
|
3051
|
-
import { existsSync as
|
|
3402
|
+
import { existsSync as existsSync6, writeFileSync as writeFileSync3 } from "fs";
|
|
3052
3403
|
import { relative as relative2, resolve as resolve3 } from "path";
|
|
3053
3404
|
import YAML from "yaml";
|
|
3054
3405
|
|
|
@@ -3085,6 +3436,7 @@ var ProxyRuntime = class {
|
|
|
3085
3436
|
guard;
|
|
3086
3437
|
agent;
|
|
3087
3438
|
defaultRunId;
|
|
3439
|
+
configured;
|
|
3088
3440
|
sessions = /* @__PURE__ */ new Set();
|
|
3089
3441
|
connections = /* @__PURE__ */ new Map();
|
|
3090
3442
|
exposed = [];
|
|
@@ -3101,6 +3453,7 @@ var ProxyRuntime = class {
|
|
|
3101
3453
|
this.guard = opts.guard;
|
|
3102
3454
|
this.agent = opts.agent;
|
|
3103
3455
|
this.defaultRunId = opts.defaultRunId;
|
|
3456
|
+
this.configured = opts.configured ?? true;
|
|
3104
3457
|
this.log = opts.log ?? ((line) => process.stderr.write(`[agentguard] ${line}
|
|
3105
3458
|
`));
|
|
3106
3459
|
this.connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
|
|
@@ -3246,6 +3599,13 @@ var ProxyRuntime = class {
|
|
|
3246
3599
|
}
|
|
3247
3600
|
}
|
|
3248
3601
|
}
|
|
3602
|
+
/**
|
|
3603
|
+
* Re-read an upstream's tools, every page (mcp-kit's `readAllPages`): at most 20 requests, each
|
|
3604
|
+
* under the upstream's `timeout_ms`, ending at a cursor already followed. It used to follow
|
|
3605
|
+
* `nextCursor` with no bound, so an upstream that echoed its cursor kept the proxy asking for
|
|
3606
|
+
* the same page forever. A refresh that fails, or stops at the 20-page / 2,000-tool ceiling,
|
|
3607
|
+
* changes nothing: the agent keeps the last good list, and the log says why.
|
|
3608
|
+
*/
|
|
3249
3609
|
async refreshTools(name) {
|
|
3250
3610
|
const conn = this.connections.get(name);
|
|
3251
3611
|
if (!conn?.connected) return;
|
|
@@ -3253,22 +3613,23 @@ var ProxyRuntime = class {
|
|
|
3253
3613
|
conn.tools = [];
|
|
3254
3614
|
return;
|
|
3255
3615
|
}
|
|
3616
|
+
const kept = () => `keeping the last good list (${conn.tools.length} tool${conn.tools.length === 1 ? "" : "s"})`;
|
|
3256
3617
|
try {
|
|
3257
|
-
const
|
|
3258
|
-
let cursor;
|
|
3259
|
-
do {
|
|
3618
|
+
const read = await readAllPages(async (cursor) => {
|
|
3260
3619
|
const page = await conn.client.listTools(cursor ? { cursor } : void 0, {
|
|
3261
3620
|
timeout: conn.upstream.timeout_ms
|
|
3262
3621
|
});
|
|
3263
|
-
tools
|
|
3264
|
-
|
|
3265
|
-
|
|
3266
|
-
|
|
3622
|
+
return { items: page.tools, nextCursor: page.nextCursor };
|
|
3623
|
+
});
|
|
3624
|
+
if (read.truncated) {
|
|
3625
|
+
this.log(`tools/list for "${name}" not applied: ${read.truncated.reason}; ${kept()}`);
|
|
3626
|
+
return;
|
|
3627
|
+
}
|
|
3628
|
+
conn.tools = read.items;
|
|
3267
3629
|
} catch (err) {
|
|
3268
3630
|
this.log(
|
|
3269
|
-
`tools/list failed for "${name}": ${err instanceof Error ? err.message : String(err)}`
|
|
3631
|
+
`tools/list failed for "${name}": ${err instanceof Error ? err.message : String(err)}; ${kept()}`
|
|
3270
3632
|
);
|
|
3271
|
-
conn.tools = [];
|
|
3272
3633
|
}
|
|
3273
3634
|
}
|
|
3274
3635
|
rebuildExposed() {
|
|
@@ -3645,6 +4006,10 @@ async function toolsCommand(args, io) {
|
|
|
3645
4006
|
io.err(
|
|
3646
4007
|
`${u.connected ? "ok " : "down"} ${u.name}: ${u.connected ? `${u.tools} tools` : u.error}`
|
|
3647
4008
|
);
|
|
4009
|
+
if (result.upstreams.length === 0)
|
|
4010
|
+
io.err(
|
|
4011
|
+
"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)"
|
|
4012
|
+
);
|
|
3648
4013
|
for (const line of formatToolTable(result.tools)) io.out(line);
|
|
3649
4014
|
const counts = result.tools.reduce(
|
|
3650
4015
|
(acc, t) => ({ ...acc, [t.classification.class]: (acc[t.classification.class] ?? 0) + 1 }),
|
|
@@ -3812,7 +4177,7 @@ async function initCommand(args, io) {
|
|
|
3812
4177
|
`found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to route it through agentguard`
|
|
3813
4178
|
);
|
|
3814
4179
|
}
|
|
3815
|
-
if (!chosen.every((c) =>
|
|
4180
|
+
if (!chosen.every((c) => existsSync6(c.path))) {
|
|
3816
4181
|
io.err(`no MCP config at ${chosen.map((c) => c.path).join(", ")}`);
|
|
3817
4182
|
return 1;
|
|
3818
4183
|
}
|
|
@@ -3827,11 +4192,11 @@ async function initCommand(args, io) {
|
|
|
3827
4192
|
}
|
|
3828
4193
|
if (chosen.length === 0)
|
|
3829
4194
|
io.out(
|
|
3830
|
-
"found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf) \u2014 writing a policy with no upstreams"
|
|
4195
|
+
"found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf, Gemini CLI) \u2014 writing a policy with no upstreams"
|
|
3831
4196
|
);
|
|
3832
4197
|
const { upstreams, skipped } = serversToUpstreams(servers);
|
|
3833
4198
|
for (const s of skipped) io.err(`skipped ${s.name}: ${s.reason}`);
|
|
3834
|
-
if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 &&
|
|
4199
|
+
if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 && existsSync6(policyPath)) {
|
|
3835
4200
|
io.out(
|
|
3836
4201
|
`already initialized: ${rel(policyPath)} exists and ${chosen.map((c) => rel(c.path)).join(", ")} already points at agentguard`
|
|
3837
4202
|
);
|
|
@@ -3858,7 +4223,7 @@ async function initCommand(args, io) {
|
|
|
3858
4223
|
}
|
|
3859
4224
|
}
|
|
3860
4225
|
const mode = flagString(args, "mode") ?? "dry-run";
|
|
3861
|
-
if (
|
|
4226
|
+
if (existsSync6(policyPath) && !flagBool(args, "force")) {
|
|
3862
4227
|
io.out(`kept ${rel(policyPath)} (exists; pass --force to regenerate)`);
|
|
3863
4228
|
} else {
|
|
3864
4229
|
writeFileSync3(policyPath, renderStarterPolicy({ upstreams, tools: probed, mode, probeErrors }));
|
|
@@ -3902,13 +4267,13 @@ async function initCommand(args, io) {
|
|
|
3902
4267
|
}
|
|
3903
4268
|
|
|
3904
4269
|
// src/commands/keys.ts
|
|
3905
|
-
import { readFileSync as
|
|
4270
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
|
|
3906
4271
|
import YAML2, { isMap, isSeq, YAMLSeq } from "yaml";
|
|
3907
4272
|
import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
|
|
3908
4273
|
async function keyCommand(args, io) {
|
|
3909
4274
|
const [sub, name] = args.positionals;
|
|
3910
4275
|
const path = policyPathFrom(args, io);
|
|
3911
|
-
const doc = YAML2.parseDocument(
|
|
4276
|
+
const doc = YAML2.parseDocument(readFileSync5(path, "utf8"));
|
|
3912
4277
|
let agents = doc.get("agents");
|
|
3913
4278
|
if (!isSeq(agents)) {
|
|
3914
4279
|
agents = new YAMLSeq();
|
|
@@ -4075,14 +4440,65 @@ async function permissionDiffCommand(args, io) {
|
|
|
4075
4440
|
}
|
|
4076
4441
|
|
|
4077
4442
|
// src/commands/proxy.ts
|
|
4078
|
-
import {
|
|
4443
|
+
import { existsSync as existsSync7 } from "fs";
|
|
4444
|
+
import { dirname } from "path";
|
|
4445
|
+
import { Guard as Guard3, defaultPolicy, resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
|
|
4446
|
+
import { FileKillSwitch as FileKillSwitch3, resolvePath } from "@agentwares/agentguard-core/node";
|
|
4079
4447
|
function newRunId(now = /* @__PURE__ */ new Date()) {
|
|
4080
4448
|
return `run_${now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}_${Math.random().toString(36).slice(2, 6)}`;
|
|
4081
4449
|
}
|
|
4450
|
+
function unconfiguredPolicy(args, io) {
|
|
4451
|
+
if (flagString(args, "config") !== void 0 || io.env.AGENTGUARD_CONFIG) return void 0;
|
|
4452
|
+
const path = policyPathFrom(args, io);
|
|
4453
|
+
if (existsSync7(path)) return void 0;
|
|
4454
|
+
const policy = defaultPolicy();
|
|
4455
|
+
const baseDir = dirname(path);
|
|
4456
|
+
return {
|
|
4457
|
+
policy,
|
|
4458
|
+
path,
|
|
4459
|
+
baseDir,
|
|
4460
|
+
stateDir: resolvePath(policy.state.dir, baseDir),
|
|
4461
|
+
auditPath: resolvePath(policy.audit.path, baseDir),
|
|
4462
|
+
killPath: resolvePath(policy.kill.file, baseDir)
|
|
4463
|
+
};
|
|
4464
|
+
}
|
|
4465
|
+
async function serveOverStdio(runtime) {
|
|
4466
|
+
const server = createDownstreamServer(runtime);
|
|
4467
|
+
const stop = async () => {
|
|
4468
|
+
await runtime.close();
|
|
4469
|
+
};
|
|
4470
|
+
process.once("SIGINT", () => void stop().then(() => process.exit(0)));
|
|
4471
|
+
process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
|
|
4472
|
+
await serveStdio(server);
|
|
4473
|
+
await stop();
|
|
4474
|
+
return 0;
|
|
4475
|
+
}
|
|
4082
4476
|
async function proxyCommand(args, io) {
|
|
4083
|
-
const loaded = loadPolicyFor(args, io);
|
|
4084
4477
|
const log = (line) => io.err(`[agentguard] ${line}`);
|
|
4085
4478
|
const modeOverride = flagString(args, "mode");
|
|
4479
|
+
const http = flagBool(args, "http") === true;
|
|
4480
|
+
const unconfigured = http ? void 0 : unconfiguredPolicy(args, io);
|
|
4481
|
+
if (unconfigured) {
|
|
4482
|
+
if (modeOverride === "dry-run" || modeOverride === "enforce")
|
|
4483
|
+
unconfigured.policy.mode = modeOverride;
|
|
4484
|
+
const runtime2 = new ProxyRuntime({
|
|
4485
|
+
loaded: unconfigured,
|
|
4486
|
+
guard: new Guard3({
|
|
4487
|
+
policy: unconfigured.policy,
|
|
4488
|
+
env: io.env,
|
|
4489
|
+
kill: new FileKillSwitch3(unconfigured.killPath)
|
|
4490
|
+
}),
|
|
4491
|
+
configured: false,
|
|
4492
|
+
defaultRunId: flagString(args, "run-id") ?? newRunId(),
|
|
4493
|
+
log
|
|
4494
|
+
});
|
|
4495
|
+
await runtime2.start();
|
|
4496
|
+
log(
|
|
4497
|
+
`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.`
|
|
4498
|
+
);
|
|
4499
|
+
return serveOverStdio(runtime2);
|
|
4500
|
+
}
|
|
4501
|
+
const loaded = loadPolicyFor(args, io);
|
|
4086
4502
|
if (modeOverride === "dry-run" || modeOverride === "enforce") loaded.policy.mode = modeOverride;
|
|
4087
4503
|
const agentName = flagString(args, "agent");
|
|
4088
4504
|
let agent = void 0;
|
|
@@ -4096,7 +4512,6 @@ async function proxyCommand(args, io) {
|
|
|
4096
4512
|
}
|
|
4097
4513
|
agent = resolved.agent;
|
|
4098
4514
|
}
|
|
4099
|
-
const http = flagBool(args, "http") === true;
|
|
4100
4515
|
let approvalBase;
|
|
4101
4516
|
const fileGuard = createFileGuard(loaded, {
|
|
4102
4517
|
log,
|
|
@@ -4122,59 +4537,55 @@ async function proxyCommand(args, io) {
|
|
|
4122
4537
|
log(
|
|
4123
4538
|
`kill switch is ON (${killed.reason}); every call returns KILLED until \`agentguard resume\``
|
|
4124
4539
|
);
|
|
4540
|
+
if (runtime.upstreamStatus().length === 0)
|
|
4541
|
+
log(
|
|
4542
|
+
`${loaded.path} lists no upstreams: serving agentguard's own read-only tools only. Add servers under \`upstreams:\` to guard them.`
|
|
4543
|
+
);
|
|
4125
4544
|
if (http) {
|
|
4126
|
-
const
|
|
4545
|
+
const server = await startHttpProxy({
|
|
4127
4546
|
runtime,
|
|
4128
4547
|
port: flagNumber(args, "port") ?? 8788,
|
|
4129
4548
|
host: flagString(args, "host") ?? "127.0.0.1",
|
|
4130
4549
|
token: io.env.AGENTGUARD_HTTP_TOKEN
|
|
4131
4550
|
});
|
|
4132
|
-
approvalBase = { baseUrl:
|
|
4551
|
+
approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
|
|
4133
4552
|
log(
|
|
4134
|
-
`Streamable HTTP at ${
|
|
4553
|
+
`Streamable HTTP at ${server.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
|
|
4135
4554
|
);
|
|
4136
4555
|
log(
|
|
4137
|
-
`point your agent at ${
|
|
4556
|
+
`point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
|
|
4138
4557
|
);
|
|
4139
|
-
const
|
|
4140
|
-
await
|
|
4558
|
+
const stop = async () => {
|
|
4559
|
+
await server.close();
|
|
4141
4560
|
await runtime.close();
|
|
4142
4561
|
process.exit(0);
|
|
4143
4562
|
};
|
|
4144
|
-
process.once("SIGINT", () => void
|
|
4145
|
-
process.once("SIGTERM", () => void
|
|
4563
|
+
process.once("SIGINT", () => void stop());
|
|
4564
|
+
process.once("SIGTERM", () => void stop());
|
|
4146
4565
|
await new Promise(() => void 0);
|
|
4147
4566
|
}
|
|
4148
|
-
|
|
4149
|
-
const stop = async () => {
|
|
4150
|
-
await runtime.close();
|
|
4151
|
-
};
|
|
4152
|
-
process.once("SIGINT", () => void stop().then(() => process.exit(0)));
|
|
4153
|
-
process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
|
|
4154
|
-
await serveStdio(server);
|
|
4155
|
-
await stop();
|
|
4156
|
-
return 0;
|
|
4567
|
+
return serveOverStdio(runtime);
|
|
4157
4568
|
}
|
|
4158
4569
|
|
|
4159
4570
|
// src/commands/report.ts
|
|
4160
4571
|
import { resolve as resolve4 } from "path";
|
|
4161
4572
|
import {
|
|
4162
|
-
buildReport,
|
|
4163
|
-
latestRunId,
|
|
4164
|
-
listRuns,
|
|
4573
|
+
buildReport as buildReport2,
|
|
4574
|
+
latestRunId as latestRunId2,
|
|
4575
|
+
listRuns as listRuns2,
|
|
4165
4576
|
renderMutationDiff,
|
|
4166
|
-
renderReportMarkdown
|
|
4577
|
+
renderReportMarkdown as renderReportMarkdown2
|
|
4167
4578
|
} from "@agentwares/agentguard-core";
|
|
4168
4579
|
import { readAuditFile, verifyAuditFile } from "@agentwares/agentguard-core/node";
|
|
4169
4580
|
function pickRun(args, entries) {
|
|
4170
4581
|
if (flagBool(args, "all")) return void 0;
|
|
4171
|
-
return flagString(args, "run") ??
|
|
4582
|
+
return flagString(args, "run") ?? latestRunId2(entries);
|
|
4172
4583
|
}
|
|
4173
4584
|
async function reportCommand(args, io) {
|
|
4174
4585
|
const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
|
|
4175
4586
|
const entries = readAuditFile(loaded.auditPath);
|
|
4176
4587
|
const runId = pickRun(args, entries);
|
|
4177
|
-
const report =
|
|
4588
|
+
const report = buildReport2(entries, { runId });
|
|
4178
4589
|
if (flagBool(args, "json")) {
|
|
4179
4590
|
io.out(
|
|
4180
4591
|
JSON.stringify(
|
|
@@ -4193,12 +4604,12 @@ async function reportCommand(args, io) {
|
|
|
4193
4604
|
);
|
|
4194
4605
|
return 0;
|
|
4195
4606
|
}
|
|
4196
|
-
io.out(
|
|
4607
|
+
io.out(renderReportMarkdown2(report));
|
|
4197
4608
|
const chain = await verifyAuditFile(loaded.auditPath);
|
|
4198
4609
|
io.out(
|
|
4199
4610
|
chain.ok ? `Audit chain: ${chain.entries} entries, verified (${loaded.auditPath}).` : `Audit chain BROKEN at entry ${chain.brokenAt}: ${chain.reason}`
|
|
4200
4611
|
);
|
|
4201
|
-
if (runId &&
|
|
4612
|
+
if (runId && listRuns2(entries).length > 1) io.out(`Other runs: agentguard report --all`);
|
|
4202
4613
|
return 0;
|
|
4203
4614
|
}
|
|
4204
4615
|
async function diffCommand(args, io) {
|
|
@@ -4225,7 +4636,7 @@ async function statusCommand(args, io) {
|
|
|
4225
4636
|
const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
|
|
4226
4637
|
const { guard, kill } = createFileGuard(loaded, { alerts: false, env: io.env });
|
|
4227
4638
|
const entries = readAuditFile(loaded.auditPath);
|
|
4228
|
-
const runId = flagString(args, "run") ??
|
|
4639
|
+
const runId = flagString(args, "run") ?? latestRunId2(entries) ?? "(none yet)";
|
|
4229
4640
|
const status = await guard.status(runId);
|
|
4230
4641
|
const killed = kill.check();
|
|
4231
4642
|
const reg = readRegistration(loaded);
|