Tools and Asking
The tools that run outside every sandbox, and how the sandbox asks you before one's call or a page goes through (Security).
Tools that run outside
lnk sandbox tool add, then --tool (lnk agent allow --tool): an MCP
server you add runs outside every sandbox, with your logins. That's its
point, and what it can do is yours to choose.
Its definition
~/.lnk/sandbox/tools/<name>.json (600), read by the proxies outside,
never through a link: its command, a snapshot of a few variables
of your environment, and --env, which may hold a token. Every value of
its environment is held as a secret where Link reads it (never printed,
zeroed when dropped), written as it is only in that file, and given to
the tool's server alone, in its environment.
What it runs with
Your PATH, HOME, user, locale, TMPDIR, SSH agent and desktop
session (DISPLAY, WAYLAND_DISPLAY, D-Bus) as they were when added,
and no more. A tool saved with any other field is refused.
Where it runs
It runs in /, never the folder it was added in, which a sandbox may
write: an argument written from there (., ./, ../) is refused,
and lnk sandbox run refuses a relative path from /. Its files, with
what their interpreter loads beside them (node_modules), go in a
folder no sandbox writes, which Link can't know for you.
Reaching it
A sandbox reaches only the tools its spec names, at
/_link/s/<secret>/mcp/<name> on its proxy ($LNK_TOOL_<NAME>), the
secret its proxy's, never logged: one request per connection, a body of
8 MB at most, one JSON-RPC message, no batches. Its proxy's mcp route
hands each to the sandbox plugin's bridge (lnk-sandbox sandbox bridge), outside every sandbox, which runs the tools and keeps each
sandbox's sessions apart.
Sessions
Each session a client starts is a server of its own; a sandbox keeps at most 8, a session with no call for 5 minutes ends, its server with it, and at most two start at once. The rest wait their turn, and one still waiting after 60 s gets a 429. A session however slow to start is kept for its next call.
Their settings
tool_sessions, tool_idle and tool_starts, read from Link's
settings the bridge is started with, never from the home a proxy runs
with, which for an agent is its harness's.
A client that closes
A client that closes its connection before its session has started gets none: the server is stopped and holds no place. One that closes it before a call is answered lets the call go, and its session stays. On Linux, one that only shuts its writing after its request (a half-close) hasn't closed it, and is answered; on macOS, whose proxy is reached over TCP, where the two look alike, that counts as closing it.
A tool added again
A tool added again with another command, folder or --env (a token
replaced) ends the sessions started before: their next request is
answered as gone, and the client's next session runs as the tool is
now. Removing a tool ends its sessions: the next call is refused.
What passes between them
The client's own capabilities are removed from initialize, and what
the server asks of the client is refused outside, so no server reaches
the sandbox's folders or a model through it. Its notifications stay
outside. Listing its tools, prompts and resources passes as it is.
Calls that ask
Each tools/call, resources/read and prompts/get goes through only
if the tool's rules allow it (--allow with the call's name, or with
resources/read or prompts/get; or --ask none). A server's hints,
such as MCP's readOnlyHint or destructiveHint, are shown and never
skip asking. Otherwise the bridge asks you.
A message without an id
It reaches the server only if it's one of MCP's own notifications
(initialized, cancelled, progress, roots/list_changed): a
tools/call sent as one, which some servers would run without an id,
is refused.
Logged
Each request is a line in the wire's log (mcp, the tool and a call's
name, never its arguments or answer), and refused calls count toward a
pause.
A tool at a URL
--url is a stdio server of Link's own, lnk-sandbox sandbox mcp-remote, run outside like any other. It reaches https://, or
http:// on this machine only, and sends each message as streamable
HTTP's POST with the server's session. Its headers, a token among them,
are in its definition's environment (LNK_MCP_HEADER_<n>), never in a
sandbox or on a command line.
A shared tool
--shared runs one server for every session of every sandbox naming
it, under a hub, lnk-sandbox sandbox mcp-shared, which the first
bridge to need it starts outside every sandbox. The bridge still asks
its tool's rules before a call reaches the hub. The sessions share one
server, and whatever it keeps between calls, so share only a tool meant
for everyone who names it.
Its socket
The hub listens on a Unix socket in Link's settings
(sandbox/tools/<name>-<key>.sock, 600), which no sandbox reaches,
<key> signed with a secret this machine makes once and keeps
(sandbox/tools/.key, 600) over what the tool runs as, its tokens among
it, so the name, which every user of the machine can list, tests no
guess of a token.
Each session's answers
It passes each session's messages on with ids of its own, so an answer goes only to the session that asked. Each session's answers are written on a thread of its own, so one that stops reading holds up no other, and one with 64 answers unread is ended.
What the hub says
What the hub says to the server (a ping's answer) is written apart from the thread that reads the server, so a server writing while it reads nothing never stalls it.
One hub
A lock beside it keeps it to one hub; a tool defined again gets a hub of its own, and the old one stops once idle. A hub that stops idle kills its server, and what the server started, never only closing its input.
Its folder
The bridge hands the hub Link's settings folder (--dir), which it
takes only if it's a folder of the user's alone, not a link, so a hub
started for an agent's tools listens, reads the definition and keeps
shared_idle there, never in the harness's home.
Its unit
Where systemd runs Link's services, the hub runs in a user unit of its
own (lnk-tool-<name>-<key>), so no agent's stop ends it or counts it
as its own; elsewhere, in a process group of its own.
What the unit is handed
HOME, PATH, LANG, XDG_CONFIG_HOME and Link's LNK_* variables
by name, never on systemd-run's command line, and none whose name
holds TOKEN, SECRET, KEY or PASS; what it's handed is in the
unit's environment, which your user and root can read (systemctl --user show). Memory's service unit is handed the same.
Its log
Beside its socket, it keeps at most 1 MiB of its server's output while
it runs, and a log past that when a hub starts is kept once as
<name>-<key>.log.1.
Asking you
The sandbox's bridge asks for each route that asks you (a tool's call, a
page to open), one question at a time, and allows that call once. The
first answer counts, from whichever way it was asked, and no answer in
time is no. Unanswered, the refusal names the --allow that lets it
through. A question waits as long as the agent that runs the sandbox
says ([connections] ask_wait, ten minutes in Link's spec), and for any
other sandbox two minutes (ask_wait, at most a day, from Link's
settings). It's asked every way there is at once:
- A dialog.
- Its proxy's controlling terminal.
- A waiting question.
- For an agent's sandbox, the owner's chats.
A dialog
macOS's osascript, passed the text as an argument, never as script; or
zenity --no-markup or kdialog with the text escaped. They get the
desktop session's variables that wrap passes the proxy (--desktop),
and its route the bridge, never the sandbox. Answered another way
first, the dialog's program is stopped, which closes it, and a click
after then is no answer.
Found where
They, a notification and your browser are found in the system's folders
(/usr/bin, /bin, /usr/local/bin), never on the program's PATH,
which can name a folder the sandbox writes.
Its proxy's terminal
With a random code of four letters to type back: the proxy answers the bridge's waiting question there. Anything else refuses it.
A waiting question
In the owner's store of questions, ~/.lnk/questions/ (600, a random
id), in a notification where no dialog shows and atop lnk sandbox log,
answered by lnk sandbox answer. The first answer is final, linked in
place. The owner questions
page says how the store keeps them.
The owner's chats
For a sandbox an agent's harness runs in, the agent's bridge sends the
question to its owner's direct chats, with its id as the code to reply
with: the code alone allows it, no and the code refuses it. The
agent's bridge
says where it asks.
Only while someone can answer
A tool's call that nobody but a dialog would see is asked on a dialog alone, and refused at once with none. It waits in the store while a proxy answers on its terminal, or the agent's chats answer, each holding a shared lock in the store while it does.
What a sandbox can't do to it
Nothing in a sandbox draws on the screen or reaches Link's settings, so it can neither fake a dialog nor answer one or a waiting question. It never sees a question's code either, so it can't answer one on a chat.
On a terminal
With no desktop or over SSH, the program shares it: it can type (macOS) but not read what's written, so it can't answer, but it can write over the question's text, so what the question names can be faked there. The code you type still allows only the call that asked.
The question as it is
A dialog, a chat or lnk sandbox asks shows the question as it is, without
control characters or those that hide text or turn it around (bidi,
zero-width). Each line is cut at 600 characters, and a question cut
short says so: allowing it allows what you can't see.
Namespace servers
A host can expose existing local MCP servers as one namespace using a
private Namespace definition. It chooses every action's targets; callers
supply arguments and a kind selector, never a server name or address.
The namespace derives its discovery schemas from the original servers.
Selection
An action with several targets requires kind, unless the host marks it
as a broadcast read. A broadcast joins each target's result under its
kind. A mutation selects one target, retaining that server's own tool name.
Such an action is offered as one object schema, since models' APIs refuse
a union at a schema's top: every target's properties, kind naming each
target, and required only what every target requires. Each call is still
checked against its chosen target's own schema.
Approval and lifetime
Every call reloads its provider definition and checks its current approval policy. A removed provider is refused; a changed launch requires a fresh session. Closing the namespace ends its owned sessions. Its host definition sets a timeout, and request and answer sizes keep the tool bridge's cap.
Argument checks
A namespace checks call arguments against the discovered provider schema, rejecting unlisted fields before forwarding or approval.