@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 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, SDK and zod bundled in); no runtime dependencies.
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` (an `initialize` opens a session and returns `Mcp-Session-Id`; every later
41
- request must send it), `GET /mcp` (standalone SSE stream, one per session), `DELETE /mcp` (close the
42
- session), `GET /health` `{ ok, name, version }` (unauthenticated). Anything else is `404` JSON.
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
- - Sessions idle for 30 minutes are evicted; each session has its own server instance (roots and guard
49
- settings are shared). The transport lives in `dist/http-lazy.js`, loaded only with `--http`, and is
50
- plain `node:http` — no express/hono at runtime.
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>] [--strict] [--log-level warn]
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`, and runs **only** the fast predicates: containment + `RelPath` rules
157
- (`..`, `CON`, NTFS ADS `ERR_CONTAINMENT` / `ERR_PATH_*`), `path.deny`, `path.allow`,
158
- `content.noSecrets` and `content.maxBytes` on the new content when the payload carries it.
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 / unknown tool | `0` | — | — |
163
- | deny | `2` | `{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":""}}` | `AXIOM GATE DENY <code>: <reason> (<relpath>)` |
164
- | malformed payload, stdin timeout (2 s), internal error | `0` (**fail open**) | | `AXIOM GATE WARN: …` |
165
- | same, with `--strict` | `2` | deny JSON | `AXIOM GATE DENY ERR_INTERNAL: …` |
166
-
167
- Root = payload `cwd`, else `--root`, else the process cwd (the hook is the one place where cwd is
168
- acceptable: the harness spawns the hook in the project directory and owns that value).
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 { f as PlanSchema, x as isErrorCode } from "./dist-FFqnyzCe.js";
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-CR-FmFVd.js.map
6284
+ //# sourceMappingURL=axm-lazy-LAnrfjUO.js.map