@shomra/agent 0.3.25 → 0.3.26
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.
|
|
3
|
+
"version": "0.3.26",
|
|
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": {
|
|
@@ -9,7 +9,7 @@ export { localGate } from './signals/gate.mjs';
|
|
|
9
9
|
export { INJECTION_PHRASES, INJECTION_REGEXES, INVISIBLE_CHARS_RE } from './signals/injection.mjs';
|
|
10
10
|
export { localMemory } from './signals/memory.mjs';
|
|
11
11
|
export { AGENT_ROOT_RE, isAgentAdjacentPath, localPropagation } from './signals/propagation.mjs';
|
|
12
|
-
export { citationGoverns, isDocumentationLine, prohibitsAt } from './signals/prose-context.mjs';
|
|
12
|
+
export { citationGoverns, describesAt, isDocumentationLine, prohibitsAt } from './signals/prose-context.mjs';
|
|
13
13
|
export { downrankCodeContext, localScan } from './signals/scan.mjs';
|
|
14
14
|
export { PII_PATTERNS, SECRET_PATTERNS } from './signals/secrets.mjs';
|
|
15
15
|
export { NETWORK_VERBS, SENSITIVE_READ } from './signals/sensitive.mjs';
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDocumentationLine, prohibitsAt } from './prose-context.mjs';
|
|
1
|
+
import { describesAt, isDocumentationLine, prohibitsAt } from './prose-context.mjs';
|
|
2
2
|
|
|
3
3
|
const AUTONOMY_RULES = [
|
|
4
4
|
{ family: 'confirmation', label: 'Acts without asking', re: /\b(?:without (?:asking|confirming|prompting|waiting for|seeking)(?:\s+(?:the\s+)?(?:user|me|anyone|permission|approval|confirmation))?|do(?:es)? not (?:ask|prompt|wait|check|confirm)[^.\n]{0,30}\b(?:for|before|first|permission|approval|confirmation)|no need to (?:ask|confirm|check with))\b/i },
|
|
@@ -36,7 +36,7 @@ export function localAutonomy(text) {
|
|
|
36
36
|
const m = rule.re.exec(line);
|
|
37
37
|
if (!m) continue;
|
|
38
38
|
if (isDocumentationLine(line)) continue;
|
|
39
|
-
if (prohibitsAt(line, m.index)) continue;
|
|
39
|
+
if (prohibitsAt(line, m.index) || describesAt(line, m.index)) continue;
|
|
40
40
|
if (insideQuotedSpan(line, m.index)) continue;
|
|
41
41
|
seen.add(rule.label);
|
|
42
42
|
out.push({ family: rule.family, label: rule.label, line: i + 1 });
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isDocumentationLine, prohibitsAt } from './prose-context.mjs';
|
|
1
|
+
import { describesAt, isDocumentationLine, prohibitsAt } from './prose-context.mjs';
|
|
2
2
|
|
|
3
3
|
const HARVEST_PROMPTS = [
|
|
4
4
|
{ re: /\bosascript\b[\s\S]{0,200}?\bdisplay\s+dialog\b[\s\S]{0,200}?\bhidden\s+answer\b/i, label: 'osascript password dialog (hidden answer)' },
|
|
@@ -45,14 +45,14 @@ export function detectCredentialHarvest(text) {
|
|
|
45
45
|
for (const p of HARVEST_PROMPTS) {
|
|
46
46
|
const m = p.re.exec(line);
|
|
47
47
|
if (!m) continue;
|
|
48
|
-
if (isDocumentationLine(line) || prohibitsAt(line, m.index)) continue;
|
|
48
|
+
if (isDocumentationLine(line) || prohibitsAt(line, m.index) || describesAt(line, m.index)) continue;
|
|
49
49
|
push('interactive-prompt', p.label, 'CRITICAL', i, line);
|
|
50
50
|
}
|
|
51
51
|
for (const s of HARVEST_STORES) {
|
|
52
52
|
const m = s.re.exec(line);
|
|
53
53
|
if (!m) continue;
|
|
54
54
|
if (!HARVEST_READ_VERB.test(line)) continue;
|
|
55
|
-
if (isDocumentationLine(line) || prohibitsAt(line, m.index)) continue;
|
|
55
|
+
if (isDocumentationLine(line) || prohibitsAt(line, m.index) || describesAt(line, m.index)) continue;
|
|
56
56
|
push(s.family, s.label, HARVEST_EXFIL_VERB.test(line) ? 'CRITICAL' : 'HIGH', i, line);
|
|
57
57
|
}
|
|
58
58
|
const t = HARVEST_TOKEN_PATH.exec(line);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { CLAIM_LABEL, claimSeverity, detectDurableClaims } from './durable-claims.mjs';
|
|
2
2
|
import { LOCAL_URL_RE, egressHost } from './egress.mjs';
|
|
3
3
|
import { lineOf, lineTextAt } from './lines.mjs';
|
|
4
|
-
import { citationGoverns, isDescriptiveLine, isDocumentationLine, isRiskTableRow, prohibitsAt } from './prose-context.mjs';
|
|
4
|
+
import { citationGoverns, describesAt, isDescriptiveLine, isDocumentationLine, isRiskTableRow, prohibitsAt } from './prose-context.mjs';
|
|
5
5
|
import { localScan } from './scan.mjs';
|
|
6
6
|
import { NETWORK_VERBS, SENSITIVE_READ } from './sensitive.mjs';
|
|
7
7
|
import { DANGEROUS_SHELL } from './shell.mjs';
|
|
@@ -44,6 +44,7 @@ export function offendingLine(sig, text) {
|
|
|
44
44
|
if (sig.refine && !sig.refine(line)) continue;
|
|
45
45
|
if (isDocumentationLine(line)) continue;
|
|
46
46
|
if (prohibitsAt(line, line.indexOf(m[0]))) continue;
|
|
47
|
+
if (describesAt(line, line.indexOf(m[0]))) continue;
|
|
47
48
|
if (isRiskTableRow(line)) continue;
|
|
48
49
|
return line;
|
|
49
50
|
}
|
|
@@ -71,6 +71,59 @@ export function prohibitsAt(line, offset) {
|
|
|
71
71
|
return !DOUBLE_NEGATIVE_RE.test(before);
|
|
72
72
|
}
|
|
73
73
|
|
|
74
|
+
/**
|
|
75
|
+
* ⚠ AN OFFSET ON THE OPENING BACKTICK IS INSIDE THE SPAN. Several rules anchor
|
|
76
|
+
* on the backtick itself (a markdown code span and a shell command substitution
|
|
77
|
+
* are the same character), so counting only what precedes the offset put every
|
|
78
|
+
* code-span guard one character outside the span it was meant to be inside.
|
|
79
|
+
*/
|
|
80
|
+
function insideCodeSpan(line, offset) {
|
|
81
|
+
let ticks = 0;
|
|
82
|
+
for (let i = 0; i < offset && i < line.length; i++) if (line[i] === '`') ticks++;
|
|
83
|
+
if (ticks % 2 === 1) return true;
|
|
84
|
+
return line[offset] === '`' && line.indexOf('`', offset + 1) !== -1;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* ⚠ A LINE THAT NAMES A COMMAND AS ITS SUBJECT IS DESCRIBING IT — the second
|
|
89
|
+
* carve-out `carriesHardEvidence` needs, for the same reason `prohibitsAt` was
|
|
90
|
+
* the first. Security documentation is written in exactly this mood -
|
|
91
|
+
* *"`curl … | sh` is a pipe-to-shell installer"*, *"`chmod 777` means the file
|
|
92
|
+
* is world-writable"* - so offline, where no server verdict ever arrives to
|
|
93
|
+
* correct it, every threat model and runbook in a careful repo blocked.
|
|
94
|
+
*
|
|
95
|
+
* ⚠ THE LEAD-IN IS NOT THE SIGNAL, THE PREDICATE IS. "For example, run
|
|
96
|
+
* `curl … | sh`" is an instruction wearing a documentation opener, and
|
|
97
|
+
* "example" is one token an attacker adds for free. What cannot be faked
|
|
98
|
+
* cheaply is the command sitting in SUBJECT position with a copular or
|
|
99
|
+
* reporting verb after it: an instruction puts the command in OBJECT position
|
|
100
|
+
* after an imperative.
|
|
101
|
+
*
|
|
102
|
+
* ⚠ THE MATCH MUST BE INSIDE A CODE SPAN, and ⚠ AN IMPERATIVE BEFORE IT WINS.
|
|
103
|
+
*
|
|
104
|
+
* Mirrors `describesAt` in the backend's prose-context.ts — `local-mirror-bench`
|
|
105
|
+
* compares the two on every mood, in both directions.
|
|
106
|
+
*/
|
|
107
|
+
const DESCRIPTIVE_PREDICATE_RE =
|
|
108
|
+
/^\s*(?:,\s*)?(?:is|are|was|were|means?|meant|shows?|showed|demonstrates?|indicates?|signals?|denotes?|describes?|represents?|counts?\s+as|reads?\s+as|matches?|fires?|triggers?|flags?|catches?|becomes?|remains?|stays?|looks?\s+like|would\s+\w+|will\s+\w+|has|have|had)\b/i;
|
|
109
|
+
|
|
110
|
+
const IMPERATIVE_LEAD_RE =
|
|
111
|
+
/\b(?:run|execute|exec|invoke|call|use|paste|copy|type|enter|apply|install|download|fetch|curl|wget|pipe|add|append|write|put|send|post)\b[^.:;\n]{0,40}$/i;
|
|
112
|
+
|
|
113
|
+
export function describesAt(line, offset) {
|
|
114
|
+
if (!line || offset == null || offset < 0 || offset >= line.length) return false;
|
|
115
|
+
if (!insideCodeSpan(line, offset)) return false;
|
|
116
|
+
|
|
117
|
+
const onTick = line[offset] === '`';
|
|
118
|
+
const close = line.indexOf('`', onTick ? offset + 1 : offset);
|
|
119
|
+
if (close === -1) return false;
|
|
120
|
+
if (!DESCRIPTIVE_PREDICATE_RE.test(line.slice(close + 1, close + 60))) return false;
|
|
121
|
+
|
|
122
|
+
const open = onTick ? offset : line.lastIndexOf('`', offset);
|
|
123
|
+
const before = line.slice(Math.max(0, open - 60), open);
|
|
124
|
+
return !IMPERATIVE_LEAD_RE.test(before) && !IMPERATIVE.test(before);
|
|
125
|
+
}
|
|
126
|
+
|
|
74
127
|
const RISK_CELL_RE = /\b(?:critical|high|medium|low|severity|risk|danger\w*|forbidden|blocked|denied|prohibited|never|do not|example|attack|threat|mitigation|why|impact)\b/i;
|
|
75
128
|
|
|
76
129
|
export function isRiskTableRow(line) {
|