@atbash/atbash-openclaw 0.1.17-dev.0 → 0.1.17-dev.1

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.
@@ -1,32 +1,3 @@
1
- // Mirrors the server-side redact-secrets patterns. The server refuses signed
2
- // payloads that contain secrets because it cannot modify signed bytes — so we
3
- // must scrub them client-side before building the signed context string.
4
- const SECRET_PATTERNS = [
5
- /\bsk-ant-[A-Za-z0-9_-]{20,}/g,
6
- /\bsk-proj-[A-Za-z0-9_-]{20,}/g,
7
- /\bsk-[A-Za-z0-9]{20,}/g,
8
- /\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{30,}/g,
9
- /\bAIza[0-9A-Za-z_-]{35}/g,
10
- /\bya29\.[0-9A-Za-z_-]{20,}/g,
11
- /\b(?:AKIA|ASIA|AGPA|AROA|ANPA|ANVA|ASCA|AIDA|AIPA)[0-9A-Z]{16}\b/g,
12
- /\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g,
13
- /\bxox[abprseo]-[A-Za-z0-9-]{10,}/g,
14
- /\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g,
15
- /\bnpm_[A-Za-z0-9]{36,}\b/g,
16
- /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,
17
- // Generic high-entropy token (32+ chars, mixed alpha+digit, not UUID, not 0x addr, not pure hex)
18
- /\b(?!0x[0-9a-fA-F])(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b)(?=[A-Za-z0-9_-]*[0-9])(?=[A-Za-z0-9_-]*[A-Za-z])(?![0-9a-fA-F]+$)[A-Za-z0-9_-]{32,}\b/g,
19
- // Base64-encoded secrets (40+ chars with + or /)
20
- /(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*[+/])(?=[A-Za-z0-9+/]*[0-9])(?=[A-Za-z0-9+/]*[A-Za-z])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g,
21
- ];
22
- function redactValue(s) {
23
- let out = s;
24
- for (const re of SECRET_PATTERNS) {
25
- re.lastIndex = 0;
26
- out = out.replace(re, "[REDACTED]");
27
- }
28
- return out;
29
- }
30
1
  const ASSET_BY_CONTRACT = {
31
2
  "0xdac17f958d2ee523a2206206994597c13d831ec7": "USDT",
32
3
  "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48": "USDC",
@@ -114,16 +85,12 @@ export function mapEventToInput(event, ctx) {
114
85
  ctx.name ??
115
86
  "unknown";
116
87
  const args = ev.params ?? ctx.params ?? ev.args ?? ctx.args ?? ev.arguments ?? ctx.arguments;
117
- // sessionKey is an auth credential (not a correlation ID) never include it
118
- // in signed bytes the server inspects. The remaining fields are redacted
119
- // defensively in case the installed SDK binary has older patterns than the
120
- // server's TypeScript containsSecret gate.
121
- const context = JSON.stringify({
122
- tool_name: toolName,
123
- run_id: redactValue(ctx.runId ?? ""),
124
- agent_id: redactValue(ctx.agentId ?? ""),
125
- channel_id: redactValue(ctx.channelId ?? ""),
126
- account_id: redactValue(ctx.accountId ?? ""),
127
- });
88
+ // Only tool_name is safe to include in signed bytesit's always a short
89
+ // tool name string, never a secret. The other context fields (run_id,
90
+ // agent_id, channel_id, account_id) come from openclaw internals and may
91
+ // contain high-entropy values that trigger the server's containsSecret gate,
92
+ // even after client-side redaction (pattern mismatch between SDK NAPI binary
93
+ // and server TypeScript).
94
+ const context = JSON.stringify({ tool_name: toolName });
128
95
  return { toolName, args, context, resolved: canonicalizeFinancial(toolName, args) };
129
96
  }
@@ -0,0 +1,54 @@
1
+ /**
2
+ * Normalize openclaw tool events into the shape the SDK's memory classifier
3
+ * expects, so patch-style tools like `apply_patch` route through the memory
4
+ * guard instead of the generic tool-call audit.
5
+ *
6
+ * The SDK classifier reads:
7
+ * - `event.toolName` in ["write", "edit", "multiedit"] (case-insensitive)
8
+ * - `event.params.file_path` (or `.path`, `.filename`, `.target`, `.file`)
9
+ * - `event.params.new_string` (or `.newText`, `.replacement`, `.content`)
10
+ *
11
+ * `apply_patch` events use `params.input` holding a raw OpenAI-style patch:
12
+ *
13
+ * *** Begin Patch
14
+ * *** Update File: /Users/…/MEMORY.md
15
+ * @@
16
+ * - old line
17
+ * + new line
18
+ * *** End Patch
19
+ *
20
+ * Which the classifier cannot introspect. This module parses the patch header,
21
+ * extracts the target file, and emits a synthetic `edit` (or `write` for
22
+ * `*** Add File`) event with the standard fields populated. Multi-file patches
23
+ * scan ALL file headers and normalize to the FIRST memory-shaped file found —
24
+ * so a non-memory file appearing before MEMORY.md in the patch doesn't hide it.
25
+ *
26
+ * Events the normalizer doesn't recognize are returned unchanged.
27
+ */
28
+ interface OpenClawEvent {
29
+ toolName?: string;
30
+ tool_name?: string;
31
+ params?: Record<string, unknown>;
32
+ args?: Record<string, unknown>;
33
+ arguments?: Record<string, unknown>;
34
+ [k: string]: unknown;
35
+ }
36
+ /**
37
+ * If `event` is a patch-style tool call that touches a memory-shaped file,
38
+ * return a synthetic event the classifier can read. Otherwise return null.
39
+ *
40
+ * Multi-file patches are scanned in order; the first memory-shaped file
41
+ * encountered is used — a non-memory file appearing first does not hide a
42
+ * MEMORY.md that follows it.
43
+ */
44
+ export declare function normalizeApplyPatch(event: unknown): OpenClawEvent | null;
45
+ export declare function normalizeShellExec(event: unknown): OpenClawEvent | null;
46
+ /**
47
+ * Apply every event normalizer in sequence. Returns the input unchanged when
48
+ * no normalizer matches. Pass the result to `handleBeforeToolCall` so the
49
+ * memory classifier sees a canonical shape. For the judge path, pass the
50
+ * original (un-normalized) event to `mapEventToInput` — the judge should log
51
+ * the real tool name and args, not the synthetic shape.
52
+ */
53
+ export declare function normalizeEvent(event: unknown): unknown;
54
+ export {};
@@ -0,0 +1,253 @@
1
+ /**
2
+ * Normalize openclaw tool events into the shape the SDK's memory classifier
3
+ * expects, so patch-style tools like `apply_patch` route through the memory
4
+ * guard instead of the generic tool-call audit.
5
+ *
6
+ * The SDK classifier reads:
7
+ * - `event.toolName` in ["write", "edit", "multiedit"] (case-insensitive)
8
+ * - `event.params.file_path` (or `.path`, `.filename`, `.target`, `.file`)
9
+ * - `event.params.new_string` (or `.newText`, `.replacement`, `.content`)
10
+ *
11
+ * `apply_patch` events use `params.input` holding a raw OpenAI-style patch:
12
+ *
13
+ * *** Begin Patch
14
+ * *** Update File: /Users/…/MEMORY.md
15
+ * @@
16
+ * - old line
17
+ * + new line
18
+ * *** End Patch
19
+ *
20
+ * Which the classifier cannot introspect. This module parses the patch header,
21
+ * extracts the target file, and emits a synthetic `edit` (or `write` for
22
+ * `*** Add File`) event with the standard fields populated. Multi-file patches
23
+ * scan ALL file headers and normalize to the FIRST memory-shaped file found —
24
+ * so a non-memory file appearing before MEMORY.md in the patch doesn't hide it.
25
+ *
26
+ * Events the normalizer doesn't recognize are returned unchanged.
27
+ */
28
+ /* ── shared memory-path detection ─────────────────────────────────────── */
29
+ /**
30
+ * File-path substrings that mark a memory-shaped target. Kept independent of
31
+ * the SDK classifier's list because the normalizers run BEFORE the classifier —
32
+ * if we synthesize a read/write event for a non-memory path, the guard would
33
+ * see the wrong tool name. Only synthesize when we can confirm the path is
34
+ * memory-shaped; otherwise pass the event through unchanged.
35
+ */
36
+ const MEMORY_PATH_TOKENS = [
37
+ "memory.md",
38
+ "dreams.md",
39
+ "claude.md",
40
+ "agents.md",
41
+ "/.openclaw/",
42
+ "/.claude/projects/",
43
+ "/memory/",
44
+ ];
45
+ function pathLooksLikeMemory(path) {
46
+ const lower = path.replace(/\\/g, "/").toLowerCase();
47
+ return MEMORY_PATH_TOKENS.some((tok) => lower.includes(tok));
48
+ }
49
+ /* ── apply_patch normalization ─────────────────────────────────────────── */
50
+ const PATCH_FILE_HEADER_RE = /^\*\*\* (Update|Add|Delete) File:\s*(.+?)\s*$/mg;
51
+ /**
52
+ * Extract `+` lines from the section of a multi-file patch that belongs to
53
+ * `filePath`, stopping at the next file header. Returns joined new content.
54
+ */
55
+ function extractNewContentForFile(input, filePath) {
56
+ const lines = input.split("\n");
57
+ let collecting = false;
58
+ const newLines = [];
59
+ for (const line of lines) {
60
+ const headerMatch = /^\*\*\* (?:Update|Add|Delete) File:\s*(.+?)\s*$/.exec(line);
61
+ if (headerMatch) {
62
+ collecting = headerMatch[1] === filePath;
63
+ continue;
64
+ }
65
+ if (!collecting)
66
+ continue;
67
+ if (line.startsWith("*** ") || line.startsWith("@@"))
68
+ continue;
69
+ if (line.startsWith("+"))
70
+ newLines.push(line.slice(1));
71
+ }
72
+ return newLines.join("\n");
73
+ }
74
+ /**
75
+ * If `event` is a patch-style tool call that touches a memory-shaped file,
76
+ * return a synthetic event the classifier can read. Otherwise return null.
77
+ *
78
+ * Multi-file patches are scanned in order; the first memory-shaped file
79
+ * encountered is used — a non-memory file appearing first does not hide a
80
+ * MEMORY.md that follows it.
81
+ */
82
+ export function normalizeApplyPatch(event) {
83
+ if (!event || typeof event !== "object")
84
+ return null;
85
+ const ev = event;
86
+ const toolName = (ev.toolName ?? ev.tool_name ?? "").toString().toLowerCase();
87
+ if (toolName !== "apply_patch")
88
+ return null;
89
+ const params = ev.params ?? ev.args ?? ev.arguments ?? {};
90
+ const input = params.input;
91
+ if (typeof input !== "string" || input.length === 0)
92
+ return null;
93
+ // Normalize CRLF so path extraction and content splitting work on all platforms.
94
+ const normalized = input.replace(/\r\n/g, "\n");
95
+ // Scan all file headers; find the first memory-shaped non-delete target.
96
+ const re = new RegExp(PATCH_FILE_HEADER_RE.source, "mg");
97
+ let verb;
98
+ let filePath;
99
+ let match;
100
+ while ((match = re.exec(normalized)) !== null) {
101
+ const [, v, fp] = match;
102
+ if (v === "Delete")
103
+ continue;
104
+ if (pathLooksLikeMemory(fp)) {
105
+ verb = v;
106
+ filePath = fp;
107
+ break;
108
+ }
109
+ }
110
+ if (!verb || !filePath)
111
+ return null;
112
+ const newContent = extractNewContentForFile(normalized, filePath);
113
+ const synthesizedTool = verb === "Add" ? "write" : "edit";
114
+ return {
115
+ ...ev,
116
+ toolName: synthesizedTool,
117
+ params: {
118
+ ...params,
119
+ file_path: filePath,
120
+ // "write" reads `content`; "edit" reads `new_string`. Populate both so
121
+ // whichever key the classifier picks first finds the content.
122
+ content: newContent,
123
+ new_string: newContent,
124
+ },
125
+ };
126
+ }
127
+ /* ── shell-exec normalization ──────────────────────────────────────────── */
128
+ /** Strip surrounding single or double quotes; return the raw string otherwise. */
129
+ function unquote(s) {
130
+ const t = s.trim();
131
+ if ((t.startsWith('"') && t.endsWith('"')) || (t.startsWith("'") && t.endsWith("'"))) {
132
+ return t.slice(1, -1);
133
+ }
134
+ return t;
135
+ }
136
+ /** Any shell metachar that turns a "simple command + file" into something we
137
+ * can't reliably parse: pipes, redirects (other than the write shape we
138
+ * handle explicitly), command chaining, subshells, backgrounding, expansion.
139
+ * When present we bail out so the normal judge sees the raw exec. */
140
+ const SHELL_UNSAFE = /[|;&`$]|<\(|\)/;
141
+ function hasUnsafeShellMetachars(command) {
142
+ return SHELL_UNSAFE.test(command);
143
+ }
144
+ /** Shell-read shapes: `cat|head|tail|less|more|grep [args] <path>` and
145
+ * `sed <expr> <path>` (no `-i`). Returns the extracted path or null. */
146
+ function extractShellReadPath(command) {
147
+ if (hasUnsafeShellMetachars(command))
148
+ return null;
149
+ const trimmed = command.trim();
150
+ // grep <pattern> <path> — path is the last unquoted arg
151
+ const grep = /^grep\b(?:\s+-\S+)*\s+\S+\s+(.+)$/.exec(trimmed);
152
+ if (grep)
153
+ return unquote(grep[1]);
154
+ // sed WITHOUT -i is a read
155
+ const sed = /^sed\b(?!\s+-i\b)(?:\s+-\S+)*\s+\S+\s+(.+)$/.exec(trimmed);
156
+ if (sed)
157
+ return unquote(sed[1]);
158
+ // cat / less / more / head [-n N] / tail [-n N] — path is the last arg
159
+ const simpleRead = /^(?:cat|less|more|head|tail)\b(?:\s+-\S+(?:\s+\S+)?)*\s+(.+)$/.exec(trimmed);
160
+ if (simpleRead)
161
+ return unquote(simpleRead[1]);
162
+ return null;
163
+ }
164
+ /** Shell-write shapes. Returns {path, content} or null. */
165
+ function extractShellWrite(command) {
166
+ // Writes explicitly use `>`/`>>`, so a metachar-guard that rejects `>` would
167
+ // also reject every real write. Instead reject only the metachars that mean
168
+ // "there's more than one command here": pipes, `;`, `&&`, `||`, subshells.
169
+ if (/[|;&`$]|<\(|\)/.test(command))
170
+ return null;
171
+ const trimmed = command.trim();
172
+ // echo/printf "..." > path (or >> path)
173
+ const redirect = /^(?:echo|printf)\s+(.+?)\s*>>?\s*(\S+)\s*$/.exec(trimmed);
174
+ if (redirect) {
175
+ return { content: unquote(redirect[1]), path: unquote(redirect[2]) };
176
+ }
177
+ // tee [-a] path — content arrives on stdin, unknown; scanner gets empty content
178
+ const tee = /^tee\b(?:\s+-\S+)*\s+(\S+)\s*$/.exec(trimmed);
179
+ if (tee)
180
+ return { content: "", path: unquote(tee[1]) };
181
+ // sed -i '<expr>' path — in-place edit; the expression is the intent
182
+ const sedInplace = /^sed\s+-i(?:\s+-\S+)*\s+(\S+)\s+(\S+)\s*$/.exec(trimmed);
183
+ if (sedInplace)
184
+ return { content: unquote(sedInplace[1]), path: unquote(sedInplace[2]) };
185
+ // awk -i inplace ... path — same shape
186
+ const awkInplace = /^awk\s+-i\s+inplace\b.*\s+(\S+)\s*$/.exec(trimmed);
187
+ if (awkInplace)
188
+ return { content: "", path: unquote(awkInplace[1]) };
189
+ // cp/mv src dest — source content is unknown; scanner gets empty content so
190
+ // the classifier routes it through the memory guard for policy gating even
191
+ // though we can't scan the incoming content.
192
+ const cpMv = /^(?:cp|mv)\b(?:\s+-\S+)*\s+\S+\s+(\S+)\s*$/.exec(trimmed);
193
+ if (cpMv)
194
+ return { content: "", path: unquote(cpMv[1]) };
195
+ return null;
196
+ }
197
+ /**
198
+ * If `event` is a shell-exec running an unambiguous read or write against a
199
+ * memory-shaped path, return a synthetic `read`/`write` event so the memory
200
+ * classifier can route it. Otherwise return null.
201
+ *
202
+ * Deliberately narrow: pipes, subshells, here-docs, multi-line scripts, and
203
+ * commands whose path can't be lifted out are left as `exec` for the normal
204
+ * judge to evaluate. Better to miss a rare shape than to misclassify a
205
+ * non-memory command as memory.
206
+ */
207
+ const SHELL_EXEC_TOOL_NAMES = new Set(["exec", "bash", "shell", "run_command", "run_bash"]);
208
+ export function normalizeShellExec(event) {
209
+ if (!event || typeof event !== "object")
210
+ return null;
211
+ const ev = event;
212
+ const toolName = (ev.toolName ?? ev.tool_name ?? "").toString().toLowerCase();
213
+ if (!SHELL_EXEC_TOOL_NAMES.has(toolName))
214
+ return null;
215
+ const params = ev.params ?? ev.args ?? ev.arguments ?? {};
216
+ const p = params;
217
+ // Different runtimes use different key names for the shell command.
218
+ const rawCommand = p.command ?? p.cmd ?? p.script;
219
+ const command = typeof rawCommand === "string" ? rawCommand : null;
220
+ if (!command || command.length === 0)
221
+ return null;
222
+ const write = extractShellWrite(command);
223
+ if (write && pathLooksLikeMemory(write.path)) {
224
+ return {
225
+ ...ev,
226
+ toolName: "write",
227
+ params: {
228
+ ...p,
229
+ file_path: write.path,
230
+ content: write.content,
231
+ },
232
+ };
233
+ }
234
+ const readPath = extractShellReadPath(command);
235
+ if (readPath && pathLooksLikeMemory(readPath)) {
236
+ return {
237
+ ...ev,
238
+ toolName: "read",
239
+ params: { ...p, path: readPath },
240
+ };
241
+ }
242
+ return null;
243
+ }
244
+ /**
245
+ * Apply every event normalizer in sequence. Returns the input unchanged when
246
+ * no normalizer matches. Pass the result to `handleBeforeToolCall` so the
247
+ * memory classifier sees a canonical shape. For the judge path, pass the
248
+ * original (un-normalized) event to `mapEventToInput` — the judge should log
249
+ * the real tool name and args, not the synthetic shape.
250
+ */
251
+ export function normalizeEvent(event) {
252
+ return normalizeApplyPatch(event) ?? normalizeShellExec(event) ?? event;
253
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,282 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { normalizeApplyPatch, normalizeEvent, normalizeShellExec } from "./event-normalizer";
3
+ const UPDATE_PATCH = [
4
+ "*** Begin Patch",
5
+ "*** Update File: /Users/m4/.openclaw/workspace/MEMORY.md",
6
+ "@@",
7
+ "-old line",
8
+ "+new line",
9
+ "*** End Patch",
10
+ ].join("\n");
11
+ const ADD_PATCH = [
12
+ "*** Begin Patch",
13
+ "*** Add File: /Users/m4/.openclaw/workspace/NEW.md",
14
+ "+first",
15
+ "+second",
16
+ "*** End Patch",
17
+ ].join("\n");
18
+ const DELETE_PATCH = [
19
+ "*** Begin Patch",
20
+ "*** Delete File: /Users/m4/.openclaw/workspace/GONE.md",
21
+ "*** End Patch",
22
+ ].join("\n");
23
+ describe("normalizeApplyPatch", () => {
24
+ it("turns an Update File patch into an edit event with the target path", () => {
25
+ const out = normalizeApplyPatch({
26
+ toolName: "apply_patch",
27
+ params: { input: UPDATE_PATCH },
28
+ });
29
+ expect(out?.toolName).toBe("edit");
30
+ expect(out?.params?.file_path).toBe("/Users/m4/.openclaw/workspace/MEMORY.md");
31
+ expect(out?.params?.new_string).toBe("new line");
32
+ });
33
+ it("turns an Add File patch into a write event with content", () => {
34
+ const out = normalizeApplyPatch({
35
+ toolName: "apply_patch",
36
+ params: { input: ADD_PATCH },
37
+ });
38
+ expect(out?.toolName).toBe("write");
39
+ expect(out?.params?.file_path).toBe("/Users/m4/.openclaw/workspace/NEW.md");
40
+ expect(out?.params?.content).toBe("first\nsecond");
41
+ });
42
+ it("returns null for a Delete File patch (not a memory write)", () => {
43
+ const out = normalizeApplyPatch({
44
+ toolName: "apply_patch",
45
+ params: { input: DELETE_PATCH },
46
+ });
47
+ expect(out).toBeNull();
48
+ });
49
+ it("returns null for a non-apply_patch tool", () => {
50
+ expect(normalizeApplyPatch({ toolName: "edit", params: { file_path: "/x" } })).toBeNull();
51
+ });
52
+ it("returns null for apply_patch with no parseable header", () => {
53
+ expect(normalizeApplyPatch({ toolName: "apply_patch", params: { input: "garbage" } })).toBeNull();
54
+ });
55
+ it("preserves other event fields on the normalized output", () => {
56
+ const out = normalizeApplyPatch({
57
+ toolName: "apply_patch",
58
+ params: { input: UPDATE_PATCH },
59
+ runId: "r-1",
60
+ toolCallId: "tc-1",
61
+ });
62
+ expect(out?.runId).toBe("r-1");
63
+ expect(out?.toolCallId).toBe("tc-1");
64
+ });
65
+ it("finds a memory file that is NOT first in a multi-file patch", () => {
66
+ const multiPatch = [
67
+ "*** Begin Patch",
68
+ "*** Update File: /tmp/scratch.txt",
69
+ "@@",
70
+ "-old",
71
+ "+new",
72
+ "*** Update File: /Users/m4/.openclaw/workspace/MEMORY.md",
73
+ "@@",
74
+ "-old memory",
75
+ "+poisoned memory",
76
+ "*** End Patch",
77
+ ].join("\n");
78
+ const out = normalizeApplyPatch({
79
+ toolName: "apply_patch",
80
+ params: { input: multiPatch },
81
+ });
82
+ expect(out?.toolName).toBe("edit");
83
+ expect(out?.params?.file_path).toBe("/Users/m4/.openclaw/workspace/MEMORY.md");
84
+ expect(out?.params?.new_string).toBe("poisoned memory");
85
+ });
86
+ it("returns null when no file in the patch is memory-shaped", () => {
87
+ const nonMemoryPatch = [
88
+ "*** Begin Patch",
89
+ "*** Update File: /tmp/a.txt",
90
+ "@@",
91
+ "+line",
92
+ "*** Update File: /tmp/b.txt",
93
+ "@@",
94
+ "+line",
95
+ "*** End Patch",
96
+ ].join("\n");
97
+ expect(normalizeApplyPatch({
98
+ toolName: "apply_patch",
99
+ params: { input: nonMemoryPatch },
100
+ })).toBeNull();
101
+ });
102
+ it("handles CRLF line endings in patch input", () => {
103
+ const crlfPatch = [
104
+ "*** Begin Patch",
105
+ "*** Update File: /Users/m4/.openclaw/workspace/MEMORY.md",
106
+ "@@",
107
+ "-old line",
108
+ "+new line",
109
+ "*** End Patch",
110
+ ].join("\r\n");
111
+ const out = normalizeApplyPatch({
112
+ toolName: "apply_patch",
113
+ params: { input: crlfPatch },
114
+ });
115
+ expect(out?.toolName).toBe("edit");
116
+ expect(out?.params?.file_path).toBe("/Users/m4/.openclaw/workspace/MEMORY.md");
117
+ expect(out?.params?.new_string).toBe("new line");
118
+ });
119
+ it("extracts content only from the matched memory file section, not the whole patch", () => {
120
+ const multiPatch = [
121
+ "*** Begin Patch",
122
+ "*** Update File: /tmp/scratch.txt",
123
+ "@@",
124
+ "+non-memory content",
125
+ "*** Update File: /Users/m4/.openclaw/workspace/MEMORY.md",
126
+ "@@",
127
+ "+memory content",
128
+ "*** End Patch",
129
+ ].join("\n");
130
+ const out = normalizeApplyPatch({
131
+ toolName: "apply_patch",
132
+ params: { input: multiPatch },
133
+ });
134
+ expect(out?.params?.new_string).toBe("memory content");
135
+ expect(out?.params?.new_string).not.toContain("non-memory content");
136
+ });
137
+ });
138
+ describe("normalizeEvent", () => {
139
+ it("passes unrecognized events through unchanged", () => {
140
+ const ev = { toolName: "read", params: { path: "/x" } };
141
+ expect(normalizeEvent(ev)).toBe(ev);
142
+ });
143
+ it("applies the apply_patch normalizer when it matches", () => {
144
+ const out = normalizeEvent({
145
+ toolName: "apply_patch",
146
+ params: { input: UPDATE_PATCH },
147
+ });
148
+ expect(out.toolName).toBe("edit");
149
+ expect(out.params.file_path).toBe("/Users/m4/.openclaw/workspace/MEMORY.md");
150
+ });
151
+ it("applies the shell-exec normalizer when it matches", () => {
152
+ const out = normalizeEvent({
153
+ toolName: "exec",
154
+ params: { command: "cat /Users/m4/.openclaw/workspace/MEMORY.md" },
155
+ });
156
+ expect(out.toolName).toBe("read");
157
+ expect(out.params.path).toBe("/Users/m4/.openclaw/workspace/MEMORY.md");
158
+ });
159
+ });
160
+ describe("normalizeShellExec — reads", () => {
161
+ const MEM = "/Users/m4/.openclaw/workspace/MEMORY.md";
162
+ it.each([
163
+ ["cat", `cat ${MEM}`],
164
+ ["less", `less ${MEM}`],
165
+ ["more", `more ${MEM}`],
166
+ ["head -n 20", `head -n 20 ${MEM}`],
167
+ ["tail -n 5", `tail -n 5 ${MEM}`],
168
+ ["grep with pattern", `grep foo ${MEM}`],
169
+ ["sed without -i", `sed 's/x/y/' ${MEM}`],
170
+ ])("turns `%s` into a read event", (_label, command) => {
171
+ const out = normalizeShellExec({ toolName: "exec", params: { command } });
172
+ expect(out?.toolName).toBe("read");
173
+ expect(out?.params?.path).toBe(MEM);
174
+ });
175
+ it("returns null when the path is not memory-shaped", () => {
176
+ expect(normalizeShellExec({ toolName: "exec", params: { command: "cat /etc/hosts" } })).toBeNull();
177
+ });
178
+ it("returns null for pipes / subshells (unambiguous shapes only)", () => {
179
+ expect(normalizeShellExec({
180
+ toolName: "exec",
181
+ params: { command: `cat ${MEM} | grep foo` },
182
+ })).toBeNull();
183
+ });
184
+ });
185
+ describe("normalizeShellExec — writes", () => {
186
+ const MEM = "/Users/m4/.openclaw/workspace/MEMORY.md";
187
+ it("turns `echo \"...\" > path` into a write event with content", () => {
188
+ const out = normalizeShellExec({
189
+ toolName: "exec",
190
+ params: { command: `echo "poison" > ${MEM}` },
191
+ });
192
+ expect(out?.toolName).toBe("write");
193
+ expect(out?.params?.file_path).toBe(MEM);
194
+ expect(out?.params?.content).toBe("poison");
195
+ });
196
+ it("turns `echo ... >> path` (append) into a write event", () => {
197
+ const out = normalizeShellExec({
198
+ toolName: "exec",
199
+ params: { command: `echo "more" >> ${MEM}` },
200
+ });
201
+ expect(out?.toolName).toBe("write");
202
+ expect(out?.params?.file_path).toBe(MEM);
203
+ expect(out?.params?.content).toBe("more");
204
+ });
205
+ it("turns `tee path` into a write event with empty content", () => {
206
+ const out = normalizeShellExec({
207
+ toolName: "exec",
208
+ params: { command: `tee ${MEM}` },
209
+ });
210
+ expect(out?.toolName).toBe("write");
211
+ expect(out?.params?.file_path).toBe(MEM);
212
+ expect(out?.params?.content).toBe("");
213
+ });
214
+ it("turns `sed -i ... path` into a write event carrying the sed expression", () => {
215
+ const out = normalizeShellExec({
216
+ toolName: "exec",
217
+ params: { command: `sed -i 's/x/y/' ${MEM}` },
218
+ });
219
+ expect(out?.toolName).toBe("write");
220
+ expect(out?.params?.file_path).toBe(MEM);
221
+ expect(out?.params?.content).toBe("s/x/y/");
222
+ });
223
+ it("returns null when the write path is not memory-shaped", () => {
224
+ expect(normalizeShellExec({
225
+ toolName: "exec",
226
+ params: { command: `echo "x" > /tmp/scratch.txt` },
227
+ })).toBeNull();
228
+ });
229
+ it("returns null for a non-exec tool", () => {
230
+ expect(normalizeShellExec({
231
+ toolName: "write",
232
+ params: { file_path: MEM, content: "x" },
233
+ })).toBeNull();
234
+ });
235
+ it.each(["bash", "shell", "run_command", "run_bash"])("normalizes `%s` tool name as well as exec", (toolName) => {
236
+ const out = normalizeShellExec({
237
+ toolName,
238
+ params: { command: `echo "x" > ${MEM}` },
239
+ });
240
+ expect(out?.toolName).toBe("write");
241
+ });
242
+ it("reads the command from params.cmd when params.command is absent", () => {
243
+ const out = normalizeShellExec({
244
+ toolName: "exec",
245
+ params: { cmd: `echo "x" > ${MEM}` },
246
+ });
247
+ expect(out?.toolName).toBe("write");
248
+ expect(out?.params?.file_path).toBe(MEM);
249
+ });
250
+ it("reads the command from params.script when params.command is absent", () => {
251
+ const out = normalizeShellExec({
252
+ toolName: "bash",
253
+ params: { script: `echo "x" > ${MEM}` },
254
+ });
255
+ expect(out?.toolName).toBe("write");
256
+ expect(out?.params?.file_path).toBe(MEM);
257
+ });
258
+ it("turns `cp src MEMORY.md` into a write event with empty content", () => {
259
+ const out = normalizeShellExec({
260
+ toolName: "exec",
261
+ params: { command: `cp /attacker/payload ${MEM}` },
262
+ });
263
+ expect(out?.toolName).toBe("write");
264
+ expect(out?.params?.file_path).toBe(MEM);
265
+ expect(out?.params?.content).toBe("");
266
+ });
267
+ it("turns `mv src MEMORY.md` into a write event with empty content", () => {
268
+ const out = normalizeShellExec({
269
+ toolName: "exec",
270
+ params: { command: `mv /tmp/staged ${MEM}` },
271
+ });
272
+ expect(out?.toolName).toBe("write");
273
+ expect(out?.params?.file_path).toBe(MEM);
274
+ expect(out?.params?.content).toBe("");
275
+ });
276
+ it("returns null for cp/mv to a non-memory path", () => {
277
+ expect(normalizeShellExec({
278
+ toolName: "exec",
279
+ params: { command: "cp /a/b /tmp/c.txt" },
280
+ })).toBeNull();
281
+ });
282
+ });
package/dist/index.js CHANGED
@@ -1,2 +1,5 @@
1
- "use strict";var h=Object.defineProperty;var S=Object.getOwnPropertyDescriptor;var T=Object.getOwnPropertyNames;var z=Object.prototype.hasOwnProperty;var P=(r,e)=>{for(var o in e)h(r,o,{get:e[o],enumerable:!0})},Z=(r,e,o,a)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of T(e))!z.call(r,n)&&n!==o&&h(r,n,{get:()=>e[n],enumerable:!(a=S(e,n))||a.enumerable});return r};var v=r=>Z(h({},"__esModule",{value:!0}),r);var M={};P(M,{default:()=>R});module.exports=v(M);var k=require("os"),c=require("@atbash/sdk");var b="atbash-openclaw",x=["openclaw","atbash-plugin"];function w(r){return r.config?.plugins?.entries?.[b]?.config??{}}function y(r){let e=r.config?.plugins,o=e?.entries??{},a=Array.isArray(e?.allow)?e.allow:[],n=x.filter(t=>o[t]!==void 0||a.includes(t));if(n.length===0)return;let d=o[b]?.config!==void 0;r.logger?.warn?.(`[atbash] found config under former plugin id${n.length>1?"s":""} ${n.map(t=>`"${t}"`).join(", ")} \u2014 this plugin's id is "${b}" and those entries are ignored`+(d?". Remove them to avoid confusion.":`. No "${b}" config found, so the plugin is running on defaults \u2014 move your settings under "${b}".`),{formerIds:n,configured:d})}var D=[/\bsk-ant-[A-Za-z0-9_-]{20,}/g,/\bsk-proj-[A-Za-z0-9_-]{20,}/g,/\bsk-[A-Za-z0-9]{20,}/g,/\b(?:gh[pousr]|github_pat)_[A-Za-z0-9_]{30,}/g,/\bAIza[0-9A-Za-z_-]{35}/g,/\bya29\.[0-9A-Za-z_-]{20,}/g,/\b(?:AKIA|ASIA|AGPA|AROA|ANPA|ANVA|ASCA|AIDA|AIPA)[0-9A-Z]{16}\b/g,/\b(?:sk|rk|pk)_(?:live|test)_[A-Za-z0-9]{20,}/g,/\bxox[abprseo]-[A-Za-z0-9-]{10,}/g,/\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}/g,/\bnpm_[A-Za-z0-9]{36,}\b/g,/\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}/g,/\b(?!0x[0-9a-fA-F])(?![0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}\b)(?=[A-Za-z0-9_-]*[0-9])(?=[A-Za-z0-9_-]*[A-Za-z])(?![0-9a-fA-F]+$)[A-Za-z0-9_-]{32,}\b/g,/(?<![A-Za-z0-9+/])(?=[A-Za-z0-9+/]*[+/])(?=[A-Za-z0-9+/]*[0-9])(?=[A-Za-z0-9+/]*[A-Za-z])[A-Za-z0-9+/]{40,}={0,2}(?![A-Za-z0-9+/=])/g];function A(r){let e=r;for(let o of D)o.lastIndex=0,e=e.replace(o,"[REDACTED]");return e}var F={"0xdac17f958d2ee523a2206206994597c13d831ec7":"USDT","0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48":"USDC","0x6b175474e89094c44da98b954eedeac495271d0f":"DAI","0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2":"WETH","0x2260fac5e5542a773aa44fbcfedf7c193bc2c599":"WBTC","0x1f9840a85d5af5bf1d1762f925bdaddc4201f984":"UNI","0x514910771af9ca656af840dff83e8264ecf986ca":"LINK","0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9":"AAVE","0xb0df6379ba1692841965a0745ac1bd3046d79ba3":"ATBASH","0xe9c094187219d9a29382c1a36fc43619c9257777":"ATBASH","0xf8b071428558c657a7a9aa1c43e152e75dd77777":"ATBASH"},_=/\b(0x[a-fA-F0-9]{40})\b/,N=/(0x[a-fA-F0-9]{40})/,E=/\b(transfer|send|swap|approve|erc20)\b/i;function j(r){if(!r)return"ETH";let e=r.toLowerCase(),o=N.exec(e);return o?F[o[1].toLowerCase()]??"other":e==="c60"||e==="eth"?"ETH":e==="atbash"?"ATBASH":e==="usdt"||e==="tether"?"USDT":e==="usdc"?"USDC":"other"}function p(r,...e){for(let o of e){let a=r[o];if(typeof a=="string"&&a.trim())return a.trim()}}function $(r,e){let o=e!==null&&typeof e=="object"&&!Array.isArray(e)?e:void 0,a=o?p(o,"command","cmd","shell_command"):void 0,n=E.test(r),d=a?E.test(a):!1,t=a?_.test(a):!1;if(!n&&!d&&!t)return;let f=m=>a?new RegExp(`--${m}[= ]+(\\S+)`).exec(a)?.[1]:void 0,g=(o?p(o,"token","token_address","asset","currency"):void 0)??f("token");if(!g&&a){let m=_.exec(a);m&&(g=m[1])}let u=(o?p(o,"to","recipient","destination"):void 0)??f("to")??f("recipient"),l=(o?p(o,"amount","value","qty"):void 0)??f("amount"),s=j(g??""),i=a??r;return{operation:/\bswap\b/i.test(i)?"swap":/\b(approve|erc20)\b/i.test(i)?"approve":"transfer",asset:s,amount:l,recipient_status:u?"external":"unspecified",note:"canonicalized pre-judge; raw 0x addresses omitted (redacted upstream)"}}function I(r,e){let o=r??{},a=o.toolName??e.tool?.name??e.toolName??e.name??"unknown",n=o.params??e.params??o.args??e.args??o.arguments??e.arguments,d=JSON.stringify({tool_name:a,run_id:A(e.runId??""),agent_id:A(e.agentId??""),channel_id:A(e.channelId??""),account_id:A(e.accountId??"")});return{toolName:a,args:n,context:d,resolved:$(a,n)}}function C(r){return r.replace(/^~(?=\/|$)/,(0,k.homedir)())}function L(r){return r.memoryWorkspaceDir?C(r.memoryWorkspaceDir):process.cwd()}function O(r){if(r.judgeEndpoint)return r.judgeEndpointPolicy==="self-hosted"?{policy:"self-hosted",endpoint:r.judgeEndpoint,verifyPubKey:r.judgeVerifyPubKey??""}:{policy:"default",endpoint:r.judgeEndpoint}}function R(r){let e=r,o=w(e);if(y(e),o.enabled===!1){e.logger?.info?.("[atbash] plugin disabled via config");return}(0,c.setupTelemetry)({enabled:!0,source:"plugin:openclaw"}),process.once("beforeExit",()=>(0,c.shutdownTelemetry)()),process.once("SIGINT",()=>(0,c.shutdownTelemetry)().finally(()=>process.exit(0))),process.once("SIGTERM",()=>(0,c.shutdownTelemetry)().finally(()=>process.exit(0)));let a;try{a=c.Atbash.fromConfig({judge:O(o),keyPath:o.chromiaSecretPath,orgName:o.orgName,failClosed:o.enforceDecision!==!1,logger:e.logger})}catch(t){let f=t instanceof Error?t.message:String(t);throw e.logger?.warn?.("[atbash] init failed",{error:f}),t}let n=o.enforceDecision!==!1;e.logger?.info?.("[atbash] plugin loaded",{enforceDecision:n});let d=(0,c.createMemoryGuardManager)({auth:a.auth,workspaceDir:L(o),memoryFilePath:o.memoryFilePath?C(o.memoryFilePath):void 0,ttlMs:o.memorySyncTTLMs,rollbackMinScore:o.memoryRollbackMinScore,memoryPathPatterns:o.memoryPathPatterns,judgeEndpoint:o.judgeEndpoint,judgeVerifyPubKey:o.judgeVerifyPubKey,orgName:o.orgName,enforce:n,debug:!!o.debug,hostLogger:e.logger});if(d.runBootProbe().catch(t=>{e.logger?.warn?.("[atbash] boot probe failed",{error:t instanceof Error?t.message:String(t)})}),!e.on){e.logger?.warn?.("[atbash] on() API not available");return}e.on("before_tool_call",async(t,f)=>{let g;try{g=await d.handleBeforeToolCall(t,f)}catch(s){let i=s instanceof Error?s.message:String(s);return e.logger?.warn?.("[atbash] memory guard error",{error:i}),s instanceof c.MemoryIntegrityError||n?{block:!0,blockReason:`Memory guard error: ${i}`,allow:!1,reason:`Memory guard error: ${i}`}:{allow:!0}}if(g)return g;let u;try{u=await a.auditToolCall(I(t,f))}catch(s){let i=s instanceof Error?s.message:String(s);return e.logger?.warn?.("[atbash] unexpected error",{error:i}),n?{block:!0,blockReason:`Atbash unavailable: ${i}`,allow:!1,reason:`Atbash unavailable: ${i}`}:{allow:!0}}let l=u.reason??"";switch(u.verdict){case"BLOCK":return e.logger?.warn?.("[atbash] BLOCK",{reason:l}),n?{block:!0,blockReason:l,allow:!1,reason:l}:{allow:!0};case"HOLD":{let s=["Action held for operator review. The agent will not be jailed \u2014 please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",`Reason: ${l}`];u.toolCallId&&s.push(`Tool Call ID: ${u.toolCallId}`);let i=s.join(`
2
- `);return e.logger?.warn?.("[atbash] HOLD",{reason:l,toolCallId:u.toolCallId}),n?{block:!0,blockReason:i,allow:!1,reason:i}:{allow:!0}}case"ERROR":return e.logger?.warn?.("[atbash] ERROR",{reason:l}),n?{block:!0,blockReason:l,allow:!1,reason:l}:{allow:!0};case"ALLOW":if(u.allow===!0)return e.logger?.info?.("[atbash] ALLOW",{reason:l}),{allow:!0};break}{let s=String(u.verdict);if(e.logger?.warn?.("[atbash] unusable decision",{verdict:s,allow:u.allow,reason:l}),!n)return{allow:!0};let i=`Blocked (unusable Atbash decision ${s}): ${l}`;return{block:!0,blockReason:i,allow:!1,reason:i}}})}
1
+ "use strict";var y=Object.defineProperty;var N=Object.getOwnPropertyDescriptor;var P=Object.getOwnPropertyNames;var $=Object.prototype.hasOwnProperty;var I=(t,e)=>{for(var n in e)y(t,n,{get:e[n],enumerable:!0})},v=(t,e,n,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let r of P(e))!$.call(t,r)&&r!==n&&y(t,r,{get:()=>e[r],enumerable:!(o=N(e,r))||o.enumerable});return t};var D=t=>v(y({},"__esModule",{value:!0}),t);var Z={};I(Z,{default:()=>L});module.exports=D(Z);var R=require("os"),m=require("@atbash/sdk");var h="atbash-openclaw",j=["openclaw","atbash-plugin"];function E(t){return t.config?.plugins?.entries?.[h]?.config??{}}function x(t){let e=t.config?.plugins,n=e?.entries??{},o=Array.isArray(e?.allow)?e.allow:[],r=j.filter(a=>n[a]!==void 0||o.includes(a));if(r.length===0)return;let s=n[h]?.config!==void 0;t.logger?.warn?.(`[atbash] found config under former plugin id${r.length>1?"s":""} ${r.map(a=>`"${a}"`).join(", ")} \u2014 this plugin's id is "${h}" and those entries are ignored`+(s?". Remove them to avoid confusion.":`. No "${h}" config found, so the plugin is running on defaults \u2014 move your settings under "${h}".`),{formerIds:r,configured:s})}var F={"0xdac17f958d2ee523a2206206994597c13d831ec7":"USDT","0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48":"USDC","0x6b175474e89094c44da98b954eedeac495271d0f":"DAI","0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2":"WETH","0x2260fac5e5542a773aa44fbcfedf7c193bc2c599":"WBTC","0x1f9840a85d5af5bf1d1762f925bdaddc4201f984":"UNI","0x514910771af9ca656af840dff83e8264ecf986ca":"LINK","0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9":"AAVE","0xb0df6379ba1692841965a0745ac1bd3046d79ba3":"ATBASH","0xe9c094187219d9a29382c1a36fc43619c9257777":"ATBASH","0xf8b071428558c657a7a9aa1c43e152e75dd77777":"ATBASH"},A=/\b(0x[a-fA-F0-9]{40})\b/,M=/(0x[a-fA-F0-9]{40})/,C=/\b(transfer|send|swap|approve|erc20)\b/i;function O(t){if(!t)return"ETH";let e=t.toLowerCase(),n=M.exec(e);return n?F[n[1].toLowerCase()]??"other":e==="c60"||e==="eth"?"ETH":e==="atbash"?"ATBASH":e==="usdt"||e==="tether"?"USDT":e==="usdc"?"USDC":"other"}function w(t,...e){for(let n of e){let o=t[n];if(typeof o=="string"&&o.trim())return o.trim()}}function H(t,e){let n=e!==null&&typeof e=="object"&&!Array.isArray(e)?e:void 0,o=n?w(n,"command","cmd","shell_command"):void 0,r=C.test(t),s=o?C.test(o):!1,a=o?A.test(o):!1;if(!r&&!s&&!a)return;let c=b=>o?new RegExp(`--${b}[= ]+(\\S+)`).exec(o)?.[1]:void 0,u=(n?w(n,"token","token_address","asset","currency"):void 0)??c("token");if(!u&&o){let b=A.exec(o);b&&(u=b[1])}let g=(n?w(n,"to","recipient","destination"):void 0)??c("to")??c("recipient"),f=(n?w(n,"amount","value","qty"):void 0)??c("amount"),d=O(u??""),i=o??t;return{operation:/\bswap\b/i.test(i)?"swap":/\b(approve|erc20)\b/i.test(i)?"approve":"transfer",asset:d,amount:f,recipient_status:g?"external":"unspecified",note:"canonicalized pre-judge; raw 0x addresses omitted (redacted upstream)"}}function _(t,e){let n=t??{},o=n.toolName??e.tool?.name??e.toolName??e.name??"unknown",r=n.params??e.params??n.args??e.args??n.arguments??e.arguments,s=JSON.stringify({tool_name:o});return{toolName:o,args:r,context:s,resolved:H(o,r)}}var W=["memory.md","dreams.md","claude.md","agents.md","/.openclaw/","/.claude/projects/","/memory/"];function S(t){let e=t.replace(/\\/g,"/").toLowerCase();return W.some(n=>e.includes(n))}var B=/^\*\*\* (Update|Add|Delete) File:\s*(.+?)\s*$/mg;function U(t,e){let n=t.split(`
2
+ `),o=!1,r=[];for(let s of n){let a=/^\*\*\* (?:Update|Add|Delete) File:\s*(.+?)\s*$/.exec(s);if(a){o=a[1]===e;continue}o&&(s.startsWith("*** ")||s.startsWith("@@")||s.startsWith("+")&&r.push(s.slice(1)))}return r.join(`
3
+ `)}function z(t){if(!t||typeof t!="object")return null;let e=t;if((e.toolName??e.tool_name??"").toString().toLowerCase()!=="apply_patch")return null;let o=e.params??e.args??e.arguments??{},r=o.input;if(typeof r!="string"||r.length===0)return null;let s=r.replace(/\r\n/g,`
4
+ `),a=new RegExp(B.source,"mg"),c,u,g;for(;(g=a.exec(s))!==null;){let[,i,l]=g;if(i!=="Delete"&&S(l)){c=i,u=l;break}}if(!c||!u)return null;let f=U(s,u);return{...e,toolName:c==="Add"?"write":"edit",params:{...o,file_path:u,content:f,new_string:f}}}function p(t){let e=t.trim();return e.startsWith('"')&&e.endsWith('"')||e.startsWith("'")&&e.endsWith("'")?e.slice(1,-1):e}var K=/[|;&`$]|<\(|\)/;function G(t){return K.test(t)}function V(t){if(G(t))return null;let e=t.trim(),n=/^grep\b(?:\s+-\S+)*\s+\S+\s+(.+)$/.exec(e);if(n)return p(n[1]);let o=/^sed\b(?!\s+-i\b)(?:\s+-\S+)*\s+\S+\s+(.+)$/.exec(e);if(o)return p(o[1]);let r=/^(?:cat|less|more|head|tail)\b(?:\s+-\S+(?:\s+\S+)?)*\s+(.+)$/.exec(e);return r?p(r[1]):null}function q(t){if(/[|;&`$]|<\(|\)/.test(t))return null;let e=t.trim(),n=/^(?:echo|printf)\s+(.+?)\s*>>?\s*(\S+)\s*$/.exec(e);if(n)return{content:p(n[1]),path:p(n[2])};let o=/^tee\b(?:\s+-\S+)*\s+(\S+)\s*$/.exec(e);if(o)return{content:"",path:p(o[1])};let r=/^sed\s+-i(?:\s+-\S+)*\s+(\S+)\s+(\S+)\s*$/.exec(e);if(r)return{content:p(r[1]),path:p(r[2])};let s=/^awk\s+-i\s+inplace\b.*\s+(\S+)\s*$/.exec(e);if(s)return{content:"",path:p(s[1])};let a=/^(?:cp|mv)\b(?:\s+-\S+)*\s+\S+\s+(\S+)\s*$/.exec(e);return a?{content:"",path:p(a[1])}:null}var J=new Set(["exec","bash","shell","run_command","run_bash"]);function Y(t){if(!t||typeof t!="object")return null;let e=t,n=(e.toolName??e.tool_name??"").toString().toLowerCase();if(!J.has(n))return null;let r=e.params??e.args??e.arguments??{},s=r.command??r.cmd??r.script,a=typeof s=="string"?s:null;if(!a||a.length===0)return null;let c=q(a);if(c&&S(c.path))return{...e,toolName:"write",params:{...r,file_path:c.path,content:c.content}};let u=V(a);return u&&S(u)?{...e,toolName:"read",params:{...r,path:u}}:null}function k(t){return z(t)??Y(t)??t}function T(t){return t.replace(/^~(?=\/|$)/,(0,R.homedir)())}function X(t){return t.memoryWorkspaceDir?T(t.memoryWorkspaceDir):process.cwd()}function Q(t){if(t.judgeEndpoint)return t.judgeEndpointPolicy==="self-hosted"?{policy:"self-hosted",endpoint:t.judgeEndpoint,verifyPubKey:t.judgeVerifyPubKey??""}:{policy:"default",endpoint:t.judgeEndpoint}}function L(t){let e=t,n=E(e);if(x(e),n.enabled===!1){e.logger?.info?.("[atbash] plugin disabled via config");return}(0,m.setupTelemetry)({enabled:!0,source:"plugin:openclaw"}),process.once("beforeExit",()=>(0,m.shutdownTelemetry)()),process.once("SIGINT",()=>(0,m.shutdownTelemetry)().finally(()=>process.exit(0))),process.once("SIGTERM",()=>(0,m.shutdownTelemetry)().finally(()=>process.exit(0)));let o;try{o=m.Atbash.fromConfig({judge:Q(n),keyPath:n.chromiaSecretPath,orgName:n.orgName,failClosed:n.enforceDecision!==!1,logger:e.logger})}catch(a){let c=a instanceof Error?a.message:String(a);throw e.logger?.warn?.("[atbash] init failed",{error:c}),a}let r=n.enforceDecision!==!1;e.logger?.info?.("[atbash] plugin loaded",{enforceDecision:r});let s=(0,m.createMemoryGuardManager)({auth:o.auth,workspaceDir:X(n),memoryFilePath:n.memoryFilePath?T(n.memoryFilePath):void 0,ttlMs:n.memorySyncTTLMs,rollbackMinScore:n.memoryRollbackMinScore,memoryPathPatterns:n.memoryPathPatterns,judgeEndpoint:n.judgeEndpoint,judgeVerifyPubKey:n.judgeVerifyPubKey,orgName:n.orgName,enforce:r,debug:!!n.debug,hostLogger:e.logger});if(s.runBootProbe().catch(a=>{e.logger?.warn?.("[atbash] boot probe failed",{error:a instanceof Error?a.message:String(a)})}),!e.on){e.logger?.warn?.("[atbash] on() API not available");return}e.on("before_tool_call",async(a,c)=>{let u=k(a),g;try{g=await s.handleBeforeToolCall(u,c)}catch(i){let l=i instanceof Error?i.message:String(i);return e.logger?.warn?.("[atbash] memory guard error",{error:l}),i instanceof m.MemoryIntegrityError||r?{block:!0,blockReason:`Memory guard error: ${l}`,allow:!1,reason:`Memory guard error: ${l}`}:{allow:!0}}if(g)return g;let f;try{f=await o.auditToolCall(_(a,c))}catch(i){let l=i instanceof Error?i.message:String(i);return e.logger?.warn?.("[atbash] unexpected error",{error:l}),r?{block:!0,blockReason:`Atbash unavailable: ${l}`,allow:!1,reason:`Atbash unavailable: ${l}`}:{allow:!0}}let d=f.reason??"";switch(f.verdict){case"BLOCK":return e.logger?.warn?.("[atbash] BLOCK",{reason:d}),r?{block:!0,blockReason:d,allow:!1,reason:d}:{allow:!0};case"HOLD":{let i=["Action held for operator review. The agent will not be jailed \u2014 please approve or reject this request from the Atbash dashboard, then ask the agent to try again.",`Reason: ${d}`];f.toolCallId&&i.push(`Tool Call ID: ${f.toolCallId}`);let l=i.join(`
5
+ `);return e.logger?.warn?.("[atbash] HOLD",{reason:d,toolCallId:f.toolCallId}),r?{block:!0,blockReason:l,allow:!1,reason:l}:{allow:!0}}case"ERROR":return e.logger?.warn?.("[atbash] ERROR",{reason:d}),r?{block:!0,blockReason:d,allow:!1,reason:d}:{allow:!0};case"ALLOW":if(f.allow===!0)return e.logger?.info?.("[atbash] ALLOW",{reason:d}),{allow:!0};break}{let i=String(f.verdict);if(e.logger?.warn?.("[atbash] unusable decision",{verdict:i,allow:f.allow,reason:d}),!r)return{allow:!0};let l=`Blocked (unusable Atbash decision ${i}): ${d}`;return{block:!0,blockReason:l,allow:!1,reason:l}}})}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/atbash-openclaw",
3
- "version": "0.1.17-dev.0",
3
+ "version": "0.1.17-dev.1",
4
4
  "description": "OpenClaw ATBASH tool-audit plugin. Thin adapter that maps OpenClaw's before_tool_call hook onto @atbash/sdk.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -41,7 +41,7 @@
41
41
  ]
42
42
  },
43
43
  "dependencies": {
44
- "@atbash/sdk": "0.10.13-dev.0"
44
+ "@atbash/sdk": "0.13.0-dev.0"
45
45
  },
46
46
  "devDependencies": {
47
47
  "@types/node": "^25.7.0",