Files
KRATOS 3f3de1a29b permissions: scope-enforced grants (permission-UX Phase 0 + 1) (#170)
* permissions: show grant scope on the permission card and decided rows

The permission card only named the tool, so 'always' read as a blind tool-wide
yes. Derive a concise scope from the call's args (the file path, directory, or
working dir it touches) and show it on the card and the persisted decision row,
so a user can see exactly what an allow covers.

Phase 0 of the permission-UX work: presentation only, no storage change.
permissionScope + scope plumbing through PermissionEvent/PermissionRequest;
covered by TestPermissionScope.

* permissions: spec for Phase 1 scope-enforced grants

* permissions: enforce grant scope so "always allow" covers only what the card showed

Phase 0 displayed the scope a tool call touches but the persisted grant was still
tool-wide: an "always allow" on a write to one file silently authorized every
write. This scopes the grant to exactly that file or directory.

sandbox/scope.go centralizes scope derivation (DeriveScope), absolute-path
resolution (resolveScopeAbs, anchored to the workspace so a grant never leaks
across projects), and matching (grantCovers: file = exact, dir = subtree,
empty = tool-wide; a tool-wide request is never covered by a narrower grant).

Grants are now stored per-tool as a list (schema v2, with v1 files migrated as
tool-wide grants). Lookup takes the request's absolute scope and applies
deny-wins / most-specific-allow precedence. engine.Grant anchors a relative
scope to the workspace; engine.Decide derives and matches the request scope.
agent.persistPermissionGrant forwards the scope, and permissionScope now shares
DeriveScope so the card and the stored grant can never diverge. Canonicalizing
tool keys on read also closes the whitespace-padded-key lookup miss.

Covered by sandbox scope/grant/engine tests and the agent/TUI always-allow tests.

* sandbox: fix Windows-only TestResolveScopeAbs by using truly-absolute paths

A leading separator (\proj\a) is rooted but not absolute on Windows
(filepath.IsAbs requires a volume like C:), so the absolute-passthrough
case was treated as relative and anchored onto the workspace root,
producing a doubled path. Build the root via filepath.Abs so the test
exercises a genuinely-absolute path on every platform.

* test: isolate session store in exec/tui tests so go test stops writing to $HOME

TestRunExecStreamJSONRunStartUsesResolvedAPIModel,
TestRunExecReadsStreamJSONPromptFromStdin, and
TestPromptSubmitInjectsLiveSessionModelContext ran real exec sessions
but only isolated cwd (t.TempDir + getwd), not the session store, so the
default store resolved sessions.DefaultRoot() off the real $HOME and
persisted into ~/.local/share/zero/sessions on every run. Set
XDG_DATA_HOME to a per-test temp dir (the existing convention in
exec_test.go / exec_scope_test.go) so each run is fully isolated.

* permissions: address review — clean root-equiv scopes, label+fit scope rows

- DeriveScope: filepath.Clean the path before the root check so ./, ./.,
  and a/.. collapse to the workspace root and surface as tool-wide instead
  of a narrower directory grant (which re-prompted inconsistently). Adds
  regression cases.
- renderPermissionRow: prefix the scope segment as scope:<value> across all
  three branches and fitStyledLine the allow branch (it returned unfit and
  could overflow narrow terminals).
- TestGrantReplacesSameScope: assert the setup store.Grant errors so a setup
  failure fails at the real cause, not a stale-state assertion.

* docs: point permission-scope spec at grant_scope.go

The design spec named internal/sandbox/scope.go as the source of truth, but
DeriveScope/resolveScopeAbs/grantCovers live in grant_scope.go (scope.go is
the unrelated write-roots Scope from #162). Fix the heading and the test
reference (grant_scope_test).
2026-06-11 20:33:52 +05:30
..