@atbash/atbash-mcp 0.0.12-dev.0 → 0.0.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -56
- package/dist/index.js +23 -128
- package/package.json +3 -7
package/README.md
CHANGED
|
@@ -84,14 +84,7 @@ Returns: `{ verdict, allow, reason, tool_call_id }`
|
|
|
84
84
|
|
|
85
85
|
#### `atbash_log`
|
|
86
86
|
|
|
87
|
-
|
|
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.
|
|
87
|
+
Log a tool call on-chain without requesting a verdict.
|
|
95
88
|
|
|
96
89
|
| Input | Type | Required |
|
|
97
90
|
|---|---|---|
|
|
@@ -137,55 +130,10 @@ Poll the status of a previously submitted judgment.
|
|
|
137
130
|
|
|
138
131
|
| Verdict | Meaning | What To Do |
|
|
139
132
|
|---|---|---|
|
|
140
|
-
| `ALLOW` | Safe to proceed | Execute the real tool
|
|
141
|
-
| `HOLD` |
|
|
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 |
|
|
142
135
|
| `BLOCK` | Policy violation | Stop; show `reason` to operator |
|
|
143
|
-
| `ERROR` | Judge unreachable | Fail closed
|
|
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.
|
|
136
|
+
| `ERROR` | Judge unreachable | Fail closed |
|
|
189
137
|
|
|
190
138
|
## Sending Better Inputs
|
|
191
139
|
|
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
|
-
|
|
19
|
+
inputSchema: z.object({
|
|
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,86 +46,6 @@ 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
|
-
|
|
129
49
|
// src/tools/judge.ts
|
|
130
50
|
import { z as z2 } from "zod";
|
|
131
51
|
|
|
@@ -146,28 +66,6 @@ function toErrorContent(error) {
|
|
|
146
66
|
}
|
|
147
67
|
|
|
148
68
|
// 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
|
-
}
|
|
171
69
|
function registerJudgeTools(server2, client2) {
|
|
172
70
|
server2.registerTool(
|
|
173
71
|
"atbash_judge",
|
|
@@ -190,17 +88,11 @@ function registerJudgeTools(server2, client2) {
|
|
|
190
88
|
parsedArgs = tool_args_json;
|
|
191
89
|
}
|
|
192
90
|
}
|
|
193
|
-
const
|
|
91
|
+
const decision = await client2.auditToolCall({
|
|
194
92
|
toolName: tool_name ?? "mcp_judge",
|
|
195
93
|
args: parsedArgs ?? { action },
|
|
196
94
|
context: `${action} \u2014 ${context}`
|
|
197
95
|
});
|
|
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
|
-
}
|
|
204
96
|
if (decision.verdict === "HOLD") {
|
|
205
97
|
const holdParts = [
|
|
206
98
|
"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.",
|
|
@@ -211,14 +103,14 @@ function registerJudgeTools(server2, client2) {
|
|
|
211
103
|
}
|
|
212
104
|
return toJsonContent(decision);
|
|
213
105
|
} catch (error) {
|
|
214
|
-
return
|
|
106
|
+
return toErrorContent(error);
|
|
215
107
|
}
|
|
216
108
|
}
|
|
217
109
|
);
|
|
218
110
|
server2.registerTool(
|
|
219
111
|
"atbash_log",
|
|
220
112
|
{
|
|
221
|
-
description: "
|
|
113
|
+
description: "Log a tool call on-chain without requesting a verdict.",
|
|
222
114
|
inputSchema: z2.object({
|
|
223
115
|
action: z2.string().describe("Action description"),
|
|
224
116
|
context: z2.string().describe("Action context"),
|
|
@@ -236,29 +128,22 @@ function registerJudgeTools(server2, client2) {
|
|
|
236
128
|
parsedArgs = tool_args_json;
|
|
237
129
|
}
|
|
238
130
|
}
|
|
239
|
-
const
|
|
131
|
+
const result = await client2.auditToolCall({
|
|
240
132
|
toolName: tool_name ?? "mcp_log",
|
|
241
133
|
args: parsedArgs ?? { action },
|
|
242
134
|
context: `${action} \u2014 ${context}`
|
|
243
135
|
});
|
|
244
|
-
|
|
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") {
|
|
136
|
+
if (result.verdict === "HOLD") {
|
|
252
137
|
const holdParts = [
|
|
253
138
|
"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.",
|
|
254
|
-
`Reason: ${
|
|
139
|
+
`Reason: ${result.reason ?? ""}`
|
|
255
140
|
];
|
|
256
|
-
if (
|
|
257
|
-
return toJsonContent({ ...
|
|
141
|
+
if (result.toolCallId) holdParts.push(`Tool Call ID: ${result.toolCallId}`);
|
|
142
|
+
return toJsonContent({ ...result, allow: false, reason: holdParts.join("\n") });
|
|
258
143
|
}
|
|
259
|
-
return toJsonContent(
|
|
144
|
+
return toJsonContent(result);
|
|
260
145
|
} catch (error) {
|
|
261
|
-
return
|
|
146
|
+
return toErrorContent(error);
|
|
262
147
|
}
|
|
263
148
|
}
|
|
264
149
|
);
|
|
@@ -494,11 +379,21 @@ function registerStatusTools(server2, client2) {
|
|
|
494
379
|
}
|
|
495
380
|
|
|
496
381
|
// src/index.ts
|
|
497
|
-
var
|
|
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;
|
|
498
389
|
setupTelemetry({ enabled: true, source: "plugin:mcp" });
|
|
499
390
|
process.once("beforeExit", () => shutdownTelemetry());
|
|
500
391
|
process.once("SIGINT", () => shutdownTelemetry().finally(() => process.exit(0)));
|
|
501
392
|
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);
|
|
502
397
|
var server = new McpServer({
|
|
503
398
|
name: "atbash-safety",
|
|
504
399
|
version: "1.0.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@atbash/atbash-mcp",
|
|
3
|
-
"version": "0.0.12
|
|
3
|
+
"version": "0.0.12",
|
|
4
4
|
"description": "Atbash safety judge exposed as a standalone MCP server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -39,16 +39,13 @@
|
|
|
39
39
|
},
|
|
40
40
|
"scripts": {
|
|
41
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",
|
|
45
42
|
"example": "npm run build && npm install --prefix examples/mcp-runtime-agent && node examples/mcp-runtime-agent/client.mjs",
|
|
46
43
|
"verify:package": "node scripts/release.mjs --verify-current",
|
|
47
44
|
"release": "node scripts/release.mjs --channel latest",
|
|
48
45
|
"release:dev": "node scripts/release.mjs --channel dev"
|
|
49
46
|
},
|
|
50
47
|
"dependencies": {
|
|
51
|
-
"@atbash/sdk": "0.
|
|
48
|
+
"@atbash/sdk": "0.7.1",
|
|
52
49
|
"@modelcontextprotocol/sdk": "^1.12.3",
|
|
53
50
|
"zod": "^3.25.76"
|
|
54
51
|
},
|
|
@@ -56,7 +53,6 @@
|
|
|
56
53
|
"@types/node": "^25.7.0",
|
|
57
54
|
"semver": "7.8.5",
|
|
58
55
|
"tsup": "^8.0.0",
|
|
59
|
-
"typescript": "^5.0.0"
|
|
60
|
-
"vitest": "^4.1.6"
|
|
56
|
+
"typescript": "^5.0.0"
|
|
61
57
|
}
|
|
62
58
|
}
|