Skip to content

CLI: doctor config validation, autonomy ceiling, changes --base, usage report - #142

Merged
gnanam1990 merged 34 commits into
mainfrom
wave1-cli-ports
Jun 8, 2026
Merged

CLI: doctor config validation, autonomy ceiling, changes --base, usage report#142
gnanam1990 merged 34 commits into
mainfrom
wave1-cli-ports

Conversation

@gnanam1990

@gnanam1990 gnanam1990 commented Jun 8, 2026

Copy link
Copy Markdown
Collaborator

Summary

Four small, dependency-free CLI improvements, batched into one PR:

  • zero doctor now validates config files. A new config.validation check parses each resolved config file and reports malformed JSON with line/col (including type-mismatch offsets, e.g. a string where an int is expected) plus semantic provider issues — all redaction-safe. Also fixes a pre-existing gap where doctor received no config paths and aborted on bad JSON before any check ran; it now degrades gracefully. (internal/config, internal/doctor, internal/cli)

  • Admin autonomy ceiling for the sandbox. A new config key sandbox.maxAutonomy (low|medium|high, env ZERO_SANDBOX_MAX_AUTONOMY) caps the autonomy a persistent grant or unsafe mode can reach. Enforced inside sandbox.Engine.Evaluate() — a request above the ceiling is clamped to a permission prompt (not silently auto-allowed). Defaults to high (a no-op, fully backward compatible); an invalid value fails loud at config resolve and closed at the engine bridge. Surfaced in zero sandbox policy. (internal/sandbox, internal/config, internal/cli, internal/zerocommands)

  • zero changes --base <ref>. Inspect the changes a branch introduced via a three-dot merge-base diff (git diff <base>...HEAD) instead of the working tree, with Files from --name-status. Additive flag on changes inspect/status (rejected on commit); the existing working-tree path is unchanged. (internal/zerogit, internal/cli)

  • zero usage report. A local token/cost report aggregated from already-persisted session usage events (no new persistence), bucketed per day, with a tokens/cost per net-LOC efficiency metric. Cost is reconstructed from the session model and labeled an estimate; net-LOC is a working-tree diff proxy, also labeled an estimate. --json, --days N, --since YYYY-MM-DD, --session <id>. (internal/zerogit, internal/usage, internal/cli)

No new module dependencies. All changes are additive and backward-compatible.

Test Plan

  • go build ./... and go vet ./... clean
  • go test ./... green (new tests across config, doctor, sandbox, zerogit, usage, cli, zerocommands)
  • go test -race ./internal/{sandbox,config,doctor,zerogit,usage,cli}/... green
  • GOOS=windows GOARCH=amd64 go build ./... green
  • git diff --check clean

Reviewer focus

  • Security: the autonomy ceiling is enforced in Evaluate() (not just UI), clamps to Prompt (Deny still wins), and an invalid sandbox.maxAutonomy fails closed — confirm the tests pin this.
  • Redaction: secrets never reach doctor details, the usage report, or the changes --base diff/base field.

Summary by CodeRabbit

  • New Features

    • Added zero usage report (default usage) with token/cost estimation, date/session filters, JSON/text output, and help listing usage
    • Added --base <ref> to zero changes inspect to diff against a specified ref
  • Improvements

    • Configurable sandbox autonomy ceiling (config/env/overrides) and policy outputs now show max_autonomy
    • doctor reports config validation with JSON parse positions and semantic issues
    • Richer git diff/stat parsing and change snapshots
  • Bug Fixes

    • Reject empty --session/--session-id and tighten CLI flag validation

KRATOS added 28 commits June 8, 2026 14:03
- Remove the osReadFile wrapper; call os.ReadFile directly at its one call site
- Add config.ValidateBytes([]byte) so each config file is read and JSON-parsed
  once in configValidationCheck instead of twice (once for position, once via
  ValidateFile); ValidateFile now reads then delegates to its own unmarshal for
  the path-prefixed error message, keeping its public signature unchanged
- Fix the misleading skip comment in configValidationCheck: configFilesCheck
  only checks path-string presence; the skip is a defensive guard for the
  unreachable case where DefaultResolveOptions passed a non-empty path that
  still fails to read
- Add utf8 multibyte case to TestOffsetToLineCol documenting byte-column
  semantics (column counts bytes, not runes)
Evaluate now treats an empty Policy.MaxAutonomy (from a directly-constructed
Policy that bypasses DefaultPolicy) as High instead of letting it normalize to
Low, which would silently clamp every Medium/High decision to Prompt. The
ceiling is a no-op unless explicitly configured.

Also extracts the "above policy ceiling" clamp reason into a package const
referenced at both engine clamp sites and the related tests, and adds a
non-vacuous engine test proving the empty-ceiling trap is closed.
- redactChangeSummary now redacts the Base field alongside Root/Branch/Commit/etc.
- --base= equals-form rejects leading-dash values via flagValueLooksLikeOption, closing the option-smuggling gap
- TestParseNameStatusRenameAndCopy asserts rename/copy three-field lines use the new/destination path with correct status, and confirms two-field modify is unaffected
Fix 1 (BLOCKING): parseUsageArgs now validates --since with time.Parse
against YYYY-MM-DD; returns exitUsage + "invalid --since %q: expected
YYYY-MM-DD" for bare strings like "foo", unpadded "2026-6-1", or
wrong-separator "06/01/2026". Valid dates are stored unchanged for the
existing lexical comparison.

Fix 2: Replace false "pre-redaction stat" claims in diffstat.go and
usage.go with accurate comments: the --stat summary line carries no
secret-bearing tokens so parsing the already-redacted DiffStat is safe.

Fix 3: Add three tests to usage_test.go —
  - TestRunUsageDaysFilter: stubs deps.now, seeds events on two dates,
    asserts --days 3 includes the recent date and excludes the old one.
  - TestRunUsageInvalidSince: table-driven; verifies "foo", "2026-6-1",
    "06/01/2026" each return exitUsage + validation message, while
    "2026-06-01" returns exitSuccess.
  - TestRunUsageEmptyStore: zero usage events → exitSuccess, output
    contains header and "total" row, no panic.
…l keys

Probe config validation against config.FileConfig instead of a bare any so
a structurally-valid document with a wrong field type (e.g. maxTurns as a
string) surfaces a *json.UnmarshalTypeError carrying the offset, instead of
losing line/col. Remove the flat top-level details["line"]/["col"] keys
that were overwritten by the last malformed file when several were present;
keep only the unambiguous per-path map entry. Tighten the existing malformed
test to assert concrete per-path line/col and add a type-mismatch test that
exercises the previously-dead UnmarshalTypeError branch.
An invalid non-empty sandbox.maxAutonomy (e.g. "moderate") previously
survived Resolve (only trimmed, never validated), failed to normalize at the
sandbox bridge, and left the policy unchanged at the default High ceiling.
A typo therefore silently disabled the admin's intended ceiling: fail-open on
a security boundary.

Validate at resolve time (fail loud): Resolve now rejects a non-empty
maxAutonomy that does not normalize, returning a clear error. As defense in
depth, applyConfiguredAutonomyCeiling now clamps an unrecognised non-empty
value to the most restrictive ceiling (low) instead of returning the policy
unchanged, so a bad value can never widen the ceiling even via direct use.
Empty/unset stays valid and keeps the default High ceiling.

Add resolver tests for invalid-fails / valid-resolves and bridge tests for
the fail-closed clamp and valid mappings.
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown
Contributor

Zero automated PR review

Verdict: No blockers found

Blockers

  • None found.

Validation

  • [pass] Diff hygiene: git diff --check
  • [pass] Tests: go test ./...
  • [pass] Build: go run ./cmd/zero-release build
  • [pass] Smoke build: go run ./cmd/zero-release smoke

Scope

Head: 1e9a86224041
Changed files (35): internal/cli/app.go, internal/cli/app_test.go, internal/cli/exec.go, internal/cli/observability.go, internal/cli/observability_test.go, internal/cli/sandbox.go, internal/cli/sandbox_test.go, internal/cli/usage.go, internal/cli/usage_test.go, internal/cli/workflow_test.go, internal/cli/workflows.go, internal/config/resolver.go, and 23 more

This deterministic review checks validation status and basic diff hygiene. A human reviewer still owns product judgment and design quality.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 14e8f197-3b93-41c2-9278-26fe8c6ee8bc

📥 Commits

Reviewing files that changed from the base of the PR and between 477c800 and 1e9a862.

📒 Files selected for processing (7)
  • internal/cli/app.go
  • internal/cli/app_test.go
  • internal/cli/exec.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/config/types.go
  • internal/doctor/doctor.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/cli/app_test.go
  • internal/cli/exec.go
  • internal/config/resolver.go

Walkthrough

Adds config-driven sandbox autonomy ceilings, a new zero usage report CLI (text/JSON), base-ref diff inspection for changes inspect, and doctor config validation with line/column diagnostics; tests added/updated across resolver, sandbox, zerogit, usage reporting, CLI, and doctor.

Changes

Sandbox autonomy ceiling and usage reporting

Layer / File(s) Summary
All checkpoints (single reviewer path)
internal/config/*, internal/sandbox/*, internal/cli/*, internal/usage/*, internal/zerogit/*, internal/doctor/*, internal/zerocommands/*
Adds SandboxConfig.MaxAutonomy to config types/resolution/overrides/env; introduces ValidateFile/ValidateBytes and doctor config.validation with positional JSON diagnostics; adds Policy.MaxAutonomy, normalization and fail-closed semantics, batch autonomy selection, and engine ceiling enforcement (clamps to prompt with "above ceiling" reason); integrates configured ceiling into CLI exec/sandbox flow and TUI snapshots; adds zero usage report CLI (collects events, parses git diff stat, reconstructs cost, text/JSON formatting); extends zerogit with BaseRef inspection and DiffStat parsing; and updates/tests across all affected areas.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Gitlawb/zero#54: Overlaps with exec/provider wiring and sandbox construction timing.
  • Gitlawb/zero#136: Touches runExec control flow and cancellation/exit behavior intersecting with sandbox setup.
  • Gitlawb/zero#77: Prior sandbox policy/engine changes that relate to enforcing MaxAutonomy.

Suggested reviewers

  • Vasanthdev2004
  • anandh8x
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 17.45% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Title comprehensively captures all four additive CLI features: doctor config validation, autonomy ceiling, changes --base flag, and usage report command.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 wave1-cli-ports

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

@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: 8

🧹 Nitpick comments (3)
internal/cli/observability_test.go (1)

266-269: ⚡ Quick win

Assert quiet stderr on JSON doctor output in this failure path.

This test validates JSON content but not that stderr stays empty, which is important for machine-readable CLI behavior consistency.

Proposed test hardening
 	if exitCode != exitProvider {
 		t.Fatalf("expected provider exit %d, got %d: %s", exitProvider, exitCode, stderr.String())
 	}
+	if stderr.Len() != 0 {
+		t.Fatalf("expected empty stderr, got %q", stderr.String())
+	}
 	var report struct {
 		OK     bool `json:"ok"`
 		Checks []struct {
🤖 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/cli/observability_test.go` around lines 266 - 269, The test
currently checks exit codes and JSON parsing but doesn't assert that stderr is
empty; update the failing assertion (the block comparing exitCode and
exitProvider that references stderr.String()) to explicitly assert stderr is
empty (e.g., check stderr.Len() == 0 or stderr.String() == "") and include that
check either as part of the t.Fatalf message or as a separate t.Fatalf/t.Errorf
after the exit-code check so the test fails if any non-JSON output was written
to stderr; refer to the existing exitCode, exitProvider, and stderr variables to
locate where to add the assertion.
internal/cli/usage_test.go (1)

148-157: ⚡ Quick win

Add coverage for empty session-value flags.

Please add cases for --session= and --session-id= expecting exitUsage, so parser behavior stays explicit and doesn’t silently broaden scope.

🤖 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/cli/usage_test.go` around lines 148 - 157, Add test coverage for
empty session-value flags by extending TestRunUsageUnknownFlag (or adding a
similar test) to call runWithDeps with arguments including "--session=" and
"--session-id=" and assert the returned exitCode equals exitUsage; also assert
stderr contains the parser error (e.g., the unknown/empty-session message) so
the test fails if the CLI silently accepts empty session values. Use the
existing helpers and symbols runWithDeps, exitUsage, and stderr buffer to
implement these additional assertions.
internal/usage/report_test.go (1)

31-68: ⚡ Quick win

Add a timezone-offset regression case for UTC bucketing.

Current tests only use ...Z timestamps, so they won’t catch offset-date rollover bugs. Add one event with a non-UTC offset and assert the bucket date is normalized to UTC day.

🤖 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/usage/report_test.go` around lines 31 - 68, Update the
TestBuildReportBucketsByDayAndSumsTokens test to include a timezone-offset event
to catch UTC bucketing regressions: add another sessions.Event (via usageEvent)
with a timestamp that includes a non-UTC offset (e.g. "+02:00" or "-05:00")
whose UTC day differs from its local date, call BuildReport as before, and
assert that the resulting report.Buckets contain the event under the normalized
UTC date (i.e., the bucket Date matches the UTC day) and that totals/requests
still aggregate correctly; locate changes around
TestBuildReportBucketsByDayAndSumsTokens, usageEvent usage, and assertions on
report.Buckets, BuildReport and report.Total to update expectations accordingly.
🤖 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/cli/sandbox.go`:
- Around line 59-61: The code currently swallows errors from deps.resolveConfig,
leaving policy as DefaultPolicy() (high autonomy) when config parsing fails;
change the behavior in the surrounding function to fail loudly by checking the
error returned from deps.resolveConfig(workspaceRoot, config.Overrides{}) and
returning that error (or an explicit "unable to resolve sandbox config" error)
instead of silently continuing, so that applyConfiguredAutonomyCeiling(policy,
resolved.Sandbox.MaxAutonomy) only runs on success; reference
deps.resolveConfig, resolved.Sandbox.MaxAutonomy,
applyConfiguredAutonomyCeiling, policy, and DefaultPolicy to locate and update
the logic.

In `@internal/cli/usage.go`:
- Around line 198-209: The parser currently accepts empty session flags and
silently treats "--session=" / "--session-id=" as no-op; update the flag
handling in the switch (the branches using nextFlagValue and the
strings.HasPrefix("--session=") / "--session-id=" cases) to validate the trimmed
value is non-empty and return an error when empty instead of setting
options.sessionID or falling through; specifically after calling
nextFlagValue(...) and before assigning options.sessionID, check value != "" and
return a descriptive error, and likewise for the strings.TrimSpace(...)
branches, reject empty strings and return an error rather than accepting them.
- Around line 63-69: The current filtering slices event.CreatedAt as a
local-date string and compares it to since, which mis-handles timestamps with
timezones; instead parse event.CreatedAt into a time.Time, convert to UTC (e.g.,
t.UTC()), normalize to a YYYY-MM-DD date string or date-only value, and compare
that UTC-normalized date to the since cutoff before appending to filtered;
update the logic around the date variable and the comparison with since
(references: event.CreatedAt, date, since, filtered) so all comparisons are
performed on UTC-normalized dates.

In `@internal/config/resolver_test.go`:
- Around line 655-656: Tests calling Resolve(ResolveOptions{...}) are using the
ambient environment because Env is nil; update the four Resolve calls (the ones
constructing ResolveOptions in resolver_test.go around the blocks using variable
name 'resolved' and error checks) to include an explicit empty environment map
by setting Env: map[string]string{} in the ResolveOptions so the resolver runs
deterministically regardless of host os.Getenv values.

In `@internal/doctor/doctor.go`:
- Around line 310-312: In ValidateBytes (internal/doctor/doctor.go) stop
fabricating coordinates for non-positional JSON unmarshal errors: instead of
returning the fake (1,1,true) use a zeroed coordinate with the same
parse-failure marker (e.g., return 0,0,true) so callers see a parse error
without misleading line/column information; update the return at the
non-positional-error branch that currently returns 1,1,true to return 0,0,true.
- Around line 247-254: When os.ReadFile(path) returns readErr in the config file
loop, don't just continue silently; treat this as a failing validation detail:
mark the associated config.validation as failed (set Passed/OK flag to false)
and append a descriptive detail/error entry including the file path and readErr
message so it surfaces in the overall report. Keep iterating to collect other
failures (don't panic), but ensure the code paths around the existing
os.ReadFile(path) invocation update the validation result (e.g., set
validation.Passed = false or similar and append to validation.Details) instead
of skipping with continue.

In `@internal/sandbox/engine.go`:
- Around line 101-109: The new ceiling check uses a rewritten/normalized value
for request.Autonomy, letting malformed inputs become AutonomyLow and bypass the
clamp; update the logic in the autonomy ceiling checks (the call to
autonomyAllowed and the Decision returning ActionPrompt/reasonAboveCeiling) to
validate the original request.Autonomy before normalization (or use a separate
variable like rawAutonomy vs normalizedAutonomy), and treat malformed/unknown
autonomy as exceeding the ceiling so it clamps to prompt; ensure the same fix is
applied in both places referenced (around the autonomyAllowed call and the
similar block at lines ~123-125) and preserve existing symbols Decision,
ActionPrompt, reasonAboveCeiling, grant, and policy.MaxAutonomy.

In `@internal/usage/report.go`:
- Around line 84-87: The code currently slices event.CreatedAt into date by
taking the first 10 characters which ignores timezone offsets; update the logic
in report.go (where date := event.CreatedAt is assigned) to parse
event.CreatedAt as a time.Time, convert it to UTC, then format it to the
YYYY-MM-DD string (e.g., using time.Parse/time.ParseInLocation and
t.UTC().Format("2006-01-02")) before assigning to date so bucketization uses the
UTC day; ensure you handle parse errors appropriately (fallback or logging) in
the surrounding function that builds the report.

---

Nitpick comments:
In `@internal/cli/observability_test.go`:
- Around line 266-269: The test currently checks exit codes and JSON parsing but
doesn't assert that stderr is empty; update the failing assertion (the block
comparing exitCode and exitProvider that references stderr.String()) to
explicitly assert stderr is empty (e.g., check stderr.Len() == 0 or
stderr.String() == "") and include that check either as part of the t.Fatalf
message or as a separate t.Fatalf/t.Errorf after the exit-code check so the test
fails if any non-JSON output was written to stderr; refer to the existing
exitCode, exitProvider, and stderr variables to locate where to add the
assertion.

In `@internal/cli/usage_test.go`:
- Around line 148-157: Add test coverage for empty session-value flags by
extending TestRunUsageUnknownFlag (or adding a similar test) to call runWithDeps
with arguments including "--session=" and "--session-id=" and assert the
returned exitCode equals exitUsage; also assert stderr contains the parser error
(e.g., the unknown/empty-session message) so the test fails if the CLI silently
accepts empty session values. Use the existing helpers and symbols runWithDeps,
exitUsage, and stderr buffer to implement these additional assertions.

In `@internal/usage/report_test.go`:
- Around line 31-68: Update the TestBuildReportBucketsByDayAndSumsTokens test to
include a timezone-offset event to catch UTC bucketing regressions: add another
sessions.Event (via usageEvent) with a timestamp that includes a non-UTC offset
(e.g. "+02:00" or "-05:00") whose UTC day differs from its local date, call
BuildReport as before, and assert that the resulting report.Buckets contain the
event under the normalized UTC date (i.e., the bucket Date matches the UTC day)
and that totals/requests still aggregate correctly; locate changes around
TestBuildReportBucketsByDayAndSumsTokens, usageEvent usage, and assertions on
report.Buckets, BuildReport and report.Total to update expectations accordingly.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 2c63756c-b830-4a43-9c9c-e9b0444466fc

📥 Commits

Reviewing files that changed from the base of the PR and between ea2acbb and 600514a.

📒 Files selected for processing (35)
  • internal/cli/app.go
  • internal/cli/app_test.go
  • internal/cli/exec.go
  • internal/cli/observability.go
  • internal/cli/observability_test.go
  • internal/cli/sandbox.go
  • internal/cli/sandbox_test.go
  • internal/cli/usage.go
  • internal/cli/usage_test.go
  • internal/cli/workflow_test.go
  • internal/cli/workflows.go
  • internal/config/resolver.go
  • internal/config/resolver_test.go
  • internal/config/types.go
  • internal/config/validate.go
  • internal/config/validate_test.go
  • internal/doctor/doctor.go
  • internal/doctor/doctor_test.go
  • internal/sandbox/batch.go
  • internal/sandbox/batch_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/engine_test.go
  • internal/sandbox/normalize.go
  • internal/sandbox/normalize_test.go
  • internal/sandbox/types.go
  • internal/usage/report.go
  • internal/usage/report_test.go
  • internal/zerocommands/sandbox_snapshots.go
  • internal/zerocommands/sandbox_snapshots_test.go
  • internal/zerogit/contracts.go
  • internal/zerogit/contracts_test.go
  • internal/zerogit/diffstat.go
  • internal/zerogit/diffstat_test.go
  • internal/zerogit/zerogit.go
  • internal/zerogit/zerogit_test.go

Comment thread internal/cli/sandbox.go Outdated
Comment thread internal/cli/usage.go Outdated
Comment thread internal/cli/usage.go
Comment thread internal/config/resolver_test.go Outdated
Comment thread internal/doctor/doctor.go
Comment thread internal/doctor/doctor.go Outdated
Comment thread internal/sandbox/engine.go Outdated
Comment thread internal/usage/report.go Outdated
KRATOS added 4 commits June 8, 2026 15:47
… errors

Treat a genuinely-invalid request autonomy as the highest tier in the engine
so it exceeds a Medium/Low ceiling and clamps to Prompt instead of being
sanitized to Low and auto-allowing on the grant/unsafe path. Surface
resolveConfig failures in the policy command instead of silently falling back
to the permissive DefaultPolicy, which would misreport trust posture.
Normalize RFC3339 timestamps to their UTC calendar date before day bucketing
(report) and before the --since/--days cutoff comparison (CLI) so offset
timestamps land on the correct UTC day and the bucket and cutoff agree;
malformed timestamps fall back to the leading-10 slice. Reject empty
--session/--session-id values with a value-required error matching the --since
validation style.
Surface a present-but-unreadable config path (permissions, is-a-directory) as
a failing per-path validation detail instead of silently skipping it; a
genuinely-missing path stays a skip. Return ok=false for a JSON parse error
that carries no offset so it routes through the no-position ValidateBytes path
rather than fabricating a (1,1) line/col. Assert empty stderr on the JSON
doctor failure path.
Pass an explicit empty Env to the maxAutonomy Resolve tests so host
environment variables cannot leak in through the nil-Env os.Getenv fallback.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

Thanks for the thorough review — all 8 actionable items + 3 nitpicks addressed in 91be859, 69afca0, 2b818ed, e270efd. Full gates green (build/vet/test/-race/GOOS=windows).

Security (the important one):

  • engine.go malformed-autonomy bypass — confirmed and fixed. An invalid request.Autonomy was sanitized to AutonomyLow before the ceiling clamp, so it slipped under any ceiling and the unsafe/grant path auto-allowed. Now the error default is AutonomyHigh (fail closed) so a bogus autonomy exceeds a Medium/Low ceiling and clamps to Prompt. Added TestEngineInvalidAutonomyFailsClosedOn{UnsafeEscalation,GrantAllow}. (NormalizeAutonomy("") is Low,nil, so only genuinely-invalid values hit this — valid flows unchanged.)

Other fixes:

  • sandbox.gorunSandboxPolicy now surfaces resolveConfig errors (exitProvider) instead of silently showing the default High posture; +test.
  • UTC bucketing (usage/report.go + cli/usage.go) — both bucket/filter by time.Parse(RFC3339).UTC() calendar day now (offset-safe), with a -07:00 regression test.
  • --session= empty — rejected with a usage error; +test.
  • doctor.go — unreadable (non-IsNotExist) config paths now fail validation instead of silently passing; the (1,1) fabricated position is gone (non-positional errors route through ValidateBytes with no fake coords); +test.
  • resolver_test.go — the four Resolve calls now pass Env: map[string]string{} to stay hermetic.
  • nitpicks — empty-stderr assertion on the JSON doctor failure path; empty---session test coverage.

@coderabbitai please re-review.

@coderabbitai

coderabbitai Bot commented Jun 8, 2026

Copy link
Copy Markdown

Tip

For best results, initiate chat on the files or code changes.

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Fetch the PR details and recent commits
gh pr view 142 --repo Gitlawb/zero --json title,state,headRefName,baseRefName,additions,deletions,changedFiles

# Get the list of commits
gh pr view 142 --repo Gitlawb/zero --json commits --jq '.commits[-6:] | .[] | {oid: .oid, message: .messageHeadline}'

Length of output: 1339


🏁 Script executed:

#!/bin/bash
# Get the diff for the fix commits
git log --oneline -10

Length of output: 839


🏁 Script executed:

#!/bin/bash
# Read the key security-relevant files that were modified in the fix commits
cat internal/sandbox/engine.go

Length of output: 6089


🏁 Script executed:

#!/bin/bash
cat internal/sandbox/normalize.go

Length of output: 2502


🏁 Script executed:

#!/bin/bash
cat internal/cli/sandbox.go

Length of output: 16861


🏁 Script executed:

#!/bin/bash
cat internal/usage/report.go

Length of output: 5092


🏁 Script executed:

#!/bin/bash
cat internal/cli/usage.go

Length of output: 10434


🏁 Script executed:

#!/bin/bash
cat internal/doctor/doctor.go

Length of output: 12016



@gnanam1990 — re-review complete. All the fixes are clean. Here's what was verified against the live files:

Security (engine.go / normalize.go) ✅

  • Invalid request.Autonomy now lands at AutonomyHigh (fail closed) after the NormalizeAutonomy error path, is stored back into request.Autonomy before the ceiling check, and will exceed any Medium/Low ceiling → ActionPrompt. Correct.
  • NormalizeAutonomy("") still returns (AutonomyLow, nil) — no error, no behaviour change for the common empty-autonomy case. Correct.
  • policy.MaxAutonomy == "" defaults to AutonomyHigh inside Evaluate, so a direct Policy{} construction is a no-op ceiling rather than a lockdown. The intent is clearly documented in-code.
  • autonomyAllowed in normalize.go applies its own fail-closed scoring on both operands (invalid requested → above-high, invalid ceiling → below-low), which is a belt-and-suspenders defence on top of the engine normalisation.

sandbox.go

  • runSandboxPolicy calls deps.resolveConfig and returns exitProvider on error — no more silent fallback to the permissive default High posture.

UTC bucketing (usage/report.go + cli/usage.go) ✅

  • Both utcDayBucket and eventUTCDate parse RFC3339 and call .UTC().Format(...) before extracting the date. The -07:00 boundary case is covered.

--session= empty ✅

  • Empty --session, --session=, and --session-id= values all produce a usage error; nextFlagValue path also trims and rejects empty.

doctor.go

  • os.IsNotExist → skip (delegation to config.files check). Any other ReadFile error → StatusFail with "unreadable: ..." detail.
  • jsonParsePosition returns (0, 0, false) for non-positional errors (e.g. unexpected EOF without an offset); the fabricated (1,1) is gone, and those errors route through ValidateBytes without a fake line/col.

resolver_test.go

  • Env: map[string]string{} is now passed to all four Resolve calls, keeping them hermetic.

No remaining actionable issues. The PR is in good shape.

[approve]

🐇 ✓


🧠 Learnings used
Learnt from: CR
Repo: Gitlawb/openclaude PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T22:10:40.834Z
Learning: If the PR touches auth, provider routing, permissions, outbound network behavior, background execution, startup/config-home behavior, skills/plugins/MCP, CI permissions, or release scripts, verify that the review calls out the risk surface and whether it introduces a blocker.

Learnt from: CR
Repo: Gitlawb/openclaude PR: 0
File: coderabbit-custom-pre-merge-checks-unique-id-file-non-traceable-F7F2B60C-1728-4C9A-8889-4F2235E186CA.txt:0-0
Timestamp: 2026-06-04T22:10:40.834Z
Learning: Verify that product, trust-model, routing-default, telemetry/network, and permission-policy changes are not hidden inside unrelated cleanup. Flag the PR if the policy decision needs explicit maintainer alignment.

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Request changes

Finding:
[P1] Preserve invalid autonomy through the ceiling checks.

In internal/sandbox/engine.go, Evaluate normalizes an invalid request.Autonomy to AutonomyHigh before the persistent grant and unsafe-mode ceiling checks. That still auto-allows when policy.MaxAutonomy is High or default, because autonomyAllowed(High, High) returns true. This contradicts the fail-closed comment and bypasses the unknown-tier guard that autonomyAllowed already has for invalid requested autonomy.

Please preserve the raw invalid autonomy or track an invalidAutonomy flag and force Prompt before the grant/unsafe allow paths. Add regression coverage for invalid autonomy under the default/High ceiling for both unsafe escalation and persistent grant allow.

Validation context:

  • GitHub CI checks are green.
  • CodeRabbit is still pending at the time of this review.
  • I did not run the full suite locally on this PR branch.

@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.

🧹 Nitpick comments (1)
internal/sandbox/engine_test.go (1)

393-430: ⚡ Quick win

Pin the grant metadata on the fail-closed path.

Lines 426-430 only prove the result gets clamped to ActionPrompt. If Evaluate still sets GrantMatched=true before the ceiling clamp, this regression would still pass while the decision keeps claiming that a persistent allow matched. Add an explicit !decision.GrantMatched assertion here.

Suggested assertion
 	if decision.Action != ActionPrompt {
 		t.Fatalf("invalid-autonomy grant decision = %#v, want prompt (fail closed above ceiling, not grant allow)", decision)
 	}
 	if decision.Reason != reasonAboveCeiling {
 		t.Fatalf("decision.Reason = %q, want %q", decision.Reason, reasonAboveCeiling)
 	}
+	if decision.GrantMatched {
+		t.Fatalf("invalid-autonomy grant decision = %#v, want no matched grant after ceiling clamp", decision)
+	}
🤖 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/sandbox/engine_test.go` around lines 393 - 430, The test
TestEngineInvalidAutonomyFailsClosedOnGrantAllow currently only checks that the
action is ActionPrompt and reason is reasonAboveCeiling; add an assertion that
the persistent grant flag is not set so the fail-closed path pins the grant
metadata: after obtaining decision from engine.Evaluate, assert that
decision.GrantMatched is false (i.e., !decision.GrantMatched) so the test fails
if Evaluate incorrectly reports a matching persistent allow even when autonomy
was clamped.
🤖 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.

Nitpick comments:
In `@internal/sandbox/engine_test.go`:
- Around line 393-430: The test TestEngineInvalidAutonomyFailsClosedOnGrantAllow
currently only checks that the action is ActionPrompt and reason is
reasonAboveCeiling; add an assertion that the persistent grant flag is not set
so the fail-closed path pins the grant metadata: after obtaining decision from
engine.Evaluate, assert that decision.GrantMatched is false (i.e.,
!decision.GrantMatched) so the test fails if Evaluate incorrectly reports a
matching persistent allow even when autonomy was clamped.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 8e697934-6a59-43fa-af98-78616bfb5bba

📥 Commits

Reviewing files that changed from the base of the PR and between 600514a and e270efd.

📒 Files selected for processing (12)
  • internal/cli/observability_test.go
  • internal/cli/sandbox.go
  • internal/cli/sandbox_test.go
  • internal/cli/usage.go
  • internal/cli/usage_test.go
  • internal/config/resolver_test.go
  • internal/doctor/doctor.go
  • internal/doctor/doctor_test.go
  • internal/sandbox/engine.go
  • internal/sandbox/engine_test.go
  • internal/usage/report.go
  • internal/usage/report_test.go
🚧 Files skipped from review as they are similar to previous changes (7)
  • internal/sandbox/engine.go
  • internal/cli/observability_test.go
  • internal/doctor/doctor.go
  • internal/config/resolver_test.go
  • internal/usage/report_test.go
  • internal/cli/usage_test.go
  • internal/cli/usage.go

…der default ceiling

Defaulting invalid request.Autonomy to High only clamped under a Medium/Low
ceiling; under the default High ceiling autonomyAllowed(High, High) returned true
and still auto-allowed (fail-open). Keep the raw requested value for the ceiling
check so autonomyAllowed's unknown-tier guard fails it closed under ANY ceiling.
Adds default-High-ceiling regression tests for the unsafe and grant-allow paths.
@gnanam1990

Copy link
Copy Markdown
Collaborator Author

@Vasanthdev2004 good catch — you're right, the fix was incomplete. Normalizing invalid autonomy to High only clamped under a Medium/Low ceiling; under the default High ceiling autonomyAllowed(High, High) returns true, so it still auto-allowed (fail-open). Fixed in 477c800:

  • Evaluate now preserves the raw requested autonomy (rawAutonomy) and uses it for both ceiling checks (grant-allow + unsafe), so autonomyAllowed's existing unknown-tier guard fails it closed under any ceiling — including the default High. The High placeholder is kept only for risk classification / grant lookup.
  • Added the regression coverage you asked for: TestEngineInvalidAutonomyFailsClosedUnderDefaultCeiling{Unsafe,Grant} — invalid autonomy under DefaultPolicy() (High ceiling) → Prompt for both the unsafe-escalation and persistent-grant-allow paths. (Verified non-vacuous: they fail against the old autonomyAllowed(High, High) path.)

Full gates green (build/vet/test/-race/GOOS=windows). Please re-review.

# Conflicts:
#	internal/config/resolver.go

@Vasanthdev2004 Vasanthdev2004 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Verdict: Approve

Conflict resolution reviewed at 1e9a862.

The previous sandbox blocker is fixed: invalid autonomy is preserved through ceiling checks and covered under the default High ceiling for unsafe and persistent grant paths. The merge with latest main keeps the provider catalog/env-key hardening and the sandbox max-autonomy validation together; internal/config/resolver.go now validates sandbox.maxAutonomy while still resolving provider apiKeyEnv through the safe catalog-aware path.

Local validation on the conflict-resolution worktree:

  • go test ./... passed
  • go run ./cmd/zero-release build passed
  • go run ./cmd/zero-release smoke passed

GitHub checks are still running after the new merge commit, but the PR is now mergeable and I did not find code blockers.

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.

2 participants