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/macaroons_cap.py ADDED
@@ -0,0 +1,80 @@
1
+ from __future__ import annotations
2
+
3
+ import base64
4
+ import hashlib
5
+ import hmac
6
+ import json
7
+ import time
8
+ from typing import Any
9
+
10
+ from gax.caps import jwt_secret
11
+
12
+
13
+ def _sign(data: bytes, key: bytes) -> str:
14
+ return base64.urlsafe_b64encode(hmac.new(key, data, hashlib.sha256).digest()).decode().rstrip("=")
15
+
16
+
17
+ def mint_macaroon(
18
+ *,
19
+ tenant_id: str,
20
+ subject: str,
21
+ commands: list[str],
22
+ scopes: list[str],
23
+ ttl_seconds: int = 3600,
24
+ caveats: list[dict[str, Any]] | None = None,
25
+ ) -> str:
26
+ """HMAC-chained capability (macaroon-style attenuation, MVP)."""
27
+ root_key = jwt_secret().encode()
28
+ now = int(time.time())
29
+ payload: dict[str, Any] = {
30
+ "v": 1,
31
+ "tenant_id": tenant_id,
32
+ "sub": subject,
33
+ "commands": commands,
34
+ "scopes": scopes,
35
+ "iat": now,
36
+ "exp": now + ttl_seconds,
37
+ "caveats": caveats or [],
38
+ }
39
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
40
+ sig = _sign(body, root_key)
41
+ for caveat in caveats or []:
42
+ sig = _sign(f"{sig}|{json.dumps(caveat, sort_keys=True)}".encode(), root_key)
43
+ blob = base64.urlsafe_b64encode(json.dumps({"p": payload, "s": sig}).encode()).decode()
44
+ return f"gaxm_{blob}"
45
+
46
+
47
+ def verify_macaroon(token: str) -> dict[str, Any]:
48
+ if token.startswith("gaxm_"):
49
+ token = token[5:]
50
+ try:
51
+ pad = "=" * (-len(token) % 4)
52
+ raw = json.loads(base64.urlsafe_b64decode(token + pad))
53
+ except Exception as e:
54
+ raise ValueError("invalid macaroon") from e
55
+ payload = raw.get("p") or {}
56
+ sig = raw.get("s", "")
57
+ root_key = jwt_secret().encode()
58
+ body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
59
+ expected = _sign(body, root_key)
60
+ for caveat in payload.get("caveats") or []:
61
+ expected = _sign(f"{expected}|{json.dumps(caveat, sort_keys=True)}".encode(), root_key)
62
+ if not hmac.compare_digest(expected, sig):
63
+ raise ValueError("macaroon signature mismatch")
64
+ if int(payload.get("exp", 0)) < time.time():
65
+ raise ValueError("macaroon expired")
66
+ return payload
67
+
68
+
69
+ def macaroon_allows_command(claims: dict[str, Any], command: str) -> bool:
70
+ allowed = claims.get("commands") or []
71
+ if "*" in allowed:
72
+ return True
73
+ return command in allowed
74
+
75
+
76
+ def macaroon_allows_scopes(claims: dict[str, Any], required: list[str]) -> bool:
77
+ held = set(claims.get("scopes") or [])
78
+ if "*" in held:
79
+ return True
80
+ return set(required).issubset(held)
@@ -0,0 +1,20 @@
1
+ command: aws.s3.list
2
+ version: "1.0.0"
3
+ description: List S3 buckets (mock MVP)
4
+ category: aws
5
+ adapter: mock
6
+ required_scopes:
7
+ - aws:s3:read
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ prefix:
14
+ type: string
15
+ default: ""
16
+ output_schema:
17
+ type: object
18
+ properties:
19
+ buckets:
20
+ type: array
@@ -0,0 +1,20 @@
1
+ command: demo.echo
2
+ version: "1.0.0"
3
+ description: Echo arguments (demo / testing without gh)
4
+ category: demo
5
+ adapter: mock
6
+ required_scopes:
7
+ - demo:echo
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ message:
14
+ type: string
15
+ default: hello
16
+ output_schema:
17
+ type: object
18
+ properties:
19
+ echo:
20
+ type: string
@@ -0,0 +1,41 @@
1
+ command: gh.pr.list
2
+ version: "1.0.0"
3
+ description: List pull requests for a repository
4
+ category: github
5
+ adapter: exec
6
+ backend:
7
+ - gh
8
+ - pr
9
+ - list
10
+ required_scopes:
11
+ - github:pull_request:read
12
+ side_effects: read
13
+ idempotent: true
14
+ input_schema:
15
+ type: object
16
+ required: [repo]
17
+ properties:
18
+ repo:
19
+ type: string
20
+ description: "owner/name"
21
+ limit:
22
+ type: integer
23
+ default: 30
24
+ state:
25
+ type: string
26
+ enum: [open, closed, merged, all]
27
+ default: open
28
+ output_schema:
29
+ type: object
30
+ properties:
31
+ items:
32
+ type: array
33
+ items:
34
+ type: object
35
+ properties:
36
+ number: { type: integer }
37
+ title: { type: string }
38
+ state: { type: string }
39
+ url: { type: string }
40
+ author: { type: string }
41
+ draft: { type: boolean }
@@ -0,0 +1,30 @@
1
+ command: gh.pr.view
2
+ version: "1.0.0"
3
+ description: View a single pull request
4
+ category: github
5
+ adapter: exec
6
+ backend:
7
+ - gh
8
+ - pr
9
+ - view
10
+ required_scopes:
11
+ - github:pull_request:read
12
+ side_effects: read
13
+ idempotent: true
14
+ input_schema:
15
+ type: object
16
+ required: [repo, number]
17
+ properties:
18
+ repo:
19
+ type: string
20
+ number:
21
+ type: integer
22
+ output_schema:
23
+ type: object
24
+ properties:
25
+ number: { type: integer }
26
+ title: { type: string }
27
+ body: { type: string }
28
+ state: { type: string }
29
+ url: { type: string }
30
+ author: { type: string }
@@ -0,0 +1,18 @@
1
+ command: jira.issue.get
2
+ version: "1.0.0"
3
+ description: Get Jira issue by key (mock MVP)
4
+ category: jira
5
+ adapter: mock
6
+ required_scopes:
7
+ - jira:issue:read
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ required: [issue_key]
13
+ properties:
14
+ issue_key:
15
+ type: string
16
+ description: e.g. PROJ-123
17
+ output_schema:
18
+ type: object
@@ -0,0 +1,35 @@
1
+ command: k8s.deployment.scale
2
+ version: "1.0.0"
3
+ description: Scale a deployment to N replicas. Mutating; scaling to 0 takes a service down.
4
+ category: kubernetes
5
+ adapter: k8s
6
+ required_scopes:
7
+ - k8s:deployments:write
8
+ side_effects: write
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ namespace:
14
+ type: string
15
+ description: Kubernetes namespace (DNS-1123)
16
+ deployment:
17
+ type: string
18
+ description: Deployment name (DNS-1123)
19
+ replicas:
20
+ type: integer
21
+ description: Desired replica count (>= 0)
22
+ dry_run:
23
+ type: boolean
24
+ description: Validate server-side without scaling (kubectl --dry-run=server)
25
+ required:
26
+ - deployment
27
+ - replicas
28
+ output_schema:
29
+ type: object
30
+ properties:
31
+ scaled: { type: boolean }
32
+ deployment: { type: string }
33
+ namespace: { type: string }
34
+ replicas: { type: integer }
35
+ dry_run: { type: boolean }
@@ -0,0 +1,30 @@
1
+ command: k8s.pod.delete
2
+ version: "1.0.0"
3
+ description: Delete a pod in a namespace. Destructive; requires a capability with a destructive ceiling.
4
+ category: kubernetes
5
+ adapter: k8s
6
+ required_scopes:
7
+ - k8s:pods:write
8
+ side_effects: destructive
9
+ idempotent: false
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ namespace:
14
+ type: string
15
+ description: Kubernetes namespace (DNS-1123)
16
+ pod:
17
+ type: string
18
+ description: Pod name to delete (DNS-1123)
19
+ dry_run:
20
+ type: boolean
21
+ description: Validate server-side without deleting (kubectl --dry-run=server)
22
+ required:
23
+ - pod
24
+ output_schema:
25
+ type: object
26
+ properties:
27
+ deleted: { type: boolean }
28
+ pod: { type: string }
29
+ namespace: { type: string }
30
+ dry_run: { type: boolean }
@@ -0,0 +1,20 @@
1
+ command: kubectl.get.pods
2
+ version: "1.0.0"
3
+ description: List pods in namespace (mock unless kubectl configured)
4
+ category: kubernetes
5
+ adapter: mock
6
+ required_scopes:
7
+ - k8s:pods:read
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ namespace:
14
+ type: string
15
+ default: default
16
+ output_schema:
17
+ type: object
18
+ properties:
19
+ items:
20
+ type: array
@@ -0,0 +1,29 @@
1
+ command: mcp.github.list_pulls
2
+ version: "1.0.0"
3
+ description: List pull requests via GitHub MCP server (governed bridge)
4
+ category: mcp
5
+ adapter: mcp
6
+ required_scopes:
7
+ - github:pull_request:read
8
+ side_effects: read
9
+ idempotent: true
10
+ mcp:
11
+ server_command: npx
12
+ server_args:
13
+ - "-y"
14
+ - "@modelcontextprotocol/server-github"
15
+ tool_name: list_pull_requests
16
+ timeout: 90
17
+ input_schema:
18
+ type: object
19
+ required: [repo]
20
+ properties:
21
+ repo:
22
+ type: string
23
+ description: owner/name
24
+ state:
25
+ type: string
26
+ enum: [open, closed, all]
27
+ default: open
28
+ output_schema:
29
+ type: object
@@ -0,0 +1,19 @@
1
+ command: pet.findpetsbystatus
2
+ version: 1.0.0
3
+ description: Find pets by status
4
+ category: pet
5
+ adapter: mock
6
+ required_scopes:
7
+ - pet:read
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ status:
14
+ type: string
15
+ description: ''
16
+ required:
17
+ - status
18
+ output_schema:
19
+ type: object
@@ -0,0 +1,19 @@
1
+ command: pet.getpetbyid
2
+ version: 1.0.0
3
+ description: Get pet by ID
4
+ category: pet
5
+ adapter: mock
6
+ required_scopes:
7
+ - pet:read
8
+ side_effects: read
9
+ idempotent: true
10
+ input_schema:
11
+ type: object
12
+ properties:
13
+ petId:
14
+ type: string
15
+ description: ''
16
+ required:
17
+ - petId
18
+ output_schema:
19
+ type: object
gax/mcp_client.py ADDED
@@ -0,0 +1,110 @@
1
+ """MCP JSON-RPC over stdio — shared by gaxd MCP adapter and eval."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ import os
7
+ import subprocess
8
+ import time
9
+ from typing import Any
10
+
11
+
12
+ class McpStdioClient:
13
+ def __init__(
14
+ self,
15
+ server_cmd: list[str],
16
+ *,
17
+ env: dict[str, str] | None = None,
18
+ timeout: float = 60.0,
19
+ ) -> None:
20
+ self._timeout = timeout
21
+ self._id = 0
22
+ proc_env = {**os.environ, **(env or {})}
23
+ self._proc = subprocess.Popen(
24
+ server_cmd,
25
+ stdin=subprocess.PIPE,
26
+ stdout=subprocess.PIPE,
27
+ stderr=subprocess.DEVNULL,
28
+ text=True,
29
+ env=proc_env,
30
+ )
31
+ assert self._proc.stdin and self._proc.stdout
32
+ self._initialize()
33
+
34
+ def _next_id(self) -> int:
35
+ self._id += 1
36
+ return self._id
37
+
38
+ def _read_message(self) -> dict[str, Any] | None:
39
+ line = self._proc.stdout.readline() # type: ignore[union-attr]
40
+ if not line:
41
+ return None
42
+ line = line.strip()
43
+ if not line:
44
+ return self._read_message()
45
+ return json.loads(line)
46
+
47
+ def _request(self, method: str, params: dict[str, Any] | None = None) -> dict[str, Any]:
48
+ rid = self._next_id()
49
+ req: dict[str, Any] = {"jsonrpc": "2.0", "id": rid, "method": method}
50
+ if params is not None:
51
+ req["params"] = params
52
+ assert self._proc.stdin
53
+ self._proc.stdin.write(json.dumps(req) + "\n")
54
+ self._proc.stdin.flush()
55
+ deadline = time.time() + self._timeout
56
+ while time.time() < deadline:
57
+ msg = self._read_message()
58
+ if msg is None:
59
+ break
60
+ if msg.get("id") == rid:
61
+ if "error" in msg:
62
+ raise RuntimeError(json.dumps(msg["error"]))
63
+ return msg.get("result") or {}
64
+ raise TimeoutError(f"MCP request timed out: {method}")
65
+
66
+ def _initialize(self) -> None:
67
+ self._request(
68
+ "initialize",
69
+ {
70
+ "protocolVersion": "2024-11-05",
71
+ "capabilities": {},
72
+ "clientInfo": {"name": "gax", "version": "0.4"},
73
+ },
74
+ )
75
+ assert self._proc.stdin
76
+ self._proc.stdin.write(
77
+ json.dumps({"jsonrpc": "2.0", "method": "notifications/initialized"}) + "\n"
78
+ )
79
+ self._proc.stdin.flush()
80
+
81
+ def list_tools(self) -> list[dict[str, Any]]:
82
+ result = self._request("tools/list", {})
83
+ return list(result.get("tools") or [])
84
+
85
+ def call_tool(self, name: str, arguments: dict[str, Any]) -> Any:
86
+ result = self._request("tools/call", {"name": name, "arguments": arguments})
87
+ content = result.get("content") or []
88
+ texts = [c.get("text", "") for c in content if c.get("type") == "text"]
89
+ if len(texts) == 1:
90
+ try:
91
+ return json.loads(texts[0])
92
+ except json.JSONDecodeError:
93
+ return {"text": texts[0]}
94
+ if texts:
95
+ return {"texts": texts}
96
+ return result
97
+
98
+ def close(self) -> None:
99
+ try:
100
+ self._proc.terminate()
101
+ self._proc.wait(timeout=5)
102
+ except Exception:
103
+ self._proc.kill()
104
+
105
+
106
+ def github_mcp_env() -> dict[str, str]:
107
+ tok = os.environ.get("GITHUB_PERSONAL_ACCESS_TOKEN") or os.environ.get("GITHUB_TOKEN")
108
+ if not tok:
109
+ raise ValueError("GITHUB_TOKEN or GITHUB_PERSONAL_ACCESS_TOKEN required for MCP GitHub server")
110
+ return {"GITHUB_PERSONAL_ACCESS_TOKEN": tok}