@shomra/agent 0.3.16 → 0.3.18

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 (156) hide show
  1. package/NOTICE +1 -1
  2. package/README.md +57 -57
  3. package/package.json +3 -9
  4. package/shomra.mjs +9 -7168
  5. package/src/agents/hook-command.mjs +19 -0
  6. package/src/agents/hook-files.mjs +41 -0
  7. package/src/agents/installers.mjs +203 -0
  8. package/src/artifacts/matchers.mjs +59 -0
  9. package/src/artifacts/report.mjs +50 -0
  10. package/src/cli/flags.mjs +68 -0
  11. package/src/cli/help-sections.mjs +309 -0
  12. package/src/cli/help.mjs +27 -0
  13. package/src/cli/main.mjs +55 -0
  14. package/src/cli/registry.mjs +80 -0
  15. package/src/cli/suggestions.mjs +33 -0
  16. package/src/commands/add.mjs +149 -0
  17. package/src/commands/agent-identity.mjs +46 -0
  18. package/src/commands/check.mjs +194 -0
  19. package/src/commands/corpus.mjs +126 -0
  20. package/src/commands/design.mjs +168 -0
  21. package/src/commands/doctor.mjs +209 -0
  22. package/src/commands/fix.mjs +115 -0
  23. package/src/commands/gate.mjs +154 -0
  24. package/src/commands/git-hooks.mjs +163 -0
  25. package/src/commands/init.mjs +36 -0
  26. package/src/commands/install-hook.mjs +51 -0
  27. package/src/commands/llm-proxy.mjs +153 -0
  28. package/src/commands/mcp-add.mjs +185 -0
  29. package/src/commands/mcp.mjs +143 -0
  30. package/src/commands/memory-scan.mjs +181 -0
  31. package/src/commands/model-scan.mjs +99 -0
  32. package/src/commands/models.mjs +145 -0
  33. package/src/commands/new.mjs +64 -0
  34. package/src/commands/plan.mjs +87 -0
  35. package/src/commands/pr.mjs +249 -0
  36. package/src/commands/protect.mjs +38 -0
  37. package/src/commands/provenance.mjs +91 -0
  38. package/src/commands/redteam.mjs +166 -0
  39. package/src/commands/rules.mjs +220 -0
  40. package/src/commands/run.mjs +128 -0
  41. package/src/commands/scan-zip.mjs +118 -0
  42. package/src/commands/scan.mjs +102 -0
  43. package/src/commands/secrets.mjs +99 -0
  44. package/src/commands/status.mjs +50 -0
  45. package/src/commands/why.mjs +88 -0
  46. package/src/core/api-client.mjs +66 -0
  47. package/src/core/api-key.mjs +6 -0
  48. package/src/core/circuit-breaker.mjs +42 -0
  49. package/src/core/config.mjs +37 -0
  50. package/src/core/exit-codes.mjs +9 -0
  51. package/src/core/json-file.mjs +13 -0
  52. package/src/core/numbers.mjs +4 -0
  53. package/src/core/package-root.mjs +10 -0
  54. package/src/core/terminal.mjs +16 -0
  55. package/src/core/version.mjs +14 -0
  56. package/src/core/wire-limits.mjs +53 -0
  57. package/src/corpus/screening.mjs +127 -0
  58. package/{ai-usage.mjs → src/detect/ai-usage.mjs} +0 -27
  59. package/src/detect/code-sast.mjs +2 -0
  60. package/{design.mjs → src/detect/design.mjs} +18 -107
  61. package/src/detect/guard-signals.mjs +18 -0
  62. package/{model-refs.mjs → src/detect/model-refs.mjs} +18 -77
  63. package/src/detect/sast/chains.mjs +30 -0
  64. package/src/detect/sast/path-expressions.mjs +76 -0
  65. package/src/detect/sast/rules-chains.mjs +33 -0
  66. package/src/detect/sast/rules-config.mjs +51 -0
  67. package/src/detect/sast/rules-javascript.mjs +109 -0
  68. package/src/detect/sast/rules-python.mjs +292 -0
  69. package/src/detect/sast/scanner.mjs +104 -0
  70. package/src/detect/sast/source-lines.mjs +115 -0
  71. package/src/detect/sast/taint.mjs +71 -0
  72. package/src/detect/signals/artifacts.mjs +113 -0
  73. package/src/detect/signals/autonomy.mjs +55 -0
  74. package/src/detect/signals/config-markers.mjs +28 -0
  75. package/src/detect/signals/credential-harvest.mjs +64 -0
  76. package/src/detect/signals/durable-claims.mjs +73 -0
  77. package/src/detect/signals/egress.mjs +56 -0
  78. package/src/detect/signals/execution-hijack.mjs +128 -0
  79. package/src/detect/signals/gate.mjs +91 -0
  80. package/src/detect/signals/injection.mjs +55 -0
  81. package/src/detect/signals/lines.mjs +42 -0
  82. package/src/detect/signals/masking.mjs +99 -0
  83. package/src/detect/signals/memory.mjs +357 -0
  84. package/src/detect/signals/packages.mjs +45 -0
  85. package/src/detect/signals/propagation.mjs +86 -0
  86. package/src/detect/signals/prose-context.mjs +82 -0
  87. package/src/detect/signals/scan.mjs +91 -0
  88. package/src/detect/signals/secrets.mjs +85 -0
  89. package/src/detect/signals/sensitive.mjs +9 -0
  90. package/src/detect/signals/severity.mjs +10 -0
  91. package/src/detect/signals/shell.mjs +96 -0
  92. package/src/detect/signals/staged-fetch.mjs +66 -0
  93. package/src/detect/signals/text-match.mjs +35 -0
  94. package/src/gate/batch.mjs +157 -0
  95. package/src/gate/environment.mjs +122 -0
  96. package/src/gate/repo-policy.mjs +65 -0
  97. package/src/gate/result.mjs +53 -0
  98. package/src/gate/sarif.mjs +33 -0
  99. package/src/gate/sast.mjs +64 -0
  100. package/src/gate/suppressions.mjs +0 -0
  101. package/src/guard/classify.mjs +50 -0
  102. package/src/guard/emit.mjs +51 -0
  103. package/src/guard/ignore.mjs +24 -0
  104. package/src/guard/ledger.mjs +112 -0
  105. package/src/guard/model-load.mjs +50 -0
  106. package/src/guard/normalize.mjs +77 -0
  107. package/src/guard/options.mjs +10 -0
  108. package/src/guard/prompt-guard.mjs +184 -0
  109. package/src/guard/report.mjs +35 -0
  110. package/src/guard/result-guard.mjs +140 -0
  111. package/src/guard/tool-guard.mjs +166 -0
  112. package/src/inventory/agent-artifacts.mjs +5 -0
  113. package/src/inventory/agent-posture.mjs +249 -0
  114. package/src/inventory/artifacts/classify.mjs +27 -0
  115. package/src/inventory/artifacts/discover.mjs +187 -0
  116. package/src/inventory/artifacts/file-read.mjs +42 -0
  117. package/src/inventory/artifacts/hooks.mjs +14 -0
  118. package/src/inventory/artifacts/limits.mjs +37 -0
  119. package/src/inventory/artifacts/marketplaces.mjs +45 -0
  120. package/src/inventory/artifacts/roots.mjs +20 -0
  121. package/src/inventory/artifacts/walk.mjs +36 -0
  122. package/src/inventory/discovery/ai-dependencies.mjs +161 -0
  123. package/src/inventory/discovery/ai-tools.mjs +23 -0
  124. package/src/inventory/discovery/all.mjs +40 -0
  125. package/src/inventory/discovery/coding-agents.mjs +77 -0
  126. package/src/inventory/discovery/fs-read.mjs +36 -0
  127. package/src/inventory/discovery/local-runtimes.mjs +53 -0
  128. package/src/inventory/discovery/mcp-clients.mjs +67 -0
  129. package/src/inventory/discovery/mcp-servers.mjs +78 -0
  130. package/src/inventory/discovery/model-keys.mjs +97 -0
  131. package/src/inventory/discovery/platform.mjs +16 -0
  132. package/src/inventory/discovery/rules-files.mjs +25 -0
  133. package/src/inventory/discovery/vector-stores.mjs +176 -0
  134. package/src/inventory/discovery/workspace.mjs +124 -0
  135. package/src/inventory/discovery.mjs +10 -0
  136. package/src/mcp/child-process.mjs +50 -0
  137. package/src/mcp/config-wrapping.mjs +75 -0
  138. package/src/mcp/connect-gate.mjs +45 -0
  139. package/src/mcp/hosts.mjs +16 -0
  140. package/src/mcp/jsonrpc.mjs +48 -0
  141. package/src/mcp/lookup.mjs +50 -0
  142. package/src/mcp/screening.mjs +103 -0
  143. package/src/mcp/server-tools.mjs +97 -0
  144. package/src/mcp/server.mjs +102 -0
  145. package/src/mcp/shim.mjs +205 -0
  146. package/src/models/lookup.mjs +79 -0
  147. package/src/models/references.mjs +103 -0
  148. package/src/rules/context.mjs +98 -0
  149. package/src/rules/generate.mjs +103 -0
  150. package/src/rules/sections.mjs +145 -0
  151. package/src/scaffold/agent-project.mjs +185 -0
  152. package/src/scaffold/artifact-templates.mjs +35 -0
  153. package/code-sast.mjs +0 -1063
  154. package/discovery.mjs +0 -977
  155. package/guard-ledger.mjs +0 -239
  156. package/guard-signals.mjs +0 -1268
package/guard-ledger.mjs DELETED
@@ -1,239 +0,0 @@
1
- /**
2
- * ─── THE FAIL-OPEN LEDGER (client half) ─────────────────────────────────────
3
- *
4
- * ── Why this exists ─────────────────────────────────────────────────────────
5
- *
6
- * The pre-tool-call guard FAILS OPEN, and it has to: an agent that hard-stops
7
- * because a SaaS backend is unreachable is an agent nobody keeps installed. The
8
- * breaker in `shomra.mjs` makes that cheap — once the backend times out, the
9
- * next calls skip the round-trip entirely for a cooldown window.
10
- *
11
- * ⚠ THE CONSEQUENCE IS THAT AN OUTAGE IS INVISIBLE ON THE SERVER. Every runtime
12
- * claim Shomra makes counts rows the backend WROTE, so a breaker-open window
13
- * produces no rows at all — byte-for-byte what a quiet, clean window produces.
14
- * "The guard was down for six hours" and "the guard saw nothing dangerous" are
15
- * the same evidence, and a reader takes the reassuring one.
16
- *
17
- * This module is the other end of `src/gate/enforcement-availability.ts` in the
18
- * backend. It remembers what this machine did while it was blind, and hands it
19
- * over on the next call that gets through — turning an absence of evidence into
20
- * evidence of an absence.
21
- *
22
- * ── ⚠ THE RULES ─────────────────────────────────────────────────────────────
23
- *
24
- * 1. **The envelope is sent ALWAYS, even empty.** Its PRESENCE is what tells the
25
- * backend this client is CAPABLE of reporting an outage. An old client sends
26
- * nothing, and nothing is also what a healthy client would send if this were
27
- * "optimised" to omit the empty case — at which point every healthy estate
28
- * becomes indistinguishable from an unobservable one. That change would look
29
- * like a bandwidth win in review. It is the whole feature.
30
- *
31
- * 2. **Only calls that WOULD have been screened are counted.** A call the guard
32
- * deliberately never escalates (benign, locally cleared, not policy-relevant
33
- * — the bulk of them) is a stated design boundary, not a gap. Counting those
34
- * would report every healthy machine as ~90% blind and the number would be
35
- * ignored within a week.
36
- *
37
- * 3. **Counts are LOWER BOUNDS and are allowed to be.** Hooks run as concurrent
38
- * short-lived processes, so two of them can read-modify-write this file at
39
- * once and lose an increment. The backend already treats every count here as
40
- * a floor for exactly this reason. ⚠ Do not "fix" that by making the writes
41
- * heavier — a lock on the firewall's hot path costs more than the precision
42
- * is worth, and the number is a floor either way.
43
- *
44
- * 4. **A window we cannot attest the END of closes as `null`, never as now().**
45
- * If this machine slept, crashed, or was rebooted mid-outage, the window on
46
- * disk is stale and we genuinely do not know when it ended. `closedAt: null`
47
- * is the backend's "we were never told it ended" — which it grades as
48
- * unmeasurable rather than as a zero-length blip.
49
- *
50
- * 5. **Nothing is ever DROPPED to stay under the cap.** Over the limit, the
51
- * oldest windows MERGE into one aggregate that keeps their summed counts and
52
- * spans their range. Truncating the list instead would silently delete
53
- * evidence of blindness, which is the one direction this file must never
54
- * fail in.
55
- *
56
- * PURE state machine + thin file I/O, split so the state rules are testable
57
- * without a filesystem (`tests/guard-ledger.test.mjs`).
58
- */
59
- import fs from 'node:fs';
60
- import path from 'node:path';
61
-
62
- /** Max gaps in one envelope. Matches `@ArrayMaxSize(50)` on the backend DTO. */
63
- export const MAX_GAPS = 50;
64
-
65
- /**
66
- * How long an open window may sit on disk before we stop claiming to know when
67
- * it ended. Six hours: comfortably longer than any real outage a 30s breaker
68
- * cooldown produces, and short enough that a laptop closed overnight does not
69
- * come back claiming a 14-hour measured blackout.
70
- */
71
- export const STALE_WINDOW_MS = 6 * 60 * 60 * 1000;
72
-
73
- /** The empty ledger. */
74
- export const emptyLedger = () => ({ open: null, pending: [] });
75
-
76
- /* ── The state machine (pure) ───────────────────────────────────────────── */
77
-
78
- /**
79
- * Begin a window, or leave an already-open one alone.
80
- *
81
- * ⚠ IDEMPOTENT ON PURPOSE. The breaker trips on every failed call, not just the
82
- * first, so a naive implementation would start a fresh window per call and
83
- * report a 200-call outage as 200 one-call outages — each with a tiny count,
84
- * none of them showing the real shape. The FIRST failure owns the window.
85
- */
86
- export function openWindow(state, { at, reason }) {
87
- const s = state ?? emptyLedger();
88
- if (s.open) return s;
89
- return { ...s, open: { openedAt: at, reason: String(reason || 'unknown').slice(0, 200), unscreened: 0, local: 0 } };
90
- }
91
-
92
- /**
93
- * Record one call that ran without a server verdict.
94
- *
95
- * `kind` is `'local'` when the on-machine Tier-0 engine screened it (a weaker
96
- * screen — no org policy, no identity, no flow, no supply chain, no intent) and
97
- * `'unscreened'` when nothing did.
98
- *
99
- * ⚠ IT OPENS A WINDOW IF NONE IS OPEN. A count with nowhere to live would be
100
- * dropped, and the paths that skip the round-trip (`breakerOpen()`) do not
101
- * themselves fail, so they never call `openWindow` on their own.
102
- */
103
- export function countCall(state, { at, kind, reason }) {
104
- const s = openWindow(state, { at, reason: reason || 'breaker-open' });
105
- const open = { ...s.open };
106
- if (kind === 'unscreened') open.unscreened += 1;
107
- else open.local += 1;
108
- return { ...s, open };
109
- }
110
-
111
- /**
112
- * End the open window and move it to the outbox.
113
- *
114
- * ⚠ A window that recorded NOTHING is discarded rather than reported. A breaker
115
- * that tripped on a call and healed before the next one cost no coverage, and a
116
- * zero-call gap row would be noise that makes the real ones harder to see.
117
- */
118
- export function closeWindow(state, { at, staleMs = STALE_WINDOW_MS } = {}) {
119
- const s = state ?? emptyLedger();
120
- if (!s.open) return s;
121
- const { openedAt, reason, unscreened, local } = s.open;
122
- if (!unscreened && !local) return { ...s, open: null };
123
- // Rule 4: too old to attest an end for.
124
- const stale = at - openedAt > staleMs;
125
- const gap = {
126
- opened_at: new Date(openedAt).toISOString(),
127
- ...(stale ? {} : { closed_at: new Date(at).toISOString() }),
128
- unscreened_calls: unscreened,
129
- locally_decided_calls: local,
130
- reason: stale ? `${reason} (end not observed)` : reason,
131
- };
132
- return { open: null, pending: compact([...s.pending, gap]) };
133
- }
134
-
135
- /**
136
- * Rule 5 — keep the list bounded WITHOUT losing counts.
137
- *
138
- * ⚠ The merged row deliberately carries `closed_at` only when every window it
139
- * absorbed had one. An aggregate spanning a window we could not attest the end
140
- * of is itself unattestable, and inventing a boundary for it would launder an
141
- * unmeasurable outage into a measured one.
142
- */
143
- export function compact(gaps, max = MAX_GAPS) {
144
- if (gaps.length <= max) return gaps;
145
- const overflow = gaps.slice(0, gaps.length - max + 1);
146
- const kept = gaps.slice(gaps.length - max + 1);
147
- const anyOpen = overflow.some((g) => !g.closed_at);
148
- const lastClose = overflow.reduce((acc, g) => (g.closed_at && (!acc || g.closed_at > acc) ? g.closed_at : acc), null);
149
- const merged = {
150
- opened_at: overflow[0].opened_at,
151
- ...(anyOpen || !lastClose ? {} : { closed_at: lastClose }),
152
- unscreened_calls: overflow.reduce((n, g) => n + (g.unscreened_calls || 0), 0),
153
- locally_decided_calls: overflow.reduce((n, g) => n + (g.locally_decided_calls || 0), 0),
154
- reason: `${overflow.length} earlier windows, merged`,
155
- };
156
- return [merged, ...kept];
157
- }
158
-
159
- /**
160
- * The envelope to attach to a request.
161
- *
162
- * ⚠ ALWAYS AN OBJECT, and `gaps` is always an array — see rule 1. Returning
163
- * `undefined` when there is nothing to report is the single change that would
164
- * silently break the whole design.
165
- */
166
- export function envelope(state, { version } = {}) {
167
- const s = state ?? emptyLedger();
168
- return { gaps: s.pending.slice(0, MAX_GAPS), ...(version ? { client_version: String(version).slice(0, 40) } : {}) };
169
- }
170
-
171
- /**
172
- * Drop the gaps a request confirmed delivery of.
173
- *
174
- * ⚠ Matched by `opened_at`, not by index or by count. A concurrent hook process
175
- * can append a new gap between building the envelope and acking it, and an
176
- * index-based drop would silently discard that one unreported. Re-sending a gap
177
- * the backend already has is free — it dedupes on (org, machine, openedAt).
178
- */
179
- export function ack(state, sent) {
180
- const s = state ?? emptyLedger();
181
- const done = new Set((sent ?? []).map((g) => g.opened_at));
182
- return { ...s, pending: s.pending.filter((g) => !done.has(g.opened_at)) };
183
- }
184
-
185
- /* ── File I/O (thin) ────────────────────────────────────────────────────── */
186
-
187
- /**
188
- * ⚠ EVERY OPERATION BELOW IS BEST-EFFORT AND SWALLOWS. This runs inside the
189
- * PreToolUse hook: a ledger that threw would break a tool call the guard had
190
- * already correctly allowed, which is a worse outcome than losing a count. The
191
- * counts are lower bounds by rule 3 regardless.
192
- */
193
- export function makeLedgerStore(configDir, { version } = {}) {
194
- const file = path.join(configDir, 'guard-ledger.json');
195
-
196
- const read = () => {
197
- try {
198
- const raw = JSON.parse(fs.readFileSync(file, 'utf8'));
199
- return { open: raw.open ?? null, pending: Array.isArray(raw.pending) ? raw.pending : [] };
200
- } catch {
201
- return emptyLedger();
202
- }
203
- };
204
-
205
- const write = (state) => {
206
- try {
207
- fs.mkdirSync(configDir, { recursive: true });
208
- // ⚠ Atomic rename, not a bare write. Two hook processes writing this file
209
- // concurrently can lose an increment (rule 3, accepted) — but a torn file
210
- // would lose the WHOLE ledger, including windows already closed and
211
- // waiting to be reported. `.tmp` is per-process so the two cannot collide.
212
- const tmp = `${file}.${process.pid}.tmp`;
213
- fs.writeFileSync(tmp, JSON.stringify(state));
214
- fs.renameSync(tmp, file);
215
- } catch {
216
- /* best-effort */
217
- }
218
- };
219
-
220
- const update = (fn) => {
221
- const next = fn(read());
222
- write(next);
223
- return next;
224
- };
225
-
226
- return {
227
- file,
228
- read,
229
- write,
230
- /** A call ran with no server verdict. */
231
- count: (kind, reason) => update((s) => countCall(s, { at: Date.now(), kind, reason })),
232
- /** The backend answered — close any window and hand back what to send. */
233
- close: () => update((s) => closeWindow(s, { at: Date.now() })),
234
- /** The envelope for this request. Always present (rule 1). */
235
- envelope: () => envelope(read(), { version }),
236
- /** Confirm delivery. */
237
- ack: (sent) => update((s) => ack(s, sent)),
238
- };
239
- }