@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/dist/cli.js CHANGED
@@ -925,7 +925,7 @@ function version() {
925
925
  }
926
926
 
927
927
  // src/context.ts
928
- import { existsSync as existsSync2 } from "fs";
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 readFileSync2 } from "fs";
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: `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.`
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 && existsSync(regFile)) {
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(readFileSync2(file, "utf8"));
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 existsSync4 } from "fs";
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 existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
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) => existsSync3(c.path));
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(readFileSync3(abs, "utf8"));
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(readFileSync3(file.path, "utf8"));
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
- if (entry.url) {
3140
+ const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
3141
+ if (url) {
2792
3142
  upstreams.push({
2793
3143
  name,
2794
- url: entry.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(readFileSync3(file.path, "utf8"));
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 (!existsSync3(backup)) copyFileSync(file.path, backup);
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 (!existsSync3(backup)) return false;
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(readFileSync3(file.path, "utf8"));
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 (!existsSync3(backup)) copyFileSync(file.path, backup);
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 (!existsSync4(c.path)) {
3282
+ if (!existsSync5(c.path)) {
2933
3283
  io.err(`no MCP config at ${c.path}`);
2934
3284
  return 1;
2935
3285
  }
2936
- const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, entry);
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 existsSync5, writeFileSync as writeFileSync3 } from "fs";
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 tools = [];
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.push(...page.tools);
3264
- cursor = page.nextCursor;
3265
- } while (cursor);
3266
- conn.tools = tools;
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) => existsSync5(c.path))) {
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 && existsSync5(policyPath)) {
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 (existsSync5(policyPath) && !flagBool(args, "force")) {
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 readFileSync4, writeFileSync as writeFileSync4 } from "fs";
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(readFileSync4(path, "utf8"));
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 { resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
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 server2 = await startHttpProxy({
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: server2.url.replace(/\/mcp$/, ""), token: server2.token };
4551
+ approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
4133
4552
  log(
4134
- `Streamable HTTP at ${server2.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
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 ${server2.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4556
+ `point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4138
4557
  );
4139
- const stop2 = async () => {
4140
- await server2.close();
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 stop2());
4145
- process.once("SIGTERM", () => void stop2());
4563
+ process.once("SIGINT", () => void stop());
4564
+ process.once("SIGTERM", () => void stop());
4146
4565
  await new Promise(() => void 0);
4147
4566
  }
4148
- const server = createDownstreamServer(runtime);
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") ?? latestRunId(entries);
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 = buildReport(entries, { runId });
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(renderReportMarkdown(report));
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 && listRuns(entries).length > 1) io.out(`Other runs: agentguard report --all`);
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") ?? latestRunId(entries) ?? "(none yet)";
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);