Skip to content

feat(cli): agent automation contract with JSON output, documented exit codes, and wait/timeout flags#825

Merged
stubbi merged 5 commits into
mainfrom
feat/cli-json-automation-775
Jul 11, 2026
Merged

feat(cli): agent automation contract with JSON output, documented exit codes, and wait/timeout flags#825
stubbi merged 5 commits into
mainfrom
feat/cli-json-automation-775

Conversation

@stubbi

@stubbi stubbi commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Thinking Path

  • Mitos boots Firecracker microVMs and forks them via copy-on-write snapshots, exposed through CRDs and a thin mitos CLI over a Backend seam.
  • The CLI (cmd/mitos, internal/agentcli) is the surface an agent or a shell pipeline drives, but it had no machine-readable output, no documented exit-code contract, and no uniform way to bound a lifecycle wait.
  • The API side already carries the LLM-legible error rule (docs/api/v2-spec.md 2.3); the CLI had not been brought up to the same automation bar, which is an "Experience is DNA" gap (no intent-shaped surface for a non-human caller).
  • This is a first, coherent increment of the agent-automation work: it makes the read verbs emit JSON, defines the exit codes as a named contract, and adds --wait/--timeout to the lifecycle verbs, all behind the existing hand-rolled dispatcher (no Cobra).
  • This pull request adds -o json/--json on sandbox ls, ws ls, and ws log; a documented exit-code contract in one place; and uniform --wait/--no-wait and --timeout on sandbox create, sandbox fork, the fork alias, and --timeout on terminate.
  • The benefit is a CLI an agent can call and branch on deterministically: JSON to parse, exit codes to switch on, and bounded waits that fail with a distinct timeout code instead of hanging.

Linked Issues or Issue Description

Related: #775

This lands the FIRST increment of #775 (JSON output, exit-code contract, wait/timeout). It intentionally defers the cp, logs, and raw api verbs and stdin-secret input to a named follow-up (see docs/cli.md Follow-ups), so the issue stays open for that second increment.

What Changed

  • New internal/agentcli/exitcodes.go: named exit-code constants (ExitOK, ExitError, ExitUsage, ExitNotFound, ExitTimeout), an ErrNotFound sentinel, an exitCodeFor classifier, and the --wait/--timeout context seam (lifecycleContext, withNoWait).
  • New internal/agentcli/output.go: -o json / --output json / --json flag parsing plus stable JSON envelopes for sandbox ls ({"sandboxes": [...]}), ws ls ({"workspaces": [...]}), and ws log ({"revisions": [...]}). Empty listings render [], never null.
  • commands.go and cli.go: the read verb sandbox ls gains -o json; sandbox create, sandbox fork (and the fork alias) gain --wait/--no-wait/--timeout; terminate gains --timeout. Every dispatcher return now uses the named exit-code constants, and backend errors flow through exitCodeFor so not-found and timeout map to their codes.
  • workspace_cmd.go: ws ls and ws log gain -o json and the exit-code contract.
  • clusterbackend.go: a deleted or missing sandbox now wraps ErrNotFound (via apierrors.IsNotFound), and create --no-wait returns the client-assigned name without polling for Ready.
  • backend.go: the FakeBackend records the no-wait signal and deadline presence so the flags are testable offline.
  • docs/cli.md: new "Agent automation contract" section with the exit-code table, the JSON envelopes, and the wait/timeout semantics; Follow-ups names the deferred cp/logs/api/stdin-secret work.

Verification

  • go test ./internal/agentcli/... ./cmd/mitos/... passes (new automation_test.go covers JSON shapes, empty-array, unknown-format usage error, not-found and timeout exit codes, and the --wait/--no-wait/--timeout flag wiring; existing suite green).
  • gofmt -l clean on the changed files.
  • golangci-lint run ./internal/agentcli/... ./cmd/mitos/... --timeout=5m clean, both default and GOOS=linux.
  • go build ./... and go vet ./internal/agentcli/... ./cmd/mitos/... clean.

Risks

Low risk. The change is CLI-surface only: no controller, forkd, fork engine, or guest changes, so no fork-correctness or node-loss surface moves. Defaults are unchanged (--wait defaults to true, human table stays the default output), so existing scripts and tests keep their behavior. --no-wait on sandbox fork is accepted uniformly but the cluster backend still waits there because child ids are assigned during readiness; this is documented.

Model Used

Claude Opus 4.8 (1M context), model id claude-opus-4-8, extended thinking enabled.

Checklist

  • PR title is a conventional commit (feat, fix, docs, ci, chore, refactor, test)
  • Thinking Path traces from project context to this change
  • Model Used is filled in (with version and capability details)
  • Tests added for behavior changes, in the same commit (TDD)
  • Docs updated in the same PR
  • Threat-model delta (docs/threat-model.md) included if the security surface moved
  • Benchmark run (bench/) included if the hot path was touched
  • No em or en dashes introduced anywhere
  • Secret values never logged, in errors, in condition messages, or on host paths
  • No internal/instance-local references (only public #NNN / mitos-run/mitos URLs)
  • Every commit carries a Signed-off-by trailer (git commit -s)

Summary by CodeRabbit

  • New Features

    • Added stable JSON output for sandbox ls, ws ls, and ws log (revision logs), including consistent empty-list behavior.
    • Expanded lifecycle flags for sandbox create, sandbox fork, and sandbox terminate with standardized --wait/--no-wait and --timeout semantics.
    • Standardized CLI exit codes and refreshed command usage text.
  • Bug Fixes

    • Improved not-found handling across sandbox/workspace operations and mapped timeout/not-found to documented exit codes.
    • Ensured CLI output honors “last flag wins” and updated readiness handling for no-wait behavior.
  • Documentation

    • Updated CLI docs with the agent automation contract and lifecycle timing semantics.
  • Tests

    • Added CLI automation tests covering JSON shapes, flag wiring, and exit-code mappings.

…t codes, and wait/timeout flags

Make the mitos CLI scriptable by an agent or a shell pipeline:

- Documented exit-code contract: named constants (ExitOK, ExitError,
  ExitUsage, ExitNotFound, ExitTimeout) in one place (exitcodes.go), used
  consistently across the dispatcher, sandbox verbs, and workspace verbs. A
  not-found sentinel (ErrNotFound) is wrapped by the cluster backend so a
  missing sandbox maps to exit 3, and a --timeout deadline maps to exit 124.
- Structured output: -o json / --output json / --json on the read verbs
  (sandbox ls, ws ls, ws log) with stable documented shapes. Human table stays
  the default; an unknown format is a usage error, never a silent fall-back.
- Uniform --wait / --no-wait and --timeout on the lifecycle verbs (sandbox
  create, sandbox fork, the fork alias; --timeout on terminate). --timeout
  bounds the wait via a context deadline; --no-wait skips the readiness poll on
  the cluster create path.

Docs: docs/cli.md gains an agent-automation section with the exit-code table,
the JSON envelopes, and the wait/timeout semantics. cp, logs, a raw api
passthrough, and stdin secrets are named as explicit follow-ups.

Refs #775

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 2717845f-f26a-4234-a0d7-bc22e258ac42

📥 Commits

Reviewing files that changed from the base of the PR and between cb4e3dd and c8c0a0e.

📒 Files selected for processing (1)
  • internal/agentcli/cli.go

📝 Walkthrough

Walkthrough

This PR defines a stable agent-automation CLI contract for mitos, adding named exit codes, lifecycle timing flags, JSON output for read commands, backend propagation of wait/timeout state, and tests/docs to match.

Changes

Agent automation contract implementation

Layer / File(s) Summary
Exit-code and lifecycle context primitives
internal/agentcli/exitcodes.go
Adds named exit codes, not-found classification, and lifecycle context helpers for no-wait and timeout handling.
Structured output rendering
internal/agentcli/output.go
Adds output-flag parsing, JSON encoding, and stable envelopes for sandbox, workspace, and revision listings.
Lifecycle backend behavior and resource errors
internal/agentcli/backend.go, internal/agentcli/clusterbackend.go, internal/agentcli/clusterbackend_test.go
Records lifecycle metadata, implements no-wait creation and forking, and maps missing resources to not-found errors.
CLI usage and top-level exit wiring
internal/agentcli/cli.go
Updates usage text and top-level dispatch to use named exit codes.
Sandbox lifecycle and output commands
internal/agentcli/commands.go
Wires sandbox lifecycle flags, output parsing, positional IDs, and standardized error results.
Workspace listing and log output
internal/agentcli/workspace_cmd.go
Adds structured output and classified backend errors for ws ls and ws log.
Automation validation and CLI contract documentation
internal/agentcli/automation_test.go, docs/cli.md, internal/agentcli/hostedbackend_test.go
Tests JSON shapes, exit codes, lifecycle flags, and argument handling while documenting the automation contract and follow-ups.

Estimated code review effort: 3 (Moderate) | ~30 minutes

Sequence Diagram(s)

sequenceDiagram
  participant CLI as agentcli.Run
  participant Commands as cmdSandbox
  participant Backend as ClusterBackend
  participant Output as renderList
  CLI->>Commands: invoke sandbox or workspace verb
  Commands->>Backend: pass lifecycle context and call backend operation
  Backend-->>Commands: result or classified error
  Commands->>Output: render JSON or human output
  Output-->>CLI: output and stable exit code
Loading

Possibly related issues

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is concise, conventional, and accurately summarizes the main CLI automation contract changes.
Description check ✅ Passed The description includes all required sections with concrete details for the change, verification, risks, and follow-up issue.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/cli-json-automation-775

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/agentcli/commands.go (1)

166-233: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

splitFirstPositional still breaks value-taking flags before the sandbox id

fork --replicas 3 sbx-1 and terminate --timeout 5 sbx-1 both treat the flag value as the id, so parsing later fails on sbx-1. Make positional extraction aware of flags that consume values, or parse flags before splitting out the id.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agentcli/commands.go` around lines 166 - 233, `cmdSandboxFork` and
`cmdSandboxTerminate` are still using `splitFirstPositional` in a way that grabs
values for flags like `--replicas` and `--timeout` as the sandbox id, causing
later parse failures. Fix the argument handling so value-taking flags are
recognized before extracting the id, either by parsing flags first or by making
`splitFirstPositional` skip flag values. Keep the behavior aligned in both
command handlers and preserve support for id-before/after-flags forms.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@internal/agentcli/automation_test.go`:
- Around line 198-214: Add a fork CLI test that exercises the “id after flags”
ordering so the splitFirstPositional parsing bug is covered. In
TestForkTimeoutAndWaitFlags, alongside the existing fork invocation, add a case
using runCLI with the sandbox id placed after value-taking flags like --count
and --timeout, and assert it still records a fork call with the expected
replicas and deadline/no-wait behavior. This should validate the parsing fix in
the command handling path that uses splitFirstPositional and protect against
regressions when the positional id comes after flags.

In `@internal/agentcli/commands.go`:
- Around line 194-196: Honor the no-wait path in ClusterBackend.Fork: the
current fork flow always proceeds into waitChildrenReady even when --no-wait is
requested, so sandbox fork blocks until children are Ready. Update Fork (and the
caller in commands.go that builds octx with lifecycleContext) to check the same
noWaitRequested(ctx) condition used by Create and return immediately after the
fork succeeds when no-wait is set, skipping waitChildrenReady and any readiness
polling.

In `@internal/agentcli/output.go`:
- Around line 20-61: In extractOutputFlag, the format selection is not honoring
“last flag wins” because setFormat only turns jsonOut on for "json" and never
clears it for human/table/text values. Update setFormat so the later occurrence
of a human format resets jsonOut to false, and keep the parsing behavior
consistent for --json, -o/--output, and the --output=/-o= forms.

In `@internal/agentcli/workspace_cmd.go`:
- Around line 53-101: The ws ls and ws log handlers repeat the same output-flag
parsing and JSON/text rendering flow, which should be centralized. Extract a
small shared helper around the workspace command handling in workspace_cmd.go
(for example, the logic used by the ls and log branches) that takes the backend
result plus JSON/text render functions and returns the appropriate exit code.
Update the existing ls and log cases to call that helper so the behavior stays
consistent and easier to extend alongside the similar pattern in commands.go.
- Around line 76-90: The ws log path in workspace_cmd.go is treating a
nonexistent workspace as success because b.Log returns an empty revision list
instead of an error. Update the Log method so it returns ErrNotFound when the
workspace itself is missing, and keep the existing ws log error handling in the
command path so exitCodeFor(err) maps that case to ExitNotFound. Use the Log and
exitCodeFor symbols to locate the behavior and ensure the not-found case no
longer prints a success response.

---

Outside diff comments:
In `@internal/agentcli/commands.go`:
- Around line 166-233: `cmdSandboxFork` and `cmdSandboxTerminate` are still
using `splitFirstPositional` in a way that grabs values for flags like
`--replicas` and `--timeout` as the sandbox id, causing later parse failures.
Fix the argument handling so value-taking flags are recognized before extracting
the id, either by parsing flags first or by making `splitFirstPositional` skip
flag values. Keep the behavior aligned in both command handlers and preserve
support for id-before/after-flags forms.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a71d5f87-7e3e-4a4e-bbcf-77ef3a6b1b0f

📥 Commits

Reviewing files that changed from the base of the PR and between f74f4f0 and 210487c.

📒 Files selected for processing (9)
  • docs/cli.md
  • internal/agentcli/automation_test.go
  • internal/agentcli/backend.go
  • internal/agentcli/cli.go
  • internal/agentcli/clusterbackend.go
  • internal/agentcli/commands.go
  • internal/agentcli/exitcodes.go
  • internal/agentcli/output.go
  • internal/agentcli/workspace_cmd.go

Comment thread internal/agentcli/automation_test.go
Comment thread internal/agentcli/commands.go
Comment thread internal/agentcli/output.go
Comment thread internal/agentcli/workspace_cmd.go Outdated
Comment thread internal/agentcli/workspace_cmd.go
- parse a sandbox id that follows value-taking flags: splitFirstPositional
  grabbed a flag value (fork --count 2 sbx-1 took "2"). Replace it with
  parseLeadingID, a double-parse that honors the id before or after flags, and
  cover both orderings plus terminate --timeout <id>.
- honor --no-wait in ClusterBackend.Fork: it now returns the child ids as soon
  as they are named in Status.Children instead of always blocking on Ready.
- ws log on a missing workspace now returns ErrNotFound (exit 3) instead of an
  empty success, via a Workspace existence check.
- output format is last-flag-wins: a human -o table after --json resets to the
  human render instead of staying stuck on JSON.
- extract a shared renderList helper so sandbox ls, ws ls, and ws log no longer
  duplicate the JSON/text branch.

Refs #775

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
The contract table claimed exit 3 globally, but ws diff/fork/revert/rm/bind
still exit 1 for a missing target; scope the claim honestly and name the
follow-up. Also state that the backend's own poll bound elapsing is exit 1
so 124 always means the caller's explicit deadline, document the text/human
table aliases, and gofmt a pre-existing test-file alignment nit.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Signed-off-by: Jannes Stubbemann <jannes@openclaw.rocks>
@stubbi stubbi enabled auto-merge (squash) July 9, 2026 21:09

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
internal/agentcli/cli.go (1)

19-25: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Document all supported lifecycle flags for sandbox terminate.

The PR contract says terminate supports --wait, --no-wait, and --timeout, but the synopsis only advertises --timeout. Add the wait flags so automation users can discover the complete interface.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@internal/agentcli/cli.go` around lines 19 - 25, Update the CLI synopsis in
the command documentation so `sandbox terminate` advertises all supported
lifecycle flags, not just `--timeout`. Use the existing CLI help text around the
`mitos sandbox terminate` entry to add `--wait` and `--no-wait` alongside
`--timeout`, matching the interface already exposed by the lifecycle commands
and keeping the wording consistent with the nearby `mitos fork` and `sandbox
exec` entries.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@internal/agentcli/cli.go`:
- Around line 19-25: Update the CLI synopsis in the command documentation so
`sandbox terminate` advertises all supported lifecycle flags, not just
`--timeout`. Use the existing CLI help text around the `mitos sandbox terminate`
entry to add `--wait` and `--no-wait` alongside `--timeout`, matching the
interface already exposed by the lifecycle commands and keeping the wording
consistent with the nearby `mitos fork` and `sandbox exec` entries.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: a732ab29-e245-4907-a88b-202c013462e8

📥 Commits

Reviewing files that changed from the base of the PR and between e5651d8 and cb4e3dd.

📒 Files selected for processing (3)
  • docs/cli.md
  • internal/agentcli/cli.go
  • internal/agentcli/hostedbackend_test.go

@stubbi stubbi merged commit b6abee2 into main Jul 11, 2026
32 checks passed
@stubbi stubbi deleted the feat/cli-json-automation-775 branch July 11, 2026 00:17
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