Skip to content

Commit f7105bb

Browse files
committed
Release 2.0.1: Azure Foundry v1 API, credentials CLI improvements
- Azure Foundry: use ChatOpenAI + base_url for cognitiveservices.azure.com/openai/v1 - Strip provider prefix (azure:gpt-5-mini -> gpt-5-mini) when calling API - credentials set: optional inline value and stdin; only mask api_key/token - .env.example: document correct Azure endpoint (openai/v1 not openai/responses) Made-with: Cursor
1 parent c2748f4 commit f7105bb

File tree

7 files changed

+427
-38
lines changed

7 files changed

+427
-38
lines changed

.env.example

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ GOOGLE_API_KEY=
99

1010
# --- Azure OpenAI (GPT: gpt-4o, gpt-5-mini on same endpoint) ---
1111
# When set, GPT models use Azure; deployment name = model name (e.g. gpt-4o, gpt-5-mini).
12-
# AZURE_OPENAI_ENDPOINT=https://your-resource.services.ai.azure.com/...
12+
# Use the CHAT COMPLETIONS base URL, not the Responses API URL:
13+
# - Correct (Azure Foundry): https://your-resource.cognitiveservices.azure.com/openai/v1
14+
# - Wrong: .../openai/responses (Responses API; causes 404 DeploymentNotFound)
15+
# AZURE_OPENAI_ENDPOINT=https://your-resource.cognitiveservices.azure.com/openai/v1
1316
# AZURE_OPENAI_API_KEY=
14-
# AZURE_OPENAI_API_VERSION=2024-05-01-preview
17+
# AZURE_OPENAI_API_VERSION=2025-04-01-preview
1518
# Optional default deployment if not passing model name: AZURE_OPENAI_DEPLOYMENT_NAME=gpt-5-mini
1619

1720
# --- Azure Anthropic (Claude: e.g. claude-opus-4-6-2) ---

CHANGELOG.md

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,44 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## [Unreleased]
99

10+
## [2.0.1] — 2026-03-10
11+
12+
### Added
13+
14+
- **Azure Foundry (v1 API) support** — When `AZURE_OPENAI_ENDPOINT` points to Azure Foundry (URL contains `cognitiveservices.azure.com` or `/openai/v1`), the provider uses the v1 chat-completions API via `ChatOpenAI` with `base_url` instead of the legacy deployment-path API, fixing 404s on Foundry resources.
15+
- **Credentials `set` inline and stdin**`hivemind credentials set <provider> <key> [value]` accepts an optional value; if omitted and stdin is not a TTY, reads value from stdin (e.g. `echo "https://..." | hivemind credentials set azure endpoint`).
16+
17+
### Changed
18+
19+
- **Credentials input masking** — Only sensitive keys (`api_key`, `token`) use hidden input; endpoint, deployment, and api_version prompts show typed input.
20+
- **Azure model spec** — Provider strips `provider:` prefix (e.g. `azure:gpt-5-mini``gpt-5-mini`) before sending to the API so deployment name is correct.
21+
- **.env.example** — Documents correct Azure Foundry endpoint (`.../openai/v1` for chat completions; avoid `.../openai/responses`).
22+
23+
## [2.0.0] — 2026-03-10
24+
25+
### Breaking Changes
26+
27+
- Provider config schema updated: existing provider strings unchanged, new backends require new config sections
28+
- Agent execution now routed through AgentSandbox by default (disable with `sandbox.enabled = false`)
29+
- Memory storage now redacts PII by default if `compliance.pii_redaction = true`
30+
31+
### Added
32+
33+
- Abstract LLM router with Ollama, vLLM, and custom OpenAI-compatible endpoint backends
34+
- Provider fallback chains: automatic failover across backends
35+
- Agent sandboxing: resource quotas, tool category restrictions, per-role sandbox profiles
36+
- Audit logging: append-only JSONL with chain integrity verification
37+
- PII redaction: regex + optional spaCy NER, configurable PII types
38+
- GDPR/CCPA compliance config section
39+
- Decision tree and rationale generation for every agent action
40+
- Simulation mode: dry-run planning without LLM calls or tool execution
41+
- `hivemind explain`, `hivemind simulate`, `hivemind audit` CLI commands
42+
- PROVIDER_FALLBACK event type
43+
44+
### Migration from 1.x
45+
46+
See docs/migration/v2.md
47+
1048
## [1.10.5] - 2026-03-10
1149

1250
### Added

hivemind/cli/main.py

Lines changed: 163 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -534,9 +534,9 @@ def _run_doctor() -> int:
534534

535535

536536
def _run_mcp_list() -> int:
537-
"""List all registered MCP servers and their tool counts."""
537+
"""List configured MCP servers and their tool counts (from live discovery)."""
538538
from hivemind.config import get_config
539-
from hivemind.tools.registry import list_tools
539+
from hivemind.tools.mcp import discover_mcp_tools
540540
cfg = get_config()
541541
servers = getattr(getattr(cfg, "mcp", None), "servers", None) or []
542542
try:
@@ -547,17 +547,14 @@ def _run_mcp_list() -> int:
547547
table.add_column("Name", style="cyan")
548548
table.add_column("Transport", style="dim")
549549
table.add_column("Tools", justify="right")
550-
all_tools = list_tools()
551-
mcp_tools = [t for t in all_tools if getattr(t, "category", "") == "mcp"]
552-
by_server: dict[str, int] = {}
553-
for t in mcp_tools:
554-
name = getattr(t, "name", "")
555-
if "." in name:
556-
srv = name.split(".", 1)[0]
557-
by_server[srv] = by_server.get(srv, 0) + 1
558550
for s in servers:
559551
sname = getattr(s, "name", "?")
560-
table.add_row(sname, getattr(s, "transport", "?"), str(by_server.get(sname, 0)))
552+
try:
553+
adapters = discover_mcp_tools(s)
554+
count = len(adapters)
555+
except Exception:
556+
count = "—"
557+
table.add_row(sname, getattr(s, "transport", "?"), str(count))
561558
if not servers:
562559
console.print("No MCP servers configured. Add [[mcp.servers]] to hivemind.toml or use [cyan]hivemind mcp add[/].")
563560
else:
@@ -1277,6 +1274,124 @@ def _run_checkpoint_restore(run_id: str) -> int:
12771274
return 0
12781275

12791276

1277+
def _run_audit_dispatch(args: object) -> int:
1278+
"""Audit: print table, export, or verify."""
1279+
from hivemind.config import get_config
1280+
from hivemind.audit.logger import AuditLogger
1281+
cmd = getattr(args, "audit_cmd", None)
1282+
run_id = getattr(args, "run_id", None)
1283+
export_fmt = getattr(args, "export", None)
1284+
if cmd == "verify":
1285+
run_id = getattr(args, "run_id", run_id)
1286+
if not run_id:
1287+
print("Error: run_id required for verify", file=sys.stderr)
1288+
return 1
1289+
cfg = get_config()
1290+
ok, msg = AuditLogger.verify(run_id, cfg.data_dir)
1291+
print(msg)
1292+
return 0 if ok else 1
1293+
if not run_id:
1294+
print("Error: run_id required (e.g. hivemind audit events_2025-03-10...)", file=sys.stderr)
1295+
return 1
1296+
cfg = get_config()
1297+
logger = AuditLogger(cfg.data_dir, run_id=run_id)
1298+
if export_fmt:
1299+
out = logger.export(run_id, format=export_fmt)
1300+
print(out)
1301+
return 0
1302+
out = logger.export(run_id, format="jsonl")
1303+
if not out:
1304+
print(f"No audit log for run_id={run_id}", file=sys.stderr)
1305+
return 1
1306+
try:
1307+
from rich.console import Console
1308+
from rich.table import Table
1309+
console = Console()
1310+
table = Table(title=f"Audit log: {run_id}")
1311+
table.add_column("timestamp")
1312+
table.add_column("event_type")
1313+
table.add_column("task_id")
1314+
table.add_column("resource")
1315+
table.add_column("success")
1316+
for line in out.strip().split("\n"):
1317+
if not line:
1318+
continue
1319+
import json
1320+
r = json.loads(line)
1321+
table.add_row(
1322+
r.get("timestamp", "")[:19],
1323+
r.get("event_type", ""),
1324+
r.get("task_id", ""),
1325+
r.get("resource", ""),
1326+
str(r.get("success", "")),
1327+
)
1328+
console.print(table)
1329+
except Exception:
1330+
print(out)
1331+
return 0
1332+
1333+
1334+
def _run_explain(args: object) -> int:
1335+
"""Explain: decision records for run or task."""
1336+
run_id = getattr(args, "run_id", None)
1337+
task_id = getattr(args, "task_id", None)
1338+
if not run_id:
1339+
print("Error: run_id required", file=sys.stderr)
1340+
return 1
1341+
try:
1342+
from hivemind.explainability.decision_tree import DecisionTreeBuilder
1343+
from hivemind.config import get_config
1344+
cfg = get_config()
1345+
events_dir = cfg.events_dir
1346+
builder = DecisionTreeBuilder()
1347+
records = builder.build_from_events(run_id, events_dir)
1348+
if not records:
1349+
print(f"No decision records for run_id={run_id}", file=sys.stderr)
1350+
return 1
1351+
if task_id:
1352+
records = [r for r in records if r.task_id == task_id]
1353+
if not records:
1354+
print(f"No task {task_id} in run {run_id}", file=sys.stderr)
1355+
return 1
1356+
for r in records:
1357+
print(f"--- {r.task_id} ---")
1358+
print(f" strategy: {r.strategy_selected}")
1359+
print(f" model: {r.model_selected} ({r.model_tier})")
1360+
print(f" tools: {r.tools_selected}")
1361+
print(f" confidence: {r.confidence:.0%}")
1362+
print(f" rationale: {r.rationale[:300]}..." if len(r.rationale or "") > 300 else f" rationale: {r.rationale}")
1363+
return 0
1364+
except Exception as e:
1365+
print(f"Error: {e}", file=sys.stderr)
1366+
return 1
1367+
1368+
1369+
def _run_simulate(args: object) -> int:
1370+
"""Simulate: dry-run planning, no LLM or tools."""
1371+
import asyncio
1372+
task = getattr(args, "task", "")
1373+
cost_only = getattr(args, "cost_only", False) or getattr(args, "cost", False)
1374+
if not task:
1375+
print("Error: task required (e.g. hivemind simulate \"Summarize X\")", file=sys.stderr)
1376+
return 1
1377+
try:
1378+
from hivemind.explainability.simulation import SimulationMode
1379+
sim = SimulationMode()
1380+
report = asyncio.run(sim.simulate(task))
1381+
if cost_only:
1382+
print(f"Estimated cost: {getattr(report, 'estimated_cost', 'N/A')}")
1383+
return 0
1384+
print(f"Tasks: {len(report.task_list)}")
1385+
for t in report.task_list:
1386+
print(f" - {t}")
1387+
print(f"Estimated cost: {getattr(report, 'estimated_cost', 'N/A')}")
1388+
print(f"Estimated duration: {getattr(report, 'estimated_duration', 'N/A')}")
1389+
return 0
1390+
except Exception as e:
1391+
print(f"Error: {e}", file=sys.stderr)
1392+
return 1
1393+
1394+
12801395
def _run_health(args: object) -> int:
12811396
"""Run health checks. Exit 0 if healthy, 1 otherwise. Print ✓/✗ per check."""
12821397
import asyncio
@@ -1867,6 +1982,7 @@ def main() -> int:
18671982
epilog="""
18681983
Examples:
18691984
hivemind credentials set openai api_key
1985+
hivemind credentials set azure endpoint \"https://.../openai/v1\"
18701986
hivemind credentials list
18711987
hivemind credentials migrate
18721988
hivemind credentials export azure # print env KEY=value for sourcing
@@ -1890,6 +2006,11 @@ def main() -> int:
18902006
nargs="?",
18912007
help="Key name (e.g. api_key)",
18922008
)
2009+
credentials_parser.add_argument(
2010+
"value",
2011+
nargs="?",
2012+
help="Value (for set only). Omit to be prompted, or pipe: echo 'val' | hivemind credentials set azure endpoint",
2013+
)
18932014
credentials_parser.set_defaults(func=lambda a: _run_credentials(a))
18942015

18952016
completion_parser = subparsers.add_parser(
@@ -1965,6 +2086,37 @@ def main() -> int:
19652086
checkpoint_restore_p.set_defaults(func=_run_checkpoint_dispatch)
19662087
checkpoint_parser.set_defaults(checkpoint_cmd="list", func=_run_checkpoint_dispatch)
19672088

2089+
audit_parser = subparsers.add_parser(
2090+
"audit",
2091+
help="View or export audit log for a run",
2092+
description="Print audit log as table, export to CSV/JSONL, or verify chain integrity.",
2093+
)
2094+
audit_parser.add_argument("run_id", nargs="?", default=None, help="Run ID (e.g. events_...)")
2095+
audit_parser.add_argument("--export", choices=["jsonl", "csv", "siem"], default=None, help="Export format")
2096+
audit_sub = audit_parser.add_subparsers(dest="audit_cmd", help="Subcommand")
2097+
audit_verify_p = audit_sub.add_parser("verify", help="Verify audit log chain integrity")
2098+
audit_verify_p.add_argument("run_id", help="Run ID to verify")
2099+
audit_verify_p.set_defaults(audit_cmd="verify")
2100+
audit_parser.set_defaults(func=_run_audit_dispatch)
2101+
2102+
explain_parser = subparsers.add_parser(
2103+
"explain",
2104+
help="Show decision records for a run or task",
2105+
description="Print decision tree and rationale for agent actions.",
2106+
)
2107+
explain_parser.add_argument("run_id", help="Run ID")
2108+
explain_parser.add_argument("task_id", nargs="?", default=None, help="Optional task ID for single task")
2109+
explain_parser.set_defaults(func=_run_explain)
2110+
2111+
simulate_parser = subparsers.add_parser(
2112+
"simulate",
2113+
help="Dry-run planning without LLM or tool execution",
2114+
description="Run planner and scheduler only; output task list and cost estimate.",
2115+
)
2116+
simulate_parser.add_argument("task", help="Root task description")
2117+
simulate_parser.add_argument("--cost", action="store_true", help="Print cost estimate only")
2118+
simulate_parser.set_defaults(func=_run_simulate)
2119+
19682120
health_parser = subparsers.add_parser(
19692121
"health",
19702122
help="Health and readiness check",

hivemind/credentials/cli.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@
2424
"azure_anthropic": ["endpoint", "api_key", "deployment"],
2525
}
2626

27+
# Keys that must never be shown in list (only "(stored)"); all others can show value
28+
SENSITIVE_KEYS = {"api_key", "token"}
29+
2730
# (provider, key) -> env var name for export
2831
PROVIDER_KEY_TO_ENV: dict[tuple[str, str], str] = {
2932
("openai", "api_key"): "OPENAI_API_KEY",
@@ -66,20 +69,37 @@ def _run_credentials_export(provider: str) -> int:
6669
return 0
6770

6871

69-
def _run_credentials_set(provider: str, key: str) -> int:
70-
"""Prompt for value and store credential."""
72+
def _run_credentials_set(provider: str, key: str, value: str | None = None) -> int:
73+
"""Store credential. Value can be passed inline, read from stdin, or prompted interactively."""
7174
if provider not in KNOWN_CREDENTIALS:
7275
print(f"Unknown provider: {provider}", file=sys.stderr)
7376
return 1
7477
if key not in KNOWN_CREDENTIALS[provider]:
7578
print(f"Unknown key for {provider}: {key}", file=sys.stderr)
7679
return 1
77-
try:
78-
value = getpass.getpass(f"Enter value for {provider}/{key}: ")
79-
except (KeyboardInterrupt, EOFError):
80-
print("\nCancelled.", file=sys.stderr)
81-
return 130
82-
if not value.strip():
80+
81+
if value is not None and value.strip():
82+
# Inline value from CLI
83+
pass
84+
elif not sys.stdin.isatty():
85+
# Piped input: echo "val" | hivemind credentials set ...
86+
try:
87+
value = sys.stdin.read().strip()
88+
except (KeyboardInterrupt, EOFError):
89+
print("\nCancelled.", file=sys.stderr)
90+
return 130
91+
else:
92+
# Interactive prompt
93+
try:
94+
if key in SENSITIVE_KEYS:
95+
value = getpass.getpass(f"Enter value for {provider}/{key}: ")
96+
else:
97+
value = input(f"Enter value for {provider}/{key}: ")
98+
except (KeyboardInterrupt, EOFError):
99+
print("\nCancelled.", file=sys.stderr)
100+
return 130
101+
102+
if not value or not value.strip():
83103
print("Empty value, not stored.", file=sys.stderr)
84104
return 1
85105
set_credential(provider, key, value.strip())
@@ -88,16 +108,23 @@ def _run_credentials_set(provider: str, key: str) -> int:
88108

89109

90110
def _run_credentials_list() -> int:
91-
"""List credentials as table (provider, key). Never shows values."""
111+
"""List credentials as table (provider, key, value). Secret keys show '(stored)', others show value."""
92112
creds = list_credentials()
93113
if not creds:
94114
print("No credentials stored.")
95115
return 0
96116
table = Table(title="Stored credentials")
97117
table.add_column("Provider", style="cyan")
98118
table.add_column("Key", style="green")
119+
table.add_column("Value", style="dim")
99120
for c in creds:
100-
table.add_row(c["provider"], c["key"])
121+
p, k = c["provider"], c["key"]
122+
if k in SENSITIVE_KEYS:
123+
value = "(stored)"
124+
else:
125+
val = get_credential(p, k)
126+
value = (val[:60] + "…") if val and len(val) > 60 else (val or "")
127+
table.add_row(p, k, value)
101128
console = Console()
102129
console.print(table)
103130
return 0
@@ -147,9 +174,10 @@ def run_credentials(args: object) -> int:
147174

148175
if sub == "set":
149176
if not provider or not key:
150-
print("Usage: hivemind credentials set <provider> <key>", file=sys.stderr)
177+
print("Usage: hivemind credentials set <provider> <key> [value]", file=sys.stderr)
151178
return 1
152-
return _run_credentials_set(provider, key)
179+
value = getattr(args, "value", None)
180+
return _run_credentials_set(provider, key, value)
153181
if sub == "list":
154182
return _run_credentials_list()
155183
if sub == "delete":

0 commit comments

Comments
 (0)