@atbash/atbash-openclaw 0.1.16 → 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.
package/LICENSE ADDED
@@ -0,0 +1,28 @@
1
+ Atbash CLI — Proprietary Software License
2
+
3
+ Copyright (c) 2026 Atbash,
4
+ All rights reserved.
5
+
6
+ Redistribution and use in source and binary forms, with or without
7
+ modification, are permitted provided that the following conditions are met:
8
+
9
+ 1. Redistributions of source code must retain the above copyright notice, this
10
+ list of conditions and the following disclaimer.
11
+ 2. Redistributions in binary form must reproduce the above copyright notice,
12
+ this list of conditions and the following disclaimer in the documentation
13
+ and/or other materials provided with the distribution.
14
+
15
+ THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
16
+ ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
17
+ WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
18
+ DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
19
+ ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
20
+ (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
21
+ LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
22
+ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
23
+ (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
24
+ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
25
+
26
+ The views and conclusions contained in the software and documentation are those
27
+ of the authors and should not be interpreted as representing official policies,
28
+ either expressed or implied, Atbash.
package/README.md CHANGED
@@ -7,9 +7,12 @@ If you're not using OpenClaw and want to use ATBASH from your own code, install
7
7
  ## Install
8
8
 
9
9
  ```bash
10
- openclaw plugins install @atbash/atbash-openclaw
10
+ openclaw plugins install @atbash/atbash-openclaw # production
11
+ openclaw plugins install @atbash/atbash-openclaw@dev # development
11
12
  ```
12
13
 
14
+ **The judge endpoint and chain ids are compiled into the build**, so the npm tag selects the environment. Install the plain tag if your agent was onboarded on production, the `@dev` tag if it was onboarded on development; `openclaw plugins list` shows which you have (a `-dev.N` version is the development build). No configuration repoints a build afterwards — the wrong one loads cleanly, fires its hook, and then fails every judge call because the agent does not exist on the chain that build targets. Organization names are not unique across environments, so a resolving org name is not proof the build is right.
15
+
13
16
  The plugin signs audit transactions locally with your agent's secp256k1 private key. The key never leaves your machine; only signed bytes plus the corresponding public key are transmitted.
14
17
 
15
18
  ## Get an agent key
@@ -34,37 +37,40 @@ Contact the ATBASH team to register an agent and obtain a key pair.
34
37
 
35
38
  ## Configure it in OpenClaw
36
39
 
37
- Open `~/.openclaw/openclaw.json` and add:
40
+ `openclaw plugins install` already recorded where the plugin lives, enabled it, and — if you keep a
41
+ `plugins.allow` list — added `atbash-openclaw` to it. Never hand-write the install directory: OpenClaw has
42
+ moved it between releases and records whichever it used, which is what it loads from. `openclaw plugins list`
43
+ shows the current state.
44
+
45
+ All that is left is your configuration. Open `~/.openclaw/openclaw.json` and add, keeping the plugins
46
+ already there:
38
47
 
39
48
  ```json
40
49
  {
41
50
  "plugins": {
42
- "allow": [
43
- "openclaw"
44
- ],
45
- "load": {
46
- "paths": [
47
- "/Users/<your-username>/.openclaw/extensions/openclaw"
48
- ]
49
- },
50
51
  "entries": {
51
- "openclaw": {
52
+ "atbash-openclaw": {
52
53
  "enabled": true,
53
54
  "config": {
54
55
  "enabled": true,
55
56
  "enforceDecision": true,
56
- "chromiaSecretPath": "~/.config/atbash/guard-client-key"
57
+ "chromiaSecretPath": "~/.config/atbash/guard-client-key",
58
+ "orgName": "<your org name>"
57
59
  },
58
60
  "hooks": {
59
61
  "allowConversationAccess": true,
60
62
  "allowPromptInjection": true
61
- }
63
+ }
62
64
  }
63
65
  }
64
66
  }
65
67
  }
66
68
  ```
67
69
 
70
+ The plugin id is `atbash-openclaw` — it comes from the `id` field in this package's `openclaw.plugin.json`,
71
+ not from the npm package name. A `plugins.entries` key that does not match the id is not an error: the
72
+ plugin loads unconfigured and audits nothing.
73
+
68
74
  ### Config fields
69
75
 
70
76
  | Field | Type | Default | What it does |
@@ -72,18 +78,19 @@ Open `~/.openclaw/openclaw.json` and add:
72
78
  | `enabled` | bool | `true` | Master switch. `false` = plugin returns immediately. |
73
79
  | `enforceDecision` | bool | `true` | Surfaced to logs. The plugin always blocks on `BLOCK`. |
74
80
  | `chromiaSecretPath` | string | `~/.config/atbash/guard-client-key` | Path to the agent key file. Supports `~/`. |
81
+ | `orgName` | string | — | Organization the agent was onboarded under, spelled as the dashboard shows it. The plugin resolves which chain to query from this, so it is required when the org is on a private chain. |
75
82
  | `debug` | bool | `false` | When `true`, logs a one-line probe for every `before_tool_call` showing `toolName`, top-level event/ctx/args keys, and whether the call was classified as a memory write. Useful for verifying memory-write classifier coverage against real traffic. Logs shape only — argument values are never printed. |
76
83
 
77
84
  ## Updating
78
85
 
79
86
  ```bash
80
- openclaw plugins update @atbash/atbash-openclaw
87
+ openclaw plugins update atbash-openclaw
81
88
  ```
82
89
 
83
90
  ## Uninstalling
84
91
 
85
92
  ```bash
86
- openclaw plugins uninstall @atbash/atbash-openclaw
93
+ openclaw plugins uninstall atbash-openclaw
87
94
  ```
88
95
 
89
96
  The agent key file at `~/.config/atbash/guard-client-key` is **not** removed — delete it manually if you want to retire the agent identity.
@@ -111,11 +118,19 @@ The agent's own tool execution is unaffected — only the data sent to the judge
111
118
  ## Troubleshooting
112
119
 
113
120
  **Tool calls aren't being audited.**
114
- Check `enabled: true` is set both on the entry (`plugins.entries.openclaw.enabled`) and inside `config` (`config.enabled`).
121
+ Check `enabled: true` is set both on the entry (`plugins.entries["atbash-openclaw"].enabled`) and inside `config` (`config.enabled`).
115
122
 
116
123
  **Every tool call is blocked, even safe ones.**
117
124
  The plugin is fail-closed: any pipeline error blocks. Look for `[atbash] …` warnings in the OpenClaw log. Most often: missing key, expired key, or restricted egress.
118
125
 
126
+ ## Maintainer release contract
127
+
128
+ Development packages are released only through the protected **Publish development package** GitHub workflow. The workflow requires the `npm` environment, the exact `PUBLISH` confirmation, and npm trusted-publisher configuration for this repository and workflow. It never accepts a registry token from package source.
129
+
130
+ The release guard refuses a dirty checkout, reads the live package and SDK tags, chooses a collision-free prerelease, copies only Git-tracked files into a temporary staging tree, pins the exact SDK version there, installs and tests from the staged lockfile, cleans and rebuilds `dist`, validates the tarball, rechecks registry state, publishes, and verifies the resulting `dev` tag. The tracked source manifest is never rewritten by a release.
131
+
132
+ Because this repository is private, npm provenance statements are not supported. Trusted publishing still removes long-lived npm tokens; configure the protected npm environment and trusted publisher before enabling the workflow.
133
+
119
134
  ## License
120
135
 
121
136
  Proprietary — all rights reserved. See [LICENSE](https://atbash.ai/license). Commercial licensing inquiries: contact the Atbash team.
package/dist/config.js CHANGED
@@ -1,5 +1,33 @@
1
1
  const PLUGIN_ID = "atbash-openclaw";
2
+ /**
3
+ * Ids this plugin shipped under before it was renamed. OpenClaw matches
4
+ * `plugins.entries` keys against the manifest id, so an entry left under one of
5
+ * these is inert — no error, no config, and the plugin runs on defaults. That
6
+ * silence is what makes the rename expensive, so we say something instead.
7
+ */
8
+ const FORMER_PLUGIN_IDS = ["openclaw", "atbash-plugin"];
2
9
  /** @internal */
3
10
  export function readPluginConfig(api) {
4
11
  return api.config?.plugins?.entries?.[PLUGIN_ID]?.config ?? {};
5
12
  }
13
+ /**
14
+ * Warn when configuration is found under a former plugin id.
15
+ *
16
+ * @internal
17
+ */
18
+ export function warnOnFormerIdConfig(api) {
19
+ const plugins = api.config?.plugins;
20
+ const entries = plugins?.entries ?? {};
21
+ const allow = Array.isArray(plugins?.allow) ? plugins.allow : [];
22
+ const stale = FORMER_PLUGIN_IDS.filter((id) => entries[id] !== undefined || allow.includes(id));
23
+ if (stale.length === 0)
24
+ return;
25
+ const configured = entries[PLUGIN_ID]?.config !== undefined;
26
+ api.logger?.warn?.(`[atbash] found config under former plugin id${stale.length > 1 ? "s" : ""} ` +
27
+ `${stale.map((id) => `"${id}"`).join(", ")} — this plugin's id is "${PLUGIN_ID}" ` +
28
+ `and those entries are ignored` +
29
+ (configured
30
+ ? ". Remove them to avoid confusion."
31
+ : `. No "${PLUGIN_ID}" config found, so the plugin is running on defaults — ` +
32
+ `move your settings under "${PLUGIN_ID}".`), { formerIds: stale, configured });
33
+ }
@@ -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,13 +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
- const context = JSON.stringify({
118
- tool_name: toolName,
119
- session_id: redactValue(ctx.sessionKey ?? ""),
120
- run_id: redactValue(ctx.runId ?? ""),
121
- agent_id: redactValue(ctx.agentId ?? ""),
122
- channel_id: redactValue(ctx.channelId ?? ""),
123
- account_id: redactValue(ctx.accountId ?? ""),
124
- });
88
+ // Only tool_name is safe to include in signed bytes — it'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 });
125
95
  return { toolName, args, context, resolved: canonicalizeFinancial(toolName, args) };
126
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 p=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var z=Object.getOwnPropertyNames;var C=Object.prototype.hasOwnProperty;var S=(r,e)=>{for(var a in e)p(r,a,{get:e[a],enumerable:!0})},R=(r,e,a,o)=>{if(e&&typeof e=="object"||typeof e=="function")for(let n of z(e))!C.call(r,n)&&n!==a&&p(r,n,{get:()=>e[n],enumerable:!(o=I(e,n))||o.enumerable});return r};var Z=r=>R(p({},"__esModule",{value:!0}),r);var O={};S(O,{default:()=>k});module.exports=Z(O);var E=require("os"),c=require("@atbash/sdk");var P="atbash-openclaw";function h(r){return r.config?.plugins?.entries?.[P]?.config??{}}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 b(r){let e=r;for(let a of D)a.lastIndex=0,e=e.replace(a,"[REDACTED]");return e}var x={"0xdac17f958d2ee523a2206206994597c13d831ec7":"USDT","0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48":"USDC","0x6b175474e89094c44da98b954eedeac495271d0f":"DAI","0xc02aaa39b223fe8d0a0e5c4f27ead9083c756cc2":"WETH","0x2260fac5e5542a773aa44fbcfedf7c193bc2c599":"WBTC","0x1f9840a85d5af5bf1d1762f925bdaddc4201f984":"UNI","0x514910771af9ca656af840dff83e8264ecf986ca":"LINK","0x7fc66500c84a76ad7e9c93437bfc5ac33e2ddae9":"AAVE","0xb0df6379ba1692841965a0745ac1bd3046d79ba3":"ATBASH","0xe9c094187219d9a29382c1a36fc43619c9257777":"ATBASH","0xf8b071428558c657a7a9aa1c43e152e75dd77777":"ATBASH"},y=/\b(0x[a-fA-F0-9]{40})\b/,v=/(0x[a-fA-F0-9]{40})/,w=/\b(transfer|send|swap|approve|erc20)\b/i;function F(r){if(!r)return"ETH";let e=r.toLowerCase(),a=v.exec(e);return a?x[a[1].toLowerCase()]??"other":e==="c60"||e==="eth"?"ETH":e==="atbash"?"ATBASH":e==="usdt"||e==="tether"?"USDT":e==="usdc"?"USDC":"other"}function m(r,...e){for(let a of e){let o=r[a];if(typeof o=="string"&&o.trim())return o.trim()}}function N(r,e){let a=e!==null&&typeof e=="object"&&!Array.isArray(e)?e:void 0,o=a?m(a,"command","cmd","shell_command"):void 0,n=w.test(r),g=o?w.test(o):!1,t=o?y.test(o):!1;if(!n&&!g&&!t)return;let d=A=>o?new RegExp(`--${A}[= ]+(\\S+)`).exec(o)?.[1]:void 0,f=(a?m(a,"token","token_address","asset","currency"):void 0)??d("token");if(!f&&o){let A=y.exec(o);A&&(f=A[1])}let u=(a?m(a,"to","recipient","destination"):void 0)??d("to")??d("recipient"),l=(a?m(a,"amount","value","qty"):void 0)??d("amount"),s=F(f??""),i=o??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 _(r,e){let a=r??{},o=a.toolName??e.tool?.name??e.toolName??e.name??"unknown",n=a.params??e.params??a.args??e.args??a.arguments??e.arguments,g=JSON.stringify({tool_name:o,session_id:b(e.sessionKey??""),run_id:b(e.runId??""),agent_id:b(e.agentId??""),channel_id:b(e.channelId??""),account_id:b(e.accountId??"")});return{toolName:o,args:n,context:g,resolved:N(o,n)}}function T(r){return r.replace(/^~(?=\/|$)/,(0,E.homedir)())}function j(r){return r.memoryWorkspaceDir?T(r.memoryWorkspaceDir):process.cwd()}function L(r){if(r.judgeEndpoint)return r.judgeEndpointPolicy==="self-hosted"?{policy:"self-hosted",endpoint:r.judgeEndpoint,verifyPubKey:r.judgeVerifyPubKey??""}:{policy:"default",endpoint:r.judgeEndpoint}}function k(r){let e=r,a=h(e);if(a.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 o;try{o=c.Atbash.fromConfig({judge:L(a),keyPath:a.chromiaSecretPath,orgName:a.orgName,failClosed:a.enforceDecision!==!1,logger:e.logger})}catch(t){let d=t instanceof Error?t.message:String(t);throw e.logger?.warn?.("[atbash] init failed",{error:d}),t}let n=a.enforceDecision!==!1;e.logger?.info?.("[atbash] plugin loaded",{enforceDecision:n});let g=(0,c.createMemoryGuardManager)({auth:o.auth,workspaceDir:j(a),memoryFilePath:a.memoryFilePath?T(a.memoryFilePath):void 0,ttlMs:a.memorySyncTTLMs,rollbackMinScore:a.memoryRollbackMinScore,memoryPathPatterns:a.memoryPathPatterns,judgeEndpoint:a.judgeEndpoint,judgeVerifyPubKey:a.judgeVerifyPubKey,orgName:a.orgName,enforce:n,debug:!!a.debug,hostLogger:e.logger});if(g.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,d)=>{let f;try{f=await g.handleBeforeToolCall(t,d)}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(f)return f;let u;try{u=await o.auditToolCall(_(t,d))}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":default:return e.logger?.info?.("[atbash] ALLOW",{reason:l}),{allow:!0}}})}
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.16",
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",
@@ -11,10 +11,13 @@
11
11
  "LICENSE"
12
12
  ],
13
13
  "scripts": {
14
- "build": "tsc && esbuild dist/index.js --bundle --platform=node --external:postchain-client --external:@atbash/sdk --minify --allow-overwrite --outfile=dist/index.js",
14
+ "build": "node scripts/clean.mjs && tsc && esbuild dist/index.js --bundle --platform=node --external:postchain-client --external:@atbash/sdk --minify --allow-overwrite --outfile=dist/index.js",
15
15
  "typecheck": "tsc --noEmit",
16
- "release": "npm pkg set \"dependencies[@atbash/sdk]=^$(npm view @atbash/sdk dist-tags.latest)\" && npm version patch --no-git-tag-version && npm run build && npx npm@10 publish --access public",
17
- "release:dev": "npm pkg set \"dependencies[@atbash/sdk]=$(npm view @atbash/sdk dist-tags.dev)\" && npm version prerelease --preid=dev --no-git-tag-version && npm run build && npx npm@10 publish --access public --tag dev"
16
+ "test": "tsc --noEmit && vitest run && node test/build.mjs && node --test test/verdict-handling.test.mjs test/safety-guard-doc.test.mjs",
17
+ "test:release": "node --test test/release-guard.test.mjs",
18
+ "verify:package": "node scripts/release.mjs --verify-current",
19
+ "release": "node scripts/release.mjs --channel latest",
20
+ "release:dev": "node scripts/release.mjs --channel dev"
18
21
  },
19
22
  "keywords": [
20
23
  "openclaw",
@@ -27,17 +30,24 @@
27
30
  ],
28
31
  "author": "atbash",
29
32
  "license": "LICENSED",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/Atbash-Ai/atbash-openclaw-plugin",
36
+ "directory": "openclaw"
37
+ },
30
38
  "openclaw": {
31
39
  "extensions": [
32
40
  "./dist/index.js"
33
41
  ]
34
42
  },
35
43
  "dependencies": {
36
- "@atbash/sdk": "^0.7.1"
44
+ "@atbash/sdk": "0.13.0-dev.0"
37
45
  },
38
46
  "devDependencies": {
39
47
  "@types/node": "^25.7.0",
40
48
  "esbuild": "^0.24.0",
41
- "typescript": "^5.9.3"
49
+ "semver": "7.8.5",
50
+ "typescript": "^5.9.3",
51
+ "vitest": "^4.1.6"
42
52
  }
43
53
  }