@cruxy/cli 0.20.0 → 0.22.0

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.
Files changed (63) hide show
  1. package/dist/approval/classify.js +24 -0
  2. package/dist/approval/policy.js +7 -0
  3. package/dist/approval/prompt.js +7 -0
  4. package/dist/approval/types.d.ts +6 -0
  5. package/dist/brand/voice.d.ts +1 -1
  6. package/dist/brand/voice.js +1 -1
  7. package/dist/cli/commands/mcp.d.ts +9 -0
  8. package/dist/cli/commands/mcp.js +87 -0
  9. package/dist/cli/commands/run.js +22 -5
  10. package/dist/cli/program.js +2 -0
  11. package/dist/cli/session-factory.d.ts +2 -2
  12. package/dist/cli/session-factory.js +20 -2
  13. package/dist/config/schema.d.ts +362 -38
  14. package/dist/config/schema.js +100 -5
  15. package/dist/constants.d.ts +8 -0
  16. package/dist/constants.js +8 -0
  17. package/dist/errors/constructors.d.ts +46 -0
  18. package/dist/errors/constructors.js +123 -0
  19. package/dist/errors/types.d.ts +26 -0
  20. package/dist/errors/types.js +41 -0
  21. package/dist/lsp/transport.d.ts +6 -15
  22. package/dist/lsp/transport.js +10 -66
  23. package/dist/mcp/adapter.d.ts +44 -0
  24. package/dist/mcp/adapter.js +70 -0
  25. package/dist/mcp/bounds.d.ts +35 -0
  26. package/dist/mcp/bounds.js +36 -0
  27. package/dist/mcp/client.d.ts +19 -0
  28. package/dist/mcp/client.js +93 -0
  29. package/dist/mcp/demarcate.d.ts +12 -0
  30. package/dist/mcp/demarcate.js +71 -0
  31. package/dist/mcp/index.d.ts +9 -0
  32. package/dist/mcp/index.js +8 -0
  33. package/dist/mcp/service.d.ts +54 -0
  34. package/dist/mcp/service.js +99 -0
  35. package/dist/mcp/transport.d.ts +30 -0
  36. package/dist/mcp/transport.js +188 -0
  37. package/dist/mcp/trust-gate.d.ts +35 -0
  38. package/dist/mcp/trust-gate.js +40 -0
  39. package/dist/mcp/trust.d.ts +52 -0
  40. package/dist/mcp/trust.js +111 -0
  41. package/dist/mcp/types.d.ts +52 -0
  42. package/dist/mcp/types.js +7 -0
  43. package/dist/tools/registry.js +3 -1
  44. package/dist/tools/types.d.ts +15 -1
  45. package/dist/utils/child-tree.d.ts +35 -0
  46. package/dist/utils/child-tree.js +76 -0
  47. package/dist/web/demarcate.d.ts +13 -0
  48. package/dist/web/demarcate.js +78 -0
  49. package/dist/web/fetch.d.ts +11 -0
  50. package/dist/web/fetch.js +174 -0
  51. package/dist/web/index.d.ts +7 -0
  52. package/dist/web/index.js +7 -0
  53. package/dist/web/provider.d.ts +29 -0
  54. package/dist/web/provider.js +77 -0
  55. package/dist/web/search.d.ts +17 -0
  56. package/dist/web/search.js +42 -0
  57. package/dist/web/ssrf.d.ts +55 -0
  58. package/dist/web/ssrf.js +223 -0
  59. package/dist/web/tools.d.ts +20 -0
  60. package/dist/web/tools.js +81 -0
  61. package/dist/web/types.d.ts +62 -0
  62. package/dist/web/types.js +1 -0
  63. package/package.json +2 -1
@@ -44,15 +44,12 @@ export declare const AgentConfigSchema: z.ZodObject<{
44
44
  export declare const ToolsConfigSchema: z.ZodObject<{
45
45
  fileEdit: z.ZodDefault<z.ZodBoolean>;
46
46
  shell: z.ZodDefault<z.ZodBoolean>;
47
- webSearch: z.ZodDefault<z.ZodBoolean>;
48
47
  }, "strict", z.ZodTypeAny, {
49
48
  fileEdit: boolean;
50
49
  shell: boolean;
51
- webSearch: boolean;
52
50
  }, {
53
51
  fileEdit?: boolean | undefined;
54
52
  shell?: boolean | undefined;
55
- webSearch?: boolean | undefined;
56
53
  }>;
57
54
  export declare const GitConfigSchema: z.ZodObject<{
58
55
  autoCommit: z.ZodDefault<z.ZodBoolean>;
@@ -446,16 +443,16 @@ export declare const LspConfigSchema: z.ZodObject<{
446
443
  }, "strict", z.ZodTypeAny, {
447
444
  startupTimeout: number;
448
445
  requestTimeout: number;
449
- enabled: boolean;
450
446
  servers: Record<string, string>;
447
+ enabled: boolean;
451
448
  maxServers: number;
452
449
  idleTimeout: number;
453
450
  maxResults: number;
454
451
  }, {
455
452
  startupTimeout?: number | undefined;
456
453
  requestTimeout?: number | undefined;
457
- enabled?: boolean | undefined;
458
454
  servers?: Record<string, string> | undefined;
455
+ enabled?: boolean | undefined;
459
456
  maxServers?: number | undefined;
460
457
  idleTimeout?: number | undefined;
461
458
  maxResults?: number | undefined;
@@ -593,20 +590,198 @@ export declare const UsageConfigSchema: z.ZodObject<{
593
590
  } | undefined;
594
591
  }>;
595
592
  export type UsageConfig = z.infer<typeof UsageConfigSchema>;
596
- /** MCP server entry — stdio or URL transport (wired up in a later phase). */
597
- export declare const McpServerSchema: z.ZodObject<{
593
+ /**
594
+ * One MCP server entry (C.27). A `command` (+ optional `args`/`env`) is a stdio
595
+ * server cruxy spawns as a child process; a `url` names a remote server. Exactly
596
+ * one transport must be given. Trusting a stdio server runs its code UNSANDBOXED
597
+ * with your privileges, which is why connection is gated by an explicit,
598
+ * fingerprinted trust decision (see `mcp/trust.ts`).
599
+ */
600
+ export declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
601
+ /** stdio transport: the server program to spawn. */
598
602
  command: z.ZodOptional<z.ZodString>;
599
- args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
603
+ /** Arguments for `command`. */
604
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
605
+ /** Extra environment variables for the spawned server (stdio only). */
606
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
607
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
600
608
  url: z.ZodOptional<z.ZodString>;
601
609
  }, "strict", z.ZodTypeAny, {
610
+ args: string[];
611
+ env: Record<string, string>;
612
+ command?: string | undefined;
613
+ url?: string | undefined;
614
+ }, {
602
615
  command?: string | undefined;
603
616
  url?: string | undefined;
604
617
  args?: string[] | undefined;
618
+ env?: Record<string, string> | undefined;
619
+ }>, {
620
+ args: string[];
621
+ env: Record<string, string>;
622
+ command?: string | undefined;
623
+ url?: string | undefined;
605
624
  }, {
606
625
  command?: string | undefined;
607
626
  url?: string | undefined;
608
627
  args?: string[] | undefined;
628
+ env?: Record<string, string> | undefined;
609
629
  }>;
630
+ export type McpServerConfig = z.infer<typeof McpServerSchema>;
631
+ /**
632
+ * MCP client integration (C.27): connect to trusted MCP servers and expose their
633
+ * tools to the agent. OFF by default — like the sandbox, LSP, and hooks, it runs
634
+ * EXTERNAL code (a stdio server executes UNSANDBOXED with your privileges), a
635
+ * real execution surface you opt into explicitly. When off, nothing connects or
636
+ * spawns and no MCP tool is ever registered. Every server-advertised tool is
637
+ * gated (destructive tier — a server can never self-declare a tool "safe"), its
638
+ * description and results are demarcated as untrusted external data with upstream
639
+ * model names scrubbed, and results are NEVER persisted. The tool list a server
640
+ * advertises is bounded (count + per-tool description/schema size) so a hostile
641
+ * server can't blow the context budget.
642
+ */
643
+ export declare const McpConfigSchema: z.ZodObject<{
644
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
645
+ * is registered (the feature stays fully inert). */
646
+ enabled: z.ZodDefault<z.ZodBoolean>;
647
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
648
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
649
+ servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
650
+ /** stdio transport: the server program to spawn. */
651
+ command: z.ZodOptional<z.ZodString>;
652
+ /** Arguments for `command`. */
653
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
654
+ /** Extra environment variables for the spawned server (stdio only). */
655
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
656
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
657
+ url: z.ZodOptional<z.ZodString>;
658
+ }, "strict", z.ZodTypeAny, {
659
+ args: string[];
660
+ env: Record<string, string>;
661
+ command?: string | undefined;
662
+ url?: string | undefined;
663
+ }, {
664
+ command?: string | undefined;
665
+ url?: string | undefined;
666
+ args?: string[] | undefined;
667
+ env?: Record<string, string> | undefined;
668
+ }>, {
669
+ args: string[];
670
+ env: Record<string, string>;
671
+ command?: string | undefined;
672
+ url?: string | undefined;
673
+ }, {
674
+ command?: string | undefined;
675
+ url?: string | undefined;
676
+ args?: string[] | undefined;
677
+ env?: Record<string, string> | undefined;
678
+ }>>>;
679
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
680
+ * not complete within this many ms. */
681
+ startupTimeout: z.ZodDefault<z.ZodNumber>;
682
+ /** Fail a single `tools/call` if the server does not respond within this many
683
+ * ms — the connection is kept, only the one call errors. */
684
+ requestTimeout: z.ZodDefault<z.ZodNumber>;
685
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
686
+ * (a hostile server can't advertise thousands of tools to flood context). */
687
+ maxToolsPerServer: z.ZodDefault<z.ZodNumber>;
688
+ /** Max characters kept from a single tool's description; the rest is truncated
689
+ * with a visible marker. */
690
+ maxDescriptionChars: z.ZodDefault<z.ZodNumber>;
691
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
692
+ * over-cap schema is replaced with a permissive one and a visible note. */
693
+ maxSchemaBytes: z.ZodDefault<z.ZodNumber>;
694
+ }, "strict", z.ZodTypeAny, {
695
+ startupTimeout: number;
696
+ requestTimeout: number;
697
+ servers: Record<string, {
698
+ args: string[];
699
+ env: Record<string, string>;
700
+ command?: string | undefined;
701
+ url?: string | undefined;
702
+ }>;
703
+ enabled: boolean;
704
+ maxToolsPerServer: number;
705
+ maxDescriptionChars: number;
706
+ maxSchemaBytes: number;
707
+ }, {
708
+ startupTimeout?: number | undefined;
709
+ requestTimeout?: number | undefined;
710
+ servers?: Record<string, {
711
+ command?: string | undefined;
712
+ url?: string | undefined;
713
+ args?: string[] | undefined;
714
+ env?: Record<string, string> | undefined;
715
+ }> | undefined;
716
+ enabled?: boolean | undefined;
717
+ maxToolsPerServer?: number | undefined;
718
+ maxDescriptionChars?: number | undefined;
719
+ maxSchemaBytes?: number | undefined;
720
+ }>;
721
+ export type McpConfig = z.infer<typeof McpConfigSchema>;
722
+ /**
723
+ * Web-search + web-fetch subtool (C.20). OFF by default. When enabled, the agent
724
+ * gets a bounded `web_search` (query → ranked title/url/snippet) and a `web_fetch`
725
+ * (read one URL as text). Both surface EXTERNAL, attacker-controllable data:
726
+ * results and fetched pages are wrapped as untrusted data (do-not-follow-instructions
727
+ * envelope, fence-forgery neutralized) with upstream model names scrubbed, and
728
+ * are NEVER persisted to memory/index/checkpoint. `web_fetch` refuses non-http(s)
729
+ * schemes and any host that resolves into a private/loopback/link-local range
730
+ * (SSRF guard) — the request is never dispatched. No provider is constructed and
731
+ * no tool is registered while this is off. Search runs through a swappable
732
+ * `SearchProvider` seam; the direct provider's API key comes from the environment
733
+ * (`apiKeyEnv`), never from config-in-repo and never logged.
734
+ */
735
+ export declare const WebConfigSchema: z.ZodObject<{
736
+ /** Master switch. When false, neither tool is registered and no provider is
737
+ * constructed (the feature stays fully inert). */
738
+ enabled: z.ZodDefault<z.ZodBoolean>;
739
+ /** Which search backend to use behind the `SearchProvider` seam. A gateway
740
+ * provider slots in here first-class if the backend ever proxies search. */
741
+ provider: z.ZodDefault<z.ZodEnum<["tavily"]>>;
742
+ /** Environment variable holding the direct provider's API key. The key is
743
+ * read at call time, sent only in the provider's auth field, and never
744
+ * logged or written to the repo. */
745
+ apiKeyEnv: z.ZodDefault<z.ZodString>;
746
+ /** Max search results returned to the model (top-N; the rest are dropped). */
747
+ maxResults: z.ZodDefault<z.ZodNumber>;
748
+ /** Max characters kept from a single result's snippet; the rest is truncated
749
+ * with a visible marker. */
750
+ snippetMaxChars: z.ZodDefault<z.ZodNumber>;
751
+ /** Max bytes read from a single `web_fetch` page; the rest is truncated with
752
+ * a visible marker (a hostile/huge page can't blow the context budget). */
753
+ fetchMaxBytes: z.ZodDefault<z.ZodNumber>;
754
+ /** Per-request timeout (search and fetch) — a slow host errors, never hangs. */
755
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
756
+ /** Max HTTP redirects `web_fetch` follows; each hop is re-checked by the SSRF
757
+ * guard so a 3xx can't bounce the request into a private range. */
758
+ maxRedirects: z.ZodDefault<z.ZodNumber>;
759
+ /** Escape hatch: allow `web_fetch` to reach private/loopback/link-local hosts.
760
+ * OFF by default (SSRF-safe); only set true for a deliberate internal-network
761
+ * use case. */
762
+ allowPrivateHosts: z.ZodDefault<z.ZodBoolean>;
763
+ }, "strict", z.ZodTypeAny, {
764
+ provider: "tavily";
765
+ timeoutMs: number;
766
+ apiKeyEnv: string;
767
+ enabled: boolean;
768
+ maxResults: number;
769
+ snippetMaxChars: number;
770
+ fetchMaxBytes: number;
771
+ maxRedirects: number;
772
+ allowPrivateHosts: boolean;
773
+ }, {
774
+ provider?: "tavily" | undefined;
775
+ timeoutMs?: number | undefined;
776
+ apiKeyEnv?: string | undefined;
777
+ enabled?: boolean | undefined;
778
+ maxResults?: number | undefined;
779
+ snippetMaxChars?: number | undefined;
780
+ fetchMaxBytes?: number | undefined;
781
+ maxRedirects?: number | undefined;
782
+ allowPrivateHosts?: boolean | undefined;
783
+ }>;
784
+ export type WebConfig = z.infer<typeof WebConfigSchema>;
610
785
  export declare const CruxyConfigSchema: z.ZodObject<{
611
786
  model: z.ZodDefault<z.ZodObject<{
612
787
  provider: z.ZodDefault<z.ZodEnum<["cruxy", "anthropic", "openai", "custom"]>>;
@@ -650,15 +825,12 @@ export declare const CruxyConfigSchema: z.ZodObject<{
650
825
  tools: z.ZodDefault<z.ZodObject<{
651
826
  fileEdit: z.ZodDefault<z.ZodBoolean>;
652
827
  shell: z.ZodDefault<z.ZodBoolean>;
653
- webSearch: z.ZodDefault<z.ZodBoolean>;
654
828
  }, "strict", z.ZodTypeAny, {
655
829
  fileEdit: boolean;
656
830
  shell: boolean;
657
- webSearch: boolean;
658
831
  }, {
659
832
  fileEdit?: boolean | undefined;
660
833
  shell?: boolean | undefined;
661
- webSearch?: boolean | undefined;
662
834
  }>>;
663
835
  git: z.ZodDefault<z.ZodObject<{
664
836
  autoCommit: z.ZodDefault<z.ZodBoolean>;
@@ -823,16 +995,16 @@ export declare const CruxyConfigSchema: z.ZodObject<{
823
995
  }, "strict", z.ZodTypeAny, {
824
996
  startupTimeout: number;
825
997
  requestTimeout: number;
826
- enabled: boolean;
827
998
  servers: Record<string, string>;
999
+ enabled: boolean;
828
1000
  maxServers: number;
829
1001
  idleTimeout: number;
830
1002
  maxResults: number;
831
1003
  }, {
832
1004
  startupTimeout?: number | undefined;
833
1005
  requestTimeout?: number | undefined;
834
- enabled?: boolean | undefined;
835
1006
  servers?: Record<string, string> | undefined;
1007
+ enabled?: boolean | undefined;
836
1008
  maxServers?: number | undefined;
837
1009
  idleTimeout?: number | undefined;
838
1010
  maxResults?: number | undefined;
@@ -1104,19 +1276,133 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1104
1276
  } | undefined;
1105
1277
  } | undefined;
1106
1278
  }>>;
1107
- mcpServers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodObject<{
1108
- command: z.ZodOptional<z.ZodString>;
1109
- args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
1110
- url: z.ZodOptional<z.ZodString>;
1279
+ mcp: z.ZodDefault<z.ZodObject<{
1280
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
1281
+ * is registered (the feature stays fully inert). */
1282
+ enabled: z.ZodDefault<z.ZodBoolean>;
1283
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
1284
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
1285
+ servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
1286
+ /** stdio transport: the server program to spawn. */
1287
+ command: z.ZodOptional<z.ZodString>;
1288
+ /** Arguments for `command`. */
1289
+ args: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
1290
+ /** Extra environment variables for the spawned server (stdio only). */
1291
+ env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
1292
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
1293
+ url: z.ZodOptional<z.ZodString>;
1294
+ }, "strict", z.ZodTypeAny, {
1295
+ args: string[];
1296
+ env: Record<string, string>;
1297
+ command?: string | undefined;
1298
+ url?: string | undefined;
1299
+ }, {
1300
+ command?: string | undefined;
1301
+ url?: string | undefined;
1302
+ args?: string[] | undefined;
1303
+ env?: Record<string, string> | undefined;
1304
+ }>, {
1305
+ args: string[];
1306
+ env: Record<string, string>;
1307
+ command?: string | undefined;
1308
+ url?: string | undefined;
1309
+ }, {
1310
+ command?: string | undefined;
1311
+ url?: string | undefined;
1312
+ args?: string[] | undefined;
1313
+ env?: Record<string, string> | undefined;
1314
+ }>>>;
1315
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
1316
+ * not complete within this many ms. */
1317
+ startupTimeout: z.ZodDefault<z.ZodNumber>;
1318
+ /** Fail a single `tools/call` if the server does not respond within this many
1319
+ * ms — the connection is kept, only the one call errors. */
1320
+ requestTimeout: z.ZodDefault<z.ZodNumber>;
1321
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
1322
+ * (a hostile server can't advertise thousands of tools to flood context). */
1323
+ maxToolsPerServer: z.ZodDefault<z.ZodNumber>;
1324
+ /** Max characters kept from a single tool's description; the rest is truncated
1325
+ * with a visible marker. */
1326
+ maxDescriptionChars: z.ZodDefault<z.ZodNumber>;
1327
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
1328
+ * over-cap schema is replaced with a permissive one and a visible note. */
1329
+ maxSchemaBytes: z.ZodDefault<z.ZodNumber>;
1111
1330
  }, "strict", z.ZodTypeAny, {
1112
- command?: string | undefined;
1113
- url?: string | undefined;
1114
- args?: string[] | undefined;
1331
+ startupTimeout: number;
1332
+ requestTimeout: number;
1333
+ servers: Record<string, {
1334
+ args: string[];
1335
+ env: Record<string, string>;
1336
+ command?: string | undefined;
1337
+ url?: string | undefined;
1338
+ }>;
1339
+ enabled: boolean;
1340
+ maxToolsPerServer: number;
1341
+ maxDescriptionChars: number;
1342
+ maxSchemaBytes: number;
1115
1343
  }, {
1116
- command?: string | undefined;
1117
- url?: string | undefined;
1118
- args?: string[] | undefined;
1119
- }>>>;
1344
+ startupTimeout?: number | undefined;
1345
+ requestTimeout?: number | undefined;
1346
+ servers?: Record<string, {
1347
+ command?: string | undefined;
1348
+ url?: string | undefined;
1349
+ args?: string[] | undefined;
1350
+ env?: Record<string, string> | undefined;
1351
+ }> | undefined;
1352
+ enabled?: boolean | undefined;
1353
+ maxToolsPerServer?: number | undefined;
1354
+ maxDescriptionChars?: number | undefined;
1355
+ maxSchemaBytes?: number | undefined;
1356
+ }>>;
1357
+ web: z.ZodDefault<z.ZodObject<{
1358
+ /** Master switch. When false, neither tool is registered and no provider is
1359
+ * constructed (the feature stays fully inert). */
1360
+ enabled: z.ZodDefault<z.ZodBoolean>;
1361
+ /** Which search backend to use behind the `SearchProvider` seam. A gateway
1362
+ * provider slots in here first-class if the backend ever proxies search. */
1363
+ provider: z.ZodDefault<z.ZodEnum<["tavily"]>>;
1364
+ /** Environment variable holding the direct provider's API key. The key is
1365
+ * read at call time, sent only in the provider's auth field, and never
1366
+ * logged or written to the repo. */
1367
+ apiKeyEnv: z.ZodDefault<z.ZodString>;
1368
+ /** Max search results returned to the model (top-N; the rest are dropped). */
1369
+ maxResults: z.ZodDefault<z.ZodNumber>;
1370
+ /** Max characters kept from a single result's snippet; the rest is truncated
1371
+ * with a visible marker. */
1372
+ snippetMaxChars: z.ZodDefault<z.ZodNumber>;
1373
+ /** Max bytes read from a single `web_fetch` page; the rest is truncated with
1374
+ * a visible marker (a hostile/huge page can't blow the context budget). */
1375
+ fetchMaxBytes: z.ZodDefault<z.ZodNumber>;
1376
+ /** Per-request timeout (search and fetch) — a slow host errors, never hangs. */
1377
+ timeoutMs: z.ZodDefault<z.ZodNumber>;
1378
+ /** Max HTTP redirects `web_fetch` follows; each hop is re-checked by the SSRF
1379
+ * guard so a 3xx can't bounce the request into a private range. */
1380
+ maxRedirects: z.ZodDefault<z.ZodNumber>;
1381
+ /** Escape hatch: allow `web_fetch` to reach private/loopback/link-local hosts.
1382
+ * OFF by default (SSRF-safe); only set true for a deliberate internal-network
1383
+ * use case. */
1384
+ allowPrivateHosts: z.ZodDefault<z.ZodBoolean>;
1385
+ }, "strict", z.ZodTypeAny, {
1386
+ provider: "tavily";
1387
+ timeoutMs: number;
1388
+ apiKeyEnv: string;
1389
+ enabled: boolean;
1390
+ maxResults: number;
1391
+ snippetMaxChars: number;
1392
+ fetchMaxBytes: number;
1393
+ maxRedirects: number;
1394
+ allowPrivateHosts: boolean;
1395
+ }, {
1396
+ provider?: "tavily" | undefined;
1397
+ timeoutMs?: number | undefined;
1398
+ apiKeyEnv?: string | undefined;
1399
+ enabled?: boolean | undefined;
1400
+ maxResults?: number | undefined;
1401
+ snippetMaxChars?: number | undefined;
1402
+ fetchMaxBytes?: number | undefined;
1403
+ maxRedirects?: number | undefined;
1404
+ allowPrivateHosts?: boolean | undefined;
1405
+ }>>;
1120
1406
  logLevel: z.ZodDefault<z.ZodEnum<["debug", "info", "warn", "error", "silent"]>>;
1121
1407
  }, "strict", z.ZodTypeAny, {
1122
1408
  cruxy: {
@@ -1188,7 +1474,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1188
1474
  tools: {
1189
1475
  fileEdit: boolean;
1190
1476
  shell: boolean;
1191
- webSearch: boolean;
1192
1477
  };
1193
1478
  git: {
1194
1479
  autoCommit: boolean;
@@ -1218,8 +1503,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1218
1503
  lsp: {
1219
1504
  startupTimeout: number;
1220
1505
  requestTimeout: number;
1221
- enabled: boolean;
1222
1506
  servers: Record<string, string>;
1507
+ enabled: boolean;
1223
1508
  maxServers: number;
1224
1509
  idleTimeout: number;
1225
1510
  maxResults: number;
@@ -1237,11 +1522,31 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1237
1522
  map: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">>;
1238
1523
  default?: "kavi" | "vaani" | "mira" | undefined;
1239
1524
  };
1240
- mcpServers: Record<string, {
1241
- command?: string | undefined;
1242
- url?: string | undefined;
1243
- args?: string[] | undefined;
1244
- }>;
1525
+ mcp: {
1526
+ startupTimeout: number;
1527
+ requestTimeout: number;
1528
+ servers: Record<string, {
1529
+ args: string[];
1530
+ env: Record<string, string>;
1531
+ command?: string | undefined;
1532
+ url?: string | undefined;
1533
+ }>;
1534
+ enabled: boolean;
1535
+ maxToolsPerServer: number;
1536
+ maxDescriptionChars: number;
1537
+ maxSchemaBytes: number;
1538
+ };
1539
+ web: {
1540
+ provider: "tavily";
1541
+ timeoutMs: number;
1542
+ apiKeyEnv: string;
1543
+ enabled: boolean;
1544
+ maxResults: number;
1545
+ snippetMaxChars: number;
1546
+ fetchMaxBytes: number;
1547
+ maxRedirects: number;
1548
+ allowPrivateHosts: boolean;
1549
+ };
1245
1550
  logLevel: "debug" | "info" | "warn" | "error" | "silent";
1246
1551
  }, {
1247
1552
  cruxy?: {
@@ -1313,7 +1618,6 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1313
1618
  tools?: {
1314
1619
  fileEdit?: boolean | undefined;
1315
1620
  shell?: boolean | undefined;
1316
- webSearch?: boolean | undefined;
1317
1621
  } | undefined;
1318
1622
  git?: {
1319
1623
  autoCommit?: boolean | undefined;
@@ -1343,8 +1647,8 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1343
1647
  lsp?: {
1344
1648
  startupTimeout?: number | undefined;
1345
1649
  requestTimeout?: number | undefined;
1346
- enabled?: boolean | undefined;
1347
1650
  servers?: Record<string, string> | undefined;
1651
+ enabled?: boolean | undefined;
1348
1652
  maxServers?: number | undefined;
1349
1653
  idleTimeout?: number | undefined;
1350
1654
  maxResults?: number | undefined;
@@ -1362,11 +1666,31 @@ export declare const CruxyConfigSchema: z.ZodObject<{
1362
1666
  map?: Partial<Record<"main-turn" | "subagent" | "plan" | "commit-msg" | "classify" | "summarize", "kavi" | "vaani" | "mira">> | undefined;
1363
1667
  default?: "kavi" | "vaani" | "mira" | undefined;
1364
1668
  } | undefined;
1365
- mcpServers?: Record<string, {
1366
- command?: string | undefined;
1367
- url?: string | undefined;
1368
- args?: string[] | undefined;
1369
- }> | undefined;
1669
+ mcp?: {
1670
+ startupTimeout?: number | undefined;
1671
+ requestTimeout?: number | undefined;
1672
+ servers?: Record<string, {
1673
+ command?: string | undefined;
1674
+ url?: string | undefined;
1675
+ args?: string[] | undefined;
1676
+ env?: Record<string, string> | undefined;
1677
+ }> | undefined;
1678
+ enabled?: boolean | undefined;
1679
+ maxToolsPerServer?: number | undefined;
1680
+ maxDescriptionChars?: number | undefined;
1681
+ maxSchemaBytes?: number | undefined;
1682
+ } | undefined;
1683
+ web?: {
1684
+ provider?: "tavily" | undefined;
1685
+ timeoutMs?: number | undefined;
1686
+ apiKeyEnv?: string | undefined;
1687
+ enabled?: boolean | undefined;
1688
+ maxResults?: number | undefined;
1689
+ snippetMaxChars?: number | undefined;
1690
+ fetchMaxBytes?: number | undefined;
1691
+ maxRedirects?: number | undefined;
1692
+ allowPrivateHosts?: boolean | undefined;
1693
+ } | undefined;
1370
1694
  logLevel?: "debug" | "info" | "warn" | "error" | "silent" | undefined;
1371
1695
  }>;
1372
1696
  export type CruxyConfig = z.infer<typeof CruxyConfigSchema>;
@@ -38,7 +38,6 @@ export const ToolsConfigSchema = z
38
38
  .object({
39
39
  fileEdit: z.boolean().default(true),
40
40
  shell: z.boolean().default(true),
41
- webSearch: z.boolean().default(false), // C.17
42
41
  })
43
42
  .strict();
44
43
  export const GitConfigSchema = z
@@ -365,12 +364,107 @@ export const UsageConfigSchema = z
365
364
  .default({}),
366
365
  })
367
366
  .strict();
368
- /** MCP server entry — stdio or URL transport (wired up in a later phase). */
367
+ /**
368
+ * One MCP server entry (C.27). A `command` (+ optional `args`/`env`) is a stdio
369
+ * server cruxy spawns as a child process; a `url` names a remote server. Exactly
370
+ * one transport must be given. Trusting a stdio server runs its code UNSANDBOXED
371
+ * with your privileges, which is why connection is gated by an explicit,
372
+ * fingerprinted trust decision (see `mcp/trust.ts`).
373
+ */
369
374
  export const McpServerSchema = z
370
375
  .object({
371
- command: z.string().optional(),
372
- args: z.array(z.string()).optional(),
376
+ /** stdio transport: the server program to spawn. */
377
+ command: z.string().min(1).optional(),
378
+ /** Arguments for `command`. */
379
+ args: z.array(z.string()).default([]),
380
+ /** Extra environment variables for the spawned server (stdio only). */
381
+ env: z.record(z.string(), z.string()).default({}),
382
+ /** Remote transport: the server URL (mutually exclusive with `command`). */
373
383
  url: z.string().url().optional(),
384
+ })
385
+ .strict()
386
+ .refine((s) => Boolean(s.command) !== Boolean(s.url), {
387
+ message: "an MCP server needs exactly one of `command` (stdio) or `url`",
388
+ });
389
+ /**
390
+ * MCP client integration (C.27): connect to trusted MCP servers and expose their
391
+ * tools to the agent. OFF by default — like the sandbox, LSP, and hooks, it runs
392
+ * EXTERNAL code (a stdio server executes UNSANDBOXED with your privileges), a
393
+ * real execution surface you opt into explicitly. When off, nothing connects or
394
+ * spawns and no MCP tool is ever registered. Every server-advertised tool is
395
+ * gated (destructive tier — a server can never self-declare a tool "safe"), its
396
+ * description and results are demarcated as untrusted external data with upstream
397
+ * model names scrubbed, and results are NEVER persisted. The tool list a server
398
+ * advertises is bounded (count + per-tool description/schema size) so a hostile
399
+ * server can't blow the context budget.
400
+ */
401
+ export const McpConfigSchema = z
402
+ .object({
403
+ /** Master switch. When false, no server is connected/spawned and no MCP tool
404
+ * is registered (the feature stays fully inert). */
405
+ enabled: z.boolean().default(false),
406
+ /** Named MCP servers, keyed by a short server id used as the tool prefix
407
+ * (`mcp__<server>__<tool>`) and in the trust prompt. */
408
+ servers: z.record(z.string(), McpServerSchema).default({}),
409
+ /** Fail a server's `initialize` handshake (its tools are skipped) if it does
410
+ * not complete within this many ms. */
411
+ startupTimeout: z.number().int().positive().default(15000),
412
+ /** Fail a single `tools/call` if the server does not respond within this many
413
+ * ms — the connection is kept, only the one call errors. */
414
+ requestTimeout: z.number().int().positive().default(30000),
415
+ /** Max tools accepted from ONE server; extras are dropped with a visible note
416
+ * (a hostile server can't advertise thousands of tools to flood context). */
417
+ maxToolsPerServer: z.number().int().positive().default(32),
418
+ /** Max characters kept from a single tool's description; the rest is truncated
419
+ * with a visible marker. */
420
+ maxDescriptionChars: z.number().int().positive().default(1024),
421
+ /** Max bytes kept from a single tool's advertised JSON input schema; an
422
+ * over-cap schema is replaced with a permissive one and a visible note. */
423
+ maxSchemaBytes: z.number().int().positive().default(8192),
424
+ })
425
+ .strict();
426
+ /**
427
+ * Web-search + web-fetch subtool (C.20). OFF by default. When enabled, the agent
428
+ * gets a bounded `web_search` (query → ranked title/url/snippet) and a `web_fetch`
429
+ * (read one URL as text). Both surface EXTERNAL, attacker-controllable data:
430
+ * results and fetched pages are wrapped as untrusted data (do-not-follow-instructions
431
+ * envelope, fence-forgery neutralized) with upstream model names scrubbed, and
432
+ * are NEVER persisted to memory/index/checkpoint. `web_fetch` refuses non-http(s)
433
+ * schemes and any host that resolves into a private/loopback/link-local range
434
+ * (SSRF guard) — the request is never dispatched. No provider is constructed and
435
+ * no tool is registered while this is off. Search runs through a swappable
436
+ * `SearchProvider` seam; the direct provider's API key comes from the environment
437
+ * (`apiKeyEnv`), never from config-in-repo and never logged.
438
+ */
439
+ export const WebConfigSchema = z
440
+ .object({
441
+ /** Master switch. When false, neither tool is registered and no provider is
442
+ * constructed (the feature stays fully inert). */
443
+ enabled: z.boolean().default(false),
444
+ /** Which search backend to use behind the `SearchProvider` seam. A gateway
445
+ * provider slots in here first-class if the backend ever proxies search. */
446
+ provider: z.enum(["tavily"]).default("tavily"),
447
+ /** Environment variable holding the direct provider's API key. The key is
448
+ * read at call time, sent only in the provider's auth field, and never
449
+ * logged or written to the repo. */
450
+ apiKeyEnv: z.string().min(1).default("TAVILY_API_KEY"),
451
+ /** Max search results returned to the model (top-N; the rest are dropped). */
452
+ maxResults: z.number().int().positive().max(20).default(5),
453
+ /** Max characters kept from a single result's snippet; the rest is truncated
454
+ * with a visible marker. */
455
+ snippetMaxChars: z.number().int().positive().default(500),
456
+ /** Max bytes read from a single `web_fetch` page; the rest is truncated with
457
+ * a visible marker (a hostile/huge page can't blow the context budget). */
458
+ fetchMaxBytes: z.number().int().positive().default(524288),
459
+ /** Per-request timeout (search and fetch) — a slow host errors, never hangs. */
460
+ timeoutMs: z.number().int().positive().default(15000),
461
+ /** Max HTTP redirects `web_fetch` follows; each hop is re-checked by the SSRF
462
+ * guard so a 3xx can't bounce the request into a private range. */
463
+ maxRedirects: z.number().int().min(0).default(3),
464
+ /** Escape hatch: allow `web_fetch` to reach private/loopback/link-local hosts.
465
+ * OFF by default (SSRF-safe); only set true for a deliberate internal-network
466
+ * use case. */
467
+ allowPrivateHosts: z.boolean().default(false),
374
468
  })
375
469
  .strict();
376
470
  export const CruxyConfigSchema = z
@@ -393,7 +487,8 @@ export const CruxyConfigSchema = z
393
487
  routing: RoutingConfigSchema.default({}),
394
488
  memory: MemoryConfigSchema.default({}),
395
489
  usage: UsageConfigSchema.default({}),
396
- mcpServers: z.record(z.string(), McpServerSchema).default({}),
490
+ mcp: McpConfigSchema.default({}),
491
+ web: WebConfigSchema.default({}),
397
492
  logLevel: z.enum(LOG_LEVELS).default("info"),
398
493
  })
399
494
  .strict();
@@ -41,6 +41,14 @@ export declare const MEMORY_FILE_NAME = "entries.json";
41
41
  * (`~/.cruxy/memory-trust.json`) — its own file, independent of hook trust, so
42
42
  * cloning a repo carries zero memory trust (C.29 supply-chain safety). */
43
43
  export declare const MEMORY_TRUST_FILE_NAME = "memory-trust.json";
44
+ /**
45
+ * MCP server integration (C.27). Per-repo trust for configured MCP servers, in
46
+ * the GLOBAL dir only (`~/.cruxy/mcp-trust.json`) — its own file, independent of
47
+ * hook and memory trust, so cloning a repo carries zero MCP trust. Trusting a
48
+ * server runs its code UNSANDBOXED with your privileges, so this decision is
49
+ * fingerprinted and re-checked on every run (C.27 supply-chain safety).
50
+ */
51
+ export declare const MCP_TRUST_FILE_NAME = "mcp-trust.json";
44
52
  /**
45
53
  * Usage telemetry + cost tracking (C.22). Per-run/per-session usage records live
46
54
  * in the GLOBAL dir only (`~/.cruxy/usage/runs.json`), `0600` — LOCAL accounting