Security defines the sandbox guarantees
What every sandbox guarantees, what can cross it, and how a spec is checked. What a harness's sandbox opens, grant by grant, is in agents' security.
Where each part is
Every sandbox starts closed, and what it closes on each OS, and how, is
the wall's: The Wall.
Every sandboxed agent with [internet] runs behind the filtering proxy,
its one way out, unless it has [network] mode = "host": The Filtering
Proxy. The
tools that run outside are on Tools and Asking, and what it
leaves open on Gaps and Limits.
What can cross the sandbox
A sandbox is only as closed as what it may talk to: a service it may
reach can be made to do more than it's for. So each way out is judged
by what it can be made to do, and check tries each one. A sandbox that
reaches any host ([internet] hosts = ["*"], or [network] mode = "host") can send anything it can read anywhere.
The network as it is
[network] mode = "host" (--network-mode host): any host and port on
the internet. On Linux also every localhost port and the machine's
abstract unix sockets, X11's among them. On macOS also every localhost
port through ::ffff:127.0.0.1, and the Mac's own address. It writes
what any service it reaches keeps.
[internet]
--internet-host: any host over HTTP CONNECT or plain http://, or
only those listed. Never this machine, its networks or a cloud's
metadata service. Agents with [internet] get it unless they have
[network] mode = "host"; on a box, always.
On macOS
When it reaches the internet, the certificate checks (trustd) too,
which TLS through the Security framework needs end to end; never DNS or
the network's settings, which the proxy does outside.
Models and a service's secret
A local model through the proxy: that runtime's inference only. Its admin API gets a 403 from Link, and on macOS its port itself is closed. A hosted model through the proxy: that provider's inference only, with the key added outside. Its server-side tools are removed and URLs it would fetch are refused. The provider keeps what it keeps of requests.
A service's secret
Through the proxy (a Telegram bot's token): that service's calls on the
paths its rule allows, with the secret added outside, but for those the
rule refuses (Telegram's setWebhook, logOut and close). A bot can
message any chat it can, so what it read can go to a chat of anyone's
choosing.
[lan]
--lan-host, through the proxy: the machines of your network it lists,
on any port, by name, address or range, * for every private one: only
private addresses (10/8, 172.16/12, 192.168/16, fc00::/7), the
carrier-grade range (100.64/10, Tailscale's) only by an exact address.
Dialed from here, never through the upstream. Whatever serves on those
machines can be made to do what it does for anyone on your network.
Never, whatever is listed
This machine, its containers and VMs (the network of any bridge it has:
Docker's, libvirt's, a VM app's), a link-local address or a cloud's
metadata service. An IPv4 address written in IPv6 (mapped, compatible,
NAT64, 6to4) or as a URL may write it (3232235777, 192.168.0x1.1) is
judged as that address; a listed name that resolves outside your network
reaches nothing.
With *
A name is dialed only at a private address this machine's resolver gives
it: a public one is refused, and where it resolves to nothing (a machine
without DNS, behind an HTTPS proxy alone) nothing is dialed. The
machine's own proxy (HTTPS_PROXY) is never the sandbox's upstream.
On a cloud's machine
On AWS, Google Cloud or Azure, by its firmware, *, or a range holding
a whole private block (0.0.0.0/0, 10.0.0.0/8, ::/0), is refused,
whoever made the spec: the private ranges there are the cloud's network.
[browser] and a page opened
--browser-host: its hosts are apart from the internet's, so a sandbox
reaching one host may still ask to open a page on any host it lists. The
page carries what the URL holds, so each one is asked.
A page opened, through the proxy
An https:// page in your browser, after you allow that one page, its
whole URL shown. It is asked for at $LNK_SANDBOX_OPEN, behind the
proxy's secret, which $BROWSER in the sandbox uses. The page opens
with your cookies and logins, so a URL can carry what the program read
to a site, or act as you on one you're signed in to: allow only a page
you asked for.
Never
An app, a file, another scheme, or this machine or its network by name
(localhost, localhost.), by a name that resolves there when it's
asked (127.0.0.1.nip.io), or by address, however a browser reads one
written (127.1, 2130706433, 0x7f000001, 0).
Its name, as it resolves
The name is checked as it resolves here within 5 seconds: one that doesn't resolve by then is let through, and the browser resolves the name again, so one whose answer changes in between, or that resolves only for the browser, is its own.
A login's callback (Linux)
For a page you allowed whose redirect_uri is a port on localhost,
that port on this machine is passed into the sandbox until the first
request arrives, or for five minutes at most. Meanwhile anything on this
machine that connects to it reaches the program. A port another program
already listens on is refused, so the login's answer never goes there.
Tools and ports
A tool at a URL: that server, called from outside with its headers,
each call asking you as any tool's does. A localhost port (--port, or
a local model's without the proxy): whatever serves it, with its whole
API, a model runtime's pull, create and delete included. check
reports it as open.
Namespace membership
A namespace's host definition fixes its member servers and action names. An MCP argument cannot add a member or change its address. Every call checks the original provider's current approval policy, and a removed provider or changed launch is refused before forwarding.
Folders
Folders it writes (--write, a harness's home, your files folder):
files only. You, lnk bucket and lnk agent state-send read them, as
What Link reads back says.
--read ~ reads your whole home folder, never Link's settings.
On macOS
Listening, behind its proxy or with no network: only on localhost and
only on the ports its spec names, so it can't take another program's
port while that one is stopped; with [network] mode = "host", on any
address and port. --developer-tools: Xcode and its preferences, read
only.
Services every sandbox gets
Users and groups, the log, notifications and the temp folder's helper, each only as far as it answers. It can write to the system log.
An agent's ways out
A Link agent's harness has the doors above that its spec opens, and two kinds beside, its only others.
Its proxy
The internet, its model and its tools, as its grants say, the keys and secrets added outside (The Filtering Proxy).
Observing wire state
Status reads the owner's stop and pause flags through the sandbox client contract on every request, without launching a command or caching the answer. The observer gains no authority to open a wire, read a secret or bypass revocation. Proxy enforcement, in-flight cancellation and private flag writes remain with the proxy and its commands.
Its bridges
A connection's (messages in and out; the connection's token stays outside), the way in (messages from its schedules, subagents, background commands and teammates), and the store bridge (its conversations' events and lease, for a spec that keeps them with a driver; the driver and what it reaches stay outside).
As itself
Each call to one is the agent's identity, the action and a proof made with the agent's own key, so a harness acts there only as itself (each bridge, and what it holds).
Specs and checks
A spec is a file, and a file can come from anyone, so lnk sandbox run --spec checks what it opens before it runs, and lnk sandbox check
tries it from inside.
What a spec can and can't open
A spec not run here before, or one that opens something else since,
prints what it opens and asks. --yes skips the question. A path a link
makes elsewhere is shown with where it really is.
What's remembered
Link's settings keep, in ~/.lnk/specs/sandbox.toml, each spec's path
and a hash of its grants with every path resolved through links and
every program as PATH found it, so a change in what it opens or a link
moved under a folder it names asks again; a new name or comment doesn't.
Refused in a spec's file
With the flags beside it: what would open Link's settings or agents'
folders, let code the program writes run outside the sandbox (in your
home's dot folders, on your PATH, in the system's folders or a
project's dot files), widen the spec's own next run, or reach other
programs' sockets (each).
Folders a run is given to write
--with-fd's write are refused as a spec's writes are
(how). A given folder
inside an agent's would open what the sandbox hides, so it's refused.
Link's settings
They stay closed to every sandbox, spec or flags, to reading and writing: a grant inside them, or a write of a folder holding them, is refused.
[localhost] ports = ["*"]
It's refused: every port of this machine would include other sandboxes'
proxies, each another one's way out. So is 40781, the port of
main's proxy.
[commands]
It limits which files run, by the kernel: Seatbelt on macOS, Landlock on
Linux (5.13 and newer, enabled). A finite effective allowlist and a spec
without [commands], which grants no programs, both require Landlock.
allow = ["*"] removes executable restrictions while retaining the
independently enforced file and network restrictions.
A launch refused
Missing Landlock, a missing in-sandbox confiner or an unsupported syscall-filter architecture refuses the launch, naming the missing capability. A Landlock probe preserves the ABI on success and the OS error on failure. An unavailable syscall may mean missing kernel support or filtering by an outer runtime.
Checked again inside
The sandbox checks again inside and stops before the target runs if creating or applying the ruleset fails. It never substitutes unrestricted execution.
Entries refused
An entry in a folder it may write, or in /tmp, /var/tmp, /dev/shm
or the temp folder, is refused, and so is a folder holding one of those.
lnk sandbox check --spec tries a program it doesn't list, and each way
a program might run code around the list. How a refused program is
named: The
Wall.
Programs found on PATH
A command named without a path is resolved to its real executable at discovery, including through user-owned package-manager links such as Homebrew's. The confinement carries that path into grant validation and wrapping, so changing the discovery link does not redirect the grant. An explicit path remains subject to the wall's refusal of non-root-owned links, as do file and folder grants.
Checking a sandbox
lnk sandbox check runs probes inside a sandbox as the flags or spec
given would make it, against bait it sets up, and removes its bait
after.
- a file with a random secret in the home folder, in Link's settings
folder, in
/var/tmpand in each hidden folder there is (agents' folders among them); and a listing of~/.ssh - a write into the home folder, and a write into each folder the spec may write, which must work
- a server on a random localhost port, tried as 127.0.0.1, ::ffff:127.0.0.1 and the machine's address
http://1.1.1.1, or behind a proxy, a tunnel to it through the proxy, and asking the proxy for this machine, your network, a host not listed, and IPv4 written in IPv6's forms- the folder
lnk-sandboxis in, and each dot file that runs outside at the top of each folder it writes - a program holding the secret in its environment (
ps -Eon macOS,/proc/<pid>/environon Linux) - each localhost port the spec allows: whatever serves it is reachable with its whole API, reported as a known gap
On macOS
open -a Finder, a search for a throwaway item Link adds to the login
Keychain, and a lookup of each system service by name. Those every
sandbox gets and those a grant opens count as allowed; the clipboard,
the screen, Apple Events, the privacy database and core services never
do. lnk-sandbox looks them up inside.
leaked
Anything that gets through without a grant allowing it is leaked, and
the command fails. A host asked to be refused is reached when the proxy
answers anything but its refusal (a 502 is an address it took that
didn't answer), but a name with no address here only when the proxy
connects: its 502 then is the name not resolving, and nothing was
dialed.
Tests
Tests also lnk sandbox run a program that tries to read ~/.ssh,
Link's settings and its caller's environment, write the home folder and
reach the network.
What Link reads back from a sandbox
Link treats everything it reads back from a sandbox as hostile.
A harness's log
lnk agent logs, kept outside its home: a regular file only (a link in
its place is refused), read from where Link recorded the start, at most
its last 4 MiB, shown without control characters. With --follow, a
line without an end comes in 64 KiB pieces.
How to run the harness
What its adapter's command says, run in its home: only plain absolute
paths that neither are nor hold its home, your files folder, your home
folder, Link's settings, /tmp or a folder of other programs' sockets
(/run, /var/run, $XDG_RUNTIME_DIR), checked as written and
resolved.
Like a spec's
None it writes is the system's (/usr, /opt, /etc, ...) or is,
holds or is in a folder on your PATH. Service names that are only
names; no HOME, TMPDIR, LD_* or DYLD_*.
What to move and back up
lnk agent state-send and lnk agent backup, from its adapter's state and
its home: paths under its home only, links carried as links. Each name
is opened from its folder's handle without following a link, so one
swapped in during the read isn't followed either.
A move's stream and a backup
lnk agent receive and lnk agent restore, from another machine's
send or the files folder's .lnk-backup: unpacked in a folder out of
every agent's reach. An entry with .. is skipped, a leading /
dropped, and one reaching out through a link, or a hard link to a file
outside, refused. Links stay links, and are moved, never followed.
Everything else
Your files folder (lnk bucket push): links are skipped, files copied
as data. Requests to its proxy: names are resolved outside. A session's
folder: Link reads nothing there. The wire's log: written outside the
sandbox, names cut at 300 characters, shown without control characters.
Ordinary-box identities
On an ordinary box each sandbox runs as a fresh non-sudo host user, with its controller and proxy outside that identity. Private state views preserve controller ownership and read-only permissions; cleanup and replacement use durable identities and process handles. The requirements, retained-file capability limit and maintenance procedure are in box security.
Sessions over SSH
lnk sandbox enter --spec <file> --folder <dir> is what a key's forced
command runs: whoever lets the key in (a box's root, for a box's
users)
names both on the line, where the key's holder can't change them. The
spec is read as any spec is, at each session, and the folder is the one
place it writes.
In the sandbox
A shell, a command, scp or sftp all run in the sandbox: what the
session asked for (SSH_ORIGINAL_COMMAND) is run by /bin/bash inside
it, so a spec whose [commands] doesn't let /bin/bash run is refused.
--check runs nothing: it says what the sandbox would open.
Its terminal
On Linux a session with a terminal keeps it: bubblewrap's new session is
for a terminal someone else reads after, and theirs is their own. scp,
sftp or a command without one gets a new session.
What it reads
A session reads nothing of the home folder it runs with but what its
spec opens, never Link's settings or an agent's folder (~/Link/Agents,
~/Link/History), even with --read ~, and has no network unless its
spec opens one.