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

92 lines
4.9 KiB
Markdown

# 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
```ts
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 |