Security
What each part of an agent is kept from reaching, starting with the harness that runs it.
Models supplied by a host
A hosting account answering managed_models receives no model or
fallback setting in a move's switch. The source keeps a private
checkpoint of model metadata under ~/.lnk/moved/<id>.models. The keys
stay in the secret keeper without a running-agent grant. Removing a key
with lnk secret delete also removes that retained value. Link restores
the remaining settings only for the same agent ID returning to this
machine, then removes the checkpoint after the agent saves its credentials.
Harnesses
What a harness is trusted with, and what Link Harness, an installer and a harness from outside Link each can reach.
Your AI, the harness
What's protected: your machine from the harness you run (Link Harness, one of the others that come with Link, or one from outside Link). It acts for you with a model key and, once connected, a connection's bot, and anything it reads may try to steer it. Where its files live: README.
What it can't protect
What the sandbox can't protect is what you give the harness: the files
folder, its own state (memory, conversations), the model key, which it
can spend, and the bot. It has the network, so it can send what it can
read anywhere. A harness's built-in tools (a shell) run inside the same
sandbox; tools added with lnk sandbox tool add run outside it.
Its gaps
Gaps, in short: the sandbox's known gaps, secrets in files and unpinned installers. See Known limitations. What every sandbox guarantees, a harness's included, is the sandbox's security page; this one is what an agent adds.
Link Harness
What Link's own harness runs, holds and reaches.
- It runs no code itself: it reads the conversation, calls the model,
makes the model's tool calls and writes the answer. A command the
model runs goes to Link's
runtool, outside it, and runs in a sandbox of its own (Run and file). It runs in the same sandbox as every harness, with the same grants. - Its tools are the MCP servers Link runs outside the sandbox for its
agent (
$LNK_MEMORY,$LNK_TOOL_<NAME>). It reaches them only through its proxy, which asks you before each call you didn't allow by name and logs every call (lnk sandbox log). It calls only the tools its environment names, never a URL a model or a tool gives it. - A tool's answer goes to the model as the tool's words, never as yours or as instructions. A tool that returns text written by someone else, like a web page or a shared file, can still try to steer the model. What that steering can do is bounded by the tools you allowed without asking.
- A long conversation's summary, kept in its log and sent at its
window's head, is the model's own words for what left the window. What
a message from others or a tool's answer said can carry into it, no
longer quoted or marked as theirs. While the model saves to its notes
before it summarizes, it may call
memory__rememberonly; any other call is refused. - A turn makes at most eight rounds of calls, at most 16 calls each (the model's others are dropped), and keeps at most 32 KiB of each answer. A turn opens a session with a tool only to call it, and ends it with the turn, so what one conversation left in a tool (a browser's pages, a shell's folder) isn't there for another's.
- Memory's alone is kept for the turns after, one for all the agent's conversations, since memory is the agent's own in each of them: Link Harness ends it once no turn has used it for 10 minutes, or an hour after it started however busy, and at once if a call to it fails other than with the tool's own error. Link Harness keeps, in memory, only that and the list of each tool's calls.
- A tool that fails to start, or takes over 30 s to, is left out of the turn and, if it never listed its tools, of every turn for 5 minutes. One that listed them before is offered what it listed, and so is one that takes over 30 s to list them again. One that lists more than 16 pages, or a page twice, is offered what it listed by then.
- What the tools offer goes into every prompt, so it has a size: at
most 128 calls from each tool, 2 KiB of each call's description (the
rest cut), a schema of at most 16 KiB, and 128 KiB for every call
together. A call past a limit is left out, and said so in the agent's
log (
lnk agent logs). - No tool can take another's call's name. A tool added with
--in its name, or ending with-, isn't offered. A call whose name is longer than a provider takes is left out rather than cut, and of two calls whose names come out the same, only the first is offered. - It holds no keys by default: behind its proxy it gets a placeholder,
and the proxy adds the model's key outside the sandbox. Without the
sandbox, or with
--network-mode host, itsconfig.jsonholds the key, mode 600, and its fallback's key too when it has one. - With a fallback (
--fallback), a turn's whole conversation goes to that hosted provider when the model on this computer is off, sends nothing back for 60 seconds, or can't hold the prompt. Link Harness then skips it for 3 minutes, its turns going to the provider too. A hosted model's fallback, for a busy provider, is that provider's. - It speaks no connection, so it never holds a connection's token or a
linked device's keys. Link's connection plugin (
lnk-<connection> bridge) carries the messages, outside the sandbox, beside the agent. It passes on the allowed users' direct messages, and their messages in a group that mention the bot, only. - Every connection is a connection the plugin opens, so nothing listens
for a connection on your machine: Slack in Socket Mode, Discord on its
Gateway,
signal-cliover its stdin and stdout, never a local port. - An answer can't mention anyone: on Slack
&,<and>are escaped, so no@connectionor disguised link; on Discord every message is sent allowing no mentions, so no@everyone. - Signal links as a device into
~/.lnk/agents/<agent>/connections/<connection>(700), which no harness reaches: the device's keys, andsignal-cliitself.lnk agent disconnectdeletes the folder; unlink the device on the phone too. - Its endpoint listens on 127.0.0.1 only, at a port of its agent's
block, where other users of this machine can reach it too. It refuses
any request without the key Link makes at each start, which it gets
in
config.jsonand the bridge inbridge.json, both mode 600, compared in constant time. While itsconfig.jsonnames no key, it refuses every request (503), never answering one unchecked. - It proves it holds that key before the bridge sends it, on the same connection: it answers the bridge's random challenge with an HMAC of it made with the key, never the key itself. A program that took its port while it started or restarted can't, and gets neither the key nor your message.
- A thread's name is letters, digits,
-,_,., one:and at most one~, never a path, so a message can't write outside the agent'schats/. Each agent's conversations are in its own folder (~/Link/Agents/<agent>/Agent/chats); a test shows one agent can't read or write another's. - It keeps each message as a line of one log per conversation,
events.jsonl, only ever added to, readable only by you (600, folders 700): the text, times, model, tokens and cost. Writers take turns on it with a lock, a number already written is refused, and a log that's a link, a pipe, or holds a line past 16 MB is refused, never followed or read. - A chat kept by an older Link Harness, a file per message, is read the same way, and goes on in the log. They are in your files, so a backup, a move and the next harness have them. Each of the agent's answers holds where the model's window starts: a message's number and time, and token counts. One that doesn't fit the messages it names is made anew from them.
- A chat kept compact (
[storage] conversations = "compact") is the same lines in zstd frames,events.zst, and those since inevents.jsonl, under the same lock and the same refusals; a frame is read only whole, past 64 MiB never. - A chat a store driver keeps (
sqlite) is in the driver's file,chats/events.sqlite(600), which it makes never through a link; the driver is a program of Link's or one you installed (lnk plugin add), run inside Link Harness's sandbox, and stopped past 30 seconds. - One off the machine (
gcs) keeps nothing in it: each event is an object of its own in a bucket, written only if it isn't there, refused for an agent without its owner and ID, and its token goes only tohttps://, orhttp://on the machine. - Its readable copy,
conversation.md, which memory searches, puts a\before any line of a message that would start a heading or HTML, so an answer can't write a turn there as yours. A message from others (a teammate, a schedule, a subagent, a background command) is headed "Others" there, not "You", says who sent it, and has each line quoted, as the model reads it. - With no tools offered in a turn, an earlier tool's answer is written into the text the model reads, saying which tool answered, not you, with each line quoted.
- The files its prompt is made of (
instructions/instruction.mdandmemory/notes.md, or those its spec names) are read at each message, at most 64 KiB each, only inside the agent's files: a path with..is refused, and one through a link, or that's a folder or a pipe, is left out, so the agent's turns go on without it. - Its standing instructions are headed in its prompt with who wrote
them: you, who alone write
instructions/by default, or, where its sandbox file lets the agent write it, you or the agent. - Its skills (
skills/<name>/SKILL.md) are read the same way at each message, the head of each at most 64 KiB: a skill that's a link, or whoseSKILL.mdis, is left out, and so is askills/that's a link. A skill's name and description go into the prompt as instructions, and the model reads the rest as it would your own, so add only skills you trust. - A skill's scripts run only as any command does, through
run, for a program the agent's sandbox file allows. - It reads and writes a conversation only in its own folder, made 700,
and refuses one that is a link. It never follows a link or waits on a
pipe there, at a file of its prompt or at its
config.json, and refuses a message's file over 16 MiB and aconfig.jsonover 1 MiB. - It opens each folder on the way from the one before, never through a
link: the agent's own folder,
chats/and a conversation's, and each folder to a file of its prompt or a skill. - It holds a conversation's folder open and reads, writes, renames and
removes in it only through that handle, so a folder swapped for a link
meanwhile (by a
runcall) is refused, or changes nothing it reads or writes. A test swaps each for a link. - Every call to a tool carries the conversation's name, its hops and
its tools in
_meta(link.local/thread,link.local/hops,link.local/tools), as the harness contract has it: a tool from outside Link learns which chat a call is from. - Each request names its agent (
Link-Agent). It answers only the agent it runs, with that agent's key: a request naming another is refused before its key is checked or the model sees it, and another agent's key proves nothing. A test shows one process answering two agents keeps each one's key, instructions and conversations its own. - Everything of an agent's it keeps is keyed by the agent's ID and
owner, never its name. A request naming by name alone an agent two
owners share is refused (409); one whose
Link-Agent-Idand name disagree, too (404). An ID two owners' agents share (the same backup restored by both) is the agent whose key the request carries. - A test runs one process for two owners'
mains, with the same ID, instructions, model and tool server: each answers from its own conversation, settings and tool sessions only. - A move carries the agent's owner. A machine signed in as another account refuses the agent before anything of it is put in place.
- A move's stream is another machine's, or a cloud's, writing: every file in it comes with its SHA-256, which the receiver checks against what it wrote, refusing the stream when one differs or has none, and one cut short (its end entry missing) is refused too.
- Only files, folders and links come, each a plain path under the folder its phase allows, unpacked into staging out of every agent's reach, a link as a link, never followed by what comes after it.
- What a cloud that hosts agents answers (agents' names and IDs, a move's ID, hashes, a refusal's reason and time, held messages) is checked before it's used or shown, its text cleaned of escape sequences, as is what its adapter says on stderr.
- A held message is given to the agent only from a sender its connection
is paired with, on a connection Link carries for it, and its answer
goes only where that connection's
sendsends: the owner's own chat, or a group they talk to the agent in. - Held messages are kept until given in
held.jsonlwith the agent's settings (0600), and, until where it went has them, on the computer running the move (~/.lnk/held/, 0600). - From a move's pause until the agent answers where it went, Ctrl-C, a
closed terminal or a stop ends the move as a failure, never
lnkmidway, so the agent is never left paused with an unstarted copy elsewhere. One move of an agent runs at a time (a lock in~/.lnk/moving/). Staging is seeded with copies of what the agent left, never hard links, so nothing written to one reaches the other. - A pull from a cloud that hosts agents puts only files in Home: a link that comes is left out.
- A switch that fails before the other end answers leaves nothing of the agent there: its staging is removed, and the agent answers where it was. A copy that never switches stays in staging 7 days at most.
Installing a harness
What a harness's install runs, and where it comes from.
- Link Harness installs nothing from a third party: each agent runs its adapter's own program, as installed, which its sandbox reads (that file, not the folder it's in) and can't change.
- Any other harness, OpenClaw, Hermes and DeepSeek Harness, which Link ships, or one from outside Link, is installed by its adapter, which runs its harness's own installer pinned (the script from the project's repository at a release's commit, run only if its SHA-256 is the one in the adapter's source), or downloads what it needs checked against a SHA-256 of its own.
- What that installer fetches itself may not all be pinned: the adapter's page says (OpenClaw, Hermes, DeepSeek Harness).
- It runs over HTTPS, in the home and the sandbox: it can download anything, but write only the home, reaching only the hosts the adapter lists.
- A new harness release comes with a release of its adapter.
A harness from outside Link
Where an outside harness's adapter comes from, and what it runs as.
- Its adapter,
lnk-<name>, comes from its own repository's releases, signed by that repository's key, whichlnkpinned when you added it (Plugins Security). - The adapter itself runs as you:
lnk-agentruns itscontract,connections,hosts,endpoint,needs,traces,writtenandstateoutside the sandbox. Itsinstall,configure,link,commandandhealth, and the harness, run in the sandbox like Link Harness's. - What it asks for is checked as for any harness: how it says to run its harness can't widen the sandbox, its ports must be in its agent's block, and it waits at most ten minutes for its harness to answer.
One home per harness
Where each harness, its installer and its configuration live.
- Each harness, its installer and its configuration steps (
config set, a harness's own repairs) run inside the sandbox withHOMEset to the harness's own home,~/Link/Agents/<agent>/Harnesses/<name>(only yours, mode 700). None of them can spread across the machine. lnk agent harness delete --harness <harness>deletes the home and what the adapter names outside it. The adapter'slnk-<name> tracesruns outside the home and the sandbox, and only prints a list.- What merely carries the harness's name as a whole word is deleted
only after asking, never with
--yes.
A harness's own services
A harness's own background service (a launchd agent or systemd unit it
installs for itself) would run outside the sandbox, so lnk agent start
stops it and keeps its plist in ~/.lnk/kept-off, at its path in your
home folder, where you can move it back from. The names come from
the adapter's command, which runs in the harness's home, so none may
be Link's own: every name starting link. or lnk-, such as an agent's
service or the exit's, is refused.
Sandbox
The sandbox every harness runs in, how it keeps agents apart, what you grant, and what no sandbox means.
One sandbox, the sandbox plugin's
Which sandbox runs each harness, and how it's made.
- Every harness runs in the sandbox plugin's sandbox (
lnk-sandbox, installed with them), on by default. The harness plugin askslnk sandbox wrapfor each step it runs (the installer,configure, the harness), with a confinement made from the grants below. - How the sandbox is built on each system, and what it keeps closed whatever a spec says (Link's settings among them), is the sandbox's security.
- A harness's spec writes its home and the files folder, the agent's
Home (the harness itself only, not its installer); one that keeps to
Link's layout writes only its conversations and the folders its
agent's sandbox file names, of Home and of the agent's own folder, and
reads the rest of Home, its instructions and skills, and on macOS
/tmp/<name>*, where a harness may keep locks. - It hides
~/Link/Agentsbut for its own agent's folders, and~/Link/History(Agents kept apart). - Missing sandbox permissions load closed independently of recreation. Both
Config/files are recreated when absent, without restoring grants. - The localhost ports it may connect to are its own, the ones its adapter names (its gateway or API, and the ports it derives from it), and its local model's.
Agents kept apart
A machine runs any number of agents, all as your user. The sandbox keeps each out of the others:
- Every harness's spec hides
~/Link/Agentsexcept its own agent's folders, even with--read '~', and~/Link/History, the files and keys of agents that left, and~/Link/Teams(and each team's folder kept elsewhere) except what the agent is granted of its own teams' (below). - What an adapter says to run can't name a folder in there, and a files folder
can't be another agent's, nor one in its own agent's folder outside
its
Home(where Link keeps its scheduler's jobs and its memory's index, out of the harness's reach). lnk agent harness checktries reading what's hidden.- Each agent but
maingets its own block of ports (LNK_PORT), as many as its harness takes and one more (200 for a harness that doesn't say). A harness whose adapter names a port outside it is refused. On macOS, without[internet], a harness may listen only on its own ports. - An agent doesn't start while another program listens on one of its harness's ports on this machine's loopback: that program would be taken for part of the agent, such as its connection's daemon. On macOS that's every port of the harness, behind its proxy too, since its connection daemons listen on the machine's loopback there.
- On Linux behind its proxy, it's the ports the proxy serves: the others are in the sandbox's own network.
- A program can still take one of those ports between that check and the
harness's bind, or while its service restarts. So a harness whose
adapter says
proof, as Link Harness's does, proves it holds its endpoint's key before Link's connections send it anything: each message goes on a connection of its own, which first answers a random challenge with an HMAC of it made with the key. - Whatever else answers gets neither the key nor the message, its owner
is told it couldn't answer, and the bridge's log says why; a
connection closed unanswered, as while its harness restarts, is tried
again for 5 seconds. An adapter without
proofgets the key and the message on whatever answers its port. - The proof shows only that the challenge reached a harness holding the key: the harness answers it before any other check. A program on the port could relay the challenge to the harness, were the harness also reachable at another address.
- Link Harness and DeepSeek Harness listen only on
127.0.0.1, and so do the ports a sandbox's proxy serves, so a program holding the port leaves the harness nowhere else to be asked. - The bridge that carries a connection's messages runs outside the sandbox, and reads an answer, streamed or whole, up to 16 MiB, and a line of a stream up to 16 MiB; past that, the answer fails.
What stays shared:
- Other agents' localhost ports, where the sandbox lets localhost
through. A harness's gateway asks for its token. On Linux behind its
proxy, only that gateway is served on this machine's loopback. On
macOS, and on Linux with
--network-mode hostor without the sandbox, the harness's other ports are there too. - A connection daemon that asks for nothing there lets another program or user of this machine send as the agent and read its messages, so each harness's adapter keeps its daemons off them where its harness can: on a unix socket in the harness's home (700), or behind a relay that asks for a secret the harness is given. What each adapter does, and what it can't, is on its own security page.
- On macOS,
/tmp/<harness>*between agents running the same harness; a harness may keep its locks there. Linux gives each sandbox its own/tmp.
Each agent needs its own bot or app:
- Telegram:
lnk agent connect telegramrefuses a bot another agent here has, by the bot's id (the token's public part), one that an agent running on a box has, or one Telegram says another program reads. - Slack:
connect slackrefuses an app another agent here has, by its bot's member id or either token, or one that an agent running on a box has. - Discord: the same, by the bot's user id or its token.
One harness for an owner's agents
Off by default. With shared_harness = true in ~/.lnk/machine.toml
(Linux), one Link Harness serves all of an owner's agents on the
machine, in one sandbox:
- Each agent keeps its own proxy, with its own model key, hosts,
endpoints, tools, calls and log. Its service runs that proxy alone
(
lnk sandbox proxy --hold), and the shared sandbox reaches each one at a port of its own, inside a network of its own with no other way out. A local model is reached through each agent's proxy, as on a Mac. - The shared sandbox holds every one of those agents' folders together.
- In the shared sandbox, which agent's conversations, instructions and settings a request touches is held by Link Harness, not by a sandbox each: a request is answered as the agent on whose port it came, or the one it names with that agent's key, and a test shows one agent's key opens no other's, and no agent's words reach another's folders or prompts. Link Harness runs no code of its own:
runandfilestill run each call in a sandbox of the agent's own.- Only an agent whose harness can share (Link Harness), sandboxed behind
its proxy, with no endpoint of its own (
tcp) and no grant past[internet], shares; any other keeps its own sandbox. Two owners' agents never share one. - The shared harness starts with the first agent and starts again, without the one that left, when an agent stops, its proxy ends, or a new one joins. It starts again once the turns it's answering finish, waiting 30 seconds at most. A turn still running then is cut short, and finished when the harness recovers it, as after any crash.
- Stopping to start again, the shared harness takes no new connection, and waits too for each connection it has to close: a message whose connection opened just before the stop is answered, never cut off.
- With no agent's proxy running for 20 seconds, it ends, forgetting the agents that left without a word.
- Two of an owner's agents starting at once configure their harness and
join the shared one in turn, never together: each holds
configure.lockin the shared harness's folder from configuring until it has joined.
Permissions
Grants open the sandbox further, in two layers, each a file of Link's settings (600, where no sandbox reaches) holding a sandbox's spec, and a harness runs with both joined:
- The agent's, what you grant it:
lnk agent allow|denywith a spec's flags (--internet-host,--read,--network-mode, ...), or a file withlnk agent start --sandbox, kept in~/Link/Agents/<agent>/Config/sandbox.toml, for any harness and no other agent. A deny a harness's grant still holds fails, naming it. Never no sandbox, the agent's own setting (below). - A harness's, only what it said it needs (
lnk-<harness> needs) and you allowed: saved in~/.lnk/harnesses/<harness>.toml, held only while that harness runs, never given to another, and the same for every agent here that runs it.lnk agent harness allowanddenychange one it declares, and nothing else: a harness only ever holds what it asked for. Never no sandbox: unsandboxedis refused, since no sandbox at all is never a grant.
lnk agent show --permissions shows both, each marked whose, and its route.
-
Asked, not assumed. A harness says what it needs beyond the defaults, and nothing of it is granted until you answer yes, in a terminal, by
lnk agent harness allow, or by--yes. What the agent has of its own asks nothing. Switching an agent to a harness whose needs open more than its old harness's is asked the same way, each line with what it opens; a no keeps it on the harness it had. -
The question names every agent here it holds for. No changes nothing. The answer is kept with the needs it answered, so a later start asks only for a new one; a need you took back isn't asked again until
lnk agent harness reset. -
A need that isn't a sandbox's spec Link takes, from an adapter older than this Link (
permissions, its contract's version 1) or with a key Link doesn't know, is refused, and the harness doesn't start. -
A risk a harness names itself (a port of its own with no credential) is asked the same way; one it can run without is off until a yes in a terminal, never
--yes, and the harness is told which it may open. -
It's told too whether it runs behind its proxy, which on Linux keeps its other ports off the machine's loopback, so it names such a risk wherever they're there: on a Mac, and anywhere without its proxy (no sandbox, or
[network] mode = "host"). -
On a move to a box, the question is asked on this computer from the box's answer: a need there that isn't a spec or a risk's name Link takes, or an agent's name that isn't one, is refused before anything is shown.
-
Always shown. The question says plainly when the programs it may run change; each start beyond the defaults prints what, as
statusdoes. -
needsgets no keys. It runs outside the sandbox, so its settings come with the model's key and the connections' tokens left empty, and its environment holds onlyPATH,HOME,LANGandTMPDIR. -
Removed with it.
lnk agent harness delete --harness <harness>deletes the harness's grants and answers, unless another agent here runs it. -
Moved with the agent. A move asks on the machine running it. Leaving it, it asks before anything moves, and of the harness's grants it gives, only what the harness asks for on the other machine (its
needsthere) is added to the harness's there: not the rest of this machine's file. -
Going anywhere, it asks before the agent starts where it goes, and the other machine grants only what its harness asks for there (
lnk agent needs --allow). -
It lands in its sandbox, whatever it ran with where it was: no sandbox is consent for one machine, so the move of an agent whose harness needs none is refused there. Refused, or failing to start there, its copy there is removed again and it starts where it was, so two never answer one bot.
-
The agent's own grants go with it: whole when it leaves the machine running the move, and kept as they are when it comes back to one that has them.
-
Otherwise they go without
--read '~',--network-mode host,--lan-host,--port,--browser-host, its endpoints (--tcp), its tools that run outside (--tool), its teams' shared folders ([teams.<team>]) or a program of its[commands]named by a path or*, each left out and said as thelnk agent allowflags that give it back. -
A stream never writes a harness's grants.
-
Each agent's own tools are its own. Its memory, scheduler, run and file are tools Link defines for it alone (
memory-<agent id>,scheduler-<agent id>,run-<agent id>,file-<agent id>) and hands only to its harness: no agent's grants name one, whether fromallow --tool,--sandbox-fileor a move. -
A namespace's name is Link's. No grant takes one (
task,tool,file,agent): its harness is handed Link's tools under them. -
[internet](on, any host): the internet, through its filtering proxy, for hosted models and connections. -
Its installer and setup steps go through a proxy of their own, which leaves from this machine, and the installer and
configurereach only the hosts its adapter lists (lnk-<harness> hosts: typically its installer, GitHub and a package registry). Either way, macOS's certificate checks (trustd), which TLS through the Security framework needs, as Python'struststoredoes. -
Without
[internet]: its proxy all the same, refusing every host, which serves the harness's own port, carries Link's ways in and keeps a local model to inference; only[network] mode = "host"goes without one. -
[commands](Link's list): what every program of the agent may start, its harness's as itsrun's: past the one Link starts (and its#!interpreter), only those listed; a harness that runs more asks in its needs.developer_tools(macOS, on): read-only Xcode tools, so/usr/bin/gitruns throughxcrun. -
[browser](off): opening anhttps://page in your browser on its hosts, through its proxy, a login's for example, after asking you for each page. The page opens with your logins, so a URL can carry data out: allow only a page you asked for. No LaunchServices, and nothing without its proxy. -
[files] read = ["~"](off): reading, not writing, the whole home folder. -
[localhost] ports(none): each port named, so a server on this machine. Every port,"*", only with[network] mode = "host": behind its proxy every port would be other agents' proxies too. -
[lan] hosts(off): machines of your network, through its proxy. -
[network] mode = "host"(off): the network as it is, in place of its filtering proxy: this machine's own addresses and its local network too, with the known gaps. An exit ignores it.
Link's tools for the agent (its memory and scheduler, run and file)
and the wire's log are on its proxy, so a sandboxed agent without one,
with [network] mode = "host" or without [internet], has neither.
Hosts
No grant opens the Keychain: no sandbox reaches securityd. Hosts (lnk agent allow --internet-host <host> or
lnk agent deny --internet-host <host>, the agent's,
in its sandbox file) are the only hosts its proxy lets it reach, on
every route (lnk agent route too). With "*", any; without
[internet], none; with [network] mode = "host", the list does
nothing.
Calls
Calls (lnk agent allow --per-minute <n>) are the most its proxy
takes within a minute, of every kind; more get a 429. The limit is the
agent's own: a deny can't lift it, and a harness's needs and grants hold
none, so none raises it.
Installs
Installs don't use these: the installer and setup steps run behind a
proxy of their own, each step with its own sockets, direct from this
machine even with an exit, logged as <agent>-<harness>-setup.
Their hosts
The installer and configure reach only the hosts the adapter lists
(lnk-<harness> hosts); an adapter that lists none lets them reach any
host on the internet, never this machine or its network. Its other
steps, such as link, which runs what the harness installed in its home
(signal-cli), reach only what the harness may reach when it runs: your
hosts and endpoints, any host without them.
Nothing it writes can widen the sandbox
Where the files that set the sandbox live, out of every harness's reach.
- The rules files live in Link's settings folder, where no harness can edit them.
- The files folder can't hold the harnesses' homes, so no harness reaches another's.
- The files folder can't be a folder whose writing is a way out: the
home folder or one holding it, a dot folder in it (
~/.config,~/.local,~/.ssh),~/Library/LaunchAgents, Link's settings orlnk's own folder. - At the top of the files folder, what runs outside the sandbox (
.git,.envrc,.vscode,.idea) is read only to the harness, so it commits in a repository it makes inside; in its own home it's its own (the sandbox's security). - What an adapter asks for (
lnk-<name> command, run in the home) is checked too: no extra folders the harness can already write, nor the home folder, Link's settings,/tmpor any team's shared folder (~/Link/Teams, or one kept elsewhere), which it reaches only as its[teams.<team>]grants; service names that are only names; noHOME,DYLD_*orLD_*. - Its own program, and an installed plugin's it runs (a store driver),
are read wherever they are,
~/.lnk/bintoo, each by its own path: never the folder it's in, which a test asks for and is refused. - The environment is cleared: no
SSH_AUTH_SOCK, no tokens from your shell. - On macOS, each start writes Link's
mktempinto the harness's home (~/.lnk/bin), from outside the sandbox, without following a link: a link in place of.lnkorbinstops the start, and one in place ofmktempis replaced, never written through.
Its log is out of its reach
Who writes a harness's log, and where.
- launchd and systemd, or
lnkfor an agent in a terminal, append the harness's output, from outside the sandbox, to a log in Link's settings folder (~/.lnk/agents/<agent>/logs). Were it in the harness's home, the harness could replace it with a link to~/.zshrcand have its output written there. lnkrefuses a log that isn't a regular file.lnk agent logsshows its last 200 lines without control characters (no escape sequences), from where the last start began at the earliest.lnkrecords that point; it isn't a marker the harness could print.- It reads at most the last 4 MiB, and
--followreads what's new at most 4 MiB at a time and shows a line without an end in 64 KiB pieces, so a harness printing a burst, or no newlines, can't make it hold ever more.
Running a command in its sandbox
lnk agent harness run runs a command under the same spec as the
harness: its home, the files folder, its grants, its hosts and
endpoints, and its proxy, with the model key added outside as the
harness's is. It has a rules file and a proxy of its own (on a Mac, its
own port; on Linux, its own sockets folder), so it never changes the
running agent's, and its calls are logged under its own name. With the
sandbox off (--no-sandbox, until --sandbox), it warns and runs
unconfined, as the harness does, beside a proxy of its own on a free
port.
Checking its sandbox
lnk agent harness check runs lnk sandbox check's
probes
inside the harness's real sandbox, with its home, its grants and a
<name>-check profile. It refuses to run with the sandbox off. For an
agent on a team, it also reads each folder of the team's it's granted,
which must work, and each other team's folder, which must not.
Its keys
With keys on the wire, it also looks for the model key and each secret the proxy adds in the harness's home. It opens each name there from its folder without following a link, and reads only regular files, at most 64 MiB each, so a harness swapping in a link or a pipe can't make it read elsewhere or hang.
Its tests
Tests run a fake harness whose installer tries to write outside its
home, and which tries to read ~/.ssh and see /run and other
processes: with bubblewrap where the kernel allows it, and a Seatbelt
twin on a Mac.
No sandbox
lnk agent start --no-sandbox turns it all off, with a warning at every
start, until lnk agent start --sandbox turns it back on: one setting
of the agent's, kept across restarts, reboots and every other command,
and shown in lnk agent status. --no-sandbox is the only way: --yes
never implies it, and no grant of a harness's or other command turns it
off or back on. A harness that can't run in any sandbox says so
(needs), and starting it without --no-sandbox is refused, saying to
add it.
Who turns it off
Only you or your script turn it off, from outside any sandbox: nothing
an agent runs reaches the files that decide it, and unsandboxed = false in ~/.lnk/machine.toml forbids it on the machine, whatever
asks; a machine.toml anyone but you (or root) could have written, or
removed (it, or a folder on its way, written by others), refuses it too.
On a box
A box never runs an agent with no sandbox: root's /etc/lnk/kind.toml
says it's one, and its agents run behind their proxy. Nor does a run
from a user namespace of its own (unshare -U), where that file can't
be told from one mounted over it.
Its proxy
With no sandbox, the harness keeps its proxy, on its own port of this
machine's loopback: its tools (but run and file, which need
one), memory, scheduler, model key and sealed secrets,
the wire's log, lnk sandbox stop and its counted usage stay. That
proxy is advice the harness may ignore, not a wall: nothing keeps the
harness to it, and it reaches what you can, Link's settings and the keys
its proxy adds among them. An exit is refused with no sandbox, which
alone would keep the harness to it (with no
sandbox).
A background service
A background service running a harness with no sandbox runs it through
lnk agent unsandboxed, which reads machine.toml and the agent's own
settings first, at every restart and boot: once the machine says
unsandboxed = false, either can't be read, or the agent's settings
don't say sandbox = false, the
service refuses, says so in the harness's log, and removes itself, so it
doesn't run again until lnk agent start starts it in its sandbox, or
--no-sandbox without.
Ubuntu's rule for user namespaces
Where Ubuntu's rule for user namespaces stops the sandbox, lnk agent start offers to add its profile first, and never starts the harness
unsandboxed on its own (Ubuntu's
rule).
Messages and tools
Who may message an agent, every way out of its sandbox, and what each of Link's tools reaches.
You and your shared chats
How each connection knows its owner, and who in a shared chat may do what.
- Telegram:
lnk agent connect telegramprints a one-time code, 8 characters, about 40 bits. It records the user id of whoever sends it, alone as the whole message, to the bot in a private chat within 10 minutes; at.melink sends it for you. Only someone who sees your terminal can. Group messages, other messages and bots are skipped. - It configures the harness with that allowlist, and some harnesses with their owner commands too; the harness refuses everyone else. For Link Harness, the bridge refuses them itself: only that user's direct messages and mentions in a group reach the harness, and the rest get no answer. Errors never print the bot token.
- Slack: the same, with the member id of whoever sends the code to the app's bot in a direct message. Link reads it with the bot token through Slack's Web API, the token in a header; messages from bots, edits and older messages are skipped. The harness takes direct messages from that member only, and in a connection only their mentions of it.
- Its two tokens, bot and app-level, are checked with
auth.testandapps.connections.open, which opens nothing. The app runs in Socket Mode: the harness connects out to Slack, and nothing on the machine listens. - Those two tokens are all
connectkeeps: the app's signing secret and a configuration token (which can edit the user's Slack apps) are only for pushed messages, given tolnk-slack hookby whatever pushes them, and the configuration token is used for the switch and never kept. - Discord: the same, with the user id of whoever sends the code to the bot in a direct message. Link sees it arrive on Discord's Gateway, a websocket, as the bot, with only the direct messages intent, while it waits; it connects before showing the code. Messages in servers and from bots are skipped. Link keeps that direct message's channel id beside the user id: a harness's home channel.
- The harness takes direct messages from that user only, and in a server only their mentions of it. Link refuses a bot whose application lacks the Message Content intent the harnesses ask for.
- Signal:
connect signaltakes the agent's own Signal number, never the owner's, and the owner's, typed at the terminal, so whoever sees it decides. The harness allows direct messages from the owner's number only, and in a group only their mentions of it. It's linked as a device, from a QR code.disconnect signalstops using it; unlinking it on the phone ends it. - Every harness, and Link's bridge for Link Harness, speaks Signal
through
signal-cli, which the Signal plugin downloads into the harness's home only if its SHA-256 is the one pinned in Link's source, as is the Java runtime it needs off Linux x86-64. - For a harness that speaks Signal,
lnk-signal cli installandcli linkrun in that harness's own sandbox, as its adapter's steps do, so nothing the harness can write runs outside it. It keeps the account's keys in~/.signal-cli/datathere, where the sandbox holds them and a move carries them, as the plugin says (lnk-signal cli state). linkrefuses a phone whose Signal is another number than the one given.- In a group, connection or server the agent is in, every harness answers only the owner, and only when they mention it: Link writes each harness's group settings that way (any group, the owner as its only sender, a mention required). On Telegram a reply to the agent counts as a mention.
- A chat the owner shared the agent with (
lnk agent share) is the exception: there a harness that reads who a message is from answers whoever is in it, when they mention it, as a member, never as the owner (A shared chat). - On Discord a reply counts only when it pings the agent, as a reply does unless its sender turns that off. On Signal every harness takes only an @mention, not a reply. The owner adds the bot to a group themselves.
- Link's own connection,
link(lnk agent chat send), has no account, bot or pairing: whoever reaches the agent's endpoint the way its bridges do is its owner. That takes the endpoint's key for this start, in~/.lnk/agents/<agent>/bridge.json(600), which only the agent's user reads, so another user of the machine can't chat with it. - The message goes as a bridge's does: on the harness's own connection,
proved before the key is sent, the thread a plain name (
link:<name>, letters, digits,-,_and.), neverLink-From. - The answers in a
linkthread nobody waited for are held in~/.lnk/agents/<agent>/connections/link/held.jsonl(600, out of the harness's reach, the latest 1,000, each cut at 64 KB), or kept by the harness, until a chat on that thread takes them: one at a time per thread, under a lock, each said done before it's shown, so two chats never show one answer. - A chat takes only those owed to the chat; one meant elsewhere is the
bridge's to send. An answer is shown with its control characters taken
out, so nothing the model writes moves the cursor or sets the
terminal's title or clipboard;
--jsonwrites the C1 controls and the bidi and zero-width marks as\uescapes. -mputs the message on the command line, which other processes of the machine can read; on stdin it isn't. For an agent on a box, the message always goes to the box on stdin, never inlnk box run's arguments.lnk agent disconnect <connection>forgets a connection's tokens.- Each connection's pairing is a plugin of its own (
lnk-telegram,lnk-slack,lnk-discord,lnk-signal), run bylnk agent connectoutside the sandbox, as the harness plugin is. It takes its tokens from its variables or the terminal, and hands them back on its stdout, a pipe to the harness plugin, never on a command line. - Its arguments are the agent's name and the ids of bots other agents have, none secret.
A shared chat
Who in a chat its owner shared the agent with may make it do what.
- The agent is the cordon. Whoever is in a chat its owner shared it
with (
lnk agent share) can make it do all it can: its files, its tools and its memory, its owner's bill. Nothing of it is narrowed for them; an owner who wants less shared makes a second agent, with only those files and its own bot. - Only the owner shares.
lnk agent shareruns the connection's plugin (lnk-<connection> share) outside the sandbox while the agent is stopped, as pairing does. - It reads only the messages of the users the connection is paired with
that mention the bot, takes a one-time code
(
link_connection_contract::pairing_code, 8 characters) compared in constant time, once, within 10 minutes, and says in the chat that it's shared. - Once the code is sent in the chat,
lnk agent sharewrites the chat into the agent's spec,[access] message, in place and atomically, reading the spec only when this user alone writes it; the running bridge never writes it.lnk agent share removetakes it out the same way and restarts the agent. - Never the owner. A member's message reaches the harness with
Link-From: person:<connection>:<user id>and their name on the connection inLink-From-Name, words for the model, never who they are. A bridge is handed the shared chats only for a harness whose contract says it readsLink-From;sharerefuses any other, and a harness that speaks the connection itself. - Still the owner's: a member's direct message to the bot,
share,unshare, and approving what the agent asks.
Questions on your chats
How a question for you reaches your chats, and who can answer it.
- A question is a call of the agent's sandbox that asks first (a tool's
call, a page to open), or a webhook's question. The agent's bridge
sends it to your direct chats alone, never a group: the connection's
ownersays which they are. - Where a turn of yours is under way, it asks on that connection; with none, on every connection. It shows the question as the dialog does, cleaned and cut, so the call's arguments reach the connection's servers.
- Its code is the question's id, six letters and digits, kept in a
store only you read (
~/.lnk/questions/). Nothing in a sandbox reads the store, so the model never learns a code it could hand anyone. - Your reply is an answer only when it's the code alone, or
noand the code, from you, on a chat it was asked on. The bridge takes it before the harness sees it: the agent learns only whether its call went through. A code from anyone else, in a group, or of a question already answered or past its time reaches the agent as a message. - The first answer counts, from a chat, a dialog, a terminal or
lnk sandbox answer, and allows that one call once. No answer within[connections] ask_waitis no. - Whoever holds your account on a connection can answer: a stolen phone's
Telegram allows what you would. A webhook's question is plain text of
600 characters at most, with no link in it, and at most
--questions-per-dayof them a day (20), so a webhook's leaked secret can't flood your chats or send you somewhere.
Messages from others
How a message from a schedule, a subagent or a teammate is told from yours.
- A message from a schedule or a subagent reaches Link Harness with
Link-From, is kept as from them, and the model is told who's speaking, with each line of it quoted, so no paragraph of it reads as yours: it's never taken as yours. ALink-Fromnaming no sender is refused. - A stranger's message (one from neither the owner's own schedules,
subagents and commands nor someone the owner let in: a webhook's) is
given only the tools its spec's
[tools] strangersnames, none by default, and always says so, so its conversation never keeps more. A harness that can't narrow a conversation's tools never takes one. - It goes only to its own conversation, named as its sender, and is
answered nowhere, so it never reads or narrows another and nothing it
says reaches the owner's.
strangerscan't name the scheduler orrun: through them the agent acts as itself, and a stranger's standing would be lost; and only the agent's own spec sets it. - Every such message carries a hop count; the scheduler refuses one
past the agent's cap (
[tool.scheduler] hops, Link's 8). Hops bound how deep a chain goes, not how many messages it sends (each firing of a recurring job starts again): that's the cap a day ([tool.scheduler] per_day), with the cap on subagents at once, so agents can't wake each other for ever. - The scheduler runs as a tool, outside the agent's sandbox, confined to
writing its jobs (
~/Link/Agents/<agent>/Agent/schedule). Which conversation a call is made in, and its hops, come from the harness (_meta), never from what the model writes, and a call lists and cancels only that conversation's jobs and its subagents'. The scheduler trusts what the harness sends: - Link Harness doesn't run what the model writes itself, and
run's commands reach no tool, but a harness that does (a shell) could name another conversation or reset its hops, bounded then by the day's cap alone. - A subagent has its parent's tools or fewer, never more. The scheduler
takes a list only of the tools the harness says the parent's
conversation has (
_metalink.local/tools), and, given none, the parent's whole list; the harness keeps the list with the subagent's conversation and narrows it again at each list, never widens it. - A harness that can't keep one isn't sent one: the bridge and the scheduler refuse it rather than send a subagent every tool it was meant not to have. The list bounds what the model is offered and calls; what each tool may do is still its grants'.
- A subagent runs on the model it was started with, else its parent
conversation's (
_metalink.local/model), only ever one of the spec's[model] allow: Link Harness refuses another, and the model proxy names only those it was granted. - The bridge's way in is a Unix socket with the agent's settings (600),
which only you reach, and each message at it is a call proved as the
agent whose scheduler or
runhands it (below). An answer it sends goes only to a chat of the owner's alone (Telegram and Signal: an allowed user's own chat. - Slack and Discord: the owner's direct messages with the bot, kept when
they sent the code), or to a group an allowed user talked to the agent
in, which the bridge keeps in the connection's folder (700, out of the
harness's reach), and none with the spec's
groups = "never"; never anyone else. - Signal sends through the running bridge's
send.sockin that folder (600), which takes a message only proved as its own agent's, with the key the machine's keyring holds for it still: a bridge outlivinglnk agent stoptakes nothing more. A Slack app or Discord bot connected before Link kept that sends no such answer untillnk agent connectagain.
Webhooks
Where a webhook's posts arrive, and how each is checked.
- A webhook's posts reach the agent's bridge on 127.0.0.1 alone, at a
port kept in its settings. It never listens on another address: a
public one is a tunnel's (
lnk tunnel open --public), which you open yourself. Each webhook's address ends in an id of 128 random bits; an address with no webhook is answered404. - A body is capped at 1 MiB before anything else is read of it (
413). Then the webhook's plugin checks its signature, handed nothing but the webhook's own secret, on its stdin, and calling nothing: Link's scheme, an HMAC-SHA256 over the time, the post's id and the body, within 5 minutes, or GitHub's. - A post signed otherwise is refused (
400or401) and never reaches the agent, nor does one signed right that carries neither JSON nor text (415). The plugin islnk-webhook(its security). - A post's id is kept 30 days once the agent's harness has taken it
(
hooks-seen.json, 600, written whole, never through a link), and a repeat inside them is refused (401), as is the same post while the first is being handed on. A post the harness refused, or that couldn't reach it, is kept nowhere and answered503, so its sender's redelivery is taken. - A post its plugin gives no id is refused (
503): every post has one, and a plugin can't turn the check off. - A post that's taken reaches the agent as a stranger's message from
webhook:<name>(above): quoted, never as yours, in its own conversation, with only the tools[tools] strangersgives, and its answer goes nowhere: the poster is told202once the harness has taken it, never waiting for its turn to end. - The bridge hands it to the agent itself, through its way in, saying
who it's from from the webhook's own record: the plugin that checks
the signature never holds the harness's endpoint or its secret, and
nothing it answers can make a post the owner's.
addrefuses an agent whose harness can't say who a message is from, or can't keep a conversation's tools. - Each webhook's secret is 32 random bytes, kept with your secrets and
given only to the agent's bridge, outside its sandbox; it's shown once
by
addorrolland never logged.rollmakes a new one and keeps taking the previous one for 24 hours, its expiry beside it, or untilroll --done, with no restart: the bridge reads each webhook's secrets at each post. - A post of
{"question": "..."}alone, signed, is a question for you (below), never a message to the agent: it is answered202with the question's id. AGETof/h/<id>/questions/<question>, signed with Link's scheme over an empty body, answers its status; a webhook GitHub signs takes none. - The bridge takes at most 64 posts at once, as its way in takes messages; a body not read within 10 seconds, or a check not answered within 30, is refused.
Every way out of its sandbox
A harness's sandbox lets it out by two kinds of door, and by nothing else: its proxy, and its bridges.
- The proxy (the sandbox's security): the internet, its model and its tools, as its permissions say, with the keys and secrets it never sees added outside.
- Its model, secrets, tools and pages are its routes, each handed, proved as the agent, to the bridge of the plugin it's for, outside every sandbox (routes).
- A proxy started blank, on a machine that packs agents, refuses every call until it's bound to one agent (a blank proxy).
- Its bridges: programs outside the sandbox, each holding what the sandbox never sees and doing one kind of thing for the agent with it.
A route's bridges
lnk-model model bridge, lnk-secret secret bridge and lnk-sandbox sandbox bridge, reached only through its proxy. What crosses: a model's
requests and answers (inference only), a service's calls by its secret's
rule, a tool's calls by its rules, a page to open, each call's verdict
before it's answered, kept in the wire's log. It holds nothing between
calls: the key, secrets or tools each call's route is given, from the
bind its proxy holds.
A connection's bridge
lnk-<connection> bridge, in lnk agent bridge. What crosses: your
messages in, the agent's answers out; for Signal, an answer nobody
waited for at send.sock. It holds the connection's token, or a linked
device's keys, and who may talk to the agent.
The way in
way-in.sock, carrying messages from others: a schedule's, a
subagent's, a background command's ending, a teammate's message, to the
agent's endpoint; the answer to a chat of yours, or back. It holds where
each answer goes, and who may message whom (the teams' files).
The store bridge
.link-store.sock, in the harness's own folder, for a legacy adapter
keeping the folder layout, or any adapter opting into authenticated
storage. What crosses: collection records, leases and versioned files
through typed store calls, never a raw query or host path. It holds the
driver's program, and whatever it needs to reach its store.
Its limits
The store bridge holds 16 calls, 8 of them one agent's, and runs 4
drivers at most (a driver that serves, kept running between calls), and
refuses a chats/ that's a link.
The files bridge
link_store_contract::files, where the agent's files folder is kept in
a bucket, on a machine that runs agents only while they answer. What
crosses: Link File's and its memory's calls on the files folder: read,
write and list a path of it, and get or put an object of its memory's
index beside it.
What it holds
What reaches the bucket, and the files folder's key: every object is encrypted, its name too, in rclone's crypt format, so the bucket sees nothing of it.
Never another's
Never another agent's files folder, a hidden path, or one outside it. A
team's folder only where a call names the team and the machine serving
the bridge says, at that very call, that the agent is on it, and then
only the folders of it the team's file grants the agent, written only
from the version read. That grant is the agent's role on the team, a
Policy Spec, and each call is checked with link-iam's allows, as
every check is (who may do what).
Its writes
A write only in a folder its [files] lets it write, only from the
version of the file it names (Link File names the one it last read or
wrote, when it did), and refused past the files folder's cap: an agent's
writes are checked against it one at a time, and what the folder holds
is counted again each time the agent is bound.
Proving and holding
A caller has 5 seconds to prove who it is, on one of 64 places kept for proving, 4 of them the same user's: a user's fifth closes its own oldest, never another user's, so 16 users each holding theirs keep none of them out (a call turned away busy is made again for about 5 seconds). Once proved, it holds one of 16 connections at most, 4 of them one agent's and 8 one owner's agents', and waits at most 30 seconds for all of its call to arrive, or for its answer to be taken.
Every call
Every call to a bridge is the same three things: the agent's identity (its ID and owner, as Link defines them), the action, and a proof: HMAC-SHA256 with the agent's own key of a random challenge the bridge sends on that connection and the identity named. Never a process's, a socket's or the system's word for who's calling. The bridge looks the key up in the machine's keyring and refuses a call whose agent isn't bound there, or whose proof isn't that agent's, before it does anything, and one that hasn't proved who it is within 5 seconds of its challenge:
- A key proves only its own agent. A harness, however it's steered,
acts on a bridge only as itself: the store bridge answers only its
agent's conversations, the way in takes another agent's message only
as from that agent, and
send.sockonly its own agent's. send.sockis handed its agent's key as its bridge starts, and checks the keyring too: once the agent stops, or starts again with a new key, a bridge still running takes nothing proved with the old one. A proof is a signature of one challenge, so it gives the key away to no one, and is of no use on another connection.- The key is made at each start: the endpoint's key, which the
harness already holds and Link keeps outside the sandbox.
lnk agent startbinds it in the keyring (~/.lnk/agents/.keys, 700, which every sandbox hides), andlnk agent stopandremoveunbind it: from then on it proves nothing. - A machine that runs an agent only while it answers binds what runs it there to the agent for the request: it's given the agent's identity and a key made for that binding, and when it stops the key is unbound and dropped. Blank, it holds no identity and no key, and no bridge takes a call of its; a key kept from a binding that ended proves nothing, and the next agent bound there gets a new key.
- A test packs two owners' agents on one machine, each a unit with its own blank proxy and sandbox and no virtual machine, and from inside each tries the other's folder, sockets, proxy, model key and conversations, and the machine's loopback: none is reached, and once a unit sleeps, its key proves nothing and its folder is empty.
- One bridge for many agents tells them apart by the proof alone, the same on Linux, macOS and any machine. Each agent's store bridge serves only that agent, whatever another agent could prove.
- The endpoint's own proof, the harness proving itself to a connection's bridge, is the same signature the other way, under a label of its own: an endpoint, which signs any challenge it's sent, is never asked to make a bridge's proof.
Every bridge runs on one server, so each holds to the same limits whatever it does with a call:
- Five seconds to prove. From its challenge, a connection has five seconds to say who's calling and prove it, the whole exchange however slowly it's sent: one that sends a byte now and then is cut off all the same. Who's calling is at most 4 KiB, refused once that much is read, and refused unless it names an agent's ID and owner; nothing of the call after it is read until it's proved.
- Unproven connections apart, by user. Connections that haven't proved who's calling hold places of their own, never a proved call's: 4 of each peer's, the user at the connection's other end as the system names it, with room kept for 16 peers. A peer's fifth closes that peer's own oldest, answered busy, never another peer's; a seventeenth peer is answered busy until one goes.
- With those limits, a flood keeps out neither a proved call nor another user's proof. On a machine where every agent's harness runs as you, they're all one peer: one harness flooding a bridge can keep out the proofs of the others there while it floods, though never a call already proved.
- Caps on proved calls: each bridge holds so many proved calls at
once, so many of one agent's and so many of one owner's agents' (the
table above; the way in 64, 16 of one sender's;
send.sock8, sent one at a time). Past any, a call is answered at once that the bridge is busy. - One error envelope. Every call that fails, on every bridge, is
answered with one line,
{"error": "<why>"}, with"why"(refused,busyorconflict) when a caller acts on it, and the connection closed. A refusal shows nothing the caller sent. - A test every bridge passes checks each of these against the bridge itself, running: a wrong key, a proof replayed on a new connection, a malformed or overlong proof, and an action it can't read, none of those run.
- It checks too a caller that never answers or answers a byte at a time, an action sent a byte at a time, a peer's flood, one agent's calls past its share and everyone's past the bridge's, an answer never taken, and the envelope.
One bridge for every agent
Off by default. With shared_bridge = true in ~/.lnk/machine.toml,
one bridge (lnk agent bridges) carries every agent's connections and
messages from others on the machine, instead of a service each, and one
scheduler runner fires every agent's jobs in a time zone:
- It runs outside every sandbox, as each agent's own bridge does, and holds what each held: every agent's endpoint key and connection tokens, read from each agent's settings (600). A fault in it stops every agent's connections until it starts again. As a service, the machine's service manager starts it again. With agents in a terminal, each agent's start does, within a few seconds.
- Each agent keeps its own way in, endpoint and key, its connections'
bridges (
lnk-<connection> bridge, one each), the answers it owes and its scheduler's jobs and caps. A message at one agent's way in is asked of that agent's harness alone, with its key, and its answer goes only to that agent's chats. A test shows neither agent's words reach the other's conversations or bot. - An agent started again is carried again on its own, and the others are left as they are. A turn cut short when the bridge stops is finished by its harness, and its answer delivered once when the bridge is back, as after any restart of a bridge.
- Whom it carries is in
~/Link/Agents/.bridge(700), which every sandbox hides: a record for each agent, written at its start and removed at its stop. With no agent to carry for 20 seconds, it ends.
An idle agent
Where one bridge carries every agent, an agent stops when it's had
nothing to do for its spec's [run] idle_after (Link's 5m): no
message, no answer still coming however long it pauses, no turn running
and no background command. It then holds no process, memory or port of
its own, and its next message starts it again:
- On Linux, for an agent sandboxed behind its proxy, the bridge listens
on the agent's endpoint port in its proxy's place, awake or asleep,
and passes each connection into the agent's sandbox through its
proxy's folder (
in.sock, in a 700 folder), as the proxy would: never through a link, and only to a socket of the user's own, since the sandbox writes that folder. - Anywhere else (a Mac, where the harness listens on the loopback itself, or an agent with no proxy), the bridge listens on the port only while the agent sleeps. At the first connection it lets go of the port, starts the agent, and passes that connection on once the harness listens there. A connection's connection in that moment is refused and tried again for a few seconds.
- Whether the agent is awake or asleep, the bridge passes bytes and holds nothing: the endpoint still proves it's the harness, with the agent's own key, before it's sent the key or a message, and the key is never the bridge's to check. Every sandbox, proxy and grant is the agent's own as at any start.
- A connection to a sleeping agent starts its job first: its service, or its start's program in a terminal.
- A background command running holds its record's lock, and keeps its
agent awake, so its supervisor, under the agent's own job, isn't
stopped. A turn running (Link Harness's
turn.json) does too, and so does any connection open to a harness holding its own port. The answers of turns nobody took are asked for when a turn ends and as it wakes. - Where an owner's agents share one harness, a sleeping agent stays one it serves, so its stopping and waking cut no other agent's turn, and with every one of them asleep, that harness ends too.
- A harness that speaks one of the agent's connections itself never sleeps: it would stop hearing it.
~/Link/Agents/.bridge/asleep/<agent id>marks an agent asleep. Withidle_after = "never", or the shared bridge off, an agent runs as before.
Teams
Who may put an agent on a team, and what a teammate's message may do.
- A team's file is in Link's settings (
~/.lnk/teams/), which no sandbox reaches, and onlylnk agent teamwrites it: no member puts itself or another on a team. - A message from another agent (
agent:<id>) is taken only when the two share a team, as the teams' files say when it comes: the bridge reads them again for each, so an agent taken off a team is refused at once, whatever its scheduler was told at its start. - Each team is a role its members hold, granting
agent.messageon each of them: the way in askslink-iam'sallowswhether the sender may message this agent, as every check is asked. - A teammate's message is a call proved with the sender's own key
(bridges): it's taken only as from
that agent, never as from another or from a schedule, only into the
receiver's conversation with it (
agent-<its ID>) or a team's they share (team-<team ID>), and its answer goes only back to the sender's scheduler, never to a chat. - A teammate's answer lands in the conversation it was asked from, handed to the asker's own way in by the asker's own scheduler. It's kept as that agent's, quoted, never yours, and bounded by the same hops and cap a day as any message from others.
- The cap a day bounds both ways: what a member sends, each message
counted once however often it's tried again, and what it takes from
each teammate, at its own way in, up to its own
per_daya day (UTC) from one teammate, answers to its messages included; past that a teammate's message is refused, and none is taken at all when the receiver's spec sets noper_day. - A message at the hop cap is delivered, but its answer, one hop past
it, isn't sent back. A teammate's message is answered with all the
receiver's tools but
remember, and the answer goes back to the teammate, so a teammate can ask for what the receiver can read or do, never have it note something for every conversation after. - Where the harness is sent others' messages (Link Harness), memory's
rememberwrites the notes only in a conversation of the owner's, as the call names it (_meta, the same word the scheduler takes): never a teammate's (agent-<id>), a team's (team-<id>), a stranger's (named as its sender) or a subagent's (task-<id>), and never in a call that names none; it tells the model why. - A chat the owner shared (
lnk agent share) is the owner's conversation, so its members can have the agent remember, as they can make it do all else it can. A subagent can't show that its parent conversation was the owner's, so the owner's own subagents don't remember either: the conversation that started one notes what it found. - The scheduler sends only to the teammates it's given, through their bridges' ways in, Unix sockets only you reach.
- A member reaches only what its sandbox file grants it of a team's
folder (
[teams.<team>]), in its harness's sandbox, inrun's andfile's, and nothing of another team's; every other sandbox hides~/Link/Teams. A grant names folders one level inside the team's folder, or all of it, and a folder only read is never inside one written. - A folder granted there is bound only where it really is: one a member
that writes the folder replaced with a link is left out, wherever the
link points. A team's shared folder is checked as a files folder is:
one a hand-written team's file names where writing leads out of the
sandbox (
~/.local/bin,~/.ssh, Link's settings), or in agents' folders, grants nothing. - Every sandbox hides each team's folder kept elsewhere, so no agent
starts while a team's file can't be read. A team's memory
(
team-<id>-memory) can't be granted as a tool, only handed to members. - A team's memory is a shared tool: one server, outside every sandbox,
started by the first member's proxy that needs it and stopped when
idle, in a sandbox of its own that reads the team's folder and its
program, and writes only its index (
~/Link/Teams/<team>/Memory). - Only the proxies of members granted all of the team's folder (
., read or written) name it, since it searches all of it: a member granted only some of the folder isn't handed it. Every member's search reads the same index, so what one member's files say, the others can find: that's what the team's folder is for, and what each member writes there is the team's.
Memory
Where an agent's memory runs, and what it reads and writes.
- The engine runs outside the agent's sandbox, as its tool (
lnk sandbox tool add, asmemory-<agent id>), and in a sandbox of its own. - The engine's sandbox reads the agent's Home, its conversations and skills in its own
folder, and its own program, not the folder it's in
(
~/.lnk/bin, withlnkand every plugin), and writes only its folder in the agent's own (Agent/memory/<engine>) and itsAgent/memory/, for the agent's notes, unless the agent's sandbox file locks its instructions (instructionsunder[files] read). - Never
chats/or anything else of either folder: a test tries. With the agent's sandbox off (--no-sandbox), it runs unconfined, a process each session. A session ends with its input closed, or its process killed: on Linux the engine is sent SIGTERM then too, so one that ignores its input doesn't outlive the session. On a Mac, an engine that ignores its input outlives a killed session. - Link Memory serves all of one owner's sandboxed agents on a machine from one process, in one sandbox holding what each agent's would hold: every one of their Homes and own folders' conversations and skills read, every state and notes folder written. So that process can read each of those agents' files.
- It answers each session from the one agent Link named as the session opened, in a line the agent never writes: the tool Link defined for it says which. Every session after is that agent's alone, its search, reads, list and notes. A test shows one agent never finds, reads or notes another's, and a session naming an agent it doesn't serve is refused. Two owners' agents never share one.
- The service's socket and what it serves are in
~/Link/Agents/.memory/<owner>, which every sandbox hides, so only the service and what runs outside any sandbox reach it, and it takes a session only from a process running as you, as the system names the one at the other end: another user's is refused. - Of that folder the service's sandbox writes only its working folder
(
run, its socket's): the records of what it serves, what it's handed of them and its log, which Link reads and opens outside any sandbox, it can only read, so the engine can't change what its next start is confined to or point its log elsewhere. - It starts at a session's first need and ends a few seconds after its last, so an idle owner runs nothing of it.
- Its sandbox's network is all its agents' together: one agent whose
memory uses a model on this machine gives the service
[network] mode = "host"for every agent it serves. - Its embedding model is served for it outside its sandbox (
lnk model serve, once for the service, stopped with it), and its sandbox is given the address in$LNK_MEMORY_MODEL, a port of this machine's loopback chosen before it starts. - Every model of the service is started at once and waited for together, at most 10 s; one that's slow or fails keeps that address and is started again every 30 s while the service runs, and the engine searches by words until it answers. Link Memory follows no redirect from it, and reads at most 64 MiB of an answer. One no runtime here has isn't started again.
- To reach it, Link Memory's sandbox has
[network] mode = "host": on Linux, the only way to this machine's ports. So it could reach the internet and, on Linux, every port of this machine's loopback, though it calls only that model, on 127.0.0.1. Nothing it indexes leaves this machine: the model runs here. - The model's server takes no key: while it runs, any program of this machine can call it on its port.
- Link Memory indexes only Home's
memory/and the agent's conversations: the rest of Home it never reads, and of the agent's own folder it reads only its conversations and skills. - It extracts the text of PDFs and Word documents in
memory/in a child process, never in the one holding agents' keys: its own program again, the document on its stdin and the text on its stdout, given no environment and none of its secrets, at most 30 seconds, 30 seconds on the processor and 1 GiB of memory (on a Mac, where memory isn't limited, the 30 seconds alone), and no core dump. - At most 32 MiB a file and 2 MiB of its text; a parser that fails, crashes or runs past its limits fails that file alone, found by its name. A crafted PDF that takes over the parser holds only that document, and can reach whatever the user it runs as can.
- Where an agent's machine keeps its conversations, its notes or its
memory off its folder (
machine.toml's[storage]: in SQLite, a bucket, or a box's store), or its files folder in a bucket on a machine that runs agents only while they answer, memory's service binds the agent: - Link hands it the agent's key for this start and where its bridges
are, and memory reads
memory/through the files bridge and its conversations, notes and index's copy through the storage bridge, as that agent, proving each call. It drops the key when the agent sleeps, and a session still open answers nothing more. - Its socket takes a session only from a process of the user it runs as,
as the system says (
SO_PEERCRED,getpeereid), and of the one other user it's told (--peer: the service binding a machine's agents, when memory runs as a user of its own), its socket then shared with that user's group alone (--group, mode 660) in a folder no one else enters. - A session's first line, naming its agent, or a control for one, carries no proof: who may connect is all that keeps anyone else from naming an agent bound there. Every binding and control names the agent by its owner and ID, so one owner's agent is never served or ended as another's of the same ID.
- On a computer it reaches the storage bridge at a socket of its own, in
a folder of its owner's memory service that its sandbox reads and
every agent's sandbox hides; through it, it changes besides what the
agent's sandbox writes only its
memorycollection and, where its notes are written,memory/notes.md. - Its cache of each agent's index is plain SQLite on the machine's disk, mode 0600 under its own user, never in a sandbox; the copy of it, kept with the agent's memory or beside its files folder, is encrypted where the place encrypts (a bucket always does), and a write back is made only from the version the cache was read from, so one machine's never overwrites another's unseen.
- A cached copy of an index is the owner's to change, and this process holds every bound agent's key, so a copy fetched is checked whole and for memory's own schema alone before it's used, and made again otherwise; every index is opened in SQLite's defensive mode, its schema untrusted.
- The bridges a binding names must be the machine's own (the ones its service is told of, or the agent's own storage bridge socket): any other is refused before the key goes anywhere. A binding is served, even from a cache already there, only once one of them takes its key, and a cache is used only while it's of the copy there now.
- Link Memory reads only its folder: it never follows a symbolic link or
goes into a hidden folder, and
readserves only what it indexed. It opens every name from its folder's handle without following a link, so a link swapped in while it reads is refused too, and reads only regular files, at most 1 MiB each, however they change meanwhile. Tests show nothing outside is reached. - Its reading holds by path: a hard link in the folder is a regular file, read like any other, so a hard link a harness makes there to a file it can already read is indexed.
- Its four calls that read never ask you first, nor does
remember, which writes only the notes file Link names,memory/notes.md, at most[tool.memory] notes(4 KB), never through a link. What the agent notes is in its prompt at every message after, as its standing instructions are, so a note it was talked into stays until it, or you, change it: lnk agent show --instructionsshows it, and the file is yours to edit. Another engine's calls that write ask, as any tool's do.- The agent reaches it only through its proxy, which logs each call
(
lnk sandbox log). Each agent has its own, over its own files; one agent's proxy names no other's. - Its index holds a copy of what it indexed, and each chunk's vector,
in a folder only you can read (700), which no harness reaches. It
also keeps the path and title of each document deleted from the
folder, the latest 10,000, for
list {since}; your agent can list them, as it could the documents before.lnk agent memory stopleaves it; delete it by hand.
Web
Where Link Web runs, and what it reaches.
- Link Web runs outside the agent's sandbox, as a tool you add (
lnk sandbox tool add web), with your user's rights and the environment you gave it. The agent reaches it only through its proxy, which logs each call (lnk sandbox log), and only once you allow it (lnk agent allow --tool web). - A fetch never reaches this machine or its networks: loopback, private
and link-local ranges, carrier-grade NAT, a cloud's metadata service
(Azure's host,
168.63.129.16, too), this machine's own addresses, and the same written as IPv6, by the sandbox proxy's own rule. - Every address a name resolves to is checked, one forbidden refuses it
whole, and the connection is made only to the addresses checked, so a
name can't point elsewhere between the check and the fetch. Each
redirect, at most ten, is checked the same way, and one from
https://tohttp://isn't followed. Onlyhttp://andhttps://are fetched, never with a login in the URL. - A host you name with
--private-hostis fetched wherever it resolves. - With a proxy in the tool's environment (
--env HTTPS_PROXY), the proxy connects: the names are checked here, but the proxy resolves them again and reaches what it reaches. - A page is read up to 5 MB, and a page, its redirects and its body together, or a search, may take 30 seconds. Nothing on a page runs: its HTML is read as text, and its scripts, styles, images and forms are left out. A page nested more than 512 elements deep comes as plain text, so its markdown can't overflow the tool's stack.
- At most 16 calls run at once; a call past them waits to be read until one is answered, so a client that sends calls without end can't make the tool hold a thread and a page for each. Two pages at a time are made markdown.
- A search sends the query to Brave's or Tavily's API with your key,
from the tool's environment (
--env), which no harness sees. An error from the API never carries the key. - A page is the web's, not yours. What it says reaches the model as a tool's answer, so a page can try to steer the agent, and a fetch can send what the agent puts in a URL to any site on the internet. The tool tells the model a page's words aren't its owner's, and the agent's sandbox and grants still bound what it can do. Allow the tool only to an agent you'd let browse.
Run and file
Where run and file serve, and the sandbox each call runs in.
- Link Harness's
runandfileserve outside its sandbox, as its tools (run-<agent id>,file-<agent id>), and run their calls in sandboxes made from the agent's sandbox file (lnk sandbox run):runone a call, ended with everything the call started;file, which runs only Link file, one a session. The servers only start those sandboxes: they read and run nothing of the agent's themselves. - Each call reads Home and the agent's instructions, skills and
conversations in its own folder, and writes only the folders its
[files] writenames, so neither rewriteschats/, and by default neither changes its instructions or skills, which are yours. Every other agent's folder stays hidden,--read '~'or not, and Link's settings stay closed, as in every sandbox. runstarts only the programs of its[commands]this machine has, checked by the tool and enforced by the sandbox, and a program one of them starts is held to the same list.- It reaches the internet only with
[internet], through a proxy of its own, holding to the file'shosts,tcpandper_minute, and its[tools]; never this machine's other servers or your network, but with[network] mode = "host". Its calls ask nothing:[commands]allows or refuses. filereaches no network and runs only itself. It never follows a link, and refuses a path outside the files folder, a file over 4 MiB and one that isn't text. It opens each folder on the way from the one before, never through a link, and reads and writes the file through its folder's handle, so a link swapped in for a folder meanwhile is refused, never followed;findwalks the same way.- Where the files folder is kept in a bucket,
fileruns no sandbox and touches no disk: each call goes to the files bridge as the agent, proved with its binding's key, which reaches only its own files folder (noteams/), and holds to[files] writeas well as the tool does. - A
replace, and awriteof a file it read or wrote before, names the version it last saw and is refused if the file changed since; awriteof a file it never read replaces what's there. run's working folder (folder) is refused through a link, looked at the same way. With any program allowed (*), a program named by path is left for the sandbox to find, so a call can't learn what exists outside it. A call's process group is stopped before its leader is reaped, so the group's ID can't be another's by then.- A command runs at most
[tool.run] timeout, a tenth less, and what's kept of what it printed is[tool.run] output. Within that time it can use the CPU, memory and disk space its folders hold. - A background command (
run__start) runs in the same sandbox as arun__commandcall, under a supervisor of its own outside it, for at most[tool.run] background_timeout, and at mostbackgroundat once. - Stopping it, or its supervisor dying however it dies, ends its whole process group: on Linux the sandbox dies with the supervisor, and a sweep kills what's left only when it's surely the command's, on the same boot, its group's leader the one recorded or gone. Stopping, restarting, moving or deleting the agent ends every one.
- Its output,
background/<id>.outin the files folder, is written outside the sandbox through descriptors, never following a link, and kept to[tool.run] background_output. The agent's sandboxes read it, and write it only if[files] writenamesbackground, which changes nothing but its own output. - Its record, which names the process group, is in the agent's folder
outside the files folder, where nothing the agent runs reaches. Both
go
background_keepafter it ended and was told, and when the agent is deleted or moves away. - Its ending reaches the agent as a message from
run:<id>, never as yours, at most once, through the agent's bridge's way in, a socket only you reach. Within its time a background command uses the CPU, memory and disk space its folders hold, as a call does. Each ending is a turn, so starts count toward a cap a day,[tool.run] background_per_day(100), besidebackgroundat once and the hops. - A background command is its conversation's:
run__list,run__readandrun__stopreach only those the conversation that asks started, as its harness names it (link.local/thread), and answer nothing to a call that names none. Its output is still a file of the files folder, which every conversation'sfilereads. - A start's conversation and hops come from the harness (
_meta), as the scheduler's do, and are trusted as far: a harness that runs what the model writes could name another conversation or reset its hops, bounded then bybackground_per_dayalone. - With the agent's sandbox off,
runandfilearen't defined, since they run code only in a sandbox.
Code
Where Link Harness's code serves, and the sandbox each language server
runs in.
- Link Harness's
codeserves outside its sandbox, as its tool (code-<agent id>), and runs each language server in a sandbox of its own (lnk sandbox run), made from the agent's sandbox file asrun's is but with no network. - Each language server's sandbox reads Home and the agent's own folder's
instructions, skills and conversations, your home only with
[files] read = ["~"], writes only the folders[files] writenames, and starts only the programs of its[commands], the server among them. - A server runs the project's code (build scripts, macros), so
codeacts as the agent, asrundoes, and is never a stranger's. - It starts only a server its
[commands]allows and this machine has, and says which it would run otherwise. A server lives in a process group of its own, killed when the tool's session ends or the tool dies. - The tool asks only about a file in the files folder, links resolved, and shows a line only of a file there: a place a server names elsewhere is given by its path alone, so a server can't read out a file it was never shown. It never applies an edit a server proposes.
- A call waits on its server at most
[tool.code] timeout, a tenth less, and a server uses the CPU, memory and disk its folders hold, asrundoes.
Keys and models
Where an agent's keys are kept, and what a local model reaches.
Secrets
Where the model key and each connection's token are kept.
- The model key and the bot token are kept with your secrets (Secrets):
~/.lnk/secrets.toml(mode 600, or the Keychain on a Mac), asAGENT_<AGENT>_MODEL_KEY,AGENT_<AGENT>_TELEGRAM_BOT_TOKENand so on, each granted to the agent by its ID, so an agent made later with its name has none of them.- The harness's own
.envin its home holds them too where the harness needs it (mode 600). A key still in the agent's settings, or in the Keychain asagent/<agent>/…, where Link kept it before, is never read and never removed: the agent's start names it and how to give it again. - A secret you grant the agent (
lnk secret create <NAME> --internet-host <host>, thenlnk secret add <NAME> --agent <agent>) is sealed: its proxy adds it to requests for that host alone, and the harness is told only a placeholder,$<NAME>, and its service's root on the proxy,$LNK_SEALED_<NAME>_URL. - One kept plain is never given to an agent, and an agent whose harness runs without a proxy of its own (no sandbox, or a harness shared with your other agents) doesn't start with one.
- They reach the adapter on stdin, never on a command line or in the
background service's definition: the LaunchAgent plist is readable by
other local users. The harness's
.envstays a file. - For a harness Link carries connections for (Link Harness), the bot token
never reaches the adapter, nor the harness's proxy:
lnk-agenthands it tolnk-<connection> bridgeon stdin, and the bridge service (lnk agent bridge) reads it from the agent's settings itself, never from its definition. - With keys on the wire (the default for Link Harness and for a harness
whose adapter says it takes the proxy's address, or
lnk agent secret wire; behind the proxy), the harness's.envgets a placeholder instead of a hosted model's key, and the proxy adds the real one outside the sandbox. - The same goes for a Telegram bot's token: the harness gets the bot's
id and
:link-wire-with a tag that's new at each start and its agent's alone, never made from the token, and reaches Telegram through its proxy, which adds the token and refusessetWebhook,logOutandclose, by the rule the Telegram plugin gives (lnk-telegram rule --json) (Sandbox Security). - An agent doesn't start while a connected connection's plugin can't say
whether it has such a rule, so its secret never reaches the harness by
mistake.
lnk agent harness checkreportsLEAKif the key or the token is anywhere in the harness's home. Slack's, Discord's and Signal's stay with a harness that speaks them. - The key proxy passes inference only: messages, completions, embeddings, a voice note's transcription and the models' list; never files, batches or the account.
- The provider's own tools that run on its servers (web search, web fetch, code execution, MCP servers) are removed from each request, and a request asking the provider to fetch a URL, such as an image or a document, is refused: they would reach the internet for the agent.
A local model
lnk agent start offers the models lnk model list finds, or on an
Apple Silicon Mac with none, to install the one lnk model recommend
picks. A local model needs no key: the harness talks to the runtime on
localhost, and nothing leaves the machine for the model. Behind its
proxy, a local model is reached for inference only: its admin API, which
pulls and deletes models, gets a 403.
On your machine
What keeps the machine up for an agent, and where Link keeps its things.
Keeping the machine up
What keeps your agent running while the machine is idle, or you've logged out.
On a Mac
The background service runs the harness under /usr/bin/caffeinate -i,
outside the sandbox. It only holds off idle sleep while the harness
runs; --let-sleep turns this off.
On Linux
lnk agent start turns on lingering for your user (loginctl enable-linger) so the agent keeps running after you log out, as a
server needs. Everything else of yours that runs as a systemd user
service does too. Where systemd doesn't let you, it prints the sudo
command instead.
Where Link keeps an agent's things
All out of every harness's reach but its own home, its agent's Home, and of its agent's own folder what's granted:
~/.lnk/teams/<team>.tomland~/Link/Teams/<team>/(700): a team's file, and what Link keeps for it, its shared folder (Shared) and its memory's index (Memory), reached only as each member is granted.~/Link/Agents/<agent>/Home: its files folder, unless--filesnamed another. Link Harness's sandbox reads it and writes only the folders the agent's sandbox file lets it ([files] write:work/by default); a harness from outside Link writes the whole folder.~/Link/Agents/<agent>/Agent(700): the agent's own folder, each of its collections kept atfilea folder of it:instructions/,skills/,chats/,memory/(its memory's index, 700) andschedule/(its scheduler's jobs, 700).- Link Harness's sandbox reads its instructions, skills and
memory/(for its notes), writes its conversations, and writes its instructions or skills only where[files] writenames them; it never writesmemory/or reachesschedule/, which its memory and scheduler, outside it, write. A harness from outside Link never reaches it. ~/Link/Agents/<agent>/Harnesses/<name>(700): a harness's home, with its install, memory and conversations. On a Mac, its.lnk/binholds Link'smktemp, first on the harness'sPATH.~/.lnk/harnesses/<harness>.toml(600): what a harness said it needs here and you allowed, for every agent that runs it, and the needs you answered.~/Link/Agents/<agent>/Config/sandbox.toml(600): what you grant the agent, whichever harness runs it.~/Link/Agents/<agent>/Config/agent.toml(600), in its folder, which none of its sandboxes reaches: its ID and name, the active harness, its ports, the model, the files folder, each connection ([connections.<name>]: its settings and your user id, its bot, and what its plugin said of it), and its memory engine. Its keys are with your secrets.~/.lnk/agents/<agent>/logs/<name>.log: its log.~/.lnk/agents/<agent>/bridge.json(600): for Link Harness, its endpoint, the key it takes and the connections Link carries;bridge.logbeside it, the carrying's log, which never holds a message.~/.lnk/agents/<agent>/connections/<connection>(700): for Link Harness, what links Signal (signal-cliand its account).~/Library/LaunchAgents/link.bridge.<agent id>.pliston macOS, orlnk-bridge-<agent id>.serviceon Linux: the service carrying its connections, stopped with the agent.~/Link/Agents/.harness/<owner>(700): the harness its owner's agents share, where the machine says so: what it serves each agent with, and its log.~/Link/Agents/.bridge(700): the bridge every agent here shares, where the machine says so: whom it carries (agents/<agent id>.json), its scheduler runners' agents, which agents are asleep (asleep/), and its log. An agent in a terminal that can sleep is told to at~/.lnk/agents/<agent>/sleep.sock(600).~/Link/Agents/.memory/<owner>(700): the memory service its owner's agents share: what each is served with, its log, and its working folder (run) with its socket.~/.lnk/agents/.keys/<agent id>.json(600, in a 700 folder): the agent's key for this start, bound while it runs, which every bridge checks a call's proof against..link-store.sock(600) in the harness's own folder: its store bridge, for an agent whose spec keeps its conversations with a driver. The harness writes that folder, so the socket is made in a folder of its own beside it and moved in place, its mode set there through that folder and never through a link: a link the harness puts in its place is replaced, and makes no file of yours anyone's.~/.lnk/agents/.net/<agent id>/<name>(700): the sockets between the harness's own network and Link's proxy (<name>-<step>, a setup step's), andurl.secretbeside them.~/.lnk/sandbox/<agent>-<name>*.sb: the macOS sandbox rules.~/Library/LaunchAgents/link.agent.<agent id>.pliston macOS, or the systemd user unitlnk-agent-<agent id>.serviceon Linux: one service per agent, replaced when it switches harness.~/Link/Agents/.moving/<agent id>(700): where a move's copy and switch arrive, withmove.json(its move and when its copy came), removed once the switch is put in place, when a switch fails, and 7 days after a copy no switch came for;~/Link/Agents/.sending, where a file over 8 MB, or memory's index, is copied aside, unnamed, to be hashed and sent as it was hashed.~/Link/Agents/.restoring-<random>, where a restore unpacks a backup, a folder made new for each restore, and removed a week after a restore cut short left it.~/.lnk/agents/<agent>/pausedandpaused.quiet: an agent paused for a move's switch, and what carries its connections saying it stopped taking messages. A start removes both.~/Link/History/Agents/: the own folders (Agent/) and Homes (Home/, where they were their own) of agents that moved away or were removed, for 30 days, or as long askeep_movedinmachine.tomlsays. Every sandbox hides it. Those an older Link kept in~/Link/Agents/.movedmove there at the firstlnk agentcommand.- An agent whose move to a box wasn't seen running there keeps its whole
folder there, its harnesses' homes among it, but not the keys a
harness keeps in them: each
.envat a home's top or in a folder at its top (.<harness>/.env) is deleted first, and the move says so. ~/.lnk/moved/<id>.toml: a note of where an agent that left went, and when.~/.lnk/machine.toml: this machine's ID and name,unsandboxed = falsewhere no agent may run without a sandbox, andshared_harness = truewhere an owner's agents share one harness (One harness for an owner's agents), andshared_bridge = truewhere every agent shares one bridge (One bridge for every agent).- A deployment may write the file with that line alone, before
lnkfirst runs: the ID and name are added to it, its other lines kept.
The layout before agents had names
The first run of lnk agent moves the agent from ~/Link/Agent to
main: its settings from harness.toml, removed after, its files, and
each harness's state (what its adapter's state names, never its
install or .env) into its new home. The old service is stopped and its
file removed before anything moves. The old installs stay in
~/Link/Agent/Harnesses (700) until you delete them, with the
harness's .env of keys (600); lnk agent harness delete --harness <harness>
deletes them with the rest.
Moving and backing up
What a move carries and checks, and what a backup and a restore keep.
Moving your agent
lnk agent move carries one agent at a time between this computer and
your boxes, or between boxes.
- What travels: the harness's state (its memory and conversations, and anything it keeps in its home but its install), the files folder, and Link's settings for it, including the model key, the Telegram bot token, the Slack app's and Discord bot's tokens, and its buckets' encryption password. The password is obscured, as rclone keeps it, which isn't encryption.
- On a harness Link carries connections for, Signal's linked device
travels too: what the connection's plugin names as its link
(
lnk-<connection> state), never a program it installed. - Where to: a box you name, or
here. A cloud account's name (lnk agent move aws) takes its one box without asking only when this computer made it. A box found in the account by its tags alone is yours only as far as anyone who can tag machines there says so: the move asks on a terminal, and without one refuses and says to name the box. - A box made elsewhere that is named like one of your cloud accounts is
asked about the same way. With
--image, the move always starts a new box from that image of yours in the account, which this computer makes; an image holds no agent, and nolnklogin (images you save). - How: one machine's
lnk agent state-sendpipes into the other'slnk agent receive, through your computer, over SSH with Link's key for a box (lnk box run), or into a cloud account's adapter when it hosts agents. Never through the relay, a bucket or a command line. A key given for the move (the provider's variable, or asked for) travels in the switch's first line. - Sealed on the way: the complete switch stream, including the harness's home, linked-device state, settings and secret values, is encrypted in records of at most 64 KiB with ChaCha20-Poly1305. HPKE seals a fresh stream key to the receiving machine's per-transfer key, or the hosting account's independent per-move receiver.
- Hosted receivers survive restart for at most 24 hours, with deletion requested after commit; a failed deletion remains bounded by that expiry. This computer passes the ciphertext through. Wrong recipients, modified or reordered records, interruption and a missing authenticated final record refuse the switch before activation.
- The first line's provider and bucket secrets are separately sealed to the same receiving machine. The preliminary copy carries only files and memory's index over SSH or the account's TLS; harness and linked connection state wait for the switch. A link is carried as a link, and a folder more than 32 deep refuses the move.
- The switch starts only after the agent is paused and stopped (
lnk agent pause). - Receiving: the stream is unpacked into
~/Link/Agents/.moving/<agent id>(700, out of every harness's reach), each file checked against its SHA-256, and no path leaves it:- an entry that isn't a plain path under the folder its phase allows
(
.., a leading/) stops the receive - an entry reaching out through a link unpacked before it, or a hard link to a file outside, stops the receive
- a stream of another format, from an older or newer
lnk, is refused before anything is put in place, and so is an agent whose name another agent there has, with a different ID - Only then is anything replaced: the state the receiving machine's
adapter names (
lnk-<harness> state), in the harness's home, and the files folder, where incoming files overwrite and nothing is deleted. What the stream says is state counts for nothing there, and nothing else that came for the home (an install, a.env) is put in place. - The state goes in as a restore's does, from folder handles (Backups
and restores), and so do the files: each
name is moved from the folder holding it, opened without following a
link, so a link a harness process that outlived
lnk agent stopswaps in for a folder there is replaced, never written through. - Links are carried as links, never followed on either side: a link in a files folder can't pull a file from outside it into the stream, nor send one out when received.
sendopens each name from the folder holding it without following a link, and reads a file from what it opened, so a link swapped in while it reads, by a harness process that outlivedlnk agent stop, goes as a link too, and one on the way to a state path stops the move.- A link is carried as it is, pointing wherever it pointed,
/etc/passwdor../..included: a program you run outside the sandbox that follows it reaches that target, on the machine it lands on. - the link rules above hold by path: a hard link is a regular file, sent like any other, so a hard link a harness makes in its home or files folder to a file it can already read is carried, by moves and backups alike.
- files and folders keep their permission bits only (
0o777): never setuid, setgid or sticky, whatever the stream says. - a linked connection's device goes to
~/.lnk/agents/<agent>/connections/<connection>(700), for a connection name Link knows only. What of it comes is what its plugin on the receiving machine names (lnk-<connection> state), never what the stream says. A path out of that folder refuses the whole stream, and links and anything else are dropped, since the plugin runs outside any sandbox. - each connection is what its plugin on the receiving machine says of
it (
lnk-<connection> describe): which fields are secret, whether it's linked, its title. A name that can't be a connection's, or that no plugin there describes, is left out, and the receive names it. Titles and hints are printed without control characters. - what the agent may do comes from the machine running the move,
never from the stream alone, which the machine it left wrote. An
agent leaving the computer you run
lnk agent moveon keeps its grants, which that computer sends in the first line. One coming back keeps the grants it has on the receiving machine. - Any other gets the stream's, but for
--read '~',--network-mode host,--lan-host,--portand--browser-host, which open the machine itself, and its endpoints (--tcp), tools (--tool) and commands named by a path or*(--command), which reach or run past its sandbox. - Nor its teams' shared folders (
[teams.<team>]), which only this machine's team files and grants give: the receive names each one left out as thelnk agent allowflags that give it (or, for a team's folder,lnk agent team add). - a model on the machine's own loopback (
ollama,local) is taken from the stream only when it comes from the computer running the move: a box could name any local port that answers, which the sandbox would open to the harness as its model's.
- an entry that isn't a plain path under the folder its phase allows
(
- The machine it left deletes its copy once the agent answers where
it went, running, with its ID: its settings (model key, bot token),
its harnesses' homes (state, install,
.env) and its service go. Its own folder and its Home are kept in~/Link/History/Agents(30 days, orkeep_moved), out of every harness's reach. - If it doesn't answer there, the machine it left keeps its whole copy, stopped, keys included, until a move completes.
- One place at a time: the machine it left keeps a note of where it
went, and
lnk agent startrefuses that agent's ID there, so two copies can't answer the same bot. A move between two boxes updates this computer's note. - One ID, one agent: a machine refuses an arriving agent whose ID another agent there has, since its services, sockets and tools are keyed by it.
- A box's word for an agent's ID is taken unless this computer's note says that agent is on another machine: then the move refuses, before stopping anything. So a box can't bring home an agent under another's ID, which would revoke the bucket key of the box that agent is on and end the note of where it is. An agent this computer never had is known only by what its box says.
- A box's backups: a box holds no cloud account of yours. For its
agent's buckets, your computer makes a key reaching only them: an AWS
IAM user
lnk-<bucket>whose inline policy allows that bucket's objects and listing it, nothing else, or a Google Cloud service account withroles/storage.objectAdminon that bucket alone. - It sends the key in the move's first line, sealed to the box, and
deletes the key the bucket had before. The box keeps it in its
rclone.conf(600). When the agent comes back to your computer, the key's user or service account is deleted; a move to another box replaces the key. - Someone who takes over a box gets that agent's bucket, still encrypted with the agent's password, which the box also holds.
- Making the key needs an account allowed to make IAM users or service accounts. Without one, the box has no backups, and the move says so.
Backups and restores
What a backup writes, and what a restore puts back.
- Backups:
lnk agent backupwrites the harness's state (the same paths a move sends, never its.envof keys, nor Link's settings) to.lnk-backup/<harness>.tarin the files folder (600). - It reads the state as a move's
senddoes, never through a symbolic link (a hard link is carried, as a move carries it), and writes the archive through the backup folder's handle: a link in the folder's place is removed first, and one swapped in after isn't written through. It thenlnk bucket pushes it and the folder to your clouds, encrypted if the agent's buckets are. - The harness's own config files go with it, and may hold what it keeps
there itself, such as a local gateway's token. Beside it,
.lnk-backup/Agent.tarholds the agent's instructions, skills and conversations from its own folder; never its memory's index or its scheduled jobs, so nothing restored from a backup acts by itself. - A full
lnk agent restoreof an agent not here unpacks it in a~/Link/Agents/.restoring-<random>too, and refuses it whole if it holds a link anywhere, symbolic or hard: it sits in Home, which the agent may write. Nothing of the Home that came is ever moved into its own folder. - Going back to a backup:
lnk agent restoreof an agent that's here pulls.lnk-backup/<harness>.tarfrom your clouds and unpacks it in a~/Link/Agents/.restoring-<random>, out of every agent's reach, the way a move's stream is unpacked, so no path leaves it. It moves only the harness's state paths into its home, never its install or.env. - The archive sits in the files folder, which the agent can write, but it only ever lands in the harness's own home, which the agent can write too.
- It's opened only once the agent is stopped, and through the backup folder's handle: the folder and the archive are each opened without following a link, and the archive refused unless it's a regular file, so a link put in the backup folder's place, to another agent's backup, or a pipe, is never opened.
- A harness process that outlived
lnk agent stopmay still change that home meanwhile, so the restore removes and moves each name from the folder holding it, opened without following a link: a link on the way to a state path stops it, and one in a state path's place is removed or replaced, never followed. - Restoring a lost agent:
lnk agent restore <name>finds an agent's buckets by their labels in yourlnk cloudaccounts, asks for the encryption password with echo off, and adopts the buckets with a key reaching only them, as a box does. The model key and bot token aren't in a bucket: they're asked again. - The harness's backup comes back the way going back to a backup does,
through a
~/Link/Agents/.restoring-<random>, with only its state paths. Its name picks the harness only among those there are here (Link's:link,openclaw,hermes,deepseek, and each added from outside Link), since the agent wrote that folder: a harness from outside Link is added first, then restored this way.
Gaps and limits
What the sandbox doesn't stop, and what Link supports.
Known gaps
A harness's sandbox has the sandbox's known
gaps,
and lnk agent harness check reports them as open. Which apply
depends on its grants:
- Behind its proxy (
[internet], the default), the host network's are closed. What stays: each of its own localhost ports that something serves, its gateway's among them, and on Linux a new.envrcin the files folder. - With
[network] mode = "host", the harness has the host network's: every localhost port and the machine's abstract unix sockets on Linux; on macOS every localhost port through::ffff:127.0.0.1, and the Mac's own address. - Without
[internet], it reaches its own localhost ports and those it's granted (not a local model's), and nothing else: on Linux through a relay into a network of its own, which nothing outside it reaches.
The sandbox stops a harness that connects to localhost or 127.0.0.1,
not one that means to get around it (Known
limitations).
- Standing instructions and skills, when the agent may write them.
By default the agent writes neither:
instructions/andskills/are yours, and it writes only its notes, throughremember, capped and headed. - With
instructionsorskillsunder[files] writein its sandbox file,file's andrun's calls write there with no question asked, so a page, a teammate's message or a tool's answer that steers the model can have it writeinstructions/instruction.mdor a new skill, which from the next message sits in the prompt of every conversation, yours included. - Its standing instructions are then headed as written by you or by it, and a skill it wrote is listed as any other.
- Processes on macOS. Every stop of a
runcall or a background command (its timeout,run__stop,lnk agent stop, a move, a delete) ends the process grouplnk sandbox runleads. - On Linux the sandbox ends everything else the call started; on macOS a
command that leaves the group (
setsid) outlives it, still in its sandbox but without its proxy, writing what the call could write and listening on its ports. A background command whose supervisor is killed has no timeout until the next sweep, and a sandboxed program's orphans outlive its proxy.
Limits
Which harnesses, connections and systems Link runs agents on.
- Harnesses: Link Harness; OpenClaw, Hermes Agent and DeepSeek Harness, which come with Link; and those from outside Link. Connections: Telegram, Slack, Discord and Signal: direct messages, and mentions in a group, from the allowed users only.
- Link Harness sends the model as much of a conversation as its window takes (32,768 tokens for a model Link doesn't know); older turns stay in the files only, where memory searches them.
- Link Harness's tools need a model that takes tools; with one that doesn't, each message fails while a tool is on.
- On Link Harness,
runruns a command to its end within its timeout, or, where it takes messages from others, in the background (run__start) until its own timeout orrun__stop; a program that needs a terminal or asks a question gets none. - On Link Harness, a voice note, video, sticker, or a photo or document
over 10 MiB, of another type or past 8, is named to the agent as left
out; an edited message isn't carried. Images and documents come only as
checked
data:URLs, downloaded from their connection's platform alone. - On Link Harness, a Discord message sent while its Gateway connection is down is missed when Discord won't resume the session: a short cut is resumed, and Discord sends what came meanwhile. Signal delivers what came meanwhile when it reconnects.
- On Link Harness, a Signal sender who hides their number gets no answer.
- Signal needs a number of the agent's own.
- Each agent runs in one place at a time; a machine runs any number.
- Behind its proxy a harness reaches the internet over TCP, through
HTTPS_PROXYorHTTP_PROXY: not UDP, and not what ignores them. Anything ignoring the proxy doesn't connect. - The exit from here carries TCP (web requests, APIs, Telegram), not UDP. It
needs this computer on, and a harness honoring
HTTPS_PROXY. It runs on Linux boxes; on a Mac, the agent's traffic leaves from the Mac anyway. - A backup holds one copy of the harness's state, the latest:
restoregoes back to it, not to an older one. - On Linux the sandbox needs bubblewrap; on Ubuntu 23.10 and newer, also an AppArmor profile.
Native adapter context
The launcher provisions storage for a harness with a restricted
collection layout; its local file collections stay in that layout.
Portable context's typed protocol can be used by a host supplying an
authenticated binding. An adapter contract has no storage opt-in.
Agent and owner binding, collection grants, CAS and leases retain
their checks. An unrestricted Home grants no write to Agent/ or the
protected memory index.
The notes grant
The notes grant permits portable documents under memory/portable/.
Notes keep the spec's byte limit and use bounded versioned writes.
Memory's own socket retains its separate index scope. Native-hook
bindings stay in private configuration, are regenerated each start and
are unbound at stop. Exported transcript roles, memory and provenance
confer no authority. The raw event log retains unknown extensions, while
context renders exposed common fields only.
The hook client
The optional hook client keeps exposed captures in private, synced retry
files under the adapter's declared .lnk/agent-context/ state. A lock
serializes clients. It replays pending captures before context
preparation and refuses corrupt, foreign or incomplete batches. The
queue permits at most 1,024 batches and 64 MiB, with 16 MiB per batch.
These are adapter-owned records, not host authority metadata. Portable
memory deletions retain their source at a new version with empty text.
Neither replay nor deletion changes collection grants.
Where it's defined
Portable context defines the additive marker, records, memory revisions, cursors and reference client.
The ledger
The host-only session/approval ledger described there keeps authority metadata outside runtime mounts and agent-writable collections. It checks the current start binding and attempt before claiming an approved call, and requires a fresh host-policy check. Approval does not expand grants. Claims are single-use and unresolved requests retain a recovery marker after explicit retirement/restart.
What it doesn't do
The ledger's caller authenticates owners and contains old processes before recovery. No existing adapter or tool proxy uses this ledger. It does not establish session isolation for arbitrary native code or enforce native process expiry.
Namespace actions
A namespace route carries the calling agent's binding proof. Its grant names the agent and exact action; the bridge checks both before reading context or forwarding a call. Context answers include only selected public fields or the agent's own notes. No input selects another agent, program, tool definition or credential. Web, scheduler and tool actions retain their providers' confinement, approval policies and caps. Namespace mutations are never broadcast or retried implicitly.
Owner messages
Owner messages and scheduled answers use the selected connection's recipient checks; a named service resolves only its configured owner chats. The host loads credentials only for delivery. Unknown connections, missing conversations and messages over 16 KiB are refused. CLI messages wait for the next read. Teammates retain their scheduler caps and approval policy; selecting a service grants no arbitrary recipients.
Child processes
The action bridge starts the namespace provider with Link's filtered environment, which removes credential variables. The provider reads its registered tool definitions and keeps their explicit environment and approval gates.