cli: --reasoning-effort notice evaluates the EFFECTIVE model (works without --model) (MED); unsafe warning fires for --auto high too (MED); --auto value validated even with --skip-permissions-unsafe (LOW); DefaultRegistry built once per exec, not 3x (LOW); --profile kept as intentional legacy-compat accept-and-ignore (contested finding resolved, doc-commented). modelregistry: removed dead FallbackFor. tools: removed dead TaskNonInteractiveMessage + dead OnSandboxDecision hook (no setter anywhere); fixed the now-stale loop.go comment. TDD where behavioral; build/vet/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
tui: Ctrl+C now flushes the in-flight run's checkpoint session events before quitting (no orphaned blobs / broken rewind), mirroring Esc-cancel (MED); removed dead permission-modal MouseMsg branch (mouse capture is intentionally off) + its test (MED); /model//mode//effort//theme pickers refuse to open mid-run with a clear message instead of opening then refusing the selection (LOW). mcp: RegisterTools is atomic — stages all servers and commits only if all succeed, so a later server failure no longer leaves dead tools (LOW). skills: duplicate frontmatter names resolve deterministically (lexicographically-first dir wins) + Duplicates() surfaces collisions (LOW). TDD incl -race; build/vet/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
executeTask propagates OnUsage into the child run so sub-agent token usage is counted (cost telemetry; activity callbacks stay nil) (MED); repeated-failure guard Stop mid-turn now appends aborted-placeholder tool results for unexecuted calls so result.Messages stays structurally valid (every tool_use answered) (LOW). The 'dead AgentEventType' finding was rejected — it's an intended TUI/headless/sessions contract, not dead code. TDD incl -race; build/vet/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
grep no longer follows in-workspace symlinks to files outside the root (confinement) and computes clean workspace-relative match paths under a symlinked root (HIGH x2); apply_patch ChangedFiles + MutationTargets are workspace-relative when cwd!="." (HIGH); aliasedStringArg(allowEmpty) skips an empty primary so a populated alias wins — no more write_file content/path data loss (HIGH); registry.Without isolates a fresh update_plan for sub-agents so a task child can't clobber the parent's plan (MED). TDD incl -race; build/vet/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
modelregistry: ModelEntry.Validate now requires DefaultReasoningEffort to be a member of ReasoningEfforts (not just a valid constant); NewRegistry adds a cross-entry pass verifying every Deprecation.FallbackID resolves to a known model so a misconfigured rule fails at startup instead of being silently ignored by ResolveWithFallback. sessions: checkpoint_test setup uses mustWriteFile/mustCapture/mustAppend helpers that check errors, so a setup failure is diagnosed at its source (errcheck-clean). The rewind double-count comment is already addressed by PR #103. Build/vet/-race/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
loop.go: keep OnUsage on the reactive retry (drop only OnText) so token telemetry counts the retry. tui: flushRunID -> flushRunIDs set so a second cancel before the first goroutine returns doesn't lose the first run's checkpoint flush; shift+tab now toggles Auto<->Ask only (Unsafe is no longer reachable by a casual keypress since it disables permission prompts). zenline: PermLayout returns inactive geometry when the button row is clipped at small heights so clicks can't resolve to allow/deny on non-button rows. providerio_test: check pw.Close (errcheck). Tests added; build/vet/-race/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
The two remaining low findings (plugin manifest tools/hooks/prompts/skills parsed but unwired; plugin skill dirs not merged into the skill tool) are intentionally discovery-only for now — documented in code so the parsed-but-unconsumed fields aren't mistaken for dead code. Runtime wiring is tracked as a separate feature.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Cancelled runs now flush in-flight session events incl. checkpoints so /rewind works + no orphaned blobs (H5); shift+tab cycles permission mode Auto/Ask/Unsafe (H7); interactive TUI defaults to Ask so write/edit/bash/apply_patch are advertised + gate through the permission prompt (H8 — VERIFY LIVE); zenline skin renders the ask_user questionnaire instead of a spinner when a prompt is pending (M11); applyExecMode runs before filter validation + --list-tools (low); mode-model deprecation notice surfaced via the shared --model path (low); zenline listed in --help (low). TDD incl. -race; build/vet/full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Redact intercepted ask_user/task outputs through the same boundary as the registry so secrets don't reach the transcript unredacted (M1); reactive mid-stream compaction retry drops OnText/OnUsage so text isn't double-streamed (M2); removed dead taskFallbackResult nil-provider branch (low); reactiveAttempted set only on a successful shrink so a no-op recover doesn't burn the one-shot budget (low); surface DroppedToolCalls even when a valid call is also present (low). TDD incl. -race; full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
MutationTargets resolves path/patch via the same alias lists the tools use, so aliased writes get checkpointed and /rewind works (H1); updatePlanTool guards currentPlan with a mutex against the agent/TUI goroutine race (M3); optional path/cwd args (grep/glob/list_directory) treat an explicit "" as the default instead of erroring (low); deleted dead stringSliceArg (low). TDD incl. -race; full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Bound+reset the glamour renderer cache mdCache (M8); clip glamour output lines to frame width with ANSI-aware truncate so long URLs/code don't break the fixed-height layout (M9); PermLayout hit-test mirrors RenderChat's width/height clamps + overlay subtraction so mouse clicks land on the modal buttons (M10). TDD; full-suite green.
Refs #102
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Plugins: LoadedPlugin gains optional Author/License/Keywords/Homepage/Interface metadata (omitempty, non-breaking, best-effort parse) surfaced in 'zero plugins'. Skills: new internal/skills package loads */SKILL.md (hand-parsed frontmatter, no YAML dep) from ZERO_SKILLS_DIR or the XDG data dir; a read-only 'skill' tool lets the agent load a named skill's content on demand (registered in CoreReadOnlyTools); 'zero skills' lists them (+--json). Build/vet/-race/full-suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
agent.Options gains Provider (auto-injected in Run) + Depth. A 'task' tool is intercepted in executeToolCall (like ask_user): it spawns agent.Run as a child with Depth+1, a registry filtered via new Registry.Without("task","ask_user") (no infinite nesting, no interactive prompts), inherited Sandbox/PermissionMode/Autonomy but all interactive callbacks nil, sharing ctx for cancellation; the child's final answer becomes the tool result. Depth guard (maxTaskDepth=2) refuses deeper nesting with a clear message. Synchronous; async task output/stop deferred. Build/vet/-race/full-suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Typing '/' surfaces a filtered, navigable command list (Tab/↑↓ cycle, Enter/Tab complete, Esc dismiss) in both default and zenline skins — purely additive UI state that never disturbs the permission/ask_user/zenline/submit key precedence. Bare /model, /effort, /mode (and /theme in zenline) open an interactive selector overlay that routes the choice through the existing handlers. Build/vet/-race/full-suite green; existing key tests unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Assistant messages now render through glamour (CommonMark + chroma-highlighted fenced code), themed to the active palette and sitting on the full-bleed bg. edit_file/apply_patch results render as colored, guttered unified diffs (green add / red del / dim context, 40-line cap). Rendered markdown is cached per (text,theme,width) with a 512-entry bound so RenderChat (called every spinner tick/keystroke) doesn't re-run glamour each frame. Streaming text stays plain (partial markdown renders badly). Adds glamour v1.0.0 + chroma v2 (offline from cache). Build/vet/-race/full-suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
boolArg accepts true/false/yes/no/on/off/1/0 (string or number); intArg accepts whole-number strings. Fixes minimax-m3's 'overwrite must be a boolean' and string-number args. Genuinely uncoercible values still error. Completes the arg-tolerance layer alongside string aliases.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Home/boot text used foreground-only styles, so content cells showed the TERMINAL's own background while lipgloss filled the surrounding whitespace with the theme bg — a visible two-tone 'card'. Added newCanvasStyles (foreground + theme background) for the full-bleed surfaces (home + boot), and set the theme bg on the input box and the centered-content wrappers, so the whole screen is one uniform themed background filling the terminal. Chat status bars keep the bg-free styles (they compose inside Panel-colored segments) so they're unaffected.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
guardState tracks consecutive same-error failures per tool: after 2 identical failures it injects a one-shot corrective hint (the tool's JSON arg schema + the exact error) so the model self-corrects; after 4 it halts the run with a clear message instead of burning turns to maxTurns. A successful call resets that tool's streak; the hint takes priority over plan reminders. Complements the arg-tolerance layer: tolerance prevents most mis-shapes, the guard backstops any that remain (any model, any tool). Build/vet/-race/full-suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Removed tea.WithMouseCellMotion() from the zenline program: mouse cell-motion reporting routed clicks to the app and broke the terminal's native text selection + copy (and made paste flaky). The permission modal is keyboard-driven (a/y/d/Esc), so capturing the mouse cost core UX for little gain. Click-to-select, Cmd/Ctrl+C copy, and Cmd/Ctrl+V paste now behave like a normal terminal.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
ask_user: coerce options from objects ({label|value|text|...})/scalars/newline-strings instead of erroring 'options must be an array of strings'; accept bare-string question items and prompt/text/q/title aliases for the question. write_file: read content from contents/text/body/data/file_content aliases (minimax mis-keys 'content'); present-but-non-string still rejected. Same weak-model tolerance already applied to update_plan. Tests added; build/vet/-race green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Proactive compaction at 80% of the model context window + reactive recovery on context-limit errors: summarizes the oldest middle messages into one user-role summary, preserves system + recent suffix. Pure Compact() (injectable Summarizer), low-water-mark guard against churn, suffix walked back to an assistant boundary so no consecutive-user/dangling-tool-result sequences, recover() gated on enabled. Disabled (no-op) when ContextWindow==0 — wired from the resolved model's context window in CLI+TUI. Build/vet/-race green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Data-driven presets (smart/deep/fast/large/precise) bundling model + reasoning effort + max turns (+ optional tool filter), resolved through the registry. --mode seeds only unset exec options so explicit --model/-r/--max-turns still win; /mode lists/switches in the TUI. Build/vet/test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
New read-only ask_user tool (questions[] with options/multiSelect); the loop intercepts it like permissions and routes to Options.OnAskUser. TUI shows an async questionnaire (buffered channel + ctx.Done select, mirroring the permission flow — no deadlock); headless leaves OnAskUser nil so it degrades gracefully to 'proceed with assumptions'. Build/vet/-race green; 13 new tests.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Stops a run after 3 consecutive empty turns (no text/tool calls) instead of burning to maxTurns; one-shot not-called reminder (by turn 3 if a multi-step task skipped update_plan) and stale reminder (after 10 tool calls since last update_plan). Dropped-call turns excluded from the empty counter. Build/vet/-race green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Resolve()/ResolveWithFallback()/FallbackFor() + DefaultReasoningEffort/EffectiveReasoningEffort wired into CLI --model and TUI /model (deprecated models auto-redirect with a notice; unknown ids like minimax-m3 pass through unchanged). zero models renders an aligned table; catalog entries decorated with match patterns/default efforts/deprecation rules. Provider reasoning-effort propagation deferred (needs request-schema change). Build/vet/test green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
Local development snapshot of one session's intertwined workstreams. Committed together because changes span shared files (provider.go, model.go, loop.go, types.go, streamjson) and can't be cleanly split without interactive hunk staging. Build + vet + full -race suite green.
Zenline TUI skin: Zen home / Statusline chat / permission-modal surfaces, 5 themes (Phosphor/Cyan/Sage/Violet/Mono), boot splash, mouse support, and a headless --snapshot mode (internal/zenline, cli/zenline.go, tui/zenline_view.go).
OpenAI provider: streaming tool-call accumulation hardening plus an idle-timeout watchdog (reader goroutine + context cancel) so a stalled upstream surfaces an error instead of hanging the agent forever.
Agent reliability: update_plan schema makes id optional/auto-numbered with an advertised item schema; malformed (nameless) tool calls feed a retry to the model instead of silently ending the turn; secret scrubbing at the registry boundary (covers agent loop AND MCP); structured ToolResult (changedFiles/display/redacted) surfaced via stream-json.
Session file checkpoints + safe rewind: before-mutation content snapshots (content-addressed, deduped, size-capped, 0600) captured on write/edit/apply_patch in TUI and headless; RestoreToSequence + ApplyRewind (path-traversal-guarded, session-locked); /rewind in TUI and 'zero sessions rewind' headless; stream-json checkpoint/restore events.
Design specs under docs/superpowers/specs/.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* feat(zerocommands): add typed sandbox policy, risk, violation, and decision snapshots
The merged permission and verification event contracts (#82) added
typed snapshots for config, providers, models, and sessions, and
PR #89 added the typed snapshot for the persistent sandbox grant
store, but the live sandbox policy, the live risk classification,
the live violation, and the live decision were still exposed as
the raw backend structs. The TUI render path, the headless
'zero sandbox policy --json' and 'zero sandbox decide' commands,
the audit log, and PR/CI automation all need typed snapshots so
they do not hand-roll map[string]any payloads and so they do not
have to re-implement the runtime default-mode translation
themselves.
This commit adds five typed snapshot constructors and a bundled
plan snapshot to internal/zerocommands.
SandboxPolicySnapshot
- mode, network, enforceWorkspace, denyDestructiveShell, and
allowPolicyOnlyRunner are copied verbatim from the underlying
sandbox.Policy.
- effectiveMode is the resolved policy mode. An empty Mode in
the input falls back to ModeEnforce, which is the runtime
default. Consumers can read EffectiveMode to render the 'what
is actually in effect' line without re-implementing the
default.
- effectiveMode is JSON-omitted when empty so the snapshot
shape stays stable for downstream consumers.
SandboxRiskSnapshot
- level, categories, reason.
- categories is copied and sorted alphabetically so the JSON
output is deterministic.
- reason is trimmed.
SandboxViolationSnapshot
- code, toolName, action, path, reason, recoverable.
- The pointer-returning helper returns nil for a nil input so
callers can chain it through SandboxDecisionSnapshot without
nil checks at the call site.
SandboxBackendSnapshot
- name, available, platform, fallback, commandWrapping,
nativeIsolation, executable, message.
- executable and message are trimmed.
SandboxPlanSnapshot
- bundles policy, backend, restrictions, and workspaceRoot into
one payload so 'zero sandbox policy --json' can return one
typed result describing the full sandbox posture.
- each entry of restrictions is trimmed, whitespace-only
entries are dropped, and the result is sorted alphabetically.
- workspaceRoot is trimmed and JSON-omitted when empty.
SandboxDecisionSnapshot
- action, reason, risk, grantMatched, grant, violation.
- The optional grant pointer is converted with
SandboxGrantSnapshotFromGrant (from PR #89) so the JSON shape
matches the persistent-grant snapshot exactly.
- The optional violation pointer is converted with
SandboxViolationSnapshotFromViolation so the snapshot always
carries the same shape regardless of the decision outcome.
- When grant or violation is nil, the field is JSON-omitted.
Tests
- TestSandboxPolicySnapshotFromPolicyFillsEffectiveMode:
verifies EffectiveMode is enforced for an empty Mode, set
to ModeDisabled for an explicit disabled mode, and
preserved for an explicit enforced mode.
- TestSandboxRiskSnapshotFromRiskSortsCategoriesAndTrimsReason:
verifies alphabetical sort, trimmed reason, and that a nil
Categories input produces a nil output (no spurious empty
slice).
- TestSandboxViolationSnapshotFromViolationHandlesNilAndTrims:
verifies the nil-input contract and the trim behaviour for
every string field.
- TestSandboxBackendSnapshotFromBackendCopiesAllFields:
verifies the boolean and string fields round-trip and the
trim behaviour for the executable and message.
- TestSandboxPlanSnapshotFromPlanSortsRestrictionsAndTrimsRoot:
verifies alphabetical sort, per-entry trim, and workspaceRoot
trim.
- TestSandboxDecisionSnapshotFromDecisionAllowBranchHasNoViolation:
verifies the allow path has no grant and no violation.
- TestSandboxDecisionSnapshotFromDecisionPersistentDenyCarriesGrantAndViolation:
verifies the deny path carries both the resolved grant and
the violation, and that the grant snapshot matches the
persistent-grant snapshot shape from PR #89.
- TestSandboxDecisionSnapshotJSONShapeIsStable: verifies the
stable keys and the nil-pointer omission.
- TestSandboxPolicySnapshotJSONOmitsEmptyEffectiveMode:
verifies the omitempty tag.
Verification
- go build ./...
- go vet ./...
- go test -count=1 -p 1 ./internal/zerocommands/... \
./internal/sandbox/... ./internal/redaction/...
DRI: Gnanam (runtime core owner per WORK_SPLIT_PRD.md §4). Closes
the typed-snapshot gap for the live sandbox surface that the TUI
render path, the 'zero sandbox' headless commands, the audit log,
and PR/CI automation consume.
* fix(npmwrapper): make copyWrapperFixture create package.json with type:module
This ensures the isolated wrapper .js fixture is always treated as ESM (matching real package installs).
Previously, without it, Node treated the .js as CJS on some platforms/runners (especially Windows), causing SyntaxError before the missing-native console.error path.
This was causing TestNodeWrapperReportsMissingNativeBinary to get empty output + timeout on windows-latest smoke jobs (e.g. in PR 92).
The test now reliably hits the error path and captures the expected message on all platforms.
Also improves robustness of the npm wrapper smoke test added in #88.
---------
Co-authored-by: Gnanam <gnanam@local>
Co-authored-by: KRATOS <[email protected]>
* feat(zerocommands): add typed MCP, hooks, and plugins snapshots
The merged permission and verification event contracts added typed
snapshots for config, providers, models, sessions, and the sandbox
grant store, but the MCP server config, hooks config, and loaded
plugin manifests still expose only the raw backend structs. The TUI
render path, the headless 'zero mcp', 'zero hooks', and
'zero plugins' commands, and PR/CI automation all need typed
snapshots so they do not hand-roll map[string]any payloads and do
not accidentally leak the secret material that lives in MCP
server Env and Headers.
This commit adds three typed snapshot constructors plus a bundled
backend lifecycle snapshot to internal/zerocommands.
MCPServerSnapshot
- name, type, identity, url, command are copied verbatim and
trimmed. They are non-secret metadata the operator needs to
see when triaging an MCP failure.
- argCount, envKeyCount, headerCount replace the slices and maps
with counts. Secret material in Env and Headers is never
serialized.
- toolCount, allowGranted, denyGranted are optional runtime
counts that the snapshot can carry when the caller has a live
tool registry and a live permission store. A nil counts
struct leaves the snapshot at zero.
HookSnapshot
- id, name, event, matcher, command, args, enabled, source.
- command and args are run through the standard redaction
pipeline. Whitespace-only args are dropped so JSON output is
tight.
PluginSnapshot
- id, name, version, description, enabled, source, root,
pluginDir, manifestPath are copied verbatim and trimmed so the
operator can see where the manifest came from.
- toolCount, promptCount, skillCount, hookCount replace the
full extension slices with counts so the headless JSON output
stays small.
BackendLifecycleSnapshot
- bundles the three slices into a single payload so 'zero
doctor' and 'zero config' can return one typed result that
describes the full extensibility surface.
- all three inner slices are always non-nil, so JSON output is
always '[]' and never 'null'.
Tests
- TestMCPServerSnapshotFromServerStripsSecretsAndCountsMaps:
verifies env and header values never appear in the JSON
output, and that the count fields record the maps' sizes.
- TestMCPServerSnapshotWithCountsMergesRuntimeCounts: verifies
the optional counts bundle is merged and that a nil bundle
leaves the snapshot at zero.
- TestMCPServerSnapshotsSortsAndReturnsEmptySliceForEmptyInput:
verifies alphabetical sort and that nil input returns a
non-nil empty slice whose JSON encoding is '[]'.
- TestHookSnapshotFromDefinitionRedactsCommandAndTrimsFields:
verifies command and args are redacted, and the surrounding
fields are trimmed.
- TestHookSnapshotsSortsByIDAndEvent: verifies the deterministic
(id, event) ordering.
- TestHookSnapshotsWithSourceTagsEverySnapshot: verifies the
helper that tags every snapshot with the same source string.
- TestPluginSnapshotFromPluginCollapsesSlicesToCounts: verifies
the path fields are preserved verbatim and the count fields
record the slices' sizes.
- TestPluginSnapshotsSortsByIDAndReturnsEmptySliceForEmptyInput:
verifies id-based sort and empty-slice stability.
- TestNewBackendLifecycleSnapshotBundlesAndDefaultsNilsToEmpty:
verifies the bundled payload, that nil inputs produce empty
slices in the JSON, and that a populated input round-trips.
Verification
- go build ./...
- go vet ./...
- go test -count=1 -p 1 ./internal/zerocommands/... \
./internal/mcp/... ./internal/hooks/... \
./internal/plugins/... ./internal/redaction/...
DRI: Gnanam (runtime core owner per WORK_SPLIT_PRD.md §4). Closes
the typed-snapshot gap for the three backend surfaces in §7.E
that the TUI render path, the headless commands, and PR/CI
automation consume.
* fix(zerocommands): strip URL credentials and preserve redacted arg position
Addresses the three actionable review items left on PR #90 by
CodeRabbit.
1. MCPServerSnapshotFromServer now strips userinfo from the URL
field so credentials embedded in an MCP server URL
(https://user:token@host) never reach the headless JSON output.
The new stripURLCredentialsFromURL helper uses net/url.Parse to
remove the User field, then returns the sanitized URL. A URL
that fails to parse is returned trimmed (not empty) so the
operator still sees the configured endpoint when triaging a
malformed MCP configuration. A blank input returns blank.
2. redactStringSlice now preserves position. Previously the helper
appended to the output slice and skipped any element whose
redacted value was empty, which meant a fully-redacted secret
in the middle of a hook command line would shift every
following argument by one position. The helper now allocates
the output slice to the input length and assigns each element
by index, so a fully-redacted element becomes an empty string
in the output rather than disappearing. Whitespace-only inputs
also become empty strings by index, which matches the previous
drop behaviour but without shifting positions. A nil or
empty input still returns nil so the JSON output omits the
field entirely.
3. Test coverage is extended for both changes:
- TestHookSnapshotFromDefinitionRedactsCommandAndTrimsFields
now asserts len(snapshot.Args) == len(def.Args) so a future
regression that drops or shifts redacted args is caught.
- TestHookSnapshotFromDefinitionPreservesPositionForRedactedArgs
verifies that a secret in the middle of an arg list ends up
as an empty-string element at the same index, while the
surrounding non-secret args round-trip verbatim.
- TestMCPServerSnapshotStripsURLCredentials verifies that an
https://admin:secret123@host URL becomes the credential-free
form and that the username, password, and the '@' separator
do not appear anywhere in the snapshot.
- TestMCPServerSnapshotPreservesURLWithoutCredentials verifies
that a URL without userinfo passes through unchanged.
- TestMCPServerSnapshotKeepsUnparseableURLInsteadOfEmpty
verifies the tolerance contract: a malformed URL is returned
trimmed rather than empty, so the operator still sees the
configured endpoint when triaging a broken configuration.
Verification
- go build ./...
- go vet ./...
- go test -count=1 -p 1 ./internal/zerocommands/... \
./internal/mcp/... ./internal/hooks/... \
./internal/plugins/... ./internal/redaction/...
DRI: Gnanam.
---------
Co-authored-by: Gnanam <gnanam@local>
The merged permission and verification event contracts added typed
snapshots for config, providers, models, and sessions, but the
persistent sandbox grant store (internal/sandbox/grants.go) still
exposes only sandbox.Grant directly. The TUI render path, the
headless 'zero sandbox' command, and PR/CI automation all need a
typed snapshot so they do not hand-roll map[string]any payloads and
do not accidentally leak grant reasons that contain API keys or
tokens.
This commit adds SandboxGrantSnapshot alongside the existing
ProviderSnapshot, ModelSnapshot, SessionSnapshot, and
SessionTreeSnapshot in internal/zerocommands. The snapshot mirrors
the format of the existing typed contracts:
- ToolName, Decision, MaxAutonomy, ApprovedAt are copied verbatim
from the underlying grant. They are non-secret metadata.
- Reason is run through the standard redaction pipeline before it
is copied, so any secret material the user typed when granting
is masked before the snapshot leaves the runtime.
- All string fields are trimmed. An empty or whitespace-only
Reason becomes an empty Reason in the snapshot so JSON output
omits the field.
Two constructors are provided:
- SandboxGrantSnapshotFromGrant(grant) for a single grant.
- SandboxGrantSnapshots(grants) for a slice. Output is sorted
alphabetically by ToolName so consumers (TUI, headless, JSON
output) see a deterministic ordering. An empty input returns
a non-nil empty slice so JSON output is always '[]' and never
'null'.
A matched/unmatched helper is also added:
- SandboxGrantMatchSnapshotFromLookup(toolName, lookup) pairs the
typed snapshot with a Matched boolean. When the lookup did not
match, the returned snapshot has Matched=false and a nil Grant
pointer, so consumers can render the absence without inspecting
an error or a typed zero value.
Tests
- TestSandboxGrantSnapshotFromGrantRedactsReasonAndTrimsFields:
verifies the secret in the reason is masked and the surrounding
fields are trimmed.
- TestSandboxGrantSnapshotFromGrantEmptyReasonBecomesEmpty:
verifies whitespace-only reasons normalize to the empty string.
- TestSandboxGrantSnapshotsSortsByToolNameAndReturnsEmptySliceForEmptyInput:
verifies alphabetical sort and that nil input returns a non-nil
empty slice whose JSON encoding is '[]'.
- TestSandboxGrantMatchSnapshotFromLookupMatchedAndUnmatched:
verifies the matched and unmatched code paths.
- TestSandboxGrantSnapshotJSONShapeIsStable:
verifies every JSON key is present so downstream consumers can
rely on the shape.
Verification
- go build ./...
- go vet ./...
- go test -count=1 -p 1 ./internal/zerocommands/... \
./internal/sandbox/... ./internal/redaction/...
DRI: Gnanam (runtime core owner per WORK_SPLIT_PRD.md §4).
Connects: 'zero sandbox' command rendering, TUI grants panel, and
PR/CI automation consumers to the persistent grant store without
leaking grant reasons.
Co-authored-by: Gnanam <gnanam@local>
* feat: add runtime permission event contract
* feat: stream permission events from exec
* feat: add runtime command snapshots
* feat: add session rewind compaction plans
* feat: add verification event contracts
* fix: harden runtime contract review gaps
* test: assert approved exec stderr contract
* fix: address CodeRabbit CHANGES_REQUESTED items for #82
- Make OnSandboxDecision callback async + panic-safe (non-blocking, recovered) in tools/registry.go per CR.
- Add explicit regression test for ConfigSnapshot when ResolveRuntimeMetadata errors (no secret leak in Message/BaseURL).
- (Many prior items like shapedPayloadPreview for permission/tool events, dual-target conflict test, stderr empty on approved path, Kind/Framework preservation, redactCommand empty slice, etc. were already landed on this branch.)
All relevant tests now pass.
* fix(agent): make sandbox decision available synchronously on Result for permission events
The recent change to invoke OnSandboxDecision asynchronously (to satisfy non-blocking requirement) broke the agent's immediate capture of the decision for building PermissionEvent after registry.RunWithOptions.
- Add SandboxDecision *sandbox.Decision to tools.Result (not serialized).
- Populate it on all sandbox-involved return paths in registry.RunWithOptions (sync, before/while firing async callback for observers).
- In agent executeToolCall, use result.SandboxDecision for buildPermissionEvent (reliable ordering) instead of side-effect capture via the callback.
- The async callback is still supported for any external OnSandboxDecision consumers.
This fixes the two failing agent tests that were causing the cross-platform 'Smoke' CI jobs to fail on this PR:
- TestRunEmitsPermissionEventForPersistentSandboxGrant
- TestRunAppliesSandboxEvenInUnsafeMode
All go tests now pass.
---------
Co-authored-by: KRATOS <[email protected]>