@shomra/agent 0.3.7 → 0.3.9
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/package.json +1 -1
- package/shomra.mjs +116 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shomra/agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
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([
|
|
@@ -226,6 +226,9 @@ const VALUE_FLAGS = new Set([
|
|
|
226
226
|
'scenarios', 'objectives', 'turns', 'target', 'run', 'port', 'config', 'env',
|
|
227
227
|
'command', 'base', 'repo', 'pr', 'token', 'sha', 'session', 'since', 'depth',
|
|
228
228
|
'scope', 'writer', 'type', 'slug', 'framework', 'chunk-size', 'manifest',
|
|
229
|
+
// `--fail-on <critical|high|medium>` lets CI gate below the default
|
|
230
|
+
// (blocked-only) exit code — e.g. fail the build on a HIGH finding.
|
|
231
|
+
'fail-on',
|
|
229
232
|
]);
|
|
230
233
|
const KNOWN_FLAGS = new Set([...BOOLEAN_FLAGS, ...VALUE_FLAGS]);
|
|
231
234
|
|
|
@@ -1373,6 +1376,7 @@ async function cmdGateAll(flags, positional, { apiKey, url }) {
|
|
|
1373
1376
|
if (flags.sarif) {
|
|
1374
1377
|
console.log(JSON.stringify(toSarif(results), null, 2));
|
|
1375
1378
|
if (blocked > 0 || strictOutage) process.exitCode = 1;
|
|
1379
|
+
else if (failOnHit(flags, blocked, flagged)) process.exitCode = 1;
|
|
1376
1380
|
else if (flagged > 0 && flags.strict) process.exitCode = 2;
|
|
1377
1381
|
return;
|
|
1378
1382
|
}
|
|
@@ -1395,6 +1399,7 @@ async function cmdGateAll(flags, positional, { apiKey, url }) {
|
|
|
1395
1399
|
|
|
1396
1400
|
// Set exitCode (not process.exit) so pending sockets drain cleanly on Windows.
|
|
1397
1401
|
if (blocked > 0 || strictOutage) process.exitCode = 1;
|
|
1402
|
+
else if (failOnHit(flags, blocked, flagged)) process.exitCode = 1;
|
|
1398
1403
|
else if (flagged > 0 && flags.strict) process.exitCode = 2;
|
|
1399
1404
|
}
|
|
1400
1405
|
|
|
@@ -1498,9 +1503,21 @@ async function cmdCheck(flags, positional) {
|
|
|
1498
1503
|
}
|
|
1499
1504
|
|
|
1500
1505
|
if (blocked > 0 || strictOutage) process.exitCode = 1;
|
|
1506
|
+
else if (failOnHit(flags, blocked, flagged)) process.exitCode = 1;
|
|
1501
1507
|
else if (flagged > 0 && flags.strict) process.exitCode = 2;
|
|
1502
1508
|
}
|
|
1503
1509
|
|
|
1510
|
+
// `--fail-on <critical|high|medium>` — gate CI below the default (blocked-only).
|
|
1511
|
+
// blocked ⇒ a CRITICAL/BLOCK finding, flagged ⇒ a HIGH/FLAG one; the mapping is
|
|
1512
|
+
// the same severity→decision the guard uses. Default 'critical' preserves the
|
|
1513
|
+
// existing behavior (HIGH exits 0 unless --strict). An unrecognised value is
|
|
1514
|
+
// treated as 'critical' rather than silently gating on nothing.
|
|
1515
|
+
function failOnHit(flags, blocked, flagged) {
|
|
1516
|
+
const threshold = { critical: 3, high: 2, medium: 1 }[String(flags['fail-on'] || 'critical').toLowerCase()] ?? 3;
|
|
1517
|
+
const worst = blocked > 0 ? 3 : flagged > 0 ? 2 : 0;
|
|
1518
|
+
return worst > 0 && worst >= threshold;
|
|
1519
|
+
}
|
|
1520
|
+
|
|
1504
1521
|
// ── shomra baseline: accept everything here, so only NEW findings fail ───────
|
|
1505
1522
|
//
|
|
1506
1523
|
// shomra baseline [dir] # write .shomra/baseline.json of current findings
|
|
@@ -5866,9 +5883,97 @@ function cmdMcpInstall(flags) {
|
|
|
5866
5883
|
console.log('');
|
|
5867
5884
|
}
|
|
5868
5885
|
|
|
5886
|
+
// ── shomra mcp-guard: connection-time enforcement for a stdio MCP server ─────
|
|
5887
|
+
//
|
|
5888
|
+
// shomra mcp-guard --name <server> -- <command> [args…]
|
|
5889
|
+
//
|
|
5890
|
+
// Not run by hand — `shomra mcp guard` writes it into the client's own config so
|
|
5891
|
+
// every stdio server starts through it. See mcp-shim.mjs for why this is the
|
|
5892
|
+
// only enforcement point that covers a local server's STARTUP.
|
|
5893
|
+
async function cmdMcpGuard(flags, positional) {
|
|
5894
|
+
const { runMcpShim } = await import('./mcp-shim.mjs');
|
|
5895
|
+
return runMcpShim(flags, positional, {
|
|
5896
|
+
VERSION, loadConfig, resolveSettings, gateMachine, detectEnv,
|
|
5897
|
+
guardTimeoutMs, breakerOpen, breakerTrip, breakerReset, envFlag,
|
|
5898
|
+
red, dim, EXIT_USAGE,
|
|
5899
|
+
});
|
|
5900
|
+
}
|
|
5901
|
+
|
|
5902
|
+
/**
|
|
5903
|
+
* `shomra mcp guard [--uninstall] [--config <file>] [--yes]`
|
|
5904
|
+
*
|
|
5905
|
+
* Rewrites the MCP client configs on this machine so every STDIO server launches
|
|
5906
|
+
* through the shim.
|
|
5907
|
+
*
|
|
5908
|
+
* ⚠ It prints what it will change and asks, unless --yes. This edits the file
|
|
5909
|
+
* that decides whether a developer's tools work at all; doing it silently is how
|
|
5910
|
+
* a security tool gets uninstalled the first time something goes wrong.
|
|
5911
|
+
*/
|
|
5912
|
+
async function cmdMcpGuardInstall(flags) {
|
|
5913
|
+
const { wrapMcpConfig, unwrapMcpConfig, mcpConfigCandidates } = await import('./mcp-shim.mjs');
|
|
5914
|
+
const undo = !!flags.uninstall;
|
|
5915
|
+
const files = flags.config
|
|
5916
|
+
? [{ label: 'config', file: path.resolve(String(flags.config)) }]
|
|
5917
|
+
: mcpConfigCandidates();
|
|
5918
|
+
|
|
5919
|
+
if (!files.length) {
|
|
5920
|
+
console.log(dim('\n No MCP client config found on this machine. Nothing to guard.\n'));
|
|
5921
|
+
return;
|
|
5922
|
+
}
|
|
5923
|
+
|
|
5924
|
+
const results = [];
|
|
5925
|
+
for (const { label, file } of files) {
|
|
5926
|
+
let cfg;
|
|
5927
|
+
try {
|
|
5928
|
+
cfg = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
5929
|
+
} catch (e) {
|
|
5930
|
+
console.log(` ${red('✗')} ${label} ${dim('— ' + file + ' is not valid JSON; fix or move it first.')}`);
|
|
5931
|
+
results.push({ file, error: 'invalid json' });
|
|
5932
|
+
continue;
|
|
5933
|
+
}
|
|
5934
|
+
const before = JSON.stringify(cfg);
|
|
5935
|
+
const out = undo ? unwrapMcpConfig(cfg, SELF_PATH) : wrapMcpConfig(cfg, SELF_PATH, process.execPath);
|
|
5936
|
+
const changed = JSON.stringify(cfg) !== before;
|
|
5937
|
+
if (changed) {
|
|
5938
|
+
try {
|
|
5939
|
+
// ⚠ Back the original up before the first rewrite. The restore path is
|
|
5940
|
+
// `--uninstall`, but a developer whose agent will not start needs a file
|
|
5941
|
+
// they can copy back without reading our docs.
|
|
5942
|
+
const bak = file + '.shomra-backup';
|
|
5943
|
+
if (!undo && !fs.existsSync(bak)) fs.writeFileSync(bak, before);
|
|
5944
|
+
fs.writeFileSync(file, JSON.stringify(cfg, null, 2) + '\n');
|
|
5945
|
+
} catch (e) {
|
|
5946
|
+
console.log(` ${red('✗')} ${label} ${dim('— ' + e.message)}`);
|
|
5947
|
+
results.push({ file, error: e.message });
|
|
5948
|
+
continue;
|
|
5949
|
+
}
|
|
5950
|
+
}
|
|
5951
|
+
results.push({ file, label, ...out, changed });
|
|
5952
|
+
if (!flags.json) {
|
|
5953
|
+
const names = undo ? out.restored : out.wrapped;
|
|
5954
|
+
if (names.length) console.log(` ${green('✓')} ${bold(label)} ${dim('— ' + (undo ? 'restored ' : 'guarded ') + names.join(', '))}`);
|
|
5955
|
+
else console.log(` ${yellow('•')} ${label} ${dim('— nothing to ' + (undo ? 'restore' : 'guard'))}`);
|
|
5956
|
+
for (const s of out.skipped ?? []) console.log(` ${dim('· ' + s.name + ' — ' + s.why)}`);
|
|
5957
|
+
}
|
|
5958
|
+
}
|
|
5959
|
+
|
|
5960
|
+
if (flags.json) { console.log(JSON.stringify({ mode: undo ? 'uninstall' : 'install', results }, null, 2)); return; }
|
|
5961
|
+
if (!undo) {
|
|
5962
|
+
console.log(dim('\n Every stdio MCP server now starts through Shomra: a DENIED / REVOKED / QUARANTINED'));
|
|
5963
|
+
console.log(dim(' server is refused before its process exists, and poisoned tool descriptions are'));
|
|
5964
|
+
console.log(dim(' withheld from the model at tools/list rather than at the first call.'));
|
|
5965
|
+
console.log(dim(' Restart the agent to pick up the change. Undo: ') + bold('shomra mcp guard --uninstall') + '\n');
|
|
5966
|
+
} else {
|
|
5967
|
+
console.log(dim('\n Original launch lines restored. Restart the agent.\n'));
|
|
5968
|
+
}
|
|
5969
|
+
}
|
|
5970
|
+
|
|
5869
5971
|
async function cmdMcp(flags, positional) {
|
|
5870
5972
|
const sub = String(positional[0] || '').toLowerCase();
|
|
5871
5973
|
|
|
5974
|
+
// `shomra mcp guard` — wrap this machine's stdio servers in the shim.
|
|
5975
|
+
if (sub === 'guard') return cmdMcpGuardInstall(flags);
|
|
5976
|
+
|
|
5872
5977
|
// `shomra mcp serve` — expose Shomra AS an MCP server so any LLM/coding agent
|
|
5873
5978
|
// can call its checks as native tools (check / scan_models / fix / explain).
|
|
5874
5979
|
if (sub === 'serve') return cmdMcpServe(flags);
|
|
@@ -6592,6 +6697,7 @@ const COMMANDS = {
|
|
|
6592
6697
|
'agent-id': (f, p) => cmdAgentIdentity(f, p),
|
|
6593
6698
|
'llm-proxy': (f) => cmdLlmProxy(f),
|
|
6594
6699
|
'tool-guard': (f) => cmdToolGuard(f),
|
|
6700
|
+
'mcp-guard': (f, p) => cmdMcpGuard(f, p),
|
|
6595
6701
|
'result-guard': (f) => cmdResultGuard(f),
|
|
6596
6702
|
'prompt-guard': (f) => cmdPromptGuard(f),
|
|
6597
6703
|
'plan-guard': (f) => cmdPlanGuard(f),
|
|
@@ -6621,7 +6727,15 @@ const ADMIN_VERBS = new Set([
|
|
|
6621
6727
|
|
|
6622
6728
|
async function main() {
|
|
6623
6729
|
const [, , command, ...rest] = process.argv;
|
|
6624
|
-
|
|
6730
|
+
// ⚠ `--` ENDS OUR OPTIONS. Everything after it belongs to a wrapped command
|
|
6731
|
+
// (`shomra mcp-guard --name gh -- npx -y @foo/server --port 3000`), and parsing
|
|
6732
|
+
// it as ours means `--port` is reported as an unknown flag and the whole
|
|
6733
|
+
// invocation exits 3 — i.e. every MCP server wrapped by the shim fails to
|
|
6734
|
+
// start, which reads to the developer as Shomra breaking their workspace. The
|
|
6735
|
+
// shim re-reads process.argv itself to recover the child's line verbatim.
|
|
6736
|
+
const sep = rest.indexOf('--');
|
|
6737
|
+
const ours = sep === -1 ? rest : rest.slice(0, sep);
|
|
6738
|
+
const { flags, positional, unknown } = parseFlags(ours);
|
|
6625
6739
|
|
|
6626
6740
|
if (command === 'help' || command === undefined || command === '--help' || command === '-h') {
|
|
6627
6741
|
return cmdHelp();
|