humanbound-cli 0.6.1__tar.gz → 0.6.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.
Files changed (67) hide show
  1. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/PKG-INFO +1 -1
  2. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/connect.py +11 -9
  3. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/posture.py +67 -36
  4. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/test.py +2 -2
  5. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/mcp_server.py +163 -1
  6. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/PKG-INFO +1 -1
  7. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/top_level.txt +1 -0
  8. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/pyproject.toml +1 -1
  9. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/LICENSE +0 -0
  10. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/README.md +0 -0
  11. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/__init__.py +0 -0
  12. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/client.py +0 -0
  13. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/__init__.py +0 -0
  14. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/api_keys.py +0 -0
  15. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/assessments.py +0 -0
  16. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/auth.py +0 -0
  17. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/campaigns.py +0 -0
  18. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/completion.py +0 -0
  19. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/connectors.py +0 -0
  20. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/coverage.py +0 -0
  21. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/discover.py +0 -0
  22. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/docs.py +0 -0
  23. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/experiments.py +0 -0
  24. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/findings.py +0 -0
  25. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/guardrails.py +0 -0
  26. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/init.py +0 -0
  27. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/inventory.py +0 -0
  28. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/logs.py +0 -0
  29. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/mcp.py +0 -0
  30. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/members.py +0 -0
  31. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/monitor.py +0 -0
  32. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/orgs.py +0 -0
  33. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/projects.py +0 -0
  34. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/providers.py +0 -0
  35. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/report.py +0 -0
  36. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/scan.py +0 -0
  37. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/sentinel.py +0 -0
  38. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/upload_logs.py +0 -0
  39. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/commands/webhooks.py +0 -0
  40. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/config.py +0 -0
  41. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/connectors/__init__.py +0 -0
  42. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/connectors/microsoft.py +0 -0
  43. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/exceptions.py +0 -0
  44. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/__init__.py +0 -0
  45. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/openapi.py +0 -0
  46. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/extractors/repo.py +0 -0
  47. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/main.py +0 -0
  48. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/__init__.py +0 -0
  49. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/fixtures.py +0 -0
  50. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/pytest_plugin/report.py +0 -0
  51. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/report.py +0 -0
  52. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/report_builder.py +0 -0
  53. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/__init__.py +0 -0
  54. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/config_builder.py +0 -0
  55. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/local_server.py +0 -0
  56. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/runtime_detector.py +0 -0
  57. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli/serve/tunnel_client.py +0 -0
  58. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/SOURCES.txt +0 -0
  59. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/dependency_links.txt +0 -0
  60. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/entry_points.txt +0 -0
  61. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/humanbound_cli.egg-info/requires.txt +0 -0
  62. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/relay/relay.py +0 -0
  63. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/setup.cfg +0 -0
  64. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/__init__.py +0 -0
  65. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/cli_integration_test.py +0 -0
  66. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/conftest.py +0 -0
  67. {humanbound_cli-0.6.1 → humanbound_cli-0.6.2}/tests/test_cli_commands.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: humanbound-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Humanbound CLI - command line interface for AI agent security testing.
5
5
  Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
6
6
  License: Apache-2.0
@@ -43,7 +43,7 @@ def _derive_agent_name(endpoint: str) -> str:
43
43
 
44
44
 
45
45
  @click.command("connect")
46
- @click.option("--endpoint", "-e", help="Bot config JSON or file path (agent path)")
46
+ @click.option("--endpoint", "-e", help="Agent config JSON or file path (agent path)")
47
47
  @click.option(
48
48
  "--vendor", "-v",
49
49
  type=click.Choice(["microsoft"]),
@@ -53,7 +53,7 @@ def _derive_agent_name(endpoint: str) -> str:
53
53
  @click.option("--prompt", "-p", type=click.Path(exists=True), help="System prompt file (agent path)")
54
54
  @click.option("--repo", "-r", type=click.Path(exists=True), help="Repository path (agent path)")
55
55
  @click.option("--openapi", "-o", type=click.Path(exists=True), help="OpenAPI spec file (agent path)")
56
- @click.option("--serve", "-s", is_flag=True, help="Launch repo bot locally (agent path, requires --repo)")
56
+ @click.option("--serve", "-s", is_flag=True, help="Launch repo agent locally (agent path, requires --repo)")
57
57
  @click.option("--tenant", help="Azure tenant ID (platform path, bypasses browser)")
58
58
  @click.option("--client-id", "client_id", help="Service principal client ID (platform path)")
59
59
  @click.option("--client-secret", "client_secret", help="Service principal secret (platform path)")
@@ -69,7 +69,7 @@ def connect_command(endpoint, vendor, name, prompt, repo, openapi, serve,
69
69
  \b
70
70
  Agent path (--endpoint):
71
71
  hb connect --endpoint ./bot-config.json
72
- Probes your bot, extracts scope, creates project, runs first test.
72
+ Probes your agent, extracts scope, creates project, runs first test.
73
73
 
74
74
  \b
75
75
  Platform path (--vendor):
@@ -97,10 +97,10 @@ def connect_command(endpoint, vendor, name, prompt, repo, openapi, serve,
97
97
  else:
98
98
  console.print("[yellow]Specify a path:[/yellow]")
99
99
  console.print()
100
- console.print(" [bold]Agent:[/bold] hb connect --endpoint ./bot-config.json")
100
+ console.print(" [bold]Agent:[/bold] hb connect --endpoint ./bot-config.json")
101
101
  console.print(" [bold]Platform:[/bold] hb connect --vendor microsoft")
102
102
  console.print()
103
- console.print("[dim]Use --endpoint to connect an AI agent, or --vendor to scan your cloud.[/dim]")
103
+ console.print("[dim]Use --endpoint to connect your AI agent, or --vendor to scan your cloud.[/dim]")
104
104
  raise SystemExit(1)
105
105
 
106
106
 
@@ -192,7 +192,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
192
192
 
193
193
  if runtime_info and not serve and not endpoint and not yes:
194
194
  console.print()
195
- console.print(f" [cyan]i[/cyan] Detected runnable bot: [bold]{runtime_info.framework.title()}[/bold] ({runtime_info.entry_point})")
195
+ console.print(f" [cyan]i[/cyan] Detected runnable agent: [bold]{runtime_info.framework.title()}[/bold] ({runtime_info.entry_point})")
196
196
  console.print(f" Start command: [dim]{runtime_info.start_cmd.replace('{port}', str(runtime_info.port))}[/dim]")
197
197
  from rich.prompt import Confirm
198
198
  if Confirm.ask(" Launch it for live probing?", default=False):
@@ -209,7 +209,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
209
209
  if spec_result:
210
210
  operations = spec_result.get("operations", [])
211
211
  console.print(f" [green]\u2713[/green] OpenAPI spec: {len(operations)} operations")
212
- summary_parts = [spec_result.get("description", "API-based bot")]
212
+ summary_parts = [spec_result.get("description", "API-based agent")]
213
213
  for op in operations:
214
214
  summary_parts.append(
215
215
  f"- {op.get('method', 'GET')} {op.get('path', '')}: {op.get('summary', '')}"
@@ -228,7 +228,7 @@ def _connect_agent(endpoint, name, prompt, repo, openapi, serve, context, yes, t
228
228
  # -- Serve lifecycle: start server + tunnel ----------------------------
229
229
  if serve and repo:
230
230
  if not runtime_info:
231
- console.print("[yellow]Could not detect a runnable bot in the repository.[/yellow]")
231
+ console.print("[yellow]Could not detect a runnable agent in the repository.[/yellow]")
232
232
  console.print("[dim]Continuing with static analysis only.[/dim]")
233
233
  else:
234
234
  _server, _tunnel, serve_source = _start_serve(
@@ -513,7 +513,7 @@ def _connect_platform(vendor, name, tenant, client_id, client_secret, yes, timeo
513
513
  def _auto_test(client, project_id, default_integration, context=None):
514
514
  """Run first test automatically and show results inline."""
515
515
  if not default_integration:
516
- console.print("\n[dim]No bot integration configured -- skipping auto-test.[/dim]")
516
+ console.print("\n[dim]No agent integration configured -- skipping auto-test.[/dim]")
517
517
  console.print("[dim]Run 'hb test -e ./bot-config.json' to test manually.[/dim]")
518
518
  return
519
519
 
@@ -569,5 +569,7 @@ def _auto_test(client, project_id, default_integration, context=None):
569
569
  console.print(f" [dim]View logs:[/dim] hb logs {exp_id}")
570
570
 
571
571
  except Exception as e:
572
+ import traceback
572
573
  console.print(f"\n[yellow]Auto-test failed:[/yellow] {e}")
574
+ console.print(f"[dim]{traceback.format_exc()}[/dim]")
573
575
  console.print("[dim]Run 'hb test' to try again.[/dim]")
@@ -18,7 +18,9 @@ console = Console()
18
18
  @click.option("--trends", is_flag=True, help="Show posture history over time")
19
19
  @click.option("--org", is_flag=True, help="Show org-level posture (3 dimensions)")
20
20
  @click.option("--coverage", is_flag=True, help="Include test coverage breakdown")
21
- def posture_command(project: str, as_json: bool, trends: bool, org: bool, coverage: bool):
21
+ def posture_command(
22
+ project: str, as_json: bool, trends: bool, org: bool, coverage: bool
23
+ ):
22
24
  """View security posture score for a project.
23
25
 
24
26
  The posture score is a composite metric (0-100) reflecting:
@@ -52,10 +54,13 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
52
54
  raise SystemExit(1)
53
55
 
54
56
  with console.status("Calculating organisation posture..."):
55
- response = client.get(f"organisations/{org_id}/posture", include_project=False)
57
+ response = client.get(
58
+ f"organisations/{org_id}/posture", include_project=False
59
+ )
56
60
 
57
61
  if as_json:
58
62
  import json
63
+
59
64
  print(json.dumps(response, indent=2, default=str))
60
65
  return
61
66
 
@@ -82,6 +87,7 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
82
87
 
83
88
  if as_json:
84
89
  import json
90
+
85
91
  print(json.dumps(response, indent=2, default=str))
86
92
  return
87
93
 
@@ -90,10 +96,13 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
90
96
 
91
97
  # Get posture from API
92
98
  with console.status("Calculating posture..."):
93
- response = client.get(f"projects/{project_id}/posture", include_project=True)
99
+ response = client.get(
100
+ f"projects/{project_id}/posture", include_project=True
101
+ )
94
102
 
95
103
  if as_json:
96
104
  import json
105
+
97
106
  print(json.dumps(response, indent=2, default=str))
98
107
  return
99
108
 
@@ -125,7 +134,7 @@ def posture_command(project: str, as_json: bool, trends: bool, org: bool, covera
125
134
 
126
135
  def _display_posture(posture: dict):
127
136
  """Display posture score with visual breakdown."""
128
- score = posture.get("score", 0)
137
+ score = posture.get("overall_score", 0)
129
138
  grade = posture.get("grade", _score_to_grade(score))
130
139
 
131
140
  # Color based on score
@@ -140,16 +149,21 @@ def _display_posture(posture: dict):
140
149
  emoji = "✗"
141
150
 
142
151
  # Main score panel
143
- console.print(Panel(
144
- f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
145
- title="Security Posture",
146
- border_style=score_color,
147
- padding=(1, 4),
148
- ))
152
+ console.print(
153
+ Panel(
154
+ f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
155
+ title="Security Posture",
156
+ border_style=score_color,
157
+ padding=(1, 4),
158
+ )
159
+ )
149
160
 
150
161
  # Breakdown table
151
- breakdown = posture.get("breakdown", {})
152
- if breakdown:
162
+ finding_metrics = posture.get("finding_metrics", {})
163
+ coverage_metrics = posture.get("coverage_metrics", {})
164
+ resilience_metrics = posture.get("resilience_metrics", {})
165
+
166
+ if finding_metrics or coverage_metrics or resilience_metrics:
153
167
  console.print("\n[bold]Score Breakdown:[/bold]\n")
154
168
 
155
169
  table = Table(show_header=True, header_style="bold")
@@ -159,15 +173,19 @@ def _display_posture(posture: dict):
159
173
  table.add_column("Bar", width=30)
160
174
 
161
175
  components = [
162
- ("Findings", breakdown.get("finding_score", 0), "40%"),
163
- ("Confidence", breakdown.get("confidence_score", 0), "25%"),
164
- ("Coverage", breakdown.get("coverage_score", 0), "20%"),
165
- ("Drift", breakdown.get("drift_score", 0), "15%"),
176
+ ("Findings", finding_metrics.get("score", 0), "40%"),
177
+ ("Confidence", finding_metrics.get("avg_confidence", 0), "25%"),
178
+ ("Coverage", coverage_metrics.get("score", 0), "20%"),
179
+ ("Resilience", resilience_metrics.get("score", 0), "15%"),
166
180
  ]
167
181
 
168
182
  for name, comp_score, weight in components:
169
183
  bar = _score_bar(comp_score)
170
- color = "green" if comp_score >= 80 else ("yellow" if comp_score >= 60 else "red")
184
+ color = (
185
+ "green"
186
+ if comp_score >= 80
187
+ else ("yellow" if comp_score >= 60 else "red")
188
+ )
171
189
  table.add_row(
172
190
  name,
173
191
  f"[{color}]{comp_score:.0f}[/{color}]",
@@ -309,14 +327,11 @@ def _calculate_fallback_posture(client: HumanboundClient, project_id: str):
309
327
  score = min(100, pass_rate)
310
328
 
311
329
  posture = {
312
- "score": score,
330
+ "overall_score": score,
313
331
  "grade": _score_to_grade(score),
314
- "breakdown": {
315
- "finding_score": pass_rate,
316
- "confidence_score": 80, # Placeholder
317
- "coverage_score": 70, # Placeholder
318
- "drift_score": 85, # Placeholder
319
- },
332
+ "finding_metrics": {"score": pass_rate},
333
+ "coverage_metrics": {"score": 70},
334
+ "resilience_metrics": {"score": 85},
320
335
  "recommendations": [],
321
336
  "last_tested": latest.get("created_at", "")[:10],
322
337
  }
@@ -353,12 +368,14 @@ def _display_org_posture(response: dict):
353
368
  emoji = "✗"
354
369
 
355
370
  # Main score panel
356
- console.print(Panel(
357
- f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
358
- title="Organisation Posture",
359
- border_style=score_color,
360
- padding=(1, 4),
361
- ))
371
+ console.print(
372
+ Panel(
373
+ f"[bold {score_color}]{emoji} {score}/100[/bold {score_color}] [dim]Grade: {grade}[/dim]",
374
+ title="Organisation Posture",
375
+ border_style=score_color,
376
+ padding=(1, 4),
377
+ )
378
+ )
362
379
 
363
380
  # Dimension breakdown
364
381
  dimensions = response.get("dimensions", {})
@@ -378,9 +395,13 @@ def _display_org_posture(response: dict):
378
395
 
379
396
  for key, label in dimension_labels.items():
380
397
  dim_data = dimensions.get(key, {})
381
- dim_score = dim_data.get("score", 0) if isinstance(dim_data, dict) else dim_data
398
+ dim_score = (
399
+ dim_data.get("score", 0) if isinstance(dim_data, dict) else dim_data
400
+ )
382
401
  bar = _score_bar(dim_score)
383
- color = "green" if dim_score >= 80 else ("yellow" if dim_score >= 60 else "red")
402
+ color = (
403
+ "green" if dim_score >= 80 else ("yellow" if dim_score >= 60 else "red")
404
+ )
384
405
  table.add_row(
385
406
  label,
386
407
  f"[{color}]{dim_score:.0f}[/{color}]",
@@ -410,7 +431,9 @@ def _display_coverage_section(response: dict):
410
431
  empty = bar_width - filled
411
432
  bar = f"[{cov_color}]{'█' * filled}[/{cov_color}][dim]{'░' * empty}[/dim]"
412
433
 
413
- console.print(f"\n[bold]Test Coverage:[/bold] {bar} [{cov_color}]{overall:.0f}%[/{cov_color}]")
434
+ console.print(
435
+ f"\n[bold]Test Coverage:[/bold] {bar} [{cov_color}]{overall:.0f}%[/{cov_color}]"
436
+ )
414
437
 
415
438
  # Category breakdown
416
439
  categories = response.get("categories", response.get("by_category", []))
@@ -427,7 +450,9 @@ def _display_coverage_section(response: dict):
427
450
 
428
451
  if total > 0:
429
452
  rate = (passed / total) * 100
430
- rate_color = "green" if rate >= 80 else ("yellow" if rate >= 50 else "red")
453
+ rate_color = (
454
+ "green" if rate >= 80 else ("yellow" if rate >= 50 else "red")
455
+ )
431
456
  rate_str = f"[{rate_color}]{rate:.0f}%[/{rate_color}]"
432
457
  else:
433
458
  rate_str = "[dim]-[/dim]"
@@ -441,7 +466,11 @@ def _display_coverage_section(response: dict):
441
466
  if gap_list:
442
467
  console.print(f"\n[yellow]Gaps ({len(gap_list)} untested):[/yellow]")
443
468
  for gap in gap_list[:5]:
444
- name = gap.get("category", gap.get("name", str(gap))) if isinstance(gap, dict) else str(gap)
469
+ name = (
470
+ gap.get("category", gap.get("name", str(gap)))
471
+ if isinstance(gap, dict)
472
+ else str(gap)
473
+ )
445
474
  console.print(f" - {name}")
446
475
  if len(gap_list) > 5:
447
476
  console.print(f" [dim]... and {len(gap_list) - 5} more[/dim]")
@@ -457,7 +486,9 @@ def _print_next(org: bool = False, has_coverage: bool = False):
457
486
  suggestions.append("hb report --org Generate org posture report")
458
487
  else:
459
488
  if not has_coverage:
460
- suggestions.append("hb posture --coverage Include test coverage breakdown")
489
+ suggestions.append(
490
+ "hb posture --coverage Include test coverage breakdown"
491
+ )
461
492
  suggestions.append("hb posture --trends View posture over time")
462
493
  suggestions.append("hb posture --org View org-level posture")
463
494
  suggestions.append("hb test Run tests to improve posture")
@@ -104,8 +104,8 @@ def _print_next(suggestions: list):
104
104
  )
105
105
  @click.option(
106
106
  "--endpoint", "-e",
107
- help="Bot integration config — JSON string or path to JSON file. "
108
- "Same shape as 'hb init --endpoint'. Overrides the project's default integration."
107
+ help="Agent integration config — JSON string or path to JSON file. "
108
+ "Same shape as 'hb connect --endpoint'. Overrides the project's default integration."
109
109
  )
110
110
  @click.option(
111
111
  "--category",
@@ -48,6 +48,8 @@ mcp = FastMCP(
48
48
  "the user wants a quick one-off scan or has no connector set up.\n\n"
49
49
 
50
50
  "CORE WORKFLOWS:\n"
51
+ " One-shot agent onboarding (fastest):\n"
52
+ " hb_connect — scan + create project + auto-test in a single call\n"
51
53
  " Discovery → Testing:\n"
52
54
  " hb_trigger_discovery → hb_list_inventory → hb_onboard_inventory_asset → hb_run_test\n"
53
55
  " Security Testing:\n"
@@ -62,7 +64,7 @@ mcp = FastMCP(
62
64
 
63
65
  "CLI-ONLY COMMANDS (suggest when relevant):\n"
64
66
  " • 'hb discover' — browser-based shadow AI discovery (no connector needed)\n"
65
- " • 'hb init' — interactive project setup with scope extraction from a URL\n"
67
+ " • 'hb connect --vendor microsoft' — browser-based platform discovery\n"
66
68
  " • 'hb sentinel setup' — configure continuous monitoring sentinel"
67
69
  ),
68
70
  )
@@ -1303,6 +1305,166 @@ def hb_break_campaign(campaign_id: str, project_id: Optional[str] = None) -> str
1303
1305
  return _err(e)
1304
1306
 
1305
1307
 
1308
+ # =========================================================================
1309
+ # CONNECT — one-shot agent onboarding (scan → project → test)
1310
+ # =========================================================================
1311
+
1312
+ @mcp.tool()
1313
+ def hb_connect(
1314
+ endpoint_config: str,
1315
+ name: Optional[str] = None,
1316
+ prompt_text: Optional[str] = None,
1317
+ context: Optional[str] = None,
1318
+ timeout: int = 180,
1319
+ ) -> str:
1320
+ """Connect an AI agent: probe it, create a project, and run the first security test — all in one call.
1321
+
1322
+ This is the fastest way to onboard an agent. It replicates the CLI
1323
+ command ``hb connect --endpoint <config> --yes``.
1324
+
1325
+ Flow:
1326
+ 1. Build extraction sources from the endpoint config (and optional prompt).
1327
+ 2. POST /scan — the backend probes the agent and extracts its scope.
1328
+ 3. Create a project with the extracted scope.
1329
+ 4. Auto-start the first security test (owasp_agentic, unit level).
1330
+
1331
+ After this tool returns, poll hb_get_experiment_status with the
1332
+ returned experiment_id until status is "Finished", then call
1333
+ hb_get_experiment_logs and hb_get_posture for results.
1334
+
1335
+ Args:
1336
+ endpoint_config: JSON string with the agent's chat-completion config.
1337
+ Example: {"streaming": false, "chat_completion": {"endpoint": "https://...",
1338
+ "headers": {"Authorization": "Bearer ..."}, "payload": {"content": "$PROMPT"}}}
1339
+ name: Project name (auto-derived from endpoint hostname if omitted).
1340
+ prompt_text: Optional system prompt text to include as an additional
1341
+ extraction source (improves scope quality).
1342
+ context: Extra context for the judge, e.g. "Authenticated as Alice,
1343
+ her PII is expected" (max 1500 chars).
1344
+ timeout: Request timeout in seconds for the scan call (default 180).
1345
+ """
1346
+ try:
1347
+ client = _get_client()
1348
+
1349
+ if not client.is_authenticated():
1350
+ return _err(ValueError("Not authenticated. The user must run 'hb login' first."))
1351
+ if not client.organisation_id:
1352
+ return _err(ValueError("No organisation selected. Use hb_set_organisation first."))
1353
+
1354
+ # -- Parse endpoint config ----------------------------------------
1355
+ try:
1356
+ bot_config = json.loads(endpoint_config)
1357
+ except json.JSONDecodeError as e:
1358
+ return _err(ValueError(f"Invalid JSON in endpoint_config: {e}"))
1359
+
1360
+ # -- Build sources array ------------------------------------------
1361
+ sources = [{"source": "endpoint", "data": bot_config}]
1362
+
1363
+ if prompt_text:
1364
+ sources.append({"source": "text", "data": {"text": prompt_text}})
1365
+
1366
+ # -- Derive project name ------------------------------------------
1367
+ if not name:
1368
+ try:
1369
+ ep_url = bot_config.get("chat_completion", {}).get("endpoint", "")
1370
+ if ep_url:
1371
+ from urllib.parse import urlparse
1372
+ hostname = urlparse(ep_url).hostname
1373
+ if hostname:
1374
+ name = hostname
1375
+ except Exception:
1376
+ pass
1377
+ if not name:
1378
+ name = "My Agent"
1379
+
1380
+ # -- POST /scan ---------------------------------------------------
1381
+ scan_response = client.post(
1382
+ "scan",
1383
+ data={"sources": sources},
1384
+ include_project=False,
1385
+ timeout=timeout,
1386
+ )
1387
+
1388
+ scope = scan_response.get("scope", {})
1389
+ risk_profile = scan_response.get("risk_profile", {})
1390
+ default_integration = scan_response.get("default_integration")
1391
+
1392
+ # -- Create project -----------------------------------------------
1393
+ project_data = {
1394
+ "name": name,
1395
+ "description": f"Project created via MCP hb_connect",
1396
+ "scope": scope,
1397
+ }
1398
+ if default_integration:
1399
+ project_data["default_integration"] = default_integration
1400
+
1401
+ project_result = client.post("projects", data=project_data)
1402
+ project_id = project_result.get("id")
1403
+
1404
+ # Auto-select the project
1405
+ client.set_project(project_id)
1406
+
1407
+ # -- Auto-test ----------------------------------------------------
1408
+ experiment_id = None
1409
+ auto_test_error = None
1410
+
1411
+ if default_integration:
1412
+ providers = client.list_providers()
1413
+ if providers:
1414
+ provider = next((p for p in providers if p.get("is_default")), providers[0])
1415
+
1416
+ configuration = {}
1417
+ if context:
1418
+ if len(context) > 1500:
1419
+ return _err(ValueError(f"Context too long ({len(context)} chars). Maximum is 1,500."))
1420
+ configuration["context"] = context
1421
+
1422
+ import time as _time
1423
+ experiment_data = {
1424
+ "name": f"connect-{_time.strftime('%Y%m%d-%H%M%S')}",
1425
+ "description": "Initial assessment from hb_connect (MCP)",
1426
+ "test_category": "humanbound/adversarial/owasp_agentic",
1427
+ "testing_level": "unit",
1428
+ "provider_id": provider.get("id"),
1429
+ "auto_start": True,
1430
+ "configuration": configuration,
1431
+ }
1432
+
1433
+ try:
1434
+ exp_result = client.post("experiments", data=experiment_data, include_project=True)
1435
+ experiment_id = exp_result.get("id")
1436
+ except Exception as e:
1437
+ auto_test_error = str(e)
1438
+ else:
1439
+ auto_test_error = "No providers configured. Add one with hb_add_provider, then run hb_run_test."
1440
+ else:
1441
+ auto_test_error = "No agent integration detected from scan — skipped auto-test. Run hb_run_test manually."
1442
+
1443
+ # -- Build response -----------------------------------------------
1444
+ result = {
1445
+ "project_id": project_id,
1446
+ "project_name": name,
1447
+ "scope": scope,
1448
+ "risk_profile": risk_profile,
1449
+ "has_integration": bool(default_integration),
1450
+ }
1451
+
1452
+ if experiment_id:
1453
+ result["experiment_id"] = experiment_id
1454
+ result["next_step"] = (
1455
+ f"Poll hb_get_experiment_status(experiment_id='{experiment_id}') "
1456
+ "until status is 'Finished', then call hb_get_experiment_logs "
1457
+ "and hb_get_posture for results."
1458
+ )
1459
+ elif auto_test_error:
1460
+ result["auto_test_skipped"] = auto_test_error
1461
+
1462
+ return _ok(result)
1463
+
1464
+ except HumanboundError as e:
1465
+ return _err(e)
1466
+
1467
+
1306
1468
  # =========================================================================
1307
1469
  # UPLOAD
1308
1470
  # =========================================================================
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: humanbound-cli
3
- Version: 0.6.1
3
+ Version: 0.6.2
4
4
  Summary: Humanbound CLI - command line interface for AI agent security testing.
5
5
  Author-email: Kostas Siabanis <hello@humanbound.ai>, Demetris Gerogiannis <hello@humanbound.ai>
6
6
  License: Apache-2.0
@@ -1,3 +1,4 @@
1
+ build
1
2
  dist
2
3
  docs
3
4
  humanbound_cli
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "humanbound-cli"
7
- version = "0.6.1"
7
+ version = "0.6.2"
8
8
  authors = [
9
9
  { name="Kostas Siabanis", email="hello@humanbound.ai" },
10
10
  { name="Demetris Gerogiannis", email="hello@humanbound.ai" },
File without changes
File without changes
File without changes