@agentwares/agentguard 0.1.2 → 0.1.4

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.
File without changes
package/dist/index.d.ts CHANGED
@@ -482,7 +482,7 @@ interface DemoOptions {
482
482
  /** Drive a connected client through the scripted scenario. Returns every step's outcome. */
483
483
  declare function runDemoAgent(client: Client, opts?: DemoOptions): Promise<DemoStep[]>;
484
484
 
485
- declare const HELP = "agentguard \u2014 MCP policy proxy for agents that touch production\n\n npx @agentwares/agentguard init read your MCP config, write agentguard.yaml (dry-run), route every server through the proxy\n agentguard report [--run id|--all] what this run did / would have destroyed / spent, where it was halted\n agentguard diff [--run id] record-by-record mutation diff from a dry run\n agentguard verify [audit.jsonl] prove the hash-chained audit log was not edited\n\n agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode dry-run|enforce]\n agentguard tools [--json] the tools your agent will see, with class and why\n agentguard status [--run id] counters vs caps, kill state, pending approvals\n\n agentguard kill [reason] halt every run now (file + running HTTP proxy); agentguard resume\n agentguard approvals [--all] pending approvals; agentguard approve <id> | deny <id> [--note text]\n agentguard key create <agent> [--allow 'crm_get_*']... [--deny p] [--writes n] [--spend n] [--mode m]\n agentguard key list | revoke <agent>\n agentguard permission-diff [--base ref] [--head ref] [--paths a,b] [--format markdown|json] [--fail-on-widen]\n\nOptions: --config <agentguard.yaml> (or AGENTGUARD_CONFIG), --json, --help, --version\nDocs: https://github.com/agentwares/agentguard/tree/main/apps/agentguard-cli";
485
+ declare const HELP = "agentguard \u2014 MCP policy proxy for agents that touch production\n\n npx @agentwares/agentguard init read your MCP config, write agentguard.yaml (dry-run), route every server through the proxy\n agentguard report [--run id|--all] what this run did / would have destroyed / spent, where it was halted\n agentguard diff [--run id] record-by-record mutation diff from a dry run\n agentguard verify [audit.jsonl] prove the hash-chained audit log was not edited\n agentguard connect <key> [--write] point this machine at your hosted proxy (paid tiers)\n\n agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode dry-run|enforce]\n agentguard tools [--json] the tools your agent will see, with class and why\n agentguard status [--run id] counters vs caps, kill state, pending approvals\n\n agentguard kill [reason] halt every run now (file + running HTTP proxy); agentguard resume\n agentguard approvals [--all] pending approvals; agentguard approve <id> | deny <id> [--note text]\n agentguard key create <agent> [--allow 'crm_get_*']... [--deny p] [--writes n] [--spend n] [--mode m]\n agentguard key list | revoke <agent>\n agentguard permission-diff [--base ref] [--head ref] [--paths a,b] [--format markdown|json] [--fail-on-widen]\n\nOptions: --config <agentguard.yaml> (or AGENTGUARD_CONFIG), --json, --help, --version\nDocs: https://github.com/agentwares/agentguard/tree/main/apps/agentguard-cli";
486
486
  declare function main(argv: readonly string[], io?: Io): Promise<number>;
487
487
 
488
488
  export { type CallContext, type ClientConfigFile, type CrmState, DEFAULT_PERMISSION_PATHS, type DemoOptions, type DemoStep, type DownstreamOptions, type DownstreamSession, type ExposedTool, type FileGuard, HELP, type HttpProxy, type HttpProxyOptions, type HttpRegistration, type Io, type PermissionDiffResult, type ProbeResult, type ProbedTool, ProxyRuntime, type RuntimeOptions, type ServerEntry, type UpstreamStatus, approvalSlackMessage, candidateConfigs, createAlerter, createCrmMcpServer, createDownstreamServer, createFileGuard, crmTools, detectClientConfigs, haltSlackMessage, loadPolicyFor, main, probeUpstreams, proxyEntry, readRegistration, readServers, registrationPath, renderStarterPolicy, restoreClientConfig, rewriteClientConfig, runDemoAgent, runIdFor, runPermissionDiff, seedState, serversToUpstreams, startHttpProxy, toCallToolResult };
package/dist/index.js CHANGED
@@ -806,6 +806,21 @@ var require_dist = __commonJS({
806
806
 
807
807
  // src/proxy/runtime.ts
808
808
  import { Client } from "@modelcontextprotocol/sdk/client/index.js";
809
+
810
+ // src/version.ts
811
+ import { readFileSync } from "fs";
812
+ var cached;
813
+ function version() {
814
+ if (cached !== void 0) return cached;
815
+ try {
816
+ cached = JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
817
+ } catch {
818
+ cached = "0.0.0";
819
+ }
820
+ return cached;
821
+ }
822
+
823
+ // src/proxy/runtime.ts
809
824
  import {
810
825
  getDefaultEnvironment,
811
826
  StdioClientTransport
@@ -886,7 +901,7 @@ var ProxyRuntime = class {
886
901
  }
887
902
  async connect(upstream) {
888
903
  const client = new Client(
889
- { name: "agentguard", version: "0.1.0" },
904
+ { name: "agentguard", version: version() },
890
905
  { capabilities: { sampling: {}, elicitation: {} } }
891
906
  );
892
907
  const conn = {
@@ -1331,7 +1346,7 @@ import {
1331
1346
  keyFromHeaders,
1332
1347
  resolveAgent
1333
1348
  } from "@agentwares/agentguard-core";
1334
- var PROXY_VERSION = "0.1.0";
1349
+ var PROXY_VERSION = version();
1335
1350
  function headerValue(extra, name) {
1336
1351
  const raw = extra.requestInfo?.headers?.[name] ?? extra.requestInfo?.headers?.[name.toLowerCase()];
1337
1352
  if (Array.isArray(raw)) return raw[0];
@@ -1559,7 +1574,7 @@ function createDownstreamServer(runtime, opts = {}) {
1559
1574
  return server;
1560
1575
  }
1561
1576
 
1562
- // ../../packages/mcp-kit/dist/chunk-AO7AV7MI.js
1577
+ // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
1563
1578
  import { completable } from "@modelcontextprotocol/sdk/server/completable.js";
1564
1579
  import { ResourceTemplate } from "@modelcontextprotocol/sdk/server/mcp.js";
1565
1580
  import {
@@ -1571,6 +1586,19 @@ import {
1571
1586
  import { z } from "zod";
1572
1587
 
1573
1588
  // ../../packages/x402/dist/index.js
1589
+ var USDC_BASE = "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913";
1590
+ var USDC_DECIMALS = 6;
1591
+ var USDC_EIP712_DOMAIN = { name: "USD Coin", version: "2" };
1592
+ function toAtomicUnits(amountUsd, decimals = USDC_DECIMALS) {
1593
+ const trimmed = amountUsd.trim();
1594
+ if (!/^\d+(\.\d+)?$/.test(trimmed)) throw new Error(`not a decimal amount: ${amountUsd}`);
1595
+ const [whole = "0", fraction = ""] = trimmed.split(".");
1596
+ if (fraction.length > decimals) {
1597
+ throw new Error(`${amountUsd} has more than ${decimals} decimal places`);
1598
+ }
1599
+ return `${whole}${fraction.padEnd(decimals, "0")}`.replace(/^0+(?=\d)/, "");
1600
+ }
1601
+ var DEFAULT_X402_NETWORK = "eip155:8453";
1574
1602
  var X402_HEADER = "X-Payment-Requirements";
1575
1603
  var X402_REQUIRED_HEADER = "PAYMENT-REQUIRED";
1576
1604
  function toAmount(priceUsd) {
@@ -1580,42 +1608,69 @@ function toAmount(priceUsd) {
1580
1608
  }
1581
1609
  function buildPaymentRequired(opts) {
1582
1610
  const amountUsd = toAmount(opts.priceUsd);
1583
- const accepts = [
1584
- {
1611
+ const creditsPayTo = opts.creditsPayTo ?? opts.selfServe?.keysUrl;
1612
+ const accepts = [];
1613
+ if (opts.payTo) {
1614
+ accepts.push({
1585
1615
  rail: "x402",
1616
+ scheme: "exact",
1586
1617
  amountUsd,
1618
+ amount: toAtomicUnits(amountUsd, opts.assetDecimals),
1587
1619
  resource: opts.resource,
1588
1620
  description: opts.description,
1589
- network: "eip155:8453",
1590
- asset: "USDC",
1621
+ mimeType: "application/json",
1622
+ network: opts.network ?? DEFAULT_X402_NETWORK,
1623
+ asset: opts.asset ?? USDC_BASE,
1624
+ extra: opts.assetDomain ?? USDC_EIP712_DOMAIN,
1591
1625
  payTo: opts.payTo,
1592
- maxTimeoutSeconds: 300
1593
- },
1594
- {
1595
- rail: "mpp",
1626
+ maxTimeoutSeconds: 300,
1627
+ ...opts.bazaar ? { bazaar: opts.bazaar } : {}
1628
+ });
1629
+ }
1630
+ if (creditsPayTo) {
1631
+ accepts.push({
1632
+ rail: "prepaid_credits",
1596
1633
  amountUsd,
1597
1634
  resource: opts.resource,
1598
1635
  description: opts.description,
1599
1636
  currency: "usd",
1600
- payTo: opts.payTo,
1601
- maxTimeoutSeconds: 300
1602
- }
1603
- ];
1637
+ payTo: creditsPayTo,
1638
+ maxTimeoutSeconds: 300,
1639
+ // funding a balance ends at a hosted Stripe Checkout page, which an agent cannot fill in
1640
+ humanRequired: true
1641
+ });
1642
+ }
1604
1643
  const live = opts.live === true;
1644
+ const selfServe = opts.selfServe;
1645
+ const x402Rail = accepts.find((a) => a.rail === "x402" && a.payTo);
1646
+ const usdcFix = x402Rail ? `Pay in USDC with no account and no human: take the \`accepts\` entry with \`rail: "x402"\`, sign an EIP-3009 transferWithAuthorization for ${x402Rail.amountUsd} USDC to ${x402Rail.payTo} on ${x402Rail.network}, and retry with it base64 in the X-PAYMENT header. The facilitator submits the transfer, so you need USDC and no gas. ` : "";
1647
+ const sampleFix = opts.sampleMode ? "Free first: retry with sample=true \u2014 no key, no payment. " : "";
1648
+ const cardFix = selfServe ? `Or pay by card: POST ${selfServe.keysUrl} (no auth, no signup) returns a key and a Stripe Checkout URL. Funding that key needs a person at a browser; if you are running unattended, surface the URL to your operator rather than retrying, or use the USDC rail above. Once funded, send Authorization: Bearer <key>, plus Idempotency-Key so a repeated call is charged once.` : "";
1649
+ const liveFix = usdcFix || cardFix ? `${sampleFix}${usdcFix}${cardFix}`.trim() : "Retry with a PAYMENT header satisfying one of `accepts`, or send an API key with credit balance.";
1605
1650
  const body = {
1606
1651
  code: "PAYMENT_REQUIRED",
1607
1652
  cause: live ? `This call costs $${amountUsd}. No valid payment or credit was presented.` : `This call costs $${amountUsd}. Pay-per-call (x402 / Stripe MPP) is coming soon; use an API key with prepaid credits.`,
1608
- fix: live ? "Retry with a PAYMENT header satisfying one of `accepts`, or send an API key with credit balance." : `Create an API key${opts.signupUrl ? ` at ${opts.signupUrl}` : ""} and retry with Authorization: Bearer <key>. Try the free sample mode first.`,
1653
+ fix: live ? liveFix : `Create an API key${opts.signupUrl ? ` at ${opts.signupUrl}` : ""} and retry with Authorization: Bearer <key>. Try the free sample mode first.`,
1609
1654
  retryable: true,
1610
1655
  priceUsd: opts.priceUsd,
1611
1656
  accepts,
1612
1657
  comingSoon: !live,
1613
- alternatives: ["api_key_credits", "sample_mode"]
1658
+ alternatives: [
1659
+ ...creditsPayTo ? ["api_key_credits"] : [],
1660
+ ...opts.sampleMode ? ["sample_mode"] : []
1661
+ ],
1662
+ ...selfServe ? { selfServe } : {},
1663
+ ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
1614
1664
  };
1615
- const encoded = JSON.stringify({ x402Version: 2, accepts, resource: opts.resource });
1665
+ const encoded = JSON.stringify({
1666
+ x402Version: 2,
1667
+ accepts,
1668
+ resource: opts.resource,
1669
+ ...opts.bazaar ? { extensions: { bazaar: opts.bazaar } } : {}
1670
+ });
1616
1671
  const headers = {
1617
1672
  "Content-Type": "application/json",
1618
- "WWW-Authenticate": `Payment realm="agentwares", methods="x402 mpp", price="${amountUsd} USD"`,
1673
+ "WWW-Authenticate": `Payment realm="agentwares", methods="x402 prepaid_credits", price="${amountUsd} USD"`,
1619
1674
  [X402_HEADER]: encoded,
1620
1675
  [X402_REQUIRED_HEADER]: btoa(encoded),
1621
1676
  "Cache-Control": "no-store"
@@ -1633,7 +1688,7 @@ var StubVerifier = class {
1633
1688
  }
1634
1689
  };
1635
1690
 
1636
- // ../../packages/mcp-kit/dist/chunk-AO7AV7MI.js
1691
+ // ../../packages/mcp-kit/dist/chunk-7LRFDQNC.js
1637
1692
  import { z as z2 } from "zod";
1638
1693
  import { z as z3 } from "zod";
1639
1694
  import { McpServer as McpServer2 } from "@modelcontextprotocol/sdk/server/mcp.js";
@@ -1815,13 +1870,13 @@ function defineTool(opts) {
1815
1870
  annotations,
1816
1871
  handler,
1817
1872
  async invoke(rawInput, ctx) {
1818
- const fail = (err) => {
1873
+ const fail2 = (err) => {
1819
1874
  const result = errorResult(err, { requestId: ctx.requestId });
1820
1875
  if (output) delete result.structuredContent;
1821
1876
  return result;
1822
1877
  };
1823
1878
  const parsed = input.safeParse(rawInput ?? {});
1824
- if (!parsed.success) return fail(invalidInputError(parsed.error.issues));
1879
+ if (!parsed.success) return fail2(invalidInputError(parsed.error.issues));
1825
1880
  try {
1826
1881
  const value = await handler(parsed.data, ctx);
1827
1882
  if (output && !isCallToolResult(value)) {
@@ -1839,7 +1894,7 @@ function defineTool(opts) {
1839
1894
  }
1840
1895
  return toCallToolResult2(value);
1841
1896
  } catch (err) {
1842
- return fail(err);
1897
+ return fail2(err);
1843
1898
  }
1844
1899
  }
1845
1900
  };
@@ -2080,6 +2135,7 @@ function withPayment(tool, opts) {
2080
2135
  );
2081
2136
  }
2082
2137
  const verifier = opts.verifier ?? new StubVerifier();
2138
+ const liveRail = opts.verifier !== void 0;
2083
2139
  const input = tool.input.extend({
2084
2140
  [sampleField]: z2.boolean().default(false).describe(
2085
2141
  "Set true to return an example response at no charge (sample mode). Default false."
@@ -2104,23 +2160,46 @@ function withPayment(tool, opts) {
2104
2160
  if (auth.ok) return tool.handler(record, { ...ctx, userId: auth.userId });
2105
2161
  reason = auth.reason;
2106
2162
  }
2163
+ const resource = ctx.resourceUrl ?? tool.name;
2107
2164
  const required = buildPaymentRequired({
2108
- resource: tool.name,
2165
+ resource,
2109
2166
  priceUsd: opts.priceUsd,
2110
2167
  live: opts.live,
2111
2168
  description: opts.description,
2112
2169
  payTo: opts.payTo,
2113
- signupUrl: opts.signupUrl
2170
+ creditsPayTo: opts.creditsPayTo,
2171
+ signupUrl: opts.signupUrl,
2172
+ selfServe: opts.selfServe,
2173
+ // withPayment always adds a free `sample` flag to the tool's input schema
2174
+ sampleMode: true,
2175
+ ...opts.bazaar ? { bazaar: opts.bazaar } : {}
2114
2176
  });
2115
- const requirement = required.body.accepts[0];
2116
- if (requirement) {
2117
- const verified = await verifier.verify(
2118
- new Request(`https://mcp.local/${tool.name}`, { headers: ctx.headers }),
2119
- requirement
2120
- );
2121
- if (verified.ok) return tool.handler(record, ctx);
2122
- reason = reason ?? verified.reason;
2177
+ const advertised = required.body.accepts.find((a) => a.rail === "prepaid_credits") ?? required.body.accepts[0];
2178
+ const requirement = advertised ?? {
2179
+ rail: "prepaid_credits",
2180
+ amountUsd: String(required.body.priceUsd),
2181
+ resource,
2182
+ ...opts.description ? { description: opts.description } : {}
2183
+ };
2184
+ const verified = await verifier.verify(
2185
+ new Request(`https://mcp.local/${tool.name}`, { headers: ctx.headers }),
2186
+ requirement
2187
+ );
2188
+ if (verified.ok) {
2189
+ let result;
2190
+ try {
2191
+ result = await tool.handler(record, ctx);
2192
+ } catch (error) {
2193
+ await opts.onPaidFailure?.(ctx, verified, error);
2194
+ throw error;
2195
+ }
2196
+ const settled = await verifier.settle?.(verified);
2197
+ if (settled && !settled.ok) {
2198
+ opts.onSettlementFailure?.(ctx, verified, settled.reason);
2199
+ }
2200
+ return result;
2123
2201
  }
2202
+ reason = liveRail ? verified.reason ?? reason : reason ?? verified.reason;
2124
2203
  throw paymentRequiredError(required.body, reason);
2125
2204
  }
2126
2205
  });
@@ -2809,7 +2888,7 @@ import {
2809
2888
  FileStateStore,
2810
2889
  loadPolicyFile
2811
2890
  } from "@agentwares/agentguard-core/node";
2812
- import { existsSync as exists, readFileSync } from "fs";
2891
+ import { existsSync as exists, readFileSync as readFileSync2 } from "fs";
2813
2892
 
2814
2893
  // ../../node_modules/.pnpm/postal-mime@2.7.5/node_modules/postal-mime/src/decode-strings.js
2815
2894
  var textEncoder = new TextEncoder();
@@ -2842,8 +2921,8 @@ var MAX_HEADERS_SIZE = 2 * 1024 * 1024;
2842
2921
 
2843
2922
  // ../../node_modules/.pnpm/resend@6.25.0/node_modules/resend/dist/index.mjs
2844
2923
  var import_standardwebhooks = __toESM(require_dist(), 1);
2845
- var version = "6.25.0";
2846
- var defaultUserAgent = `resend-node:${version}`;
2924
+ var version2 = "6.25.0";
2925
+ var defaultUserAgent = `resend-node:${version2}`;
2847
2926
 
2848
2927
  // ../../packages/notify/dist/index.js
2849
2928
  var palette = {
@@ -3193,7 +3272,7 @@ function readRegistration(loaded) {
3193
3272
  const file = registrationPath(loaded.stateDir);
3194
3273
  if (!exists(file)) return void 0;
3195
3274
  try {
3196
- const reg = JSON.parse(readFileSync(file, "utf8"));
3275
+ const reg = JSON.parse(readFileSync2(file, "utf8"));
3197
3276
  return reg;
3198
3277
  } catch {
3199
3278
  return void 0;
@@ -3209,7 +3288,7 @@ function describeError(err) {
3209
3288
  }
3210
3289
 
3211
3290
  // src/configs.ts
3212
- import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "fs";
3291
+ import { copyFileSync, existsSync as existsSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync2 } from "fs";
3213
3292
  import { homedir as homedir2, platform } from "os";
3214
3293
  import { join as join2, resolve as resolve2 } from "path";
3215
3294
  function candidateConfigs(cwd = process.cwd(), home = homedir2(), env = process.env) {
@@ -3272,7 +3351,7 @@ function describeConfigPath(path2) {
3272
3351
  if (known) return known;
3273
3352
  let serversKey = "mcpServers";
3274
3353
  try {
3275
- const parsed = JSON.parse(readFileSync2(abs, "utf8"));
3354
+ const parsed = JSON.parse(readFileSync3(abs, "utf8"));
3276
3355
  if (parsed.servers && !parsed.mcpServers) serversKey = "servers";
3277
3356
  } catch {
3278
3357
  }
@@ -3284,12 +3363,13 @@ function describeConfigPath(path2) {
3284
3363
  };
3285
3364
  }
3286
3365
  function readServers(file) {
3287
- const raw = JSON.parse(readFileSync2(file.path, "utf8"));
3366
+ const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3288
3367
  const servers = raw[file.serversKey];
3289
3368
  if (!servers || typeof servers !== "object") return {};
3290
3369
  return servers;
3291
3370
  }
3292
3371
  var PROXY_SERVER_NAME = "agentguard";
3372
+ var PROXY_PACKAGE = "@agentwares/agentguard";
3293
3373
  function isProxyEntry(entry) {
3294
3374
  return (entry.args ?? []).some((a) => a === "agentguard" || /(^|\/)agentguard(\.js)?$/.test(a)) && (entry.args ?? []).includes("proxy");
3295
3375
  }
@@ -3333,14 +3413,14 @@ function backupPath(file) {
3333
3413
  return `${file.path}.agentguard-backup`;
3334
3414
  }
3335
3415
  function proxyEntry(file, policyPath, opts = {}) {
3336
- const args = ["-y", "agentguard", "proxy", "--config", policyPath];
3416
+ const args = ["-y", PROXY_PACKAGE, "proxy", "--config", policyPath];
3337
3417
  if (opts.agent) args.push("--agent", opts.agent);
3338
3418
  const entry = { command: "npx", args };
3339
3419
  if (file.serversKey === "servers") entry.type = "stdio";
3340
3420
  return entry;
3341
3421
  }
3342
3422
  function rewriteClientConfig(file, policyPath, opts = {}) {
3343
- const raw = JSON.parse(readFileSync2(file.path, "utf8"));
3423
+ const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3344
3424
  const servers = raw[file.serversKey] ?? {};
3345
3425
  const replaced = Object.keys(servers).filter((n) => !isProxyEntry(servers[n]));
3346
3426
  const backup = backupPath(file);
@@ -3355,6 +3435,16 @@ function restoreClientConfig(file) {
3355
3435
  copyFileSync(backup, file.path);
3356
3436
  return true;
3357
3437
  }
3438
+ function mergeServerIntoConfig(file, name, entry) {
3439
+ const raw = JSON.parse(readFileSync3(file.path, "utf8"));
3440
+ const servers = raw[file.serversKey] ?? {};
3441
+ const replaced = name in servers;
3442
+ const backup = backupPath(file);
3443
+ if (!existsSync3(backup)) copyFileSync(file.path, backup);
3444
+ raw[file.serversKey] = { ...servers, [name]: entry };
3445
+ writeFileSync2(file.path, JSON.stringify(raw, null, 2) + "\n");
3446
+ return { backup, replaced };
3447
+ }
3358
3448
 
3359
3449
  // src/commands/init.ts
3360
3450
  import { existsSync as existsSync4, writeFileSync as writeFileSync3 } from "fs";
@@ -3671,6 +3761,11 @@ async function initCommand(args, io) {
3671
3761
  }
3672
3762
  if (flagBool(args, "rewrite") !== false) {
3673
3763
  for (const c of chosen) {
3764
+ if (resolve3(c.path) === resolve3(policyPath)) {
3765
+ throw new Error(
3766
+ `refusing: ${rel(c.path)} is both the policy file (--config) and a client config. Rewriting it would replace the policy with the proxy entry and leave the proxy pointing at itself. Use --config for the policy path and --client for the client's config, or let --config default to agentguard.yaml.`
3767
+ );
3768
+ }
3674
3769
  const { backup, replaced } = rewriteClientConfig(c, policyPath, {
3675
3770
  agent: flagString(args, "agent")
3676
3771
  });
@@ -4083,8 +4178,111 @@ ${steps.length} calls; halts: ${halts.join(", ") || "none"}. Now run: agentguard
4083
4178
  }
4084
4179
  }
4085
4180
 
4086
- // src/cli.ts
4087
- import { readFileSync as readFileSync4 } from "fs";
4181
+ // src/commands/connect.ts
4182
+ import { existsSync as existsSync5 } from "fs";
4183
+ import { relative as relative2 } from "path";
4184
+ var DEFAULT_PROXY_URL = "https://agentwares-agentguard-proxy.vercel.app";
4185
+ function fail(e) {
4186
+ throw e;
4187
+ }
4188
+ async function connectCommand(args, io, fetchImpl = fetch) {
4189
+ const key = args.positionals[0] ?? io.env.AGENTGUARD_KEY;
4190
+ if (!key) {
4191
+ io.err("usage: agentguard connect <key> [--write] [--client <path>] [--url <proxy-url>]");
4192
+ io.err(" the key is on your dashboard; it starts with agk_");
4193
+ return 2;
4194
+ }
4195
+ const base = (flagString(args, "url") ?? io.env.AGENTGUARD_PROXY_URL ?? DEFAULT_PROXY_URL).replace(/\/+$/, "");
4196
+ let res;
4197
+ try {
4198
+ res = await fetchImpl(`${base}/connect`, {
4199
+ method: "POST",
4200
+ headers: { "content-type": "application/json" },
4201
+ body: JSON.stringify({ key })
4202
+ });
4203
+ } catch (err) {
4204
+ fail({
4205
+ code: "PROXY_UNREACHABLE",
4206
+ cause: `could not reach ${base}: ${err instanceof Error ? err.message : String(err)}`,
4207
+ fix: "check the network, or pass --url if you run your own proxy",
4208
+ retryable: true
4209
+ });
4210
+ }
4211
+ const text = await res.text();
4212
+ let body;
4213
+ try {
4214
+ body = JSON.parse(text);
4215
+ } catch {
4216
+ fail({
4217
+ code: "PROXY_BAD_RESPONSE",
4218
+ cause: `${base}/connect returned ${res.status} that was not JSON: ${text.slice(0, 200)}`,
4219
+ fix: "check --url points at an agentguard proxy, not at the dashboard",
4220
+ retryable: false
4221
+ });
4222
+ }
4223
+ if (!res.ok) {
4224
+ const e = body;
4225
+ fail({
4226
+ code: e.code ?? "CONNECT_REFUSED",
4227
+ cause: e.cause ?? `${base}/connect returned ${res.status}`,
4228
+ fix: e.fix ?? "check the key on your dashboard; revoked keys stop working immediately",
4229
+ retryable: e.retryable ?? false
4230
+ });
4231
+ }
4232
+ const conn = body;
4233
+ if (flagBool(args, "json")) {
4234
+ io.out(JSON.stringify(conn, null, 2));
4235
+ return 0;
4236
+ }
4237
+ const rel = (p) => p.startsWith(io.cwd + "/") ? relative2(io.cwd, p) : p;
4238
+ const entry = conn.mcpServers[PROXY_SERVER_NAME] ?? {
4239
+ type: "http",
4240
+ url: conn.mcpUrl,
4241
+ headers: conn.headers
4242
+ };
4243
+ if (!flagBool(args, "write")) {
4244
+ io.out(JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: entry } }, null, 2));
4245
+ } else {
4246
+ const explicit = flagString(args, "client");
4247
+ const configs = explicit ? [describeConfigPath(explicit)] : detectClientConfigs(io.cwd, homeFrom(io), io.env);
4248
+ const projectLevel = configs.filter((c) => c.path.startsWith(io.cwd + "/"));
4249
+ let chosen;
4250
+ if (explicit || flagBool(args, "all")) chosen = configs;
4251
+ else chosen = projectLevel.slice(0, 1);
4252
+ if (chosen.length === 0) {
4253
+ io.err(
4254
+ "no MCP config found here (.mcp.json, .cursor/mcp.json, .vscode/mcp.json). Create one, or paste this into your client:"
4255
+ );
4256
+ io.out(JSON.stringify({ mcpServers: { [PROXY_SERVER_NAME]: entry } }, null, 2));
4257
+ return 1;
4258
+ }
4259
+ for (const c of chosen) {
4260
+ if (!existsSync5(c.path)) {
4261
+ io.err(`no MCP config at ${c.path}`);
4262
+ return 1;
4263
+ }
4264
+ const { backup, replaced } = mergeServerIntoConfig(c, PROXY_SERVER_NAME, entry);
4265
+ io.out(
4266
+ `${replaced ? "updated" : "wrote "} ${rel(c.path)} (${c.label}) \u2192 ${PROXY_SERVER_NAME} = ${conn.mcpUrl}`
4267
+ );
4268
+ io.out(`backup ${rel(backup)}`);
4269
+ }
4270
+ for (const c of configs.filter((c2) => !chosen.includes(c2)))
4271
+ io.out(
4272
+ `found ${c.path} (${c.label}, user-level) \u2014 not touched; pass --client ${JSON.stringify(c.path)} to write it too`
4273
+ );
4274
+ }
4275
+ const band = conn.band ? ` band=${conn.band.tier} (${conn.band.quantity} calls/mo)` : "";
4276
+ io.out(`
4277
+ proxy ${conn.slug} (${conn.proxyId}) mode=${conn.mode}${band}`);
4278
+ if (conn.upstreams?.length) io.out(`upstreams: ${conn.upstreams.join(", ")}`);
4279
+ if (conn.dashboardUrl) io.out(`dashboard: ${conn.dashboardUrl}`);
4280
+ if (conn.claudeCode) io.out(`claude code: ${conn.claudeCode}`);
4281
+ if (!flagBool(args, "write"))
4282
+ io.out(`
4283
+ run again with --write to add this to your MCP client config`);
4284
+ return 0;
4285
+ }
4088
4286
 
4089
4287
  // src/commands/control.ts
4090
4288
  async function pokeHttp(io, reg, path2, body) {
@@ -4178,13 +4376,13 @@ async function approvalsCommand(args, io) {
4178
4376
  }
4179
4377
 
4180
4378
  // src/commands/keys.ts
4181
- import { readFileSync as readFileSync3, writeFileSync as writeFileSync4 } from "fs";
4379
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
4182
4380
  import YAML2, { isMap, isSeq, YAMLSeq } from "yaml";
4183
4381
  import { generateAgentKey, hashAgentKey } from "@agentwares/agentguard-core";
4184
4382
  async function keyCommand(args, io) {
4185
4383
  const [sub, name] = args.positionals;
4186
4384
  const path2 = policyPathFrom(args, io);
4187
- const doc = YAML2.parseDocument(readFileSync3(path2, "utf8"));
4385
+ const doc = YAML2.parseDocument(readFileSync4(path2, "utf8"));
4188
4386
  let agents = doc.get("agents");
4189
4387
  if (!isSeq(agents)) {
4190
4388
  agents = new YAMLSeq();
@@ -4461,19 +4659,13 @@ async function statusCommand(args, io) {
4461
4659
  }
4462
4660
 
4463
4661
  // src/cli.ts
4464
- function version2() {
4465
- try {
4466
- return JSON.parse(readFileSync4(new URL("../package.json", import.meta.url), "utf8")).version;
4467
- } catch {
4468
- return "0.0.0";
4469
- }
4470
- }
4471
4662
  var HELP = `agentguard \u2014 MCP policy proxy for agents that touch production
4472
4663
 
4473
4664
  npx @agentwares/agentguard init read your MCP config, write agentguard.yaml (dry-run), route every server through the proxy
4474
4665
  agentguard report [--run id|--all] what this run did / would have destroyed / spent, where it was halted
4475
4666
  agentguard diff [--run id] record-by-record mutation diff from a dry run
4476
4667
  agentguard verify [audit.jsonl] prove the hash-chained audit log was not edited
4668
+ agentguard connect <key> [--write] point this machine at your hosted proxy (paid tiers)
4477
4669
 
4478
4670
  agentguard proxy [--http --port 8788] [--agent name] [--run-id id] [--mode dry-run|enforce]
4479
4671
  agentguard tools [--json] the tools your agent will see, with class and why
@@ -4490,7 +4682,7 @@ Docs: https://github.com/agentwares/agentguard/tree/main/apps/agentguard-cli`;
4490
4682
  async function main(argv, io = defaultIo()) {
4491
4683
  const args = parseArgs(argv);
4492
4684
  if (args.flags.version) {
4493
- io.out(version2());
4685
+ io.out(version());
4494
4686
  return 0;
4495
4687
  }
4496
4688
  if (args.command === void 0 && !args.flags.help && io.tty === false) {
@@ -4518,6 +4710,8 @@ async function main(argv, io = defaultIo()) {
4518
4710
  return await diffCommand(args, io);
4519
4711
  case "verify":
4520
4712
  return await verifyCommand(args, io);
4713
+ case "connect":
4714
+ return await connectCommand(args, io);
4521
4715
  case "status":
4522
4716
  return await statusCommand(args, io);
4523
4717
  case "tools":