@shomra/agent 0.2.12 → 0.3.2

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/README.md CHANGED
@@ -1,8 +1,10 @@
1
1
  # @shomra/agent
2
2
 
3
- **The firewall for AI agents**, as a local-first CLI. It sits inside your coding
4
- agent and CI and blocks dangerous tool-calls, shell commands and data
5
- exfiltration *before they run* — on your machine, even offline. It also vets AI
3
+ **Adversarial assurance for AI agents**, as a local-first CLI. It sits inside
4
+ your coding agent and CI and blocks dangerous tool-calls, shell commands and
5
+ data exfiltration *before they run* — on your machine, even offline. Enrolled,
6
+ it also attacks your org’s own guardrails (`shomra admin redteam`) and turns
7
+ each breach into a false-positive-gated control (`shomra harden`). It also vets AI
6
8
  artifacts (MCP configs, Skills, slash commands, hooks, rules files) before they
7
9
  install. Start with a free on-machine scan — no signup.
8
10
 
@@ -10,6 +12,15 @@ Zero dependencies — Node ≥ 18 built-ins only.
10
12
 
11
13
  ## Install
12
14
 
15
+ Also available as a [Dev Container Feature](devcontainer-feature/) (the guard
16
+ exists before the first keystroke in Codespaces / Gitpod / a local rebuild) and a
17
+ [GitHub Action](action.yml).
18
+
19
+ **There is no `curl … | sh` installer, deliberately.** Shomra's own Tier-0 guard
20
+ blocks piping a downloaded script into a shell, and the rules block it writes
21
+ tells coding agents never to do it. Shipping that one-liner would be the product
22
+ contradicting its own control in its own README.
23
+
13
24
  ```bash
14
25
  npm i -g @shomra/agent # global `shomra`
15
26
  # or run without installing:
@@ -40,6 +51,8 @@ shomra fix .mcp.json --apply # AI-fix one artifact and write it back
40
51
  shomra why .mcp.json # why each finding matters + is-it-a-false-positive
41
52
  shomra install-precommit # block risky staged AI artifacts on git commit
42
53
  shomra install-hook --agent claude # wire the runtime firewall into Claude Code
54
+ shomra rules --write # teach the agent what gets blocked, so it never writes it
55
+ shomra mcp install # let the agent gate its own content BEFORE writing it
43
56
  shomra scan # discover AI tooling on this machine
44
57
  shomra status # config + firewall health
45
58
  shomra help # full command list
@@ -66,6 +79,18 @@ shomra gate my-skill/SKILL.md # vet ONE artifact (auto-classified from
66
79
  shomra gate --all . # vet every AI artifact in the repo (the CI form)
67
80
  ```
68
81
 
82
+ ### Starting a new agent project
83
+
84
+ ```bash
85
+ shomra new agent triage-bot # a project that starts compliant
86
+ ```
87
+
88
+ Guard enforcing on every model call, an egress allowlist in code rather than in
89
+ the prompt, untrusted input kept out of the system prompt, secrets referenced
90
+ from the environment, and the gate wired into CI — from commit zero. Remediating
91
+ a project into this shape later means changing decisions that have already been
92
+ built on.
93
+
69
94
  ## `gate` in CI
70
95
 
71
96
  `gate`/`gate --all` are **local-first**: real static analysis (dangerous shell,
@@ -74,14 +99,39 @@ install-lure prose) runs on-machine, so you get a genuine verdict even if the
74
99
  backend is unreachable. When enrolled + reachable, your **org policy** is layered
75
100
  on top.
76
101
 
77
- **Exit codes:** `0` = allowed · `1` = blocked (or `--strict` + backend outage) · `2` = flagged with `--strict`.
102
+ **Exit codes** (one convention across every command):
103
+
104
+ | Code | Meaning |
105
+ |------|---------|
106
+ | `0` | clean / pass |
107
+ | `1` | hard fail — BLOCK, vulnerable model, secret found, FAIL verdict, below `--min`, regression (also `--strict` + backend outage) |
108
+ | `2` | soft fail — FLAG under `--strict` (REVIEW when strict) |
109
+ | `3` | usage / config error — not configured, bad flags, unknown command |
78
110
 
79
111
  **Backend outage:** by default it falls back to the on-machine verdict (org
80
112
  policy not applied). `--strict` fails closed (exit 1) because org policy can't be
81
113
  verified. Every backend call is bounded by `SHOMRA_API_TIMEOUT_MS` (default 30s),
82
114
  so a job never hangs.
83
115
 
84
- ### GitHub Actions
116
+ ### GitHub Actions — the reusable action
117
+
118
+ ```yaml
119
+ - uses: actions/checkout@v4
120
+ - uses: shomra-org/agent@v0
121
+ with:
122
+ args: check # --strict is appended unless fail-on-flag: 'false'
123
+ api-key: ${{ secrets.SHOMRA_API_KEY }} # optional — the gate is local-first
124
+ url: ${{ secrets.SHOMRA_URL }}
125
+ ```
126
+
127
+ Wire it as a **required status check** on a protected branch and it becomes the
128
+ un-bypassable control on GitHub.com, which has no server-side hooks. On
129
+ self-hosted Git and GitHub Enterprise, `shomra install-precommit --pre-receive`
130
+ (below) refuses the push itself.
131
+
132
+ The hand-written workflow below still works and shows what the action does.
133
+
134
+ ### GitHub Actions — by hand
85
135
 
86
136
  ```yaml
87
137
  name: Shomra AI-artifact gate
@@ -153,6 +203,9 @@ runs local-first; with a key, your **org policy** (below) drives the verdict.
153
203
  with: { sarif_file: shomra.sarif }
154
204
  ```
155
205
 
206
+ `shomra pr` accepts it too: bare `--sarif` writes SARIF to stdout,
207
+ `--sarif=shomra.sarif` writes a file alongside the normal check-run output.
208
+
156
209
  ### Org policy + triage on top of CI
157
210
 
158
211
  When enrolled, the same **org policy** that governs the dashboard decides the CI
@@ -180,6 +233,25 @@ shomra-gate:
180
233
  SHOMRA_URL: $SHOMRA_URL
181
234
  ```
182
235
 
236
+ ### pre-receive (server-side, cannot be skipped)
237
+
238
+ ```bash
239
+ shomra install-precommit --pre-receive /srv/git/your-repo.git
240
+ ```
241
+
242
+ A pre-commit hook is a courtesy: it lives on the developer's machine, it is one
243
+ `--no-verify` away, and a machine that never ran `install-precommit` has no gate
244
+ at all. A pre-receive hook runs on the **server**, on every push, for every
245
+ developer. Same check; the difference between a reminder and a control.
246
+
247
+ It **fails closed** — the opposite of the client hook. Blocking a local commit
248
+ because a binary is missing is hostile; waving a push through for the same reason
249
+ makes deleting the binary the bypass.
250
+
251
+ Available on self-hosted Git (GitLab, Gitea, Bitbucket DC, plain bare repos) and
252
+ GitHub Enterprise. GitHub.com does not run server-side hooks — use the action as
253
+ a required status check instead.
254
+
183
255
  ### pre-commit (local, blocks risky artifacts before they land)
184
256
 
185
257
  `.git/hooks/pre-commit` (or a [pre-commit](https://pre-commit.com) `local` hook):
@@ -206,6 +278,231 @@ backend, behind a short timeout + circuit breaker — so a slow or down backend
206
278
  never freezes the agent. Fail-open by default; `SHOMRA_GUARD_STRICT=1` fails
207
279
  closed on the server tier.
208
280
 
281
+ Three channels are screened:
282
+
283
+ | Channel | Hook | What it stops |
284
+ |---|---|---|
285
+ | **Tool call** | PreToolUse / `beforeShellExecution` | the shell command, artifact write or MCP call, before it runs |
286
+ | **Tool result** | PostToolUse / `afterMCPExecution` | injection, exfil sinks and hidden payloads in what a fetch/read brings *back* |
287
+ | **Prompt** | `UserPromptSubmit` (Claude Code) / `beforeSubmitPrompt` (Cursor) | what **you** paste, before it leaves the machine |
288
+ | **Plan** | `PreToolUse` on `ExitPlanMode` (Claude Code) | nothing — it *informs*. See [`shomra plan`](#shomra-plan--threat-model-what-the-agent-is-about-to-build) |
289
+
290
+ The prompt channel is the one a person controls, and the only one where the leak
291
+ is a paste rather than a tool call. A live credential in a prompt is refused;
292
+ pasted text that reads as an instruction to an agent is passed through but
293
+ flagged **to the model** as untrusted data rather than blocked — you meant to
294
+ send it, the risk is that you did not read it. Backtick-quoted payloads are
295
+ down-ranked, so asking *why does `<pattern>` get flagged* is never blocked.
296
+ `SHOMRA_PROMPT_GUARD_OFF=1` disables just this channel. Only the two vendors with
297
+ a documented pre-submit hook that can stop a submission are wired; the rest get
298
+ nothing rather than a guessed event name that would silently never fire.
299
+
300
+ ## Prevention: get in front of the model
301
+
302
+ Everything above intercepts *after* the model has written something. These run
303
+ before it.
304
+
305
+ ```bash
306
+ shomra design docs/rfc.md # threat-model a system that does not exist yet
307
+ shomra add model owner/m # vet anything before it lands on the machine
308
+ shomra rules --write # teach the agent what gets blocked here
309
+ shomra rules --check # CI: fail when the block goes stale
310
+ shomra mcp install # let the agent gate its own content before writing it
311
+ ```
312
+
313
+ ### `shomra design` — threat-model the ticket, not the repo
314
+
315
+ Every other command needs an artifact. This one reads a **description** — an RFC,
316
+ a design doc, a Jira/Linear ticket, a PR body — and answers the only question
317
+ worth asking before anyone writes code: does the thing being described hand an
318
+ attacker a path from untrusted input to a consequence?
319
+
320
+ ```bash
321
+ shomra design docs/rfc-042.md
322
+ shomra design docs/ --strict # every design doc, fail on any closed path
323
+ gh issue view 42 --json body -q .body | shomra design -
324
+ shomra design docs/rfc-042.md --checklist | gh issue comment 42 -F -
325
+ ```
326
+
327
+ It uses the platform's own model: capabilities split into **sources** (untrusted
328
+ input, sensitive data, filesystem) and **sinks** (network egress, execution,
329
+ destructive action). A closed source→sink pair is an attack path. That model does
330
+ not care whether the capabilities came from a scan or from a sentence — here they
331
+ come from a sentence, and each one cites the line that evidenced it so you can
332
+ disagree with the machine's reading.
333
+
334
+ `--checklist` emits the conditions as a markdown task list, which is the form
335
+ anyone actually acts on: paste it into the ticket as acceptance criteria.
336
+
337
+ > **It reads prose, so it sees only what was written down.** There is deliberately
338
+ > no clean verdict. `NOT_DESCRIBED` means the document did not describe
339
+ > capabilities in a way this matched — it is **not** a statement that the system
340
+ > has none. A threat model that reads as a clean bill of health is worse than
341
+ > none, because it is consumed exactly when the design is still cheap to change.
342
+
343
+ Exit codes: `1` when untrusted input reaches execution or a destructive action
344
+ (the shape where the attacker picks the action), `2` for any other closed path
345
+ under `--strict`.
346
+
347
+ ### `shomra plan` — threat-model what the agent is about to build
348
+
349
+ `design` reads a document a human remembered to write. Coding agents produce a
350
+ **plan** before every non-trivial task, constantly and automatically, and nothing
351
+ looks at it. Same analysis, a hundred times the frequency, zero human effort.
352
+
353
+ The loop: agent proposes a plan → Shomra threat-models it → the controls land in
354
+ the agent's context **before it writes line one**. The agent builds the guarded
355
+ version first, instead of building the unguarded one and having the firewall
356
+ refuse it three tool calls later.
357
+
358
+ Three ways in, deliberately redundant, strongest first:
359
+
360
+ 1. **`shomra_review_plan`** — an MCP tool, so any MCP-capable agent can call it
361
+ mid-task with no vendor hook. Register it with `shomra mcp install`.
362
+ 2. **The rules block asks the agent to call it.** Once the MCP server is
363
+ registered, `shomra rules --write` adds a *Before you implement a plan*
364
+ section — so `mcp install` and `rules --write` compose into a closed loop.
365
+ 3. **A Claude Code `PreToolUse` hook on `ExitPlanMode`**, wired by
366
+ `install-hook`. Zero-effort, but that tool name is not in the published hook
367
+ docs, so it is the optional path and never the only one.
368
+
369
+ ```bash
370
+ shomra plan plan.md # or: … | shomra plan -
371
+ ```
372
+
373
+ **A plan is a proposal, so the default is to inform, never refuse.** Denying a
374
+ plan spends a turn and tells the model only that it was wrong, not how — the
375
+ controls are the useful payload. Only untrusted-input-reaches-a-hard-sink
376
+ escalates to *ask*, and only under `SHOMRA_GUARD_STRICT=1`.
377
+ `SHOMRA_PLAN_GUARD_OFF=1` disables just this channel.
378
+
379
+ ### `shomra corpus` — screen the index, not the retrieval
380
+
381
+ The result firewall screens what a retrieval brings *back*. Nothing screened what
382
+ went **in** — so a poisoned document sits in the vector store indefinitely,
383
+ clean-until-retrieved, and is judged for the first time at the worst possible
384
+ moment: as one chunk, stripped of its document, inside a request a user is
385
+ waiting on.
386
+
387
+ Index time wins on all three counts. The whole document is present, so a payload
388
+ split across paragraphs is visible. The cost is paid once per document instead of
389
+ once per retrieval. And a document that fails is simply never embedded — a
390
+ control rather than a detection.
391
+
392
+ ```bash
393
+ shomra corpus ./kb --manifest .shomra/corpus.json
394
+ ```
395
+
396
+ ```
397
+ ✗ QUARANTINE escalation.md
398
+ HIGH Injected instruction: "ignore all previous" (line 243 · chunk 19)
399
+ ✗ QUARANTINE hidden.md
400
+ CRITICAL Invisible / bidirectional characters
401
+ ⚠ 2 files could not be read — they are NOT covered by the result above:
402
+ 2 × binary format — no text extractor
403
+ ```
404
+
405
+ Findings carry the **chunk index**, not just the line, because retrieval returns
406
+ chunks and the chunk is what actually reaches the model. The manifest is the
407
+ point of the command: feed it to your ingestion job so a quarantined document is
408
+ never embedded.
409
+
410
+ > **Absence accounting is load-bearing.** Real corpora are mostly PDF, DOCX and
411
+ > PPTX — formats this cannot read. A screen that silently skips them and prints
412
+ > "clean" is a lie about the majority of the corpus, so every unreadable file is
413
+ > counted and reported next to the verdict, and `--strict` fails on them:
414
+ > *we could not check it* is not *it is fine*.
415
+
416
+ Fenced code blocks are down-ranked — a docs corpus is full of examples, and an
417
+ example is not a live instruction. A directive in prose is the real threat and
418
+ survives the down-rank.
419
+
420
+ ### `shomra add` — vet at acquisition, not after
421
+
422
+ `mcp add` gated one channel. An agent acquires from four, and the other three had
423
+ no gate at all: a skill copied out of a gist, a model pulled from the Hub, a
424
+ package installed because an agent suggested the name.
425
+
426
+ ```bash
427
+ shomra add mcp files npx -y @modelcontextprotocol/server-filesystem /tmp
428
+ shomra add skill ./downloaded-skill # manifest AND the scripts it bundles
429
+ shomra add model openai-community/gpt2 # against the Model Index, before any weights download
430
+ shomra add package langchian --type pypi # → BLOCK: 2 edits from langchain
431
+ ```
432
+
433
+ One verdict vocabulary (ALLOW / FLAG / BLOCK), one exit-code contract, `--force`
434
+ to override a BLOCK deliberately rather than by accident. After something lands
435
+ the question changes from *should we take this?* to *is it safe to remove?*,
436
+ which is a much worse question to be asked.
437
+
438
+ **Unknown is never clean.** An unreachable Model Index, an unscanned model, and a
439
+ package the AI catalog does not recognise all return **FLAG**, not ALLOW —
440
+ "we could not check" and "it is fine" are different answers.
441
+
442
+ **`shomra rules`** compiles what Shomra actually enforces — plus what *this repo*
443
+ already trips, plus your org's policy when enrolled — into the agent's own
444
+ context files:
445
+
446
+ | Agent | File |
447
+ |---|---|
448
+ | Claude Code | `CLAUDE.md` |
449
+ | Codex CLI (and the cross-vendor default) | `AGENTS.md` |
450
+ | Cursor | `.cursor/rules/shomra.mdc` |
451
+ | GitHub Copilot | `.github/copilot-instructions.md` |
452
+ | Gemini CLI | `GEMINI.md` |
453
+ | Windsurf | `.windsurfrules` |
454
+ | Cline | `.clinerules/shomra.md` |
455
+
456
+ It writes inside a `<!-- BEGIN SHOMRA MANAGED BLOCK -->` marker pair and **never
457
+ touches a line outside it**, so your own rules are safe and re-running is a
458
+ no-op. The block is derived, not boilerplate: sections switch on according to
459
+ what the repo holds (MCP configs, skills, model loads, agent-calling code), and
460
+ an **"Already present in this repo"** section names the findings a local gate
461
+ pass actually found, with paths. Commit the result and keep it honest with
462
+ `shomra rules --check`, which exits 1 when the block is missing or stale.
463
+
464
+ The generated block is itself an AI rules file, so `shomra rules` gates its own
465
+ output and refuses to write anything its own checker would block.
466
+
467
+ **`shomra mcp install`** registers Shomra *as* an MCP server with your agents, so
468
+ the model can call it in its own loop — most usefully `shomra_review_change`,
469
+ which takes proposed file content plus its intended path and returns a verdict
470
+ **without writing anything to disk**. A BLOCK there costs nothing; the same
471
+ content on disk costs a blocked tool call and a wasted turn. `shomra_rules`,
472
+ `shomra_check`, `shomra_explain`, `shomra_fix` and `shomra_scan_models` are
473
+ exposed too. `shomra mcp serve` runs the server directly (stdio JSON-RPC) if you
474
+ prefer to wire it by hand.
475
+
476
+ ## Adopting Shomra on an existing repo
477
+
478
+ A brand-new gate on a repo with history will flag things. Three layers make
479
+ adoption friction-free — all of them re-grade the artifact, so a fully
480
+ suppressed file drops to ALLOW and never fails the build:
481
+
482
+ - **`shomra baseline`** records every current finding (line-independent
483
+ fingerprints) in `.shomra/baseline.json` — commit it so the whole team shares
484
+ it. From then on only findings introduced *after* the baseline fail; re-run it
485
+ to refresh after cleanups. Skip it per-run with `--no-baseline`.
486
+ - **`.shomraignore`** — a repo file of `path/glob` lines (skip the file) or
487
+ `path/glob :: title-substring` lines (skip one finding class in those files).
488
+ The runtime firewall honors it too, so test fixtures and detection source
489
+ aren't withheld. Silence a single finding inline with `// shomra-ignore` (or
490
+ `# shomra-ignore`) on the finding's line or the line above, or opt a whole
491
+ file out with `shomra-ignore-file` in its first lines (works in JSON as a
492
+ `"_shomra": "shomra-ignore-file"` key). `--no-suppress` ignores all of this.
493
+ - **`.shomra/policy.yml`** — policy-as-code, reviewed in PRs like any code:
494
+
495
+ ```yaml
496
+ block: high # min severity that BLOCKS (critical|high|medium|low|none)
497
+ flag: medium # min severity that FLAGS
498
+ allow: # finding-title substrings to always downgrade away
499
+ - "IPv4 address"
500
+ ```
501
+
502
+ For a local verdict the repo policy fully re-grades; when the backend
503
+ returned an org decision it can only make it *stricter* (worst-wins) — repo
504
+ config never loosens org enforcement. `--no-policy` skips it.
505
+
209
506
  ## Environment variables
210
507
 
211
508
  | Var | Purpose |
@@ -214,9 +511,19 @@ closed on the server tier.
214
511
  | `SHOMRA_URL` | Backend URL (overrides config) |
215
512
  | `SHOMRA_API_TIMEOUT_MS` | Per-request backend timeout (default 30000) |
216
513
  | `SHOMRA_AGENT` | Agent-identity handle presented to `llm-proxy` + firewall |
514
+ | `SHOMRA_GATE_CONCURRENCY` | Parallel backend calls in batch gate / model lookups (default 8, 1–32) |
515
+ | `SHOMRA_GH_TOKEN` | GitHub token for `shomra pr` (falls back to `GITHUB_TOKEN`) |
217
516
  | `SHOMRA_GUARD_STRICT` | `1` = firewall fails closed on the server tier |
218
517
  | `SHOMRA_GUARD_LOCAL` | `0` = disable the on-machine Tier-0 guard |
518
+ | `SHOMRA_GUARD_IGNORE` | Comma-separated file globs the runtime guard treats as known-safe (adds to `.shomraignore`) |
519
+ | `SHOMRA_GUARD_ALWAYS_ESCALATE` | `1` = send every call to the server (full telemetry, higher overhead) |
219
520
  | `SHOMRA_GUARD_TIMEOUT_MS` | Firewall per-call server timeout (default 2000) |
220
521
  | `SHOMRA_GUARD_BREAKER_MS` | Skip the server this long after a failure (default 30000; `0` disables) |
522
+ | `SHOMRA_LLM_PROXY_BASE` | Proxy base URL `install-hook` writes for Aider (default `http://127.0.0.1:4141/openai/v1`) |
523
+ | `SHOMRA_MODEL_GUARD` | `0` = disable the model-load screen in the PreToolUse hook |
524
+ | `SHOMRA_PROMPT_GUARD_OFF` | `1` = disable the prompt channel only (tool-call and tool-result guards stay on) |
525
+ | `SHOMRA_PLAN_GUARD_OFF` | `1` = disable the plan channel only |
526
+ | `SHOMRA_MODEL_CACHE` | `0` = disable the on-machine model-index verdict cache |
527
+ | `SHOMRA_MODEL_CACHE_TTL_MS` | Model-cache freshness window (default 7 days) |
221
528
 
222
529
  Run `shomra help` for the full command reference.
package/ai-usage.mjs CHANGED
@@ -60,6 +60,35 @@ const PROVIDERS = [
60
60
  { id: 'llama-cpp', label: 'llama.cpp', category: 'local-runtime', npm: ['node-llama-cpp'], py: ['llama_cpp'], call: [/\bLlama\s*\(\s*model_path\s*=/] },
61
61
  ];
62
62
 
63
+ /**
64
+ * Every AI package name this catalog knows, flattened for name comparison at
65
+ * ACQUISITION time (`shomra add package`). A typosquat is only detectable
66
+ * against a list of the real names, and this catalog is already that list —
67
+ * maintaining a second copy is how the two drift and the check quietly stops
68
+ * matching the packages people actually install.
69
+ *
70
+ * `ecosystem` matters: `openai` exists on both npm and PyPI, but `crewai` is
71
+ * PyPI-only, so `npm i crewai` is a different and more suspicious event than
72
+ * `pip install crewai`.
73
+ */
74
+ export const KNOWN_AI_PACKAGES = (() => {
75
+ const out = [];
76
+ const seen = new Set();
77
+ const add = (name, ecosystem, p) => {
78
+ const key = `${ecosystem}:${name}`;
79
+ if (!name || seen.has(key)) return;
80
+ seen.add(key);
81
+ out.push({ name, ecosystem, provider: p.id, label: p.label, category: p.category });
82
+ };
83
+ for (const p of PROVIDERS) {
84
+ for (const n of p.npm ?? []) add(n, 'npm', p);
85
+ for (const n of p.npmPrefix ?? []) add(n.replace(/\/$/, ''), 'npm', p);
86
+ for (const n of p.py ?? []) add(n, 'pypi', p);
87
+ for (const n of p.pyRoot ?? []) add(n, 'pypi', p);
88
+ }
89
+ return out;
90
+ })();
91
+
63
92
  const MODEL_ON_LINE = /\bmodel(?:_?id|_?name)?\s*[=:]\s*['"]([A-Za-z0-9][\w.:\/-]{1,80})['"]/;
64
93
  const MAX_CODE_LEN = 240;
65
94
  const clipLine = (s) => (s.length > MAX_CODE_LEN ? s.slice(0, MAX_CODE_LEN) + '…' : s);