Skip to content

feat: agent substrate — dry-run JSON, audit log, endpoint caution (0.2.0)#9

Merged
igor-ctrl merged 3 commits into
mainfrom
feat/agent-substrate
May 7, 2026
Merged

feat: agent substrate — dry-run JSON, audit log, endpoint caution (0.2.0)#9
igor-ctrl merged 3 commits into
mainfrom
feat/agent-substrate

Conversation

@igor-ctrl
Copy link
Copy Markdown
Owner

Summary

Phase 1 of the agent-substrate work. We're investing in the deterministic CLI/SDK layer so external agents (Claude Code, Cursor, Codex via MCP) drive bcli better, rather than bcli becoming an agent runtime itself. Three orthogonal additions, bundled because they share a single mental model:

  • Structured --dry-run output. Write commands (post / patch / delete / attach upload) emit a stable JSON envelope on stdout when -f json / ndjson / raw is selected — dry_run, method, endpoint, resolved_url, profile, environment, company_id, body, record_id. Agents can show the user what would happen before approving a real call. Human format keeps the rich panel on stderr but is now augmented with the resolved URL + profile context. See docs/write-operations.md.
  • Opt-in audit log. New [audit] config section persists every CLI write to a per-profile JSONL file with response status, BC correlation_id, latency, redacted request body, and outcome (completed / failed / dry_run). Single-backup rotation bounds disk usage. SDK does NOT auto-emit. See docs/configuration.md#audit-log.
  • Endpoint caution flag. EndpointMetadata.caution (low|medium|high) populated automatically by importers via a verb-name heuristic (post/release/cancel/void/reverse/apply/unapplyhigh). Surfaced in bcli endpoint info and the list_endpoints MCP tool so agents can require explicit user confirmation before touching posted/closed records.

Also bumps pyproject.toml to 0.2.0, refreshes uv.lock (CI uses uv sync --locked), updates CHANGELOG.md, and adds three new agent recipes to AGENTS.md covering dry-run-first writes, caution interpretation, and audit-log location.

A second commit fixes three [P2] findings from a codex review pass:

  1. Subcommand -f json is now honored by --dry-run (was being shadowed by global state.format).
  2. Audit entries record the actual resolved_url for completed/failed writes instead of null.
  3. attach upload --standard dry-runs now show the standard /api/v2.0/ URL the actual upload will hit, not the custom registry route. Refactored URL resolution into shared src/bcli_cli/_url_resolve.py.

OSS-genericism guarded throughout: no domain-specific endpoint names, no aviation/Beautech vocabulary, generic examples in examples/queries/sample.yaml left untouched.

Test plan

  • CI green: uv run pytest tests/ -v (528 passing locally)
  • CI green: uv run ruff check src/
  • Smoke: bcli --dry-run -f json post customers --data '{"displayName":"X"}' returns the JSON envelope
  • Smoke: bcli --dry-run post customers --data '{"displayName":"X"}' (no -f) shows the human panel with resolved URL
  • Smoke: bcli endpoint info salesInvoicePost shows caution: high
  • Smoke: enable [audit] in ~/.config/bcli/config.toml, run a --dry-run, confirm ~/.config/bcli/audit/<profile>.jsonl gets a dry_run entry
  • After merge: tag v0.2.0, push tag, confirm publish.yml uploads bc-cli==0.2.0 to PyPI
  • After publish: uv tool install bc-cli --upgrade pulls 0.2.0 cleanly

igor-ctrl added 3 commits May 6, 2026 13:15
Phase 1 of the agent-substrate work: invest in the deterministic CLI
layer so external agents (Claude Code, Cursor, Codex via MCP) drive bcli
better, without bcli becoming an agent runtime itself.

Three orthogonal additions, bundled because they share a single mental
model and are individually too small to justify separate PRs:

* Structured --dry-run output. Write commands (post / patch / delete /
  attach upload) emit a stable JSON envelope on stdout when the active
  format is machine-readable (json / ndjson / raw). Includes resolved
  URL, profile context, and the request body — agents can show users
  what would happen before committing to a real call. Human format
  keeps the rich panel on stderr, now augmented with the resolved URL.

* Opt-in audit log. New [audit] config section persists every CLI write
  to a per-profile JSONL file with response status, BC correlation_id,
  latency, redacted body, and outcome (completed / failed / dry_run).
  Single-backup rotation bounds disk usage. SDK does NOT auto-emit;
  audit is a CLI-layer ergonomic on top of BC permission sets.

* Endpoint caution flag. EndpointMetadata.caution (low / medium / high)
  surfaces in `bcli endpoint info` and the list_endpoints MCP tool.
  Importers populate it via a verb-name heuristic — entities containing
  post / release / cancel / void / reverse / apply / unapply are flagged
  high so agents can require explicit user confirmation before touching
  posted/closed records.

Also updates AGENTS.md with three new agent recipes (dry-run-first,
caution interpretation, audit-log location) and bumps to 0.2.0.

523 tests pass (+32), ruff clean.
Three correctness gaps codex flagged on the v0.2.0 commit:

1. Subcommand `-f json` was not honored by --dry-run. The write commands
   resolved `output_format = format or state.format` into a local but
   never wrote it back to state.format, so render_dry_run (which reads
   state.format) emitted the human preview instead of JSON when -f was
   placed after the subcommand. Fixed by syncing state.format = output_format
   in post / patch / delete / attach (both subcommands) — matches the
   existing state.quiet pattern.

2. Audit entries for completed/failed writes recorded resolved_url: null
   despite the documented contract. Each _audited_xxx wrapper now resolves
   the URL up front and threads it through audited_write so the entry
   captures the actual route hit (custom registry vs standard v2.0,
   correct company, correct env).

3. `attach upload --standard` produced misleading dry-run + audit URLs.
   When the profile has a custom documentAttachments registry entry, the
   actual upload bypasses it and uses /api/v2.0/, but the preview still
   showed the custom route. render_dry_run and try_resolve_url now accept
   force_standard, mirroring upload_attachment's branch.

Refactor: extract the URL-resolution logic to a new src/bcli_cli/_url_resolve.py
shared by both dry-run and audit paths. Single source of truth for the
"best-effort, never raise" contract.

Tests: +5 covering each finding (force_standard preview, audited_post URL
recording, audited_delete URL recording, plus failure-path resolved_url
in the wrapper). 528 passing total, ruff clean.
@igor-ctrl igor-ctrl merged commit 0dcb7f2 into main May 7, 2026
3 checks passed
@igor-ctrl igor-ctrl deleted the feat/agent-substrate branch May 7, 2026 00:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant