@shomra/agent 0.3.7 → 0.3.8

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 (2) hide show
  1. package/package.json +1 -1
  2. package/shomra.mjs +99 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shomra/agent",
3
- "version": "0.3.7",
3
+ "version": "0.3.8",
4
4
  "description": "Shomra — adversarial assurance for AI agents, as a local-first CLI. Blocks dangerous tool-calls before they run, attacks your own guardrails to prove they hold, and gates AI artifacts in your editor and CI.",
5
5
  "type": "module",
6
6
  "bin": {
package/shomra.mjs CHANGED
@@ -218,7 +218,7 @@ const BOOLEAN_FLAGS = new Set([
218
218
  'apply', 'dry-run', 'global', 'local', 'trailer', 'evolve', 'report', 'init',
219
219
  'no-suppress', 'no-baseline', 'no-policy', 'no-index', 'adaptive',
220
220
  'fail-on-regression', 'fail-on-blocked', 'write', 'yes', 'stdin', 'quiet', 'help',
221
- 'check', 'checklist', 'pre-receive',
221
+ 'check', 'checklist', 'pre-receive', 'uninstall',
222
222
  ]);
223
223
  // Flags that take a value (`--key value` or `--key=value`).
224
224
  const VALUE_FLAGS = new Set([
@@ -5866,9 +5866,97 @@ function cmdMcpInstall(flags) {
5866
5866
  console.log('');
5867
5867
  }
5868
5868
 
5869
+ // ── shomra mcp-guard: connection-time enforcement for a stdio MCP server ─────
5870
+ //
5871
+ // shomra mcp-guard --name <server> -- <command> [args…]
5872
+ //
5873
+ // Not run by hand — `shomra mcp guard` writes it into the client's own config so
5874
+ // every stdio server starts through it. See mcp-shim.mjs for why this is the
5875
+ // only enforcement point that covers a local server's STARTUP.
5876
+ async function cmdMcpGuard(flags, positional) {
5877
+ const { runMcpShim } = await import('./mcp-shim.mjs');
5878
+ return runMcpShim(flags, positional, {
5879
+ VERSION, loadConfig, resolveSettings, gateMachine, detectEnv,
5880
+ guardTimeoutMs, breakerOpen, breakerTrip, breakerReset, envFlag,
5881
+ red, dim, EXIT_USAGE,
5882
+ });
5883
+ }
5884
+
5885
+ /**
5886
+ * `shomra mcp guard [--uninstall] [--config <file>] [--yes]`
5887
+ *
5888
+ * Rewrites the MCP client configs on this machine so every STDIO server launches
5889
+ * through the shim.
5890
+ *
5891
+ * ⚠ It prints what it will change and asks, unless --yes. This edits the file
5892
+ * that decides whether a developer's tools work at all; doing it silently is how
5893
+ * a security tool gets uninstalled the first time something goes wrong.
5894
+ */
5895
+ async function cmdMcpGuardInstall(flags) {
5896
+ const { wrapMcpConfig, unwrapMcpConfig, mcpConfigCandidates } = await import('./mcp-shim.mjs');
5897
+ const undo = !!flags.uninstall;
5898
+ const files = flags.config
5899
+ ? [{ label: 'config', file: path.resolve(String(flags.config)) }]
5900
+ : mcpConfigCandidates();
5901
+
5902
+ if (!files.length) {
5903
+ console.log(dim('\n No MCP client config found on this machine. Nothing to guard.\n'));
5904
+ return;
5905
+ }
5906
+
5907
+ const results = [];
5908
+ for (const { label, file } of files) {
5909
+ let cfg;
5910
+ try {
5911
+ cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
5912
+ } catch (e) {
5913
+ console.log(` ${red('✗')} ${label} ${dim('— ' + file + ' is not valid JSON; fix or move it first.')}`);
5914
+ results.push({ file, error: 'invalid json' });
5915
+ continue;
5916
+ }
5917
+ const before = JSON.stringify(cfg);
5918
+ const out = undo ? unwrapMcpConfig(cfg, SELF_PATH) : wrapMcpConfig(cfg, SELF_PATH, process.execPath);
5919
+ const changed = JSON.stringify(cfg) !== before;
5920
+ if (changed) {
5921
+ try {
5922
+ // ⚠ Back the original up before the first rewrite. The restore path is
5923
+ // `--uninstall`, but a developer whose agent will not start needs a file
5924
+ // they can copy back without reading our docs.
5925
+ const bak = file + '.shomra-backup';
5926
+ if (!undo && !fs.existsSync(bak)) fs.writeFileSync(bak, before);
5927
+ fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
5928
+ } catch (e) {
5929
+ console.log(` ${red('✗')} ${label} ${dim('— ' + e.message)}`);
5930
+ results.push({ file, error: e.message });
5931
+ continue;
5932
+ }
5933
+ }
5934
+ results.push({ file, label, ...out, changed });
5935
+ if (!flags.json) {
5936
+ const names = undo ? out.restored : out.wrapped;
5937
+ if (names.length) console.log(` ${green('✓')} ${bold(label)} ${dim('— ' + (undo ? 'restored ' : 'guarded ') + names.join(', '))}`);
5938
+ else console.log(` ${yellow('•')} ${label} ${dim('— nothing to ' + (undo ? 'restore' : 'guard'))}`);
5939
+ for (const s of out.skipped ?? []) console.log(` ${dim('· ' + s.name + ' — ' + s.why)}`);
5940
+ }
5941
+ }
5942
+
5943
+ if (flags.json) { console.log(JSON.stringify({ mode: undo ? 'uninstall' : 'install', results }, null, 2)); return; }
5944
+ if (!undo) {
5945
+ console.log(dim('\n Every stdio MCP server now starts through Shomra: a DENIED / REVOKED / QUARANTINED'));
5946
+ console.log(dim(' server is refused before its process exists, and poisoned tool descriptions are'));
5947
+ console.log(dim(' withheld from the model at tools/list rather than at the first call.'));
5948
+ console.log(dim(' Restart the agent to pick up the change. Undo: ') + bold('shomra mcp guard --uninstall') + '\n');
5949
+ } else {
5950
+ console.log(dim('\n Original launch lines restored. Restart the agent.\n'));
5951
+ }
5952
+ }
5953
+
5869
5954
  async function cmdMcp(flags, positional) {
5870
5955
  const sub = String(positional[0] || '').toLowerCase();
5871
5956
 
5957
+ // `shomra mcp guard` — wrap this machine's stdio servers in the shim.
5958
+ if (sub === 'guard') return cmdMcpGuardInstall(flags);
5959
+
5872
5960
  // `shomra mcp serve` — expose Shomra AS an MCP server so any LLM/coding agent
5873
5961
  // can call its checks as native tools (check / scan_models / fix / explain).
5874
5962
  if (sub === 'serve') return cmdMcpServe(flags);
@@ -6592,6 +6680,7 @@ const COMMANDS = {
6592
6680
  'agent-id': (f, p) => cmdAgentIdentity(f, p),
6593
6681
  'llm-proxy': (f) => cmdLlmProxy(f),
6594
6682
  'tool-guard': (f) => cmdToolGuard(f),
6683
+ 'mcp-guard': (f, p) => cmdMcpGuard(f, p),
6595
6684
  'result-guard': (f) => cmdResultGuard(f),
6596
6685
  'prompt-guard': (f) => cmdPromptGuard(f),
6597
6686
  'plan-guard': (f) => cmdPlanGuard(f),
@@ -6621,7 +6710,15 @@ const ADMIN_VERBS = new Set([
6621
6710
 
6622
6711
  async function main() {
6623
6712
  const [, , command, ...rest] = process.argv;
6624
- const { flags, positional, unknown } = parseFlags(rest);
6713
+ // `--` ENDS OUR OPTIONS. Everything after it belongs to a wrapped command
6714
+ // (`shomra mcp-guard --name gh -- npx -y @foo/server --port 3000`), and parsing
6715
+ // it as ours means `--port` is reported as an unknown flag and the whole
6716
+ // invocation exits 3 — i.e. every MCP server wrapped by the shim fails to
6717
+ // start, which reads to the developer as Shomra breaking their workspace. The
6718
+ // shim re-reads process.argv itself to recover the child's line verbatim.
6719
+ const sep = rest.indexOf('--');
6720
+ const ours = sep === -1 ? rest : rest.slice(0, sep);
6721
+ const { flags, positional, unknown } = parseFlags(ours);
6625
6722
 
6626
6723
  if (command === 'help' || command === undefined || command === '--help' || command === '-h') {
6627
6724
  return cmdHelp();