@shomra/agent 0.3.26 → 0.3.27

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shomra/agent",
3
- "version": "0.3.26",
3
+ "version": "0.3.27",
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": {
@@ -0,0 +1,67 @@
1
+ import { PII_PATTERNS, SECRET_PATTERNS, isPlaceholderSecret, luhnValid } from './signals/secrets.mjs';
2
+
3
+ const MAX_TEXT = 200_000;
4
+ const MAX_SPANS = 200;
5
+
6
+
7
+ export function redactLocally(text, opts = {}) {
8
+ const src = String(text ?? '');
9
+ if (!src || src.length > MAX_TEXT) return { text: src, masked: [], unmaskable: [], changed: false };
10
+
11
+ const categories = opts.categories ?? ['secret', 'pii'];
12
+ const spans = [];
13
+
14
+ const collect = (re, label, category) => {
15
+ const rx = new RegExp(re.source, re.flags.includes('g') ? re.flags : re.flags + 'g');
16
+ let m;
17
+ let guard = 0;
18
+ while ((m = rx.exec(src)) !== null && guard++ < MAX_SPANS) {
19
+ if (!m[0]) { rx.lastIndex += 1; continue; }
20
+ if (category === 'secret' && isPlaceholderSecret(m[0])) continue;
21
+ if (label === 'Credit card number' && !luhnValid(m[0])) continue;
22
+ spans.push({ start: m.index, end: m.index + m[0].length, label, category });
23
+ }
24
+ };
25
+
26
+ if (categories.includes('secret')) for (const { name, re } of SECRET_PATTERNS) collect(re, name, 'secret');
27
+ if (categories.includes('pii')) for (const { name, re } of PII_PATTERNS) collect(re, name, 'pii');
28
+
29
+ if (!spans.length) return { text: src, masked: [], unmaskable: [], changed: false };
30
+
31
+ spans.sort((a, b) => a.start - b.start || b.end - a.end);
32
+ const merged = [];
33
+ for (const s of spans) {
34
+ const last = merged[merged.length - 1];
35
+ if (last && s.start < last.end) {
36
+ last.end = Math.max(last.end, s.end);
37
+ if (!last.labels.includes(s.label)) last.labels.push(s.label);
38
+ continue;
39
+ }
40
+ merged.push({ start: s.start, end: s.end, labels: [s.label], category: s.category });
41
+ }
42
+
43
+ let out = '';
44
+ let cursor = 0;
45
+ const masked = [];
46
+ for (const m of merged) {
47
+ out += src.slice(cursor, m.start);
48
+ out += `[shomra:redacted:${m.category}]`;
49
+ cursor = m.end;
50
+ masked.push({ label: m.labels.join(' + '), category: m.category, chars: m.end - m.start });
51
+ }
52
+ out += src.slice(cursor);
53
+
54
+ return { text: out, masked, unmaskable: [], changed: true };
55
+ }
56
+
57
+
58
+ export function unmaskableFindings(findings, redaction) {
59
+ const maskedLabels = new Set((redaction?.masked ?? []).flatMap((m) => String(m.label).split(' + ')));
60
+ return (findings ?? [])
61
+ .filter((f) => f && (f.category === 'secret' || f.category === 'pii'))
62
+ .filter((f) => {
63
+ const label = String(f.label ?? '').replace(/^Live credential:\s*/, '');
64
+ return !maskedLabels.has(label);
65
+ })
66
+ .map((f) => f.label);
67
+ }
@@ -3,6 +3,7 @@ import { gateMachine } from '../core/api-client.mjs';
3
3
  import { breakerOpen, breakerReset, breakerTrip, guardTimeoutMs } from '../core/circuit-breaker.mjs';
4
4
  import { loadConfig, resolveSettings } from '../core/config.mjs';
5
5
  import { downrankCodeContext, localScan } from '../detect/guard-signals.mjs';
6
+ import { redactLocally } from '../detect/local-redact.mjs';
6
7
  import { detectEnv } from '../gate/environment.mjs';
7
8
  import { parentSessionFrom } from './normalize.mjs';
8
9
  import { envFlag, resolveAgentFlag } from './options.mjs';
@@ -169,10 +170,24 @@ function promptInjectionNote(injection) {
169
170
  );
170
171
  }
171
172
 
173
+
172
174
  function buildPromptGuardBody(norm, agent, clientDecision, clientReason) {
175
+ const redaction = localTierDisabled()
176
+ ? { text: norm.prompt, masked: [], changed: false }
177
+ : redactLocally(norm.prompt);
178
+
173
179
  return {
174
180
  tool_name: 'UserPromptSubmit',
175
- tool_input: { prompt: norm.prompt },
181
+ tool_input: { prompt: redaction.text },
182
+ ...(redaction.changed
183
+ ? {
184
+ client_masked: {
185
+ count: redaction.masked.length,
186
+ labels: redaction.masked.map((m) => m.label).slice(0, 20),
187
+ where: 'client',
188
+ },
189
+ }
190
+ : {}),
176
191
  cwd: norm.cwd,
177
192
  session_id: norm.session_id,
178
193
  ...(norm.parent_session_id ? { parent_session_id: norm.parent_session_id } : {}),