@hasna/mcps 0.0.14 → 0.0.16

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/index.js CHANGED
@@ -9547,6 +9547,7 @@ function getDb() {
9547
9547
  command TEXT NOT NULL,
9548
9548
  args TEXT NOT NULL DEFAULT '[]',
9549
9549
  env TEXT NOT NULL DEFAULT '{}',
9550
+ credential_refs TEXT NOT NULL DEFAULT '{}',
9550
9551
  transport TEXT NOT NULL DEFAULT 'stdio',
9551
9552
  url TEXT,
9552
9553
  source TEXT NOT NULL DEFAULT 'local',
@@ -9573,6 +9574,9 @@ function getDb() {
9573
9574
  try {
9574
9575
  db.exec("ALTER TABLE servers ADD COLUMN last_error TEXT");
9575
9576
  } catch {}
9577
+ try {
9578
+ db.exec("ALTER TABLE servers ADD COLUMN credential_refs TEXT NOT NULL DEFAULT '{}'");
9579
+ } catch {}
9576
9580
  db.exec(`
9577
9581
  CREATE TABLE IF NOT EXISTS sources (
9578
9582
  id TEXT PRIMARY KEY,
@@ -16627,17 +16631,17 @@ __export(exports_sources, {
16627
16631
  clearCache: () => clearCache,
16628
16632
  addSource: () => addSource
16629
16633
  });
16630
- import { mkdirSync as mkdirSync6, existsSync as existsSync6, readFileSync as readFileSync2, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
16631
- import { join as join7 } from "path";
16634
+ import { mkdirSync as mkdirSync6, existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync3, unlinkSync } from "fs";
16635
+ import { join as join8 } from "path";
16632
16636
  function getCacheFile(sourceId) {
16633
- return join7(CACHE_DIR, `${sourceId}.json`);
16637
+ return join8(CACHE_DIR, `${sourceId}.json`);
16634
16638
  }
16635
16639
  function readCache(sourceId) {
16636
16640
  try {
16637
16641
  const file = getCacheFile(sourceId);
16638
- if (!existsSync6(file))
16642
+ if (!existsSync7(file))
16639
16643
  return null;
16640
- const data = JSON.parse(readFileSync2(file, "utf-8"));
16644
+ const data = JSON.parse(readFileSync3(file, "utf-8"));
16641
16645
  return data;
16642
16646
  } catch {
16643
16647
  return null;
@@ -16651,7 +16655,7 @@ function writeCache(sourceId, results) {
16651
16655
  }
16652
16656
  function clearCache(sourceId) {
16653
16657
  try {
16654
- if (!existsSync6(CACHE_DIR))
16658
+ if (!existsSync7(CACHE_DIR))
16655
16659
  return;
16656
16660
  const files = readdirSync3(CACHE_DIR);
16657
16661
  for (const file of files) {
@@ -16659,7 +16663,7 @@ function clearCache(sourceId) {
16659
16663
  continue;
16660
16664
  if (!sourceId || file.startsWith(`${sourceId}.`)) {
16661
16665
  try {
16662
- unlinkSync(join7(CACHE_DIR, file));
16666
+ unlinkSync(join8(CACHE_DIR, file));
16663
16667
  } catch {}
16664
16668
  }
16665
16669
  }
@@ -16905,12 +16909,166 @@ var CACHE_DIR, DEFAULT_TTL_MS;
16905
16909
  var init_sources = __esm(() => {
16906
16910
  init_db();
16907
16911
  init_config2();
16908
- CACHE_DIR = join7(MCPS_DIR, "cache");
16912
+ CACHE_DIR = join8(MCPS_DIR, "cache");
16909
16913
  DEFAULT_TTL_MS = 10 * 60 * 1000;
16910
16914
  });
16911
16915
 
16912
16916
  // src/lib/registry.ts
16913
16917
  init_db();
16918
+
16919
+ // src/lib/credentials.ts
16920
+ init_config2();
16921
+ import { existsSync as existsSync6, readFileSync as readFileSync2 } from "fs";
16922
+ import { join as join7 } from "path";
16923
+
16924
+ class CredentialReferenceError extends Error {
16925
+ constructor(message) {
16926
+ super(message);
16927
+ this.name = "CredentialReferenceError";
16928
+ }
16929
+ }
16930
+ var SECRET_KEY_PATTERN = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
16931
+ var SECRET_VALUE_PATTERN = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
16932
+ var REDACTED_CREDENTIAL_VALUE = "<redacted>";
16933
+ function normalizeKey(key) {
16934
+ return key.trim();
16935
+ }
16936
+ function isRecord(value) {
16937
+ return typeof value === "object" && value !== null && !Array.isArray(value);
16938
+ }
16939
+ function isSecretLikeEnvKey(key) {
16940
+ return SECRET_KEY_PATTERN.test(key);
16941
+ }
16942
+ function isSecretLikeValue(value) {
16943
+ return SECRET_VALUE_PATTERN.test(value.trim());
16944
+ }
16945
+ function normalizeCredentialRef(ref) {
16946
+ const source = ref.source;
16947
+ if (source !== "env" && source !== "local-vault" && source !== "hosted") {
16948
+ throw new CredentialReferenceError(`Unsupported credential reference source: ${String(source)}`);
16949
+ }
16950
+ const name = ref.name?.trim();
16951
+ if (!name) {
16952
+ throw new CredentialReferenceError("Credential reference name is required");
16953
+ }
16954
+ return {
16955
+ source,
16956
+ name,
16957
+ required: ref.required !== false,
16958
+ ...ref.description ? { description: ref.description } : {}
16959
+ };
16960
+ }
16961
+ function normalizeCredentialRefs(refs) {
16962
+ const normalized = {};
16963
+ for (const [rawKey, ref] of Object.entries(refs ?? {})) {
16964
+ const key = normalizeKey(rawKey);
16965
+ if (!key)
16966
+ throw new CredentialReferenceError("Credential reference env key is required");
16967
+ normalized[key] = normalizeCredentialRef(ref);
16968
+ }
16969
+ return normalized;
16970
+ }
16971
+ function parseCredentialRefs(value) {
16972
+ if (!isRecord(value))
16973
+ return {};
16974
+ const refs = {};
16975
+ for (const [key, ref] of Object.entries(value)) {
16976
+ if (!isRecord(ref))
16977
+ continue;
16978
+ const source = ref.source;
16979
+ const name = ref.name;
16980
+ if ((source === "env" || source === "local-vault" || source === "hosted") && typeof name === "string" && name.trim()) {
16981
+ refs[key] = normalizeCredentialRef({
16982
+ source,
16983
+ name,
16984
+ required: typeof ref.required === "boolean" ? ref.required : true,
16985
+ description: typeof ref.description === "string" ? ref.description : undefined
16986
+ });
16987
+ }
16988
+ }
16989
+ return refs;
16990
+ }
16991
+ function normalizeLiteralEnv(env) {
16992
+ const normalized = {};
16993
+ for (const [rawKey, rawValue] of Object.entries(env ?? {})) {
16994
+ const key = normalizeKey(rawKey);
16995
+ if (!key)
16996
+ continue;
16997
+ const value = String(rawValue);
16998
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
16999
+ throw new CredentialReferenceError(`Refusing to store raw secret-like env value for "${key}". Use a credential reference instead.`);
17000
+ }
17001
+ normalized[key] = value;
17002
+ }
17003
+ return normalized;
17004
+ }
17005
+ function redactEnv(env) {
17006
+ const redacted = {};
17007
+ for (const [key, value] of Object.entries(env)) {
17008
+ redacted[key] = isSecretLikeEnvKey(key) || isSecretLikeValue(value) ? REDACTED_CREDENTIAL_VALUE : value;
17009
+ }
17010
+ return redacted;
17011
+ }
17012
+ function redactServerCredentials(server) {
17013
+ return {
17014
+ ...server,
17015
+ env: redactEnv(server.env),
17016
+ credentialRefs: normalizeCredentialRefs(server.credentialRefs)
17017
+ };
17018
+ }
17019
+ function readLocalVault() {
17020
+ const path = process.env.HASNA_MCPS_CREDENTIAL_VAULT_PATH ?? join7(MCPS_DIR, "credentials.local.json");
17021
+ if (!existsSync6(path))
17022
+ return {};
17023
+ const parsed = JSON.parse(readFileSync2(path, "utf-8"));
17024
+ if (!isRecord(parsed))
17025
+ return {};
17026
+ const values = {};
17027
+ for (const [key, value] of Object.entries(parsed)) {
17028
+ if (typeof value === "string")
17029
+ values[key] = value;
17030
+ }
17031
+ return values;
17032
+ }
17033
+ function resolveCredentialRef(envKey, ref) {
17034
+ if (ref.source === "env") {
17035
+ const value = process.env[ref.name];
17036
+ if (value === undefined && ref.required !== false) {
17037
+ throw new CredentialReferenceError(`Missing required environment credential "${ref.name}" for "${envKey}"`);
17038
+ }
17039
+ return value;
17040
+ }
17041
+ if (ref.source === "local-vault") {
17042
+ const value = readLocalVault()[ref.name];
17043
+ if (value === undefined && ref.required !== false) {
17044
+ throw new CredentialReferenceError(`Missing required local vault credential "${ref.name}" for "${envKey}"`);
17045
+ }
17046
+ return value;
17047
+ }
17048
+ if (ref.required !== false) {
17049
+ throw new CredentialReferenceError(`Hosted credential "${ref.name}" for "${envKey}" cannot be resolved by the local runtime`);
17050
+ }
17051
+ return;
17052
+ }
17053
+ function resolveServerEnv(server) {
17054
+ const resolved = { ...server.env };
17055
+ const refs = normalizeCredentialRefs(server.credentialRefs);
17056
+ for (const [envKey, ref] of Object.entries(refs)) {
17057
+ const value = resolveCredentialRef(envKey, ref);
17058
+ if (value !== undefined)
17059
+ resolved[envKey] = value;
17060
+ }
17061
+ return resolved;
17062
+ }
17063
+ function credentialRefPlaceholders(refs) {
17064
+ const placeholders = {};
17065
+ for (const key of Object.keys(refs ?? {})) {
17066
+ placeholders[key] = REDACTED_CREDENTIAL_VALUE;
17067
+ }
17068
+ return placeholders;
17069
+ }
17070
+
17071
+ // src/lib/registry.ts
16914
17072
  function parseRow(row) {
16915
17073
  return {
16916
17074
  id: row.id,
@@ -16919,6 +17077,7 @@ function parseRow(row) {
16919
17077
  command: row.command,
16920
17078
  args: safeJsonParse(row.args, []),
16921
17079
  env: safeJsonParse(row.env, {}),
17080
+ credentialRefs: parseCredentialRefs(safeJsonParse(row.credential_refs, {})),
16922
17081
  transport: row.transport,
16923
17082
  url: row.url || null,
16924
17083
  source: row.source,
@@ -16986,9 +17145,9 @@ function addServer(opts) {
16986
17145
  if (!id) {
16987
17146
  throw new Error("Unable to generate a valid server ID");
16988
17147
  }
16989
- const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, transport, url, source)
16990
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
16991
- RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(opts.env || {}), opts.transport || "stdio", opts.url || null, opts.source || "local");
17148
+ const row = db2.prepare(`INSERT INTO servers (id, name, description, command, args, env, credential_refs, transport, url, source)
17149
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
17150
+ RETURNING *`).get(id, name, opts.description || null, command, JSON.stringify(opts.args || []), JSON.stringify(normalizeLiteralEnv(opts.env)), JSON.stringify(normalizeCredentialRefs(opts.credentialRefs)), opts.transport || "stdio", opts.url || null, opts.source || "local");
16992
17151
  return parseRow(row);
16993
17152
  }
16994
17153
  function removeServer(id) {
@@ -17027,7 +17186,11 @@ function updateServer(id, updates) {
17027
17186
  }
17028
17187
  if (updates.env !== undefined) {
17029
17188
  sets.push("env = ?");
17030
- values.push(JSON.stringify(updates.env));
17189
+ values.push(JSON.stringify(normalizeLiteralEnv(updates.env)));
17190
+ }
17191
+ if (updates.credentialRefs !== undefined) {
17192
+ sets.push("credential_refs = ?");
17193
+ values.push(JSON.stringify(normalizeCredentialRefs(updates.credentialRefs)));
17031
17194
  }
17032
17195
  if (updates.transport !== undefined) {
17033
17196
  sets.push("transport = ?");
@@ -17061,7 +17224,7 @@ function setServerEnv(id, key, value) {
17061
17224
  if (!server)
17062
17225
  throw new Error(`Server "${id}" not found`);
17063
17226
  const env = { ...server.env, [key]: value };
17064
- db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
17227
+ db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeLiteralEnv(env)), id);
17065
17228
  }
17066
17229
  function unsetServerEnv(id, key) {
17067
17230
  const db2 = getDb();
@@ -17072,6 +17235,23 @@ function unsetServerEnv(id, key) {
17072
17235
  delete env[key];
17073
17236
  db2.prepare("UPDATE servers SET env = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(env), id);
17074
17237
  }
17238
+ function setServerCredentialRef(id, key, ref) {
17239
+ const db2 = getDb();
17240
+ const server = getServer(id);
17241
+ if (!server)
17242
+ throw new Error(`Server "${id}" not found`);
17243
+ const credentialRefs = normalizeCredentialRefs({ ...server.credentialRefs ?? {}, [key]: ref });
17244
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(credentialRefs), id);
17245
+ }
17246
+ function unsetServerCredentialRef(id, key) {
17247
+ const db2 = getDb();
17248
+ const server = getServer(id);
17249
+ if (!server)
17250
+ throw new Error(`Server "${id}" not found`);
17251
+ const credentialRefs = { ...server.credentialRefs ?? {} };
17252
+ delete credentialRefs[key];
17253
+ db2.prepare("UPDATE servers SET credential_refs = ?, updated_at = datetime('now') WHERE id = ?").run(JSON.stringify(normalizeCredentialRefs(credentialRefs)), id);
17254
+ }
17075
17255
  function cacheTools(serverId, tools) {
17076
17256
  const db2 = getDb();
17077
17257
  const insert = db2.prepare("INSERT INTO tool_cache (server_id, name, description, input_schema) VALUES (?, ?, ?, ?)");
@@ -17111,6 +17291,7 @@ function cloneServer(id, newName) {
17111
17291
  command: server.command,
17112
17292
  args: server.args,
17113
17293
  env: server.env,
17294
+ credentialRefs: server.credentialRefs,
17114
17295
  transport: server.transport,
17115
17296
  url: server.url ?? undefined,
17116
17297
  source: server.source
@@ -25242,6 +25423,185 @@ class StreamableHTTPClientTransport {
25242
25423
  // src/lib/proxy.ts
25243
25424
  init_config2();
25244
25425
  init_db();
25426
+
25427
+ // src/lib/local-command-consent.ts
25428
+ class LocalCommandConsentError extends Error {
25429
+ review;
25430
+ constructor(message, review) {
25431
+ super(message);
25432
+ this.name = "LocalCommandConsentError";
25433
+ this.review = review;
25434
+ }
25435
+ }
25436
+ var SHELL_COMMANDS = new Set(["bash", "sh", "zsh", "fish", "cmd", "cmd.exe", "powershell", "powershell.exe", "pwsh"]);
25437
+ var DESTRUCTIVE_COMMANDS = new Set([
25438
+ "rm",
25439
+ "dd",
25440
+ "mkfs",
25441
+ "shutdown",
25442
+ "reboot",
25443
+ "poweroff",
25444
+ "halt",
25445
+ "killall",
25446
+ "pkill"
25447
+ ]);
25448
+ var SHELL_EVAL_FLAGS = new Set(["-c", "/c", "-Command", "-command", "-EncodedCommand", "-encodedcommand"]);
25449
+ var SECRET_FLAG_PATTERN = /(?:^|[-_])(api[-_]?key|token|secret|password|passwd|credential|auth|private[-_]?key)(?:$|[-_])/i;
25450
+ var SECRET_KEY_PATTERN2 = /(?:^|[_-])(api[_-]?key|token|secret|password|passwd|credential|auth|private[_-]?key)(?:$|[_-])/i;
25451
+ var SECRET_VALUE_PATTERN2 = /^(sk_(?:live|test)_[A-Za-z0-9_]+|ghp_[A-Za-z0-9_]+|github_pat_[A-Za-z0-9_]+|xox[baprs]-[A-Za-z0-9-]+|AIza[A-Za-z0-9_-]{20,}|eyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+)$/;
25452
+ var SHELL_META_PATTERN = /[;&|`<>]|\$\(/;
25453
+ function commandBase(command) {
25454
+ return command.trim().split(/[\\/]/).pop()?.toLowerCase() || command.trim().toLowerCase();
25455
+ }
25456
+ function normalizeArgs(args) {
25457
+ return (args ?? []).map((arg) => String(arg));
25458
+ }
25459
+ function isSecretKey(key) {
25460
+ return SECRET_KEY_PATTERN2.test(key) || SECRET_FLAG_PATTERN.test(key);
25461
+ }
25462
+ function isSecretValue(value) {
25463
+ return SECRET_VALUE_PATTERN2.test(value.trim());
25464
+ }
25465
+ function isSecretAssignment(arg) {
25466
+ const eqIdx = arg.indexOf("=");
25467
+ if (eqIdx <= 0)
25468
+ return false;
25469
+ const key = arg.slice(0, eqIdx);
25470
+ const value = arg.slice(eqIdx + 1);
25471
+ return isSecretKey(key) || isSecretValue(value);
25472
+ }
25473
+ function isSecretArg(args, index) {
25474
+ const arg = args[index] ?? "";
25475
+ const previous = args[index - 1] ?? "";
25476
+ return isSecretAssignment(arg) || isSecretValue(arg) || SECRET_FLAG_PATTERN.test(previous);
25477
+ }
25478
+ function quoteArg(value) {
25479
+ return JSON.stringify(value);
25480
+ }
25481
+ function displayCommand(command, args) {
25482
+ return [quoteArg(command), ...args.map((arg, index) => quoteArg(isSecretArg(args, index) ? "<redacted>" : arg))].join(" ");
25483
+ }
25484
+ function pushRisk(risks, risk) {
25485
+ if (risks.some((existing) => existing.code === risk.code && existing.evidence === risk.evidence))
25486
+ return;
25487
+ risks.push(risk);
25488
+ }
25489
+ function inspectRisks(command, args, env) {
25490
+ const risks = [];
25491
+ const base = commandBase(command);
25492
+ const joined = [command, ...args].join(" ");
25493
+ if (SHELL_COMMANDS.has(base)) {
25494
+ pushRisk(risks, {
25495
+ code: "shell_interpreter",
25496
+ severity: "warning",
25497
+ message: "Command launches a shell interpreter.",
25498
+ evidence: base
25499
+ });
25500
+ if (args.some((arg) => SHELL_EVAL_FLAGS.has(arg))) {
25501
+ pushRisk(risks, {
25502
+ code: "shell_eval",
25503
+ severity: "danger",
25504
+ message: "Shell command evaluates an inline script.",
25505
+ evidence: base
25506
+ });
25507
+ }
25508
+ }
25509
+ if (base === "sudo") {
25510
+ pushRisk(risks, {
25511
+ code: "privilege_escalation",
25512
+ severity: "danger",
25513
+ message: "Command requests elevated privileges.",
25514
+ evidence: base
25515
+ });
25516
+ }
25517
+ if (DESTRUCTIVE_COMMANDS.has(base) || /\brm\s+-[^\s]*[rf][^\s]*\b/.test(joined) || /--no-preserve-root\b/.test(joined)) {
25518
+ pushRisk(risks, {
25519
+ code: "destructive_command",
25520
+ severity: "danger",
25521
+ message: "Command includes a destructive system operation.",
25522
+ evidence: base
25523
+ });
25524
+ }
25525
+ if (/\b(curl|wget)\b[\s\S]*\|[\s\S]*\b(sh|bash|zsh|fish)\b/.test(joined)) {
25526
+ pushRisk(risks, {
25527
+ code: "download_pipe_shell",
25528
+ severity: "danger",
25529
+ message: "Command downloads remote content and pipes it to a shell."
25530
+ });
25531
+ }
25532
+ if ([command, ...args].some((part) => SHELL_META_PATTERN.test(part))) {
25533
+ pushRisk(risks, {
25534
+ code: "shell_metacharacters",
25535
+ severity: "warning",
25536
+ message: "Command or arguments contain shell metacharacters."
25537
+ });
25538
+ }
25539
+ if (args.some((arg, index) => isSecretArg(args, index))) {
25540
+ pushRisk(risks, {
25541
+ code: "inline_secret",
25542
+ severity: "danger",
25543
+ message: "Command arguments appear to contain inline secret material."
25544
+ });
25545
+ }
25546
+ const secretEnvKeys = Object.keys(env).filter(isSecretKey).sort();
25547
+ if (secretEnvKeys.length > 0) {
25548
+ pushRisk(risks, {
25549
+ code: "secret_env",
25550
+ severity: "warning",
25551
+ message: "Environment contains secret-like keys; values are redacted from consent output.",
25552
+ evidence: secretEnvKeys.join(", ")
25553
+ });
25554
+ }
25555
+ return risks;
25556
+ }
25557
+ function inspectLocalCommand(input) {
25558
+ const args = normalizeArgs(input.args);
25559
+ const env = input.env ?? {};
25560
+ const transport = input.transport ?? "stdio";
25561
+ const risks = inspectRisks(input.command, args, env);
25562
+ return {
25563
+ requiresConsent: transport === "stdio",
25564
+ operation: input.operation ?? "launch",
25565
+ command: input.command,
25566
+ args,
25567
+ displayCommand: displayCommand(input.command, args),
25568
+ envKeys: Object.keys(env).sort(),
25569
+ risks,
25570
+ hasDangerousRisk: risks.some((risk) => risk.severity === "danger")
25571
+ };
25572
+ }
25573
+ function formatLocalCommandReview(review) {
25574
+ const lines = [
25575
+ `Command: ${review.displayCommand}`,
25576
+ review.envKeys.length > 0 ? `Env keys: ${review.envKeys.join(", ")}` : "Env keys: <none>"
25577
+ ];
25578
+ if (review.risks.length > 0) {
25579
+ lines.push("Risks:");
25580
+ for (const risk of review.risks) {
25581
+ lines.push(`- ${risk.severity}: ${risk.code} - ${risk.message}${risk.evidence ? ` (${risk.evidence})` : ""}`);
25582
+ }
25583
+ } else {
25584
+ lines.push("Risks: none detected");
25585
+ }
25586
+ return lines.join(`
25587
+ `);
25588
+ }
25589
+ function assertLocalCommandConsent(input, consent = {}) {
25590
+ const review = inspectLocalCommand(input);
25591
+ if (!review.requiresConsent)
25592
+ return review;
25593
+ if (consent.approved !== true) {
25594
+ throw new LocalCommandConsentError(`local stdio command approval is required before ${review.operation}.
25595
+ ${formatLocalCommandReview(review)}`, review);
25596
+ }
25597
+ if (review.hasDangerousRisk && consent.allowRisky !== true) {
25598
+ throw new LocalCommandConsentError(`risky command approval is required before ${review.operation}.
25599
+ ${formatLocalCommandReview(review)}`, review);
25600
+ }
25601
+ return review;
25602
+ }
25603
+
25604
+ // src/lib/proxy.ts
25245
25605
  var connections = new Map;
25246
25606
  var inflightConnections = new Map;
25247
25607
  function buildEnv(extra) {
@@ -25267,7 +25627,7 @@ function requireUrl(entry) {
25267
25627
  throw new Error(`Server "${entry.id}" has an invalid URL: ${entry.url}`);
25268
25628
  }
25269
25629
  }
25270
- async function connectToServer(entry) {
25630
+ async function connectToServer(entry, options = {}) {
25271
25631
  if (connections.has(entry.id)) {
25272
25632
  return connections.get(entry.id);
25273
25633
  }
@@ -25283,10 +25643,17 @@ async function connectToServer(entry) {
25283
25643
  if (!entry.command?.trim()) {
25284
25644
  throw new Error(`Server "${entry.id}" is missing a command`);
25285
25645
  }
25646
+ assertLocalCommandConsent({
25647
+ command: entry.command,
25648
+ args: entry.args,
25649
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
25650
+ transport: entry.transport,
25651
+ operation: "launch"
25652
+ }, options.localCommandConsent);
25286
25653
  transport = new StdioClientTransport({
25287
25654
  command: entry.command,
25288
25655
  args: entry.args,
25289
- env: buildEnv(entry.env)
25656
+ env: buildEnv(resolveServerEnv(entry))
25290
25657
  });
25291
25658
  } else if (entry.transport === "sse") {
25292
25659
  transport = new SSEClientTransport(requireUrl(entry));
@@ -25418,13 +25785,28 @@ async function refreshTools(id) {
25418
25785
  }
25419
25786
 
25420
25787
  // src/lib/doctor.ts
25421
- async function diagnoseServer(server) {
25788
+ async function diagnoseServer(server, options = {}) {
25422
25789
  const checks3 = [];
25790
+ let hasLocalConsent = true;
25423
25791
  if (server.transport === "stdio") {
25792
+ try {
25793
+ assertLocalCommandConsent({
25794
+ command: server.command,
25795
+ args: server.args,
25796
+ env: { ...server.env, ...credentialRefPlaceholders(server.credentialRefs) },
25797
+ transport: server.transport,
25798
+ operation: "diagnose"
25799
+ }, options.localCommandConsent);
25800
+ } catch (err) {
25801
+ hasLocalConsent = false;
25802
+ checks3.push({ name: "local command consent", pass: false, message: err.message });
25803
+ }
25424
25804
  try {
25425
25805
  const path = execFileSync("which", [server.command], { stdio: "pipe" }).toString().trim();
25426
25806
  let version2 = "";
25427
25807
  try {
25808
+ if (!hasLocalConsent)
25809
+ throw new Error("local stdio command approval is required before version probing");
25428
25810
  version2 = execFileSync(server.command, ["--version"], { stdio: "pipe" }).toString().trim().split(`
25429
25811
  `)[0];
25430
25812
  } catch {}
@@ -25440,12 +25822,29 @@ async function diagnoseServer(server) {
25440
25822
  }
25441
25823
  }
25442
25824
  const missingEnv = Object.entries(server.env).filter(([, v]) => !v);
25443
- if (Object.keys(server.env).length === 0) {
25444
- checks3.push({ name: "env vars", pass: true, message: "no env vars required" });
25445
- } else if (missingEnv.length > 0) {
25446
- checks3.push({ name: "env vars", pass: false, message: `missing values for: ${missingEnv.map(([k]) => k).join(", ")}` });
25825
+ const credentialRefCount = Object.keys(server.credentialRefs ?? {}).length;
25826
+ let credentialError = null;
25827
+ try {
25828
+ resolveServerEnv(server);
25829
+ } catch (err) {
25830
+ credentialError = err.message;
25831
+ }
25832
+ if (Object.keys(server.env).length === 0 && credentialRefCount === 0) {
25833
+ checks3.push({ name: "env vars", pass: true, message: "no env vars or credential refs required" });
25834
+ } else if (missingEnv.length > 0 || credentialError) {
25835
+ const parts = [];
25836
+ if (missingEnv.length > 0)
25837
+ parts.push(`missing literal values for: ${missingEnv.map(([k]) => k).join(", ")}`);
25838
+ if (credentialError)
25839
+ parts.push(credentialError);
25840
+ checks3.push({ name: "env vars", pass: false, message: parts.join("; ") });
25447
25841
  } else {
25448
- checks3.push({ name: "env vars", pass: true, message: `${Object.keys(server.env).length} env var(s) set` });
25842
+ const literalCount = Object.keys(server.env).length;
25843
+ checks3.push({
25844
+ name: "env vars",
25845
+ pass: true,
25846
+ message: `${literalCount} literal env var(s), ${credentialRefCount} credential ref(s) available`
25847
+ });
25449
25848
  }
25450
25849
  if (server.transport !== "stdio" && server.url) {
25451
25850
  try {
@@ -25455,10 +25854,10 @@ async function diagnoseServer(server) {
25455
25854
  checks3.push({ name: "URL reachable", pass: false, message: `unreachable: ${err.message}` });
25456
25855
  }
25457
25856
  }
25458
- if (server.enabled) {
25857
+ if (server.enabled && hasLocalConsent) {
25459
25858
  try {
25460
25859
  await Promise.race([
25461
- connectToServer(server),
25860
+ connectToServer(server, { localCommandConsent: options.localCommandConsent }),
25462
25861
  new Promise((_, reject) => setTimeout(() => reject(new Error("timeout after 10s")), 1e4))
25463
25862
  ]);
25464
25863
  await disconnectServer(server.id);
@@ -25466,8 +25865,10 @@ async function diagnoseServer(server) {
25466
25865
  } catch (err) {
25467
25866
  checks3.push({ name: "connect & list tools", pass: false, message: err.message });
25468
25867
  }
25469
- } else {
25868
+ } else if (!server.enabled) {
25470
25869
  checks3.push({ name: "connect & list tools", pass: true, message: "skipped (server disabled)" });
25870
+ } else {
25871
+ checks3.push({ name: "connect & list tools", pass: false, message: "skipped until local stdio command is approved" });
25471
25872
  }
25472
25873
  return {
25473
25874
  server,
@@ -25507,7 +25908,7 @@ async function getRegistryServer(id) {
25507
25908
  const all = entries.map(parseRegistryEntry);
25508
25909
  return all.find((s) => s.id === id) || null;
25509
25910
  }
25510
- async function installFromRegistry(id) {
25911
+ async function installFromRegistry(id, options = {}) {
25511
25912
  const server = await getRegistryServer(id);
25512
25913
  if (!server) {
25513
25914
  throw new Error(`Server "${id}" not found in registry`);
@@ -25527,6 +25928,13 @@ async function installFromRegistry(id) {
25527
25928
  transport = pkg.transport.type;
25528
25929
  }
25529
25930
  }
25931
+ assertLocalCommandConsent({
25932
+ command,
25933
+ args,
25934
+ transport,
25935
+ env: {},
25936
+ operation: "register"
25937
+ }, options.localCommandConsent);
25530
25938
  return addServer({
25531
25939
  name: server.name,
25532
25940
  description: server.description,
@@ -25763,11 +26171,19 @@ function installProviderProfile(id, options = {}) {
25763
26171
  if (!command) {
25764
26172
  throw new Error(`Provider profile "${id}" does not define an install fallback command`);
25765
26173
  }
26174
+ assertLocalCommandConsent({
26175
+ command,
26176
+ args,
26177
+ env: fallback?.env ?? {},
26178
+ transport: useFallback ? "stdio" : profile.transport,
26179
+ operation: "register"
26180
+ }, options.localCommandConsent);
25766
26181
  return addServer({
25767
26182
  name: options.name ?? profile.displayName,
25768
26183
  description: profile.description ?? undefined,
25769
26184
  command,
25770
26185
  args,
26186
+ env: fallback?.env,
25771
26187
  transport: useFallback ? "stdio" : profile.transport,
25772
26188
  url: useFallback ? fallback?.url : profile.endpoint ?? undefined,
25773
26189
  source: "provider-profile"
@@ -25787,8 +26203,8 @@ init_provider_profile_seeds();
25787
26203
 
25788
26204
  // src/lib/install.ts
25789
26205
  import { execFileSync as execFileSync2 } from "child_process";
25790
- import { existsSync as existsSync7, readFileSync as readFileSync3, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
25791
- import { join as join8 } from "path";
26206
+ import { existsSync as existsSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7 } from "fs";
26207
+ import { join as join9 } from "path";
25792
26208
  import { homedir as homedir8 } from "os";
25793
26209
  function installToClaude(entry) {
25794
26210
  try {
@@ -25800,7 +26216,7 @@ function installToClaude(entry) {
25800
26216
  "--scope",
25801
26217
  "user"
25802
26218
  ];
25803
- for (const [k, v] of Object.entries(entry.env)) {
26219
+ for (const [k, v] of Object.entries(assertAgentInstallEnv(entry))) {
25804
26220
  args.push("--env", `${k}=${v}`);
25805
26221
  }
25806
26222
  args.push(entry.id, "--", entry.command, ...entry.args);
@@ -25812,9 +26228,9 @@ function installToClaude(entry) {
25812
26228
  }
25813
26229
  function installToCodex(entry) {
25814
26230
  try {
25815
- const configDir = join8(homedir8(), ".codex");
25816
- const configPath = join8(configDir, "config.toml");
25817
- if (!existsSync7(configDir)) {
26231
+ const configDir = join9(homedir8(), ".codex");
26232
+ const configPath = join9(configDir, "config.toml");
26233
+ if (!existsSync8(configDir)) {
25818
26234
  mkdirSync7(configDir, { recursive: true });
25819
26235
  }
25820
26236
  const block = `
@@ -25822,7 +26238,7 @@ function installToCodex(entry) {
25822
26238
  ` + `command = ${JSON.stringify(entry.command)}
25823
26239
  ` + `args = [${entry.args.map((a) => JSON.stringify(a)).join(", ")}]
25824
26240
  `;
25825
- const existing = existsSync7(configPath) ? readFileSync3(configPath, "utf-8") : "";
26241
+ const existing = existsSync8(configPath) ? readFileSync4(configPath, "utf-8") : "";
25826
26242
  if (existing.includes(`[mcp_servers.${entry.id}]`)) {
25827
26243
  return { agent: "codex", success: true };
25828
26244
  }
@@ -25834,21 +26250,22 @@ function installToCodex(entry) {
25834
26250
  }
25835
26251
  function installToGemini(entry) {
25836
26252
  try {
25837
- const configDir = join8(homedir8(), ".gemini");
25838
- const configPath = join8(configDir, "settings.json");
25839
- if (!existsSync7(configDir)) {
26253
+ const configDir = join9(homedir8(), ".gemini");
26254
+ const configPath = join9(configDir, "settings.json");
26255
+ if (!existsSync8(configDir)) {
25840
26256
  mkdirSync7(configDir, { recursive: true });
25841
26257
  }
25842
26258
  let settings = {};
25843
- if (existsSync7(configPath)) {
25844
- settings = JSON.parse(readFileSync3(configPath, "utf-8"));
26259
+ if (existsSync8(configPath)) {
26260
+ settings = JSON.parse(readFileSync4(configPath, "utf-8"));
25845
26261
  }
25846
26262
  if (!settings.mcpServers)
25847
26263
  settings.mcpServers = {};
26264
+ const env = assertAgentInstallEnv(entry);
25848
26265
  settings.mcpServers[entry.id] = {
25849
26266
  command: entry.command,
25850
26267
  args: entry.args,
25851
- ...Object.keys(entry.env).length > 0 ? { env: entry.env } : {}
26268
+ ...Object.keys(env).length > 0 ? { env } : {}
25852
26269
  };
25853
26270
  writeFileSync3(configPath, JSON.stringify(settings, null, 2), "utf-8");
25854
26271
  return { agent: "gemini", success: true };
@@ -25856,7 +26273,43 @@ function installToGemini(entry) {
25856
26273
  return { agent: "gemini", success: false, error: err.message };
25857
26274
  }
25858
26275
  }
25859
- function installToAgents(entry, targets = ["claude", "codex", "gemini"]) {
26276
+ function assertAgentInstallEnv(entry) {
26277
+ const refs = entry.credentialRefs ?? {};
26278
+ if (Object.keys(refs).length > 0) {
26279
+ throw new CredentialReferenceError(`Server "${entry.id}" uses credential references; refusing to materialize secrets into local agent config files`);
26280
+ }
26281
+ for (const [key, value] of Object.entries(entry.env)) {
26282
+ if (isSecretLikeEnvKey(key) || isSecretLikeValue(value)) {
26283
+ throw new CredentialReferenceError(`Server "${entry.id}" has legacy raw secret-like env "${key}"; move it to a credential reference before installing to agents`);
26284
+ }
26285
+ }
26286
+ return entry.env;
26287
+ }
26288
+ function installToAgents(entry, targets = ["claude", "codex", "gemini"], options = {}) {
26289
+ try {
26290
+ assertLocalCommandConsent({
26291
+ command: entry.command,
26292
+ args: entry.args,
26293
+ env: { ...entry.env, ...credentialRefPlaceholders(entry.credentialRefs) },
26294
+ transport: entry.transport,
26295
+ operation: "install"
26296
+ }, options.localCommandConsent);
26297
+ } catch (err) {
26298
+ return targets.map((target) => ({
26299
+ agent: target,
26300
+ success: false,
26301
+ error: err.message
26302
+ }));
26303
+ }
26304
+ try {
26305
+ assertAgentInstallEnv(entry);
26306
+ } catch (err) {
26307
+ return targets.map((target) => ({
26308
+ agent: target,
26309
+ success: false,
26310
+ error: err.message
26311
+ }));
26312
+ }
25860
26313
  return targets.map((target) => {
25861
26314
  if (target === "claude")
25862
26315
  return installToClaude(entry);
@@ -26066,11 +26519,11 @@ function seedDefaultMachines() {
26066
26519
  // src/lib/fleet.ts
26067
26520
  init_config2();
26068
26521
  import { spawn as spawn2 } from "child_process";
26069
- import { existsSync as existsSync8, mkdirSync as mkdirSync8, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "fs";
26070
- import { join as join9 } from "path";
26522
+ import { existsSync as existsSync9, mkdirSync as mkdirSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "fs";
26523
+ import { join as join10 } from "path";
26071
26524
  var NPM_SEARCH_URL = "https://registry.npmjs.org/-/v1/search";
26072
26525
  var NPM_REGISTRY_URL = "https://registry.npmjs.org";
26073
- var CATALOG_CACHE_PATH = join9(MCPS_DIR, "cache", "hasna-catalog.json");
26526
+ var CATALOG_CACHE_PATH = join10(MCPS_DIR, "cache", "hasna-catalog.json");
26074
26527
  var DEFAULT_CATALOG_CACHE_TTL_MS = 60 * 60 * 1000;
26075
26528
  var DEFAULT_REMOTE_TIMEOUT_MS = 180000;
26076
26529
  var DEFAULT_HANDSHAKE_TIMEOUT_MS = 2500;
@@ -26080,9 +26533,9 @@ function normalizeQueryList(values) {
26080
26533
  }
26081
26534
  function readCatalogCache(maxAgeMs) {
26082
26535
  try {
26083
- if (!existsSync8(CATALOG_CACHE_PATH))
26536
+ if (!existsSync9(CATALOG_CACHE_PATH))
26084
26537
  return null;
26085
- const parsed = JSON.parse(readFileSync4(CATALOG_CACHE_PATH, "utf-8"));
26538
+ const parsed = JSON.parse(readFileSync5(CATALOG_CACHE_PATH, "utf-8"));
26086
26539
  if (!parsed.cachedAt || !Array.isArray(parsed.entries))
26087
26540
  return null;
26088
26541
  if (Date.now() - parsed.cachedAt > maxAgeMs)
@@ -26094,7 +26547,7 @@ function readCatalogCache(maxAgeMs) {
26094
26547
  }
26095
26548
  function writeCatalogCache(entries) {
26096
26549
  try {
26097
- mkdirSync8(join9(MCPS_DIR, "cache"), { recursive: true });
26550
+ mkdirSync8(join10(MCPS_DIR, "cache"), { recursive: true });
26098
26551
  writeFileSync4(CATALOG_CACHE_PATH, JSON.stringify({ cachedAt: Date.now(), entries }, null, 2), "utf-8");
26099
26552
  } catch {}
26100
26553
  }
@@ -26765,22 +27218,22 @@ async function runFleetInstall(options = {}, dependencies = {}) {
26765
27218
  }));
26766
27219
  }
26767
27220
  // src/lib/version.ts
26768
- import { existsSync as existsSync9, readFileSync as readFileSync5 } from "fs";
26769
- import { dirname as dirname3, join as join10 } from "path";
27221
+ import { existsSync as existsSync10, readFileSync as readFileSync6 } from "fs";
27222
+ import { dirname as dirname3, join as join11 } from "path";
26770
27223
  import { fileURLToPath } from "url";
26771
27224
  var FALLBACK_VERSION = "0.0.1";
26772
27225
  function readPackageVersion(moduleUrl, fallback = FALLBACK_VERSION) {
26773
27226
  const baseDir = dirname3(fileURLToPath(moduleUrl));
26774
27227
  const candidates = [
26775
- join10(baseDir, "..", "..", "package.json"),
26776
- join10(baseDir, "..", "package.json"),
26777
- join10(baseDir, "package.json")
27228
+ join11(baseDir, "..", "..", "package.json"),
27229
+ join11(baseDir, "..", "package.json"),
27230
+ join11(baseDir, "package.json")
26778
27231
  ];
26779
27232
  for (const candidate of candidates) {
26780
- if (!existsSync9(candidate))
27233
+ if (!existsSync10(candidate))
26781
27234
  continue;
26782
27235
  try {
26783
- const pkg = JSON.parse(readFileSync5(candidate, "utf-8"));
27236
+ const pkg = JSON.parse(readFileSync6(candidate, "utf-8"));
26784
27237
  if (pkg.version)
26785
27238
  return pkg.version;
26786
27239
  } catch {}
@@ -26796,19 +27249,27 @@ export {
26796
27249
  updateServer,
26797
27250
  updateMachine,
26798
27251
  unsetServerEnv,
27252
+ unsetServerCredentialRef,
26799
27253
  setServerEnv,
27254
+ setServerCredentialRef,
26800
27255
  seedDefaultProviderProfiles,
26801
27256
  seedDefaultMachines,
26802
27257
  searchRegistry,
26803
27258
  searchProviderProfiles,
26804
27259
  runFleetInstall,
26805
27260
  runFleetHealthCheck,
27261
+ resolveServerEnv,
26806
27262
  removeSource,
26807
27263
  removeServer,
26808
27264
  removeProviderProfile,
26809
27265
  removeMachine,
26810
27266
  refreshTools,
27267
+ redactServerCredentials,
27268
+ redactEnv,
26811
27269
  readPackageVersion,
27270
+ normalizeLiteralEnv,
27271
+ normalizeCredentialRefs,
27272
+ normalizeCredentialRef,
26812
27273
  listSources,
26813
27274
  listServers,
26814
27275
  listProviderProfiles,
@@ -26816,9 +27277,12 @@ export {
26816
27277
  listHasnaMcpCatalog,
26817
27278
  listAwesomeServers,
26818
27279
  listAllTools,
27280
+ isSecretLikeValue,
27281
+ isSecretLikeEnvKey,
26819
27282
  installToAgents,
26820
27283
  installProviderProfile,
26821
27284
  installFromRegistry,
27285
+ inspectLocalCommand,
26822
27286
  getToolCounts,
26823
27287
  getSource,
26824
27288
  getServer,
@@ -26827,6 +27291,7 @@ export {
26827
27291
  getMachine,
26828
27292
  getDb,
26829
27293
  getCachedTools,
27294
+ formatLocalCommandReview,
26830
27295
  findServers,
26831
27296
  enableSource,
26832
27297
  enableServer,
@@ -26837,13 +27302,18 @@ export {
26837
27302
  disableServer,
26838
27303
  disableProviderProfile,
26839
27304
  diagnoseServer,
27305
+ credentialRefPlaceholders,
26840
27306
  connectToServer,
26841
27307
  closeDb,
26842
27308
  cloneServer,
26843
27309
  callTool,
27310
+ assertLocalCommandConsent,
26844
27311
  addSource,
26845
27312
  addServer,
26846
27313
  addMachine,
27314
+ REDACTED_CREDENTIAL_VALUE,
27315
+ LocalCommandConsentError,
26847
27316
  DEFAULT_PROVIDER_PROFILE_SEEDS,
26848
- DEFAULT_MACHINE_SEEDS
27317
+ DEFAULT_MACHINE_SEEDS,
27318
+ CredentialReferenceError
26849
27319
  };