humanbound 2.0.3__tar.gz → 2.0.4__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.
Files changed (113) hide show
  1. {humanbound-2.0.3 → humanbound-2.0.4}/PKG-INFO +9 -2
  2. {humanbound-2.0.3 → humanbound-2.0.4}/README.md +6 -0
  3. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/PKG-INFO +9 -2
  4. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/SOURCES.txt +4 -5
  5. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/requires.txt +2 -1
  6. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/__init__.py +0 -4
  7. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/api_keys.py +5 -0
  8. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/assessments.py +3 -0
  9. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/auth.py +3 -0
  10. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/campaigns.py +3 -0
  11. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/connect.py +53 -246
  12. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/experiments.py +8 -0
  13. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/findings.py +20 -2
  14. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/firewall.py +2 -0
  15. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/guardrails.py +2 -0
  16. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/logs.py +4 -0
  17. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/members.py +4 -0
  18. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/monitor.py +2 -0
  19. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/orgs.py +6 -0
  20. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/posture.py +17 -0
  21. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/projects.py +8 -0
  22. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/providers.py +5 -0
  23. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/redteam.py +2 -0
  24. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/report.py +2 -0
  25. humanbound-2.0.4/humanbound_cli/commands/telemetry.py +62 -0
  26. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/test.py +111 -43
  27. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/webhooks.py +8 -0
  28. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/config.py +19 -0
  29. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/main.py +15 -2
  30. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/mcp_server.py +1 -2
  31. humanbound-2.0.4/humanbound_cli/telemetry/__init__.py +86 -0
  32. humanbound-2.0.4/humanbound_cli/telemetry/client.py +176 -0
  33. humanbound-2.0.4/humanbound_cli/telemetry/consent.py +140 -0
  34. {humanbound-2.0.3 → humanbound-2.0.4}/pyproject.toml +6 -3
  35. humanbound-2.0.3/humanbound_cli/commands/scan.py +0 -3
  36. humanbound-2.0.3/humanbound_cli/commands/sentinel.py +0 -1039
  37. humanbound-2.0.3/humanbound_cli/commands/upload_logs.py +0 -126
  38. humanbound-2.0.3/humanbound_cli/connectors/__init__.py +0 -3
  39. humanbound-2.0.3/humanbound_cli/connectors/microsoft.py +0 -1691
  40. {humanbound-2.0.3 → humanbound-2.0.4}/LICENSE +0 -0
  41. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/__init__.py +0 -0
  42. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/bot.py +0 -0
  43. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/callbacks.py +0 -0
  44. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/orchestrators.py +0 -0
  45. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/py.typed +0 -0
  46. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/runner.py +0 -0
  47. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound/schemas.py +0 -0
  48. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/dependency_links.txt +0 -0
  49. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/entry_points.txt +0 -0
  50. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound.egg-info/top_level.txt +0 -0
  51. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/__init__.py +0 -0
  52. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/adapters/__init__.py +0 -0
  53. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/adapters/promptfoo.py +0 -0
  54. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/adapters/pyrit.py +0 -0
  55. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/client.py +0 -0
  56. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/_report_helper.py +0 -0
  57. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/completion.py +0 -0
  58. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/config_cmd.py +0 -0
  59. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/docs.py +0 -0
  60. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/commands/mcp.py +0 -0
  61. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/__init__.py +0 -0
  62. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/bot.py +0 -0
  63. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/callbacks.py +0 -0
  64. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/compliance.py +0 -0
  65. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/__init__.py +0 -0
  66. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/azureopenai.py +0 -0
  67. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/claude.py +0 -0
  68. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/gemini.py +0 -0
  69. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/grok.py +0 -0
  70. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/ollama.py +0 -0
  71. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/llm/openai.py +0 -0
  72. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/local_runner.py +0 -0
  73. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/__init__.py +0 -0
  74. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/base.py +0 -0
  75. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/behavioral_qa/__init__.py +0 -0
  76. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/behavioral_qa/config.py +0 -0
  77. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/behavioral_qa/generator.py +0 -0
  78. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/behavioral_qa/judge.py +0 -0
  79. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/behavioral_qa/orchestrator.py +0 -0
  80. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_agentic/__init__.py +0 -0
  81. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_agentic/config.py +0 -0
  82. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_agentic/generator.py +0 -0
  83. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_agentic/judge.py +0 -0
  84. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_agentic/orchestrator.py +0 -0
  85. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_single_turn/__init__.py +0 -0
  86. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_single_turn/config.py +0 -0
  87. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_single_turn/generator.py +0 -0
  88. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_single_turn/judge.py +0 -0
  89. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/orchestrators/owasp_single_turn/orchestrator.py +0 -0
  90. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/platform_runner.py +0 -0
  91. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/presenter.py +0 -0
  92. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/runner.py +0 -0
  93. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/schemas.py +0 -0
  94. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/engine/scope.py +0 -0
  95. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/exceptions.py +0 -0
  96. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/extractors/__init__.py +0 -0
  97. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/extractors/openapi.py +0 -0
  98. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/extractors/repo.py +0 -0
  99. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/py.typed +0 -0
  100. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/pytest_plugin/__init__.py +0 -0
  101. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
  102. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/pytest_plugin/report.py +0 -0
  103. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/report.py +0 -0
  104. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/report_builder.py +0 -0
  105. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/banking.yaml +0 -0
  106. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/ecommerce.yaml +0 -0
  107. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/eu-ai-act.yaml +0 -0
  108. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/healthcare.yaml +0 -0
  109. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/insurance.yaml +0 -0
  110. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/compliance/legal.yaml +0 -0
  111. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/logo.svg +0 -0
  112. {humanbound-2.0.3 → humanbound-2.0.4}/humanbound_cli/templates/report_base.html +0 -0
  113. {humanbound-2.0.3 → humanbound-2.0.4}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: humanbound
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Humanbound — open-source AI agent red-team engine, SDK, and CLI.
5
5
  Author-email: Humanbound <hello@humanbound.ai>
6
6
  Maintainer-email: Demetris Gerogiannis <hello@humanbound.ai>, Kostas Siabanis <hello@humanbound.ai>
@@ -34,8 +34,8 @@ Requires-Dist: rich>=13.0.0
34
34
  Requires-Dist: requests>=2.32.0
35
35
  Requires-Dist: pyyaml>=6.0.0
36
36
  Requires-Dist: pydantic>=2.0
37
- Requires-Dist: msal>=1.31.0
38
37
  Requires-Dist: pyperclip>=1.8.0
38
+ Requires-Dist: posthog>=3.0
39
39
  Provides-Extra: engine
40
40
  Requires-Dist: openai>=1.0.0; extra == "engine"
41
41
  Requires-Dist: anthropic>=0.20.0; extra == "engine"
@@ -58,6 +58,7 @@ Requires-Dist: mypy>=1.11; extra == "dev"
58
58
  Requires-Dist: build>=1.2; extra == "dev"
59
59
  Requires-Dist: twine>=5.0; extra == "dev"
60
60
  Requires-Dist: pre-commit>=3.8; extra == "dev"
61
+ Requires-Dist: mkdocs>=1.6; extra == "dev"
61
62
  Dynamic: license-file
62
63
 
63
64
  <p align="center">
@@ -188,6 +189,12 @@ loop, release process, and CLA requirement (see [CLA.md](./CLA.md)).
188
189
  - 🔒 [Report a security issue](./SECURITY.md) — **not via public Issues**
189
190
  - 💬 [Join Discord](https://discord.gg/gQyXjVBF)
190
191
 
192
+ ## Telemetry
193
+
194
+ The `hb` CLI sends anonymous usage data to help us improve it.
195
+ Disable with `hb telemetry disable`, `HB_TELEMETRY_DISABLED=1`, or
196
+ `DO_NOT_TRACK=1`. Full disclosure: [PRIVACY.md](./PRIVACY.md).
197
+
191
198
  ## License
192
199
 
193
200
  [Apache-2.0](./LICENSE). Free to use in any context — commercial or
@@ -126,6 +126,12 @@ loop, release process, and CLA requirement (see [CLA.md](./CLA.md)).
126
126
  - 🔒 [Report a security issue](./SECURITY.md) — **not via public Issues**
127
127
  - 💬 [Join Discord](https://discord.gg/gQyXjVBF)
128
128
 
129
+ ## Telemetry
130
+
131
+ The `hb` CLI sends anonymous usage data to help us improve it.
132
+ Disable with `hb telemetry disable`, `HB_TELEMETRY_DISABLED=1`, or
133
+ `DO_NOT_TRACK=1`. Full disclosure: [PRIVACY.md](./PRIVACY.md).
134
+
129
135
  ## License
130
136
 
131
137
  [Apache-2.0](./LICENSE). Free to use in any context — commercial or
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: humanbound
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Humanbound — open-source AI agent red-team engine, SDK, and CLI.
5
5
  Author-email: Humanbound <hello@humanbound.ai>
6
6
  Maintainer-email: Demetris Gerogiannis <hello@humanbound.ai>, Kostas Siabanis <hello@humanbound.ai>
@@ -34,8 +34,8 @@ Requires-Dist: rich>=13.0.0
34
34
  Requires-Dist: requests>=2.32.0
35
35
  Requires-Dist: pyyaml>=6.0.0
36
36
  Requires-Dist: pydantic>=2.0
37
- Requires-Dist: msal>=1.31.0
38
37
  Requires-Dist: pyperclip>=1.8.0
38
+ Requires-Dist: posthog>=3.0
39
39
  Provides-Extra: engine
40
40
  Requires-Dist: openai>=1.0.0; extra == "engine"
41
41
  Requires-Dist: anthropic>=0.20.0; extra == "engine"
@@ -58,6 +58,7 @@ Requires-Dist: mypy>=1.11; extra == "dev"
58
58
  Requires-Dist: build>=1.2; extra == "dev"
59
59
  Requires-Dist: twine>=5.0; extra == "dev"
60
60
  Requires-Dist: pre-commit>=3.8; extra == "dev"
61
+ Requires-Dist: mkdocs>=1.6; extra == "dev"
61
62
  Dynamic: license-file
62
63
 
63
64
  <p align="center">
@@ -188,6 +189,12 @@ loop, release process, and CLA requirement (see [CLA.md](./CLA.md)).
188
189
  - 🔒 [Report a security issue](./SECURITY.md) — **not via public Issues**
189
190
  - 💬 [Join Discord](https://discord.gg/gQyXjVBF)
190
191
 
192
+ ## Telemetry
193
+
194
+ The `hb` CLI sends anonymous usage data to help us improve it.
195
+ Disable with `hb telemetry disable`, `HB_TELEMETRY_DISABLED=1`, or
196
+ `DO_NOT_TRACK=1`. Full disclosure: [PRIVACY.md](./PRIVACY.md).
197
+
191
198
  ## License
192
199
 
193
200
  [Apache-2.0](./LICENSE). Free to use in any context — commercial or
@@ -50,13 +50,9 @@ humanbound_cli/commands/projects.py
50
50
  humanbound_cli/commands/providers.py
51
51
  humanbound_cli/commands/redteam.py
52
52
  humanbound_cli/commands/report.py
53
- humanbound_cli/commands/scan.py
54
- humanbound_cli/commands/sentinel.py
53
+ humanbound_cli/commands/telemetry.py
55
54
  humanbound_cli/commands/test.py
56
- humanbound_cli/commands/upload_logs.py
57
55
  humanbound_cli/commands/webhooks.py
58
- humanbound_cli/connectors/__init__.py
59
- humanbound_cli/connectors/microsoft.py
60
56
  humanbound_cli/engine/__init__.py
61
57
  humanbound_cli/engine/bot.py
62
58
  humanbound_cli/engine/callbacks.py
@@ -97,6 +93,9 @@ humanbound_cli/extractors/repo.py
97
93
  humanbound_cli/pytest_plugin/__init__.py
98
94
  humanbound_cli/pytest_plugin/fixtures.py
99
95
  humanbound_cli/pytest_plugin/report.py
96
+ humanbound_cli/telemetry/__init__.py
97
+ humanbound_cli/telemetry/client.py
98
+ humanbound_cli/telemetry/consent.py
100
99
  humanbound_cli/templates/logo.svg
101
100
  humanbound_cli/templates/report_base.html
102
101
  humanbound_cli/templates/compliance/banking.yaml
@@ -3,8 +3,8 @@ rich>=13.0.0
3
3
  requests>=2.32.0
4
4
  pyyaml>=6.0.0
5
5
  pydantic>=2.0
6
- msal>=1.31.0
7
6
  pyperclip>=1.8.0
7
+ posthog>=3.0
8
8
 
9
9
  [dev]
10
10
  pytest>=7.0.0
@@ -15,6 +15,7 @@ mypy>=1.11
15
15
  build>=1.2
16
16
  twine>=5.0
17
17
  pre-commit>=3.8
18
+ mkdocs>=1.6
18
19
 
19
20
  [engine]
20
21
  openai>=1.0.0
@@ -23,9 +23,7 @@ from . import (
23
23
  projects,
24
24
  providers,
25
25
  report,
26
- sentinel,
27
26
  test,
28
- upload_logs,
29
27
  webhooks,
30
28
  )
31
29
 
@@ -50,8 +48,6 @@ __all__ = [
50
48
  "api_keys",
51
49
  "members",
52
50
  "campaigns",
53
- "upload_logs",
54
- "sentinel",
55
51
  "completion",
56
52
  "connect",
57
53
  "report",
@@ -10,6 +10,7 @@ from rich.panel import Panel
10
10
  from rich.prompt import Confirm
11
11
  from rich.table import Table
12
12
 
13
+ from .. import telemetry
13
14
  from ..client import HumanboundClient
14
15
  from ..exceptions import APIError, NotAuthenticatedError
15
16
 
@@ -38,6 +39,7 @@ def _list_keys(as_json: bool):
38
39
  client = HumanboundClient()
39
40
 
40
41
  if not client.is_authenticated():
42
+ telemetry.fire_gated_command_hit()
41
43
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
42
44
  raise SystemExit(1)
43
45
 
@@ -106,6 +108,7 @@ def create_key(name: str, scopes: str):
106
108
  client = HumanboundClient()
107
109
 
108
110
  if not client.is_authenticated():
111
+ telemetry.fire_gated_command_hit()
109
112
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
110
113
  raise SystemExit(1)
111
114
 
@@ -152,6 +155,7 @@ def update_key(key_id: str, name: str, scopes: str, active):
152
155
  client = HumanboundClient()
153
156
 
154
157
  if not client.is_authenticated():
158
+ telemetry.fire_gated_command_hit()
155
159
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
156
160
  raise SystemExit(1)
157
161
 
@@ -198,6 +202,7 @@ def revoke_key(key_id: str, force: bool):
198
202
  client = HumanboundClient()
199
203
 
200
204
  if not client.is_authenticated():
205
+ telemetry.fire_gated_command_hit()
201
206
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
202
207
  raise SystemExit(1)
203
208
 
@@ -9,6 +9,7 @@ from rich.console import Console
9
9
  from rich.panel import Panel
10
10
  from rich.table import Table
11
11
 
12
+ from .. import telemetry
12
13
  from ..client import HumanboundClient
13
14
  from ..exceptions import APIError, NotAuthenticatedError
14
15
 
@@ -53,6 +54,7 @@ def assessments_group(ctx, page, size, as_json):
53
54
  client = HumanboundClient()
54
55
 
55
56
  if not client.is_authenticated():
57
+ telemetry.fire_gated_command_hit()
56
58
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
57
59
  raise SystemExit(1)
58
60
 
@@ -132,6 +134,7 @@ def show_assessment(assessment_id: str, as_json: bool):
132
134
  client = HumanboundClient()
133
135
 
134
136
  if not client.is_authenticated():
137
+ telemetry.fire_gated_command_hit()
135
138
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
136
139
  raise SystemExit(1)
137
140
 
@@ -6,6 +6,7 @@ import click
6
6
  from rich.console import Console
7
7
  from rich.panel import Panel
8
8
 
9
+ from .. import telemetry
9
10
  from ..client import HumanboundClient
10
11
  from ..config import DEFAULT_BASE_URL
11
12
  from ..exceptions import AuthenticationError
@@ -44,6 +45,8 @@ def login(base_url: str, port: int, force: bool):
44
45
  console.print("Starting authentication...")
45
46
  client.login(callback_port=port)
46
47
 
48
+ telemetry.identify_from_credentials()
49
+
47
50
  # Auto-select default organisation and resolve name
48
51
  org_display = "not set"
49
52
  if client.default_organisation_id:
@@ -10,6 +10,7 @@ from rich.panel import Panel
10
10
  from rich.prompt import Confirm
11
11
  from rich.table import Table
12
12
 
13
+ from .. import telemetry
13
14
  from ..client import HumanboundClient
14
15
  from ..exceptions import APIError, NotAuthenticatedError
15
16
 
@@ -40,6 +41,7 @@ def campaigns_group(ctx, as_json):
40
41
  client = HumanboundClient()
41
42
 
42
43
  if not client.is_authenticated():
44
+ telemetry.fire_gated_command_hit()
43
45
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
44
46
  raise SystemExit(1)
45
47
 
@@ -136,6 +138,7 @@ def terminate_campaign(force: bool):
136
138
  client = HumanboundClient()
137
139
 
138
140
  if not client.is_authenticated():
141
+ telemetry.fire_gated_command_hit()
139
142
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
140
143
  raise SystemExit(1)
141
144
 
@@ -13,6 +13,7 @@ import click
13
13
  from rich.console import Console
14
14
  from rich.panel import Panel
15
15
 
16
+ from .. import telemetry
16
17
  from ..client import HumanboundClient
17
18
  from ..exceptions import APIError, NotAuthenticatedError
18
19
  from .test import _load_integration, _resolve_context
@@ -226,6 +227,30 @@ def _get_source_description(prompt: str, endpoint: str, repo: str, openapi: str)
226
227
  return ", ".join(sources)
227
228
 
228
229
 
230
+ def _resolve_init_mode(
231
+ endpoint: str | None,
232
+ prompt: str | None,
233
+ repo: str | None,
234
+ openapi: str | None,
235
+ ) -> str:
236
+ """Derive the `init` telemetry mode from the CLI flags."""
237
+ if endpoint:
238
+ return "endpoint"
239
+ if prompt:
240
+ return "text"
241
+ if repo or openapi:
242
+ return "agentic"
243
+ return "none"
244
+
245
+
246
+ def _fire_init_event(mode: str, success: bool, duration_ms: int) -> None:
247
+ """Emit the `init` telemetry event. Safe to call from try/finally."""
248
+ telemetry.capture(
249
+ "init",
250
+ {"mode": mode, "success": success, "duration_ms": duration_ms},
251
+ )
252
+
253
+
229
254
  def _print_next(suggestions: list):
230
255
  """Print Next: suggestions block."""
231
256
  console.print("\n[dim]Next:[/dim]")
@@ -297,24 +322,11 @@ def _derive_agent_name(endpoint: str) -> str:
297
322
 
298
323
 
299
324
  @click.command("connect")
300
- @click.option("--endpoint", "-e", help="Agent config JSON or file path (agent path)")
301
- @click.option(
302
- "--vendor",
303
- "-v",
304
- type=click.Choice(["microsoft"]),
305
- help="Cloud vendor to scan (platform path)",
306
- )
325
+ @click.option("--endpoint", "-e", help="Agent config JSON or file path")
307
326
  @click.option("--name", "-n", help="Project name (optional, auto-generated)")
308
- @click.option(
309
- "--prompt", "-p", type=click.Path(exists=True), help="System prompt file (agent path)"
310
- )
311
- @click.option("--repo", "-r", type=click.Path(exists=True), help="Repository path (agent path)")
312
- @click.option(
313
- "--openapi", "-o", type=click.Path(exists=True), help="OpenAPI spec file (agent path)"
314
- )
315
- @click.option("--tenant", help="Azure tenant ID (platform path, bypasses browser)")
316
- @click.option("--client-id", "client_id", help="Service principal client ID (platform path)")
317
- @click.option("--client-secret", "client_secret", help="Service principal secret (platform path)")
327
+ @click.option("--prompt", "-p", type=click.Path(exists=True), help="System prompt file")
328
+ @click.option("--repo", "-r", type=click.Path(exists=True), help="Repository path")
329
+ @click.option("--openapi", "-o", type=click.Path(exists=True), help="OpenAPI spec file")
318
330
  @click.option(
319
331
  "--context",
320
332
  "-c",
@@ -331,63 +343,47 @@ def _derive_agent_name(endpoint: str) -> str:
331
343
  @click.option("--timeout", "-t", type=int, default=SCAN_TIMEOUT, help="Request timeout in seconds")
332
344
  def connect_command(
333
345
  endpoint,
334
- vendor,
335
346
  name,
336
347
  prompt,
337
348
  repo,
338
349
  openapi,
339
- tenant,
340
- client_id,
341
- client_secret,
342
350
  context,
343
351
  level,
344
352
  yes,
345
353
  timeout,
346
354
  ):
347
- """Connect your AI agent or scan your cloud platform.
355
+ """Connect your AI agent.
348
356
 
349
- Two paths, one command:
350
-
351
- \b
352
- Agent path (--endpoint):
353
- hb connect --endpoint ./bot-config.json
354
- Probes your agent, extracts scope, creates project, runs first test.
355
-
356
- \b
357
- Platform path (--vendor):
358
- hb connect --vendor microsoft
359
- Scans cloud for shadow AI, evaluates 39 signals, saves to inventory.
357
+ Probes your agent, extracts scope, creates a project, and runs the first test.
360
358
 
361
359
  \b
362
360
  Examples:
363
361
  hb connect --endpoint ./config.json
364
362
  hb connect --endpoint ./config.json --prompt ./system.txt
365
- hb connect --vendor microsoft
366
- hb connect --vendor microsoft --tenant abc-123 --client-id x --client-secret y
367
363
  """
368
- has_agent_flags = any([endpoint, prompt, repo, openapi])
369
- has_platform_flags = any([vendor, tenant, client_id, client_secret])
364
+ import time
370
365
 
371
- if has_agent_flags and has_platform_flags:
372
- console.print(
373
- "[red]Cannot combine agent flags (--endpoint/--prompt/--repo/--openapi) with platform flags (--vendor/--tenant).[/red]"
374
- )
375
- raise SystemExit(1)
366
+ start = time.monotonic()
367
+ mode = _resolve_init_mode(endpoint, prompt, repo, openapi)
368
+ success = False
376
369
 
377
- if has_platform_flags:
378
- _connect_platform(vendor, name, tenant, client_id, client_secret, yes, timeout)
379
- elif has_agent_flags:
380
- _connect_agent(endpoint, name, prompt, repo, openapi, context, level, yes, timeout)
381
- else:
382
- console.print("[yellow]Specify a path:[/yellow]")
383
- console.print()
384
- console.print(" [bold]Agent:[/bold] hb connect --endpoint ./bot-config.json")
385
- console.print(" [bold]Platform:[/bold] hb connect --vendor microsoft")
386
- console.print()
387
- console.print(
388
- "[dim]Use --endpoint to connect your AI agent, or --vendor to scan your cloud.[/dim]"
389
- )
390
- raise SystemExit(1)
370
+ try:
371
+ has_agent_flags = any([endpoint, prompt, repo, openapi])
372
+
373
+ if has_agent_flags:
374
+ _connect_agent(endpoint, name, prompt, repo, openapi, context, level, yes, timeout)
375
+ else:
376
+ console.print("[yellow]Specify a source:[/yellow]")
377
+ console.print()
378
+ console.print(" hb connect --endpoint ./bot-config.json")
379
+ console.print()
380
+ console.print("[dim]Use --endpoint, --prompt, --repo, or --openapi.[/dim]")
381
+ raise SystemExit(1)
382
+
383
+ success = True
384
+ finally:
385
+ duration_ms = int((time.monotonic() - start) * 1000)
386
+ _fire_init_event(mode=mode, success=success, duration_ms=duration_ms)
391
387
 
392
388
 
393
389
  # -- Agent path ----------------------------------------------------------------
@@ -593,6 +589,7 @@ def _connect_agent_platform(
593
589
  )
594
590
 
595
591
  except NotAuthenticatedError:
592
+ telemetry.fire_gated_command_hit()
596
593
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
597
594
  raise SystemExit(1)
598
595
  except APIError as e:
@@ -738,196 +735,6 @@ def _print_platform_note():
738
735
  )
739
736
 
740
737
 
741
- # -- Platform path -------------------------------------------------------------
742
-
743
-
744
- def _connect_platform(vendor, name, tenant, client_id, client_secret, yes, timeout):
745
- """Platform path: scan -> assess -> auto-save -> show posture."""
746
- from .discover import (
747
- _display_auth_error,
748
- _display_device_code,
749
- _display_evaluations,
750
- _display_persist_summary,
751
- _display_results,
752
- _get_connector,
753
- )
754
-
755
- client = HumanboundClient()
756
-
757
- if not client.is_authenticated():
758
- console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
759
- raise SystemExit(1)
760
-
761
- if not client.organisation_id:
762
- console.print("[yellow]No organisation selected.[/yellow]")
763
- console.print("Use 'hb switch <id>' to select an organisation first.")
764
- raise SystemExit(1)
765
-
766
- # Default vendor
767
- if not vendor:
768
- if any([tenant, client_id, client_secret]):
769
- vendor = "microsoft"
770
- else:
771
- console.print("[red]--vendor is required for platform path.[/red]")
772
- console.print("[dim]Example: hb connect --vendor microsoft[/dim]")
773
- raise SystemExit(1)
774
-
775
- # Validate service principal flags (all-or-none)
776
- sp_flags = [tenant, client_id, client_secret]
777
- if any(sp_flags) and not all(sp_flags):
778
- console.print(
779
- "[red]Service principal auth requires all three: --tenant, --client-id, --client-secret[/red]"
780
- )
781
- raise SystemExit(1)
782
-
783
- console.print()
784
- console.print(
785
- Panel(
786
- "[bold]AI Service Discovery[/bold]\n\n"
787
- f"Vendor: [bold]{vendor}[/bold]\n"
788
- "Mode: [bold]connect[/bold] (scan + assess + save)\n\n"
789
- "This will:\n"
790
- " 1. Sign in to your cloud tenant\n"
791
- " 2. Scan for AI services [dim](read-only)[/dim]\n"
792
- " 3. Assess against 38 security signals\n"
793
- " 4. Save results to your AI inventory",
794
- border_style="blue",
795
- )
796
- )
797
- console.print()
798
-
799
- if not yes:
800
- if not click.confirm("Proceed with discovery?", default=True):
801
- console.print("[dim]Cancelled.[/dim]")
802
- return
803
-
804
- console.print()
805
-
806
- try:
807
- # -- Authenticate ------------------------------------------------------
808
- connector = _get_connector(vendor, verbose=False)
809
-
810
- if all(sp_flags):
811
- # Service principal auth (non-interactive)
812
- console.print("[dim]Authenticating with service principal...[/dim]")
813
- try:
814
- connector.authenticate_sp(
815
- tenant_id=tenant,
816
- client_id=client_id,
817
- client_secret=client_secret,
818
- )
819
- except AttributeError:
820
- console.print(
821
- "[yellow]Service principal auth not yet supported for this vendor.[/yellow]"
822
- )
823
- console.print("[dim]Use browser auth instead: hb connect --vendor microsoft[/dim]")
824
- raise SystemExit(1)
825
- except PermissionError as e:
826
- console.print(f"[red]Authentication failed:[/red] {e}")
827
- raise SystemExit(1)
828
- console.print("[green]Authenticated via service principal.[/green]\n")
829
- else:
830
- # Browser device-code flow
831
- try:
832
- connector.authenticate(callback=_display_device_code)
833
- except PermissionError as e:
834
- _display_auth_error(str(e))
835
- raise SystemExit(1)
836
- console.print("[green]Signed in successfully.[/green]\n")
837
-
838
- # -- Scan --------------------------------------------------------------
839
- with console.status("[bold blue]Scanning for AI services..."):
840
- services, metadata = connector.discover()
841
-
842
- if not services:
843
- status = metadata.get("status", "unknown")
844
- if status == "failed":
845
- console.print("[red]Discovery failed.[/red] Could not query any APIs.")
846
- else:
847
- console.print("[yellow]No AI services found.[/yellow]")
848
- return
849
-
850
- console.print(f"Found [bold]{len(services)}[/bold] AI services. Analysing...\n")
851
-
852
- # -- Analyse -----------------------------------------------------------
853
- org_id = client.organisation_id
854
- payload = {
855
- "vendor": vendor,
856
- "services": services,
857
- "sources_metadata": {
858
- "status": metadata.get("status", "unknown"),
859
- "apis_queried": metadata.get("apis_queried", []),
860
- "apis_failed": metadata.get("apis_failed", []),
861
- "permissions_missing": metadata.get("permissions_missing", []),
862
- },
863
- "topology": metadata.get("topology", {}),
864
- }
865
-
866
- with console.status("[bold blue]Analysing discovered services..."):
867
- analysis = client.post(
868
- f"organisations/{org_id}/analyse",
869
- data=payload,
870
- include_org=False,
871
- timeout=timeout,
872
- )
873
-
874
- # -- Overlay evaluator risk onto services for consistent display -------
875
- evals = analysis.get("evaluations", [])
876
- if evals:
877
- eval_risk_map = {
878
- ev["service_name"]: ev["risk_level"]
879
- for ev in evals
880
- if "service_name" in ev and "risk_level" in ev
881
- }
882
- for svc in analysis.get("services", []):
883
- eval_rl = eval_risk_map.get(svc.get("name"))
884
- if eval_rl:
885
- svc["risk"] = eval_rl
886
-
887
- new_by_risk = {}
888
- for svc in analysis.get("services", []):
889
- r = svc.get("risk", "unknown")
890
- new_by_risk[r] = new_by_risk.get(r, 0) + 1
891
- if "summary" in analysis:
892
- analysis["summary"]["by_risk"] = new_by_risk
893
-
894
- # -- Display -----------------------------------------------------------
895
- _display_results(analysis, metadata)
896
-
897
- evaluations = analysis.get("evaluations", [])
898
- posture_estimate = analysis.get("posture_estimate")
899
- if evaluations:
900
- _display_evaluations(evaluations, posture_estimate)
901
-
902
- # -- Auto-save (always persist in connect mode) ------------------------
903
- nonce = analysis.get("nonce")
904
- if nonce:
905
- with console.status("[bold blue]Saving to inventory..."):
906
- persist_result = client.persist_discovery(nonce)
907
- _display_persist_summary(persist_result)
908
- else:
909
- console.print(
910
- "\n[yellow]Cannot persist:[/yellow] server did not return a session token."
911
- )
912
-
913
- # -- Next suggestions --------------------------------------------------
914
- _print_next(
915
- [
916
- ("hb inventory", "Browse all assets"),
917
- ("hb posture --org", "Full org posture (3 dimensions)"),
918
- ("hb report --org", "Org-wide report"),
919
- ("hb discover --report", "Export HTML report"),
920
- ]
921
- )
922
-
923
- except NotAuthenticatedError:
924
- console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
925
- raise SystemExit(1)
926
- except APIError as e:
927
- console.print(f"[red]Error:[/red] {e}")
928
- raise SystemExit(1)
929
-
930
-
931
738
  # -- Monitoring recommendation -------------------------------------------------
932
739
 
933
740
  # Regulations that require or strongly recommend continuous AI monitoring
@@ -11,6 +11,7 @@ from rich.progress import Progress, SpinnerColumn, TextColumn
11
11
  from rich.prompt import Confirm
12
12
  from rich.table import Table
13
13
 
14
+ from .. import telemetry
14
15
  from ..client import HumanboundClient
15
16
  from ..exceptions import APIError, NotAuthenticatedError
16
17
 
@@ -84,6 +85,7 @@ def _list_experiments(page: int, size: int):
84
85
  console.print(f"\n[dim]Page {page} of more. Use --page to navigate.[/dim]")
85
86
 
86
87
  except NotAuthenticatedError:
88
+ telemetry.fire_gated_command_hit()
87
89
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
88
90
  raise SystemExit(1)
89
91
  except APIError as e:
@@ -149,6 +151,7 @@ def show_experiment(experiment_id: str):
149
151
  console.print(f" {i}. {insight.get('explanation', '')[:80]}...")
150
152
 
151
153
  except NotAuthenticatedError:
154
+ telemetry.fire_gated_command_hit()
152
155
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
153
156
  raise SystemExit(1)
154
157
  except APIError as e:
@@ -227,6 +230,7 @@ def experiment_status(experiment_id: str, watch: bool, interval: int, show_all:
227
230
  _show_failure_details(client, experiment_id)
228
231
 
229
232
  except NotAuthenticatedError:
233
+ telemetry.fire_gated_command_hit()
230
234
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
231
235
  raise SystemExit(1)
232
236
  except APIError as e:
@@ -315,6 +319,7 @@ def _poll_all_experiments(client: HumanboundClient):
315
319
  cycle += 1
316
320
 
317
321
  except NotAuthenticatedError:
322
+ telemetry.fire_gated_command_hit()
318
323
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
319
324
  raise SystemExit(1)
320
325
  except APIError as e:
@@ -424,6 +429,7 @@ def experiment_wait(experiment_id: str, timeout: int):
424
429
  poll_interval = min(poll_interval * 2, max_interval)
425
430
 
426
431
  except NotAuthenticatedError:
432
+ telemetry.fire_gated_command_hit()
427
433
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
428
434
  raise SystemExit(1)
429
435
  except APIError as e:
@@ -467,6 +473,7 @@ def terminate_experiment(experiment_id: str):
467
473
  console.print(f"[dim]ID: {experiment_id}[/dim]")
468
474
 
469
475
  except NotAuthenticatedError:
476
+ telemetry.fire_gated_command_hit()
470
477
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
471
478
  raise SystemExit(1)
472
479
  except APIError as e:
@@ -508,6 +515,7 @@ def delete_experiment(experiment_id: str, force: bool):
508
515
  console.print(f"[dim]{exp_name} ({experiment_id})[/dim]")
509
516
 
510
517
  except NotAuthenticatedError:
518
+ telemetry.fire_gated_command_hit()
511
519
  console.print("[red]Not authenticated.[/red] Run 'hb login' first.")
512
520
  raise SystemExit(1)
513
521
  except APIError as e: