Sandbox interface for isolated programs
What other programs can rely on from lnk sandbox.
lnk sandbox
A plugin confining a program makes a link_sandbox_client::Confinement
from a spec (Confinement::of) and calls link_sandbox_client::wrap
(lnk sandbox wrap, installing the sandbox plugin first if it's
missing), then runs the command it gets with the environment it
chooses. The sandbox decides only what the program may touch; what
runs, where and with which environment stays the caller's.
The contract and the spec
The contract's source is
sandbox.rs.
A sandbox's spec, link_sandbox_client::Spec
(reference), is the type a plugin keeps
one as; what it opens is the sandbox plugin's to say, by running it.
lnk sandbox wrap
Reads {"confinement", "argv"}, or {"unwalled", "argv"}
(link_sandbox_client::WrapRequest), on stdin, and prints {"argv", "env"}: the command running argv confined to it on this OS, and what
to add to its environment.
What it refuses
An unknown key in the confinement, its net or its proxy, or a policy
({"policy", "argv"}, as callers sent before confinements), is refused.
What it adds to the environment
With a proxy, HTTPS_PROXY and the rest, and each route's env: a
tool's address, LNK_TOOL_<NAME>, a local runtime's,
LNK_LOCAL_MODEL_<PORT>, and with open, BROWSER and its address,
LNK_SANDBOX_OPEN. Those of a route that needs it carry secret, 16 to
64 letters and digits the proxy requires on it, or one of the wrap's
own.
What it writes
Its rules to a unique profile under ~/.lnk/sandbox/ (macOS), its
routes and their caller to <name>.routes.json, and secret to
<name>.url-secret beside them.
Who reads it
lnk-agent: every step of a harness (install, configure,
command, health, the harness); with no sandbox, the harness alone,
unwalled, and lnk agent harness run.
A confinement
link_sandbox_client::Confinement, made from a spec by
Confinement::of: {"name", "write", "write_dot_files"?, "read", "read_home"?, "hide"?, "tmp_prefix"?, "terminal"?, "commands", "developer_tools"?, "net"}, commands "any" or {"only": [<path>]}.
What stays closed
Link's settings stay closed whatever the confinement says; hide
closes folders the same way, but for what write and read name in
them.
Dot files
At the top of each of write, a project's dot files that run outside
(.git, .envrc, .vscode, .idea) are read only, but in those
write_dot_files names.
Its net
A confinement's net is one mode holding only its own keys:
"closed", {"loopback": {"ports", "dir"}}, {"proxied": {"proxy", "ports"}}, {"reaching": {"reached": [{"dir", "port"}], "ports"}} or
{"host": {"ports", "localhost"}}.
loopback
Its dir a folder: it reaches its ports on this machine's loopback
and nothing else, without a proxy (on Linux, relayed through sockets in
that folder).
reaching
Linux: a network of its own whose only ways out are those proxies, each
at its port inside, through the sockets in its dir (lnk sandbox proxy-inside --also <dir>=<port>): one sandbox for several agents'
proxies.
reaching's folders
lnk sandbox wrap keeps a reached folder that's its user's with mode
2770 as it is, and its relay inside makes in.sock 0660, of the
folder's group.
A proxy
A proxied confinement's proxy (link_sandbox_client::Proxy) is
{"upstream", "dir", "serve"?, "hosts"?, "tcp"?, "listen"?, "per_minute"?, "routes"?, "caller"?, "secret"?, "lan"?, "no_internet"?, "hold"?}. On macOS the proxy listens on listen, else
binds port zero itself before starting its program.
Pointed at it
For a proxy on this machine's loopback (macOS, unwalled), with
pointed (a sandbox on macOS always), the wrap makes a new secret of
its own, which HTTPS_PROXY alone names
(http://lnk:<secret>@127.0.0.1:<port>) and the proxy reads there
(--pointed-by HTTPS_PROXY).
Proxy-Authorization
The proxy requires that secret as the Proxy-Authorization (Basic,
any user) of every CONNECT and plain http:// request, answering
407 without it.
hold
Linux: the command is the proxy's alone (lnk sandbox proxy --hold),
holding no program until stopped, and the environment is the program's
as it reaches the proxy at listen from a sandbox started apart. tcp
and ports to reach are refused, and a local model is reached on the
proxy, at local_model_url.
Port zero in Wrapped.env
For a loopback proxy with no fixed listen, generated proxy URLs in a
Wrapped.env use port zero as a launch placeholder. Run the returned
command with that environment. The proxy owns its listener before
filling the actual port in its child's generated and inherited run
URLs; those placeholder URLs are not addresses to call before launching
the command.
Routes
Each of a proxy's routes (link_sandbox_route::Route) is a way out
beside the network, mounted by the proxy and served by a bridge outside
the sandbox: what each is, the routes Link makes and their bridges are
on Routes.
Unwalled
unwalled (link_sandbox_client::Unwalled, made from a spec by
Unwalled::of) is {"name", "proxy", "pointed"}: no wall, argv run
as it is under its proxy (lnk sandbox proxy --listen) on this
machine's loopback at the proxy's listen, its dir, serve and
hold unread. Nothing holds the program to it.
Its listener
None, a port it binds before starting the program. A listener there its
caller holds from choosing the port, link_sandbox_client::HeldPort, is
handed to it open, its descriptor named by LNK_PROXY_LISTENER_FD, and
served as it is, never bound again. The proxy refuses one that isn't a
listening TCP socket on 127.0.0.1:<listen>, and its program gets
neither the listener nor the variable.
pointed
With pointed, the environment points it at the proxy
(HTTPS_PROXY, GIT_SSH_COMMAND), else only at Link's ways out on it
(LNK_TOOL_<NAME>, LNK_LOCAL_MODEL_<PORT>, LNK_SANDBOX_OPEN).
What it refuses
An upstream but direct is refused, and lnk sandbox check --policy
refuses it, having no wall to check.
A blank proxy
lnk sandbox proxy --blank --control <socket> --dir <dir> [--binder <uid>] [--agent-user <uid> --group <gid>] (--hold | -- <argv>)
(hidden, Linux): a proxy holding nothing of anyone's, for a machine
that packs agents, never with --listen or --dns. It listens on
<dir>/out.sock answering every call with a 503.
Its binding
On <socket> (0600, its user's connections only, and <uid>'s with
--binder) it waits for one line of JSON, link_sandbox_client::ProxyBind
({"name"?, "wire"?, "upstream", "host"?, "tcp"?, "serve"?, "reach"?, "desktop"?, "lan"?, "no_internet"?, "per_minute"?, "routes"?, "caller"?, "url_secret"?}): what lnk sandbox proxy takes, its routes
as wrap's, what each is given, their caller's key and the URL secret
inline, never in a file.
Its answer
link_sandbox_route::check_routes refuses routes that can't be mounted
together, and a bind with routes and no caller is refused. It answers
one line, {} or {"error"} (ProxyBound;
link_sandbox_client::bind_proxy sends it). Bound, it's the proxy those
arguments start; a second bind is refused, and the binding ends with the
process.
--agent-user and --group
With them (any lnk sandbox proxy --dir, blank or not), it runs as a
user other than its agent's: <dir> must be of group <gid>, mode
2770, or it doesn't start; its sockets there are made 0660 of that
group; and it takes in.sock only as a socket of <uid>'s.
lnk sandbox run
lnk sandbox run --yes --write <dir> --write-dot-files <dir> --internet-host <host> -- <argv> runs argv in a sandbox writing only
<dir> (its dot files too) and reaching only <host> on the internet,
and exits with its code.
--spec and --with-fd
lnk sandbox run --spec <file> --yes --with-fd <N> -- <argv> runs
argv in the spec's sandbox as run does, first reading
link_sandbox_client::RunWith as JSON from descriptor N to its end:
{"env", "secrets": [{"secret", "url", "placeholder", "git"?}], "serve", "write"}.
env
Variables the program gets: none run keeps or sets, nor LD_, DYLD_
or _PROXY ones. Its program and file and network grants still
determine what code it explicitly asks to run can do.
Refused in env
It refuses its own variables, and dynamic-loader, shell-startup and
interpreter-injection ones: BASH_ENV, ENV, NODE_OPTIONS,
NODE_PATH, PYTHONPATH, PYTHONHOME, PYTHONSTARTUP, PERL5OPT,
PERL5LIB, RUBYOPT, RUBYLIB, JAVA_TOOL_OPTIONS, _JAVA_OPTIONS,
JDK_JAVA_OPTIONS, GCONV_PATH and LOCPATH among them.
secrets
Secrets its proxy adds, each a Secret as wrap takes it, but for its
placeholder, which run makes for this run. The variable url names
holds secret_url, the one placeholder names that placeholder.
git
With git, for a secret in a header, git's own configuration is in the
variables too: GIT_CONFIG_COUNT with url.<secret_url>/.insteadOf https://<host>/ and http.<secret_url>/.extraHeader holding the header
with the placeholder, refused beside a GIT_CONFIG_ variable of the
caller's.
serve and write
Ports it serves on this machine's loopback too, and folders, in full, it writes beside its spec's, each refused as a spec's write would be (which). A proxy with no internet is made for secrets or ports when the spec opens none.
Handed, never in a file
Its proxy is handed its routes, the secrets' among them, on a pipe as it
starts (lnk sandbox proxy --routes-handed <variable>, read by
link_plugin::Secret::from_env), never in a file. The rest of what it
was given is removed when it ends, stopped by SIGTERM or SIGHUP too
(each passed on to what it runs).
Other commands
What lnk measure and lnk agent read of what a sandbox carried, its
state and its readiness.
lnk sandbox traffic --json
[{"pid", "sandbox", "up", "down"}]: what each running sandbox's proxy
(its process, pid) has carried since it started, every call it let
through, in bytes from the sandbox (up) and to it (down). Read by
lnk measure (an agent's network: its proxies' among its processes),
as this plugin's answer to its measure.traffic,
link_measure_contract::client::asked::Traffic.
Published counters
The proxy also publishes these typed records under sandbox/traffic/ in
the user's private settings, once per PID with its start marker. Measure
uses the shared reader directly when an agent advertises that source. The
proxy retains publication cadence and exit cleanup. The sandbox command and
the next proxy startup remove stale files, while observation stays read-only.
An agent's model tokens
Its model route's (lnk-model model bridge),
link_measure_contract::client::Tokens, {"requests", "unmetered", "input", "output", "cached", "written"}: the requests for a model's
answer it carried that were answered, and of them those whose answer
gave no usage.
The tokens
The rest's usage says the prompt's tokens, cached or not (Anthropic's three parts summed); the answer's; and of the prompt, those read from the provider's cache, and those written to it.
lnk sandbox paused --json
link_sandbox_client::Wires, {"stopped", "paused": [{"sandbox", "why"}]}: whether every wire is stopped, and the sandboxes
(<agent>-<harness>, a box user's name, run-<pid>) whose wire is
paused, and why. link_sandbox_client::wires reads the same owner-defined
flags afresh without a child command, as lnk agent status does. Missing
state is open, unreadable state is unavailable, and reading creates no
directory. This observation never replaces the proxy's enforcement checks.
lnk sandbox on-stop <name> [-- <args>]
Hidden: lnk <args> runs when lnk sandbox stop closes the wires, for
<name>, kept in ~/.lnk/sandbox/on-stop/<name>.json; with no args,
nothing does anymore. Never lnk sandbox itself.
Who sets it
lnk agent start (agent-<agent>: agent stop -a <agent>, so the
kill switch stops an agent with [network] mode = "host" too, which has
no wire), and it's forgotten when the agent leaves.
lnk sandbox ready [--yes]
Exit status 0 once the sandbox can run here, checked afresh (what was
kept of this machine forgotten); in a terminal, offers what it needs
(Ubuntu's AppArmor profile), talking on stdout. With --yes, asks
nothing and adds it where sudo -n works (installing bubblewrap with
apt if it's missing).
Who reads it
lnk agent start, lnk agent harness check, and lnk agent move and
lnk agent create --on readying a box (--yes, through lnk box run).
lnk sandbox tool list --json
One JSON object per tool that runs outside: {"name", "argv", "ask", "allow"} (ask: all or none; allow: the calls that don't ask). A
proxied confinement's proxy's tools names some of them; its program
finds each at $LNK_TOOL_<NAME>. Read by lnk agent allow --tool, and
lnk agent start (a tool removed since is left out).
lnk sandbox check --policy
Reads {"confinement"} on stdin (link_sandbox_client::Wrap), as
wrap takes it (a proxied one too), the flag keeping its name, runs the
probes inside it, prints a line per probe (blocked, allowed,
failed, open, missed, leaked), and exits 1 when something leaked. Read by
lnk agent harness check.