gax-cli 0.4.0__py3-none-any.whl

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 (66) hide show
  1. gax/__init__.py +3 -0
  2. gax/adapters/__init__.py +3 -0
  3. gax/adapters/base.py +25 -0
  4. gax/adapters/exec_adapter.py +261 -0
  5. gax/adapters/http_adapter.py +33 -0
  6. gax/adapters/k8s_adapter.py +338 -0
  7. gax/adapters/mcp_bridge.py +108 -0
  8. gax/adapters/mock_adapter.py +49 -0
  9. gax/audit.py +54 -0
  10. gax/caps.py +91 -0
  11. gax/cli.py +722 -0
  12. gax/client.py +58 -0
  13. gax/compliance.py +71 -0
  14. gax/config/oauth_providers.yaml +21 -0
  15. gax/config/policy.rego +20 -0
  16. gax/config/policy.yaml +59 -0
  17. gax/daemon.py +187 -0
  18. gax/envelope.py +64 -0
  19. gax/executor.py +151 -0
  20. gax/macaroons_cap.py +80 -0
  21. gax/manifests/aws.s3.list.yaml +20 -0
  22. gax/manifests/demo.echo.yaml +20 -0
  23. gax/manifests/gh.pr.list.yaml +41 -0
  24. gax/manifests/gh.pr.view.yaml +30 -0
  25. gax/manifests/jira.issue.get.yaml +18 -0
  26. gax/manifests/k8s.deployment.scale.yaml +35 -0
  27. gax/manifests/k8s.pod.delete.yaml +30 -0
  28. gax/manifests/kubectl.get.pods.yaml +20 -0
  29. gax/manifests/mcp.github.list_pulls.yaml +29 -0
  30. gax/manifests/pet_findpetsbystatus.yaml +19 -0
  31. gax/manifests/pet_getpetbyid.yaml +19 -0
  32. gax/mcp_client.py +110 -0
  33. gax/mcp_server.py +299 -0
  34. gax/oauth.py +184 -0
  35. gax/onboarding.py +397 -0
  36. gax/opa_policy.py +46 -0
  37. gax/openapi_gen.py +96 -0
  38. gax/otel_audit.py +50 -0
  39. gax/paths.py +46 -0
  40. gax/plan.py +151 -0
  41. gax/policy.py +33 -0
  42. gax/policy_bundle.py +64 -0
  43. gax/profiles/github/gh.issue.list.yaml +25 -0
  44. gax/profiles/github/gh.issue.view.yaml +23 -0
  45. gax/profiles/github/gh.pr.comment.yaml +30 -0
  46. gax/profiles/github/gh.pr.merge.yaml +29 -0
  47. gax/profiles/github/gh.run.list.yaml +22 -0
  48. gax/profiles/k8s/k8s.deployment.list.yaml +17 -0
  49. gax/profiles/k8s/k8s.deployment.restart.yaml +25 -0
  50. gax/profiles/k8s/k8s.namespace.delete.yaml +22 -0
  51. gax/profiles/k8s/k8s.pod.describe.yaml +22 -0
  52. gax/profiles/k8s/k8s.pod.logs.yaml +28 -0
  53. gax/profiles/k8s/k8s.service.list.yaml +17 -0
  54. gax/profiles.py +143 -0
  55. gax/projection.py +42 -0
  56. gax/registry.py +130 -0
  57. gax/schemas/envelope.v1.json +46 -0
  58. gax/secrets.py +49 -0
  59. gax/side_effects.py +67 -0
  60. gax/spiffe.py +32 -0
  61. gax/vault.py +69 -0
  62. gax_cli-0.4.0.dist-info/METADATA +164 -0
  63. gax_cli-0.4.0.dist-info/RECORD +66 -0
  64. gax_cli-0.4.0.dist-info/WHEEL +5 -0
  65. gax_cli-0.4.0.dist-info/entry_points.txt +4 -0
  66. gax_cli-0.4.0.dist-info/top_level.txt +1 -0
gax/projection.py ADDED
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ import copy
4
+ from typing import Any
5
+
6
+ MODEL_MAX_ROWS = 10
7
+ MODEL_MAX_STRING = 500
8
+ REDACT_KEYS = {"token", "secret", "password", "authorization"}
9
+
10
+
11
+ def _redact(obj: Any) -> Any:
12
+ if isinstance(obj, dict):
13
+ out = {}
14
+ for k, v in obj.items():
15
+ if k.lower() in REDACT_KEYS:
16
+ out[k] = "[redacted]"
17
+ else:
18
+ out[k] = _redact(v)
19
+ return out
20
+ if isinstance(obj, list):
21
+ return [_redact(x) for x in obj]
22
+ if isinstance(obj, str) and len(obj) > MODEL_MAX_STRING:
23
+ return obj[:MODEL_MAX_STRING] + "…"
24
+ return obj
25
+
26
+
27
+ def project_data(data: Any, surface: str) -> tuple[Any, dict[str, Any]]:
28
+ meta: dict[str, Any] = {}
29
+ if surface == "full":
30
+ return data, meta
31
+
32
+ if surface not in ("model", "human"):
33
+ surface = "model"
34
+
35
+ projected = _redact(copy.deepcopy(data))
36
+ if isinstance(projected, dict) and "items" in projected and isinstance(projected["items"], list):
37
+ items = projected["items"]
38
+ if len(items) > MODEL_MAX_ROWS:
39
+ projected["items"] = items[:MODEL_MAX_ROWS]
40
+ meta["truncated"] = True
41
+ meta["row_count"] = len(items)
42
+ return projected, meta
gax/registry.py ADDED
@@ -0,0 +1,130 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ import yaml
8
+
9
+ from gax.paths import MANIFESTS_DIR, USER_MANIFESTS_DIR
10
+
11
+
12
+ @dataclass
13
+ class CommandManifest:
14
+ command: str
15
+ version: str
16
+ description: str
17
+ category: str
18
+ adapter: str
19
+ backend: list[str] = field(default_factory=list)
20
+ required_scopes: list[str] = field(default_factory=list)
21
+ side_effects: str = "read"
22
+ idempotent: bool = True
23
+ input_schema: dict[str, Any] = field(default_factory=dict)
24
+ output_schema: dict[str, Any] = field(default_factory=dict)
25
+ raw: dict[str, Any] = field(default_factory=dict)
26
+
27
+ @property
28
+ def cmd_id(self) -> str:
29
+ return f"{self.command}@{self.version}"
30
+
31
+ @property
32
+ def schema_uri(self) -> str:
33
+ return f"https://schemas.gax.dev/{self.command.replace('.', '/')}/v{self.version.split('.')[0]}"
34
+
35
+
36
+ class Registry:
37
+ """
38
+ Command registry.
39
+
40
+ Loads from two places, in order: the manifests bundled with the package, then
41
+ `~/.gax/manifests/`. The user directory wins on conflict, so installing a
42
+ profile or hand-editing a command survives a package upgrade instead of being
43
+ overwritten by it.
44
+ """
45
+
46
+ def __init__(self, manifests_dir: Path | None = None) -> None:
47
+ self._dir = manifests_dir or MANIFESTS_DIR
48
+ # An explicit dir (tests, alternate deployments) means exactly that dir.
49
+ self._extra_dirs: list[Path] = [] if manifests_dir else [USER_MANIFESTS_DIR]
50
+ self._commands: dict[str, CommandManifest] = {}
51
+ self.reload()
52
+
53
+ @property
54
+ def source_dirs(self) -> list[Path]:
55
+ return [self._dir, *self._extra_dirs]
56
+
57
+ def reload(self) -> None:
58
+ self._commands.clear()
59
+ for directory in self.source_dirs:
60
+ if directory.exists():
61
+ self._load_dir(directory)
62
+
63
+ def _load_dir(self, directory: Path) -> None:
64
+ for path in sorted(directory.glob("*.yaml")):
65
+ data = yaml.safe_load(path.read_text()) or {}
66
+ cmd = data.get("command")
67
+ if not cmd:
68
+ continue
69
+ self._commands[cmd] = CommandManifest(
70
+ command=cmd,
71
+ version=str(data.get("version", "1.0.0")),
72
+ description=str(data.get("description", "")),
73
+ category=str(data.get("category", "general")),
74
+ adapter=str(data.get("adapter", "mock")),
75
+ backend=list(data.get("backend") or []),
76
+ required_scopes=list(data.get("required_scopes") or []),
77
+ side_effects=str(data.get("side_effects", "read")),
78
+ idempotent=bool(data.get("idempotent", True)),
79
+ input_schema=dict(data.get("input_schema") or {}),
80
+ output_schema=dict(data.get("output_schema") or {}),
81
+ raw=data,
82
+ )
83
+
84
+ def get(self, command: str) -> CommandManifest | None:
85
+ return self._commands.get(command)
86
+
87
+ def list_commands(self) -> list[CommandManifest]:
88
+ return list(self._commands.values())
89
+
90
+ def search(self, query: str, limit: int = 5) -> list[CommandManifest]:
91
+ q = query.lower().strip()
92
+ if not q:
93
+ return self.list_commands()[:limit]
94
+ scored: list[tuple[int, CommandManifest]] = []
95
+ for m in self._commands.values():
96
+ hay = f"{m.command} {m.description} {m.category}".lower()
97
+ score = 0
98
+ if q in m.command.lower():
99
+ score += 10
100
+ if q in m.description.lower():
101
+ score += 5
102
+ for word in q.split():
103
+ if word in hay:
104
+ score += 2
105
+ if score > 0:
106
+ scored.append((score, m))
107
+ scored.sort(key=lambda x: (-x[0], x[1].command))
108
+ return [m for _, m in scored[:limit]]
109
+
110
+ def doc_stub(self, command: str) -> dict[str, Any] | None:
111
+ m = self.get(command)
112
+ if not m:
113
+ return None
114
+ props = (m.input_schema or {}).get("properties") or {}
115
+ return {
116
+ "command": m.command,
117
+ "version": m.version,
118
+ "description": m.description,
119
+ "usage": f"gax {m.command} --repo owner/name",
120
+ "required_scopes": m.required_scopes,
121
+ "arguments": {
122
+ k: {
123
+ "type": v.get("type", "string"),
124
+ "description": v.get("description", ""),
125
+ "required": k in ((m.input_schema or {}).get("required") or []),
126
+ }
127
+ for k, v in props.items()
128
+ },
129
+ "side_effects": m.side_effects,
130
+ }
@@ -0,0 +1,46 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://schemas.gax.dev/envelope/v1",
4
+ "title": "GAX Envelope v1",
5
+ "type": "object",
6
+ "required": ["v", "ok", "cmd", "audit_id", "surface", "data", "meta"],
7
+ "properties": {
8
+ "v": { "const": 1 },
9
+ "ok": { "type": "boolean" },
10
+ "cmd": { "type": "string", "description": "command@version" },
11
+ "audit_id": { "type": "string" },
12
+ "schema": { "type": "string", "format": "uri" },
13
+ "surface": { "enum": ["model", "human", "full"] },
14
+ "data": {},
15
+ "meta": {
16
+ "type": "object",
17
+ "properties": {
18
+ "truncated": { "type": "boolean" },
19
+ "cursor": { "type": "string" },
20
+ "row_count": { "type": "integer" },
21
+ "duration_ms": { "type": "number" }
22
+ },
23
+ "additionalProperties": true
24
+ },
25
+ "error": {
26
+ "type": ["object", "null"],
27
+ "properties": {
28
+ "kind": { "type": "string" },
29
+ "message": { "type": "string" },
30
+ "retryable": { "type": "boolean" }
31
+ }
32
+ },
33
+ "next": {
34
+ "type": "array",
35
+ "items": {
36
+ "type": "object",
37
+ "required": ["cmd", "args"],
38
+ "properties": {
39
+ "cmd": { "type": "string" },
40
+ "args": { "type": "object" },
41
+ "reason": { "type": "string" }
42
+ }
43
+ }
44
+ }
45
+ }
46
+ }
gax/secrets.py ADDED
@@ -0,0 +1,49 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+ from typing import Any
6
+
7
+ from gax.paths import ensure_gax_home
8
+
9
+
10
+ def _use_keyring() -> bool:
11
+ try:
12
+ import keyring # noqa: F401
13
+
14
+ return True
15
+ except ImportError:
16
+ return False
17
+
18
+
19
+ def _service_name(tenant: str, provider: str) -> str:
20
+ return f"gax:{tenant}:{provider}"
21
+
22
+
23
+ def save_secret(tenant: str, provider: str, data: dict[str, Any], *, file_path: Path) -> None:
24
+ """Prefer OS keychain when keyring is installed; else encrypted-at-rest file (mode 600)."""
25
+ payload = json.dumps(data)
26
+ if _use_keyring():
27
+ import keyring
28
+
29
+ keyring.set_password(_service_name(tenant, provider), "oauth_tokens", payload)
30
+ # Remove plaintext file if migrating
31
+ if file_path.exists():
32
+ file_path.unlink()
33
+ return
34
+ ensure_gax_home()
35
+ file_path.parent.mkdir(parents=True, exist_ok=True)
36
+ file_path.write_text(payload)
37
+ file_path.chmod(0o600)
38
+
39
+
40
+ def load_secret(tenant: str, provider: str, *, file_path: Path) -> dict[str, Any] | None:
41
+ if _use_keyring():
42
+ import keyring
43
+
44
+ raw = keyring.get_password(_service_name(tenant, provider), "oauth_tokens")
45
+ if raw:
46
+ return json.loads(raw)
47
+ if file_path.exists():
48
+ return json.loads(file_path.read_text())
49
+ return None
gax/side_effects.py ADDED
@@ -0,0 +1,67 @@
1
+ """
2
+ Side-effect ladder — the danger level of a command, and the ceiling a capability
3
+ is allowed to reach.
4
+
5
+ Every manifest declares `side_effects: read | write | destructive`. Every capability
6
+ carries a `max_side_effect` ceiling (default `read`). A capability may only invoke a
7
+ command whose level is at or below its ceiling.
8
+
9
+ **Why this exists as a second check.** The capability already carries an explicit
10
+ command allowlist, so in principle naming the command is enough. But an allowlist is
11
+ edited by humans under time pressure, and the failure mode is silent: add
12
+ `k8s.pod.delete` to a token that was meant to be read-only and nothing complains. The
13
+ ceiling makes that mistake inert — two independent things must be wrong before
14
+ something gets deleted, and the second one has to be stated in the explicit language
15
+ of danger rather than as an opaque command name.
16
+
17
+ Defaults fail closed:
18
+ - a manifest with no/unknown `side_effects` is treated as `destructive`
19
+ - a capability with no ceiling is treated as `read`
20
+
21
+ so a command added without thinking about danger is refused rather than allowed.
22
+ """
23
+
24
+ from __future__ import annotations
25
+
26
+ from typing import Any
27
+
28
+ # Ordered least → most dangerous. Index is the comparison key.
29
+ LADDER: tuple[str, ...] = ("read", "write", "destructive")
30
+
31
+ DEFAULT_CEILING = "read"
32
+ # Unknown manifest levels fail closed at the top of the ladder.
33
+ UNKNOWN_LEVEL = "destructive"
34
+
35
+
36
+ def level_rank(level: str | None) -> int:
37
+ """Rank of a side-effect level. Unknown/missing → most dangerous."""
38
+ if not level:
39
+ return LADDER.index(UNKNOWN_LEVEL)
40
+ try:
41
+ return LADDER.index(str(level).strip().lower())
42
+ except ValueError:
43
+ return LADDER.index(UNKNOWN_LEVEL)
44
+
45
+
46
+ def normalize(level: str | None) -> str:
47
+ """Canonical level name, failing closed on anything unrecognized."""
48
+ return LADDER[level_rank(level)]
49
+
50
+
51
+ def capability_ceiling(claims: dict[str, Any]) -> str:
52
+ """
53
+ The ceiling this capability may reach.
54
+
55
+ Absent → `read`. A capability minted before ceilings existed therefore cannot
56
+ invoke a destructive command, which is the safe direction for that migration.
57
+ """
58
+ return normalize(claims.get("max_side_effect") or DEFAULT_CEILING)
59
+
60
+
61
+ def exceeds_ceiling(claims: dict[str, Any], command_level: str | None) -> bool:
62
+ """True when `command_level` is more dangerous than the capability allows."""
63
+ return level_rank(command_level) > level_rank(capability_ceiling(claims))
64
+
65
+
66
+ def is_destructive(level: str | None) -> bool:
67
+ return normalize(level) == "destructive"
gax/spiffe.py ADDED
@@ -0,0 +1,32 @@
1
+ """SPIFFE / workload identity hooks for gaxd (MVP)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+
10
+ def workload_identity() -> dict[str, Any]:
11
+ """Read SPIFFE material from env (set by tbot, SPIRE agent, or K8s CSI)."""
12
+ cert = os.environ.get("GAX_SPIFFE_CERT_PATH", "")
13
+ key = os.environ.get("GAX_SPIFFE_KEY_PATH", "")
14
+ svid = os.environ.get("GAX_SPIFFE_SVID", "")
15
+ spiffe_id = os.environ.get("GAX_SPIFFE_ID", "")
16
+ out: dict[str, Any] = {
17
+ "enabled": bool(cert or svid or spiffe_id),
18
+ "spiffe_id": spiffe_id or None,
19
+ "cert_present": bool(cert and Path(cert).exists()),
20
+ "key_present": bool(key and Path(key).exists()),
21
+ }
22
+ if svid:
23
+ out["svid_jwt_prefix"] = svid[:20] + "…"
24
+ return out
25
+
26
+
27
+ def attest_metadata() -> dict[str, Any]:
28
+ """Metadata attached to audit events when workload identity is configured."""
29
+ wi = workload_identity()
30
+ if not wi["enabled"]:
31
+ return {}
32
+ return {"workload_identity": wi}
gax/vault.py ADDED
@@ -0,0 +1,69 @@
1
+ """Multi-tenant secret vault (file MVP; HashiCorp Vault hook via env)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import httpx
11
+
12
+ from gax.paths import GAX_HOME, ensure_gax_home
13
+ from gax.secrets import load_secret, save_secret
14
+
15
+ VAULT_DIR = GAX_HOME / "vault"
16
+
17
+
18
+ def _tenant_path(tenant: str) -> Path:
19
+ ensure_gax_home()
20
+ d = VAULT_DIR / tenant
21
+ d.mkdir(parents=True, exist_ok=True)
22
+ return d / "secrets.json"
23
+
24
+
25
+ def vault_put(tenant: str, key: str, value: str) -> None:
26
+ hv = os.environ.get("GAX_HASHICORP_VAULT_ADDR")
27
+ if hv:
28
+ _vault_hc_put(hv, tenant, key, value)
29
+ return
30
+ path = _tenant_path(tenant)
31
+ data = load_secret(tenant, "vault", file_path=path) or {}
32
+ data[key] = value
33
+ save_secret(tenant, "vault", data, file_path=path)
34
+
35
+
36
+ def vault_get(tenant: str, key: str) -> str | None:
37
+ hv = os.environ.get("GAX_HASHICORP_VAULT_ADDR")
38
+ if hv:
39
+ return _vault_hc_get(hv, tenant, key)
40
+ path = _tenant_path(tenant)
41
+ data = load_secret(tenant, "vault", file_path=path) or {}
42
+ return data.get(key)
43
+
44
+
45
+ def _vault_hc_headers() -> dict[str, str]:
46
+ tok = os.environ.get("GAX_HASHICORP_VAULT_TOKEN", "")
47
+ h = {"Content-Type": "application/json"}
48
+ if tok:
49
+ h["X-Vault-Token"] = tok
50
+ return h
51
+
52
+
53
+ def _vault_hc_put(addr: str, tenant: str, key: str, value: str) -> None:
54
+ path = f"{addr.rstrip('/')}/v1/secret/data/gax/{tenant}/{key}"
55
+ httpx.post(
56
+ path,
57
+ headers=_vault_hc_headers(),
58
+ json={"data": {"value": value}},
59
+ timeout=15.0,
60
+ ).raise_for_status()
61
+
62
+
63
+ def _vault_hc_get(addr: str, tenant: str, key: str) -> str | None:
64
+ path = f"{addr.rstrip('/')}/v1/secret/data/gax/{tenant}/{key}"
65
+ r = httpx.get(path, headers=_vault_hc_headers(), timeout=15.0)
66
+ if r.status_code == 404:
67
+ return None
68
+ r.raise_for_status()
69
+ return (r.json().get("data") or {}).get("data", {}).get("value")
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.4
2
+ Name: gax-cli
3
+ Version: 0.4.0
4
+ Summary: Governed execution layer for agent shell commands — capability-gated, audited, MCP-compatible
5
+ Author: Sparsh Nagpal
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/0sparsh2/GAX
8
+ Project-URL: Repository, https://github.com/0sparsh2/GAX
9
+ Project-URL: Documentation, https://github.com/0sparsh2/GAX/blob/main/docs/QUICKSTART.md
10
+ Project-URL: Issues, https://github.com/0sparsh2/GAX/issues
11
+ Keywords: ai-agents,mcp,model-context-protocol,agent-governance,capability-security,audit,llm-tools
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Intended Audience :: System Administrators
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Security
22
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
23
+ Classifier: Topic :: System :: Systems Administration
24
+ Requires-Python: >=3.10
25
+ Description-Content-Type: text/markdown
26
+ Requires-Dist: click>=8.1
27
+ Requires-Dist: pyyaml>=6.0
28
+ Requires-Dist: jsonschema>=4.20
29
+ Requires-Dist: PyJWT>=2.8
30
+ Requires-Dist: httpx>=0.27
31
+ Provides-Extra: dev
32
+ Requires-Dist: pytest>=8.0; extra == "dev"
33
+ Provides-Extra: keyring
34
+ Requires-Dist: keyring>=25.0; extra == "keyring"
35
+
36
+ # GAX — Governed Agent eXecution
37
+
38
+ **Your MCP gateway can't see `bash`. GAX governs both.**
39
+
40
+ GAX is a governed execution layer for agent shell commands. Agents get a
41
+ command-line-shaped surface; OAuth, policy, audit, and tenancy live in a sidecar the
42
+ model never sees.
43
+
44
+ The guarantee: **an agent can only run commands you registered, every invoke is checked
45
+ against a capability token before any backend runs, and every invoke produces an
46
+ `audit_id`.**
47
+
48
+ It **complements MCP** rather than competing with it — MCP servers become adapters behind
49
+ stable command names, so one policy file and one audit trail cover both your MCP calls
50
+ and your shell calls.
51
+
52
+ Full docs, research, and evaluation: **https://github.com/0sparsh2/GAX**
53
+
54
+ ---
55
+
56
+ ## Install
57
+
58
+ ```bash
59
+ pip install gax-cli
60
+ gax init --profile k8s --profile github
61
+ ```
62
+
63
+ That creates `~/.gax`, mints a 30-day **read-only** capability, starts the sidecar, and
64
+ registers 22 real commands. Measured on a clean machine: about a second.
65
+
66
+ ```bash
67
+ gax k8s.pod.logs --pod web-1 # works immediately, no exports needed
68
+ gax doctor # diagnose config, capability, sidecar, backends
69
+ ```
70
+
71
+ ## See it stop something dangerous
72
+
73
+ ```bash
74
+ export GAX_K8S_MOCK=1 # no cluster needed
75
+ gax k8s.namespace.delete --namespace prod
76
+ ```
77
+
78
+ ```json
79
+ {
80
+ "ok": false,
81
+ "error": {
82
+ "kind": "policy_denied",
83
+ "message": "side_effects 'destructive' exceeds capability ceiling 'read'"
84
+ },
85
+ "audit_id": "aud_cef808f0fadd41e7"
86
+ }
87
+ ```
88
+
89
+ Refused before `kubectl` was ever spawned, and the denial is audited with its arguments.
90
+
91
+ Even if the command sits on the token's allowlist it is *still* refused, because the
92
+ danger ceiling is a separate control — **two independent things must be wrong before
93
+ something gets deleted.**
94
+
95
+ To run it, raise the ceiling deliberately, with `--dry-run` available first:
96
+
97
+ ```bash
98
+ export GAX_CAP="$(gax auth cap-mint --command k8s.namespace.delete \
99
+ --scope k8s:namespaces:write --max-side-effect destructive --raw)"
100
+
101
+ gax k8s.namespace.delete --namespace staging --dry-run
102
+ ```
103
+
104
+ ## Use it from any MCP client
105
+
106
+ ```bash
107
+ claude mcp add gax -- gax-mcp
108
+ ```
109
+
110
+ Publishes exactly three tools — `gax_search`, `gax_doc`, `gax_invoke` — keeping the
111
+ registry behind them, so schema cost stays constant no matter how many commands you
112
+ register.
113
+
114
+ ## Add your own command
115
+
116
+ One YAML file in `~/.gax/manifests/`:
117
+
118
+ ```yaml
119
+ command: db.migration.run
120
+ version: "1.0.0"
121
+ description: Apply pending migrations
122
+ adapter: exec
123
+ required_scopes: [db:schema:write]
124
+ side_effects: destructive # sets the ceiling required to invoke it
125
+ input_schema:
126
+ type: object
127
+ properties:
128
+ env: { type: string, description: Target environment }
129
+ dry_run: { type: boolean, description: Validate without applying }
130
+ required: [env]
131
+ ```
132
+
133
+ CLI flags are generated from the schema. Omit `side_effects` and GAX fails closed —
134
+ undeclared commands are treated as `destructive`.
135
+
136
+ Generate manifests instead of writing them: `gax openapi generate spec.json`.
137
+
138
+ ## Commands
139
+
140
+ | | |
141
+ |---|---|
142
+ | `gax init` / `gax doctor` | Setup and diagnostics |
143
+ | `gax profile list` / `add` | Bundled command sets (`k8s`, `github`) |
144
+ | `gax search` / `doc` / `schema` | Lazy discovery |
145
+ | `gax <command>` | Invoke a registered command |
146
+ | `gax auth cap-mint` | Mint a capability (`--max-side-effect read\|write\|destructive`) |
147
+ | `gax auth login` | OAuth 2.0 device flow |
148
+ | `gax plan run <file>` | Multi-step / parallel workflows |
149
+ | `gax vault put` / `get` | Tenant secrets (file or HashiCorp) |
150
+ | `gax compliance export` | SOC2-aligned audit export |
151
+ | `gaxd start` / `stop` / `status` | Sidecar lifecycle |
152
+ | `gax-mcp` | Run GAX as an MCP server (stdio) |
153
+
154
+ Audit log: `~/.gax/audit.jsonl`, one JSON object per invocation, including denials.
155
+
156
+ ## Honest scope
157
+
158
+ - Raw CLI costs ~3.5× fewer tokens than GAX on like-for-like tasks. Governance is not
159
+ free, and we publish that number rather than a flattering one.
160
+ - Vault, SPIFFE, and OPA integrations are hooks and stubs, not production features.
161
+ - The evaluation is a self-assessment by the author; methodology and known defects are
162
+ documented in the repository.
163
+
164
+ MIT licensed. Issues and contributions: https://github.com/0sparsh2/GAX
@@ -0,0 +1,66 @@
1
+ gax/__init__.py,sha256=MtEPF32M-McRPfaigQYR6LxocQx0UzKyWW5x7mbAp5A,63
2
+ gax/audit.py,sha256=xsYEQ9OP2seWC4MJOrC9HCCinbJf6UCBg3hhXvCqVnc,1390
3
+ gax/caps.py,sha256=R6UanAet-EEgAkuCUxm954e-3bsmUGe4sI6YEUODiVo,2889
4
+ gax/cli.py,sha256=7wi9AVg5hcr5i4rYkPLFCNoWbyOBfCVD3tyJAFsrNiE,24173
5
+ gax/client.py,sha256=wBlg_5q28K2XS5ixW6TPF6y7rQDbSK_Ws6EKgTZZePU,1838
6
+ gax/compliance.py,sha256=xZ1tD__hxC0GMFiGtpGU6ZBbXrhStCV9oTojFJJqvvA,2025
7
+ gax/daemon.py,sha256=ZblMjUu1nicmc6BTFP6arLrF1z2mfbdGrvYLSow75-8,5996
8
+ gax/envelope.py,sha256=AF7qcfpI1y1XCLwFIeICgBE0vRbiMb1W0BaHkCjrpr4,1443
9
+ gax/executor.py,sha256=WU2Wyjl0MZsY4vWpFl4EqY9DfjmC8UdU1IzHZS7952k,4091
10
+ gax/macaroons_cap.py,sha256=VJPKletpgwOPdkxB7Ltes_xBfsoisCSTCslv1-hKZRM,2517
11
+ gax/mcp_client.py,sha256=UQwM_upcT54uUgiMv-GGUxMQGGnamkBFZlDO6UnAXNk,3587
12
+ gax/mcp_server.py,sha256=wiC_ErL0FWC3PwfHNXgZstNfORDCiGyfDCaXuZG4e38,10249
13
+ gax/oauth.py,sha256=U4ZoTjI1RefFWcRp35DiWlCYYgqp4Z2K0hKsBO8U-4w,6069
14
+ gax/onboarding.py,sha256=sQUWnsR6M92GHoCyFK7PAki7X1Jp57UnY2IaOvGTzps,13008
15
+ gax/opa_policy.py,sha256=2JfkvZjTtC16Du34icypxzpLf1yR34NvmZ9BgydiciU,1432
16
+ gax/openapi_gen.py,sha256=siVSMBq6Ua45yPHWD0aJc1D6fBkVcq-mxNcPNi1AIQ8,3297
17
+ gax/otel_audit.py,sha256=D0ReRaA-mYLPj7LfA3MHQ5-lnKyHH7YeHWyT-fRvaQQ,1327
18
+ gax/paths.py,sha256=nzU2t51yGfldhw_w_n9L_b-dAYGogCmiY1JvopxoDug,1574
19
+ gax/plan.py,sha256=H0djp11oao9k2hJkz46CiOlm6SFD_xNzb4Tjqtpa44A,4793
20
+ gax/policy.py,sha256=ZVCo20a--Fy9fgW9IK9moRrCSrQ2TSc4PerfvQnBWGE,1353
21
+ gax/policy_bundle.py,sha256=jUYHu_TJddIZPcxk_rQHceWqg_BrqwQjxFFcwK3fkY4,2200
22
+ gax/profiles.py,sha256=ScNM2AjEv5_LCJ4krIBNGsydSZhx-xgCwA4RuGDSUfI,4475
23
+ gax/projection.py,sha256=pv1DmGK1bNjXXnHR7p2FzYSKE_BVaozizdYFMknjruw,1247
24
+ gax/registry.py,sha256=Ba27QZAbIzmReC3oKRkwdjo6KhQUtn4vDq5bfDqlzvM,4683
25
+ gax/secrets.py,sha256=KDGNl3dyGTGYenb6tKaCsSO_m2lK7pId3Syi0xujZxk,1356
26
+ gax/side_effects.py,sha256=gtYKUJ7ZtCWcbhigjQXjcm2bE3z3-FqyqN5izh41NEE,2566
27
+ gax/spiffe.py,sha256=F5EBL3I1Sxhp3uJHWWEHrKSgslsgerGJpKAVtuCTkt0,1039
28
+ gax/vault.py,sha256=c9gFLLGCbaWOycIksvQyj_QPsy9mAVmJQYBZn4Sz2AU,1972
29
+ gax/adapters/__init__.py,sha256=2tFcSP8OoYq9z8b9suULv-r8QV3nFz737pX86qABScc,69
30
+ gax/adapters/base.py,sha256=WvenAGsiGe3SpyjKxCAc9rBXr8bCb7PZTYbi3Ze5aTQ,887
31
+ gax/adapters/exec_adapter.py,sha256=RdeyxAlyoD6BOMGXE65MnIhS8wcLrlInGVxEjO0fi3o,8008
32
+ gax/adapters/http_adapter.py,sha256=M7E79JIm4bfMwERWDH7ggziTrBKuDVb2THoVbUWAbHQ,1078
33
+ gax/adapters/k8s_adapter.py,sha256=p3pvwxFoBrPsf6ECr-CEqgCiNNi8Xso4oxZcTmHsYZo,10873
34
+ gax/adapters/mcp_bridge.py,sha256=srprT_oBQVT5vxNEO2Ebe9FW1TSNYgU5MnuVZ57SjBI,3610
35
+ gax/adapters/mock_adapter.py,sha256=mCButxH3n1Ctal8tPQDPqAuZ_PEZfuew-JfvyoF9mQU,1885
36
+ gax/config/oauth_providers.yaml,sha256=yHT9uGVGSFStT2-1pQf-2FXfRpYfmjwXhVknXvxx7ac,730
37
+ gax/config/policy.rego,sha256=mJEUxxhQegn2U87vvIaRp0YTc8P6ydOwaojBbFtDJ-g,298
38
+ gax/config/policy.yaml,sha256=-1eaJdVEDo4LrowFFBcij-P1QvGQ2UK4-skFwQYXksI,1948
39
+ gax/manifests/aws.s3.list.yaml,sha256=yNrKSXGMe7RPwqgfFq9y3JRHMr1R-JrKhs-5-ivDvXo,342
40
+ gax/manifests/demo.echo.yaml,sha256=yUUG9QjYLa775FfRPe2WL3_iNHEN1XI7o3bWh4qngqQ,357
41
+ gax/manifests/gh.pr.list.yaml,sha256=ydaisewA8GqZ_rlR6I5UtdDFT1AUEKDTc9JO3-pXKvc,826
42
+ gax/manifests/gh.pr.view.yaml,sha256=kJVGwUUguesQWBtm1tWGQnpz6c4-BuJzWONixCfuZJU,565
43
+ gax/manifests/jira.issue.get.yaml,sha256=k7Bz28o6gKyFaIsfwUt-Wx0PJaFu9puPc3GyAPUM1oI,353
44
+ gax/manifests/k8s.deployment.scale.yaml,sha256=ic-HlPhFQlX2ArGos1SYIfxe8YGJ36lTaCChAwJHEP0,906
45
+ gax/manifests/k8s.pod.delete.yaml,sha256=gSGcVn_E7J1Za3Uem7WghNUV0OpZLdVqzZQFcKrw4N8,768
46
+ gax/manifests/kubectl.get.pods.yaml,sha256=GLSHU6rrumgCNN0UJaRJim-mj30K_WxLABlBPFe5Kmo,391
47
+ gax/manifests/mcp.github.list_pulls.yaml,sha256=nWDUGOnh-QW5UhMnNIOE_wg41Z68o4DQ2SJHa_YF3ag,605
48
+ gax/manifests/pet_findpetsbystatus.yaml,sha256=8mvQ9F8RVIuqp5AAES0drPbP1oPMN-t1HgEjkOpEj50,319
49
+ gax/manifests/pet_getpetbyid.yaml,sha256=lJ8nWdnXLWeOyLlRhavtCo4TElw0WdwiNRTZto2t8SQ,305
50
+ gax/profiles/github/gh.issue.list.yaml,sha256=Qh80Z3MHmKMW7UiwMRwddTSri3Aij6uYuTQZvS3Ks2U,487
51
+ gax/profiles/github/gh.issue.view.yaml,sha256=LlFdKLjkutG6t-j-JGTnK-VOo9O6g2OcUBxOw43YBnw,412
52
+ gax/profiles/github/gh.pr.comment.yaml,sha256=ax-RTErkvEpxI3Vgwgx3-DvSw6FysQwhlx35ZoPHdWI,580
53
+ gax/profiles/github/gh.pr.merge.yaml,sha256=wkuFsbIOCTxkWACLRToMEw_q2vr-V6Z88rcDKvCbhoE,625
54
+ gax/profiles/github/gh.run.list.yaml,sha256=csBxO1qh_m8SFSZCfW95PjxX59KBxfulS7AXdT3gPx0,421
55
+ gax/profiles/k8s/k8s.deployment.list.yaml,sha256=sXsE0KfPoikaIDIkH1_bjsBOM_hAu2zZYNd5eAa3QoQ,350
56
+ gax/profiles/k8s/k8s.deployment.restart.yaml,sha256=d5_viY8w02yu--hdjfWONQC1dLKXbSR5pc4yjLPx8qk,553
57
+ gax/profiles/k8s/k8s.namespace.delete.yaml,sha256=3FZkRnC8Q2i_9fvXuYDB3Zlf2BLUumm1pr4AarY6YJ8,499
58
+ gax/profiles/k8s/k8s.pod.describe.yaml,sha256=QtEMd6u_UAWKCxyPDM9jrOxlqur3D5qK0TdVasBl49w,430
59
+ gax/profiles/k8s/k8s.pod.logs.yaml,sha256=NySwRyDbnBbqwGXMDvaE6AQwnk2mQDeJZN9BAOZlovI,595
60
+ gax/profiles/k8s/k8s.service.list.yaml,sha256=yQv8Xe3Xd-k7P2ZKOpHSoUx88V-m_2p-LvLyvr4us2o,341
61
+ gax/schemas/envelope.v1.json,sha256=8xJzS2DOa9OLboSTTF9g2z3qsCw6CbPC-8z7zWjZjwk,1321
62
+ gax_cli-0.4.0.dist-info/METADATA,sha256=ag56qONifVADjiKjh2jZn1NfrGjgugnLBAF5rvTHCjc,5713
63
+ gax_cli-0.4.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
64
+ gax_cli-0.4.0.dist-info/entry_points.txt,sha256=Jnu0YxVDRRiSZ-x_aZr3XD6xWp7KTkzrICfxgF9yZM8,90
65
+ gax_cli-0.4.0.dist-info/top_level.txt,sha256=1LX0rekJftkdwIHUy--CeuWoPGXAtOkBkJsa4AfuUAQ,4
66
+ gax_cli-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,4 @@
1
+ [console_scripts]
2
+ gax = gax.cli:main
3
+ gax-mcp = gax.mcp_server:main
4
+ gaxd = gax.daemon:main