Get Started
How Link's code is laid out, what may depend on what, and where new code goes. Why it's built this way: Philosophy and Decisions.
The core and its plugins
The core architectural decision: lnk is a small core, and everything
Link does is a plugin, the way git runs git-<name>. It is also the
first thing a user meets: install.sh installs the core alone, and lnk up asks which plugins to add (docs/en/plugins).
- A plugin is its own program,
lnk-<name>, next tolnk;lnk <group> ...runs it. What a user installs is exactly what runs, and one plugin can't break another. - The core only installs, runs, updates and removes plugins. New behavior goes in a plugin, never the core.
- Plugins use each other only by running
lnk <command>and reading a documented output (docs/en/dev/contracts.md), never by linking each other's code or reading each other's files. - Each of Link's plugins ships as its own signed archive, at the core's version, and so do Link Harness, Link's tools (Link Memory) and the OpenClaw, Hermes and DeepSeek Harness adapters, built from their own repositories; a harness from outside Link ships from its own repository, signed by its own key.
- Code goes in the plugin whose purpose it serves, and a plugin's
purpose is its name. Before adding code to a plugin, ask: would I
still use this code without installing the plugin it serves? Does
this plugin have to know another's files, API or quirks? If either is
yes, it goes in the plugin it serves, or in a plugin of its own when
several need it and none owns it (the login is
accounts, not the tunnel's, because boxes need it too). The same holds forlink-pluginand the core. The test worked through: "Where code goes: the seam test" indocs/en/dev/plugins.md.
Layout
docs/ (the docs of the core and its plugins, in English in
docs/en/; the website, link-web beside this folder, renders them)
and src/ (a Cargo workspace,
Cargo.toml at the root), organized by where the code runs:
| Folder | Runs on | Ships as |
|---|---|---|
src/server/ | The public internet | link-relay, plus deploy scripts in src/server/deploy |
src/local/ | The user's machine | lnk (cli, the core) and its plugins (plugins/<name>) |
src/shared/ | Both sides | The protocol library, and release-signers: the keys that may sign a release |
src/demos/ | A developer's machine | Demo binaries, never shipped |
src/tests/ | CI | Nothing (e2e: end to end, architecture, licenses, docs; conformance: the kit every harness runs; fixtures/mini-harness: the harness the core's tests run on) |
Dependency rules
src/tests/e2e/tests/architecture.rs enforces these:
link-protocolandlink-plugindepend on no otherlink-*crate.link-relayandlink-agentdepend only onlink-protocol. The server and the local machine never depend on each other.- Plugins (
link-plugin-*) depend only onlink-pluginandlink-agent. They never depend on each other: one uses another by runninglnk <command>(link_plugin::lnk()) and reading a documented output, such aslnk model serve --json. link-cli(the core) depends only onlink-plugin; it runs plugins as programs and links none of them.- Demos depend on no
link-*crate. - The mini harness and the conformance kit depend only on
link-plugin.
A new crate goes in members of the root Cargo.toml, and in its
[workspace.dependencies] when another crate depends on it, and in allowed() in architecture.rs if it is a new kind.
Where does new code go?
| You are adding… | Put it in |
|---|---|
| A message or field on the agent↔relay wire | src/shared/protocol (see "Changing the protocol") |
| Public edge behavior: routing, auth, limits, domains | src/server/relay |
| Hosting the relay: TLS, Caddy, systemd, updates, backups | src/server/deploy. Scripts stay idempotent, pass shellcheck, and those run on a Mac work with bash 3.2 |
| Connection behavior: reconnects, multiplexing, several services per connection | src/local/agent |
Your login to the relay (lnk auth): logging in, its token, who you are | src/local/plugins/accounts. Other plugins ask lnk auth ... --json for it, never its file |
| Tunnel behavior on the user's side | src/local/plugins/tunnel |
| Finding, serving or managing models | src/local/plugins/models |
| Installing, configuring, running or switching harnesses | src/local/plugins/harness |
Where an agent's traffic leaves from (lnk agent exit), and the filtering proxy | src/local/plugins/harness (which upstream: settings.upstream, moving.rs), the sandbox plugin (proxy.rs: a sandbox in its own network, Link's proxy the only way out) and the vpn plugin (src/local/plugins/vpn: lnk vpn exit <machine> home, this computer as a machine's exit, held by the agents using it; for a box through lnk box forward, SSH home). The exit is always the user's own computer |
Moving an agent between machines (lnk agent move) | src/local/plugins/harness (moving.rs, transfer.rs), plus each harness adapter's state. One machine's lnk agent send piped into the other's receive, over SSH (lnk box run); never the relay |
| Support for one channel (Telegram, Slack, Discord, ...), where you talk to your agent | A new plugin src/local/plugins/<channel> answering connect, describe and rule in link_plugin::channel (making the bot and pairing its owner, saying what the harness keeps of it), plus PLUGINS (core) and each adapter that speaks it; the harness plugin names no channel (see "Adding a channel" in docs/en/dev/plugins.md) |
| Link Harness: its turns, its conversations, its window, calling tools | Link Harness's repository (link-harness), not this one, with its pages (link-harness/docs/en/harness). The core's tests run on the mini harness (src/tests/fixtures/mini-harness), and every harness passes the conformance kit (src/tests/conformance) |
| What the adapter contract asks of every harness | src/local/plugin (link_plugin::harness), answered first by the mini harness and checked by the conformance kit |
| A tool for agents (an MCP server any harness calls), or Link Memory: what it indexes, its search, its MCP tools | Link's tools repository (link-tools), not this one, with its pages (link-tools/docs/en/memory). lnk-link-memory serve <folder> is an MCP server over stdio; it reads only its folder, and never follows a link out of it |
| Support for one memory engine | A plugin of its own (Link's in link-tools) answering install and command in link_plugin::memory, plus MEMORIES (harness settings), PLUGINS and needs() (core) (see "Adding a memory engine" in docs/en/dev/plugins.md) |
| Support for one harness (OpenClaw, Hermes, ...) | Its own repository, not this one: an lnk-<name> answering the adapter commands in link_plugin::harness (with contract), released and signed with its own key, added with lnk plugin add <url> (see "Adding a harness" in docs/en/dev/plugins.md). It runs sandboxed with HOME = the harness's home folder: write everything under link_plugin::home_dir(). What's specific to it is its adapter's answer through the contract. OpenClaw, Hermes and DeepSeek Harness ship with Link: they're named in PLUGINS (core) and KNOWN (harness settings), and nowhere else under src/ (architecture.rs) |
What a sandbox can and can't reach: the Seatbelt and bubblewrap rules, permissions, the probes (lnk sandbox check), lnk sandbox run | src/local/plugins/sandbox; the policy and permissions a caller passes are link_plugin::sandbox. What one harness may do (its folders, ports, which permissions it has) stays in src/local/plugins/harness. Anything else that should run confined builds a Policy and calls link_plugin::sandbox::wrap |
Measuring agents and this machine (lnk measure): what's read from outside, the watcher, what it keeps, the export | src/local/plugins/measure. The measures and how they cross programs, OTLP, are link_plugin::measure; what a harness counts itself it appends to LNK_MEASURES (Link Harness's repository for its own) |
Files to the user's clouds (lnk bucket) | src/local/plugins/bucket. Cloud keys stay on the machine; files never pass through the relay |
Connecting cloud accounts (lnk cloud), and machines in them (lnk box) | src/local/plugins/cloud and src/local/plugins/box. Neither knows which cloud: that's the adapter. A box is a machine running lnk; what runs on it belongs to the plugin that runs it locally |
| Support for one cloud (AWS, Google Cloud, ...) | A new plugin src/local/plugins/<name> answering the adapter commands in link_plugin::cloud, plus KNOWN there, PLUGINS and needs() (core) (see "Adding a cloud" in docs/en/dev/plugins.md) |
A new part of Link (a new lnk group) | A new plugin src/local/plugins/<name> (see "Adding a plugin" in docs/en/dev/plugins.md) |
| A new kind of thing to expose (whisper, ...) | The owning plugin serves it on 127.0.0.1 via serve <target> --json, like lnk model serve; the tunnel runs that for lnk tunnel open <target> |
A new lnk command | The plugin owning its group plus its line in COMMANDS in src/local/cli/src/main.rs (a new group also in GROUPS) |
What lnk uninstall stops and deletes of a plugin | That plugin, answering uninstall (link_plugin::uninstall); the core only asks |
| Something every plugin needs | src/local/plugin, only if small and truly shared |
| A test spanning server + local + demo | src/tests/e2e/tests/ |
| Website | link-web, beside this folder, not this one. It talks to the relay only over HTTP |
Prefer a new plugin over growing the core or another plugin. Keep crates
small and names literal. Adding a plugin: "Adding a plugin" in
docs/en/dev/plugins.md.
Changing the protocol
Relays and installed CLIs upgrade at different times, so old peers must
keep working. Prefer additive #[serde(default)] fields (no version bump).
Anything breaking bumps link_protocol::VERSION, and the relay keeps
serving agents down to MIN_AGENT_VERSION. New behavior is
negotiated in Hello/Welcome, like flow_control. Details and tests:
docs/en/dev/protocol.md.
Principles
- Every engine is swappable behind a documented contract. A harness
is any
lnk-<name>answering the adapter commands, so closed code stays in its own repository, and Link never names it. - Link owns what stays the same when the harness changes: settings, the files folder, the channels, switching, and which machine runs it. A harness that speaks a channel keeps speaking it; for one that speaks none, Link carries the messages.
- Work runs where it belongs. A box routes, a model cloud thinks, a bucket remembers, a scheduler wakes. Nothing that belongs elsewhere runs on the box.
- Link's harness runs no code. Tools are MCP servers outside it, sandboxed, in any language, and shared by every harness. Keys never reach it: the proxy adds them.
- Turns hold no state between them. Any process on any machine can take the next turn.
- Conversations are append-only. An event log, never rewritten, and the prompt sent the same way so the provider's cache holds. One layout on disk and in a bucket, in plain files a person can read, behind a storage seam. Tokens are always recorded, and cost only from a real rate.
- Build the seams, not the implementations. An interface with one implementation; the others aren't built until something needs them.
- Written for the densest deployment, run at the safest. Code is written as if many agents share one process (the agent always an argument, every path from one function, a test that one agent can't reach another's), and runs one process per agent.
