@shomra/agent 0.3.14 → 0.3.15
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 +133 -5
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shomra/agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.15",
|
|
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
|
@@ -175,12 +175,13 @@ async function api(url, key, route, body, opts = {}) {
|
|
|
175
175
|
const timer = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
176
176
|
let res;
|
|
177
177
|
try {
|
|
178
|
+
const method = opts.method ?? 'POST';
|
|
178
179
|
res = await fetch(`${url}${route}`, {
|
|
179
|
-
method
|
|
180
|
+
method,
|
|
180
181
|
// Connection: close avoids undici keep-alive sockets lingering after the
|
|
181
182
|
// command finishes (which can crash process.exit on Windows).
|
|
182
183
|
headers: { 'Content-Type': 'application/json', 'X-Shomra-Key': key, Connection: 'close' },
|
|
183
|
-
body: JSON.stringify(body),
|
|
184
|
+
...(method === 'GET' ? {} : { body: JSON.stringify(body) }),
|
|
184
185
|
signal: ctrl.signal,
|
|
185
186
|
});
|
|
186
187
|
} catch (e) {
|
|
@@ -219,7 +220,7 @@ const BOOLEAN_FLAGS = new Set([
|
|
|
219
220
|
'apply', 'dry-run', 'global', 'local', 'trailer', 'evolve', 'report', 'init',
|
|
220
221
|
'no-suppress', 'no-baseline', 'no-policy', 'no-index', 'adaptive',
|
|
221
222
|
'fail-on-regression', 'fail-on-blocked', 'write', 'yes', 'stdin', 'quiet', 'help',
|
|
222
|
-
'check', 'checklist', 'pre-receive', 'uninstall', 'save',
|
|
223
|
+
'check', 'checklist', 'pre-receive', 'uninstall', 'save', 'list',
|
|
223
224
|
]);
|
|
224
225
|
// Flags that take a value (`--key value` or `--key=value`).
|
|
225
226
|
const VALUE_FLAGS = new Set([
|
|
@@ -232,8 +233,23 @@ const VALUE_FLAGS = new Set([
|
|
|
232
233
|
'fail-on',
|
|
233
234
|
// `shomra design --save --subject KIND:id` — pin the analysis to a subject.
|
|
234
235
|
'subject', 'title', 'note', 'actor',
|
|
236
|
+
// `shomra run <playbook> --input key=value` — repeatable, see REPEATABLE_FLAGS.
|
|
237
|
+
'input',
|
|
235
238
|
]);
|
|
236
239
|
const KNOWN_FLAGS = new Set([...BOOLEAN_FLAGS, ...VALUE_FLAGS]);
|
|
240
|
+
// ⚠ Flags that ACCUMULATE instead of overwriting. Everything else keeps the
|
|
241
|
+
// last value; a playbook takes several inputs, and silently dropping all but
|
|
242
|
+
// the last one would run it on defaults nobody asked for.
|
|
243
|
+
const REPEATABLE_FLAGS = new Set(['input']);
|
|
244
|
+
|
|
245
|
+
function setFlag(flags, name, value) {
|
|
246
|
+
if (!REPEATABLE_FLAGS.has(name)) {
|
|
247
|
+
flags[name] = value;
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
const prev = flags[name];
|
|
251
|
+
flags[name] = prev === undefined ? value : [].concat(prev, value);
|
|
252
|
+
}
|
|
237
253
|
|
|
238
254
|
function parseFlags(argv) {
|
|
239
255
|
const flags = {};
|
|
@@ -249,7 +265,7 @@ function parseFlags(argv) {
|
|
|
249
265
|
if (eq !== -1) {
|
|
250
266
|
const name = body.slice(0, eq);
|
|
251
267
|
if (!KNOWN_FLAGS.has(name)) unknown.push(name);
|
|
252
|
-
flags
|
|
268
|
+
setFlag(flags, name, body.slice(eq + 1));
|
|
253
269
|
continue;
|
|
254
270
|
}
|
|
255
271
|
if (!KNOWN_FLAGS.has(body)) {
|
|
@@ -263,7 +279,7 @@ function parseFlags(argv) {
|
|
|
263
279
|
}
|
|
264
280
|
const next = argv[i + 1];
|
|
265
281
|
if (next !== undefined && !next.startsWith('--')) {
|
|
266
|
-
flags
|
|
282
|
+
setFlag(flags, body, next);
|
|
267
283
|
i++;
|
|
268
284
|
} else flags[body] = true;
|
|
269
285
|
} else positional.push(a);
|
|
@@ -2730,6 +2746,114 @@ async function cmdMemoryScan(flags, positional) {
|
|
|
2730
2746
|
// `--min <resilience>` and/or `--fail-on-regression`. Exit: 0 = pass,
|
|
2731
2747
|
// 2 = below the resilience floor or a regression appeared.
|
|
2732
2748
|
|
|
2749
|
+
// ── playbooks: the whole assurance loop as ONE command ────────────────────
|
|
2750
|
+
//
|
|
2751
|
+
// `pre-release` and `agent-release-check` were written to be run by a pipeline
|
|
2752
|
+
// and had no client — the backend has carried an API-key controller for exactly
|
|
2753
|
+
// this since the engine shipped. Exit code is the product: a failed `gate` step
|
|
2754
|
+
// comes back FAILED, and that is the assertion holding, not a broken run.
|
|
2755
|
+
|
|
2756
|
+
const STEP_MARK = { SUCCESS: () => green('✓'), FAILED: () => red('✗'), SKIPPED: () => dim('–'), RUNNING: () => cyan('•'), PENDING: () => dim('·') };
|
|
2757
|
+
|
|
2758
|
+
async function cmdRun(flags, positional) {
|
|
2759
|
+
const id = positional[0];
|
|
2760
|
+
|
|
2761
|
+
/**
|
|
2762
|
+
* ⚠ USAGE BEFORE ENROLMENT. A malformed `--input` is a typo the caller can
|
|
2763
|
+
* fix without a key, and answering it with "not configured" sends them to
|
|
2764
|
+
* the wrong problem.
|
|
2765
|
+
*
|
|
2766
|
+
* --input k=v, repeatable. Numbers and booleans keep their type, because a
|
|
2767
|
+
* threshold arriving as "80" is not the same argument.
|
|
2768
|
+
*/
|
|
2769
|
+
const inputs = {};
|
|
2770
|
+
for (const raw of [].concat(flags.input ?? [])) {
|
|
2771
|
+
const eq = String(raw).indexOf('=');
|
|
2772
|
+
if (eq < 0) {
|
|
2773
|
+
console.error(` ${red('✗')} --input expects key=value (got ${JSON.stringify(raw)})\n`);
|
|
2774
|
+
process.exit(EXIT_USAGE);
|
|
2775
|
+
}
|
|
2776
|
+
const k = String(raw).slice(0, eq).trim();
|
|
2777
|
+
const v = String(raw).slice(eq + 1);
|
|
2778
|
+
inputs[k] = v === 'true' ? true : v === 'false' ? false : /^-?\d+(\.\d+)?$/.test(v) ? Number(v) : v;
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
const cfg = loadConfig();
|
|
2782
|
+
const { apiKey, url } = resolveSettings(cfg);
|
|
2783
|
+
if (!apiKey) exitNotConfigured();
|
|
2784
|
+
|
|
2785
|
+
if (!id || flags.list) {
|
|
2786
|
+
let catalog;
|
|
2787
|
+
try {
|
|
2788
|
+
catalog = await api(url, apiKey, '/playbooks/agent/catalog', null, { method: 'GET' });
|
|
2789
|
+
} catch (e) {
|
|
2790
|
+
console.error(` ${red('✗')} ${e.message}\n`);
|
|
2791
|
+
process.exit(1);
|
|
2792
|
+
}
|
|
2793
|
+
if (flags.json) {
|
|
2794
|
+
console.log(JSON.stringify(catalog, null, 2));
|
|
2795
|
+
return;
|
|
2796
|
+
}
|
|
2797
|
+
console.log(`\n ${bold('Playbooks')} ${dim('— run one end-to-end, exit non-zero when a gate holds')}\n`);
|
|
2798
|
+
for (const p of catalog) {
|
|
2799
|
+
console.log(` ${cyan(p.id)} ${dim(`v${p.version} · ${p.steps.length} steps`)}`);
|
|
2800
|
+
console.log(` ${p.name}`);
|
|
2801
|
+
const required = Object.entries(p.inputs || {}).filter(([, d]) => d.required).map(([k]) => k);
|
|
2802
|
+
if (required.length) console.log(` ${dim('needs')} ${required.map((k) => bold(k)).join(', ')}`);
|
|
2803
|
+
console.log('');
|
|
2804
|
+
}
|
|
2805
|
+
console.log(dim(` Run one: ${bold('shomra run <id> --input key=value')}\n`));
|
|
2806
|
+
if (!id) process.exitCode = EXIT_USAGE;
|
|
2807
|
+
return;
|
|
2808
|
+
}
|
|
2809
|
+
|
|
2810
|
+
if (!flags.json) process.stdout.write(dim(`\n Running ${bold(id)}… `));
|
|
2811
|
+
let run;
|
|
2812
|
+
try {
|
|
2813
|
+
run = await api(url, apiKey, `/playbooks/agent/${encodeURIComponent(id)}/run`, {
|
|
2814
|
+
inputs,
|
|
2815
|
+
...(flags.project ? { projectId: String(flags.project) } : {}),
|
|
2816
|
+
actor: `${os.hostname()}/${os.userInfo().username}`,
|
|
2817
|
+
});
|
|
2818
|
+
} catch (e) {
|
|
2819
|
+
if (!flags.json) console.log(red('failed'));
|
|
2820
|
+
console.error(` ${red('✗')} ${e.message}\n`);
|
|
2821
|
+
process.exit(1);
|
|
2822
|
+
}
|
|
2823
|
+
if (!flags.json) console.log(green('done'));
|
|
2824
|
+
|
|
2825
|
+
if (flags.json) {
|
|
2826
|
+
console.log(JSON.stringify(run, null, 2));
|
|
2827
|
+
} else {
|
|
2828
|
+
console.log(`\n ${bold(run.playbookName)} ${dim(`· ${run.playbookId}`)}\n`);
|
|
2829
|
+
for (const s of run.steps || []) {
|
|
2830
|
+
const mark = (STEP_MARK[s.status] || STEP_MARK.PENDING)();
|
|
2831
|
+
// ⚠ SKIPPED is stated as its own thing. A step whose condition was false
|
|
2832
|
+
// did not run, and printing it as a pass claims work nothing did.
|
|
2833
|
+
const note =
|
|
2834
|
+
s.status === 'SKIPPED'
|
|
2835
|
+
? dim('skipped — its condition was false')
|
|
2836
|
+
: s.error
|
|
2837
|
+
? (s.uses === 'gate' ? yellow(s.error) : red(s.error))
|
|
2838
|
+
: '';
|
|
2839
|
+
console.log(` ${mark} ${s.name}${note ? ' ' + dim('·') + ' ' + note : ''}`);
|
|
2840
|
+
}
|
|
2841
|
+
const failedStep = (run.steps || []).find((s) => s.status === 'FAILED');
|
|
2842
|
+
const gateHeld = run.status === 'FAILED' && failedStep?.uses === 'gate';
|
|
2843
|
+
console.log('');
|
|
2844
|
+
if (run.status === 'FAILED' && gateHeld) {
|
|
2845
|
+
console.log(` ${yellow('⚠')} ${bold('The gate held.')} ${dim('That is the assertion firing — exiting non-zero.')}\n`);
|
|
2846
|
+
} else if (run.status === 'FAILED') {
|
|
2847
|
+
console.log(` ${red('✗')} ${bold('A step failed.')} ${dim(run.error || '')}\n`);
|
|
2848
|
+
} else {
|
|
2849
|
+
console.log(` ${green('✓')} ${bold('Completed.')} ${dim('Every step that was meant to run did.')}\n`);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
|
|
2853
|
+
// The exit code IS the feature: a pipeline gates on this.
|
|
2854
|
+
if (run.status === 'FAILED') process.exitCode = 1;
|
|
2855
|
+
}
|
|
2856
|
+
|
|
2733
2857
|
async function cmdRedteam(flags) {
|
|
2734
2858
|
const cfg = loadConfig();
|
|
2735
2859
|
const { apiKey, url } = resolveSettings(cfg);
|
|
@@ -6652,6 +6776,9 @@ ${bold('COMMANDS')}
|
|
|
6652
6776
|
${cyan('scan')} Discover AI tooling on this machine ${dim('[--report] [--json] [--path <dir>]')}
|
|
6653
6777
|
${cyan('report')} Discover + send inventory to your Shomra org ${dim('(alias: scan --report) [--json]')}
|
|
6654
6778
|
${cyan('status')} Show config, enrollment + firewall health
|
|
6779
|
+
${cyan('run')} ${bold('Run a whole assurance playbook')} ${dim('<id> [--input k=v]… [--project <id>] [--json] · --list for the catalog')}
|
|
6780
|
+
${dim('scan → red-team → harden → compliance → gate, as one command. Exits')}
|
|
6781
|
+
${dim('non-zero when a gate holds, so a pipeline can block the release.')}
|
|
6655
6782
|
|
|
6656
6783
|
${dim('Setup — run once per machine / repo')}
|
|
6657
6784
|
${cyan('init')} Configure + enroll this machine ${dim('--key shm_live_… [--url <backend>]')}
|
|
@@ -6900,6 +7027,7 @@ const COMMANDS = {
|
|
|
6900
7027
|
scan: (f) => cmdScan(f),
|
|
6901
7028
|
report: (f) => cmdScan({ ...f, report: true }),
|
|
6902
7029
|
gate: (f, p) => cmdGate(f, p),
|
|
7030
|
+
run: (f, p) => cmdRun(f, p),
|
|
6903
7031
|
check: (f, p) => cmdCheck(f, p),
|
|
6904
7032
|
pr: (f, p) => cmdPr(f, p),
|
|
6905
7033
|
baseline: (f, p) => cmdBaseline(f, p),
|