feat: agent substrate — dry-run JSON, audit log, endpoint caution (0.2.0)#9
Merged
Conversation
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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
bclibetter, rather thanbclibecoming an agent runtime itself. Three orthogonal additions, bundled because they share a single mental model:--dry-runoutput. Write commands (post/patch/delete/attach upload) emit a stable JSON envelope on stdout when-f json/ndjson/rawis 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. Seedocs/write-operations.md.[audit]config section persists every CLI write to a per-profile JSONL file with response status, BCcorrelation_id, latency, redacted request body, and outcome (completed/failed/dry_run). Single-backup rotation bounds disk usage. SDK does NOT auto-emit. Seedocs/configuration.md#audit-log.cautionflag.EndpointMetadata.caution(low|medium|high) populated automatically by importers via a verb-name heuristic (post/release/cancel/void/reverse/apply/unapply→high). Surfaced inbcli endpoint infoand thelist_endpointsMCP tool so agents can require explicit user confirmation before touching posted/closed records.Also bumps
pyproject.tomlto 0.2.0, refreshesuv.lock(CI usesuv sync --locked), updatesCHANGELOG.md, and adds three new agent recipes toAGENTS.mdcovering dry-run-first writes, caution interpretation, and audit-log location.A second commit fixes three [P2] findings from a
codex reviewpass:-f jsonis now honored by--dry-run(was being shadowed by globalstate.format).resolved_urlfor completed/failed writes instead ofnull.attach upload --standarddry-runs now show the standard/api/v2.0/URL the actual upload will hit, not the custom registry route. Refactored URL resolution into sharedsrc/bcli_cli/_url_resolve.py.OSS-genericism guarded throughout: no domain-specific endpoint names, no aviation/Beautech vocabulary, generic examples in
examples/queries/sample.yamlleft untouched.Test plan
uv run pytest tests/ -v(528 passing locally)uv run ruff check src/bcli --dry-run -f json post customers --data '{"displayName":"X"}'returns the JSON envelopebcli --dry-run post customers --data '{"displayName":"X"}'(no-f) shows the human panel with resolved URLbcli endpoint info salesInvoicePostshowscaution: high[audit]in~/.config/bcli/config.toml, run a--dry-run, confirm~/.config/bcli/audit/<profile>.jsonlgets adry_runentryv0.2.0, push tag, confirmpublish.ymluploadsbc-cli==0.2.0to PyPIuv tool install bc-cli --upgradepulls 0.2.0 cleanly