Files
platform/docs/plugin-dependency-scope.md
T
pastilhasandClaude Opus 5 4efc2552d0 os-deps: a dependency declares who has to be able to run it
The probe asked the owner's login shell where a binary was, and the install reported that as a fact
about the machine. For a confined plugin the work happens as the member's own Linux user, in their own
home, where the owner's ~/.local/bin is neither on PATH nor reliably reachable.

On alpha it worked — because /home/pastilhas happens to be mode 751, world-traversable, so an
absolute path resolved. At 750, the more usual default, every member would have lost every binary with
nothing reporting it: the install would still have said satisfied, having only ever asked the owner.
Nobody chose 751, and that is the part worth fixing.

`scope: 'owner' | 'shared'` on an osDependency, defaulting to owner, an unrecognised value being a
manifest error rather than a silent fall back to the permissive one.

Shared changes three things. It is searched for on system directories only — asking the owner's shell
would find the copy in the owner's home and call it satisfied, the exact false pass this exists to
stop. Reachability is checked as world-executable AND every parent traversable to the root, because a
binary at 755 inside a home at 700 is unreachable and stat on the file alone says it is fine. And a
release binary goes to /usr/local/bin through sudo -n install, staged in the owner's space first so
the download itself never runs as root.

An unreachable shared dependency gets its own state and refuses the install ahead of every other
check, because no package manager fixes it: the binary is installed and works, and what is wrong is
where it lives — reinstalling would report success and change nothing.

Not a security boundary, and docs/plugin-dependency-scope.md says so: anything in /usr/local/bin is
runnable by any account with a shell, grant or no grant. That is already true of ffmpeg. The plugin's
data is gated; its executable is not.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-21 00:15:38 +00:00

4.9 KiB

Plugin dependency scope

Built 2026-08-21. A plugin declares WHO has to be able to run each host binary, and the platform installs and checks accordingly.

The bug

os-deps.ts probes a dependency by asking the owner's login shell where the binary is. That is the right question for most plugins and the wrong one for some, and nothing distinguished them — a per-user answer was reported as a fact about the machine.

It matters because confined plugins do their work as the member's own Linux user, in their own home. A binary in the owner's ~/.local/bin is not on that user's PATH, and reaching it means traversing the owner's home directory.

On alpha it happened to work. /home/pastilhas is mode 751 — world-traversable, not readable — so an absolute path resolved for members. Mode 750, which is the more usual default, would have broken every member's binary with nothing reporting anything: the install would still say satisfied, because the install only ever asked the owner.

Working by accident is the part worth fixing. Nobody chose 751.

The field

osDependencies: [{ binary: 'iris', reason: '…', packages: {…}, scope: 'shared' }]

owner is the default and stays right for most things — anything an execution plugin uses runs as the owner in the owner's home by definition. shared says every account with a Linux user must be able to run it, which changes both the install target and the check.

An unrecognised value is a manifest error rather than a silent fallback to owner, because the fallback is the permissive one.

What changes when it is shared

Where it is looked for. System directories only — /usr/local/bin, /usr/bin, /bin, and the sbin trio. Deliberately not the owner's login shell: that shell finds the copy in the owner's home and reports the dependency satisfied, which is the exact false answer this exists to stop.

What counts as reachable. Two things, and checking only the first is how this hid:

  • the file is world-executable (0o001 — group is not enough, members are not in the owner's group)
  • every directory above it is traversable, walked to the root

A binary at 755 inside a home at 700 is unreachable, and stat on the file alone says it is fine.

Where a release binary is installed. /usr/local/bin, via sudo -n install -m 0755. The download is still staged in the owner's own space — fetching straight into a root-owned directory would mean running the download as root, which is a bigger ask than moving the result. -n so a plugin install never blocks on an invisible password prompt.

What blocks an install. A shared dependency that is present and working but unreachable gets its own state, unreachable, and its own refusal — checked before everything else, because no package manager fixes it. The binary is installed; what is wrong is where it lives. Reinstalling the package would report success and change nothing.

What this deliberately does not do

It does not gate execution on a grant. A binary in /usr/local/bin is runnable by any account with a shell, whether or not they hold the plugin's permission. The kernel does not know about role_permissions and never will.

That is the same boundary ffmpeg and everything else in /usr/bin already has, and it is the right one: the plugin's data is gated by the permission system, its executable is not. For iris it is benign — a member who runs it captures their own $HOME profile and their own sessions, which is the isolation the wrapper already depends on.

If a binary must not merely be reachable but unusable without a grant, scope is not the mechanism. That needs the plugin's own surface behind the permission gate, and the binary not on anyone's PATH.

Iris

The motivating case, and still the only shared one. PLUGIN.md had already recorded it as open item 3 — "the binary lands in the owner's ~/.local/bin, and a confined member's agent cannot read it there" — but that had not been connected to the probe being owner-scoped, so the install reported success either way.

iris and iris-browser are built by the sidecar rather than fetched, so declaring scope: 'shared' makes the probe honest about who can run them; moving the sidecar's build output into /usr/local/bin is the remaining half, and it is a change to the sidecar rather than to this.

Status

Built scope on a dependency, validated, defaulting to owner
Built shared probing on system paths, with a traversal walk to the root
Built shared release binaries installed to /usr/local/bin via sudo -n
Built an unreachable shared dependency refuses the install, naming the directory
Open iris's sidecar still builds into ~/.local/bin — see above