@codai/axiom-mcp 2.1.0 → 2.2.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.
- package/README.md +56 -24
- package/dist/{axm-lazy-CR-FmFVd.js → axm-lazy-LAnrfjUO.js} +32 -3
- package/dist/axm-lazy-Xin1nh7Q.js +6284 -0
- package/dist/axm-lazy.js +119 -9
- package/dist/cli-main.js +1650 -14959
- package/dist/dist-Bt9Wvd_P.js +6379 -0
- package/dist/{dist-FFqnyzCe.js → dist-D4M2Lyji.js} +264 -27
- package/dist/gate-lazy.js +581 -46
- package/dist/gc-lazy.js +6885 -0
- package/dist/http-lazy.js +18262 -9889
- package/dist/index.d.ts +140 -4
- package/dist/index.js +575 -41
- package/dist/mcp-lazy.js +23050 -0
- package/dist/migrate-lazy.js +48 -7
- package/dist/verify-tree-lazy.js +6893 -0
- package/package.json +14 -9
- package/spec/codai-tools.json +283 -0
- package/spec/tools.json +1487 -160
package/README.md
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
MCP server (stdio or Streamable HTTP) and CLI for AXIOM v2 — the transactional write gate for coding agents:
|
|
4
4
|
`Plan` → canonical `ManifestBundle` → set-level checks → hash-gated two-phase `apply` → journal.
|
|
5
|
-
`dist/cli.js` (thin entry) + `dist/cli-main.js` (lazy-loaded engines
|
|
5
|
+
`dist/cli.js` (thin entry) + `dist/cli-main.js` (lazy-loaded engines; the MCP SDK v2 and zod are
|
|
6
|
+
bundled into the `mcp-lazy` / `http-lazy` chunks); no runtime dependencies.
|
|
6
7
|
|
|
7
8
|
## Install & run
|
|
8
9
|
|
|
@@ -15,6 +16,16 @@ npx @codai/axiom-mcp --help # CLI verbs
|
|
|
15
16
|
`--root` may repeat. Every tool `root` argument must equal or lie inside one of them; with exactly one
|
|
16
17
|
root it is the default. There is **no** env-var or `cwd` fallback (`ERR_ROOT_REQUIRED` / `ERR_ROOT_NOT_ALLOWED`).
|
|
17
18
|
|
|
19
|
+
### Protocol revisions — `--wire 2026|2025|2026-only`
|
|
20
|
+
|
|
21
|
+
Built on MCP TypeScript SDK **v2** (`@modelcontextprotocol/server` 2.0.0). The server speaks the
|
|
22
|
+
**2026-07-28** revision (no `initialize`, per-request `_meta` envelope, `server/discover`,
|
|
23
|
+
`ttlMs`/`cacheScope` on list results) **and** the 2025-era revisions (`initialize` handshake,
|
|
24
|
+
HTTP sessions) from the same entry — the SDK pins each stdio connection, or routes each HTTP
|
|
25
|
+
request, to the era the client opened with. `--wire 2026` (default) and `--wire 2025` both serve
|
|
26
|
+
both; `--wire 2026-only` refuses 2025 openings with the unsupported-protocol-version error. Clients
|
|
27
|
+
on SDK v1 need no change. Details: [docs/mcp_api.md](../../docs/mcp_api.md#protocol-revisions---wire-d-19).
|
|
28
|
+
|
|
18
29
|
### VS Code — `.vscode/mcp.json`
|
|
19
30
|
|
|
20
31
|
```json
|
|
@@ -37,17 +48,19 @@ axiom mcp --root /abs/repo --http 0 # random port; URL
|
|
|
37
48
|
AXIOM_HTTP_TOKEN=$(openssl rand -hex 32) axiom mcp --root /abs/repo --http 0.0.0.0:3411 --log-level info
|
|
38
49
|
```
|
|
39
50
|
|
|
40
|
-
- Endpoints: `POST /mcp` (
|
|
41
|
-
|
|
42
|
-
|
|
51
|
+
- Endpoints: `POST /mcp` (2026-07-28 requests are served statelessly; a 2025 `initialize` opens a
|
|
52
|
+
session and returns `Mcp-Session-Id`, which every later 2025 request must send), `GET /mcp`
|
|
53
|
+
(standalone SSE stream, one per 2025 session), `DELETE /mcp` (close the session), `GET /health` →
|
|
54
|
+
`{ ok, name, version }` (unauthenticated). Anything else is `404` JSON.
|
|
43
55
|
- **Loopback by default.** A non-loopback host **refuses to start** unless a bearer token is present in
|
|
44
56
|
the env var named by `--http-token-env <NAME>` (default `AXIOM_HTTP_TOKEN`, ≥ 16 chars). Clients send
|
|
45
57
|
`Authorization: Bearer <token>`; the compare is constant-time. A token is optional on loopback.
|
|
46
58
|
- DNS-rebinding protection is on for loopback binds (`Host` must be `<host>:<port>`, `localhost:<port>`
|
|
47
59
|
or `127.0.0.1:<port>`; otherwise `403`). Request bodies over 4 MiB are `413`.
|
|
48
|
-
-
|
|
49
|
-
|
|
50
|
-
plain `node:http` — no
|
|
60
|
+
- 2025 sessions idle for 30 minutes are evicted; each session (and each 2026 request) has its own
|
|
61
|
+
server instance from one factory (roots, discovered sub-roots and guard settings are shared). The
|
|
62
|
+
transport lives in `dist/http-lazy.js`, loaded only with `--http`, and is plain `node:http` — no
|
|
63
|
+
express/hono at runtime.
|
|
51
64
|
- VS Code: `{ "type": "http", "url": "http://127.0.0.1:3411/mcp" }`; add
|
|
52
65
|
`"headers": { "Authorization": "Bearer ${input:axiom-token}" }` when a token is set.
|
|
53
66
|
|
|
@@ -70,8 +83,14 @@ in CI with an expected-failures baseline (`packages/conformance/baseline.yml`).
|
|
|
70
83
|
|---|---|---|---|
|
|
71
84
|
| `axiom_plan_validate` | READ | `{ plan }` | `{ ok, planDigest?, errors[] }` |
|
|
72
85
|
| `axiom_plan_compile` | ACT | `{ plan, store?: inline\|cas, root? }` | `ManifestBundle` (writes only under `<root>/.axiom/` — CAS blobs and the stored manifest — when a root is given) |
|
|
73
|
-
| `axiom_manifest_verify` | READ | `{ bundle, root? }` | `{ ok, manifestDigest, canonical, signed, missing[], errors[], signatures?: { trustFile, keyids[], findings[], ok } }` — `signatures` only when `root` has `.axiom/trust/keys.json` |
|
|
74
|
-
| `axiom_check` | READ | `{ bundle, profile?, root? }` | `CheckReport` (`verdict: pass\|fail\|error`) |
|
|
86
|
+
| `axiom_manifest_verify` | READ | `{ bundle, root? }` | `{ ok, manifestDigest, canonical, signed, missing[], errors[], signatures?: { trustFile, keyids[], findings[], ok, code? } }` — `signatures` only when `root` has `.axiom/trust/keys.json`; `code` is `ERR_SIGNATURE_MISSING` \| `ERR_SIGNATURE_INVALID` when not `ok` |
|
|
87
|
+
| `axiom_check` | READ | `{ bundle, profile?, root? }` | `CheckReport` (`verdict: pass\|fail\|error`, `preImage: verified\|drifted\|unverified`) |
|
|
88
|
+
| `axiom_check_start` | READ | `{ bundle, profile?, root? }` | `{ taskId, tool, status: "working", pollIntervalMs, ttlMs, elapsedMs }` — same evaluation as `axiom_check`, returned immediately as a **task** so long `guard.external` suites (up to 15 min per guard) outlive the client's per-call timeout (S-406 / D-24) |
|
|
89
|
+
| `axiom_task_get` | READ | `{ taskId }` | descriptor + `result: CheckReport` once `completed`, or `error: { code, message }` once `failed`/`cancelled`; unknown/expired id → `ERR_TASK_NOT_FOUND` |
|
|
90
|
+
| `axiom_task_cancel` | ACT | `{ taskId }` | descriptor; kills every running guard tree, task ends `cancelled` with `ERR_TASK_CANCELLED` (idempotent on terminal tasks) |
|
|
91
|
+
| `axiom_plan_begin` | ACT | Plan header: `{ name, intent, profile?, capabilities?, checks?, counter?, metadata? }` | `{ sessionId, artifacts: 0, bytes: 0, limits: { maxArtifacts, maxBytes }, ttlMs }` — opens a **chunked plan session** for Plans whose JSON would exceed the 4 MiB call cap |
|
|
92
|
+
| `axiom_plan_add` | ACT | `{ sessionId, artifacts[] }` (each call ≤ 4 MiB) | session descriptor; duplicate path across chunks → `ERR_INVALID_PLAN`, over budget (2000 artifacts / 64 MiB) or sealed → `ERR_PLAN_SESSION_STATE` |
|
|
93
|
+
| `axiom_plan_seal` | ACT | `{ sessionId, store?: inline\|cas, root? }` | `ManifestBundle` — compiled by the same code path as `axiom_plan_compile`, so the digest equals a one-shot compile of the assembled Plan (property-tested); the session is consumed |
|
|
75
94
|
| `axiom_apply_dry_run` | READ | `{ bundle, root, profile? }` | `ApplyResult{mode:"dry-run", diff}` |
|
|
76
95
|
| `axiom_apply` | SENSITIVE | `{ bundle, root, profile?, confirmDigest }` | `ApplyResult` |
|
|
77
96
|
| `axiom_rollback` | SENSITIVE | `{ root, manifestDigest }` | `{ status:"rolled-back", phase, steps }` |
|
|
@@ -100,6 +119,10 @@ Resources: `axiom://manifest/{sha}`, `axiom://report/{sha}`, `axiom://applied/{s
|
|
|
100
119
|
Pre-apply checks run against the profile (default `default`, or `<root>/.axiom/profiles/<name>.json`);
|
|
101
120
|
a non-`pass` verdict aborts with `ERR_CHECKS_FAILED` before any write.
|
|
102
121
|
- Payloads over 4 MiB are rejected up front (`ERR_BUNDLE_TOO_LARGE`).
|
|
122
|
+
- Tasks and plan sessions live in the server **process** (shared by every connection/request the
|
|
123
|
+
process serves; never on disk). A restart forgets them; finished tasks stay pollable for 10 min, idle
|
|
124
|
+
sessions expire after 30 min; at most 8 tasks run concurrently (`ERR_EBUSY` beyond that). Stopping
|
|
125
|
+
the server aborts every running task and kills its guard trees.
|
|
103
126
|
- `.axiom/lock` makes apply single-writer per root; the journal makes it crash-safe and reversible.
|
|
104
127
|
- stdout carries only JSON-RPC. Logs are JSON lines on stderr (`--log-level error|warn|info|debug`, default `warn`).
|
|
105
128
|
- External guards (`guard.external`) are **off** unless the process is started with `--allow-guards`
|
|
@@ -120,6 +143,7 @@ axiom mcp [--root <abs>]... [--allow-guards] [--guard-allowlist <abs>]... [-
|
|
|
120
143
|
[--http <host:port>] [--http-token-env AXIOM_HTTP_TOKEN]
|
|
121
144
|
axiom compile <plan.json> [-o out.json] [--store cas --root .] [--allow-net [--net-allow host[,host]]] [--allow-file]
|
|
122
145
|
axiom verify <bundle.json> [--root .] (--root: also verify signatures against .axiom/trust/keys.json)
|
|
146
|
+
axiom verify <bundle.json> --tree <root> [--pre] [--attest out.intoto.json] (tree matches manifest? docs/verify-tree.md)
|
|
123
147
|
axiom check <bundle.json> --root . [--profile p] [--json] [--allow-guards] [--guard-allowlist <abs>]...
|
|
124
148
|
axiom apply <bundle.json> --root . [--dry-run] [--profile p] [--confirm <digest>] [--allow-guards] [--guard-allowlist <abs>]...
|
|
125
149
|
axiom rollback <digest> --root .
|
|
@@ -128,9 +152,9 @@ axiom diff <a.json> <b.json>
|
|
|
128
152
|
axiom schema <Plan|Manifest|ManifestBundle|CheckReport|ApplyResult|Profile|Journal|RepoSnapshot>
|
|
129
153
|
axiom emitters [--json]
|
|
130
154
|
axiom keygen [--out <dir>] [--name <label>] (ed25519; private key → <dir>/axiom-signing-<id>.key 0600, public entry → stdout)
|
|
131
|
-
axiom sign <bundle.json> [--key-file <path>] [-o out.json] (key from --key-file or $AXIOM_SIGNING_KEY)
|
|
132
|
-
axiom trust add <pub.json> --root . | remove <keyid> --root . | list --root .
|
|
133
|
-
axiom gate --stdin [--root <dir>] [--profile <file>] [--
|
|
155
|
+
axiom sign <bundle.json> [--key-file <path>] [-o out.json] [--root-id <id>] (key from --key-file or $AXIOM_SIGNING_KEY; --root-id = root-bound envelope)
|
|
156
|
+
axiom trust add <pub.json> --root . | remove <keyid> --root . | list --root . | root-id [<id>|--clear] --root .
|
|
157
|
+
axiom gate --stdin [--root <dir>] [--profile <file>] [--fail-open] [--no-shell-scan] [--no-root-discovery] [--log-level warn]
|
|
134
158
|
axiom migrate v1 <manifest.json> [-o plan.json] [--profile default] [--cas <root>] [--content <dir>] [--overwrite]
|
|
135
159
|
(v1 manifest → v2 Plan, lazy chunk; exit 1 = migrated with warnings — docs/migrate.md)
|
|
136
160
|
axiom snapshot --root . [-o snap.json] [--include <glob>]... [--exclude <glob>]... [--max-files n] [--max-bytes n] [--no-gitignore] [--no-digest]
|
|
@@ -153,22 +177,30 @@ A PreToolUse hook for Claude Code, Copilot CLI and VS Code agent hooks. It reads
|
|
|
153
177
|
payload from stdin (both `{tool_name, tool_input, cwd}` and `{toolName, toolArgs, cwd}` casings;
|
|
154
178
|
`toolArgs` may be a JSON string), extracts the write target(s) of `Write|Edit|MultiEdit|NotebookEdit`,
|
|
155
179
|
`create_file|replace_string_in_file|insert_edit_into_file|apply_patch|multi_replace_string_in_file|edit_notebook_file`
|
|
156
|
-
and generic `write|edit`,
|
|
157
|
-
(
|
|
158
|
-
`
|
|
180
|
+
and generic `write|edit`, scans **shell** tools (`Bash`, `run_in_terminal`, …) for write
|
|
181
|
+
primitives (`>`, `>>`, `tee`, `rm`, `mv`, `cp`, `sed -i`, `git checkout|reset|clean`, PowerShell
|
|
182
|
+
`Set-Content`/`Remove-Item`, … — a heuristic, documented in docs/hooks.md), and runs **only** the
|
|
183
|
+
fast predicates: containment + `RelPath` rules (`..`, `CON`, NTFS ADS → `ERR_CONTAINMENT` /
|
|
184
|
+
`ERR_PATH_*`), `path.deny`, `path.allow`, `content.noSecrets` and `content.maxBytes` on the new
|
|
185
|
+
content when the payload carries it. **Fail-closed** (D-18): a non-answer is a deny.
|
|
159
186
|
|
|
160
187
|
| outcome | exit | stdout | stderr |
|
|
161
188
|
|---|---|---|---|
|
|
162
|
-
| allow /
|
|
163
|
-
| deny | `2` | `
|
|
164
|
-
|
|
|
165
|
-
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
| allow / non-write, non-shell tool | `0` | — | — |
|
|
190
|
+
| deny | `2` | one object: `hookSpecificOutput{…}` (Claude) + flat `permissionDecision`/`permissionDecisionReason` (Copilot) + `axiom{verdict, code, path, toolClass, standard:"owasp-acs/0.1"}` | `AXIOM GATE DENY <code>: <reason> (<relpath>)` |
|
|
191
|
+
| write tool with no recognised path key | `2` | deny JSON | `AXIOM GATE DENY ERR_UNSUPPORTED_OP: …` |
|
|
192
|
+
| malformed payload, stdin timeout (2 s), internal error | `2` (**fail closed**) | deny JSON | `AXIOM GATE DENY ERR_INTERNAL: … (fail-closed; pass --fail-open to allow)` |
|
|
193
|
+
| same, with `--fail-open` | `0` | — | `AXIOM GATE WARN: … — failing open (--fail-open)` |
|
|
194
|
+
|
|
195
|
+
Root = payload `cwd`, walked up to the nearest `.git` / repository `.axiom/` ancestor (never the
|
|
196
|
+
home dir; `--no-root-discovery` disables), else `--root`, else the process cwd (the hook is the
|
|
197
|
+
one place where cwd is acceptable: the harness spawns the hook in the project directory and owns
|
|
198
|
+
that value). Relative targets stay relative to `cwd`.
|
|
169
199
|
Profile = `--profile <file>` → `<root>/.axiom/gate-profile.json` → `~/.axiom/gate-profile.json` →
|
|
170
|
-
built-in `{ deny: [".git/**", ".axiom/**", "**/*.lock", "pnpm-lock.yaml", ".env", ".env.*", "**/node_modules/**"], noSecrets: true }`.
|
|
171
|
-
Schema: `{ deny: string[], allow?: string[], noSecrets: boolean, maxBytes?: number }` (strict).
|
|
200
|
+
built-in `{ deny: [".git/**", ".axiom/**", "**/*.lock", "pnpm-lock.yaml", ".env", ".env.*", "**/node_modules/**"], noSecrets: true, pii: false }`.
|
|
201
|
+
Schema: `{ deny: string[], allow?: string[], noSecrets: boolean, pii: boolean, maxBytes?: number }` (strict).
|
|
202
|
+
`pii: true` additionally scans for personal data (`cnp`, `email`, `phoneRo`, `card`) — off by default
|
|
203
|
+
since S-414 (maintainer e-mails in `pyproject.toml` denied real edits; see `docs/checks.md`).
|
|
172
204
|
|
|
173
205
|
`gate` is a separate lazy chunk (`dist/gate-lazy.js`, no MCP SDK): in-process p95 ≈ 5 ms per payload,
|
|
174
206
|
end-to-end ≈ 150–200 ms including node startup; `check-gate-latency` guards p95 ≤ 250 ms.
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { C as isErrorCode, m as PlanSchema } from "./dist-Bt9Wvd_P.js";
|
|
2
2
|
function PRINT_ERROR(msg) {
|
|
3
3
|
/* istanbul ignore else - can't override global.console in node.js */
|
|
4
4
|
if (console && console.error) console.error(`Error: ${msg}`);
|
|
@@ -5584,6 +5584,7 @@ const ModeKw = keyword("ModeKw", "mode");
|
|
|
5584
5584
|
const OpKw = keyword("OpKw", "op");
|
|
5585
5585
|
const Inline = keyword("Inline", "inline");
|
|
5586
5586
|
const Template = keyword("Template", "template");
|
|
5587
|
+
const Patch = keyword("Patch", "patch");
|
|
5587
5588
|
const Cas = keyword("Cas", "cas");
|
|
5588
5589
|
const Ref = keyword("Ref", "ref");
|
|
5589
5590
|
const Check = keyword("Check", "check");
|
|
@@ -5746,6 +5747,7 @@ const allTokens = [
|
|
|
5746
5747
|
OpKw,
|
|
5747
5748
|
Inline,
|
|
5748
5749
|
Template,
|
|
5750
|
+
Patch,
|
|
5749
5751
|
Cas,
|
|
5750
5752
|
Ref,
|
|
5751
5753
|
Check,
|
|
@@ -6015,6 +6017,26 @@ var Compiler = class {
|
|
|
6015
6017
|
params: params ?? {}
|
|
6016
6018
|
};
|
|
6017
6019
|
}
|
|
6020
|
+
const patch = first(nodes(ctx, "patchSource"));
|
|
6021
|
+
if (patch !== void 0) {
|
|
6022
|
+
const pc = patch.children;
|
|
6023
|
+
const f = first(tokens(pc, "format"));
|
|
6024
|
+
const d = first(tokens(pc, "preImage"));
|
|
6025
|
+
const ab = first(tokens(pc, "absent"));
|
|
6026
|
+
const h = first(tokens(pc, "body"));
|
|
6027
|
+
this.at(`${path}.format`, tokenRange(f));
|
|
6028
|
+
this.at(`${path}.preImage`, tokenRange(d ?? ab ?? f));
|
|
6029
|
+
this.at(`${path}.body`, tokenRange(h));
|
|
6030
|
+
if (ab !== void 0 && ab.image !== "absent") this.error("patch pre-image must be a \"sha256:…\" digest or the word absent", tokenRange(ab));
|
|
6031
|
+
const payload = h?.payload;
|
|
6032
|
+
const body = payload === void 0 ? "" : `${payload.content}\n`;
|
|
6033
|
+
return {
|
|
6034
|
+
type: "patch",
|
|
6035
|
+
format: f?.image ?? "",
|
|
6036
|
+
preImage: d !== void 0 ? decodeString(d.image) : "absent",
|
|
6037
|
+
body
|
|
6038
|
+
};
|
|
6039
|
+
}
|
|
6018
6040
|
}
|
|
6019
6041
|
check(n, path) {
|
|
6020
6042
|
const ctx = n.children;
|
|
@@ -6155,9 +6177,16 @@ var AxmParser = class extends CstParser {
|
|
|
6155
6177
|
{ ALT: () => this.SUBRULE(this.inlineSource) },
|
|
6156
6178
|
{ ALT: () => this.SUBRULE(this.templateSource) },
|
|
6157
6179
|
{ ALT: () => this.SUBRULE(this.casSource) },
|
|
6158
|
-
{ ALT: () => this.SUBRULE(this.refSource) }
|
|
6180
|
+
{ ALT: () => this.SUBRULE(this.refSource) },
|
|
6181
|
+
{ ALT: () => this.SUBRULE(this.patchSource) }
|
|
6159
6182
|
]);
|
|
6160
6183
|
});
|
|
6184
|
+
patchSource = this.RULE("patchSource", () => {
|
|
6185
|
+
this.CONSUME(Patch);
|
|
6186
|
+
this.CONSUME(IdentLike, { LABEL: "format" });
|
|
6187
|
+
this.OR([{ ALT: () => this.CONSUME(Digest, { LABEL: "preImage" }) }, { ALT: () => this.CONSUME2(IdentLike, { LABEL: "absent" }) }]);
|
|
6188
|
+
this.CONSUME(HereDoc, { LABEL: "body" });
|
|
6189
|
+
});
|
|
6161
6190
|
inlineSource = this.RULE("inlineSource", () => {
|
|
6162
6191
|
this.CONSUME(Inline);
|
|
6163
6192
|
this.CONSUME(HereDoc, { LABEL: "content" });
|
|
@@ -6252,4 +6281,4 @@ function parseAxm(source) {
|
|
|
6252
6281
|
}
|
|
6253
6282
|
export { parseAxm };
|
|
6254
6283
|
|
|
6255
|
-
//# sourceMappingURL=axm-lazy-
|
|
6284
|
+
//# sourceMappingURL=axm-lazy-LAnrfjUO.js.map
|