Context
A plugin is a Deno process started with no permissions at all — no file, network, environment or subprocess access — and everything it asks for arrives over stdio and is checked by the capability broker. That is two layers, and ADR-003 is the argument for them. Both layers are in userspace. Deno’s permission model is enforced by Deno; if a plugin escapes it, nothing below stops it reaching the host. Nothing did, and nothing was watching for it either. A second question arrived with it: whether a sandbox could replace the broker — “sub-sandbox the plugin and let it do the thing itself” — which would remove the need to widen Cordial’s Flatpak manifest every time a plugin needs anything.Decision
Add a third layer, below Deno, usingbwrap on a host install.
Do not take the Flatpak route. A Flatpak install keeps the two layers it
already had. See below — the portal that would provide the third one costs more
than it buys.
Keep the broker exactly as ADR-007 states it. A sub-sandbox is not an
alternative to brokering and cannot become one.
A missing sandbox binary does not stop a plugin running. It is reported
instead: crates/cordial-plugins/src/sandbox.rs’s Sandbox::describe is printed
at every spawn, naming the layers that are actually in force.
Why a sandbox cannot replace the broker
Because a sub-sandbox only ever subtracts.bwrap and flatpak-spawn --sandbox both narrow what the parent holds; neither grants anything the parent
lacks. So confinement can never let a plugin do something Cordial cannot, and
every effect still has to be performed by Cordial. ADR-007 is untouched by this
change, not weakened by it.
The concrete case, because the abstract one invites cleverness. The Flatpak
manifest grants Cordial Discord’s IPC socket, narrowly, by path. Hand that
socket to a confined plugin so it can set its own presence and the plugin holds
a Discord IPC connection — and that protocol does considerably more than set
presence. The sandbox did not narrow the socket; it narrowed the filesystem
around it.
“We sandbox plugins now” is the argument someone will eventually use to
justify passing a plugin a file descriptor. It is written down here, and in
that module’s header, so the next person meets the counter-argument before they
meet the idea.
Why its absence is a downgrade rather than a hole
This is the whole reason the layer can be optional, and it is worth being precise about. Withbwrap absent, a plugin still has zero Deno permissions and
still reaches nothing except through the broker. The security model does not
rest on the new layer; the new layer catches a failure of the old one.
The alternative — refusing to run plugins without bwrap — would turn a
packaging detail into a permission. A machine without the binary would either
lose plugins entirely or, worse, run them and imply confinement it did not have.
The second is this project’s stub-that-lies failure wearing different clothes,
which is why the layers in force are printed rather than assumed.
Why a Flatpak install gets nothing here
The original draft of this ADR added--talk-name=org.freedesktop.Flatpak to
the manifest and called it “one override, once”. That was wrong, and the
manifest itself said so before the change was written. Its header has always
listed the absence of that grant as deliberate: “arbitrary command execution on
the host, which would hand every plugin the sandbox escape the capability model
exists to prevent — below the level any broker can see” (ADR-002 §2).
The reason is that flatpak-spawn --sandbox and flatpak-spawn --host are the
same D-Bus name. A permission that lets Cordial create a narrower sandbox is
the same permission that lets it run an arbitrary command on the host, outside
the sandbox entirely. There is no way to take one and not the other.
So taking it would mean adding a sandbox escape to Cordial in order to sandbox
plugins — strictly worse than not having the layer, because the layer defends
against a Deno escape while the grant hands out a host escape unconditionally.
bwrap cannot substitute inside a Flatpak either: the outer sandbox blocks the
user-namespace nesting it needs. It is deliberately not attempted, because a
failed spawn is a plugin that does not start rather than one that starts
unconfined.
A Flatpak install therefore reports Sandbox::None and keeps Deno’s zero
permissions and the broker — exactly what it had before this ADR. That is the
downgrade-not-a-hole case, and it is why that principle had to be settled first.
Consequences
- Three layers where there were two, and the third is kernel-enforced.
- No change to the Flatpak manifest, and a Flatpak install gains nothing from this ADR. Host installs get the third layer; Flatpak keeps two.
- Plugin startup names its own confinement, so “was this plugin sandboxed” is a log lookup rather than an inference about what the host had installed.
- The sandbox has to make the interpreter reachable, which is less obvious than
it sounds: binding
/usr,/lib,/lib64and/binis where a distribution puts Deno and is not where a Homebrew or Nix install puts it. The first version bound only those and every sandboxed plugin failed to start. The interpreter’s real path and its install prefix are resolved and bound read-only;interpreter()carries the failure that motivated it. - A cold Deno cache per launch, because
DENO_DIRpoints into the sandbox’s own tmpfs rather than the user’s. For a single local module this is not measurable, and it keeps a plugin’s cache off the host.
What would change this
Evidence that the confinement blocks something a plugin legitimately needs and the broker cannot express. That would be an argument for extending the broker, not for loosening the sandbox — but it would be an argument, and it should be made here rather than by widening the bind list until a symptom goes away. A Flatpak portal that creates sub-sandboxes without also granting host command execution would change the Flatpak half of this immediately. The objection is to whatorg.freedesktop.Flatpak bundles together, not to sub-sandboxing.
If a future Deno gains an isolation primitive that subsumes this layer, drop the
layer rather than keeping two that overlap. Note that Deno’s permission model
was already the second layer when this was written, and it was not enough on its
own precisely because it is enforced by the thing being confined.