Your MCP gateway can't see bash. GAX governs both.
Quickstart · Plan · Narrative · Envelope spec · Research · Evaluation
- What is GAX?
- The gap GAX fills
- How it enforces
- About the token argument
- Architecture
- How it works
- Evaluation
- Adapters
- Installation
- How to use
- Protocol & envelope
- Repository structure
- Research & benchmarks
- Development
- Roadmap
- License
GAX (Governed Agent eXecution) is a governed execution layer for agent shell commands. Agents get a command-line-shaped surface (gax gh.pr.list --repo org/api); OAuth, policy, audit, and tenancy live in a sidecar the model never sees.
The enforcement guarantee: an agent can only name commands that already exist in the registry, every invoke carries a capability token checked before any adapter runs, and every invoke produces an audit_id.
GAX complements MCP rather than competing with it. MCP servers become adapters behind stable GAX command names, so one policy file, one capability model, and one audit trail cover both your MCP calls and your shell calls.
| Component | Path | Description |
|---|---|---|
| Reference implementation | gax/ |
Python package: gax CLI + gaxd daemon (v0.4) |
| Envelope spec | docs/acsp/ |
Envelope v1, discovery, conformance tests |
| Research hub | research/ |
MCP vs CLI analysis, diagrams, comparisons |
| Evaluation harness | eval/ |
Reproducible CLI / MCP / GAX measurements |
| 2026 H2 plan | docs/PLAN-2026H2.md |
Repositioning + eval-integrity work |
MCP won the tool-calling standards war — donated to the Linux Foundation's Agentic AI Foundation in Dec 2025, 97M downloads, 6,400+ registered servers. A funded gateway category (Docker, Cloudflare, Kong, Bifrost, Lunar, MintMCP, TrueFoundry) now ships policy enforcement, audit, and OAuth for MCP traffic.
Three things that stack does not cover:
1. The shell is ungoverned. Every gateway above governs MCP traffic. But agents act overwhelmingly through bash — Claude Code's own architecture is file access + bash + MCP, and the dominant setup pattern mixes shell, skills, hooks, and MCP servers. An MCP gateway sees none of the shell half. NVIDIA OpenShell (GTC 2026) validates the problem but operates at the syscall layer, with no notion of which registered command, under what capability, producing what audit record.
2. Governance is the acknowledged gap in the standard. The MCP 2026 roadmap names enterprise governance, audit trails, and SSO-integrated auth as priorities it does not yet fully address. The ecosystem outgrew its security model: 30+ CVEs in Jan–Feb 2026, including Asana's cross-tenant data leak, Smithery's path traversal exposing 3,243 apps, and tool-poisoning attacks.
3. Tool poisoning has no purchase here. Because the model can only invoke commands that already exist in gax/manifests/, a poisoned tool description cannot introduce a new action. The action surface is fixed at deploy time, not negotiated at runtime.
| MCP gateway | Shell sandbox | GAX | |
|---|---|---|---|
| Governs MCP calls | ✅ | ❌ | ✅ |
| Governs shell commands | ❌ | ✅ registered commands | |
| Capability checked pre-invoke | ✅ | ❌ | ✅ |
| Danger ceiling per credential | ❌ | ❌ | ✅ read/write/destructive |
| One audit trail across both | ❌ | ❌ | ✅ |
| Detects a tool changing after approval | ❌ | n/a | ✅ pinned |
GAX splits three planes:
| Plane | Visible to the model? | Responsibility |
|---|---|---|
| Invocation | Yes | Short commands: gax <command> [args] |
| Control | No | OAuth, vault, policy, capability mint/revoke |
| Data | Filtered | Envelope v1 JSON; surface=model truncates for the LLM |
Trust boundary: the model proposes which registered command + args; gaxd enforces before any adapter runs (executor.py).
Five invariants:
- No arbitrary shell — only registered commands (policy + allowlists). This is the core guarantee.
- Capability per invoke — JWT or macaroon (
GAX_CAP/GAX-Capabilityheader); fail closed, with a danger ceiling (--max-side-effect read|write|destructive) so an allowlist edit cannot silently promote a read-only token into one that deletes things - Uniform envelope — every response:
ok,cmd,audit_id,data,meta, optionalnext— so errors and audit correlation are the same shape across shell, MCP, and HTTP backends - Lazy discovery —
gax search/gax doc/gax schema, never the full registry in context - Composable plans —
gax plan run workflow.yaml(sequential + parallel), one envelope out
Fail-closed matters because the emerging enterprise best practice is "if the audit write fails, the tool call should fail." Capability checks, scope checks, and policy all run before the adapter — see the governance receipts in SAMPLE_RUN (policy_denied, scope_mismatch, expired_cap, each with a correlated audit_id).
export GAX_K8S_MOCK=1 # no cluster needed
export GAX_CAP="$(gax auth cap-mint --command k8s.pod.delete \
--scope k8s:pods:write --max-side-effect read --raw)"
gax k8s.pod.delete --namespace prod --pod web-1{
"ok": false,
"error": {
"kind": "policy_denied",
"message": "side_effects 'destructive' exceeds capability ceiling 'read': k8s.pod.delete"
},
"audit_id": "aud_cef808f0fadd41e7"
}The command was on that token's allowlist. It was refused anyway, before kubectl was
ever spawned, because the token's danger ceiling is read — two independent things must
be wrong before something gets deleted. The denial is audited with its arguments.
Covered by 26 adversarial tests that attack the check (expired token + destructive, scope mismatch, allowlisted-but-over-ceiling, wildcard capability, shell metacharacters in pod names) rather than confirm the happy path. Full walkthrough: QUICKSTART.
Earlier versions of this README led with token economics. That argument has largely expired, and we'd rather say so than have you discover it.
When GAX started, naive MCP setups injected 44k+ tokens of tool schemas per session, and lazy discovery was a real differentiator. In 2026 the model vendors shipped the fix themselves:
- Anthropic's Tool Search Tool (
defer_loading: true) — ~85% token reduction, plus measured accuracy gains (Opus 4.5: 79.5% → 88.1% on MCP evals). OpenAI shipped defer-loading too. - Code Mode — up to 92.8% lower input tokens at 500+ tools, 100% pass rate.
GAX's lazy discovery is now a platform feature, not a differentiator. We don't claim a token advantage over a well-configured modern MCP setup. On like-for-like tasks our own harness measures GAX at roughly 3.5× the tokens of raw CLI — governance is not free, and we'd rather publish that number than a flattering one.
What survives is the enforcement layer: registered commands, capability-per-invoke, one audit trail spanning shell and MCP. Those are orthogonal to how tool schemas get loaded.
┌──────────────┐ short commands ┌──────────────┐
│ LLM / Agent │ ───────────────────────▶│ gax CLI │
└──────────────┘ └──────┬───────┘
│ HTTP + GAX-Capability
▼
┌──────────────┐
│ gaxd │
│ (sidecar) │
├──────────────┤
│ Registry │
│ Policy OPA │
│ Projection │
│ Audit / OTEL │
└──────┬───────┘
┌──────────────────────────┼──────────────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ exec │ │ mcp │ │ http │
│ (gh, …) │ │ bridge │ │ OpenAPI │
└──────────┘ └──────────┘ └──────────┘
Invocation flow: research/diagrams/png/sequence-invoke.png · Full architecture doc
| Component | Role |
|---|---|
gax |
Client CLI; discovery, auth, invoke, plans |
gaxd |
HTTP sidecar on 127.0.0.1:9477 (default) |
manifests/*.yaml |
Command registry: adapter, scopes, schemas |
~/.gax/ |
Config, OAuth tokens, audit log, vault |
- Register commands in
gax/manifests/(YAML) or generate from OpenAPI:gax openapi generate spec.json - Start sidecar:
gaxd start - Authenticate:
gax auth login(OAuth device flow) orgax auth cap-mint(dev JWT/macaroon) - Mint capability:
export GAX_CAP="$(gax auth cap-from-oauth --export | sed 's/export GAX_CAP=//')" - Discover (low tokens):
gax search "pull requests"→gax doc gh.pr.list - Invoke:
gax gh.pr.list --repo octocat/Hello-World --surface model - Audit: every invoke gets
audit_idin~/.gax/audit.jsonl(+ optional OTEL export)
Example envelope (model surface):
{
"v": 1,
"ok": true,
"cmd": "gh.pr.list@1.0.0",
"audit_id": "aud_0b20bea710fe48fc",
"surface": "model",
"schema": "https://schemas.gax.dev/gh/pr.list/v1",
"data": { "items": [ { "number": 42, "title": "…", "state": "OPEN" } ] },
"meta": { "truncated": true, "row_count": 10, "duration_ms": 355 },
"next": [
{
"cmd": "gh.pr.view",
"args": { "repo": "octocat/Hello-World", "number": 42 },
"reason": "inspect first PR in list"
}
]
}Reproducible harness: 18 tasks (happy path, errors, policy denial, truncation, multi-turn, plan failure, MCP bridge). Token counts use tiktoken (cl100k_base), not hardcoded estimates.
Bias disclosure: GAX is our implementation. We report separate metrics — no team-chosen weighted composite. See eval/METHODOLOGY.md.
Known defects, being fixed (tracked in the plan) — we found these in our own review and are publishing them before the fix lands:
| # | Defect | Status |
|---|---|---|
| W1 | cli median was over 7 tasks, gax over 15; only 6 overlap — published ratio 1.3× understated GAX cost |
Fixed — paired comparison; true ratio ~3.5× |
| W2 | Expected-failure rows rewritten to ok=True (32/150) made every modality report success_rate: 1.0 |
Fixed — split into completion / expected_outcome / fail_closed |
| W3 | Mock MCP (1 tool) tabled beside live MCP (26 tools) | Queued |
| W4 | mcp_naive_43 is a borrowed constant + arithmetic, not a measurement |
Queued |
| Modality | What it measures | Derivation |
|---|---|---|
cli |
Shell command + stdout in agent transcript | measured |
gax |
gax doc stub + envelope v1 |
measured |
gax_mcp_bridge |
Envelope over MCP tool (schema not in prompt) | measured |
mcp_live |
Real tools/list size (--live-mcp) |
measured |
mcp_naive_43 |
Same work + ~44k schema tax | modeled from Scalekit fixture |
Paired comparison (the 6 tasks where both cli and gax produce a real row) — this is the honest like-for-like number:
| median tokens | |
|---|---|
| cli | 80 |
| gax | 250 |
| median ratio | ~3.5× (range 1.3×–5.9×) |
Per-task ratios and the excluded-task list are in eval/results/comparison.md. Live gh calls vary run to run, so expect ~3–3.5×; every paired task costs GAX more than CLI.
Governance properties are by design, verified by test — not experimental outcomes: cli emits no audit_id (0%) and gax emits one on every invoke (100%) because that is what each architecture is.
Details: eval/results/comparison.md · Extended (ablations + MCP catalog): docs/ABLATIONS.md · Case study: eval/case_study/README.md
examples/agent_pr_triage.py runs an actual LLM with only gax_search / gax_doc / gax_invoke — no hardcoded tool catalog. The agent discovers commands at runtime, lists and inspects a live PR on octocat/Hello-World, summarizes review risk, and posts a draft comment via demo.echo. A deterministic governance block (policy deny, scope mismatch, expired capability) runs first; every invoke gets an audit_id verifiable in ~/.gax/audit.jsonl.
Proof run: examples/agent_runs/SAMPLE_RUN/ (20260518T193305Z — Gemini 2.5 Flash-Lite, recovery probe + full agent loop, all audit IDs correlated). See examples/README.md.
pip install -r examples/requirements-agent.txt
# .env: GITHUB_TOKEN, GEMINI_API_KEY (or OPENAI_API_KEY / ANTHROPIC_API_KEY)
# optional: GEMINI_FALLBACK_KEY, GEMINI_MODEL=gemini-2.5-flash
python examples/agent_pr_triage.pypip install -r eval/requirements.txt
cd gax && python3 -m venv .venv && source .venv/bin/activate
pip install -e ".[dev]"
# Put GITHUB_TOKEN in repo-root .env (gitignored) or export it
python ../eval/run_full.py
python ../eval/run_comparison.py --live-mcp
python ../eval/case_study/run_case_study.pyIndependent references: mcp_vs_cli_benchmarks_2026/report.md · research/10-evaluation.md
GAX commands map to backends via the adapter field in each manifest.
| Adapter | Purpose | Example commands |
|---|---|---|
exec |
Wrap existing CLIs | gh.pr.list, gh.pr.view (uses gh subprocess; GH_TOKEN from OAuth) |
mcp |
One MCP tool per GAX command (schema stays in sidecar) | mcp.github.list_pulls |
http |
OpenAPI-generated GET calls | pet.findpetsbystatus (from gax openapi generate) |
mock |
Tests & demos without credentials | demo.echo, kubectl.get.pods, aws.s3.list, jira.issue.get |
Create gax/manifests/my.command.yaml:
command: my.command
version: "1.0.0"
description: What this command does
category: myapp
adapter: mock # or exec | mcp | http
required_scopes:
- myapp:read
side_effects: read
input_schema:
type: object
properties:
id: { type: string }
output_schema:
type: objectRestart gaxd or use gax --local.
GAX ships as an MCP server, so Claude Code, Cursor, or any MCP client gets governed shell execution with zero GAX-specific integration:
claude mcp add gax -- gax-mcp
# or, in .mcp.json / client config:
# { "mcpServers": { "gax": { "command": "gax-mcp" } } }It publishes exactly three tools — gax_search, gax_doc, gax_invoke — and keeps
the command registry behind them. A naive MCP server publishes one tool per capability,
so a 43-command registry costs ~44k tokens of schema before the first turn. GAX's surface
is constant-size regardless of how many commands you register, which is the same shape
as Anthropic's tool-search / defer_loading pattern — it composes with the platform fix
rather than competing with it.
The governance boundary is unchanged: gax_invoke calls the same executor as the CLI, so
capability, scope, and policy checks run before any adapter, and every call returns an
audit_id. A client cannot reach an unregistered command, and cannot bypass the capability
check by rephrasing — the model only ever proposes a command name plus args.
eval "$(gax auth cap-mint --command demo.echo --scope demo:echo --export)"
gax-mcp # stdio JSON-RPC; normally launched by the clientPoint GAX at any of the ~6,400 MCP servers. It enumerates the tools and writes one governed command per tool, hashing each tool's contract:
gax mcp import --id filesystem npx -y @modelcontextprotocol/server-filesystem /tmp Imported 3 of 3 tool(s) from 'filesystem'
! mcp.filesystem.write_file sha256:9c1a4e7b0f2d…
mcp.filesystem.read_file sha256:034f134f70ea…
mcp.filesystem.list_dir sha256:5e2b81cc94af…
The pin covers the tool's name, description, and input schema — the three things
that define what the model will be told to do. Before every invoke, the live tool is
hashed again. If it changed, the call fails closed with pin_mismatch and the tool
is never called.
That closes the tool-poisoning class behind several 2026 MCP CVEs: a server can rewrite a tool's description to smuggle new instructions to the model, and a gateway that proxies whatever the server currently advertises will forward it. Description is in scope precisely because it's the injection vector — schema-only pinning misses it.
gax mcp verify # re-check every pin against the live serversImport is a review step, not an approval. Anything not clearly read-only is
imported as destructive, so your read-only capability can't invoke it until a human
reads what it does and raises the ceiling. Re-importing never silently re-pins a
changed tool — that would let tampering be laundered by re-running import.
Expose a single MCP tool without loading all tool schemas into the agent:
adapter: mcp
mcp:
server_command: npx
server_args: ["-y", "@modelcontextprotocol/server-github"]
tool_name: list_pull_requestsexport GITHUB_TOKEN=...
gax mcp.github.list_pulls --repo octocat/Hello-World --surface modelgax openapi generate examples/petstore-openapi.json --prefix pet --adapter mockRequirements: Python 3.10+
git clone https://github.com/0sparsh2/GAX.git
cd GAX/gax
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -e ".[dev]"
# Optional: OS keychain for OAuth tokens
pip install -e ".[keyring]"Then one command sets everything up — ~/.gax, a 30-day read-only dev capability,
the sidecar, and a set of real commands:
gax init --profile k8s --profile github
gax k8s.pod.logs --pod web-1 # works immediately; no exports needed
gax doctor # diagnose config, capability, sidecar, backendsProfiles ship working commands so your first task isn't authoring YAML:
| Profile | Commands |
|---|---|
k8s |
pod.list pod.logs pod.describe deployment.list service.list · deployment.restart (write) · pod.delete namespace.delete (destructive) |
github |
pr.list pr.view issue.list issue.view run.list · pr.comment (write) · pr.merge (destructive) |
gax profile list shows them by danger level; gax profile add <name> installs into
~/.gax/manifests/, which survives package upgrades. Installing grants nothing —
init only adds a profile's read commands to your capability, so the write and
destructive ones stay refused until you mint for them deliberately.
Measured in a clean virtualenv with a fresh $HOME: install → 22 registered commands →
governed invoke → destructive refused → doctor all green in ~1 second. Full
walkthrough: QUICKSTART.
gaxd start # foreground
gaxd start --background # background (pid in ~/.gax/gaxd.pid)
gaxd start --host 0.0.0.0 # hosted (put TLS on gateway)export GAX_CAP="$(gax auth cap-mint \
--command demo.echo \
--command gh.pr.list \
--command gh.pr.view \
--scope demo:echo \
--scope github:pull_request:read \
--export | sed 's/export GAX_CAP=//')"Macaroon-style cap: add --macaroon.
- Create a GitHub OAuth App with Device Flow enabled
export GAX_GITHUB_CLIENT_ID=Ov23li...gax auth login --tenant acme-corpexport GAX_CAP="$(gax auth cap-from-oauth --export | sed 's/export GAX_CAP=//')"
gax search "pull request"
gax doc gh.pr.list
gax schema gh.pr.list
gax gh.pr.list --repo octocat/Hello-World --limit 5 --surface model
gax gh.pr.view --repo octocat/Hello-World --number 1
gax demo.echo --message hellogax plan run examples/plan-demo.yaml # list PRs → view first
gax plan run examples/plan-parallel.yaml # parallel demo.echo branchesgax vault put api_key "secret-value" --tenant acme
gax vault get api_key --tenant acme
gax compliance export --format csv # ~/.gax/exports/audit_soc2.csv
gax compliance export --format json
# Policy: gax/config/policy.yaml + optional OPA (config/policy.rego)gax --local demo.echo --message "no daemon"| Variable | Purpose |
|---|---|
GAX_CAP |
Capability JWT or macaroon |
GAX_HOST / GAX_PORT |
gaxd address (default 127.0.0.1:9477) |
GAX_GITHUB_CLIENT_ID |
OAuth device flow |
GITHUB_TOKEN |
Used by gh exec adapter / MCP GitHub server |
GAX_HASHICORP_VAULT_ADDR |
Optional Vault backend for gax vault |
GAX_SPIFFE_ID |
Workload identity metadata in audit |
GAX_OTEL_STDOUT=1 |
Emit OTEL-shaped logs to stdout |
| Command | Description |
|---|---|
gaxd start / stop / status |
Sidecar lifecycle |
gax auth login |
OAuth device flow |
gax auth cap-mint |
Mint dev capability |
gax auth cap-from-oauth |
Capability from stored OAuth |
gax auth status |
List stored tokens |
gax search / doc / schema |
Lazy discovery |
gax run <cmd> |
Explicit invoke |
gax <cmd> |
Shorthand for registered commands |
gax plan run <file> |
DAG-style workflows |
gax openapi generate |
OpenAPI → manifests |
gax vault put/get |
Tenant secrets |
gax compliance export |
Audit export |
HTTP API: POST /invoke, GET /search?q=, GET /commands/{id}/doc, GET /health — see gax/README.md.
| Doc | Topic |
|---|---|
| docs/acsp/protocol.md | ACSP overview |
| docs/acsp/envelope-v1.md | Response envelope |
| docs/acsp/discovery.md | search / doc / schema |
| gax/schemas/envelope.v1.json | JSON Schema |
Surfaces: model (truncated for LLM), human (TTY), full (automation).
Exit codes: 0 ok · 2 policy denied · 3 invalid cap · 4 not found · 5 adapter error · 6 pin mismatch
GAX/
├── README.md ← you are here
├── LICENSE
├── gax/ ← Python reference implementation
│ ├── gax/ ← package source (cli, daemon, adapters, …)
│ ├── manifests/ ← command registry (YAML)
│ ├── config/ ← OAuth providers, policy.yaml, policy.rego
│ ├── schemas/ ← envelope.v1.json
│ ├── examples/ ← plans, OpenAPI samples
│ └── tests/
├── docs/acsp/ ← protocol specification
├── eval/ ← CLI vs MCP vs GAX benchmarks
│ ├── run_comparison.py
│ ├── run_full.py
│ └── results/
├── research/ ← background, architecture, comparisons
│ └── diagrams/png/ ← architecture diagrams
├── mcp_vs_cli_benchmarks_2026/
│ ├── report.md ← cited benchmark synthesis
│ └── results/*.json
└── deep-research/ ← phased research skill (outline → JSON → report)
| Resource | Description |
|---|---|
| research/README.md | Research index |
| research/01-background-mcp-vs-cli.md | Why MCP vs CLI matters |
| research/02-gax-proposal.md | GAX thesis |
| research/05-comparison-matrix.md | CLI / MCP / GAX matrix |
| mcp_vs_cli_benchmarks_2026/report.md | Deep research report (Scalekit, Anthropic, Cloudflare) |
| research/11-project-completion.md | Project summary |
Primary external benchmarks:
- Scalekit — MCP vs CLI (4×–32× tokens, 28% MCP timeouts)
- Anthropic — Code execution with MCP (~98.7% token reduction example)
- Cloudflare — Code Mode (~1k vs ~1.17M tokens)
cd gax
source .venv/bin/activate
pip install -e ".[dev]"
pytest -q # unit tests
python ../eval/run_full.py # tests + eval
# Regenerate diagram PNGs
cd ../research/diagrams
for f in *.mmd; do
npx -y @mermaid-js/mermaid-cli@11 -i "$f" -o "png/${f%.mmd}.png" -b transparent
doneValidate deep-research JSON:
python ../deep-research/scripts/validate_json.py \
-f ../mcp_vs_cli_benchmarks_2026/fields.yaml \
-j ../mcp_vs_cli_benchmarks_2026/results/*.json| Phase | Status | Highlights |
|---|---|---|
| 0 Prototype | Working | Envelope, gaxd, manifests, JWT caps |
| 1 Hardening | Working | OAuth, plans, macaroons, eval v2 |
| 2 Ecosystem | Mixed | MCP bridge (prototype); kubectl/aws/jira (stub) |
| 3 Enterprise | Mostly stub | Vault/SPIFFE/OPA hooks; compliance export (prototype) |
Next up — see docs/PLAN-2026H2.md:
GAX as an MCP server— shipped:claude mcp add gax -- gax-mcp(above)- Eval integrity — W1/W2 done; W3 (mock/live split) and W4 (derivation labels) queued
- Real OPA — policy-as-code is table stakes for the regulated-CI/CD wedge; Vault and SPIFFE stay out of the pitch until they're genuine
Explicitly not doing: competing on breadth of integrations. Composio, StackOne, and Docker's 200-image catalog win that permanently. GAX competes on depth of the enforcement guarantee for a narrow, high-stakes surface.
Full checklist: research/06-implementation-roadmap.md
See CONTRIBUTING.md — adapters, eval tasks, manifests, protocol change process.
Quick links
- gax package README — install & command details
- ACSP spec
- Evaluation guide
