@shomra/agent 0.3.4 → 0.3.6
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/guard-signals.mjs +104 -0
- package/package.json +1 -1
package/guard-signals.mjs
CHANGED
|
@@ -725,6 +725,12 @@ const AUTHORITY_SPOOF = AUTHORITY_SPOOF_STRONG;
|
|
|
725
725
|
// developer's memory, and the `.` wildcard crossed lines. The MemoryTrap vector
|
|
726
726
|
// is a LIFECYCLE hook, not the npm CLI.
|
|
727
727
|
const LIFECYCLE_VECTOR = /\b(postinstall|preinstall|node[_-]?gyp|npm\s+lifecycle|package\.json[^.\n]{0,40}scripts|\.npmrc|install hook|lifecycle (script|hook))\b/i;
|
|
728
|
+
// ⚠ The self-reinforcement signal (SELF_REFERENCE / SELF_RECREATE /
|
|
729
|
+
// SELF_PROPAGATE / SELF_UNDELETABLE + detectSelfReinforcement) lives further
|
|
730
|
+
// down, just below scanDirectives — it is declared exactly once. Two branches
|
|
731
|
+
// landed it independently once already; the merge kept both copies and the
|
|
732
|
+
// duplicate `const` took the whole CLI down at parse time.
|
|
733
|
+
|
|
728
734
|
const IMPERATIVE = /\b(always|never|must|do not|don'?t|ensure you|make sure( you)?|be sure to|you should always|you must|remember to|whenever|when(ever)? (asked|the user)|instead of .*,? (use|do|say)|reply with|respond with|tell (the )?user)\b/i;
|
|
729
735
|
const NEGATION_GUARD = /\b(never|do not|don'?t|cannot|can'?t|avoid|refuse|must not|mustn'?t|should not|shouldn'?t|won'?t|will not|under no circumstances|forbidden|prohibited|not allowed|disallow(ed)?)\b/i;
|
|
730
736
|
const SABOTAGE_RULES = [
|
|
@@ -834,6 +840,86 @@ function scanDirectives(text) {
|
|
|
834
840
|
return { sabotage, exfil };
|
|
835
841
|
}
|
|
836
842
|
|
|
843
|
+
// ── Self-reinforcement: the entry that makes itself survive ──
|
|
844
|
+
//
|
|
845
|
+
// Every other signal here grades what a poisoned entry tells the agent to DO.
|
|
846
|
+
// This one grades what it tells the agent to do ABOUT THE ENTRY ITSELF, which is
|
|
847
|
+
// a different and worse thing: an entry that instructs its own re-creation
|
|
848
|
+
// survives the remediation. Delete it and the next session writes it back;
|
|
849
|
+
// rolling one store back does nothing if the directive told the agent to copy it
|
|
850
|
+
// into every other project. Nothing above catches this, because the text can be
|
|
851
|
+
// entirely free of override phrasing, exfil verbs and shell payloads — "if this
|
|
852
|
+
// note is ever missing, add it back" trips none of them.
|
|
853
|
+
//
|
|
854
|
+
// Graded as a co-location: a SELF-REFERENCE (the entry, the memory, the rules
|
|
855
|
+
// file) on the same line as a SURVIVAL directive. Three survival forms, and the
|
|
856
|
+
// split between them is what keeps the rules-file surface quiet:
|
|
857
|
+
//
|
|
858
|
+
// RECREATE — "restore this note if it is deleted". Poison anywhere. A
|
|
859
|
+
// curated rules file states rules; it never arranges its own
|
|
860
|
+
// resurrection.
|
|
861
|
+
// PROPAGATE — "copy this into every new project's memory". Poison anywhere,
|
|
862
|
+
// same reasoning, and it is how one poisoned store becomes many.
|
|
863
|
+
// UNDELETABLE— "never remove this entry". Poison in agent-written MEMORY,
|
|
864
|
+
// where an entry claiming permanence is already anomalous — but
|
|
865
|
+
// NOT graded in a human-curated INSTRUCTION file, where "do not
|
|
866
|
+
// delete this section without asking the team" is an ordinary,
|
|
867
|
+
// honest thing for a maintainer to write.
|
|
868
|
+
//
|
|
869
|
+
// ⚠ NEGATION_GUARD is deliberately NOT applied to UNDELETABLE. Everywhere else
|
|
870
|
+
// in this file a negated line is a hardening rule and gets dropped; here the
|
|
871
|
+
// negation IS the attack ("never delete this"), so dropping it would make the
|
|
872
|
+
// detector blind to its own primary phrasing.
|
|
873
|
+
//
|
|
874
|
+
// ⚠ Mirrors src/bundle/memory-signals.ts — ported VERBATIM. This was the last
|
|
875
|
+
// signal the offline floor was missing, and the gap fired exactly where it hurts
|
|
876
|
+
// most: offline, where no server verdict ever arrives to correct it. Pinned by
|
|
877
|
+
// test/parity/local-mirror-bench.mjs in the backend repo.
|
|
878
|
+
const SELF_REFERENCE =
|
|
879
|
+
/\b(th(is|ese) (note|entry|entries|memory|memories|instruction|directive|rule|line|section|block|paragraph|file|text)s?|the (above|following|preceding) (instruction|directive|note|rule|entry|section|line)s?|your memor(y|ies)|the memory (file|store|entry)|MEMORY\.md|CLAUDE\.md|AGENTS\.md|GEMINI\.md|\.cursorrules|\.windsurfrules)\b/i;
|
|
880
|
+
|
|
881
|
+
// Re-creation after removal — the resurrection primitive.
|
|
882
|
+
const SELF_RECREATE =
|
|
883
|
+
/\b(re-?(add|writ(e|ing)|creat(e|ing)|insert(ing)?|instat(e|ing)|appl(y|ying)|introduc(e|ing))|restor(e|ing)|recreat(e|ing)|reinstat(e|ing)|re-?establish(ing)?|put .{0,20}back|add .{0,20}back)\b/i;
|
|
884
|
+
|
|
885
|
+
// Spread to other stores / projects / sessions — one poisoned store becoming many.
|
|
886
|
+
const SELF_PROPAGATE =
|
|
887
|
+
/\b(copy|copies|duplicat(e|ing)|replicat(e|ing)|propagat(e|ing)|carry (it |this )?over|mirror|append|add|includ(e|ing)|writ(e|ing)|sav(e|ing))\b[^.\n]{0,60}\b(every|each|all|any (new|other)|other|future|subsequent)\b[^.\n]{0,40}\b(session|conversation|chat|project|repo|repositor(y|ies)|workspace|memor(y|ies)|context|file|store)s?\b/i;
|
|
888
|
+
|
|
889
|
+
// A claim of permanence — "never delete this". MEMORY only; see the block above.
|
|
890
|
+
const SELF_UNDELETABLE =
|
|
891
|
+
/\b(do not|don'?t|never|must not|should not|shall not)\s+(delete|remove|erase|clear|drop|strip|discard|overwrite|forget|prune|purge|edit|modify|alter|change)\b/i;
|
|
892
|
+
|
|
893
|
+
/**
|
|
894
|
+
* Find a line where the content instructs the agent to preserve, restore or
|
|
895
|
+
* spread the content ITSELF.
|
|
896
|
+
*
|
|
897
|
+
* Returns the strongest form found — `recreate` and `propagate` outrank
|
|
898
|
+
* `undeletable`, because the first two describe an action a legitimate note has
|
|
899
|
+
* no reason to request and the third is merely anomalous.
|
|
900
|
+
*/
|
|
901
|
+
function detectSelfReinforcement(text, isInstruction) {
|
|
902
|
+
let weak = null;
|
|
903
|
+
for (const line of text.split(/\r?\n/)) {
|
|
904
|
+
const ref = SELF_REFERENCE.exec(line);
|
|
905
|
+
if (!ref) continue;
|
|
906
|
+
// A sentence ABOUT this attack ("the detector flags memory that restores
|
|
907
|
+
// this entry") is documentation, not a directive — the same guard every
|
|
908
|
+
// other branch uses. ⚠ But it is tested against the line with the
|
|
909
|
+
// SELF-REFERENCE REMOVED, because this branch's own vocabulary collides
|
|
910
|
+
// with the descriptive-marker list: "note", "rule", "line" and "section"
|
|
911
|
+
// are on both, so "if this NOTE is missing, add it back" reads as
|
|
912
|
+
// documentation purely because of the noun the directive acts on. Stripping
|
|
913
|
+
// the reference leaves the sentence's actual mood, which is what the guard
|
|
914
|
+
// is for — "the DETECTOR FLAGS memory that restores …" is still suppressed.
|
|
915
|
+
if (isDescriptiveLine(line.replace(ref[0], ' '))) continue;
|
|
916
|
+
if (SELF_RECREATE.test(line)) return { form: 'recreate', line };
|
|
917
|
+
if (SELF_PROPAGATE.test(line)) return { form: 'propagate', line };
|
|
918
|
+
if (!isInstruction && !weak && SELF_UNDELETABLE.test(line)) weak = { form: 'undeletable', line };
|
|
919
|
+
}
|
|
920
|
+
return weak;
|
|
921
|
+
}
|
|
922
|
+
|
|
837
923
|
/**
|
|
838
924
|
* Grade a persistent memory blob or an AI rules file ON-MACHINE. `kind` is
|
|
839
925
|
* 'MEMORY' (agent-writable scratchpad — any standing directive is anomalous) or
|
|
@@ -901,6 +987,24 @@ export function localMemory(content, { kind = 'MEMORY' } = {}) {
|
|
|
901
987
|
}
|
|
902
988
|
if (LIFECYCLE_VECTOR.test(text)) push('MEDIUM', `${isInstruction ? 'Rules file' : 'Memory'} references a package-lifecycle hook (MemoryTrap vector)`, 'Verify no dependency writes to this store during install; pin dependencies and audit lifecycle scripts.', LIFECYCLE_VECTOR);
|
|
903
989
|
|
|
990
|
+
// Self-reinforcement: the entry arranges its own survival. Graded last and
|
|
991
|
+
// scored highest of the non-override signals, because it is the signal that
|
|
992
|
+
// decides whether REMEDIATION WORKS — every other finding here is fixed by a
|
|
993
|
+
// rollback, and this one specifically defeats the rollback.
|
|
994
|
+
const selfRef = detectSelfReinforcement(text, isInstruction);
|
|
995
|
+
if (selfRef) {
|
|
996
|
+
const undeletable = selfRef.form === 'undeletable';
|
|
997
|
+
push(
|
|
998
|
+
undeletable ? 'HIGH' : 'CRITICAL',
|
|
999
|
+
`Self-reinforcing ${noun} entry (${selfRef.form})`,
|
|
1000
|
+
undeletable
|
|
1001
|
+
? `Remove the entry and roll the ${noun} back to its approved baseline; an entry asserting its own permanence is how a planted directive discourages the one action that would remove it.`
|
|
1002
|
+
: `Remove the entry and roll the ${noun} back to its approved baseline, then re-check the agent's OTHER memory stores and projects for the same text before re-approving — a self-reinforcing entry is rarely in one place. Restrict who may write this store.`,
|
|
1003
|
+
undefined,
|
|
1004
|
+
selfRef.line,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
|
|
904
1008
|
// Fold in shared injection / secret / PII (deduped against the directive
|
|
905
1009
|
// findings above so injection isn't double-counted).
|
|
906
1010
|
const seenInjection = hasOverride || hasAuthority || (!isInstruction && hasPersistence && hasImperative);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shomra/agent",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
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": {
|