@cruxy/cli 0.25.0 → 0.27.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.
- package/dist/approval/prompt.d.ts +7 -1
- package/dist/approval/prompt.js +52 -17
- package/dist/cli/commands/mcp.js +106 -7
- package/dist/cli/commands/skills.js +10 -2
- package/dist/cli/repl.js +9 -3
- package/dist/components/frame.d.ts +6 -3
- package/dist/components/frame.js +21 -23
- package/dist/components/fuzzy.js +5 -1
- package/dist/components/select.js +4 -1
- package/dist/config/credentials.d.ts +9 -0
- package/dist/config/credentials.js +29 -1
- package/dist/config/manager.js +30 -3
- package/dist/config/schema.d.ts +182 -8
- package/dist/config/schema.js +43 -5
- package/dist/errors/constructors.d.ts +29 -0
- package/dist/errors/constructors.js +69 -0
- package/dist/errors/types.d.ts +15 -0
- package/dist/errors/types.js +18 -0
- package/dist/mcp/http-transport.d.ts +89 -0
- package/dist/mcp/http-transport.js +299 -0
- package/dist/mcp/index.d.ts +4 -2
- package/dist/mcp/index.js +3 -1
- package/dist/mcp/service.d.ts +19 -2
- package/dist/mcp/service.js +92 -20
- package/dist/mcp/trust-gate.d.ts +35 -11
- package/dist/mcp/trust-gate.js +87 -22
- package/dist/mcp/trust.d.ts +12 -2
- package/dist/mcp/trust.js +26 -2
- package/dist/mcp/types.d.ts +10 -0
- package/dist/mcp/url-guard.d.ts +48 -0
- package/dist/mcp/url-guard.js +62 -0
- package/dist/net/ip-guard.d.ts +55 -0
- package/dist/net/ip-guard.js +229 -0
- package/dist/render/capabilities.d.ts +11 -0
- package/dist/render/capabilities.js +19 -3
- package/dist/render/diff.d.ts +1 -1
- package/dist/render/diff.js +23 -7
- package/dist/render/index.d.ts +5 -2
- package/dist/render/index.js +9 -2
- package/dist/render/layout.d.ts +59 -0
- package/dist/render/layout.js +158 -0
- package/dist/render/motion.d.ts +76 -0
- package/dist/render/motion.js +94 -0
- package/dist/render/resize.d.ts +36 -0
- package/dist/render/resize.js +45 -0
- package/dist/render/state.d.ts +13 -0
- package/dist/render/state.js +38 -0
- package/dist/render/tty-renderer.d.ts +25 -3
- package/dist/render/tty-renderer.js +94 -32
- package/dist/render/types.d.ts +15 -1
- package/dist/web/ssrf.d.ts +8 -22
- package/dist/web/ssrf.js +11 -183
- package/dist/web/types.d.ts +4 -2
- package/package.json +1 -1
package/dist/config/schema.d.ts
CHANGED
|
@@ -649,13 +649,19 @@ export declare const UsageConfigSchema: z.ZodObject<{
|
|
|
649
649
|
}>;
|
|
650
650
|
export type UsageConfig = z.infer<typeof UsageConfigSchema>;
|
|
651
651
|
/**
|
|
652
|
-
* One MCP server entry
|
|
653
|
-
*
|
|
654
|
-
* one transport must be given.
|
|
655
|
-
*
|
|
656
|
-
* fingerprinted trust decision (
|
|
652
|
+
* One MCP server entry. A `command` (+ optional `args`/`env`) is a stdio server
|
|
653
|
+
* cruxy spawns as a child process (C.27); a `url` names a remote server reached
|
|
654
|
+
* over Streamable HTTP (C.27b). Exactly one transport must be given.
|
|
655
|
+
*
|
|
656
|
+
* Both are gated by an explicit, fingerprinted trust decision (`mcp/trust.ts`),
|
|
657
|
+
* but the escalation differs: a stdio server runs its code UNSANDBOXED with your
|
|
658
|
+
* privileges, while a `url` server runs remotely — cruxy sends it your tool
|
|
659
|
+
* arguments over the network (https + cert-validated + pinned to a public IP; http
|
|
660
|
+
* only for a loopback dev server) and treats its responses as untrusted data. A
|
|
661
|
+
* url's trust also binds its resolved IP set at trust time, so a later IP-set
|
|
662
|
+
* change re-gates (weaker than a local binary fingerprint — see `mcp/types.ts`).
|
|
657
663
|
*/
|
|
658
|
-
export declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
|
|
664
|
+
export declare const McpServerSchema: z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
659
665
|
/** stdio transport: the server program to spawn. */
|
|
660
666
|
command: z.ZodOptional<z.ZodString>;
|
|
661
667
|
/** Arguments for `command`. */
|
|
@@ -664,26 +670,78 @@ export declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
|
|
|
664
670
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
665
671
|
/** Remote transport: the server URL (mutually exclusive with `command`). */
|
|
666
672
|
url: z.ZodOptional<z.ZodString>;
|
|
673
|
+
/**
|
|
674
|
+
* Remote auth (C.27c, url only): the NAME of a bearer credential, resolved
|
|
675
|
+
* solely from `~/.cruxy/credentials.json` (0600) as `Authorization: Bearer …`.
|
|
676
|
+
* This field holds a NAME, never the secret — a project config may name a
|
|
677
|
+
* credential it does not contain, so the value never lives in-repo. Resolution
|
|
678
|
+
* never consults the environment or any config file. Requires an `https` URL.
|
|
679
|
+
*/
|
|
680
|
+
credentialRef: z.ZodOptional<z.ZodString>;
|
|
681
|
+
/**
|
|
682
|
+
* Remote auth (C.27c, url only): raw request headers sent to the endpoint.
|
|
683
|
+
* A LIVE header value is a secret, so this is accepted ONLY from user-scope
|
|
684
|
+
* config (`~/.cruxy/config.json`) — the loader REJECTS it from project-scope
|
|
685
|
+
* config (a cloned repo must not inject live headers). Prefer `credentialRef`.
|
|
686
|
+
* Requires an `https` URL; reserved framing headers cannot be overridden.
|
|
687
|
+
*/
|
|
688
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
667
689
|
}, "strict", z.ZodTypeAny, {
|
|
668
690
|
args: string[];
|
|
669
691
|
env: Record<string, string>;
|
|
670
692
|
command?: string | undefined;
|
|
693
|
+
credentialRef?: string | undefined;
|
|
694
|
+
url?: string | undefined;
|
|
695
|
+
headers?: Record<string, string> | undefined;
|
|
696
|
+
}, {
|
|
697
|
+
command?: string | undefined;
|
|
698
|
+
credentialRef?: string | undefined;
|
|
699
|
+
url?: string | undefined;
|
|
700
|
+
args?: string[] | undefined;
|
|
701
|
+
env?: Record<string, string> | undefined;
|
|
702
|
+
headers?: Record<string, string> | undefined;
|
|
703
|
+
}>, {
|
|
704
|
+
args: string[];
|
|
705
|
+
env: Record<string, string>;
|
|
706
|
+
command?: string | undefined;
|
|
707
|
+
credentialRef?: string | undefined;
|
|
671
708
|
url?: string | undefined;
|
|
709
|
+
headers?: Record<string, string> | undefined;
|
|
672
710
|
}, {
|
|
673
711
|
command?: string | undefined;
|
|
712
|
+
credentialRef?: string | undefined;
|
|
674
713
|
url?: string | undefined;
|
|
675
714
|
args?: string[] | undefined;
|
|
676
715
|
env?: Record<string, string> | undefined;
|
|
716
|
+
headers?: Record<string, string> | undefined;
|
|
677
717
|
}>, {
|
|
678
718
|
args: string[];
|
|
679
719
|
env: Record<string, string>;
|
|
680
720
|
command?: string | undefined;
|
|
721
|
+
credentialRef?: string | undefined;
|
|
681
722
|
url?: string | undefined;
|
|
723
|
+
headers?: Record<string, string> | undefined;
|
|
682
724
|
}, {
|
|
683
725
|
command?: string | undefined;
|
|
726
|
+
credentialRef?: string | undefined;
|
|
684
727
|
url?: string | undefined;
|
|
685
728
|
args?: string[] | undefined;
|
|
686
729
|
env?: Record<string, string> | undefined;
|
|
730
|
+
headers?: Record<string, string> | undefined;
|
|
731
|
+
}>, {
|
|
732
|
+
args: string[];
|
|
733
|
+
env: Record<string, string>;
|
|
734
|
+
command?: string | undefined;
|
|
735
|
+
credentialRef?: string | undefined;
|
|
736
|
+
url?: string | undefined;
|
|
737
|
+
headers?: Record<string, string> | undefined;
|
|
738
|
+
}, {
|
|
739
|
+
command?: string | undefined;
|
|
740
|
+
credentialRef?: string | undefined;
|
|
741
|
+
url?: string | undefined;
|
|
742
|
+
args?: string[] | undefined;
|
|
743
|
+
env?: Record<string, string> | undefined;
|
|
744
|
+
headers?: Record<string, string> | undefined;
|
|
687
745
|
}>;
|
|
688
746
|
export type McpServerConfig = z.infer<typeof McpServerSchema>;
|
|
689
747
|
/**
|
|
@@ -704,7 +762,7 @@ export declare const McpConfigSchema: z.ZodObject<{
|
|
|
704
762
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
705
763
|
/** Named MCP servers, keyed by a short server id used as the tool prefix
|
|
706
764
|
* (`mcp__<server>__<tool>`) and in the trust prompt. */
|
|
707
|
-
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
765
|
+
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
708
766
|
/** stdio transport: the server program to spawn. */
|
|
709
767
|
command: z.ZodOptional<z.ZodString>;
|
|
710
768
|
/** Arguments for `command`. */
|
|
@@ -713,26 +771,78 @@ export declare const McpConfigSchema: z.ZodObject<{
|
|
|
713
771
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
714
772
|
/** Remote transport: the server URL (mutually exclusive with `command`). */
|
|
715
773
|
url: z.ZodOptional<z.ZodString>;
|
|
774
|
+
/**
|
|
775
|
+
* Remote auth (C.27c, url only): the NAME of a bearer credential, resolved
|
|
776
|
+
* solely from `~/.cruxy/credentials.json` (0600) as `Authorization: Bearer …`.
|
|
777
|
+
* This field holds a NAME, never the secret — a project config may name a
|
|
778
|
+
* credential it does not contain, so the value never lives in-repo. Resolution
|
|
779
|
+
* never consults the environment or any config file. Requires an `https` URL.
|
|
780
|
+
*/
|
|
781
|
+
credentialRef: z.ZodOptional<z.ZodString>;
|
|
782
|
+
/**
|
|
783
|
+
* Remote auth (C.27c, url only): raw request headers sent to the endpoint.
|
|
784
|
+
* A LIVE header value is a secret, so this is accepted ONLY from user-scope
|
|
785
|
+
* config (`~/.cruxy/config.json`) — the loader REJECTS it from project-scope
|
|
786
|
+
* config (a cloned repo must not inject live headers). Prefer `credentialRef`.
|
|
787
|
+
* Requires an `https` URL; reserved framing headers cannot be overridden.
|
|
788
|
+
*/
|
|
789
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
716
790
|
}, "strict", z.ZodTypeAny, {
|
|
717
791
|
args: string[];
|
|
718
792
|
env: Record<string, string>;
|
|
719
793
|
command?: string | undefined;
|
|
794
|
+
credentialRef?: string | undefined;
|
|
720
795
|
url?: string | undefined;
|
|
796
|
+
headers?: Record<string, string> | undefined;
|
|
721
797
|
}, {
|
|
722
798
|
command?: string | undefined;
|
|
799
|
+
credentialRef?: string | undefined;
|
|
723
800
|
url?: string | undefined;
|
|
724
801
|
args?: string[] | undefined;
|
|
725
802
|
env?: Record<string, string> | undefined;
|
|
803
|
+
headers?: Record<string, string> | undefined;
|
|
726
804
|
}>, {
|
|
727
805
|
args: string[];
|
|
728
806
|
env: Record<string, string>;
|
|
729
807
|
command?: string | undefined;
|
|
808
|
+
credentialRef?: string | undefined;
|
|
730
809
|
url?: string | undefined;
|
|
810
|
+
headers?: Record<string, string> | undefined;
|
|
731
811
|
}, {
|
|
732
812
|
command?: string | undefined;
|
|
813
|
+
credentialRef?: string | undefined;
|
|
733
814
|
url?: string | undefined;
|
|
734
815
|
args?: string[] | undefined;
|
|
735
816
|
env?: Record<string, string> | undefined;
|
|
817
|
+
headers?: Record<string, string> | undefined;
|
|
818
|
+
}>, {
|
|
819
|
+
args: string[];
|
|
820
|
+
env: Record<string, string>;
|
|
821
|
+
command?: string | undefined;
|
|
822
|
+
credentialRef?: string | undefined;
|
|
823
|
+
url?: string | undefined;
|
|
824
|
+
headers?: Record<string, string> | undefined;
|
|
825
|
+
}, {
|
|
826
|
+
command?: string | undefined;
|
|
827
|
+
credentialRef?: string | undefined;
|
|
828
|
+
url?: string | undefined;
|
|
829
|
+
args?: string[] | undefined;
|
|
830
|
+
env?: Record<string, string> | undefined;
|
|
831
|
+
headers?: Record<string, string> | undefined;
|
|
832
|
+
}>, {
|
|
833
|
+
args: string[];
|
|
834
|
+
env: Record<string, string>;
|
|
835
|
+
command?: string | undefined;
|
|
836
|
+
credentialRef?: string | undefined;
|
|
837
|
+
url?: string | undefined;
|
|
838
|
+
headers?: Record<string, string> | undefined;
|
|
839
|
+
}, {
|
|
840
|
+
command?: string | undefined;
|
|
841
|
+
credentialRef?: string | undefined;
|
|
842
|
+
url?: string | undefined;
|
|
843
|
+
args?: string[] | undefined;
|
|
844
|
+
env?: Record<string, string> | undefined;
|
|
845
|
+
headers?: Record<string, string> | undefined;
|
|
736
846
|
}>>>;
|
|
737
847
|
/** Fail a server's `initialize` handshake (its tools are skipped) if it does
|
|
738
848
|
* not complete within this many ms. */
|
|
@@ -756,7 +866,9 @@ export declare const McpConfigSchema: z.ZodObject<{
|
|
|
756
866
|
args: string[];
|
|
757
867
|
env: Record<string, string>;
|
|
758
868
|
command?: string | undefined;
|
|
869
|
+
credentialRef?: string | undefined;
|
|
759
870
|
url?: string | undefined;
|
|
871
|
+
headers?: Record<string, string> | undefined;
|
|
760
872
|
}>;
|
|
761
873
|
enabled: boolean;
|
|
762
874
|
maxToolsPerServer: number;
|
|
@@ -767,9 +879,11 @@ export declare const McpConfigSchema: z.ZodObject<{
|
|
|
767
879
|
requestTimeout?: number | undefined;
|
|
768
880
|
servers?: Record<string, {
|
|
769
881
|
command?: string | undefined;
|
|
882
|
+
credentialRef?: string | undefined;
|
|
770
883
|
url?: string | undefined;
|
|
771
884
|
args?: string[] | undefined;
|
|
772
885
|
env?: Record<string, string> | undefined;
|
|
886
|
+
headers?: Record<string, string> | undefined;
|
|
773
887
|
}> | undefined;
|
|
774
888
|
enabled?: boolean | undefined;
|
|
775
889
|
maxToolsPerServer?: number | undefined;
|
|
@@ -1380,7 +1494,7 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1380
1494
|
enabled: z.ZodDefault<z.ZodBoolean>;
|
|
1381
1495
|
/** Named MCP servers, keyed by a short server id used as the tool prefix
|
|
1382
1496
|
* (`mcp__<server>__<tool>`) and in the trust prompt. */
|
|
1383
|
-
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
1497
|
+
servers: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodEffects<z.ZodEffects<z.ZodObject<{
|
|
1384
1498
|
/** stdio transport: the server program to spawn. */
|
|
1385
1499
|
command: z.ZodOptional<z.ZodString>;
|
|
1386
1500
|
/** Arguments for `command`. */
|
|
@@ -1389,26 +1503,78 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1389
1503
|
env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1390
1504
|
/** Remote transport: the server URL (mutually exclusive with `command`). */
|
|
1391
1505
|
url: z.ZodOptional<z.ZodString>;
|
|
1506
|
+
/**
|
|
1507
|
+
* Remote auth (C.27c, url only): the NAME of a bearer credential, resolved
|
|
1508
|
+
* solely from `~/.cruxy/credentials.json` (0600) as `Authorization: Bearer …`.
|
|
1509
|
+
* This field holds a NAME, never the secret — a project config may name a
|
|
1510
|
+
* credential it does not contain, so the value never lives in-repo. Resolution
|
|
1511
|
+
* never consults the environment or any config file. Requires an `https` URL.
|
|
1512
|
+
*/
|
|
1513
|
+
credentialRef: z.ZodOptional<z.ZodString>;
|
|
1514
|
+
/**
|
|
1515
|
+
* Remote auth (C.27c, url only): raw request headers sent to the endpoint.
|
|
1516
|
+
* A LIVE header value is a secret, so this is accepted ONLY from user-scope
|
|
1517
|
+
* config (`~/.cruxy/config.json`) — the loader REJECTS it from project-scope
|
|
1518
|
+
* config (a cloned repo must not inject live headers). Prefer `credentialRef`.
|
|
1519
|
+
* Requires an `https` URL; reserved framing headers cannot be overridden.
|
|
1520
|
+
*/
|
|
1521
|
+
headers: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
1392
1522
|
}, "strict", z.ZodTypeAny, {
|
|
1393
1523
|
args: string[];
|
|
1394
1524
|
env: Record<string, string>;
|
|
1395
1525
|
command?: string | undefined;
|
|
1526
|
+
credentialRef?: string | undefined;
|
|
1527
|
+
url?: string | undefined;
|
|
1528
|
+
headers?: Record<string, string> | undefined;
|
|
1529
|
+
}, {
|
|
1530
|
+
command?: string | undefined;
|
|
1531
|
+
credentialRef?: string | undefined;
|
|
1532
|
+
url?: string | undefined;
|
|
1533
|
+
args?: string[] | undefined;
|
|
1534
|
+
env?: Record<string, string> | undefined;
|
|
1535
|
+
headers?: Record<string, string> | undefined;
|
|
1536
|
+
}>, {
|
|
1537
|
+
args: string[];
|
|
1538
|
+
env: Record<string, string>;
|
|
1539
|
+
command?: string | undefined;
|
|
1540
|
+
credentialRef?: string | undefined;
|
|
1541
|
+
url?: string | undefined;
|
|
1542
|
+
headers?: Record<string, string> | undefined;
|
|
1543
|
+
}, {
|
|
1544
|
+
command?: string | undefined;
|
|
1545
|
+
credentialRef?: string | undefined;
|
|
1546
|
+
url?: string | undefined;
|
|
1547
|
+
args?: string[] | undefined;
|
|
1548
|
+
env?: Record<string, string> | undefined;
|
|
1549
|
+
headers?: Record<string, string> | undefined;
|
|
1550
|
+
}>, {
|
|
1551
|
+
args: string[];
|
|
1552
|
+
env: Record<string, string>;
|
|
1553
|
+
command?: string | undefined;
|
|
1554
|
+
credentialRef?: string | undefined;
|
|
1396
1555
|
url?: string | undefined;
|
|
1556
|
+
headers?: Record<string, string> | undefined;
|
|
1397
1557
|
}, {
|
|
1398
1558
|
command?: string | undefined;
|
|
1559
|
+
credentialRef?: string | undefined;
|
|
1399
1560
|
url?: string | undefined;
|
|
1400
1561
|
args?: string[] | undefined;
|
|
1401
1562
|
env?: Record<string, string> | undefined;
|
|
1563
|
+
headers?: Record<string, string> | undefined;
|
|
1402
1564
|
}>, {
|
|
1403
1565
|
args: string[];
|
|
1404
1566
|
env: Record<string, string>;
|
|
1405
1567
|
command?: string | undefined;
|
|
1568
|
+
credentialRef?: string | undefined;
|
|
1406
1569
|
url?: string | undefined;
|
|
1570
|
+
headers?: Record<string, string> | undefined;
|
|
1407
1571
|
}, {
|
|
1408
1572
|
command?: string | undefined;
|
|
1573
|
+
credentialRef?: string | undefined;
|
|
1409
1574
|
url?: string | undefined;
|
|
1410
1575
|
args?: string[] | undefined;
|
|
1411
1576
|
env?: Record<string, string> | undefined;
|
|
1577
|
+
headers?: Record<string, string> | undefined;
|
|
1412
1578
|
}>>>;
|
|
1413
1579
|
/** Fail a server's `initialize` handshake (its tools are skipped) if it does
|
|
1414
1580
|
* not complete within this many ms. */
|
|
@@ -1432,7 +1598,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1432
1598
|
args: string[];
|
|
1433
1599
|
env: Record<string, string>;
|
|
1434
1600
|
command?: string | undefined;
|
|
1601
|
+
credentialRef?: string | undefined;
|
|
1435
1602
|
url?: string | undefined;
|
|
1603
|
+
headers?: Record<string, string> | undefined;
|
|
1436
1604
|
}>;
|
|
1437
1605
|
enabled: boolean;
|
|
1438
1606
|
maxToolsPerServer: number;
|
|
@@ -1443,9 +1611,11 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1443
1611
|
requestTimeout?: number | undefined;
|
|
1444
1612
|
servers?: Record<string, {
|
|
1445
1613
|
command?: string | undefined;
|
|
1614
|
+
credentialRef?: string | undefined;
|
|
1446
1615
|
url?: string | undefined;
|
|
1447
1616
|
args?: string[] | undefined;
|
|
1448
1617
|
env?: Record<string, string> | undefined;
|
|
1618
|
+
headers?: Record<string, string> | undefined;
|
|
1449
1619
|
}> | undefined;
|
|
1450
1620
|
enabled?: boolean | undefined;
|
|
1451
1621
|
maxToolsPerServer?: number | undefined;
|
|
@@ -1633,7 +1803,9 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1633
1803
|
args: string[];
|
|
1634
1804
|
env: Record<string, string>;
|
|
1635
1805
|
command?: string | undefined;
|
|
1806
|
+
credentialRef?: string | undefined;
|
|
1636
1807
|
url?: string | undefined;
|
|
1808
|
+
headers?: Record<string, string> | undefined;
|
|
1637
1809
|
}>;
|
|
1638
1810
|
enabled: boolean;
|
|
1639
1811
|
maxToolsPerServer: number;
|
|
@@ -1781,9 +1953,11 @@ export declare const CruxyConfigSchema: z.ZodObject<{
|
|
|
1781
1953
|
requestTimeout?: number | undefined;
|
|
1782
1954
|
servers?: Record<string, {
|
|
1783
1955
|
command?: string | undefined;
|
|
1956
|
+
credentialRef?: string | undefined;
|
|
1784
1957
|
url?: string | undefined;
|
|
1785
1958
|
args?: string[] | undefined;
|
|
1786
1959
|
env?: Record<string, string> | undefined;
|
|
1960
|
+
headers?: Record<string, string> | undefined;
|
|
1787
1961
|
}> | undefined;
|
|
1788
1962
|
enabled?: boolean | undefined;
|
|
1789
1963
|
maxToolsPerServer?: number | undefined;
|
package/dist/config/schema.js
CHANGED
|
@@ -414,11 +414,17 @@ export const UsageConfigSchema = z
|
|
|
414
414
|
})
|
|
415
415
|
.strict();
|
|
416
416
|
/**
|
|
417
|
-
* One MCP server entry
|
|
418
|
-
*
|
|
419
|
-
* one transport must be given.
|
|
420
|
-
*
|
|
421
|
-
* fingerprinted trust decision (
|
|
417
|
+
* One MCP server entry. A `command` (+ optional `args`/`env`) is a stdio server
|
|
418
|
+
* cruxy spawns as a child process (C.27); a `url` names a remote server reached
|
|
419
|
+
* over Streamable HTTP (C.27b). Exactly one transport must be given.
|
|
420
|
+
*
|
|
421
|
+
* Both are gated by an explicit, fingerprinted trust decision (`mcp/trust.ts`),
|
|
422
|
+
* but the escalation differs: a stdio server runs its code UNSANDBOXED with your
|
|
423
|
+
* privileges, while a `url` server runs remotely — cruxy sends it your tool
|
|
424
|
+
* arguments over the network (https + cert-validated + pinned to a public IP; http
|
|
425
|
+
* only for a loopback dev server) and treats its responses as untrusted data. A
|
|
426
|
+
* url's trust also binds its resolved IP set at trust time, so a later IP-set
|
|
427
|
+
* change re-gates (weaker than a local binary fingerprint — see `mcp/types.ts`).
|
|
422
428
|
*/
|
|
423
429
|
export const McpServerSchema = z
|
|
424
430
|
.object({
|
|
@@ -430,11 +436,43 @@ export const McpServerSchema = z
|
|
|
430
436
|
env: z.record(z.string(), z.string()).default({}),
|
|
431
437
|
/** Remote transport: the server URL (mutually exclusive with `command`). */
|
|
432
438
|
url: z.string().url().optional(),
|
|
439
|
+
/**
|
|
440
|
+
* Remote auth (C.27c, url only): the NAME of a bearer credential, resolved
|
|
441
|
+
* solely from `~/.cruxy/credentials.json` (0600) as `Authorization: Bearer …`.
|
|
442
|
+
* This field holds a NAME, never the secret — a project config may name a
|
|
443
|
+
* credential it does not contain, so the value never lives in-repo. Resolution
|
|
444
|
+
* never consults the environment or any config file. Requires an `https` URL.
|
|
445
|
+
*/
|
|
446
|
+
credentialRef: z.string().min(1).optional(),
|
|
447
|
+
/**
|
|
448
|
+
* Remote auth (C.27c, url only): raw request headers sent to the endpoint.
|
|
449
|
+
* A LIVE header value is a secret, so this is accepted ONLY from user-scope
|
|
450
|
+
* config (`~/.cruxy/config.json`) — the loader REJECTS it from project-scope
|
|
451
|
+
* config (a cloned repo must not inject live headers). Prefer `credentialRef`.
|
|
452
|
+
* Requires an `https` URL; reserved framing headers cannot be overridden.
|
|
453
|
+
*/
|
|
454
|
+
headers: z.record(z.string(), z.string()).optional(),
|
|
433
455
|
})
|
|
434
456
|
.strict()
|
|
435
457
|
.refine((s) => Boolean(s.command) !== Boolean(s.url), {
|
|
436
458
|
message: "an MCP server needs exactly one of `command` (stdio) or `url`",
|
|
459
|
+
})
|
|
460
|
+
.refine((s) => !((s.credentialRef || s.headers) && s.command), {
|
|
461
|
+
message: "`credentialRef`/`headers` are for `url` servers only (stdio auth uses `env`)",
|
|
462
|
+
})
|
|
463
|
+
.refine((s) => !((s.credentialRef || s.headers) && s.url && !isHttps(s.url)), {
|
|
464
|
+
message: "a url server with `credentialRef`/`headers` must use https — a credential is " +
|
|
465
|
+
"NEVER sent over http (including loopback); there is no plaintext dev carve-out",
|
|
437
466
|
});
|
|
467
|
+
/** Whether a URL string parses as https (a credential is only ever sent over TLS). */
|
|
468
|
+
function isHttps(url) {
|
|
469
|
+
try {
|
|
470
|
+
return new URL(url).protocol === "https:";
|
|
471
|
+
}
|
|
472
|
+
catch {
|
|
473
|
+
return false; // an invalid URL is rejected by `z.string().url()` upstream
|
|
474
|
+
}
|
|
475
|
+
}
|
|
438
476
|
/**
|
|
439
477
|
* MCP client integration (C.27): connect to trusted MCP servers and expose their
|
|
440
478
|
* tools to the agent. OFF by default — like the sandbox, LSP, and hooks, it runs
|
|
@@ -303,6 +303,35 @@ export declare function mcpUntrusted(root: string, servers: string[]): CruxyErro
|
|
|
303
303
|
* gag-scrubbed (U.8) before it reaches the user-facing cause.
|
|
304
304
|
*/
|
|
305
305
|
export declare function mcpConnect(server: string, underlying?: unknown): CruxyError;
|
|
306
|
+
/**
|
|
307
|
+
* A network (`url`) MCP server was REFUSED for a security reason (SSRF address
|
|
308
|
+
* block, a non-https/non-loopback scheme, or an endpoint redirect) — distinct
|
|
309
|
+
* from a transient connect failure. `reason` is our own guard's message (not
|
|
310
|
+
* server-controlled), but it is still scrubbed for symmetry with `mcpConnect`.
|
|
311
|
+
*/
|
|
312
|
+
export declare function mcpBlocked(server: string, reason: string): CruxyError;
|
|
313
|
+
/**
|
|
314
|
+
* A network (`url`) MCP server's CREDENTIAL failed (C.27c) — either the endpoint
|
|
315
|
+
* REJECTED it (401/403) or the configured `credentialRef` names no token in the
|
|
316
|
+
* `~/.cruxy` store. Distinct from `mcpConnect` (the server was reachable) and from
|
|
317
|
+
* `mcpBlocked` (a security refusal before connect). The token is NEVER included in
|
|
318
|
+
* this error — only the server name and the credential NAME appear.
|
|
319
|
+
*/
|
|
320
|
+
export declare function mcpAuth(server: string, detail: {
|
|
321
|
+
kind: "rejected";
|
|
322
|
+
status: number;
|
|
323
|
+
} | {
|
|
324
|
+
kind: "missing";
|
|
325
|
+
ref: string;
|
|
326
|
+
}): CruxyError;
|
|
327
|
+
/**
|
|
328
|
+
* A project-scope config tried to set raw `headers` on an MCP server (C.27c). A
|
|
329
|
+
* live header value is a secret, and project scope is a possibly-cloned repo, so
|
|
330
|
+
* this is REFUSED at load — never silently dropped (a silent drop would let a repo
|
|
331
|
+
* believe auth is configured when it is not). The repo may only NAME a credential
|
|
332
|
+
* via `credentialRef`; the value lives solely in the user's `~/.cruxy` store.
|
|
333
|
+
*/
|
|
334
|
+
export declare function mcpProjectHeaders(server: string, file: string): CruxyError;
|
|
306
335
|
/**
|
|
307
336
|
* `web.enabled` is on but no usable search provider is configured — the API-key
|
|
308
337
|
* environment variable is unset (or the provider is unknown). THE HONESTY RULE:
|
|
@@ -1034,6 +1034,75 @@ export function mcpConnect(server, underlying) {
|
|
|
1034
1034
|
underlying,
|
|
1035
1035
|
});
|
|
1036
1036
|
}
|
|
1037
|
+
/**
|
|
1038
|
+
* A network (`url`) MCP server was REFUSED for a security reason (SSRF address
|
|
1039
|
+
* block, a non-https/non-loopback scheme, or an endpoint redirect) — distinct
|
|
1040
|
+
* from a transient connect failure. `reason` is our own guard's message (not
|
|
1041
|
+
* server-controlled), but it is still scrubbed for symmetry with `mcpConnect`.
|
|
1042
|
+
*/
|
|
1043
|
+
export function mcpBlocked(server, reason) {
|
|
1044
|
+
return new CruxyError({
|
|
1045
|
+
code: ErrorCode.McpBlocked,
|
|
1046
|
+
title: `refused to connect to MCP server "${server}"`,
|
|
1047
|
+
cause: reason,
|
|
1048
|
+
nextSteps: [
|
|
1049
|
+
"use an `https://` URL that resolves to a public address",
|
|
1050
|
+
"for a local dev server use `http://` to a loopback host, or a `command` (stdio) server",
|
|
1051
|
+
],
|
|
1052
|
+
meta: { server },
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
/**
|
|
1056
|
+
* A network (`url`) MCP server's CREDENTIAL failed (C.27c) — either the endpoint
|
|
1057
|
+
* REJECTED it (401/403) or the configured `credentialRef` names no token in the
|
|
1058
|
+
* `~/.cruxy` store. Distinct from `mcpConnect` (the server was reachable) and from
|
|
1059
|
+
* `mcpBlocked` (a security refusal before connect). The token is NEVER included in
|
|
1060
|
+
* this error — only the server name and the credential NAME appear.
|
|
1061
|
+
*/
|
|
1062
|
+
export function mcpAuth(server, detail) {
|
|
1063
|
+
if (detail.kind === "rejected") {
|
|
1064
|
+
return new CruxyError({
|
|
1065
|
+
code: ErrorCode.McpAuth,
|
|
1066
|
+
title: `credential for MCP server "${server}" was rejected`,
|
|
1067
|
+
cause: `the endpoint returned ${detail.status} — the bearer credential is missing, wrong, or expired`,
|
|
1068
|
+
nextSteps: [
|
|
1069
|
+
`check the credential in \`~/.cruxy/credentials.json\` and re-set it with \`cruxy mcp login ${server}\``,
|
|
1070
|
+
"confirm the token is still valid on the server side",
|
|
1071
|
+
],
|
|
1072
|
+
meta: { server, status: detail.status },
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
return new CruxyError({
|
|
1076
|
+
code: ErrorCode.McpAuth,
|
|
1077
|
+
title: `MCP server "${server}" needs a credential that is not set`,
|
|
1078
|
+
cause: `no credential named "${detail.ref}" was found in \`~/.cruxy/credentials.json\``,
|
|
1079
|
+
nextSteps: [
|
|
1080
|
+
`set it with \`cruxy mcp login ${server}\` (stored owner-only in ~/.cruxy)`,
|
|
1081
|
+
"the token is never read from the repo, config, or the environment",
|
|
1082
|
+
],
|
|
1083
|
+
meta: { server, credentialRef: detail.ref },
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
/**
|
|
1087
|
+
* A project-scope config tried to set raw `headers` on an MCP server (C.27c). A
|
|
1088
|
+
* live header value is a secret, and project scope is a possibly-cloned repo, so
|
|
1089
|
+
* this is REFUSED at load — never silently dropped (a silent drop would let a repo
|
|
1090
|
+
* believe auth is configured when it is not). The repo may only NAME a credential
|
|
1091
|
+
* via `credentialRef`; the value lives solely in the user's `~/.cruxy` store.
|
|
1092
|
+
*/
|
|
1093
|
+
export function mcpProjectHeaders(server, file) {
|
|
1094
|
+
return new CruxyError({
|
|
1095
|
+
code: ErrorCode.McpConfig,
|
|
1096
|
+
title: `raw MCP \`headers\` are not allowed in project config`,
|
|
1097
|
+
cause: `server "${server}" in ${file} sets \`headers\` directly — a live header value ` +
|
|
1098
|
+
"is a secret, and a project config (a possibly-cloned repo) must never carry one",
|
|
1099
|
+
nextSteps: [
|
|
1100
|
+
`remove \`headers\` from server "${server}" and use \`credentialRef: "<name>"\` instead`,
|
|
1101
|
+
`store the token with \`cruxy mcp login ${server}\`, or set raw \`headers\` only in \`~/.cruxy/config.json\``,
|
|
1102
|
+
],
|
|
1103
|
+
meta: { server, file },
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1037
1106
|
// ── web search + fetch (exit 17) — C.20 ───────────────────────────────────────
|
|
1038
1107
|
/**
|
|
1039
1108
|
* `web.enabled` is on but no usable search provider is configured — the API-key
|
package/dist/errors/types.d.ts
CHANGED
|
@@ -110,6 +110,21 @@ export declare const ErrorCode: {
|
|
|
110
110
|
* or list its tools. Surfaced (that server contributes no tools) rather than
|
|
111
111
|
* silently swallowed; never fatal to the run. */
|
|
112
112
|
readonly McpConnect: "CRUXY_E_MCP_CONNECT";
|
|
113
|
+
/** A network (`url`) MCP server was REFUSED before/at connect for a SECURITY
|
|
114
|
+
* reason, distinct from an ordinary connect failure: a non-https(non-loopback)
|
|
115
|
+
* scheme, a host that resolves into a private/loopback/link-local range (SSRF),
|
|
116
|
+
* or an endpoint that tried to redirect. A refusal, not a transient error. */
|
|
117
|
+
readonly McpBlocked: "CRUXY_E_MCP_BLOCKED";
|
|
118
|
+
/** A network (`url`) MCP server's CREDENTIAL failed (C.27c): the endpoint
|
|
119
|
+
* rejected it (401/403) or the named `credentialRef` has no token in the
|
|
120
|
+
* `~/.cruxy` store. Distinct from a transport connect failure — the server was
|
|
121
|
+
* reached (or the credential was simply absent), not merely unreachable. */
|
|
122
|
+
readonly McpAuth: "CRUXY_E_MCP_AUTH";
|
|
123
|
+
/** A project-scope config was REFUSED for a credential-safety reason (C.27c):
|
|
124
|
+
* it tried to set raw `headers` on an MCP server. A live header value is a
|
|
125
|
+
* secret; project scope (a possibly-cloned repo) may only NAME a credential via
|
|
126
|
+
* `credentialRef`, never carry the value. Rejected loudly, never silently. */
|
|
127
|
+
readonly McpConfig: "CRUXY_E_MCP_CONFIG";
|
|
113
128
|
/** `web.enabled` is on but no usable search provider is configured — the API
|
|
114
129
|
* key env var is unset or the provider is unknown. Actionable, NEVER a silent
|
|
115
130
|
* empty result: "no provider" must not read as "no search results". */
|
package/dist/errors/types.js
CHANGED
|
@@ -128,6 +128,21 @@ export const ErrorCode = {
|
|
|
128
128
|
* or list its tools. Surfaced (that server contributes no tools) rather than
|
|
129
129
|
* silently swallowed; never fatal to the run. */
|
|
130
130
|
McpConnect: "CRUXY_E_MCP_CONNECT",
|
|
131
|
+
/** A network (`url`) MCP server was REFUSED before/at connect for a SECURITY
|
|
132
|
+
* reason, distinct from an ordinary connect failure: a non-https(non-loopback)
|
|
133
|
+
* scheme, a host that resolves into a private/loopback/link-local range (SSRF),
|
|
134
|
+
* or an endpoint that tried to redirect. A refusal, not a transient error. */
|
|
135
|
+
McpBlocked: "CRUXY_E_MCP_BLOCKED",
|
|
136
|
+
/** A network (`url`) MCP server's CREDENTIAL failed (C.27c): the endpoint
|
|
137
|
+
* rejected it (401/403) or the named `credentialRef` has no token in the
|
|
138
|
+
* `~/.cruxy` store. Distinct from a transport connect failure — the server was
|
|
139
|
+
* reached (or the credential was simply absent), not merely unreachable. */
|
|
140
|
+
McpAuth: "CRUXY_E_MCP_AUTH",
|
|
141
|
+
/** A project-scope config was REFUSED for a credential-safety reason (C.27c):
|
|
142
|
+
* it tried to set raw `headers` on an MCP server. A live header value is a
|
|
143
|
+
* secret; project scope (a possibly-cloned repo) may only NAME a credential via
|
|
144
|
+
* `credentialRef`, never carry the value. Rejected loudly, never silently. */
|
|
145
|
+
McpConfig: "CRUXY_E_MCP_CONFIG",
|
|
131
146
|
// web search + fetch (exit 17) — C.20
|
|
132
147
|
/** `web.enabled` is on but no usable search provider is configured — the API
|
|
133
148
|
* key env var is unset or the provider is unknown. Actionable, NEVER a silent
|
|
@@ -293,6 +308,9 @@ const EXIT_CODES = {
|
|
|
293
308
|
// never fatal on its own. Grouped for a greppable exit code.
|
|
294
309
|
[ErrorCode.McpUntrusted]: 16,
|
|
295
310
|
[ErrorCode.McpConnect]: 16,
|
|
311
|
+
[ErrorCode.McpBlocked]: 16,
|
|
312
|
+
[ErrorCode.McpAuth]: 16,
|
|
313
|
+
[ErrorCode.McpConfig]: 16,
|
|
296
314
|
// Web search + fetch (C.20). A missing provider, a search/fetch failure, and an
|
|
297
315
|
// SSRF-blocked host all surface inside a tool result (the agent reads and
|
|
298
316
|
// adapts) and only exit the process if thrown directly. Grouped for a greppable
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { fetch as undiciFetch } from "undici";
|
|
2
|
+
import type { McpTransport } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* JSON-RPC 2.0 over MCP's Streamable HTTP transport (C.27b) — the network sibling
|
|
5
|
+
* of {@link ../mcp/transport McpStdioTransport}. It satisfies the SAME
|
|
6
|
+
* {@link McpTransport} seam, so `McpClient`, the single adapter (`mcpToolsFrom`),
|
|
7
|
+
* bounds, demarcation, gating, and non-persistence are all inherited UNCHANGED —
|
|
8
|
+
* every C.27 security property holds over the wire by construction, and all the
|
|
9
|
+
* network-specific hardening lives here, below the seam.
|
|
10
|
+
*
|
|
11
|
+
* Trust boundary (this is a remote endpoint, not a local process):
|
|
12
|
+
* - PINNED CONNECTIONS. The connection is pinned (undici `Agent` + {@link pinnedLookup})
|
|
13
|
+
* to the exact address set the SSRF guard already validated at trust time. The
|
|
14
|
+
* transport itself does NO DNS resolution — it dials only the passed addresses,
|
|
15
|
+
* so a DNS rebind cannot flip check→connect. The Host header / TLS SNI still
|
|
16
|
+
* carry the original hostname, so certificate hostname validation is unaffected.
|
|
17
|
+
* - HTTPS + CERT VALIDATION. TLS validation is undici's default and is NEVER
|
|
18
|
+
* disabled — there is deliberately no skip-verify path (JC-E). `https` vs
|
|
19
|
+
* loopback-`http` is enforced upstream by the url-guard.
|
|
20
|
+
* - NO REDIRECTS. `maxRedirections: 0` and a manual 3xx check: an MCP RPC endpoint
|
|
21
|
+
* has no legitimate reason to redirect, so a redirect is REFUSED (never re-pinned
|
|
22
|
+
* and followed to a new, unvalidated host).
|
|
23
|
+
* - NO SOCKET BEFORE TRUST (JC-C). The constructor opens no socket and resolves no
|
|
24
|
+
* DNS; the first network I/O is the `initialize` POST, which the service issues
|
|
25
|
+
* only after the trust gate passes.
|
|
26
|
+
*
|
|
27
|
+
* Inbound posture mirrors stdio: cruxy advertises NO capabilities (no sampling, no
|
|
28
|
+
* roots, no elicitation), so it never solicits server→client requests; any that
|
|
29
|
+
* arrive on a response stream are ignored, never acted on. This is a deliberate
|
|
30
|
+
* SUBSET of Streamable HTTP — no standalone GET listening stream and no SSE
|
|
31
|
+
* resumption — sufficient for request/response tool use and easy to reason about.
|
|
32
|
+
*/
|
|
33
|
+
export interface McpHttpSpec {
|
|
34
|
+
/** The server endpoint (already scheme/SSRF-validated by the url-guard). */
|
|
35
|
+
url: string;
|
|
36
|
+
/** Pre-validated address set to pin the connection to (never re-resolved here). */
|
|
37
|
+
addresses: string[];
|
|
38
|
+
/** `initialize` + `tools/list` budget, ms. */
|
|
39
|
+
connectTimeout: number;
|
|
40
|
+
/** Per `tools/call` budget, ms. */
|
|
41
|
+
requestTimeout: number;
|
|
42
|
+
/**
|
|
43
|
+
* Resolved auth headers (C.27c), attached to every request to THIS pinned,
|
|
44
|
+
* https endpoint (never on a redirect target, never over http). Empty/absent for
|
|
45
|
+
* an unauthenticated server. Reserved framing headers here are ignored — a
|
|
46
|
+
* credential can never rewrite content-type/accept/session framing.
|
|
47
|
+
*/
|
|
48
|
+
authHeaders?: Record<string, string>;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The endpoint rejected our credential (401/403) — C.27c. A DISTINCT type so the
|
|
52
|
+
* service maps it to `CRUXY_E_MCP_AUTH` ("credential rejected") rather than the
|
|
53
|
+
* generic connect failure. Carries only the status; the token never touches it.
|
|
54
|
+
*/
|
|
55
|
+
export declare class McpHttpAuthError extends Error {
|
|
56
|
+
readonly status: number;
|
|
57
|
+
constructor(status: number);
|
|
58
|
+
}
|
|
59
|
+
/** Minimal fetch surface we depend on (injectable for tests). */
|
|
60
|
+
type FetchLike = typeof undiciFetch;
|
|
61
|
+
export declare class McpHttpTransport implements McpTransport {
|
|
62
|
+
private readonly url;
|
|
63
|
+
private readonly connectTimeout;
|
|
64
|
+
private readonly requestTimeout;
|
|
65
|
+
private readonly dispatcher;
|
|
66
|
+
private readonly fetchImpl;
|
|
67
|
+
/** Sanitized auth headers, sent ONLY over https to this pinned endpoint. */
|
|
68
|
+
private readonly authHeaders;
|
|
69
|
+
private nextId;
|
|
70
|
+
private disposed;
|
|
71
|
+
/** Streamable HTTP session id, captured from the `initialize` response. */
|
|
72
|
+
private sessionId;
|
|
73
|
+
private crashHandler;
|
|
74
|
+
/** In-flight request aborts, so `dispose()` can cancel them. */
|
|
75
|
+
private readonly inflight;
|
|
76
|
+
constructor(spec: McpHttpSpec, fetchImpl?: FetchLike);
|
|
77
|
+
/** Whether the `initialize` handshake budget or the per-call budget applies. */
|
|
78
|
+
private budgetFor;
|
|
79
|
+
request(method: string, params: unknown, timeoutMs: number): Promise<unknown>;
|
|
80
|
+
notify(method: string, params: unknown): void;
|
|
81
|
+
onCrash(handler: (info: {
|
|
82
|
+
code: number | null;
|
|
83
|
+
signal: string | null;
|
|
84
|
+
}) => void): void;
|
|
85
|
+
dispose(): Promise<void>;
|
|
86
|
+
/** POST one framed JSON-RPC message; refuse redirects; reject non-2xx. */
|
|
87
|
+
private post;
|
|
88
|
+
}
|
|
89
|
+
export {};
|