@atbash/atbash-mcp 0.0.10 → 0.0.12-dev.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +56 -4
  2. package/dist/index.js +128 -23
  3. package/package.json +16 -7
package/README.md CHANGED
@@ -84,7 +84,14 @@ Returns: `{ verdict, allow, reason, tool_call_id }`
84
84
 
85
85
  #### `atbash_log`
86
86
 
87
- Log a tool call on-chain without requesting a verdict.
87
+ Submit a tool call through Atbash's redacting safety-judgment path. The judge
88
+ logs/broadcasts the call and returns the same fail-closed decision contract as
89
+ `atbash_judge`: `{ verdict, allow, reason, toolCallId }`.
90
+
91
+ Despite its legacy name, this is not a distinct non-judging log operation. The
92
+ pinned SDK has no server-broadcasting audit-only API; its `logToolCall()` method
93
+ only creates a local signed transaction, so this MCP tool deliberately does not
94
+ use or expose that local-only result as proof of persistence.
88
95
 
89
96
  | Input | Type | Required |
90
97
  |---|---|---|
@@ -130,10 +137,55 @@ Poll the status of a previously submitted judgment.
130
137
 
131
138
  | Verdict | Meaning | What To Do |
132
139
  |---|---|---|
133
- | `ALLOW` | Safe to proceed | Execute the real tool |
134
- | `HOLD` | Logged for async review | Execution is allowed; the action is flagged for operator review in the background — keep `tool_call_id` to poll status later |
140
+ | `ALLOW` | Safe to proceed | Execute the real tool only when `allow` is also exactly `true` |
141
+ | `HOLD` | Pending operator review | Do not proceed; keep `tool_call_id` to poll status later |
135
142
  | `BLOCK` | Policy violation | Stop; show `reason` to operator |
136
- | `ERROR` | Judge unreachable | Fail closed |
143
+ | `ERROR` | Judge unreachable | Fail closed — do not proceed |
144
+
145
+ Unknown or missing verdicts, and `ALLOW` responses carrying `allow: false`, are
146
+ also returned with `allow: false`.
147
+
148
+ ### Enforcement is advisory in MCP
149
+
150
+ An MCP server exposes tools; it cannot intercept the *other* tools your client
151
+ calls. So this package can tell you an action is unsafe, but it cannot stop the
152
+ client from running it — the model has to honour the verdict. If a verdict is
153
+ unreachable, both `atbash_judge` and `atbash_log` return
154
+ `{"verdict": "ERROR", "allow": false}` with an explicit instruction not to
155
+ proceed, rather than a bare transport error that is easy to mistake for a
156
+ broken tool.
157
+
158
+ For enforcement that a compromised or prompt-injected agent cannot talk its way
159
+ past, use a plugin that wraps the tool-call boundary itself — for example
160
+ `@atbash/atbash-langchain`, `@atbash/atbash-langgraph`, the OpenClaw plugin, or
161
+ the Codex/ChatGPT hook, all of which refuse the call before the tool executes.
162
+
163
+ An `ALLOW` also has to have a verdict behind it. `@atbash/sdk` 0.5.3-dev.0
164
+ treats a judge response body with no `verdict` field as an audit-tier pass and
165
+ synthesizes `{"allow": true, "verdict": "ALLOW"}` from it — including for a body
166
+ that goes on to say `"action_type": "block"`. The shipped configuration points
167
+ at an allowlisted endpoint, for which the SDK sets no `verifyPubKey` and so
168
+ skips response-signature verification entirely, which leaves schema drift, a
169
+ truncated response and a rewriting proxy all arriving as permission. This
170
+ package therefore re-checks every allow against the `JudgeResult` the SDK parsed
171
+ that body into, and refuses it with `{"verdict": "ERROR", "allow": false}`
172
+ unless the judge sent a verdict and an `action_type` of exactly `allow`.
173
+
174
+ Be precise about what that inspects, because it is one layer above the HTTP
175
+ body: `action_type` is the response body's value verbatim, whereas the verdict
176
+ has already been case-folded and aliased by the SDK. So `"verdict": "green"`
177
+ is honoured as an `ALLOW` — `GREEN` is the judge's own alias, not drift — while
178
+ an absent verdict is refused. An organization on a logging-only tier therefore
179
+ sees denials rather than passes; that is deliberate, since absence of a verdict
180
+ is not a verdict.
181
+
182
+ The judge endpoint is resolved through `Atbash.fromConfig`, which validates
183
+ `ATBASH_ENDPOINT` against the SDK's trusted allowlist / self-hosted policy. The
184
+ plain `new Atbash(privkey, { endpoint })` constructor performs no such check,
185
+ so an injected endpoint would be trusted outright. Startup also requires the
186
+ endpoint-validation and fail-closed audit capabilities of the pinned
187
+ `@atbash/sdk`; the process refuses to start if they are absent, rather than
188
+ running with an inert safety gate.
137
189
 
138
190
  ## Sending Better Inputs
139
191
 
package/dist/index.js CHANGED
@@ -16,10 +16,10 @@ function registerSafetyPrompt(server2) {
16
16
  "atbash_safety_check",
17
17
  {
18
18
  description: "Prepare a consistent pre-execution Atbash safety review request.",
19
- inputSchema: z.object({
19
+ argsSchema: {
20
20
  action: z.string().describe("Action to evaluate"),
21
21
  context: z.string().describe("Why the action is being taken")
22
- })
22
+ }
23
23
  },
24
24
  async ({ action, context }) => ({
25
25
  messages: [
@@ -46,6 +46,86 @@ function registerPolicyResource(server2, client2) {
46
46
  void client2;
47
47
  }
48
48
 
49
+ // src/startup.ts
50
+ function assertMcpSdkCapabilities(sdk) {
51
+ const missing = [
52
+ typeof sdk.fromConfig === "function" ? null : "fromConfig",
53
+ typeof sdk.prototype?.auditToolCall === "function" ? null : "auditToolCall"
54
+ ].filter((name) => name !== null);
55
+ if (missing.length > 0) {
56
+ throw new Error(
57
+ `Incompatible @atbash/sdk: missing required MCP capabilities: ${missing.join(", ")}. Refusing to start without an active safety gate.`
58
+ );
59
+ }
60
+ }
61
+ var DECISION_VERDICTS = [
62
+ "ALLOW",
63
+ "HOLD",
64
+ "BLOCK",
65
+ "ERROR"
66
+ ];
67
+ function isDecisionShaped(value) {
68
+ const decision = value;
69
+ return typeof decision === "object" && decision !== null && typeof decision.allow === "boolean" && DECISION_VERDICTS.includes(decision.verdict);
70
+ }
71
+ function hardenJudgeClient(client2) {
72
+ const sdkAudit = client2.auditToolCall;
73
+ const sdkJudge = client2.judgeAction;
74
+ async function guardedAuditToolCall(input) {
75
+ let wire;
76
+ const shadow = Object.create(client2);
77
+ Object.defineProperty(shadow, "judgeAction", {
78
+ value: async (...args) => {
79
+ const result = await sdkJudge.apply(client2, args);
80
+ wire = result;
81
+ return result;
82
+ }
83
+ });
84
+ const decision = await sdkAudit.call(shadow, input);
85
+ if (!isDecisionShaped(decision)) {
86
+ throw new Error(
87
+ "judge decision is not verdict-shaped and cannot be trusted"
88
+ );
89
+ }
90
+ if (decision.verdict !== "ALLOW" || decision.allow !== true) {
91
+ return { ...decision, allow: false };
92
+ }
93
+ if (wire === void 0) {
94
+ throw new Error(
95
+ "judge returned an allow without a judge response to back it"
96
+ );
97
+ }
98
+ if (wire.verdict !== "ALLOW" || wire.actionType !== "allow") {
99
+ throw new Error(
100
+ `judge response body does not carry an allow (verdict ${JSON.stringify(wire.verdict)}, action_type ${JSON.stringify(wire.actionType)})`
101
+ );
102
+ }
103
+ return decision;
104
+ }
105
+ Object.defineProperty(client2, "auditToolCall", {
106
+ value: guardedAuditToolCall,
107
+ writable: true,
108
+ configurable: true
109
+ });
110
+ return client2;
111
+ }
112
+ function createAtbashClient(env, sdk) {
113
+ assertMcpSdkCapabilities(sdk);
114
+ const agentKey = env.ATBASH_AGENT_PRIVKEY;
115
+ if (!agentKey) {
116
+ throw new Error("ATBASH_AGENT_PRIVKEY env var is required");
117
+ }
118
+ const endpoint = env.ATBASH_ENDPOINT?.trim();
119
+ const orgName = env.ATBASH_ORG_NAME?.trim();
120
+ return hardenJudgeClient(
121
+ sdk.fromConfig({
122
+ agentKey,
123
+ ...endpoint ? { judge: { endpoint } } : {},
124
+ ...orgName ? { orgName } : {}
125
+ })
126
+ );
127
+ }
128
+
49
129
  // src/tools/judge.ts
50
130
  import { z as z2 } from "zod";
51
131
 
@@ -66,6 +146,28 @@ function toErrorContent(error) {
66
146
  }
67
147
 
68
148
  // src/tools/judge.ts
149
+ function toJudgeFailureContent(error, toolName = "atbash_judge") {
150
+ return {
151
+ content: [
152
+ {
153
+ type: "text",
154
+ text: JSON.stringify(
155
+ {
156
+ verdict: "ERROR",
157
+ allow: false,
158
+ reason: `Atbash judge unavailable \u2014 treat this as BLOCK and do NOT proceed with the action (fail-closed). Retry ${toolName} once the service is reachable. Cause: ${safeErrorMessage(error)}`
159
+ },
160
+ null,
161
+ 2
162
+ )
163
+ }
164
+ ],
165
+ isError: true
166
+ };
167
+ }
168
+ function denyUnlessExplicitlyAllowed(decision) {
169
+ return decision.verdict === "ALLOW" && decision.allow === true ? decision : { ...decision, allow: false };
170
+ }
69
171
  function registerJudgeTools(server2, client2) {
70
172
  server2.registerTool(
71
173
  "atbash_judge",
@@ -88,11 +190,17 @@ function registerJudgeTools(server2, client2) {
88
190
  parsedArgs = tool_args_json;
89
191
  }
90
192
  }
91
- const decision = await client2.auditToolCall({
193
+ const rawDecision = await client2.auditToolCall({
92
194
  toolName: tool_name ?? "mcp_judge",
93
195
  args: parsedArgs ?? { action },
94
196
  context: `${action} \u2014 ${context}`
95
197
  });
198
+ const decision = denyUnlessExplicitlyAllowed(rawDecision);
199
+ if (decision.verdict === "ERROR") {
200
+ return toJudgeFailureContent(
201
+ new Error(decision.reason ?? "judge returned an error verdict")
202
+ );
203
+ }
96
204
  if (decision.verdict === "HOLD") {
97
205
  const holdParts = [
98
206
  "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.",
@@ -103,14 +211,14 @@ function registerJudgeTools(server2, client2) {
103
211
  }
104
212
  return toJsonContent(decision);
105
213
  } catch (error) {
106
- return toErrorContent(error);
214
+ return toJudgeFailureContent(error);
107
215
  }
108
216
  }
109
217
  );
110
218
  server2.registerTool(
111
219
  "atbash_log",
112
220
  {
113
- description: "Log a tool call on-chain without requesting a verdict.",
221
+ description: "Submit a tool call for Atbash safety judgment and logging. Returns ALLOW, HOLD, BLOCK, or ERROR.",
114
222
  inputSchema: z2.object({
115
223
  action: z2.string().describe("Action description"),
116
224
  context: z2.string().describe("Action context"),
@@ -128,22 +236,29 @@ function registerJudgeTools(server2, client2) {
128
236
  parsedArgs = tool_args_json;
129
237
  }
130
238
  }
131
- const result = await client2.auditToolCall({
239
+ const rawDecision = await client2.auditToolCall({
132
240
  toolName: tool_name ?? "mcp_log",
133
241
  args: parsedArgs ?? { action },
134
242
  context: `${action} \u2014 ${context}`
135
243
  });
136
- if (result.verdict === "HOLD") {
244
+ const decision = denyUnlessExplicitlyAllowed(rawDecision);
245
+ if (decision.verdict === "ERROR") {
246
+ return toJudgeFailureContent(
247
+ new Error(decision.reason ?? "judge returned an error verdict"),
248
+ "atbash_log"
249
+ );
250
+ }
251
+ if (decision.verdict === "HOLD") {
137
252
  const holdParts = [
138
253
  "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.",
139
- `Reason: ${result.reason ?? ""}`
254
+ `Reason: ${decision.reason ?? ""}`
140
255
  ];
141
- if (result.toolCallId) holdParts.push(`Tool Call ID: ${result.toolCallId}`);
142
- return toJsonContent({ ...result, allow: false, reason: holdParts.join("\n") });
256
+ if (decision.toolCallId) holdParts.push(`Tool Call ID: ${decision.toolCallId}`);
257
+ return toJsonContent({ ...decision, allow: false, reason: holdParts.join("\n") });
143
258
  }
144
- return toJsonContent(result);
259
+ return toJsonContent(decision);
145
260
  } catch (error) {
146
- return toErrorContent(error);
261
+ return toJudgeFailureContent(error, "atbash_log");
147
262
  }
148
263
  }
149
264
  );
@@ -379,21 +494,11 @@ function registerStatusTools(server2, client2) {
379
494
  }
380
495
 
381
496
  // src/index.ts
382
- var privkey = process.env.ATBASH_AGENT_PRIVKEY;
383
- if (!privkey) {
384
- console.error("ATBASH_AGENT_PRIVKEY env var is required");
385
- process.exit(1);
386
- }
387
- var endpoint = process.env.ATBASH_ENDPOINT;
388
- var orgName = process.env.ATBASH_ORG_NAME;
497
+ var client = createAtbashClient(process.env, Atbash);
389
498
  setupTelemetry({ enabled: true, source: "plugin:mcp" });
390
499
  process.once("beforeExit", () => shutdownTelemetry());
391
500
  process.once("SIGINT", () => shutdownTelemetry().finally(() => process.exit(0)));
392
501
  process.once("SIGTERM", () => shutdownTelemetry().finally(() => process.exit(0)));
393
- var clientOpts = {};
394
- if (endpoint) clientOpts.endpoint = endpoint;
395
- if (orgName) clientOpts.orgName = orgName;
396
- var client = new Atbash(privkey, clientOpts);
397
502
  var server = new McpServer({
398
503
  name: "atbash-safety",
399
504
  version: "1.0.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@atbash/atbash-mcp",
3
- "version": "0.0.10",
3
+ "version": "0.0.12-dev.0",
4
4
  "description": "Atbash safety judge exposed as a standalone MCP server",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -33,21 +33,30 @@
33
33
  "policy"
34
34
  ],
35
35
  "license": "SEE LICENSE IN LICENSE",
36
+ "repository": {
37
+ "type": "git",
38
+ "url": "https://github.com/Atbash-Ai/atbash-mcp-plugin"
39
+ },
36
40
  "scripts": {
37
- "build": "tsup src/index.ts --format esm --dts --clean",
41
+ "build": "node scripts/clean.mjs && tsup src/index.ts --format esm --dts --clean",
42
+ "typecheck": "tsc -p tsconfig.json --noEmit",
43
+ "test:compile": "tsc -p tsconfig.json --outDir dist --declaration false --sourceMap false",
44
+ "test": "vitest run && npm run test:compile && node --test tests/*.test.mjs",
38
45
  "example": "npm run build && npm install --prefix examples/mcp-runtime-agent && node examples/mcp-runtime-agent/client.mjs",
39
- "prepublishOnly": "npm run build",
40
- "publish:prod": "npm publish --tag latest --access public",
41
- "publish:dev": "node scripts/publish-dev.mjs"
46
+ "verify:package": "node scripts/release.mjs --verify-current",
47
+ "release": "node scripts/release.mjs --channel latest",
48
+ "release:dev": "node scripts/release.mjs --channel dev"
42
49
  },
43
50
  "dependencies": {
44
- "@atbash/sdk": "0.5.6",
51
+ "@atbash/sdk": "0.10.11-dev.0",
45
52
  "@modelcontextprotocol/sdk": "^1.12.3",
46
53
  "zod": "^3.25.76"
47
54
  },
48
55
  "devDependencies": {
49
56
  "@types/node": "^25.7.0",
57
+ "semver": "7.8.5",
50
58
  "tsup": "^8.0.0",
51
- "typescript": "^5.0.0"
59
+ "typescript": "^5.0.0",
60
+ "vitest": "^4.1.6"
52
61
  }
53
62
  }