general-augment-cli 0.1.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 (42) hide show
  1. general_augment_cli-0.1.0.dist-info/METADATA +180 -0
  2. general_augment_cli-0.1.0.dist-info/RECORD +42 -0
  3. general_augment_cli-0.1.0.dist-info/WHEEL +4 -0
  4. general_augment_cli-0.1.0.dist-info/entry_points.txt +2 -0
  5. platform_cli/__init__.py +5 -0
  6. platform_cli/branding.py +27 -0
  7. platform_cli/client.py +179 -0
  8. platform_cli/commands/__init__.py +1 -0
  9. platform_cli/commands/approvals.py +150 -0
  10. platform_cli/commands/auth.py +96 -0
  11. platform_cli/commands/billing.py +143 -0
  12. platform_cli/commands/channels.py +212 -0
  13. platform_cli/commands/deploy.py +72 -0
  14. platform_cli/commands/dev.py +38 -0
  15. platform_cli/commands/doctor.py +170 -0
  16. platform_cli/commands/identity.py +433 -0
  17. platform_cli/commands/init.py +55 -0
  18. platform_cli/commands/integrate.py +94 -0
  19. platform_cli/commands/keys.py +116 -0
  20. platform_cli/commands/logs.py +43 -0
  21. platform_cli/commands/mcp.py +258 -0
  22. platform_cli/commands/memory.py +316 -0
  23. platform_cli/commands/mock.py +30 -0
  24. platform_cli/commands/model_providers.py +226 -0
  25. platform_cli/commands/observability.py +174 -0
  26. platform_cli/commands/onboarding.py +72 -0
  27. platform_cli/commands/projects.py +302 -0
  28. platform_cli/commands/skills.py +116 -0
  29. platform_cli/commands/smoke.py +280 -0
  30. platform_cli/commands/status.py +49 -0
  31. platform_cli/commands/tools.py +179 -0
  32. platform_cli/commands/users.py +150 -0
  33. platform_cli/commands/validate.py +96 -0
  34. platform_cli/commands/verify.py +648 -0
  35. platform_cli/config.py +114 -0
  36. platform_cli/errors.py +103 -0
  37. platform_cli/local_mock.py +1392 -0
  38. platform_cli/main.py +130 -0
  39. platform_cli/openapi.py +1048 -0
  40. platform_cli/output.py +47 -0
  41. platform_cli/readiness.py +176 -0
  42. platform_cli/runtime.py +22 -0
@@ -0,0 +1,180 @@
1
+ Metadata-Version: 2.4
2
+ Name: general-augment-cli
3
+ Version: 0.1.0
4
+ Summary: Standalone CLI for General Augment.
5
+ Author: General Augment
6
+ License-Expression: MIT
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: httpx>=0.27.0
9
+ Requires-Dist: pydantic>=2.7.0
10
+ Requires-Dist: pyyaml>=6.0.0
11
+ Requires-Dist: rich>=13.7.0
12
+ Requires-Dist: typer[all]>=0.12.0
13
+ Provides-Extra: dev
14
+ Requires-Dist: pytest-asyncio>=0.23.0; extra == 'dev'
15
+ Requires-Dist: pytest>=8.0.0; extra == 'dev'
16
+ Description-Content-Type: text/markdown
17
+
18
+ # General Augment CLI
19
+
20
+ Standalone developer CLI for General Augment.
21
+
22
+ During private beta, use the repo-local command prefix unless the published package is
23
+ available in your Python package index:
24
+
25
+ ```bash
26
+ uv run --project packages/cli genaug --version
27
+ uv run --project packages/cli genaug --help
28
+ ```
29
+
30
+ When the package is available, the same commands work through the installed `genaug`
31
+ entrypoint:
32
+
33
+ ```bash
34
+ pip install general-augment-cli
35
+ genaug --version
36
+ genaug auth login --api-key gaadmlive...
37
+ genaug doctor
38
+ genaug projects list
39
+ genaug init dayplan-agent --tool web_search
40
+ genaug integrate https://petstore3.swagger.io/api/v3/openapi.json
41
+ genaug validate ./petstore-agent/genaug-agent.yaml
42
+ genaug deploy ./petstore-agent/genaug-agent.yaml
43
+ genaug keys create --project petstore-agent --name "Production backend"
44
+ genaug mock --host 127.0.0.1 --port 8787 --quiet
45
+ genaug smoke --idempotency-key smoke-replay-1 --metadata feature=spark
46
+ genaug smoke --project petstore-agent
47
+ genaug verify --project petstore-agent
48
+ genaug onboarding verify --project petstore-agent --json
49
+ ```
50
+
51
+ If the package install fails during private beta, keep using
52
+ `uv run --project packages/cli genaug ...` from the General Augment repository instead
53
+ of assuming the package has shipped to your package index.
54
+
55
+ `genaug auth login` verifies the key against `/api/v1/admin/me` before writing local
56
+ config. The CLI stores auth at `~/.genaug/config.yaml` by default with owner-only file
57
+ permissions. Set
58
+ `GENAUG_CLI_CONFIG` to use a custom path.
59
+
60
+ Preferred environment overrides:
61
+
62
+ ```bash
63
+ export GENAUG_ADMIN_API_KEY=gaadmlive...
64
+ export GENAUG_ADMIN_BASE_URL=https://api.generalaugment.com
65
+ ```
66
+
67
+ `GENAUG_API_KEY` and `GENAUG_API_BASE_URL` are also accepted, which keeps local
68
+ mock and SDK test scripts easy to share.
69
+
70
+ The generated manifest is `genaug-agent.yaml`. Use `genaug init <name>` when you want
71
+ a starter agent before an OpenAPI spec exists. Use
72
+ `genaug integrate <openapi-spec> --auto-deploy` when you want the CLI to create or
73
+ update the project and register the generated OpenAPI tools in one pass. Without
74
+ `--auto-deploy`, review the scaffold first, then run
75
+ `genaug validate ./<agent>/genaug-agent.yaml` and
76
+ `genaug deploy ./<agent>/genaug-agent.yaml`. `deploy` runs the same local validation
77
+ before calling the hosted API. Both scaffolds include
78
+ `CODING_AGENT_PROMPT.md`, which is the paste-ready backend handoff for a coding agent.
79
+
80
+ ## Common workflows
81
+
82
+ - Auth: `genaug auth login`, `genaug auth whoami`, `genaug auth logout`
83
+ - Starter scaffold: `genaug init <name> --tool web_search`
84
+ - Local config validation: `genaug validate ./genaug-agent.yaml --json`
85
+ - Projects: `genaug projects list`, `genaug projects create`, `genaug projects usage`,
86
+ `genaug projects runtime-policy`, `genaug projects export`, `genaug projects archive`
87
+ - API keys: `genaug keys create`, `genaug keys list`, `genaug keys update`,
88
+ `genaug keys revoke`
89
+ - Skills, tools, and channels: `genaug skills list`, `genaug skills view`,
90
+ `genaug skills apply`, `genaug skills delete`, `genaug tools list`, `genaug tools toggle`,
91
+ `genaug tools discovery`, `genaug channels status`, `genaug channels connect`,
92
+ `genaug channels test`, `genaug channels disconnect`. Telegram supports connect,
93
+ test, and disconnect; WhatsApp/SMS support sender configuration and clearing.
94
+ - MCP servers: `genaug mcp list`, `genaug mcp add`, `genaug mcp test`,
95
+ `genaug mcp delete`. Use exactly one transport per server: `--url` for HTTP
96
+ endpoints or `--command` for stdio servers.
97
+ - Model providers: `genaug model-providers list`, `genaug model-providers set`,
98
+ `genaug model-providers health`, `genaug model-providers revoke`
99
+ - Billing: `genaug billing checkout`, `genaug billing portal`,
100
+ `genaug billing events`
101
+ - Memory: `genaug memory store`, `genaug memory search`, `genaug memory profile`,
102
+ `genaug memory delete`, `genaug memory purge-user`
103
+ - Users and identity: `genaug users list`, `genaug users detail`,
104
+ `genaug users delete`, `genaug identity list`, `genaug identity create-test`,
105
+ `genaug identity link-user`, `genaug identity verification-code`,
106
+ `genaug identity magic-link`, `genaug identity verify`,
107
+ `genaug identity resolve`, `genaug identity unlink`
108
+ - Observability: `genaug observability trace`, `genaug observability support-bundle`
109
+ - Approvals: `genaug approvals list`, `genaug approvals approve`, `genaug approvals deny`
110
+ - Operations: `genaug doctor`, `genaug status`, `genaug logs`
111
+ - App smoke checks: `genaug smoke --message "Reply exactly with: ok"`,
112
+ `genaug smoke --structured`, `genaug smoke --json`
113
+ - Project acceptance checks: `genaug verify --project <project-slug>`, which checks
114
+ project keys, hosted agent test, tools, logs, usage, usage limits, observability,
115
+ runtime policy model routing, memory lifecycle, and tool-call audit before printing
116
+ dashboard URLs for the same tenant.
117
+ - One-command onboarding gate: `genaug onboarding verify --project <project-slug> --json`,
118
+ which wraps the same project checks with CLI/API version metadata and a coding-agent
119
+ friendly ready/blocked payload.
120
+ - Local development: `genaug dev ./genaug-agent.yaml --message "Hello"`
121
+ - Local mock testing: `genaug mock --host 127.0.0.1 --port 8787 --quiet`
122
+
123
+ ## Billing
124
+
125
+ ```bash
126
+ genaug billing checkout --project dayplan-agent --tier pro
127
+ genaug billing portal --project dayplan-agent
128
+ genaug billing events --project dayplan-agent --json
129
+ ```
130
+
131
+ Use these commands for hosted billing actions when Stripe is configured for the
132
+ project. `checkout` returns a hosted Stripe Checkout URL for Pro or Team, `portal`
133
+ returns a hosted Stripe Customer Portal URL for linked customers, and `events` lists
134
+ stored Stripe webhook events such as checkout completion, invoice payment, and payment
135
+ failure. These commands do not create Stripe products, prices, or webhooks directly;
136
+ those stay in the server-side billing setup and readiness flow.
137
+
138
+ For a full CLI-to-dashboard onboarding proof from this repository, run:
139
+
140
+ ```bash
141
+ make app-developer-onboarding-smoke
142
+ ```
143
+
144
+ That harness creates a fresh dummy tenant from a richer OpenAPI fixture, proves
145
+ generated tool governance, deploys SOUL.md and skills, sends real `/v1/responses`
146
+ smokes for multiple app users, runs `genaug verify --json`, starts an owned dashboard
147
+ dev server, and writes JSON evidence plus screenshots for the project overview, skills,
148
+ tools, integrate, and analytics pages. Add `GENAUG_DASHBOARD_SMOKE_ARCHIVE_PROJECT=1`
149
+ when CI should archive the created smoke project after the artifact is captured.
150
+
151
+ `genaug smoke` checks `/health/ready` and sends one project-keyed `/v1/responses`
152
+ request using bearer auth. Use `--idempotency-key`, `--request-id`, `--traceparent`,
153
+ and repeated `--metadata key=value` flags when you need replayable support/debug
154
+ evidence from hosted API or the local mock. Use `--project <project-slug>` when the
155
+ configured key is a management key and the app-facing request needs `X-Project-ID`.
156
+ Use `--structured` to request the default `json_schema` smoke response, or
157
+ `--schema-file ./schema.json` to verify an app-specific structured-output contract.
158
+ With `--json`, smoke output includes the `/health/ready` payload, the full
159
+ `/v1/responses` object, `response_id`, `request_id`, and `trace_id` so automation can
160
+ prove health and tracing from one command.
161
+
162
+ When the API returns a rate-limit `429`, the CLI prints the stable reason and
163
+ `Retry-After` timing when the platform includes them.
164
+
165
+ `genaug doctor` checks the resolved config path, base URL, API key presence,
166
+ `/health/ready`, and `/api/v1/admin/me` without printing secret values. Run it before
167
+ `integrate` when a new developer is unsure whether local auth or network setup is the
168
+ problem.
169
+
170
+ `genaug mock` runs the deterministic local HTTP mock. Use it for offline app contract tests against
171
+ `/v1/responses`, memory routes, project setup, OpenAPI tool registration, key
172
+ management, logs, usage, observability, health checks, idempotency replays, trace
173
+ metadata, structured-output fixtures, and semantic SSE fixtures.
174
+
175
+ The console commands are defined in `pyproject.toml`:
176
+
177
+ ```toml
178
+ [project.scripts]
179
+ genaug = "platform_cli.main:app"
180
+ ```
@@ -0,0 +1,42 @@
1
+ platform_cli/__init__.py,sha256=DBLEXhxNCZ-Ktmkv1pHf96Gxdju6CudLAXko3tNUNlc,103
2
+ platform_cli/branding.py,sha256=bIf4iFjfhqdZCyi8SLrj7ntfh7ntd5lx-WT822sRzUM,808
3
+ platform_cli/client.py,sha256=lrI5ZQhsbvf40J9cLbKDF_pdED4UzEbXNTjxlkEaxPk,5774
4
+ platform_cli/config.py,sha256=vrS5EQf3ga_V5ogHR-QjIzrJF9haNGbCf5VYuG6tUcM,3329
5
+ platform_cli/errors.py,sha256=winlh1baNtsGOITbUVLi6_4qsPD0waBHhHMQ5LACDSM,3377
6
+ platform_cli/local_mock.py,sha256=cwb-sj59kdc16sBIbbjUWl9Y0bAtpniW_grjxd4p-eU,56202
7
+ platform_cli/main.py,sha256=NNwCV0WQTuTnc5WnxaNRsuPKFjJk-dq7rjgKW42GMiM,3717
8
+ platform_cli/openapi.py,sha256=WTDc7_kXU2QSEvl4gUfVnhM7DL2okBqhq1pHT-miGHw,38949
9
+ platform_cli/output.py,sha256=7Cu_oXlAaC-C2-zoU5DHqwYPaSwkH7cASo-ZQZlUgdM,1229
10
+ platform_cli/readiness.py,sha256=6upaOGnmpQrY9OadEgvkaOZBxkM0cWGkCZyTRZdOZuQ,5505
11
+ platform_cli/runtime.py,sha256=sacxB7xcX1OqIG1XpbT4AGwZorcGBRCopqaPs9pQK-4,566
12
+ platform_cli/commands/__init__.py,sha256=A8DLS6s7JOgyk1tKlSPcVH1JOS1WCon1VWyG5kXjcKs,52
13
+ platform_cli/commands/approvals.py,sha256=tUfYBtOhuH-mG2yLlopKE8Cu0O2zHCZS_KWhxVe-Ptw,4724
14
+ platform_cli/commands/auth.py,sha256=8F8zEngqU9OCtSPaK1OKSXnQ6l8PQ6JjXReuqCtwvCc,3174
15
+ platform_cli/commands/billing.py,sha256=pf7YM7B1VDN4MoQTzKeKKS6tcZWr8TpRoWy_eExdNc4,4656
16
+ platform_cli/commands/channels.py,sha256=tw3AdUeheJvluoExQ5VCxTG_Q-TLVxnEW9cjgMb2yfY,8117
17
+ platform_cli/commands/deploy.py,sha256=toQfSh3oSbjXAi6UtG0Ptg5xZgsAs5QmtHwvqUTywl8,2363
18
+ platform_cli/commands/dev.py,sha256=NDZ09zqwHkpO2XrF7tc9z1RBVUx2QC9jROX7Vd5Xf2w,1134
19
+ platform_cli/commands/doctor.py,sha256=DCuRA4QEm_eDvSdm84vV2iSzNsBngVEuT9OjX4fJ-nc,5414
20
+ platform_cli/commands/identity.py,sha256=FpjAVxPU4_C9UNkh5V8uCgIiAHXG2fxX9dIdFjA0i2U,14636
21
+ platform_cli/commands/init.py,sha256=AN5MtGnqyxi16DD6gWigN3ZR4t4iTreSKQ6JwBqiwYo,2145
22
+ platform_cli/commands/integrate.py,sha256=RMEFEEsSd6oDFob2jOzlD_Fg6loBI2fDy0SzBI42HUc,3515
23
+ platform_cli/commands/keys.py,sha256=f1aS4oDx41MEeYXZYGebPoP4-cMpHBifCmwFrB0Tdf8,4101
24
+ platform_cli/commands/logs.py,sha256=rpaMIEFLWRHaFqZeoH6Ea371kXdeqrB2hXL5rNajKj0,1415
25
+ platform_cli/commands/mcp.py,sha256=2JULbTBiPqNZPSV0Szl6r4lxAnA0Q9l5WDKDCQKicVU,8759
26
+ platform_cli/commands/memory.py,sha256=vVh8Mi5NQn5q7pCZMnTQR-LfWV8grEftiMYThbmVZMA,10644
27
+ platform_cli/commands/mock.py,sha256=yo27qm8siuF6ij-dX27MmDGjR4o1HT_TVMNf5F8--oA,860
28
+ platform_cli/commands/model_providers.py,sha256=gSvz3eHvF73Y73dtLgXJPkWlMTGNm26rH09_R4vkwdM,8006
29
+ platform_cli/commands/observability.py,sha256=OzChmay-LzzKbGiHmUR4uxW6w2biKUiwFddp-7oC9_w,5868
30
+ platform_cli/commands/onboarding.py,sha256=5cVn2A_me19SWhIMVqtVV6HX1t-e29S6-Uuj1tTa_1I,2735
31
+ platform_cli/commands/projects.py,sha256=xAk83RQ-l3fzZTT7M2xiBz4LFLFNdBjMsLfFptuueFg,10874
32
+ platform_cli/commands/skills.py,sha256=YTzrAMecdYZL8eZJX0zEmXSbq_HaQ97S6xWGtHYUN6Q,3900
33
+ platform_cli/commands/smoke.py,sha256=C8XGfG_UZ6huOuaIfbZuHDF_vG-Te_Xj49q9Zgiufhw,9521
34
+ platform_cli/commands/status.py,sha256=Cgce1sM_Qesajb_0iihoPq7pj6OqldRn-CLS_Ojhcdg,1893
35
+ platform_cli/commands/tools.py,sha256=Oel0Zhc0HDJMXYMhqLtg4Mw4PGCu9D68_HLGK2yY7rc,6184
36
+ platform_cli/commands/users.py,sha256=fJtulN-If1ofAMnOUFMkv6blnc2e_KIOilMaoovV0oE,5191
37
+ platform_cli/commands/validate.py,sha256=OcRz5ciUX8B_Dh-K6PDryc3MnyoJNBJgt8g3TCp555s,3247
38
+ platform_cli/commands/verify.py,sha256=q1a8Jxb_RzXy4JG3o1SO14trkQB30co8vvBk_OsbE5U,21798
39
+ general_augment_cli-0.1.0.dist-info/METADATA,sha256=Qupg4x7qEgZUvG3txFkYIHk8XutJ4D5J_dlIxifjRcU,8629
40
+ general_augment_cli-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
41
+ general_augment_cli-0.1.0.dist-info/entry_points.txt,sha256=gIrBMQrsprNFXJjV_Xo6afAg7HILdJ611BoJnoAiAVU,49
42
+ general_augment_cli-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ genaug = platform_cli.main:app
@@ -0,0 +1,5 @@
1
+ """Standalone CLI package for the agent platform."""
2
+
3
+ __all__ = ["__version__"]
4
+
5
+ __version__ = "0.1.0"
@@ -0,0 +1,27 @@
1
+ """Small branding model for standalone CLI copy."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class Branding(BaseModel):
11
+ """User-facing CLI branding loaded from environment variables."""
12
+
13
+ product_name: str = Field(
14
+ default_factory=lambda: os.getenv("BRAND_PRODUCT_NAME", "General Augment"),
15
+ )
16
+ product_slug: str = Field(
17
+ default_factory=lambda: os.getenv("BRAND_PRODUCT_SLUG", "general-augment"),
18
+ )
19
+ docs_url: str = Field(
20
+ default_factory=lambda: os.getenv("BRAND_DOCS_URL", "https://docs.generalaugment.com"),
21
+ )
22
+ api_key_prefix: str = Field(default_factory=lambda: os.getenv("BRAND_API_KEY_PREFIX", "gaadm"))
23
+
24
+
25
+ def get_branding() -> Branding:
26
+ """Return current process branding."""
27
+ return Branding()
platform_cli/client.py ADDED
@@ -0,0 +1,179 @@
1
+ """Thin HTTP client for the standalone CLI."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from collections.abc import Mapping
6
+ from typing import Any
7
+ from urllib.parse import quote
8
+
9
+ import httpx
10
+
11
+ from platform_cli.config import CLIConfig
12
+ from platform_cli.errors import APIError, CLIError
13
+
14
+ ADMIN_PREFIX = "/api/v1/admin"
15
+ INTEGRATIONS_PREFIX = "/api/v1/integrations"
16
+ REQUEST_TIMEOUT_SECONDS = 30.0
17
+
18
+
19
+ class PlatformClient:
20
+ """Synchronous HTTP client for platform admin and public endpoints."""
21
+
22
+ def __init__(self, config: CLIConfig, *, timeout: float = REQUEST_TIMEOUT_SECONDS) -> None:
23
+ """Initialize the client from CLI config."""
24
+ self.config = config
25
+ self.base_url = config.base_url.rstrip("/")
26
+ self.timeout = timeout
27
+ self._client = httpx.Client(timeout=timeout)
28
+
29
+ def close(self) -> None:
30
+ """Close the underlying HTTP client."""
31
+ self._client.close()
32
+
33
+ def __enter__(self) -> PlatformClient:
34
+ """Return this client in context managers."""
35
+ return self
36
+
37
+ def __exit__(self, *_: object) -> None:
38
+ """Close this client."""
39
+ self.close()
40
+
41
+ def admin(
42
+ self,
43
+ method: str,
44
+ path: str,
45
+ *,
46
+ json: Mapping[str, Any] | None = None,
47
+ params: Mapping[str, Any] | None = None,
48
+ ) -> Any:
49
+ """Call an admin API endpoint."""
50
+ if not self.config.api_key:
51
+ raise CLIError("No API key configured. Run genaug auth login first.")
52
+ return self._request(
53
+ method,
54
+ f"{ADMIN_PREFIX}{path}",
55
+ json=json,
56
+ params=params,
57
+ authenticated=True,
58
+ )
59
+
60
+ def public(
61
+ self,
62
+ method: str,
63
+ path: str,
64
+ *,
65
+ params: Mapping[str, Any] | None = None,
66
+ ) -> Any:
67
+ """Call a public API endpoint."""
68
+ return self._request(method, path, params=params, authenticated=False)
69
+
70
+ def integrations(
71
+ self,
72
+ method: str,
73
+ path: str,
74
+ *,
75
+ json: Mapping[str, Any] | None = None,
76
+ params: Mapping[str, Any] | None = None,
77
+ ) -> Any:
78
+ """Call an app-integration endpoint with admin API-key auth."""
79
+ if not self.config.api_key:
80
+ raise CLIError("No API key configured. Run genaug auth login first.")
81
+ return self._request(
82
+ method,
83
+ f"{INTEGRATIONS_PREFIX}{path}",
84
+ json=json,
85
+ params=params,
86
+ authenticated=True,
87
+ )
88
+
89
+ def app(
90
+ self,
91
+ method: str,
92
+ path: str,
93
+ *,
94
+ json: Mapping[str, Any] | None = None,
95
+ params: Mapping[str, Any] | None = None,
96
+ headers: Mapping[str, str] | None = None,
97
+ ) -> Any:
98
+ """Call an app-facing endpoint using bearer auth."""
99
+ if not self.config.api_key:
100
+ raise CLIError("No API key configured. Run genaug auth login first.")
101
+ return self._request(
102
+ method,
103
+ path,
104
+ json=json,
105
+ params=params,
106
+ extra_headers=headers,
107
+ authenticated=True,
108
+ auth_mode="bearer",
109
+ )
110
+
111
+ def _request(
112
+ self,
113
+ method: str,
114
+ path: str,
115
+ *,
116
+ json: Mapping[str, Any] | None = None,
117
+ params: Mapping[str, Any] | None = None,
118
+ extra_headers: Mapping[str, str] | None = None,
119
+ authenticated: bool,
120
+ auth_mode: str = "admin",
121
+ ) -> Any:
122
+ """Send one request and decode the response."""
123
+ headers: dict[str, str] = {}
124
+ if authenticated and self.config.api_key:
125
+ if auth_mode == "bearer":
126
+ headers["Authorization"] = f"Bearer {self.config.api_key}"
127
+ else:
128
+ headers["X-Admin-Key"] = self.config.api_key
129
+ headers.update(dict(extra_headers or {}))
130
+ try:
131
+ response = self._client.request(
132
+ method,
133
+ f"{self.base_url}{path}",
134
+ headers=headers,
135
+ json=dict(json) if json is not None else None,
136
+ params=dict(params) if params is not None else None,
137
+ )
138
+ except (httpx.ConnectError, httpx.TimeoutException, httpx.RequestError) as exc:
139
+ raise CLIError(f"Could not reach the platform API at {self.base_url}: {exc}") from exc
140
+ if response.status_code >= 400:
141
+ raise APIError(response.status_code, _error_detail(response), response.headers)
142
+ if not response.content:
143
+ return None
144
+ try:
145
+ return response.json()
146
+ except ValueError as exc:
147
+ if authenticated:
148
+ raise CLIError(
149
+ "Platform API returned malformed JSON for an authenticated request."
150
+ ) from exc
151
+ return response.text
152
+
153
+
154
+ def resolve_project(client: PlatformClient, project_ref: str) -> dict[str, Any]:
155
+ """Resolve a project by id, slug, or name."""
156
+ payload = client.admin("GET", "/projects")
157
+ items = payload.get("items", []) if isinstance(payload, dict) else []
158
+ for item in items:
159
+ if not isinstance(item, dict):
160
+ continue
161
+ if project_ref in {str(item.get("id")), str(item.get("slug")), str(item.get("name"))}:
162
+ return item
163
+ raise CLIError(f"Project not found: {project_ref}")
164
+
165
+
166
+ def encode_path_segment(value: str) -> str:
167
+ """Encode one path segment for safe URL interpolation."""
168
+ return quote(value, safe="")
169
+
170
+
171
+ def _error_detail(response: httpx.Response) -> Any:
172
+ """Extract one error detail from an HTTP response."""
173
+ try:
174
+ payload = response.json()
175
+ except ValueError:
176
+ return response.text
177
+ if isinstance(payload, dict):
178
+ return payload.get("detail") or payload
179
+ return payload
@@ -0,0 +1 @@
1
+ """Typer command modules for the standalone CLI."""
@@ -0,0 +1,150 @@
1
+ """Approval queue management commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from platform_cli.client import encode_path_segment, resolve_project
10
+ from platform_cli.output import print_json, print_success, table
11
+ from platform_cli.runtime import Runtime
12
+
13
+ app = typer.Typer(help="Manage governed tool approvals.")
14
+ APPROVAL_STATUSES = {"pending", "all"}
15
+
16
+
17
+ @app.command("list")
18
+ def list_approvals(
19
+ ctx: typer.Context,
20
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
21
+ status: Annotated[
22
+ str,
23
+ typer.Option(help="Approval rows to list: pending or all."),
24
+ ] = "pending",
25
+ json_output: Annotated[
26
+ bool,
27
+ typer.Option("--json", help="Print machine-readable JSON."),
28
+ ] = False,
29
+ ) -> None:
30
+ """List approval rows for one project."""
31
+ normalized_status = _approval_status(status)
32
+ runtime: Runtime = ctx.obj
33
+ with runtime.client() as client:
34
+ project_payload = resolve_project(client, project)
35
+ response = client.admin(
36
+ "GET",
37
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/approvals",
38
+ params={"status": normalized_status},
39
+ )
40
+ if json_output:
41
+ print_json(response)
42
+ return
43
+ items = response.get("items", []) if isinstance(response, dict) else []
44
+ rows = [_approval_row(item) for item in items if isinstance(item, dict)]
45
+ table(
46
+ "Approvals",
47
+ ["Approval ID", "Tool", "Status", "Action", "Channel", "Expires At"],
48
+ rows,
49
+ )
50
+
51
+
52
+ @app.command("approve")
53
+ def approve_approval(
54
+ ctx: typer.Context,
55
+ approval_id: Annotated[str, typer.Argument(help="Approval id to approve.")],
56
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
57
+ yes: Annotated[
58
+ bool,
59
+ typer.Option("--yes", help="Confirm approving and resuming this governed action."),
60
+ ] = False,
61
+ json_output: Annotated[
62
+ bool,
63
+ typer.Option("--json", help="Print machine-readable JSON."),
64
+ ] = False,
65
+ ) -> None:
66
+ """Approve one pending governed tool action."""
67
+ _resolve_approval(
68
+ ctx,
69
+ approval_id,
70
+ project=project,
71
+ action="approve",
72
+ yes=yes,
73
+ json_output=json_output,
74
+ )
75
+
76
+
77
+ @app.command("deny")
78
+ def deny_approval(
79
+ ctx: typer.Context,
80
+ approval_id: Annotated[str, typer.Argument(help="Approval id to deny.")],
81
+ project: Annotated[str, typer.Option(help="Project id, slug, or name.")],
82
+ yes: Annotated[
83
+ bool,
84
+ typer.Option("--yes", help="Confirm denying and resuming this governed action."),
85
+ ] = False,
86
+ json_output: Annotated[
87
+ bool,
88
+ typer.Option("--json", help="Print machine-readable JSON."),
89
+ ] = False,
90
+ ) -> None:
91
+ """Deny one pending governed tool action."""
92
+ _resolve_approval(
93
+ ctx,
94
+ approval_id,
95
+ project=project,
96
+ action="deny",
97
+ yes=yes,
98
+ json_output=json_output,
99
+ )
100
+
101
+
102
+ def _resolve_approval(
103
+ ctx: typer.Context,
104
+ approval_id: str,
105
+ *,
106
+ project: str,
107
+ action: str,
108
+ yes: bool,
109
+ json_output: bool,
110
+ ) -> None:
111
+ """Approve or deny one approval row."""
112
+ if not yes and not typer.confirm(f"{action.title()} approval {approval_id}?"):
113
+ raise typer.Exit(1)
114
+ runtime: Runtime = ctx.obj
115
+ with runtime.client() as client:
116
+ project_payload = resolve_project(client, project)
117
+ response = client.admin(
118
+ "POST",
119
+ (
120
+ f"/projects/{encode_path_segment(str(project_payload['id']))}/approvals/"
121
+ f"{encode_path_segment(approval_id)}/{action}"
122
+ ),
123
+ )
124
+ if json_output:
125
+ print_json(response)
126
+ return
127
+ approval = response.get("approval", {}) if isinstance(response, dict) else {}
128
+ enqueued = response.get("enqueued", False) if isinstance(response, dict) else False
129
+ status = approval.get("status", action + "d") if isinstance(approval, dict) else action + "d"
130
+ print_success(f"Approval {approval_id} {status}; enqueued={enqueued}.")
131
+
132
+
133
+ def _approval_row(approval: dict[str, object]) -> list[object]:
134
+ """Return a compact approval row."""
135
+ return [
136
+ approval.get("approval_id", ""),
137
+ approval.get("tool_id", ""),
138
+ approval.get("status", ""),
139
+ approval.get("action_summary", ""),
140
+ approval.get("channel", ""),
141
+ approval.get("expires_at", ""),
142
+ ]
143
+
144
+
145
+ def _approval_status(value: str) -> str:
146
+ """Validate approval list status."""
147
+ normalized = value.strip().lower()
148
+ if normalized not in APPROVAL_STATUSES:
149
+ raise typer.BadParameter("--status must be one of: all, pending.")
150
+ return normalized