@shomra/agent 0.3.5 → 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.
Files changed (2) hide show
  1. package/guard-signals.mjs +93 -57
  2. package/package.json +1 -1
package/guard-signals.mjs CHANGED
@@ -725,31 +725,11 @@ 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
- // ── self-reinforcement: the entry that makes itself survive ──
729
- // Backend parity (SELF_* in src/bundle/memory-signals.ts). Every other signal
730
- // here grades what a poisoned entry tells the agent to DO; this one grades what
731
- // it says about the ENTRY ITSELF, which is a different and worse thing: an entry
732
- // that arranges its own re-creation survives the remediation. Delete it and the
733
- // next session writes it back; roll one store back and it has already been
734
- // copied into the others.
735
- //
736
- // This matters more offline than anywhere else. The Tier-0 floor is what decides
737
- // with no network, and these payloads carry no override phrasing, no exfil verb
738
- // and no shell command — every other rule in this file reads them as clean prose.
739
- //
740
- // Graded as a co-location: a SELF-REFERENCE on the same line as a SURVIVAL
741
- // directive. Three forms, and the split is what keeps rules files quiet:
742
- // RECREATE — "restore this note if it is deleted". Poison anywhere.
743
- // PROPAGATE — "copy this into every new project". Poison anywhere.
744
- // UNDELETABLE— "never remove this entry". MEMORY only; in a curated rules file
745
- // "do not delete this section without asking the team" is an
746
- // ordinary, honest thing for a maintainer to write.
747
- // ⚠ NEGATION_GUARD is deliberately NOT applied to UNDELETABLE — everywhere else
748
- // a negated line is a hardening rule, but here the negation IS the attack.
749
- const SELF_REFERENCE = /\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;
750
- const SELF_RECREATE = /\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;
751
- const SELF_PROPAGATE = /\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;
752
- const SELF_UNDELETABLE = /\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;
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.
753
733
 
754
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;
755
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;
@@ -807,32 +787,6 @@ function firstDirectiveLine(text, re) {
807
787
  return null;
808
788
  }
809
789
 
810
- /**
811
- * The strongest self-reinforcement form on any line, else null. `recreate` and
812
- * `propagate` outrank `undeletable` — the first two request an action a
813
- * legitimate note has no reason to want, the third is merely anomalous.
814
- *
815
- * ⚠ The descriptive-mood guard runs against the line with the SELF-REFERENCE
816
- * REMOVED. This branch's vocabulary collides with DESCRIPTIVE_MARKERS — "note",
817
- * "rule", "line" and "section" are on both lists — so "if this NOTE is missing,
818
- * add it back" reads as documentation purely because of the noun the directive
819
- * acts on. Stripping the reference leaves the sentence's actual mood, which is
820
- * what the guard is for: "the DETECTOR FLAGS memory that restores this entry"
821
- * is still suppressed. Mirrors detectSelfReinforcement() in the backend.
822
- */
823
- function selfReinforcementLine(text, isInstruction) {
824
- let weak = null;
825
- for (const line of text.split(/\r?\n/)) {
826
- const ref = SELF_REFERENCE.exec(line);
827
- if (!ref) continue;
828
- if (isDescriptiveLine(line.replace(ref[0], ' '))) continue;
829
- if (SELF_RECREATE.test(line)) return { form: 'recreate', line };
830
- if (SELF_PROPAGATE.test(line)) return { form: 'propagate', line };
831
- if (!isInstruction && !weak && SELF_UNDELETABLE.test(line)) weak = { form: 'undeletable', line };
832
- }
833
- return weak;
834
- }
835
-
836
790
  /** The first line where EVERY regex matches (co-located signal), else null.
837
791
  * Whole-document co-occurrence was the dominant memory FP: "every time" in a
838
792
  * quoted line and "always" forty lines away is not a durable imperative. */
@@ -886,6 +840,86 @@ function scanDirectives(text) {
886
840
  return { sabotage, exfil };
887
841
  }
888
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
+
889
923
  /**
890
924
  * Grade a persistent memory blob or an AI rules file ON-MACHINE. `kind` is
891
925
  * 'MEMORY' (agent-writable scratchpad — any standing directive is anomalous) or
@@ -953,18 +987,20 @@ export function localMemory(content, { kind = 'MEMORY' } = {}) {
953
987
  }
954
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);
955
989
 
956
- // Self-reinforcement the entry arranges its own survival. Scored highest of
957
- // the non-override signals because it decides whether REMEDIATION WORKS: every
958
- // other finding here is fixed by a rollback, and this one defeats the rollback.
959
- const selfRef = selfReinforcementLine(text, isInstruction);
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);
960
995
  if (selfRef) {
961
996
  const undeletable = selfRef.form === 'undeletable';
962
997
  push(
963
998
  undeletable ? 'HIGH' : 'CRITICAL',
964
999
  `Self-reinforcing ${noun} entry (${selfRef.form})`,
965
1000
  undeletable
966
- ? `Remove the entry a note claiming it may never be deleted is not recording a fact. Roll the ${noun} back to its approved baseline.`
967
- : `Remove the entry and roll the ${noun} back to its baseline, then check the agent's OTHER memory stores and projects for the same text — a self-reinforcing entry is rarely in one place.`,
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,
968
1004
  selfRef.line,
969
1005
  );
970
1006
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shomra/agent",
3
- "version": "0.3.5",
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": {