@cruxy/cli 0.26.0 → 0.28.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 (40) hide show
  1. package/dist/cli/commands/mcp.js +106 -7
  2. package/dist/config/credentials.d.ts +9 -0
  3. package/dist/config/credentials.js +29 -1
  4. package/dist/config/manager.js +30 -3
  5. package/dist/config/schema.d.ts +182 -8
  6. package/dist/config/schema.js +43 -5
  7. package/dist/errors/constructors.d.ts +29 -0
  8. package/dist/errors/constructors.js +69 -0
  9. package/dist/errors/types.d.ts +15 -0
  10. package/dist/errors/types.js +18 -0
  11. package/dist/mcp/http-transport.d.ts +89 -0
  12. package/dist/mcp/http-transport.js +299 -0
  13. package/dist/mcp/index.d.ts +4 -2
  14. package/dist/mcp/index.js +3 -1
  15. package/dist/mcp/service.d.ts +19 -2
  16. package/dist/mcp/service.js +92 -20
  17. package/dist/mcp/trust-gate.d.ts +35 -11
  18. package/dist/mcp/trust-gate.js +87 -22
  19. package/dist/mcp/trust.d.ts +12 -2
  20. package/dist/mcp/trust.js +26 -2
  21. package/dist/mcp/types.d.ts +10 -0
  22. package/dist/mcp/url-guard.d.ts +48 -0
  23. package/dist/mcp/url-guard.js +62 -0
  24. package/dist/net/ip-guard.d.ts +55 -0
  25. package/dist/net/ip-guard.js +229 -0
  26. package/dist/render/index.d.ts +1 -0
  27. package/dist/render/index.js +1 -0
  28. package/dist/render/motion.d.ts +76 -0
  29. package/dist/render/motion.js +94 -0
  30. package/dist/render/tty-renderer.d.ts +17 -3
  31. package/dist/render/tty-renderer.js +58 -21
  32. package/dist/tools/file/apply-patch.js +12 -8
  33. package/dist/tools/file/edit-file.d.ts +0 -2
  34. package/dist/tools/file/edit-file.js +10 -19
  35. package/dist/tools/file/match.d.ts +43 -0
  36. package/dist/tools/file/match.js +127 -0
  37. package/dist/web/ssrf.d.ts +8 -22
  38. package/dist/web/ssrf.js +11 -183
  39. package/dist/web/types.d.ts +4 -2
  40. package/package.json +1 -1
@@ -1,9 +1,11 @@
1
1
  import path from "node:path";
2
2
  import { Command } from "commander";
3
- import { loadConfig } from "../../config/index.js";
4
- import { shouldUseColor } from "../../errors/index.js";
3
+ import { loadConfig, writeMcpCredential } from "../../config/index.js";
4
+ import { defaultOnboardingIO } from "../../onboarding/index.js";
5
+ import { interactiveRequired, shouldUseColor } from "../../errors/index.js";
5
6
  import { themeForColor } from "../../theme/index.js";
6
- import { fileMcpTrustStore, fingerprintMcpServers, isMcpTrusted, } from "../../mcp/index.js";
7
+ import { fileMcpTrustStore, fingerprintMcpServers, isMcpTrusted, resolveMcpEndpoints, } from "../../mcp/index.js";
8
+ import { BlockedHostError, defaultResolveHost, HostUnresolvedError, } from "../../net/ip-guard.js";
7
9
  import { logger } from "../../utils/logger.js";
8
10
  /**
9
11
  * `cruxy mcp` — inspect and control MCP server integration (C.27). `list` shows
@@ -36,11 +38,16 @@ export function mcpCommand() {
36
38
  for (const [name, cfg] of Object.entries(servers)) {
37
39
  logger.print(` ${t.strong(name)} ${t.muted(describeServer(cfg))}`);
38
40
  }
41
+ if (trusted && Object.values(servers).some((c) => c.url)) {
42
+ // JC-D: url servers are also bound to their resolved IP set, re-checked at
43
+ // connect; a changed IP set re-gates even though the static status is "trusted".
44
+ logger.print(t.muted("\n note: url servers are re-verified against their trusted IP set on connect"));
45
+ }
39
46
  });
40
47
  cmd
41
48
  .command("trust [path]")
42
49
  .description("trust this repo's MCP integrations (they run with your full privileges)")
43
- .action((target) => {
50
+ .action(async (target) => {
44
51
  const t = themeForColor(shouldUseColor(process.stdout));
45
52
  const { config } = loadConfig();
46
53
  const servers = config.mcp.servers;
@@ -50,17 +57,91 @@ export function mcpCommand() {
50
57
  logger.print(t.muted(`no MCP servers configured under ${root}`));
51
58
  return;
52
59
  }
53
- logger.print(t.danger(t.strong(`trusting ${names.length} MCP server${names.length === 1 ? "" : "s"} — these run their own code UNSANDBOXED with your privileges:`)));
60
+ const hasStdio = Object.values(servers).some((c) => c.command);
61
+ const hasUrl = Object.values(servers).some((c) => c.url);
62
+ logger.print(t.danger(t.strong(`trusting ${names.length} MCP server${names.length === 1 ? "" : "s"} for ${root}:`)));
54
63
  for (const [name, cfg] of Object.entries(servers)) {
55
64
  logger.print(` ${t.strong(name)} ${t.muted(describeServer(cfg))}`);
56
65
  }
66
+ if (hasStdio) {
67
+ logger.print(t.muted(" stdio servers run their code UNSANDBOXED with your full privileges."));
68
+ }
69
+ if (hasUrl) {
70
+ logger.print(t.muted(" url servers receive your tool arguments over the network; trust binds\n" +
71
+ " the URL + its current IP set (weaker than a local binary — see docs)."));
72
+ // C.27c: a server that also RECEIVES a credential is a bigger grant —
73
+ // name it so the grant is explicit at trust time.
74
+ for (const [name, cfg] of Object.entries(servers)) {
75
+ if (!cfg.credentialRef && !cfg.headers)
76
+ continue;
77
+ const what = cfg.credentialRef
78
+ ? `your "${cfg.credentialRef}" credential`
79
+ : "your configured auth headers";
80
+ logger.print(t.danger(` "${name}" will RECEIVE ${what} on every request.`));
81
+ }
82
+ }
83
+ // JC-D: capture + SSRF-validate each url server's endpoint set and bind it
84
+ // into the decision. A refused URL (SSRF/scheme) fails closed — no record.
85
+ let endpoints = {};
86
+ try {
87
+ endpoints = await resolveMcpEndpoints(servers, defaultResolveHost);
88
+ }
89
+ catch (err) {
90
+ if (err instanceof BlockedHostError ||
91
+ err instanceof HostUnresolvedError) {
92
+ logger.print(t.danger(`refused to trust — a url server could not be validated: ${err.message}`));
93
+ return;
94
+ }
95
+ throw err;
96
+ }
57
97
  fileMcpTrustStore().record({
58
98
  root,
59
99
  fingerprint: fingerprintMcpServers(servers),
60
100
  at: new Date().toISOString(),
101
+ endpoints,
61
102
  });
62
103
  logger.print(`${t.success("trusted")} — cruxy will connect these servers for ${root}. ` +
63
- t.muted("changing the config will require re-trusting."));
104
+ t.muted("changing the config (or a url's IP set) requires re-trusting."));
105
+ });
106
+ cmd
107
+ .command("login <server>")
108
+ .description("set a bearer credential for a remote MCP integration (owner-only in ~/.cruxy)")
109
+ .action(async (server) => {
110
+ const t = themeForColor(shouldUseColor(process.stdout));
111
+ const { config } = loadConfig();
112
+ const cfg = config.mcp.servers[server];
113
+ if (!cfg) {
114
+ logger.print(t.danger(`no MCP server "${server}" is configured (mcp.servers)`));
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+ if (!cfg.url) {
119
+ logger.print(t.danger(`server "${server}" is a stdio server — it has no bearer credential ` +
120
+ "(stdio servers take secrets via `env`)"));
121
+ process.exitCode = 1;
122
+ return;
123
+ }
124
+ // The store is keyed by the credential NAME the config references; default
125
+ // to the server id when the config names none.
126
+ const ref = cfg.credentialRef ?? server;
127
+ if (!process.stdin.isTTY) {
128
+ // Never read a secret from a pipe silently — same discipline as onboarding.
129
+ throw interactiveRequired(`a credential for MCP server "${server}"`, [
130
+ "run this in an interactive terminal",
131
+ ]);
132
+ }
133
+ const io = defaultOnboardingIO(shouldUseColor(process.stderr));
134
+ io.write(`${t.strong(`credential for "${server}"`)} ${t.muted(`(stored as "${ref}" in ~/.cruxy/credentials.json, 0600)`)}\n`);
135
+ io.write(`${t.muted("paste the bearer token (input hidden): ")}`);
136
+ const token = (await io.readSecret()).trim();
137
+ if (!token) {
138
+ logger.print(t.warning("no token entered — nothing was stored"));
139
+ process.exitCode = 1;
140
+ return;
141
+ }
142
+ writeMcpCredential(ref, token);
143
+ logger.print(`${t.success("stored")} — cruxy will send this credential to "${server}" over https only. ` +
144
+ t.muted("re-run to rotate; the value is never shown or logged."));
64
145
  });
65
146
  cmd
66
147
  .command("untrust [path]")
@@ -83,5 +164,23 @@ function describeServer(cfg) {
83
164
  if (cfg.command) {
84
165
  return `(stdio: ${[cfg.command, ...(cfg.args ?? [])].join(" ")})`;
85
166
  }
86
- return cfg.url ? `(url: ${cfg.url})` : "(no transport)";
167
+ if (!cfg.url)
168
+ return "(no transport)";
169
+ return `(url: ${cfg.url}${describeAuth(cfg)})`;
170
+ }
171
+ /**
172
+ * A REDACTED auth descriptor for display (C.27c) — names the credential and
173
+ * counts raw headers, but NEVER prints a header value or a token. Display
174
+ * redaction is defense-in-depth; the load-bearing guard is that project config
175
+ * can't carry a value at all.
176
+ */
177
+ function describeAuth(cfg) {
178
+ const parts = [];
179
+ if (cfg.credentialRef)
180
+ parts.push(`credential: ${cfg.credentialRef}`);
181
+ const headerCount = cfg.headers ? Object.keys(cfg.headers).length : 0;
182
+ if (headerCount > 0) {
183
+ parts.push(`${headerCount} header${headerCount === 1 ? "" : "s"} (redacted)`);
184
+ }
185
+ return parts.length ? `, ${parts.join(", ")}` : "";
87
186
  }
@@ -2,6 +2,15 @@
2
2
  export declare function credentialsPath(): string;
3
3
  /** The stored key for `provider`, or `undefined`. Never throws. */
4
4
  export declare function readCredential(provider: string, file?: string): string | undefined;
5
+ /**
6
+ * The stored MCP bearer token for credential name `ref`, or `undefined` (C.27c).
7
+ * Reads ONLY this owner-only store — never the environment, never any config file
8
+ * — so a project config that merely NAMES a credential can never widen where the
9
+ * secret is sourced from. Never throws.
10
+ */
11
+ export declare function readMcpCredential(ref: string, file?: string): string | undefined;
12
+ /** Persist MCP bearer token for credential name `ref`. Same 0600/0700 as keys. */
13
+ export declare function writeMcpCredential(ref: string, token: string, file?: string): void;
5
14
  /**
6
15
  * Persist `key` for `provider`, merging into any existing store. The file is
7
16
  * written `0600` and its directory `0700` so the secret is owner-only — enforced
@@ -39,12 +39,40 @@ export function readCredential(provider, file = credentialsPath()) {
39
39
  const key = store?.keys[provider];
40
40
  return typeof key === "string" && key !== "" ? key : undefined;
41
41
  }
42
+ /**
43
+ * The stored MCP bearer token for credential name `ref`, or `undefined` (C.27c).
44
+ * Reads ONLY this owner-only store — never the environment, never any config file
45
+ * — so a project config that merely NAMES a credential can never widen where the
46
+ * secret is sourced from. Never throws.
47
+ */
48
+ export function readMcpCredential(ref, file = credentialsPath()) {
49
+ const store = readStore(file);
50
+ const token = store?.mcp?.[ref];
51
+ return typeof token === "string" && token !== "" ? token : undefined;
52
+ }
53
+ /** Persist MCP bearer token for credential name `ref`. Same 0600/0700 as keys. */
54
+ export function writeMcpCredential(ref, token, file = credentialsPath()) {
55
+ writeInto(file, (store) => {
56
+ store.mcp ??= {};
57
+ store.mcp[ref] = token;
58
+ });
59
+ }
42
60
  /**
43
61
  * Persist `key` for `provider`, merging into any existing store. The file is
44
62
  * written `0600` and its directory `0700` so the secret is owner-only — enforced
45
63
  * with an explicit `chmod` after write (mkdir/write modes are umask-masked).
46
64
  */
47
65
  export function writeCredential(provider, key, file = credentialsPath()) {
66
+ writeInto(file, (store) => {
67
+ store.keys[provider] = key;
68
+ });
69
+ }
70
+ /**
71
+ * Merge `mutate` into the store and persist it owner-only: dir `0700`, file
72
+ * `0600`, enforced with an explicit `chmod` after write (mkdir/write modes are
73
+ * umask-masked). The one write path shared by every credential namespace.
74
+ */
75
+ function writeInto(file, mutate) {
48
76
  const dir = dirname(file);
49
77
  mkdirSync(dir, { recursive: true, mode: 0o700 });
50
78
  try {
@@ -55,7 +83,7 @@ export function writeCredential(provider, key, file = credentialsPath()) {
55
83
  }
56
84
  const store = readStore(file) ?? { version: CREDENTIALS_VERSION, keys: {} };
57
85
  store.version = CREDENTIALS_VERSION;
58
- store.keys[provider] = key;
86
+ mutate(store);
59
87
  writeFileSync(file, JSON.stringify(store, null, 2) + "\n", {
60
88
  encoding: "utf8",
61
89
  mode: 0o600,
@@ -1,6 +1,6 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
- import { configInvalid, configParse } from "../errors/index.js";
3
+ import { configInvalid, configParse, mcpProjectHeaders, } from "../errors/index.js";
4
4
  import { CruxyConfigSchema } from "./schema.js";
5
5
  import { globalConfigPath, findProjectConfig } from "./paths.js";
6
6
  import { readCredential } from "./credentials.js";
@@ -36,6 +36,29 @@ function readJsonFile(path) {
36
36
  throw err;
37
37
  }
38
38
  }
39
+ /**
40
+ * §0 credential-safety gate (C.27c): a project-scope config file must not set raw
41
+ * `mcp.servers.<id>.headers`. A live header value is a secret, and this file is a
42
+ * possibly-cloned repo, so a raw header here is REFUSED — loudly, naming the
43
+ * server — never merged and never silently dropped (a silent drop would let the
44
+ * repo believe auth is wired up when it is not). Only user-scope config
45
+ * (`~/.cruxy/config.json`) may carry raw headers; every other file (a discovered
46
+ * project config, or an explicit `--config` that may point into a repo) is
47
+ * project scope. A project config may still NAME a credential via `credentialRef`.
48
+ */
49
+ function rejectProjectScopeHeaders(obj, file) {
50
+ const mcp = obj.mcp;
51
+ if (!isPlainObject(mcp))
52
+ return;
53
+ const servers = mcp.servers;
54
+ if (!isPlainObject(servers))
55
+ return;
56
+ for (const [id, server] of Object.entries(servers)) {
57
+ if (isPlainObject(server) && "headers" in server) {
58
+ throw mcpProjectHeaders(id, file);
59
+ }
60
+ }
61
+ }
39
62
  /** Overrides sourced from environment variables. */
40
63
  function envOverrides() {
41
64
  const out = {};
@@ -67,13 +90,17 @@ export function loadConfig(opts = {}) {
67
90
  sources.global = gPath;
68
91
  }
69
92
  if (opts.configPath) {
70
- merged = deepMerge(merged, readJsonFile(opts.configPath));
93
+ const obj = readJsonFile(opts.configPath);
94
+ rejectProjectScopeHeaders(obj, opts.configPath);
95
+ merged = deepMerge(merged, obj);
71
96
  sources.explicit = opts.configPath;
72
97
  }
73
98
  else {
74
99
  const pPath = findProjectConfig(opts.cwd);
75
100
  if (pPath) {
76
- merged = deepMerge(merged, readJsonFile(pPath));
101
+ const obj = readJsonFile(pPath);
102
+ rejectProjectScopeHeaders(obj, pPath);
103
+ merged = deepMerge(merged, obj);
77
104
  sources.project = pPath;
78
105
  }
79
106
  }
@@ -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 (C.27). A `command` (+ optional `args`/`env`) is a stdio
653
- * server cruxy spawns as a child process; a `url` names a remote server. Exactly
654
- * one transport must be given. Trusting a stdio server runs its code UNSANDBOXED
655
- * with your privileges, which is why connection is gated by an explicit,
656
- * fingerprinted trust decision (see `mcp/trust.ts`).
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;