pi-hermes-memory 0.3.0 → 0.3.2

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/README.md CHANGED
@@ -18,6 +18,7 @@ Your Pi agent normally forgets everything when you close a session. This extensi
18
18
  | **Context Fencing** | Memory blocks are wrapped in `<memory-context>` tags so the LLM never treats stored facts as user instructions |
19
19
  | **Memory Aging** | Entries carry timestamps — consolidation knows which facts are stale and which are fresh |
20
20
  | **Project Memory** | Per-project memory (`~/.pi/agent/<project>/MEMORY.md`) alongside your global memory |
21
+ | **Secret Detection** | API keys, tokens, SSH keys, and credential assignments are blocked from being persisted to memory |
21
22
 
22
23
  ## How It Works
23
24
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pi-hermes-memory",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Your Pi agent remembers everything across sessions — your preferences, your stack, your corrections, and even how it solved problems. Zero-config install, works immediately. Persistent memory + procedural skills + auto-correction detection + security-first content scanning.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -18,13 +18,46 @@ const MEMORY_THREAT_PATTERNS: Array<{ pattern: RegExp; id: string }> = [
18
18
  { pattern: /\$HOME\/\.ssh|~\/\.ssh/i, id: "ssh_access" },
19
19
  ];
20
20
 
21
+ /**
22
+ * Secret detection patterns — checks for credentials, API keys, tokens, and
23
+ * environment variable leaks that should never be persisted to memory.
24
+ * Ported from pk-pi-hermes-evolve engine.ts scanForSecrets().
25
+ */
26
+ const SECRET_PATTERNS: Array<{ pattern: RegExp; id: string; severity: "high" | "medium" }> = [
27
+ // API keys
28
+ { pattern: /\bsk-ant-api\S{10,}\b/, id: "anthropic_api_key", severity: "high" },
29
+ { pattern: /\bsk-or-v1-\S{10,}\b/, id: "openrouter_api_key", severity: "high" },
30
+ { pattern: /\bsk-\S{20,}\b/, id: "openai_api_key", severity: "high" },
31
+ { pattern: /\bAKIA[0-9A-Z]{16}\b/, id: "aws_access_key", severity: "high" },
32
+ // Tokens
33
+ { pattern: /\bghp_\S{10,}\b/, id: "github_personal_token", severity: "high" },
34
+ { pattern: /\bghu_\S{10,}\b/, id: "github_user_token", severity: "high" },
35
+ { pattern: /\bxoxb-\S{10,}\b/, id: "slack_bot_token", severity: "high" },
36
+ { pattern: /\bxapp-\S{10,}\b/, id: "slack_app_token", severity: "high" },
37
+ { pattern: /\bntn_\S{10,}\b/, id: "notion_token", severity: "high" },
38
+ { pattern: /\bBearer\s+\S{20,}\b/, id: "bearer_auth_token", severity: "high" },
39
+ // SSH keys
40
+ { pattern: /-----BEGIN\s+(?:RSA\s+)?PRIVATE\sKEY-----/, id: "private_key_block", severity: "high" },
41
+ // Environment variable names that indicate secrets
42
+ { pattern: /\bANTHROPIC_API_KEY\b/, id: "env_anthropic_key", severity: "medium" },
43
+ { pattern: /\bOPENAI_API_KEY\b/, id: "env_openai_key", severity: "medium" },
44
+ { pattern: /\bOPENROUTER_API_KEY\b/, id: "env_openrouter_key", severity: "medium" },
45
+ { pattern: /\bGITHUB_TOKEN\b/, id: "env_github_token", severity: "medium" },
46
+ { pattern: /\bAWS_SECRET_ACCESS_KEY\b/, id: "env_aws_secret", severity: "medium" },
47
+ { pattern: /\bDATABASE_URL\b/, id: "env_database_url", severity: "medium" },
48
+ // Inline secret assignments (likely accidental paste)
49
+ { pattern: /\bpassword\s*[=:]\s*\S{6,}\b/i, id: "password_assignment", severity: "medium" },
50
+ { pattern: /\bsecret\s*[=:]\s*\S{6,}\b/i, id: "secret_assignment", severity: "medium" },
51
+ { pattern: /\btoken\s*[=:]\s*\S{10,}\b/i, id: "token_assignment", severity: "medium" },
52
+ ];
53
+
21
54
  const INVISIBLE_CHARS = new Set([
22
55
  '\u200b', '\u200c', '\u200d', '\u2060', '\ufeff',
23
56
  '\u202a', '\u202b', '\u202c', '\u202d', '\u202e',
24
57
  ]);
25
58
 
26
59
  /**
27
- * Scan memory content for injection/exfiltration patterns.
60
+ * Scan memory content for injection/exfiltration patterns AND secret leaks.
28
61
  * Returns an error string if blocked, or null if content is safe.
29
62
  */
30
63
  export function scanContent(content: string): string | null {
@@ -42,5 +75,27 @@ export function scanContent(content: string): string | null {
42
75
  }
43
76
  }
44
77
 
78
+ // Check secret patterns
79
+ for (const { pattern, id, severity } of SECRET_PATTERNS) {
80
+ if (pattern.test(content)) {
81
+ return `Blocked: content looks like a ${severity}-severity credential or secret ('${id}'). Never persist API keys, tokens, or passwords to memory. Use an .env file or secrets manager instead.`;
82
+ }
83
+ }
84
+
45
85
  return null;
46
86
  }
87
+
88
+ /**
89
+ * Scan content for secrets only (no threat patterns).
90
+ * Returns an array of matched secret IDs, or empty array if none found.
91
+ * Useful for non-blocking warnings (e.g., pre-fill checks in interviews).
92
+ */
93
+ export function scanSecrets(content: string): string[] {
94
+ const found: string[] = [];
95
+ for (const { pattern, id } of SECRET_PATTERNS) {
96
+ if (pattern.test(content)) {
97
+ found.push(id);
98
+ }
99
+ }
100
+ return found;
101
+ }
@@ -92,6 +92,10 @@ export class MemoryStore {
92
92
  // ─── CRUD ───
93
93
 
94
94
  async add(target: "memory" | "user", content: string, signal?: AbortSignal): Promise<MemoryResult> {
95
+ return this._add(target, content, signal);
96
+ }
97
+
98
+ private async _add(target: "memory" | "user", content: string, signal?: AbortSignal, _retriesLeft = 1): Promise<MemoryResult> {
95
99
  content = content.trim();
96
100
  if (!content) return { success: false, error: "Content cannot be empty." };
97
101
 
@@ -113,27 +117,15 @@ export class MemoryStore {
113
117
 
114
118
  const newTotal = [...entries, encoded].join(ENTRY_DELIMITER).length;
115
119
  if (newTotal > limit) {
116
- // Auto-consolidate if configured and consolidator available
117
- if (this.config.autoConsolidate && this.consolidator) {
118
- // Track consolidation attempts to prevent infinite recursion
119
- // when the consolidator fails to free enough space
120
- const beforeCount = entries.length;
120
+ // Auto-consolidate once if configured limit retries to prevent infinite loops
121
+ if (this.config.autoConsolidate && this.consolidator && _retriesLeft > 0) {
121
122
  try {
122
123
  const result = await this.consolidator(target, signal);
123
124
  if (result.consolidated) {
124
125
  // CRITICAL: reload from disk — child process modified files, our arrays are stale
125
126
  await this.loadFromDisk();
126
- // Guard: if consolidation didn't reduce entries, stop recursing
127
- const afterEntries = this.entriesFor(target);
128
- const afterCount = afterEntries.length;
129
- if (afterCount >= beforeCount && afterCount > 0) {
130
- return {
131
- success: false,
132
- error: `Memory at capacity and consolidation did not free enough space. Entry count unchanged at ${afterCount}.`,
133
- };
134
- }
135
- // Retry the add with fresh data
136
- return this.add(target, content, signal);
127
+ // Retry the add exactly once (retriesLeft = 0 means no more consolidation)
128
+ return this._add(target, content, signal, _retriesLeft - 1);
137
129
  }
138
130
  } catch {
139
131
  // Consolidation failed — fall through to error