2b5c2b42f3
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.