bonito-cli 0.5.2__tar.gz → 0.6.1__tar.gz
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.
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/CHANGELOG.md +21 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/PKG-INFO +1 -1
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/__init__.py +1 -1
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/app.py +16 -0
- bonito_cli-0.6.1/bonito_cli/commands/approval.py +207 -0
- bonito_cli-0.6.1/bonito_cli/commands/audit.py +79 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/auth.py +35 -40
- bonito_cli-0.6.1/bonito_cli/commands/compliance.py +110 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/deploy.py +43 -23
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/init.py +1 -1
- bonito_cli-0.6.1/bonito_cli/commands/mcp.py +221 -0
- bonito_cli-0.6.1/bonito_cli/commands/memory.py +170 -0
- bonito_cli-0.6.1/bonito_cli/commands/rbac.py +171 -0
- bonito_cli-0.6.1/bonito_cli/commands/routing.py +178 -0
- bonito_cli-0.6.1/bonito_cli/commands/scheduler.py +253 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/secrets.py +10 -11
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/pyproject.toml +1 -1
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/.gitignore +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/CLI_SPEC.md +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/README.md +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/__main__.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/api.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/__init__.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/admin.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/agents.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/analytics.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/chat.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/deployments.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/gateway.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/groups.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/kb.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/models.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/plan.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/policies.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/projects.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/providers.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/commands/sso.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/config.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/__init__.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/auth.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/display.py +0 -0
- {bonito_cli-0.5.2 → bonito_cli-0.6.1}/bonito_cli/utils/feature_gate.py +0 -0
|
@@ -2,6 +2,27 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to the Bonito CLI will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.6.0] - 2026-05-07
|
|
6
|
+
|
|
7
|
+
### Added
|
|
8
|
+
- **8 new command modules** for full API parity:
|
|
9
|
+
- `bonito rbac` — role-based access control (roles, permissions, assignments)
|
|
10
|
+
- `bonito compliance` — SOC-2, HIPAA, GDPR, ISO27001 policy checks
|
|
11
|
+
- `bonito approval` — human-in-the-loop approval queue management
|
|
12
|
+
- `bonito scheduler` — cron-based autonomous agent execution
|
|
13
|
+
- `bonito audit` — audit log viewing and export
|
|
14
|
+
- `bonito memory` — persistent agent memory management
|
|
15
|
+
- `bonito mcp` — MCP server registration and management per-agent
|
|
16
|
+
- `bonito routing` — routing policy builder (cost/latency/balanced/failover/ab_test)
|
|
17
|
+
|
|
18
|
+
### Fixed
|
|
19
|
+
- **`bonito deploy` now updates existing agents** instead of creating duplicates on re-deploy. Model, prompt, and config changes in `bonito.yaml` now propagate correctly.
|
|
20
|
+
- P0 bugs: signup flow, secrets API calls, stale URLs
|
|
21
|
+
|
|
22
|
+
### Changed
|
|
23
|
+
- Deploy summary now shows `~` (yellow) for updated agents vs `v` (green) for newly created ones
|
|
24
|
+
|
|
25
|
+
|
|
5
26
|
## [0.5.1] - 2026-03-02
|
|
6
27
|
|
|
7
28
|
### Added
|
|
@@ -26,6 +26,14 @@ from .commands.sso import app as sso_app
|
|
|
26
26
|
from .commands.plan import app as plan_app
|
|
27
27
|
from .commands.deploy import app as deploy_app
|
|
28
28
|
from .commands.secrets import app as secrets_app
|
|
29
|
+
from .commands.rbac import app as rbac_app
|
|
30
|
+
from .commands.compliance import app as compliance_app
|
|
31
|
+
from .commands.approval import app as approval_app
|
|
32
|
+
from .commands.scheduler import app as scheduler_app
|
|
33
|
+
from .commands.audit import app as audit_app
|
|
34
|
+
from .commands.memory import app as memory_app
|
|
35
|
+
from .commands.mcp import app as mcp_app
|
|
36
|
+
from .commands.routing import app as routing_app
|
|
29
37
|
|
|
30
38
|
console = Console()
|
|
31
39
|
|
|
@@ -88,6 +96,14 @@ app.add_typer(sso_app, name="sso", help="🔐 SAML Single Sign-O
|
|
|
88
96
|
app.add_typer(plan_app, name="plan", help="💎 Subscription plans & usage")
|
|
89
97
|
app.add_typer(deploy_app, name="deploy", help="📦 Deploy from bonito.yaml")
|
|
90
98
|
app.add_typer(secrets_app, name="secrets", help="🔐 Org secrets management")
|
|
99
|
+
app.add_typer(rbac_app, name="rbac", help="🛡️ Role-based access control")
|
|
100
|
+
app.add_typer(compliance_app, name="compliance", help="📋 Compliance checks & reports")
|
|
101
|
+
app.add_typer(approval_app, name="approval", help="✅ Agent approval workflows")
|
|
102
|
+
app.add_typer(scheduler_app, name="scheduler", help="⏰ Agent scheduling")
|
|
103
|
+
app.add_typer(audit_app, name="audit", help="📝 Audit logs")
|
|
104
|
+
app.add_typer(memory_app, name="memory", help="🧠 Agent persistent memory")
|
|
105
|
+
app.add_typer(mcp_app, name="mcp", help="🔌 MCP server management")
|
|
106
|
+
app.add_typer(routing_app, name="routing", help="🔀 Routing rule management")
|
|
91
107
|
|
|
92
108
|
|
|
93
109
|
# ── Callbacks ──────────────────────────────────────────────────
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
"""Agent approval workflow commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
from rich.prompt import Prompt
|
|
11
|
+
|
|
12
|
+
from ..api import api, APIError
|
|
13
|
+
from ..utils.auth import ensure_authenticated
|
|
14
|
+
from ..utils.display import (
|
|
15
|
+
get_output_format,
|
|
16
|
+
print_error,
|
|
17
|
+
print_info,
|
|
18
|
+
print_success,
|
|
19
|
+
print_table,
|
|
20
|
+
print_dict_as_table,
|
|
21
|
+
format_timestamp,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
console = Console()
|
|
25
|
+
app = typer.Typer(help="Agent approval queue and human-in-the-loop workflows")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
# -- list-pending ------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@app.command("list-pending")
|
|
32
|
+
def list_pending(
|
|
33
|
+
org_id: str = typer.Option(..., "--org", help="Organization ID"),
|
|
34
|
+
agent_id: Optional[str] = typer.Option(None, "--agent", help="Filter by agent ID"),
|
|
35
|
+
risk_level: Optional[str] = typer.Option(None, "--risk", help="Filter by risk level (low/medium/high/critical)"),
|
|
36
|
+
limit: int = typer.Option(50, "--limit", "-l", help="Max results"),
|
|
37
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
38
|
+
):
|
|
39
|
+
"""List pending approval actions."""
|
|
40
|
+
fmt = get_output_format(json_output)
|
|
41
|
+
ensure_authenticated()
|
|
42
|
+
|
|
43
|
+
params = {"limit": limit}
|
|
44
|
+
if agent_id:
|
|
45
|
+
params["agent_id"] = agent_id
|
|
46
|
+
if risk_level:
|
|
47
|
+
params["risk_level"] = risk_level
|
|
48
|
+
|
|
49
|
+
try:
|
|
50
|
+
with console.status("[cyan]Fetching pending approvals...[/cyan]"):
|
|
51
|
+
items = api.get(f"/organizations/{org_id}/approvals/queue", params=params)
|
|
52
|
+
|
|
53
|
+
if fmt == "json":
|
|
54
|
+
console.print_json(_json.dumps(items, default=str))
|
|
55
|
+
else:
|
|
56
|
+
if items:
|
|
57
|
+
data = [
|
|
58
|
+
{
|
|
59
|
+
"ID": str(i.get("id", ""))[:8] + "...",
|
|
60
|
+
"Agent": i.get("agent_name", ""),
|
|
61
|
+
"Action": i.get("action_type", ""),
|
|
62
|
+
"Risk": i.get("risk_level", ""),
|
|
63
|
+
"Requester": i.get("requester_name", "") or "",
|
|
64
|
+
"Created": format_timestamp(i.get("created_at")),
|
|
65
|
+
}
|
|
66
|
+
for i in items
|
|
67
|
+
]
|
|
68
|
+
print_table(data, title="Pending Approvals")
|
|
69
|
+
print_info(f"{len(items)} pending action(s)")
|
|
70
|
+
else:
|
|
71
|
+
print_info("No pending approvals.")
|
|
72
|
+
except APIError as exc:
|
|
73
|
+
print_error(f"Failed to list pending approvals: {exc}")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
# -- approve -----------------------------------------------------------
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
@app.command("approve")
|
|
80
|
+
def approve(
|
|
81
|
+
action_id: str = typer.Argument(..., help="Approval action ID"),
|
|
82
|
+
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Review notes"),
|
|
83
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
84
|
+
):
|
|
85
|
+
"""Approve a pending action."""
|
|
86
|
+
fmt = get_output_format(json_output)
|
|
87
|
+
ensure_authenticated()
|
|
88
|
+
|
|
89
|
+
body = {"action": "approve"}
|
|
90
|
+
if notes:
|
|
91
|
+
body["review_notes"] = notes
|
|
92
|
+
|
|
93
|
+
try:
|
|
94
|
+
with console.status("[cyan]Approving action...[/cyan]"):
|
|
95
|
+
result = api.post(f"/approvals/{action_id}/review", body)
|
|
96
|
+
|
|
97
|
+
if fmt == "json":
|
|
98
|
+
console.print_json(_json.dumps(result, default=str))
|
|
99
|
+
else:
|
|
100
|
+
print_success(f"Action {action_id} approved")
|
|
101
|
+
except APIError as exc:
|
|
102
|
+
print_error(f"Failed to approve action: {exc}")
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
# -- reject ------------------------------------------------------------
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
@app.command("reject")
|
|
109
|
+
def reject(
|
|
110
|
+
action_id: str = typer.Argument(..., help="Approval action ID"),
|
|
111
|
+
notes: Optional[str] = typer.Option(None, "--notes", "-n", help="Rejection reason"),
|
|
112
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
113
|
+
):
|
|
114
|
+
"""Reject a pending action."""
|
|
115
|
+
fmt = get_output_format(json_output)
|
|
116
|
+
ensure_authenticated()
|
|
117
|
+
|
|
118
|
+
body = {"action": "reject"}
|
|
119
|
+
if notes:
|
|
120
|
+
body["review_notes"] = notes
|
|
121
|
+
|
|
122
|
+
try:
|
|
123
|
+
with console.status("[cyan]Rejecting action...[/cyan]"):
|
|
124
|
+
result = api.post(f"/approvals/{action_id}/review", body)
|
|
125
|
+
|
|
126
|
+
if fmt == "json":
|
|
127
|
+
console.print_json(_json.dumps(result, default=str))
|
|
128
|
+
else:
|
|
129
|
+
print_success(f"Action {action_id} rejected")
|
|
130
|
+
except APIError as exc:
|
|
131
|
+
print_error(f"Failed to reject action: {exc}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# -- get-config --------------------------------------------------------
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
@app.command("get-config")
|
|
138
|
+
def get_config(
|
|
139
|
+
agent_id: str = typer.Argument(..., help="Agent ID"),
|
|
140
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
141
|
+
):
|
|
142
|
+
"""List approval configs for an agent."""
|
|
143
|
+
fmt = get_output_format(json_output)
|
|
144
|
+
ensure_authenticated()
|
|
145
|
+
|
|
146
|
+
try:
|
|
147
|
+
with console.status("[cyan]Fetching approval configs...[/cyan]"):
|
|
148
|
+
configs = api.get(f"/agents/{agent_id}/approval-configs")
|
|
149
|
+
|
|
150
|
+
if fmt == "json":
|
|
151
|
+
console.print_json(_json.dumps(configs, default=str))
|
|
152
|
+
else:
|
|
153
|
+
if configs:
|
|
154
|
+
data = [
|
|
155
|
+
{
|
|
156
|
+
"ID": str(c.get("id", ""))[:8] + "...",
|
|
157
|
+
"Action Type": c.get("action_type", ""),
|
|
158
|
+
"Requires Approval": "Yes" if c.get("requires_approval") else "No",
|
|
159
|
+
"Timeout (hrs)": str(c.get("timeout_hours", "")),
|
|
160
|
+
"Required Approvers": str(c.get("required_approvers", 1)),
|
|
161
|
+
}
|
|
162
|
+
for c in configs
|
|
163
|
+
]
|
|
164
|
+
print_table(data, title="Approval Configs")
|
|
165
|
+
else:
|
|
166
|
+
print_info("No approval configs found for this agent.")
|
|
167
|
+
except APIError as exc:
|
|
168
|
+
print_error(f"Failed to get approval configs: {exc}")
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
# -- update-config -----------------------------------------------------
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@app.command("update-config")
|
|
175
|
+
def update_config(
|
|
176
|
+
config_id: str = typer.Argument(..., help="Approval config ID"),
|
|
177
|
+
requires_approval: Optional[bool] = typer.Option(None, "--requires-approval", help="Whether approval is required"),
|
|
178
|
+
timeout_hours: Optional[int] = typer.Option(None, "--timeout-hours", help="Approval timeout in hours"),
|
|
179
|
+
required_approvers: Optional[int] = typer.Option(None, "--required-approvers", help="Number of required approvers"),
|
|
180
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
181
|
+
):
|
|
182
|
+
"""Update an approval configuration."""
|
|
183
|
+
fmt = get_output_format(json_output)
|
|
184
|
+
ensure_authenticated()
|
|
185
|
+
|
|
186
|
+
body = {}
|
|
187
|
+
if requires_approval is not None:
|
|
188
|
+
body["requires_approval"] = requires_approval
|
|
189
|
+
if timeout_hours is not None:
|
|
190
|
+
body["timeout_hours"] = timeout_hours
|
|
191
|
+
if required_approvers is not None:
|
|
192
|
+
body["required_approvers"] = required_approvers
|
|
193
|
+
|
|
194
|
+
if not body:
|
|
195
|
+
print_error("No updates specified")
|
|
196
|
+
return
|
|
197
|
+
|
|
198
|
+
try:
|
|
199
|
+
with console.status("[cyan]Updating approval config...[/cyan]"):
|
|
200
|
+
result = api.put(f"/approval-configs/{config_id}", body)
|
|
201
|
+
|
|
202
|
+
if fmt == "json":
|
|
203
|
+
console.print_json(_json.dumps(result, default=str))
|
|
204
|
+
else:
|
|
205
|
+
print_success("Approval config updated successfully")
|
|
206
|
+
except APIError as exc:
|
|
207
|
+
print_error(f"Failed to update approval config: {exc}")
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
"""Audit log commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
from rich.console import Console
|
|
10
|
+
|
|
11
|
+
from ..api import api, APIError
|
|
12
|
+
from ..utils.auth import ensure_authenticated
|
|
13
|
+
from ..utils.display import (
|
|
14
|
+
get_output_format,
|
|
15
|
+
print_error,
|
|
16
|
+
print_info,
|
|
17
|
+
print_table,
|
|
18
|
+
format_timestamp,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
app = typer.Typer(help="Audit log viewer")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
@app.command("list")
|
|
26
|
+
def list_logs(
|
|
27
|
+
action: Optional[str] = typer.Option(None, "--action", "-a", help="Filter by action (e.g. create, update, delete)"),
|
|
28
|
+
resource_type: Optional[str] = typer.Option(None, "--resource", "-r", help="Filter by resource type"),
|
|
29
|
+
user_name: Optional[str] = typer.Option(None, "--user", "-u", help="Filter by user name"),
|
|
30
|
+
date_from: Optional[str] = typer.Option(None, "--from", help="Filter from date (ISO format: 2026-01-01)"),
|
|
31
|
+
date_to: Optional[str] = typer.Option(None, "--to", help="Filter to date (ISO format: 2026-12-31)"),
|
|
32
|
+
page: int = typer.Option(1, "--page", help="Page number"),
|
|
33
|
+
page_size: int = typer.Option(20, "--page-size", help="Page size (max 100)"),
|
|
34
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
35
|
+
):
|
|
36
|
+
"""List audit logs with optional filters."""
|
|
37
|
+
fmt = get_output_format(json_output)
|
|
38
|
+
ensure_authenticated()
|
|
39
|
+
|
|
40
|
+
params = {"page": page, "page_size": page_size}
|
|
41
|
+
if action:
|
|
42
|
+
params["action"] = action
|
|
43
|
+
if resource_type:
|
|
44
|
+
params["resource_type"] = resource_type
|
|
45
|
+
if user_name:
|
|
46
|
+
params["user_name"] = user_name
|
|
47
|
+
if date_from:
|
|
48
|
+
params["date_from"] = date_from
|
|
49
|
+
if date_to:
|
|
50
|
+
params["date_to"] = date_to
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
with console.status("[cyan]Fetching audit logs...[/cyan]"):
|
|
54
|
+
result = api.get("/audit", params=params)
|
|
55
|
+
|
|
56
|
+
if fmt == "json":
|
|
57
|
+
console.print_json(_json.dumps(result, default=str))
|
|
58
|
+
else:
|
|
59
|
+
items = result.get("items", [])
|
|
60
|
+
total = result.get("total", 0)
|
|
61
|
+
|
|
62
|
+
if items:
|
|
63
|
+
data = [
|
|
64
|
+
{
|
|
65
|
+
"Timestamp": format_timestamp(i.get("created_at")),
|
|
66
|
+
"User": i.get("user_name", ""),
|
|
67
|
+
"Action": i.get("action", ""),
|
|
68
|
+
"Resource": i.get("resource_type", ""),
|
|
69
|
+
"Resource ID": str(i.get("resource_id", ""))[:8] + "..." if i.get("resource_id") else "",
|
|
70
|
+
"Details": (str(i.get("details", "")) or "")[:40],
|
|
71
|
+
}
|
|
72
|
+
for i in items
|
|
73
|
+
]
|
|
74
|
+
print_table(data, title="Audit Logs")
|
|
75
|
+
print_info(f"Page {page} - Showing {len(items)} of {total} total entries")
|
|
76
|
+
else:
|
|
77
|
+
print_info("No audit log entries found.")
|
|
78
|
+
except APIError as exc:
|
|
79
|
+
print_error(f"Failed to list audit logs: {exc}")
|
|
@@ -33,21 +33,24 @@ def signup(
|
|
|
33
33
|
password: Optional[str] = typer.Option(None, "--password", "-p", help="Account password"),
|
|
34
34
|
name: Optional[str] = typer.Option(None, "--name", "-n", help="Full name"),
|
|
35
35
|
org: Optional[str] = typer.Option(None, "--org", "-o", help="Organization name"),
|
|
36
|
+
invite_code: Optional[str] = typer.Option(None, "--invite-code", "-i", help="Invite code (required for invite-only registration)"),
|
|
36
37
|
non_interactive: bool = typer.Option(False, "--non-interactive", help="Non-interactive mode (requires all fields as flags)"),
|
|
37
38
|
api_url: Optional[str] = typer.Option(None, "--api-url", help="Custom API URL"),
|
|
38
39
|
):
|
|
39
40
|
"""
|
|
40
41
|
Sign up for a new Bonito account.
|
|
41
42
|
|
|
43
|
+
After signup you must verify your email before logging in.
|
|
44
|
+
|
|
42
45
|
Examples:
|
|
43
46
|
bonito auth signup
|
|
44
|
-
bonito auth signup --
|
|
45
|
-
bonito auth signup --
|
|
47
|
+
bonito auth signup --invite-code ABC123
|
|
48
|
+
bonito auth signup --email admin@company.com --name "John Doe" --org "ACME Corp" --invite-code ABC123
|
|
46
49
|
"""
|
|
47
50
|
# In non-interactive mode, all fields must be provided as flags
|
|
48
51
|
if non_interactive:
|
|
49
|
-
if not all([email, password, name
|
|
50
|
-
console.print("[red]✗ In non-interactive mode,
|
|
52
|
+
if not all([email, password, name]):
|
|
53
|
+
console.print("[red]✗ In non-interactive mode, --email, --password, and --name are required[/red]")
|
|
51
54
|
raise typer.Exit(1)
|
|
52
55
|
else:
|
|
53
56
|
# Interactive prompts
|
|
@@ -57,11 +60,13 @@ def signup(
|
|
|
57
60
|
password = Prompt.ask("[cyan]Password[/cyan]", password=True)
|
|
58
61
|
if not name:
|
|
59
62
|
name = Prompt.ask("[cyan]Full name[/cyan]")
|
|
60
|
-
if not
|
|
61
|
-
|
|
63
|
+
if not invite_code:
|
|
64
|
+
invite_code = Prompt.ask("[cyan]Invite code (leave blank if not required)[/cyan]", default="")
|
|
65
|
+
if not invite_code:
|
|
66
|
+
invite_code = None
|
|
62
67
|
|
|
63
|
-
if not all([email, password, name
|
|
64
|
-
console.print("[red]✗
|
|
68
|
+
if not all([email, password, name]):
|
|
69
|
+
console.print("[red]✗ Email, password, and name are required[/red]")
|
|
65
70
|
raise typer.Exit(1)
|
|
66
71
|
|
|
67
72
|
# Allow overriding the API URL
|
|
@@ -74,53 +79,43 @@ def signup(
|
|
|
74
79
|
api._client = None
|
|
75
80
|
|
|
76
81
|
try:
|
|
82
|
+
body = {
|
|
83
|
+
"email": email,
|
|
84
|
+
"password": password,
|
|
85
|
+
"name": name,
|
|
86
|
+
}
|
|
87
|
+
if invite_code:
|
|
88
|
+
body["invite_code"] = invite_code
|
|
89
|
+
|
|
77
90
|
with console.status("[cyan]Creating account…[/cyan]"):
|
|
78
91
|
resp = httpx.post(
|
|
79
92
|
f"{api.base_url}/api/auth/register",
|
|
80
|
-
json=
|
|
81
|
-
"email": email,
|
|
82
|
-
"password": password,
|
|
83
|
-
"name": name,
|
|
84
|
-
},
|
|
93
|
+
json=body,
|
|
85
94
|
timeout=15,
|
|
86
95
|
)
|
|
87
96
|
|
|
88
|
-
if resp.status_code
|
|
97
|
+
if resp.status_code not in (200, 201):
|
|
89
98
|
detail = "Registration failed"
|
|
90
99
|
try:
|
|
91
|
-
|
|
100
|
+
err = resp.json()
|
|
101
|
+
detail = err.get("detail") or err.get("message", detail)
|
|
92
102
|
except Exception:
|
|
93
103
|
pass
|
|
94
104
|
console.print(f"[red]✗ {detail}[/red]")
|
|
95
105
|
raise typer.Exit(1)
|
|
96
106
|
|
|
97
107
|
data = resp.json()
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
# Fetch user profile
|
|
108
|
-
try:
|
|
109
|
-
me = api.get("/auth/me")
|
|
110
|
-
creds["user"] = me
|
|
111
|
-
save_credentials(creds)
|
|
112
|
-
org_name = me.get("org", {}).get("name", "—")
|
|
113
|
-
console.print(
|
|
114
|
-
Panel(
|
|
115
|
-
f"[green]✓ Account created and logged in as [bold]{email}[/bold][/green]\n"
|
|
116
|
-
f" Organization: {org_name}\n"
|
|
117
|
-
f" Role: {me.get('role', 'member')}",
|
|
118
|
-
title="🎉 Welcome to Bonito",
|
|
119
|
-
border_style="green",
|
|
120
|
-
)
|
|
108
|
+
message = data.get("message", "Account created")
|
|
109
|
+
|
|
110
|
+
console.print(
|
|
111
|
+
Panel(
|
|
112
|
+
f"[green]✓ {message}[/green]\n\n"
|
|
113
|
+
f" Please verify your email, then run:\n"
|
|
114
|
+
f" [cyan]bonito auth login --email {email}[/cyan]",
|
|
115
|
+
title="🎉 Welcome to Bonito",
|
|
116
|
+
border_style="green",
|
|
121
117
|
)
|
|
122
|
-
|
|
123
|
-
console.print(f"[green]✓ Account created and logged in as {email}[/green]")
|
|
118
|
+
)
|
|
124
119
|
|
|
125
120
|
except httpx.RequestError as exc:
|
|
126
121
|
console.print(f"[red]✗ Connection failed: {exc}[/red]")
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"""Compliance checks and reporting commands."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json as _json
|
|
6
|
+
|
|
7
|
+
import typer
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from ..api import api, APIError
|
|
11
|
+
from ..utils.auth import ensure_authenticated
|
|
12
|
+
from ..utils.display import (
|
|
13
|
+
get_output_format,
|
|
14
|
+
print_error,
|
|
15
|
+
print_info,
|
|
16
|
+
print_success,
|
|
17
|
+
print_table,
|
|
18
|
+
print_dict_as_table,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
console = Console()
|
|
22
|
+
app = typer.Typer(help="Compliance checks and reporting")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
# -- run-check ---------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
@app.command("run-check")
|
|
29
|
+
def run_check(
|
|
30
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
31
|
+
):
|
|
32
|
+
"""Run a compliance scan across all providers."""
|
|
33
|
+
fmt = get_output_format(json_output)
|
|
34
|
+
ensure_authenticated()
|
|
35
|
+
|
|
36
|
+
try:
|
|
37
|
+
with console.status("[cyan]Running compliance scan...[/cyan]"):
|
|
38
|
+
result = api.post("/compliance/scan")
|
|
39
|
+
|
|
40
|
+
if fmt == "json":
|
|
41
|
+
console.print_json(_json.dumps(result, default=str))
|
|
42
|
+
else:
|
|
43
|
+
print_success("Compliance scan completed")
|
|
44
|
+
if isinstance(result, dict):
|
|
45
|
+
print_dict_as_table(result, title="Scan Results")
|
|
46
|
+
except APIError as exc:
|
|
47
|
+
print_error(f"Failed to run compliance scan: {exc}")
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
# -- list-checks -------------------------------------------------------
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
@app.command("list-checks")
|
|
54
|
+
def list_checks(
|
|
55
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
56
|
+
):
|
|
57
|
+
"""List all compliance checks and their status."""
|
|
58
|
+
fmt = get_output_format(json_output)
|
|
59
|
+
ensure_authenticated()
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
with console.status("[cyan]Fetching compliance checks...[/cyan]"):
|
|
63
|
+
checks = api.get("/compliance/checks")
|
|
64
|
+
|
|
65
|
+
if fmt == "json":
|
|
66
|
+
console.print_json(_json.dumps(checks, default=str))
|
|
67
|
+
else:
|
|
68
|
+
if checks:
|
|
69
|
+
data = [
|
|
70
|
+
{
|
|
71
|
+
"Check": c.get("name", c.get("check_id", "")),
|
|
72
|
+
"Framework": c.get("framework", ""),
|
|
73
|
+
"Status": c.get("status", ""),
|
|
74
|
+
"Severity": c.get("severity", ""),
|
|
75
|
+
"Last Run": str(c.get("last_checked_at", ""))[:10] if c.get("last_checked_at") else "",
|
|
76
|
+
}
|
|
77
|
+
for c in checks
|
|
78
|
+
]
|
|
79
|
+
print_table(data, title="Compliance Checks")
|
|
80
|
+
print_info(f"{len(checks)} check(s)")
|
|
81
|
+
else:
|
|
82
|
+
print_info("No compliance checks found.")
|
|
83
|
+
except APIError as exc:
|
|
84
|
+
print_error(f"Failed to list compliance checks: {exc}")
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
# -- get-report --------------------------------------------------------
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@app.command("get-report")
|
|
91
|
+
def get_report(
|
|
92
|
+
json_output: bool = typer.Option(False, "--json", help="Output as JSON"),
|
|
93
|
+
):
|
|
94
|
+
"""Get the full compliance report."""
|
|
95
|
+
fmt = get_output_format(json_output)
|
|
96
|
+
ensure_authenticated()
|
|
97
|
+
|
|
98
|
+
try:
|
|
99
|
+
with console.status("[cyan]Generating compliance report...[/cyan]"):
|
|
100
|
+
report = api.get("/compliance/report")
|
|
101
|
+
|
|
102
|
+
if fmt == "json":
|
|
103
|
+
console.print_json(_json.dumps(report, default=str))
|
|
104
|
+
else:
|
|
105
|
+
if isinstance(report, dict):
|
|
106
|
+
print_dict_as_table(report, title="Compliance Report")
|
|
107
|
+
else:
|
|
108
|
+
console.print(report)
|
|
109
|
+
except APIError as exc:
|
|
110
|
+
print_error(f"Failed to get compliance report: {exc}")
|