Files
Zecheng Zhang 2b5c2b42f3 feat(permissions): one role per session, mounts narrow instead of gating
A role is now the whole permission document a session runs under. There
is no workspace `permissions:` block, no `permissions:` on a mount, and
no `extends`, so reading `profiles.<name>` is reading everything that
role may do. `mounts:` states infrastructure only.

Two rules decide a line, and they are the whole law. A rule naming no
path is read by verb, deny before ask, wherever in the document it is
written. A rule carrying paths, and every hide, is read by anchor depth:
the deeper entry wins, ties break by verb. `decide()` holds both.

Naming a mount narrows it; it is not an allowlist. A mount a role does
not name keeps its own mode. Keeping a session away from one is a hide,
which answers ENOENT rather than a refusal that names what the role
cannot see (EACCES on a create, since a silent success would leave a
file the session cannot find).

Three ways a command still learned a hidden path was there, all fixed
above the backend rather than per command:

- MountView splits into `descendants` (every mount below a path, for
  pruning) and `visible_descendants` (session-filtered, for naming), so
  `tree`, `tar` and `zip` stop drawing or naming a hidden mount while
  still refusing to cross it.
- `du` decided existence from the bound accessor, which knows nothing of
  hides. It asks the dispatcher first and its content probe now counts
  only entries the session may see.
- TypeScript `readlink` treated only PolicyDenied and EINVAL as silent,
  so ENOENT printed a raw path. It matches python's `except OSError`.

dsh's read-only twin copied only the source session's mount modes, which
used to be the allowlist; it now carries the hides too.
2026-08-20 07:37:00 -07:00
..