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:

  1. A dialog.
  2. Its proxy's controlling terminal.
  3. A waiting question.
  4. 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.