@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/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;
@@ -1931,7 +1935,7 @@ async function serveStdio(serverOrOpts) {
1931
1935
  }
1932
1936
 
1933
1937
  // src/proxy/http.ts
1934
- import { writeFileSync, unlinkSync, existsSync, mkdirSync } from "fs";
1938
+ import { writeFileSync, unlinkSync, existsSync as existsSync2, mkdirSync } from "fs";
1935
1939
  import { createServer } from "http";
1936
1940
  import { join } from "path";
1937
1941
  import { FileKillSwitch } from "@agentwares/agentguard-core/node";
@@ -1955,6 +1959,278 @@ import {
1955
1959
  keyFromHeaders,
1956
1960
  resolveAgent
1957
1961
  } from "@agentwares/agentguard-core";
1962
+
1963
+ // src/proxy/own-tools.ts
1964
+ import {
1965
+ buildReport,
1966
+ latestRunId,
1967
+ listRuns,
1968
+ parseAuditJsonl,
1969
+ renderReportMarkdown,
1970
+ verifyChain
1971
+ } from "@agentwares/agentguard-core";
1972
+ import { existsSync, readFileSync as readFileSync2 } from "fs";
1973
+ import { z as z5 } from "zod";
1974
+ var INIT_COMMAND = "npx -y @agentwares/agentguard init";
1975
+ var READ_ONLY = {
1976
+ readOnlyHint: true,
1977
+ destructiveHint: false,
1978
+ idempotentHint: true,
1979
+ openWorldHint: false
1980
+ };
1981
+ var runIdInput = z5.string().min(1).max(128).optional().describe(
1982
+ "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."
1983
+ );
1984
+ var counters = z5.record(z5.string(), z5.number()).describe(
1985
+ "counter name (tool_calls, writes, deletes, emails, spend_usd or a custom one) \u2192 value"
1986
+ );
1987
+ function readAudit(path) {
1988
+ if (!existsSync(path)) return { exists: false, entries: [] };
1989
+ try {
1990
+ return { exists: true, entries: parseAuditJsonl(readFileSync2(path, "utf8")) };
1991
+ } catch (err) {
1992
+ return { exists: true, entries: [], error: err instanceof Error ? err.message : String(err) };
1993
+ }
1994
+ }
1995
+ function unreadable(path, error) {
1996
+ return toolError({
1997
+ code: "AUDIT_UNREADABLE",
1998
+ cause: `the audit log at ${path} could not be read: ${error}`,
1999
+ fix: "call agentguard_verify_audit_log to see where it breaks; a hand-edited or truncated line has to be restored from a copy",
2000
+ retryable: false
2001
+ });
2002
+ }
2003
+ function numbers(caps) {
2004
+ const out = {};
2005
+ for (const [k, v] of Object.entries(caps)) if (typeof v === "number") out[k] = v;
2006
+ return out;
2007
+ }
2008
+ function nextStep(runtime) {
2009
+ if (!runtime.configured)
2010
+ 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.`;
2011
+ if (runtime.policy.upstreams.length === 0)
2012
+ 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.`;
2013
+ return "agentguard is running; run your agent, then read agentguard_get_report.";
2014
+ }
2015
+ var StatusOutput = z5.object({
2016
+ configured: z5.boolean().describe(
2017
+ "true when a policy file was found and loaded; false means agentguard guards nothing"
2018
+ ),
2019
+ policy_path: z5.string().describe("absolute path of the policy file read, or looked for"),
2020
+ mode: z5.enum(["dry-run", "enforce"]).describe("dry-run fakes writes and spends and records them; enforce lets allowed calls run"),
2021
+ upstreams: z5.array(
2022
+ z5.object({
2023
+ name: z5.string().describe("upstream name from the policy"),
2024
+ connected: z5.boolean().describe("whether agentguard is connected to it"),
2025
+ tools: z5.number().int().describe("tools it exposes"),
2026
+ transport: z5.enum(["stdio", "http"]).describe("how agentguard reaches it"),
2027
+ error: z5.string().optional().describe("why it is not connected")
2028
+ })
2029
+ ).describe("the MCP servers this proxy fronts; empty means none are configured"),
2030
+ run_id: z5.string().nullable().describe(
2031
+ "run the usage is for: the requested run_id, else the latest run in the log, else null"
2032
+ ),
2033
+ caps: z5.object({ per_run: counters, per_day: counters }).describe("limits from the policy; a counter that is absent has no cap"),
2034
+ used: z5.object({ per_run: counters, per_day: counters }).describe("what that run, and today, have used against the caps"),
2035
+ remaining: z5.record(
2036
+ z5.string(),
2037
+ z5.object({ per_run: z5.number().optional(), per_day: z5.number().optional() })
2038
+ ).describe("budget left per capped counter, per run and per day"),
2039
+ kill_switch: z5.object({
2040
+ on: z5.boolean().describe("true halts every tool call with KILLED"),
2041
+ reason: z5.string().optional().describe("why it was pulled"),
2042
+ source: z5.string().optional().describe("file, env or memory")
2043
+ }).describe("the kill switch state"),
2044
+ pending_approvals: z5.number().int().describe("destructive calls waiting for a human decision"),
2045
+ audit_log: z5.object({
2046
+ path: z5.string().describe("absolute path of the hash-chained JSONL audit log"),
2047
+ entries: z5.number().int().describe("entries in it; 0 when it does not exist yet")
2048
+ }).describe("where every tool call is recorded"),
2049
+ next_step: z5.string().describe("the one thing to do next, with the command when there is one")
2050
+ });
2051
+ var ReportOutput = z5.object({
2052
+ configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
2053
+ audit_log_path: z5.string().describe("absolute path of the audit log the report was built from"),
2054
+ run_id: z5.string().nullable().describe("the run reported on; null when the log has no runs"),
2055
+ runs_in_log: z5.number().int().describe("how many runs the audit log holds"),
2056
+ tool_calls: z5.number().int().describe("tool calls in this run"),
2057
+ would_have: z5.object({
2058
+ creates: z5.number().int(),
2059
+ updates: z5.number().int(),
2060
+ deletes: z5.number().int(),
2061
+ sends: z5.number().int(),
2062
+ executes: z5.number().int(),
2063
+ spends: z5.number().int(),
2064
+ spend_usd: z5.number()
2065
+ }).describe("what dry-run mode faked: what the run would have done had the calls executed"),
2066
+ did: z5.object({
2067
+ writes: z5.number().int(),
2068
+ deletes: z5.number().int(),
2069
+ sends: z5.number().int(),
2070
+ spend_usd: z5.number()
2071
+ }).describe("what actually executed upstream"),
2072
+ halts: z5.array(
2073
+ z5.object({
2074
+ seq: z5.number().int().describe("audit entry number"),
2075
+ ts: z5.string().describe("ISO time of the call"),
2076
+ code: z5.string().describe("CAP_EXCEEDED, LOOP_DETECTED, KILLED, APPROVAL_REQUIRED, \u2026"),
2077
+ tool: z5.string().describe("the tool that was stopped"),
2078
+ cause: z5.string().describe("why")
2079
+ })
2080
+ ).describe("every call agentguard stopped, in order"),
2081
+ 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)"),
2082
+ markdown: z5.string().describe("the same report `agentguard report` prints, as Markdown")
2083
+ });
2084
+ var VerifyOutput = z5.object({
2085
+ configured: z5.boolean().describe("false when no policy file was found (agentguard guards nothing)"),
2086
+ path: z5.string().describe("absolute path of the audit log checked"),
2087
+ exists: z5.boolean().describe("false when no call has been recorded yet; an absent log verifies"),
2088
+ ok: z5.boolean().describe("true when every entry's hash and prev_hash recompute"),
2089
+ entries: z5.number().int().describe("entries checked"),
2090
+ head_hash: z5.string().nullable().describe(
2091
+ "hash of the last entry; record it with the count to detect entries cut from the end"
2092
+ ),
2093
+ broken_at: z5.number().int().nullable().describe("seq of the first entry that does not verify"),
2094
+ reason: z5.string().nullable().describe("why verification failed")
2095
+ });
2096
+ function createOwnTools(runtime) {
2097
+ const status = defineTool({
2098
+ name: "agentguard_get_status",
2099
+ title: "agentguard status",
2100
+ 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.",
2101
+ input: z5.strictObject({ run_id: runIdInput }),
2102
+ output: StatusOutput,
2103
+ annotations: { title: "agentguard status", ...READ_ONLY },
2104
+ async handler({ run_id }) {
2105
+ const { loaded, guard } = runtime;
2106
+ const audit = readAudit(loaded.auditPath);
2107
+ if (audit.error) throw unreadable(loaded.auditPath, audit.error);
2108
+ const runId = run_id ?? latestRunId(audit.entries) ?? null;
2109
+ const state = await guard.status(runId ?? "(none yet)");
2110
+ const pending = await guard.approvals.list("pending");
2111
+ return {
2112
+ configured: runtime.configured,
2113
+ policy_path: loaded.path,
2114
+ mode: runtime.policy.mode,
2115
+ upstreams: runtime.upstreamStatus(),
2116
+ run_id: runId,
2117
+ caps: { per_run: numbers(state.caps.per_run), per_day: numbers(state.caps.per_day) },
2118
+ used: state.usage,
2119
+ remaining: state.remaining,
2120
+ kill_switch: {
2121
+ on: state.killed.killed,
2122
+ ...state.killed.reason ? { reason: state.killed.reason } : {},
2123
+ ...state.killed.source ? { source: state.killed.source } : {}
2124
+ },
2125
+ pending_approvals: pending.length,
2126
+ audit_log: { path: loaded.auditPath, entries: audit.entries.length },
2127
+ next_step: nextStep(runtime)
2128
+ };
2129
+ }
2130
+ });
2131
+ const report = defineTool({
2132
+ name: "agentguard_get_report",
2133
+ title: "agentguard run report",
2134
+ 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.",
2135
+ input: z5.strictObject({ run_id: runIdInput }),
2136
+ output: ReportOutput,
2137
+ annotations: { title: "agentguard run report", ...READ_ONLY },
2138
+ handler({ run_id }) {
2139
+ const { loaded } = runtime;
2140
+ const audit = readAudit(loaded.auditPath);
2141
+ if (audit.error) throw unreadable(loaded.auditPath, audit.error);
2142
+ const runs = listRuns(audit.entries);
2143
+ if (run_id && !runs.some((r) => r.runId === run_id)) {
2144
+ throw toolError({
2145
+ code: "NOT_FOUND",
2146
+ cause: `no run "${run_id}" in the audit log at ${loaded.auditPath}`,
2147
+ 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"}`,
2148
+ retryable: false
2149
+ });
2150
+ }
2151
+ const runId = run_id ?? latestRunId(audit.entries) ?? null;
2152
+ const built = buildReport(audit.entries, { runId: runId ?? void 0 });
2153
+ const markdown = runId ? renderReportMarkdown(built) : `# agentguard report
2154
+
2155
+ No runs recorded in ${loaded.auditPath} yet. ${nextStep(runtime)}`;
2156
+ return {
2157
+ configured: runtime.configured,
2158
+ audit_log_path: loaded.auditPath,
2159
+ run_id: runId,
2160
+ runs_in_log: runs.length,
2161
+ tool_calls: runId ? audit.entries.filter((e) => e.run_id === runId).length : 0,
2162
+ would_have: {
2163
+ creates: built.wouldHave.creates,
2164
+ updates: built.wouldHave.updates,
2165
+ deletes: built.wouldHave.deletes,
2166
+ sends: built.wouldHave.sends,
2167
+ executes: built.wouldHave.executes,
2168
+ spends: built.wouldHave.spends,
2169
+ spend_usd: built.wouldHave.spendUsd
2170
+ },
2171
+ did: {
2172
+ writes: built.did.writes,
2173
+ deletes: built.did.deletes,
2174
+ sends: built.did.sends,
2175
+ spend_usd: built.did.spendUsd
2176
+ },
2177
+ halts: built.halts,
2178
+ top_tools: built.topTools,
2179
+ markdown
2180
+ };
2181
+ }
2182
+ });
2183
+ const verify = defineTool({
2184
+ name: "agentguard_verify_audit_log",
2185
+ title: "Verify agentguard audit log",
2186
+ 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.",
2187
+ input: z5.strictObject({}),
2188
+ output: VerifyOutput,
2189
+ annotations: { title: "Verify agentguard audit log", ...READ_ONLY },
2190
+ async handler() {
2191
+ const { loaded } = runtime;
2192
+ const audit = readAudit(loaded.auditPath);
2193
+ if (audit.error) {
2194
+ return {
2195
+ configured: runtime.configured,
2196
+ path: loaded.auditPath,
2197
+ exists: true,
2198
+ ok: false,
2199
+ entries: 0,
2200
+ head_hash: null,
2201
+ broken_at: null,
2202
+ reason: audit.error
2203
+ };
2204
+ }
2205
+ const result = await verifyChain(audit.entries);
2206
+ return {
2207
+ configured: runtime.configured,
2208
+ path: loaded.auditPath,
2209
+ exists: audit.exists,
2210
+ ok: result.ok,
2211
+ entries: result.entries,
2212
+ // an empty chain's head is the genesis hash, which is not an entry anyone can record
2213
+ head_hash: result.entries > 0 ? result.head ?? null : null,
2214
+ broken_at: result.brokenAt ?? null,
2215
+ reason: result.reason ?? null
2216
+ };
2217
+ }
2218
+ });
2219
+ return [status, report, verify];
2220
+ }
2221
+ function ownToolsActive(runtime) {
2222
+ return runtime.upstreamStatus().length === 0;
2223
+ }
2224
+ function ownToolList(tools) {
2225
+ return listToolManifest(tools);
2226
+ }
2227
+ async function callOwnTool(tools, name, args, requestId) {
2228
+ const tool = tools.find((t) => t.name === name);
2229
+ if (!tool) return void 0;
2230
+ return tool.invoke(args, createToolContext({ requestId }));
2231
+ }
2232
+
2233
+ // src/proxy/server.ts
1958
2234
  var PROXY_VERSION = version();
1959
2235
  function headerValue(extra, name) {
1960
2236
  const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
@@ -2055,6 +2331,11 @@ function killedError(state) {
2055
2331
  { code: -32e3 }
2056
2332
  );
2057
2333
  }
2334
+ function serverInstructions(runtime) {
2335
+ if (ownToolsActive(runtime))
2336
+ 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.`;
2337
+ 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.`;
2338
+ }
2058
2339
  function createDownstreamServer(runtime, opts = {}) {
2059
2340
  const server = new McpServer3(
2060
2341
  { name: "agentguard", version: PROXY_VERSION, title: "agentguard policy proxy" },
@@ -2066,9 +2347,10 @@ function createDownstreamServer(runtime, opts = {}) {
2066
2347
  logging: {},
2067
2348
  completions: {}
2068
2349
  },
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.`
2350
+ instructions: serverInstructions(runtime)
2070
2351
  }
2071
2352
  );
2353
+ const ownTools = createOwnTools(runtime);
2072
2354
  const session = {
2073
2355
  id: crypto.randomUUID(),
2074
2356
  server,
@@ -2106,12 +2388,22 @@ function createDownstreamServer(runtime, opts = {}) {
2106
2388
  touch();
2107
2389
  const { agent } = await agentFor(extra);
2108
2390
  const scope = agent === void 0 ? runtime.agent : agent;
2391
+ if (ownToolsActive(runtime)) return { tools: ownToolList(ownTools) };
2109
2392
  return {
2110
2393
  tools: runtime.listTools(scope).map((t) => annotateTool(t, scope?.mode ?? runtime.policy.mode))
2111
2394
  };
2112
2395
  });
2113
2396
  low.setRequestHandler(CallToolRequestSchema2, async (req, extra) => {
2114
2397
  touch();
2398
+ if (ownToolsActive(runtime)) {
2399
+ const own = await callOwnTool(
2400
+ ownTools,
2401
+ req.params.name,
2402
+ req.params.arguments ?? {},
2403
+ String(extra.requestId)
2404
+ );
2405
+ if (own) return own;
2406
+ }
2115
2407
  const { agent, error } = await agentFor(extra);
2116
2408
  const runId = runIdFor(extra, req.params._meta, opts.defaultRunId ?? runtime.defaultRunId);
2117
2409
  if (error) {
@@ -2378,7 +2670,7 @@ async function startHttpProxy(opts) {
2378
2670
  port,
2379
2671
  token,
2380
2672
  close: () => new Promise((resolve5, reject) => {
2381
- if (opts.registerFile !== false && existsSync(regFile)) {
2673
+ if (opts.registerFile !== false && existsSync2(regFile)) {
2382
2674
  try {
2383
2675
  unlinkSync(regFile);
2384
2676
  } catch {
@@ -2674,7 +2966,7 @@ function readRegistration(loaded) {
2674
2966
  const file = registrationPath(loaded.stateDir);
2675
2967
  if (!exists(file)) return void 0;
2676
2968
  try {
2677
- const reg = JSON.parse(readFileSync2(file, "utf8"));
2969
+ const reg = JSON.parse(readFileSync3(file, "utf8"));
2678
2970
  return reg;
2679
2971
  } catch {
2680
2972
  return void 0;
@@ -2690,11 +2982,11 @@ function describeError(err) {
2690
2982
  }
2691
2983
 
2692
2984
  // src/commands/connect.ts
2693
- import { existsSync as existsSync4 } from "fs";
2985
+ import { existsSync as existsSync5 } from "fs";
2694
2986
  import { relative } from "path";
2695
2987
 
2696
2988
  // src/configs.ts
2697
- import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
2989
+ import { copyFileSync, existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync2 } from "fs";
2698
2990
  import { homedir as homedir2, platform } from "os";
2699
2991
  import { join as join2, resolve as resolve2 } from "path";
2700
2992
  function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.env) {
@@ -2727,6 +3019,12 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
2727
3019
  serversKey: "servers"
2728
3020
  },
2729
3021
  { kind: "generic", label: "mcp.json", path: join2(cwd, "mcp.json"), serversKey: "mcpServers" },
3022
+ {
3023
+ kind: "gemini-project",
3024
+ label: "Gemini CLI (project .gemini/settings.json)",
3025
+ path: join2(cwd, ".gemini", "settings.json"),
3026
+ serversKey: "mcpServers"
3027
+ },
2730
3028
  { kind: "claude-desktop", label: "Claude Desktop", path: desktop, serversKey: "mcpServers" },
2731
3029
  {
2732
3030
  kind: "cursor-user",
@@ -2745,11 +3043,20 @@ function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.
2745
3043
  label: "Claude Code (user ~/.claude.json)",
2746
3044
  path: join2(home, ".claude.json"),
2747
3045
  serversKey: "mcpServers"
3046
+ },
3047
+ {
3048
+ kind: "gemini-user",
3049
+ label: "Gemini CLI (user ~/.gemini/settings.json)",
3050
+ path: join2(home, ".gemini", "settings.json"),
3051
+ serversKey: "mcpServers"
2748
3052
  }
2749
3053
  ];
2750
3054
  }
3055
+ function isGeminiConfig(file) {
3056
+ return file.kind === "gemini-project" || file.kind === "gemini-user" || /[/\\]\.gemini[/\\]settings\.json$/.test(file.path);
3057
+ }
2751
3058
  function detectClientConfigs(cwd, home, env) {
2752
- return candidateConfigs(cwd, home, env).filter((c) => existsSync3(c.path));
3059
+ return candidateConfigs(cwd, home, env).filter((c) => existsSync4(c.path));
2753
3060
  }
2754
3061
  function describeConfigPath(path) {
2755
3062
  const abs = resolve2(path);
@@ -2757,7 +3064,7 @@ function describeConfigPath(path) {
2757
3064
  if (known) return known;
2758
3065
  let serversKey = "mcpServers";
2759
3066
  try {
2760
- const parsed = JSON.parse(readFileSync3(abs, "utf8"));
3067
+ const parsed = JSON.parse(readFileSync4(abs, "utf8"));
2761
3068
  if (parsed.servers && !parsed.mcpServers) serversKey = "servers";
2762
3069
  } catch {
2763
3070
  }
@@ -2769,7 +3076,7 @@ function describeConfigPath(path) {
2769
3076
  };
2770
3077
  }
2771
3078
  function readServers(file) {
2772
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3079
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
2773
3080
  const servers = raw[file.serversKey];
2774
3081
  if (!servers || typeof servers !== "object") return {};
2775
3082
  return servers;
@@ -2788,10 +3095,11 @@ function serversToUpstreams(servers) {
2788
3095
  continue;
2789
3096
  }
2790
3097
  const name = rawName.replace(/[^a-zA-Z0-9_-]/g, "-").replace(/^[^a-zA-Z0-9]+/, "") || "upstream";
2791
- if (entry.url) {
3098
+ const url = typeof entry.httpUrl === "string" ? entry.httpUrl : entry.url;
3099
+ if (url) {
2792
3100
  upstreams.push({
2793
3101
  name,
2794
- url: entry.url,
3102
+ url,
2795
3103
  headers: entry.headers ?? {},
2796
3104
  args: [],
2797
3105
  env: {},
@@ -2826,27 +3134,27 @@ function proxyEntry(file, policyPath, opts = {}) {
2826
3134
  return entry;
2827
3135
  }
2828
3136
  function rewriteClientConfig(file, policyPath, opts = {}) {
2829
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3137
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
2830
3138
  const servers = raw[file.serversKey] ?? {};
2831
3139
  const replaced = Object.keys(servers).filter((n) => !isProxyEntry(servers[n]));
2832
3140
  const backup = backupPath(file);
2833
- if (!existsSync3(backup)) copyFileSync(file.path, backup);
3141
+ if (!existsSync4(backup)) copyFileSync(file.path, backup);
2834
3142
  raw[file.serversKey] = { [PROXY_SERVER_NAME]: proxyEntry(file, policyPath, opts) };
2835
3143
  writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
2836
3144
  return { backup, replaced };
2837
3145
  }
2838
3146
  function restoreClientConfig(file) {
2839
3147
  const backup = backupPath(file);
2840
- if (!existsSync3(backup)) return false;
3148
+ if (!existsSync4(backup)) return false;
2841
3149
  copyFileSync(backup, file.path);
2842
3150
  return true;
2843
3151
  }
2844
3152
  function mergeServerIntoConfig(file, name, entry) {
2845
- const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3153
+ const raw = JSON.parse(readFileSync4(file.path, "utf8"));
2846
3154
  const servers = raw[file.serversKey] ?? {};
2847
3155
  const replaced = name in servers;
2848
3156
  const backup = backupPath(file);
2849
- if (!existsSync3(backup)) copyFileSync(file.path, backup);
3157
+ if (!existsSync4(backup)) copyFileSync(file.path, backup);
2850
3158
  raw[file.serversKey] = { ...servers, [name]: entry };
2851
3159
  writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
2852
3160
  return { backup, replaced };
@@ -2929,11 +3237,12 @@ async function connectCommand(args, io, fetchImpl = fetch) {
2929
3237
  return 1;
2930
3238
  }
2931
3239
  for (const c of chosen) {
2932
- if (!existsSync4(c.path)) {
3240
+ if (!existsSync5(c.path)) {
2933
3241
  io.err(`no MCP config at ${c.path}`);
2934
3242
  return 1;
2935
3243
  }
2936
- const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, entry);
3244
+ const forClient = isGeminiConfig(c) ? { httpUrl: entry.url, ...entry.headers ? { headers: entry.headers } : {} } : entry;
3245
+ const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, forClient);
2937
3246
  io.out(
2938
3247
  `${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
2939
3248
  );
@@ -3048,7 +3357,7 @@ async function approvalsCommand(args, io) {
3048
3357
  }
3049
3358
 
3050
3359
  // src/commands/init.ts
3051
- import { existsSync as existsSync5, writeFileSync as writeFileSync3 } from "fs";
3360
+ import { existsSync as existsSync6, writeFileSync as writeFileSync3 } from "fs";
3052
3361
  import { relative as relative2, resolve as resolve3 } from "path";
3053
3362
  import YAML from "yaml";
3054
3363
 
@@ -3085,6 +3394,7 @@ var ProxyRuntime = class {
3085
3394
  guard;
3086
3395
  agent;
3087
3396
  defaultRunId;
3397
+ configured;
3088
3398
  sessions = /* @__PURE__ */ new Set();
3089
3399
  connections = /* @__PURE__ */ new Map();
3090
3400
  exposed = [];
@@ -3101,6 +3411,7 @@ var ProxyRuntime = class {
3101
3411
  this.guard = opts.guard;
3102
3412
  this.agent = opts.agent;
3103
3413
  this.defaultRunId = opts.defaultRunId;
3414
+ this.configured = opts.configured ?? true;
3104
3415
  this.log = opts.log ?? ((line) => process.stderr.write(`[agentguard] ${line}
3105
3416
  `));
3106
3417
  this.connectTimeoutMs = opts.connectTimeoutMs ?? 15e3;
@@ -3645,6 +3956,10 @@ async function toolsCommand(args, io) {
3645
3956
  io.err(
3646
3957
  `${u.connected ? "ok " : "down"} ${u.name}: ${u.connected ? `${u.tools} tools` : u.error}`
3647
3958
  );
3959
+ if (result.upstreams.length === 0)
3960
+ io.err(
3961
+ "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)"
3962
+ );
3648
3963
  for (const line of formatToolTable(result.tools)) io.out(line);
3649
3964
  const counts = result.tools.reduce(
3650
3965
  (acc, t) => ({ ...acc, [t.classification.class]: (acc[t.classification.class] ?? 0) + 1 }),
@@ -3812,7 +4127,7 @@ async function initCommand(args, io) {
3812
4127
  `found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to route it through agentguard`
3813
4128
  );
3814
4129
  }
3815
- if (!chosen.every((c) => existsSync5(c.path))) {
4130
+ if (!chosen.every((c) => existsSync6(c.path))) {
3816
4131
  io.err(`no MCP config at ${chosen.map((c) => c.path).join(", ")}`);
3817
4132
  return 1;
3818
4133
  }
@@ -3827,11 +4142,11 @@ async function initCommand(args, io) {
3827
4142
  }
3828
4143
  if (chosen.length === 0)
3829
4144
  io.out(
3830
- "found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf) \u2014 writing a policy with no upstreams"
4145
+ "found no MCP config (Claude Code .mcp.json, Cursor, VS Code, Claude Desktop, Windsurf, Gemini CLI) \u2014 writing a policy with no upstreams"
3831
4146
  );
3832
4147
  const { upstreams, skipped } = serversToUpstreams(servers);
3833
4148
  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)) {
4149
+ if (skipped.some((s) => s.reason.includes("already")) && upstreams.length === 0 && existsSync6(policyPath)) {
3835
4150
  io.out(
3836
4151
  `already initialized: ${rel(policyPath)} exists and ${chosen.map((c) => rel(c.path)).join(", ")} already points at agentguard`
3837
4152
  );
@@ -3858,7 +4173,7 @@ async function initCommand(args, io) {
3858
4173
  }
3859
4174
  }
3860
4175
  const mode = flagString(args, "mode") ?? "dry-run";
3861
- if (existsSync5(policyPath) && !flagBool(args, "force")) {
4176
+ if (existsSync6(policyPath) && !flagBool(args, "force")) {
3862
4177
  io.out(`kept ${rel(policyPath)} (exists; pass --force to regenerate)`);
3863
4178
  } else {
3864
4179
  writeFileSync3(policyPath, renderStarterPolicy({ upstreams, tools: probed, mode, probeErrors }));
@@ -3902,13 +4217,13 @@ async function initCommand(args, io) {
3902
4217
  }
3903
4218
 
3904
4219
  // src/commands/keys.ts
3905
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4220
+ import { readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
3906
4221
  import YAML2, { isMap, isSeq, YAMLSeq } from "yaml";
3907
4222
  import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
3908
4223
  async function keyCommand(args, io) {
3909
4224
  const [sub, name] = args.positionals;
3910
4225
  const path = policyPathFrom(args, io);
3911
- const doc = YAML2.parseDocument(readFileSync4(path, "utf8"));
4226
+ const doc = YAML2.parseDocument(readFileSync5(path, "utf8"));
3912
4227
  let agents = doc.get("agents");
3913
4228
  if (!isSeq(agents)) {
3914
4229
  agents = new YAMLSeq();
@@ -4075,14 +4390,65 @@ async function permissionDiffCommand(args, io) {
4075
4390
  }
4076
4391
 
4077
4392
  // src/commands/proxy.ts
4078
- import { resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
4393
+ import { existsSync as existsSync7 } from "fs";
4394
+ import { dirname } from "path";
4395
+ import { Guard as Guard3, defaultPolicy, resolveAgent as resolveAgent2 } from "@agentwares/agentguard-core";
4396
+ import { FileKillSwitch as FileKillSwitch3, resolvePath } from "@agentwares/agentguard-core/node";
4079
4397
  function newRunId(now = /* @__PURE__ */ new Date()) {
4080
4398
  return `run_${now.toISOString().replace(/[-:.TZ]/g, "").slice(0, 14)}_${Math.random().toString(36).slice(2, 6)}`;
4081
4399
  }
4400
+ function unconfiguredPolicy(args, io) {
4401
+ if (flagString(args, "config") !== void 0 || io.env.AGENTGUARD_CONFIG) return void 0;
4402
+ const path = policyPathFrom(args, io);
4403
+ if (existsSync7(path)) return void 0;
4404
+ const policy = defaultPolicy();
4405
+ const baseDir = dirname(path);
4406
+ return {
4407
+ policy,
4408
+ path,
4409
+ baseDir,
4410
+ stateDir: resolvePath(policy.state.dir, baseDir),
4411
+ auditPath: resolvePath(policy.audit.path, baseDir),
4412
+ killPath: resolvePath(policy.kill.file, baseDir)
4413
+ };
4414
+ }
4415
+ async function serveOverStdio(runtime) {
4416
+ const server = createDownstreamServer(runtime);
4417
+ const stop = async () => {
4418
+ await runtime.close();
4419
+ };
4420
+ process.once("SIGINT", () => void stop().then(() => process.exit(0)));
4421
+ process.once("SIGTERM", () => void stop().then(() => process.exit(0)));
4422
+ await serveStdio(server);
4423
+ await stop();
4424
+ return 0;
4425
+ }
4082
4426
  async function proxyCommand(args, io) {
4083
- const loaded = loadPolicyFor(args, io);
4084
4427
  const log = (line) => io.err(`[agentguard] ${line}`);
4085
4428
  const modeOverride = flagString(args, "mode");
4429
+ const http = flagBool(args, "http") === true;
4430
+ const unconfigured = http ? void 0 : unconfiguredPolicy(args, io);
4431
+ if (unconfigured) {
4432
+ if (modeOverride === "dry-run" || modeOverride === "enforce")
4433
+ unconfigured.policy.mode = modeOverride;
4434
+ const runtime2 = new ProxyRuntime({
4435
+ loaded: unconfigured,
4436
+ guard: new Guard3({
4437
+ policy: unconfigured.policy,
4438
+ env: io.env,
4439
+ kill: new FileKillSwitch3(unconfigured.killPath)
4440
+ }),
4441
+ configured: false,
4442
+ defaultRunId: flagString(args, "run-id") ?? newRunId(),
4443
+ log
4444
+ });
4445
+ await runtime2.start();
4446
+ log(
4447
+ `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.`
4448
+ );
4449
+ return serveOverStdio(runtime2);
4450
+ }
4451
+ const loaded = loadPolicyFor(args, io);
4086
4452
  if (modeOverride === "dry-run" || modeOverride === "enforce") loaded.policy.mode = modeOverride;
4087
4453
  const agentName = flagString(args, "agent");
4088
4454
  let agent = void 0;
@@ -4096,7 +4462,6 @@ async function proxyCommand(args, io) {
4096
4462
  }
4097
4463
  agent = resolved.agent;
4098
4464
  }
4099
- const http = flagBool(args, "http") === true;
4100
4465
  let approvalBase;
4101
4466
  const fileGuard = createFileGuard(loaded, {
4102
4467
  log,
@@ -4122,59 +4487,55 @@ async function proxyCommand(args, io) {
4122
4487
  log(
4123
4488
  `kill switch is ON (${killed.reason}); every call returns KILLED until \`agentguard resume\``
4124
4489
  );
4490
+ if (runtime.upstreamStatus().length === 0)
4491
+ log(
4492
+ `${loaded.path} lists no upstreams: serving agentguard's own read-only tools only. Add servers under \`upstreams:\` to guard them.`
4493
+ );
4125
4494
  if (http) {
4126
- const server2 = await startHttpProxy({
4495
+ const server = await startHttpProxy({
4127
4496
  runtime,
4128
4497
  port: flagNumber(args, "port") ?? 8788,
4129
4498
  host: flagString(args, "host") ?? "127.0.0.1",
4130
4499
  token: io.env.AGENTGUARD_HTTP_TOKEN
4131
4500
  });
4132
- approvalBase = { baseUrl: server2.url.replace(/\/mcp$/, ""), token: server2.token };
4501
+ approvalBase = { baseUrl: server.url.replace(/\/mcp$/, ""), token: server.token };
4133
4502
  log(
4134
- `Streamable HTTP at ${server2.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
4503
+ `Streamable HTTP at ${server.url} \xB7 GET ${approvalBase.baseUrl}/health \xB7 control token in ${loaded.stateDir}/http.json`
4135
4504
  );
4136
4505
  log(
4137
- `point your agent at ${server2.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4506
+ `point your agent at ${server.url} (send X-Run-Id per run; Authorization: Bearer <agent key> for scoped agents)`
4138
4507
  );
4139
- const stop2 = async () => {
4140
- await server2.close();
4508
+ const stop = async () => {
4509
+ await server.close();
4141
4510
  await runtime.close();
4142
4511
  process.exit(0);
4143
4512
  };
4144
- process.once("SIGINT", () => void stop2());
4145
- process.once("SIGTERM", () => void stop2());
4513
+ process.once("SIGINT", () => void stop());
4514
+ process.once("SIGTERM", () => void stop());
4146
4515
  await new Promise(() => void 0);
4147
4516
  }
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;
4517
+ return serveOverStdio(runtime);
4157
4518
  }
4158
4519
 
4159
4520
  // src/commands/report.ts
4160
4521
  import { resolve as resolve4 } from "path";
4161
4522
  import {
4162
- buildReport,
4163
- latestRunId,
4164
- listRuns,
4523
+ buildReport as buildReport2,
4524
+ latestRunId as latestRunId2,
4525
+ listRuns as listRuns2,
4165
4526
  renderMutationDiff,
4166
- renderReportMarkdown
4527
+ renderReportMarkdown as renderReportMarkdown2
4167
4528
  } from "@agentwares/agentguard-core";
4168
4529
  import { readAuditFile, verifyAuditFile } from "@agentwares/agentguard-core/node";
4169
4530
  function pickRun(args, entries) {
4170
4531
  if (flagBool(args, "all")) return void 0;
4171
- return flagString(args, "run") ?? latestRunId(entries);
4532
+ return flagString(args, "run") ?? latestRunId2(entries);
4172
4533
  }
4173
4534
  async function reportCommand(args, io) {
4174
4535
  const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
4175
4536
  const entries = readAuditFile(loaded.auditPath);
4176
4537
  const runId = pickRun(args, entries);
4177
- const report = buildReport(entries, { runId });
4538
+ const report = buildReport2(entries, { runId });
4178
4539
  if (flagBool(args, "json")) {
4179
4540
  io.out(
4180
4541
  JSON.stringify(
@@ -4193,12 +4554,12 @@ async function reportCommand(args, io) {
4193
4554
  );
4194
4555
  return 0;
4195
4556
  }
4196
- io.out(renderReportMarkdown(report));
4557
+ io.out(renderReportMarkdown2(report));
4197
4558
  const chain = await verifyAuditFile(loaded.auditPath);
4198
4559
  io.out(
4199
4560
  chain.ok ? `Audit chain: ${chain.entries} entries, verified (${loaded.auditPath}).` : `Audit chain BROKEN at entry ${chain.brokenAt}: ${chain.reason}`
4200
4561
  );
4201
- if (runId && listRuns(entries).length > 1) io.out(`Other runs: agentguard report --all`);
4562
+ if (runId && listRuns2(entries).length > 1) io.out(`Other runs: agentguard report --all`);
4202
4563
  return 0;
4203
4564
  }
4204
4565
  async function diffCommand(args, io) {
@@ -4225,7 +4586,7 @@ async function statusCommand(args, io) {
4225
4586
  const loaded = loadPolicyFor(args, io, { allowMissingEnv: true });
4226
4587
  const { guard, kill } = createFileGuard(loaded, { alerts: false, env: io.env });
4227
4588
  const entries = readAuditFile(loaded.auditPath);
4228
- const runId = flagString(args, "run") ?? latestRunId(entries) ?? "(none yet)";
4589
+ const runId = flagString(args, "run") ?? latestRunId2(entries) ?? "(none yet)";
4229
4590
  const status = await guard.status(runId);
4230
4591
  const killed = kill.check();
4231
4592
  const reg = readRegistration(loaded);