bonito-cli 0.9.0__tar.gz → 0.9.2__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.9.0 → bonito_cli-0.9.2}/.gitignore +1 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/PKG-INFO +1 -1
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/auth.py +1 -1
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/deploy.py +147 -4
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/projects.py +1 -1
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/pyproject.toml +1 -1
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/CHANGELOG.md +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/CLI_SPEC.md +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/README.md +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/__init__.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/__main__.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/api.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/app.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/__init__.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/admin.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/agents.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/analytics.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/approval.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/audit.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/chat.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/compliance.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/deployments.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/gateway.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/groups.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/init.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/kb.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/mcp.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/memory.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/models.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/plan.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/policies.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/providers.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/rbac.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/routing.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/scheduler.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/secrets.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/commands/sso.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/config.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/__init__.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/enterprise.yaml +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/multi-provider-failover.yaml +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/quickstart.yaml +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/examples/support-agent.yaml +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/__init__.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/auth.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/display.py +0 -0
- {bonito_cli-0.9.0 → bonito_cli-0.9.2}/bonito_cli/utils/feature_gate.py +0 -0
|
@@ -68,7 +68,41 @@ def _find_unresolved(value: Any, path: str = "") -> List[str]:
|
|
|
68
68
|
|
|
69
69
|
_REQUIRED_TOP_KEYS = {"version", "name"}
|
|
70
70
|
_KNOWN_TOP_KEYS = {"version", "name", "description", "gateway", "mcp_servers",
|
|
71
|
-
"knowledge_bases", "agents", "observability", "secrets"
|
|
71
|
+
"knowledge_bases", "agents", "observability", "secrets",
|
|
72
|
+
"integrations", "budget_stops"}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _normalize_section(section: Any) -> Any:
|
|
76
|
+
"""Accept a section as EITHER a mapping (name -> cfg) OR a list of
|
|
77
|
+
{name, ...} entries; always return a mapping keyed by name. Non-conforming
|
|
78
|
+
shapes are returned unchanged so _validate can report them."""
|
|
79
|
+
if isinstance(section, list):
|
|
80
|
+
out: Dict[str, Any] = {}
|
|
81
|
+
for entry in section:
|
|
82
|
+
if isinstance(entry, dict) and entry.get("name"):
|
|
83
|
+
name = entry["name"]
|
|
84
|
+
out[name] = {k: v for k, v in entry.items() if k != "name"}
|
|
85
|
+
return out
|
|
86
|
+
return section
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def _normalize_agents(section: Any) -> Any:
|
|
90
|
+
"""Normalize agents to the mapping form + reconcile field aliases so the
|
|
91
|
+
list-style schema (model_id / system_prompt_path / model_config) works the
|
|
92
|
+
same as the mapping-style schema (model.primary / system_prompt / parameters)."""
|
|
93
|
+
agents = _normalize_section(section)
|
|
94
|
+
if not isinstance(agents, dict):
|
|
95
|
+
return agents
|
|
96
|
+
for cfg in agents.values():
|
|
97
|
+
if not isinstance(cfg, dict):
|
|
98
|
+
continue
|
|
99
|
+
if "system_prompt" not in cfg and "system_prompt_path" in cfg:
|
|
100
|
+
cfg["system_prompt"] = cfg["system_prompt_path"]
|
|
101
|
+
if "model" not in cfg and "model_id" in cfg:
|
|
102
|
+
cfg["model"] = {"primary": cfg["model_id"]}
|
|
103
|
+
if "parameters" not in cfg and isinstance(cfg.get("model_config"), dict):
|
|
104
|
+
cfg["parameters"] = cfg["model_config"]
|
|
105
|
+
return agents
|
|
72
106
|
|
|
73
107
|
|
|
74
108
|
def _validate(cfg: Dict[str, Any]) -> List[str]:
|
|
@@ -93,6 +127,14 @@ def _validate(cfg: Dict[str, Any]) -> List[str]:
|
|
|
93
127
|
if "knowledge_bases" in cfg and not isinstance(cfg["knowledge_bases"], dict):
|
|
94
128
|
errors.append("'knowledge_bases' must be a mapping of kb-name -> config")
|
|
95
129
|
|
|
130
|
+
if "integrations" in cfg:
|
|
131
|
+
if not isinstance(cfg["integrations"], list):
|
|
132
|
+
errors.append("'integrations' must be a list of integration entries")
|
|
133
|
+
else:
|
|
134
|
+
for i, entry in enumerate(cfg["integrations"]):
|
|
135
|
+
if not isinstance(entry, dict) or not entry.get("type"):
|
|
136
|
+
errors.append(f"integrations[{i}]: must be a mapping with a 'type'")
|
|
137
|
+
|
|
96
138
|
# Validate each agent
|
|
97
139
|
for agent_name, agent_cfg in (cfg.get("agents") or {}).items():
|
|
98
140
|
if not isinstance(agent_cfg, dict):
|
|
@@ -117,22 +159,26 @@ class DeployResult:
|
|
|
117
159
|
self.mcp_servers: List[Dict[str, str]] = []
|
|
118
160
|
self.knowledge_bases: List[Dict[str, str]] = []
|
|
119
161
|
self.agents: List[Dict[str, str]] = []
|
|
162
|
+
self.integrations: List[Dict[str, str]] = []
|
|
120
163
|
|
|
121
164
|
@property
|
|
122
165
|
def total(self) -> int:
|
|
123
|
-
return
|
|
166
|
+
return (
|
|
167
|
+
len(self.providers) + len(self.mcp_servers) + len(self.knowledge_bases)
|
|
168
|
+
+ len(self.agents) + len(self.integrations)
|
|
169
|
+
)
|
|
124
170
|
|
|
125
171
|
@property
|
|
126
172
|
def errors(self) -> int:
|
|
127
173
|
return sum(
|
|
128
|
-
1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents)
|
|
174
|
+
1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents, self.integrations)
|
|
129
175
|
for item in items if item["status"] == "error"
|
|
130
176
|
)
|
|
131
177
|
|
|
132
178
|
@property
|
|
133
179
|
def skipped(self) -> int:
|
|
134
180
|
return sum(
|
|
135
|
-
1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents)
|
|
181
|
+
1 for items in (self.providers, self.mcp_servers, self.knowledge_bases, self.agents, self.integrations)
|
|
136
182
|
for item in items if item["status"] == "skipped"
|
|
137
183
|
)
|
|
138
184
|
|
|
@@ -827,6 +873,93 @@ def _print_next_steps(
|
|
|
827
873
|
))
|
|
828
874
|
|
|
829
875
|
|
|
876
|
+
# -- step: integrations ------------------------------------------------------
|
|
877
|
+
|
|
878
|
+
|
|
879
|
+
def _deploy_integrations(
|
|
880
|
+
integrations: List[Dict[str, Any]],
|
|
881
|
+
agent_id_map: Dict[str, str],
|
|
882
|
+
result: DeployResult,
|
|
883
|
+
dry_run: bool,
|
|
884
|
+
verbose: bool,
|
|
885
|
+
) -> None:
|
|
886
|
+
"""Provision declared integrations (Slack today) against the API.
|
|
887
|
+
|
|
888
|
+
Each entry is connected, pointed at an inbound agent, and bound to the
|
|
889
|
+
listed agents. Agent references resolve by name via agent_id_map.
|
|
890
|
+
Schema: integrations: [{type: slack, bot_token, signing_secret,
|
|
891
|
+
inbound_agent, inbound_routes: {channel_id: agent}, bindings: [agent...]}].
|
|
892
|
+
"""
|
|
893
|
+
if not integrations:
|
|
894
|
+
return
|
|
895
|
+
|
|
896
|
+
console.print("\n[bold cyan]Integrations[/bold cyan]")
|
|
897
|
+
for entry in integrations:
|
|
898
|
+
if not isinstance(entry, dict):
|
|
899
|
+
result.integrations.append({"name": "integration", "status": "error", "detail": "must be a mapping"})
|
|
900
|
+
continue
|
|
901
|
+
itype = (entry.get("type") or "").lower()
|
|
902
|
+
name = itype or "integration"
|
|
903
|
+
try:
|
|
904
|
+
if dry_run:
|
|
905
|
+
result.integrations.append({"name": name, "status": "ok", "detail": "dry-run"})
|
|
906
|
+
console.print(f" [dim]~ {name} (dry-run)[/dim]")
|
|
907
|
+
continue
|
|
908
|
+
|
|
909
|
+
if itype != "slack":
|
|
910
|
+
result.integrations.append({"name": name, "status": "error", "detail": f"unsupported type '{itype}' (only slack today)"})
|
|
911
|
+
console.print(f" [red]x {name}: unsupported (only slack today)[/red]")
|
|
912
|
+
continue
|
|
913
|
+
|
|
914
|
+
inbound_name = entry.get("inbound_agent")
|
|
915
|
+
inbound_id = agent_id_map.get(inbound_name) if inbound_name else None
|
|
916
|
+
if inbound_name and not inbound_id:
|
|
917
|
+
result.integrations.append({"name": name, "status": "error", "detail": f"inbound_agent '{inbound_name}' not in this deploy"})
|
|
918
|
+
console.print(f" [red]x slack: inbound_agent '{inbound_name}' not found[/red]")
|
|
919
|
+
continue
|
|
920
|
+
|
|
921
|
+
# 1. Connect the customer-owned Slack app (team_id auto-resolved)
|
|
922
|
+
payload: Dict[str, Any] = {
|
|
923
|
+
"bot_token": entry.get("bot_token"),
|
|
924
|
+
"signing_secret": entry.get("signing_secret"),
|
|
925
|
+
}
|
|
926
|
+
if inbound_id:
|
|
927
|
+
payload["inbound_agent_id"] = inbound_id
|
|
928
|
+
conn = api.post("/integrations/slack/connect/app", payload)
|
|
929
|
+
conn_id = conn.get("id")
|
|
930
|
+
detail = [f"connected ({conn.get('account_label') or 'slack'})"]
|
|
931
|
+
|
|
932
|
+
# 2. Per-channel inbound routing overrides (channel_id -> agent name)
|
|
933
|
+
routes_cfg = entry.get("inbound_routes") or {}
|
|
934
|
+
resolved = {ch: agent_id_map[a] for ch, a in routes_cfg.items() if a in agent_id_map}
|
|
935
|
+
if resolved:
|
|
936
|
+
body: Dict[str, Any] = {"inbound_routes": resolved}
|
|
937
|
+
if inbound_id:
|
|
938
|
+
body["inbound_agent_id"] = inbound_id
|
|
939
|
+
api.patch(f"/integrations/connections/{conn_id}/inbound", body)
|
|
940
|
+
detail.append(f"{len(resolved)} channel route(s)")
|
|
941
|
+
|
|
942
|
+
# 3. Outbound bindings (give agents the slack_post_message tool)
|
|
943
|
+
bound = 0
|
|
944
|
+
for agent_name in (entry.get("bindings") or []):
|
|
945
|
+
aid = agent_id_map.get(agent_name)
|
|
946
|
+
if not aid:
|
|
947
|
+
continue
|
|
948
|
+
try:
|
|
949
|
+
api.post("/integrations/bindings", {"agent_id": aid, "connection_id": conn_id})
|
|
950
|
+
bound += 1
|
|
951
|
+
except APIError:
|
|
952
|
+
pass
|
|
953
|
+
if bound:
|
|
954
|
+
detail.append(f"{bound} binding(s)")
|
|
955
|
+
|
|
956
|
+
result.integrations.append({"name": name, "status": "ok", "detail": ", ".join(detail)})
|
|
957
|
+
console.print(f" [green]v[/green] slack [dim]({', '.join(detail)})[/dim]")
|
|
958
|
+
except (APIError, Exception) as exc:
|
|
959
|
+
result.integrations.append({"name": name, "status": "error", "detail": str(exc)})
|
|
960
|
+
console.print(f" [red]x {name}: {exc}[/red]")
|
|
961
|
+
|
|
962
|
+
|
|
830
963
|
# -- main command ------------------------------------------------------------
|
|
831
964
|
|
|
832
965
|
|
|
@@ -884,6 +1017,13 @@ def deploy(
|
|
|
884
1017
|
# -- 2. Resolve env vars -------------------------------------------------
|
|
885
1018
|
cfg = _resolve_env(cfg)
|
|
886
1019
|
|
|
1020
|
+
# Accept BOTH the mapping form (name -> cfg) and the list form
|
|
1021
|
+
# ([{name, ...}]) for agents + knowledge_bases, reconciling field aliases.
|
|
1022
|
+
if "agents" in cfg:
|
|
1023
|
+
cfg["agents"] = _normalize_agents(cfg["agents"])
|
|
1024
|
+
if "knowledge_bases" in cfg:
|
|
1025
|
+
cfg["knowledge_bases"] = _normalize_section(cfg["knowledge_bases"])
|
|
1026
|
+
|
|
887
1027
|
unresolved = _find_unresolved(cfg)
|
|
888
1028
|
if unresolved:
|
|
889
1029
|
console.print(f"\n[yellow]{len(unresolved)} unresolved environment variable(s):[/yellow]")
|
|
@@ -966,6 +1106,9 @@ def deploy(
|
|
|
966
1106
|
# -- 8. Agent connections (delegation) -----------------------------------
|
|
967
1107
|
_create_agent_connections(agents, agent_id_map, result, dry_run, verbose)
|
|
968
1108
|
|
|
1109
|
+
# -- 8b. Integrations (Slack etc.) ---------------------------------------
|
|
1110
|
+
_deploy_integrations(cfg.get("integrations", []), agent_id_map, result, dry_run, verbose)
|
|
1111
|
+
|
|
969
1112
|
# -- 9. Summary ----------------------------------------------------------
|
|
970
1113
|
_print_summary(result, stack_name, dry_run)
|
|
971
1114
|
|
|
@@ -367,7 +367,7 @@ def project_token_create(
|
|
|
367
367
|
|
|
368
368
|
try:
|
|
369
369
|
with console.status("[cyan]Creating project token…[/cyan]"):
|
|
370
|
-
result = api.post(f"/projects/{project_id}/tokens",
|
|
370
|
+
result = api.post(f"/projects/{project_id}/tokens", data=body)
|
|
371
371
|
|
|
372
372
|
raw_token = result.get("token", "")
|
|
373
373
|
console.print(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|