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.

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 run tool, 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__remember only; 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, its config.json holds 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-cli over its stdin and stdout, never a local port.
  • An answer can't mention anyone: on Slack &, < and > are escaped, so no @connection or 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, and signal-cli itself. lnk agent disconnect deletes 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.json and the bridge in bridge.json, both mode 600, compared in constant time. While its config.json names 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's chats/. 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 in events.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 to https://, or http:// 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.md and memory/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 whose SKILL.md is, is left out, and so is a skills/ 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 a config.json over 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 run call) 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-Id and 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 send sends: the owner's own chat, or a group they talk to the agent in.
  • Held messages are kept until given in held.jsonl with 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 lnk midway, 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.

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, which lnk pinned when you added it (Plugins Security).
  • The adapter itself runs as you: lnk-agent runs its contract, connections, hosts, endpoint, needs, traces, written and state outside the sandbox. Its install, configure, link, command and health, 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 with HOME set 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's lnk-<name> traces runs 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 asks lnk sandbox wrap for 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/Agents but 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/Agents except 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 check tries reading what's hidden.
  • Each agent but main gets 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 proof gets 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 host or 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 telegram refuses 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 slack refuses 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:
  • run and file still 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.lock in 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|deny with a spec's flags (--internet-host, --read, --network-mode, ...), or a file with lnk 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 allow and deny change one it declares, and nothing else: a harness only ever holds what it asked for. Never no sandbox:
  • unsandboxed is 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 status does.

  • needs gets 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 only PATH, HOME, LANG and TMPDIR.

  • 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 needs there) 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 the lnk agent allow flags 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 from allow --tool, --sandbox-file or 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 configure reach 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's truststore does.

  • 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 its run'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/git runs through xcrun.

  • [browser] (off): opening an https:// 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 or lnk'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, /tmp or 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; no HOME, DYLD_* or LD_*.
  • Its own program, and an installed plugin's it runs (a store driver), are read wherever they are, ~/.lnk/bin too, 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 mktemp into the harness's home (~/.lnk/bin), from outside the sandbox, without following a link: a link in place of .lnk or bin stops the start, and one in place of mktemp is replaced, never written through.

Its log is out of its reach

Who writes a harness's log, and where.

  • launchd and systemd, or lnk for 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 ~/.zshrc and have its output written there.
  • lnk refuses a log that isn't a regular file.
  • lnk agent logs shows its last 200 lines without control characters (no escape sequences), from where the last start began at the earliest. lnk records that point; it isn't a marker the harness could print.
  • It reads at most the last 4 MiB, and --follow reads 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 telegram prints 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; a t.me link 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.test and apps.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 connect keeps: the app's signing secret and a configuration token (which can edit the user's Slack apps) are only for pushed messages, given to lnk-slack hook by 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 signal takes 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 signal stops 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 install and cli link run 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/data there, where the sandbox holds them and a move carries them, as the plugin says (lnk-signal cli state).
  • link refuses 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 .), never Link-From.
  • The answers in a link thread 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; --json writes the C1 controls and the bidi and zero-width marks as \u escapes.
  • -m puts 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 in lnk 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 by lnk agent connect outside 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 share runs 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 share writes 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 remove takes 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 in Link-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 reads Link-From; share refuses 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 owner says 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 no and 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_wait is 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-day of 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. A Link-From naming 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] strangers names, 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. strangers can't name the scheduler or run: 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 (_meta link.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 (_meta link.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 run hands 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.sock in 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 outliving lnk agent stop takes nothing more. A Slack app or Discord bot connected before Link kept that sends no such answer until lnk agent connect again.

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 answered 404.
  • 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 (400 or 401) and never reaches the agent, nor does one signed right that carries neither JSON nor text (415). The plugin is lnk-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 answered 503, 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] strangers gives, and its answer goes nowhere: the poster is told 202 once 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. add refuses 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 add or roll and never logged. roll makes a new one and keeps taking the previous one for 24 hours, its expiry beside it, or until roll --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 answered 202 with the question's id. A GET of /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.sock only its own agent's.
  • send.sock is 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 start binds it in the keyring (~/.lnk/agents/.keys, 700, which every sandbox hides), and lnk agent stop and remove unbind 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.sock 8, 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, busy or conflict) 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. With idle_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 only lnk agent team writes 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.message on each of them: the way in asks link-iam's allows whether 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_day a 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 no per_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 remember writes 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, in run's and file'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, as memory-<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, with lnk and every plugin), and writes only its folder in the agent's own (Agent/memory/<engine>) and its Agent/memory/, for the agent's notes, unless the agent's sandbox file locks its instructions (instructions under [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 memory collection 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 read serves 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 --instructions shows 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 stop leaves 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:// to http:// isn't followed. Only http:// and https:// are fetched, never with a login in the URL.
  • A host you name with --private-host is 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 run and file serve 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): run one 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] write names, so neither rewrites chats/, 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.
  • run starts 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's hosts, tcp and per_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.
  • file reaches 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; find walks the same way.
  • Where the files folder is kept in a bucket, file runs 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 (no teams/), and holds to [files] write as well as the tool does.
  • A replace, and a write of a file it read or wrote before, names the version it last saw and is refused if the file changed since; a write of 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 a run__command call, under a supervisor of its own outside it, for at most [tool.run] background_timeout, and at most background at 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>.out in 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] write names background, 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_keep after 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), beside background at once and the hops.
  • A background command is its conversation's: run__list, run__read and run__stop reach 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's file reads.
  • 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 by background_per_day alone.
  • With the agent's sandbox off, run and file aren'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 code serves 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 as run'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] write names, and starts only the programs of its [commands], the server among them.
  • A server runs the project's code (build scripts, macros), so code acts as the agent, as run does, 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, as run does.

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), as AGENT_<AGENT>_MODEL_KEY, AGENT_<AGENT>_TELEGRAM_BOT_TOKEN and 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 .env in its home holds them too where the harness needs it (mode 600). A key still in the agent's settings, or in the Keychain as agent/<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>, then lnk 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 .env stays a file.
  • For a harness Link carries connections for (Link Harness), the bot token never reaches the adapter, nor the harness's proxy: lnk-agent hands it to lnk-<connection> bridge on 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 .env gets 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 refuses setWebhook, logOut and close, 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 check reports LEAK if 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.

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>.toml and ~/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 --files named 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 at file a folder of it: instructions/, skills/, chats/, memory/ (its memory's index, 700) and schedule/ (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] write names them; it never writes memory/ or reaches schedule/, 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/bin holds Link's mktemp, first on the harness's PATH.
  • ~/.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.log beside it, the carrying's log, which never holds a message.
  • ~/.lnk/agents/<agent>/connections/<connection> (700): for Link Harness, what links Signal (signal-cli and its account).
  • ~/Library/LaunchAgents/link.bridge.<agent id>.plist on macOS, or lnk-bridge-<agent id>.service on 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), and url.secret beside them.
  • ~/.lnk/sandbox/<agent>-<name>*.sb: the macOS sandbox rules.
  • ~/Library/LaunchAgents/link.agent.<agent id>.plist on macOS, or the systemd user unit lnk-agent-<agent id>.service on Linux: one service per agent, replaced when it switches harness.
  • ~/Link/Agents/.moving/<agent id> (700): where a move's copy and switch arrive, with move.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>/paused and paused.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 as keep_moved in machine.toml says. Every sandbox hides it. Those an older Link kept in ~/Link/Agents/.moved move there at the first lnk agent command.
  • 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 .env at 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 = false where no agent may run without a sandbox, and shared_harness = true where an owner's agents share one harness (One harness for an owner's agents), and shared_bridge = true where every agent shares one bridge (One bridge for every agent).
  • A deployment may write the file with that line alone, before lnk first 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 no lnk login (images you save).
  • How: one machine's lnk agent state-send pipes into the other's lnk 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 stop swaps 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.
    • send opens 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 outlived lnk 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/passwd or ../.. 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 move on 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, --port and --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 the lnk agent allow flags 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.
  • 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, or keep_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 start refuses 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 with roles/storage.objectAdmin on 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 backup writes the harness's state (the same paths a move sends, never its .env of keys, nor Link's settings) to .lnk-backup/<harness>.tar in the files folder (600).
  • It reads the state as a move's send does, 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 then lnk 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.tar holds 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 restore of 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 restore of an agent that's here pulls .lnk-backup/<harness>.tar from 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 stop may 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 your lnk cloud accounts, 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 .envrc in 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/ and skills/ are yours, and it writes only its notes, through remember, capped and headed.
  • With instructions or skills under [files] write in its sandbox file, file's and run'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 write instructions/instruction.md or 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 run call or a background command (its timeout, run__stop, lnk agent stop, a move, a delete) ends the process group lnk sandbox run leads.
  • 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, run runs a command to its end within its timeout, or, where it takes messages from others, in the background (run__start) until its own timeout or run__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_PROXY or HTTP_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: restore goes 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.