Plugin Contracts
Every lnk command whose output another program reads, what it prints, and who reads it. Changing one is changing an interface: keep old fields, add new ones. How plugins use them: plugins.md.
Agents, measures and models
lnk agent
lnk agent list --json:{"agents": [...], "unreached": [{"id", "name", "error"}]}: every agent, each asstatus --jsonprints one: this machine's and each running box's (lnk box run <box> -- agent list --json --here);unreached, the boxes it couldn't ask (idtheir machine ID). An agent that moved to a box is listed as the box says, or, while the box doesn't answer, as the note here says (runningnull,locationthe box).--hereasks only this machine, and lists no notes. Each agent here also hasprocesses,{"pids", "group"}: the main processes it runs as now (its service's and its channels' service's, or its start in a terminal;[]when it's stopped) and, on Linux, its service's own control group (/sys/fs/cgroup/.../lnk-agent-<agent id>.service, elsenull); andmeasures, the file its harness appends its own measures to (LNK_MEASURES); bothnullfor an agent that moved away. Read bylnk agent liston another machine (--here),lnk agent connect <channel>(a bot or app another agent runs:bots, or from an older lnk, the channel's own field),lnk bucket(--here: each agent's files folder, by its ID, and whether it runs),lnk measure(--here: what to measure).lnk agent status --json:{"id", "name", "machine", "harness", "running", "files", "port", "bot", "slack", "discord", "whatsapp", "signal", "channels", "bots", "exit", "location", "processes", "measures", "wire", "permissions", "beyond_defaults"}, for the agent-anames (else the only one):permissions, what it may do on this machine while its harness runs it, the agent's own and what the harness needs (["network", "developer-tools", "lan"],unsandboxedamong them when it runs with no sandbox), andbeyond_defaults, of that, what Link's defaults don't give, aslnk agent denyorlnk agent <harness> denytakes it back (["lan", "tcp github.com:22", "tool gh"]; bothnullfor an agent that moved away); its ID and name, this machine's{"id", "name"}(machine.toml;nullif it can't be read or written), its base port (null: the adapter's own), its bot{"id", "username"}(never the token;nullwithout Telegram), its Slack app's bot{"id", "name", "team"}(its member id, never a token;nullwithout Slack), its Discord bot{"id", "name"}(its user id, never the token;nullwithout Discord), the channels connected (["discord", "slack", "telegram"], whether or not the harness speaks them), each one's bot inbots({"<channel>": {"id", "name", "team", "title"}}, never a token;bot,slack,discord,whatsappandsignalare the same for Link's own channels, as they were beforebots: WhatsApp's and Signal's{"id"}the agent's number), its exit ("home"ornull), the active harness, whether the agent runs (in the background or in a terminal), its files folder, ornullwhen that setting is onelnk agent startwouldn't accept as it is,locationnull, andwire, why its harness's wire is closed (every wire stopped, or its own paused, and why), elsenull; onlystatusgives it, notlist. For an agent that moved away, what its note says: its ID (nulluntil the box says it), name, the machine it went to,locationthat machine's name, andnullfor the rest. Exits nonzero only when the settings can't be read. Read bylnk agent move(whether the agent answers where it went).
lnk measure
lnk measure [<unit>] --json:{"at", "units": [{"id", "kind", "parent", "name", "measures": {"<measure>": {"now", "day"}}}]}: this machine, each agent here and each of its processes (kindmachine,agent,sandboxorprocess; a process'sid<agent id>/<pid>), or the unit named and its processes;atin Unix seconds. Each measure is one oflink_plugin::measure::MEASURES(measure.rs), its value now (nullfor what's counted as it happens: a harness's per turn, and tokens and model requests) and over the last day, as the watcher kept it (nullwithout); one with neither is left out. For one unit,"hours": [{"at", "measures": {"<measure>": <value>}}], the last day an hour at a time, those with anything kept. Read bylnk agent list(each agent'slink.memory.used.sumnow andlink.cost.sumover the day, by its ID) and Link Harness's density script.lnk measure start --quiet: The watcher runs in the background once it exits; nothing said, and nothing done where background services are off. Read bylnk agent start.
lnk model
lnk model list --json:[{"name", "base_url", "models": ["..."]}], one entry per runtime found, at a default port or an addresslnk model addremembers (namethen the one it was given); with--start, after starting an installed Ollama that isn't running. Read bylnk agent start(--start: offers the models here, Ollama's too when it was stopped).lnk model serve <target> --json: One line,{"url", "description"}, once it listens on a127.0.0.1port; it stops when its stdin closes. Exit status 3: no runtime here serves the model named. Read bylnk tunnel open <target>(exposes that port; on status 3 offerslnk model install), bylnk agent memory servefor a memory engine'smodel(passes<url>to the engine as$LNK_MEMORY_MODEL, empty on status 3 or when no line comes within 10 s), and by Link Memory with--embed <model>run outside a sandbox (calls<url>/v1/embeddingswhile it serves; on status 3 searches by words and sayslnk model install <model>).lnk model recommend --json:{"model", "size", "why"}, ornullwhere Link recommends no local model. Read bylnk agent start(offers to install it when no model runs here).lnk model install <model> --yes: Exit status 0 once the model is pulled and Ollama lists it. Read bylnk agent start(installs the local model picked).
Storage and accounts
lnk bucket
lnk bucket clouds --json:[{"name", "kind", "backend", "region", "buckets": {"personal" | "<agent id>": {"bucket", "encrypted", "remote", "files", "bytes", "agent"?: {"id", "name"}}}}], one entry per connected cloud; a folder without a bucket there is left out. Read by scripts only.lnk bucket tree [<path>] --json:{"personal": [...], "<agent id>": [...]}, one key per folder (each agent's by its ID) (only the folder holding<path>, when given); each row{"path", "size", "here", "clouds": [["<cloud>", "<mark>"], ...]},hereone ofhere,changed,new,offloaded, a mark one ofchecked,archived,unchecked,differs,missing. Read by scripts only.
lnk auth
The accounts plugin's: your login to the relay. No other plugin reads its file.
lnk auth relay [--relay <url>] --json:{"relay", "token"}: the relay to use (--relay, else the one logged in to, elsehttps://local.link) and the login token for it (nullwhen not logged in to that relay). It refuses to print to a terminal. Read bylnk tunnel openandstatus, which connect with it.lnk auth token --json:{"relay", "user", "token", "expires_at"}: a token for the relay's share API that expires in 10 minutes (or when the relay restarts), which the relay'sGET /_link/whoamialso takes, saying whose it is; fails when not logged in. Read bylnk bucket share,unshare,shares.lnk auth ticket --json:{"relay", "ticket", "expires_in"}: a confirmation given ahead by this machine, for one new login of its account on its relay, lastingexpires_inseconds (10 minutes); fails when not logged in, or the relay is older than tickets. Read bylnk box start, which hands it to the box'slnk auth loginasLNK_LOGIN_TICKET, so that login, approved in the browser, needs nolnk auth approve.lnk auth whoami --json:{"username", "relay", "user_id"}: who this machine is logged in as, and to which relay (usernameisnullwhen logged out), and the GitHub account's id (nulltoo for a login from before relays sent it). Read bylnk box start(installslnkon the box from that relay and logs it in there),lnk box(tags boxeslnk-owner=github-<user_id>, and lists them by it),lnk bucket(labels an agent's bucketslnk-owner, and finds them by it).
lnk cloud
lnk cloud list --json:[{"name", "kind", "place", "identity", "login"}], one entry per connected account; never its keys. Read bylnk agent move(whether where it goes is a cloud account).lnk cloud env <name> --json: One account aslink_plugin::cloud::Account:{"name", "kind", "place", "identity", "login", "region"?, "profile"?, "project"?, "env"},envholding what the cloud's CLI needs to act as it, keys included. Exits nonzero when there's no such account. Read bylnk boxandlnk bucket(every adapter call, which each then makes itself:link_plugin::cloud::call),lnk bucket create --cloudandconnect --cloud.
Boxes and the VPN
lnk box
lnk box forward <box> --socket <path> --to <local>:ssh -Rto a Unix socket at<path>in the box's home folder, until it drops: a SOCKS proxy there, the one on this machine's loopback at<local>. The socket's folder is made first, only the box user's, and a socket left there by a connection before removed.--port <p>instead: on127.0.0.1:<p>of the box. Without--toit refuses: ssh's own SOCKS, which reaches anything here, is never used. Read bylnk vpn exit <box> home(as its background service, underlnk sandbox socks).lnk sandbox socks -- <command>: while<command>runs, a SOCKS5 server (CONNECT to an address, no authentication) on this machine's loopback at a free port,{port}in the command, that reaches only the internet, never this machine or its networks. Exits with the command's code. Read bylnk vpn exit <machine> home(as its background service).lnk box run <box> -- <args>:lnk <args>on the box, its input and output passed through untouched, its exit status. Read bylnk agent moveandlnk agent new --on(readying the box first:plugin add harness,sandbox ready --yes,agent route direct --default),lnk agent status(when the agent is on a box),lnk agent list(a box without the harness plugin has no agents).lnk box start <account> --yes: Makes a box in the cloud account, without asking, talking on stdout; exit status 0 once it's up. It is then the newest (created) inaccountoflnk box list --json --here. With--image <name>, the box's disk starts from that image of yours there. Read bylnk agent move(to a cloud account with no box of yours, and with--imagealways).lnk box list --json:[{"name", "account", "kind", "place", "id", "machine_id", "size", "ip"?, "state", "created", "found", "stopped_by_cloud"}], one entry per box: those this computer knows and those each connected account has tagged as yours (created: when this computer made it, in Unix seconds, 0 for one found in a cloud;found:"here", or"cloud"for one found just now;idis the cloud's instance id,machine_idits machine ID, empty for a box from before IDs until Link reaches it;stopped_by_cloud: a spot box its cloud stopped, as its adapter says; from an adapter that can't say, a spot box this computer last saw running and found stopped, as another computer'slnk box stopleaves it too).--hereasks no cloud;--refreshasks each for each box's state. Read bylnk agent move(which boxes there are, and in whichaccount: a cloud account's name moves to its one box, unasked only when itscreatedisn't 0, made on this computer),lnk agent list(which to ask),lnk vpn exit(--here: whether a machine is a box).
lnk vpn
lnk vpn exit <machine> home|off [--for <id>] [--port]: Holds (home) or gives back (off) the machine's exit for<id>(defaultyou): a SOCKS proxy whose connections leave from here, running while anyone holds it, on a Unix socket at~/.config/lnk/vpn/exit.sockof the machine. With--port,<id>takes it on the machine's127.0.0.1:40780instead, and it's there while any holder asked so. Exit status 0 once done. Read bylnk agent exit,lnk agent move(for the agent's box, by the agent's ID;--portfor a box whose lnk takes no socket).lnk vpn list --json:[{"machine", "via", "running", "holders", "socket"}]: the exits this computer keeps (socket: on the socket, not the port). Read bylnk agent status(whether its box's exit is up).
Moving an agent
What moving an agent between machines uses (lnk agent move, restore); most are hidden from --help.
lnk agent send: The stopped agent as a tar stream on stdout:lnk-move-2.json({"format": 2, "version", "agent": {"id", "name"}, "harness", "model", "connected", "telegram"?, "bot"?, "slack"?, "slack_bot"?, "discord"?, "discord_bot"?, "whatsapp"?, "whatsapp_number"?, "signal"?, "sandbox"?, "spec"?, "exit", "state", "channels", "bucket"},sandboxwhat you granted the agent (its sandbox file, when it has one),specits own spec's text,connectedthe agent's channels as its settings keep them ({"<channel>": {"settings", "bot", "about"}}), and the per-channel fields the same for Link's own channels, for an olderreceive; a manifest withoutconnectedis read from them,bucketwhatlnk bucket exportsays, ornull), thenharness/...(the statelnk-<name> statenames) andfiles/...(the files folder), links as links. The manifest's name is its format's, so an olderreceivefinds none. Read bylnk agent move.lnk agent receive [--no-start]: Reads one JSON line ({"key"?, "upstream"?, "bucket_keys"?, "sandbox"?, "grants"?, "yes"?}: a model key, where its traffic goes there, for a box,lnk bucket keys' keys, and, when the agent leaves the machine running the move, what it may do there:sandboxthe agent's sandbox file as that machine has it, kept whole, andgrantsits harness's file (~/.config/lnk/harnesses/<harness>.toml), added to the harness's here; withoutsandbox, the agent keeps its file here, else takes the stream's withoutread-home,localhost,lan,open,tcportools;yes, what its harness asks for beyond the defaults is allowed) thensend's stream on stdin; puts the agent in place here, as the agent the stream names, its buckets with it (lnk bucket adopt), and starts it;movealways passes--no-start, then askslnk agent needsand starts it withlnk agent start. Refuses another format (lnk-move.jsonis the one before), an agent here of that name with another ID, and one of another name with its ID. Read bylnk agent move.lnk agent needs(hidden):{"harness", "needs": [[name, why]], "running": [agent]}: for the agent-anames, what its harness needs beyond Link's defaults (lnk-<harness> needs) and hasn't been answered on this machine, and the agents here running that harness.--allow <need>(repeated): grants those to the harness first, each one it asks for now, as answered; one it doesn't ask for refuses them all. Read bylnk agent move(where the agent came, before it starts there: asked in the move's terminal, granted with--allow).lnk agent left --to <machine> [--to-id <id>]: On the machine an agent left, once it answers on<machine>: deletes its settings, harnesses and service, keeps its files 30 days (ormachine.toml'skeep_moved), writes its note. Read bylnk agent move.lnk bucket export <id>(hidden):{"password", "buckets": {"<cloud>": "<bucket>"}}: agent<id>'s buckets here and their password, obscured as rclone keeps it;nullwithout any. Read bylnk agent send.lnk bucket adopt <id>(hidden): Reads{"password" | "plain", "buckets", "keys"?: {"<cloud>": {"bucket", "key"}}}; puts the agent's buckets in place here: its encryption on buckets this machine knew, buckets in clouds of the same name, and a cloud<cloud>-<id>for it alone from each key. Nonzero when none is reachable (a wrong password). Read bylnk agent receive, andlnk agent restorefrom beforebucket restore.lnk bucket forget <id>(hidden): The agent left: its encryption and clouds only for it go; its buckets stay known. Read bylnk agent left.lnk bucket keys <id>/revoke <id>(hidden):{"<cloud>": {"bucket", "key"}}: throughlnk cloud, a key reaching only each of the agent's buckets (found by itslnk-agentlabel), any before replaced; or those keys gone. Read bylnk agent move(to a box; back home).lnk bucket restore --name <name> [--id <id>] --into <folder>(hidden): Finds agent<name>'s buckets in yourlnk cloudaccounts by their labels (and yours,lnk-owner), asks their encryption password (on stderr, reading stdin), puts them in place here asadoptdoes, with a key reaching only them where a cloud makes one, and pulls the agent's files into<folder>; prints{"id", "buckets": {"<cloud>": "<bucket>"}}on stdout, and all else on stderr. Refuses a name agents of two IDs have, without--id. Read bylnk agent restore, the one bucket command it runs.lnk bucket find [--name] [--id](hidden):[{"cloud", "bucket", "place", "id", "name"}]: agents' buckets in yourlnk cloudaccounts, by their labels (and yours,lnk-owner). Read bylnk agent restorefrom beforebucket restore.lnk --version:lnk <version>on its first line, then where it runs from and any otherlnkon the PATH. Read bylnk agent move(throughlnk box run: a box older than this computer is upgraded withlnk upgrade <version>).lnk agent route <upstream|none> [--default](hidden): On the machine the agent runs on: its proxy's upstream (direct,socks5://127.0.0.1:<p>,socks5://<socket>, orhome: the home exit's socket in this machine's home folder, which an lnk from before it refuses), or none for no route of its own (direct, unless the harness haslan); restarts it if running. With--default, this machine's default (agent-defaults.toml): every agent made here from then on starts with it, and one here with no route yet takes it. Read bylnk agent exit(throughlnk box run;home, else the port for a box that refuses it),lnk agent moveandlnk agent new --onreadying a box (route direct --default).
lnk sandbox
A plugin confining a program builds a link_plugin::sandbox::Policy
and calls link_plugin::sandbox::wrap (lnk sandbox wrap, installing
the sandbox plugin first if it's missing), then runs the command it gets
with the environment it chooses. The sandbox decides only what the
program may touch; what runs, where and with which environment stays
the caller's. The contract's source is sandbox.rs. A sandbox's spec, link_plugin::sandbox::Spec (sandbox/spec.rs), is the type a plugin keeps one as; what it opens is the sandbox plugin's to say, by running it.
lnk sandbox wrap: Reads{"policy", "argv"}(link_plugin::sandbox::WrapRequest: the policy{"name", "write", "read", "write_dot_files"?, "hide"?, "tmp_prefix"?, "ports", "permissions", "proxy"?: {"upstream", "dir", "serve", "hosts"?, "tcp"?, "tools"?, "models"?, "model"?: {"provider", "key", "server_tools"?}, "listen"?, "per_minute"?, "secrets"?: [{"name", "value", "host", "paths": [{"prefix", "rest"}], "header"?: {"name", "value"}, "refuse"?, "methods"?, "public"?}], "secret"?, "lan"?, "no_internet"?, "browser"?}, "loopback"?, "terminal"?, "commands"?}) on stdin; prints{"argv", "env"}, the command runningargvconfined to it on this OS and what to add to its environment (with a proxy,HTTPS_PROXYand the rest, each oftools' address,LNK_TOOL_<NAME>, and each ofmodels' on the proxy,LNK_LOCAL_MODEL_<PORT>, and withopen,BROWSERand the verb's address,LNK_SANDBOX_OPEN; all three carrysecret, 16 to 64 letters and digits the proxy requires on its tool, local model andopenverbs, or one of the wrap's own), after writing its rules to~/.config/lnk/sandbox/<name>.sb(macOS) a hosted model's key to<name>.model.jsoneach ofsecretsto<name>.secret-<secret>.jsonandsecretto<name>.url-secretbeside them (the program reaches the model atlink_plugin::sandbox::model_urlwithplaceholder_key, a local model atlocal_model_url, and a secret's service atsecret_urlwithplaceholder_secret:link_plugin::sandbox::Secretsays where the proxy may send the secret and what it refuses; on macOS the proxy listens onlisten, else a free port). Withloopback(a folder), it reaches the policy'sportson this machine's loopback and nothing else, without a proxy (on Linux, relayed through sockets in that folder); refused besideproxy,network,localhost,lanoropen. Link's settings stay closed whatever the policy says;hidecloses folders the same way, but for whatwriteandreadname in them. At the top of each ofwrite, a project's dot files that run outside (.git,.envrc,.vscode,.idea) are read only, but in thosewrite_dot_filesnames. Read bylnk-harness(every step of a harness:install,configure,command,health, the harness).lnk sandbox traffic --json:[{"pid", "sandbox", "up", "down", "model"?}]: what each running sandbox's proxy (its process,pid) has carried since it started, every call it let through, in bytes from the sandbox (up) and to it (down); for a proxy that carries a model (a hosted one's key, a local runtime's port),model,link_plugin::measure::Tokens,{"requests", "unmetered", "input", "output", "cached", "written"}: the requests for a model's answer it carried that were answered, of them those whose answer gave no usage, and the tokens the rest's usage said (the prompt's, cached or not, Anthropic's three parts summed; the answer's; of the prompt, read from the provider's cache, and written to it). Read bylnk measure(an agent's network and tokens: its proxies' among its processes).lnk sandbox paused --json:link_plugin::sandbox::Wires,{"stopped", "paused": [{"sandbox", "why"}]}: whether every wire is stopped, and the sandboxes (<agent>-<harness>,guest-<name>,run-<pid>) whose wire is paused, and why. Read bylnk agent status(link_plugin::sandbox::wires).lnk sandbox on-stop <name> [-- <args>](hidden):lnk <args>runs whenlnk sandbox stopcloses the wires, for<name>, kept in~/.config/lnk/sandbox/on-stop/<name>.json; with no args, nothing does anymore. Neverlnk sandboxitself. Read bylnk agent start(agent-<agent>:agent stop -a <agent>, so the kill switch stops an agent withlantoo, which has no wire), and forgotten when the agent leaves.lnk sandbox ready [--yes]: Exit status 0 once the sandbox can run here; in a terminal, offers what it needs (Ubuntu's AppArmor profile), talking on stdout. With--yes, asks nothing and adds it wheresudo -nworks (installing bubblewrap with apt if it's missing). Read bylnk agent start,lnk agent <harness> check, andlnk agent moveandlnk agent new --onreadying a box (--yes, throughlnk box run).lnk sandbox tool list --json: One JSON object per tool that runs outside:{"name", "argv", "ask", "allow"}(ask:allornone;allow: the calls that don't ask). A proxied policy'stoolsnames some of them; its program finds each at$LNK_TOOL_<NAME>. Read bylnk agent allow tool, andlnk agent start(a tool removed since is left out).lnk sandbox check --policy: Reads aPolicyon stdin (a proxied one too), runs the probes inside it, prints a line per probe (blocked,allowed,failed,open,LEAK); exit status 1 when something leaked. Read bylnk agent <harness> check.
Adding and removing plugins
lnk plugin
The core's.
lnk plugin list --json: A JSON list oflink_plugin::plugins::Listed,{"name", "installed", "source"?, "key"?}: Link's plugins, then each added from outside Link with the repository it came from (https://github.com/<owner>/<repo>) and the key pinned for it (ssh-ed25519 <base64>). Read bylnk-harness(link_plugin::plugins::outside:lnk agent uselists the outside harnesses, a restore takes their backups, and a move adds the agent's on a box).lnk plugin add <url> --yes --key <key>: Adds the plugin from<url>'s latest release, trusting only<key>, without asking. Run bylnk agent moveandlnk agent new --onon a box (lnk box run), with the source and key this computer pinned.
Uninstalling (lnk-<plugin> uninstall)
lnk-<plugin> uninstall --parts --json:link_plugin::uninstall::Kept:{"parts": [{"name", "about", "paths", "keys", "first"?}], "stops"?, "stays"?}: what it keeps, by part (agents,personal,buckets,clouds,relay, or its own), each part's folders and files (absolute, inside~/.config/lnkor~/Link, there or not) and Keychain accounts (exact, or a prefix ending in/), how to keep a copy first; what uninstalling stops, and what stays the user's elsewhere. A plugin that doesn't answer keeps nothing the core knows of. Read bylnk uninstall(each installed plugin, parts of one name merged;restis what none claims) andlnk plugin remove(to say what stays).lnk-<plugin> uninstall [--delete <part>]...: Stops its background services, then deletes the parts named (link_plugin::uninstall::run); exit status 0 once done. Read bylnk uninstall(each installed plugin, before its program goes, with the parts picked) andlnk plugin remove(no part: only its services stop; on a failure it keeps the program and shows the plugin's stderr).
Adapters, engines and channels
Harness adapters (lnk-<name>)
What lnk-link-harness answers, and every harness adapter from outside
Link.
Versions. The core and an outside adapter release apart, so the
contract has a version (link_plugin::harness::VERSION, now 1), and
each side keeps working with the other's older ones. A change is
additive, as in the relay's protocol: a new command an older adapter
doesn't answer, with what lnk-harness assumes without it (each
command below says), or a new field with a default. Only a change an
older side can't follow bumps the version.
install, configure, command and health run with HOME set to
the harness's home, TMPDIR its tmp/, LNK_FILES the files folder
(and LINK_FILES, its old name, for older adapters), LNK_AGENT the agent's name, LNK_AGENT_ID its ID and LNK_OWNER
its owner (github-<user id>, or <user>@<machine> before you sign
in), LNK_PORT the agent's base port (not for main: the adapter's
default; its ports go from there, within as many as its contract's
ports and one more, or 200 when it doesn't say, and Launch.ports
outside that are refused), LNK_MEASURES a file in its home where the harness may
append its own measures, a line of OTLP JSON at a time
(link_plugin::measure::append: what it counts itself, a turn's tokens,
cost, calls and times; the rest is dropped as lnk measure reads it),
inside the sandbox (unless it's off); traces, channels, state, hosts,
needs, endpoint and contract run outside it, and only print. The contract's source is harness.rs.
lnk-<name> contract:link_plugin::harness::Contractas JSON:{"version", "proxy", "proof"?, "agent"?, "ports"?, "instructions"?, "from"?, "recover"?, "tools"?, "folders"?, "prompt"?}, the version of this contract it speaks (1:contractandLaunch.wait_secs;needs,proofandagentcame after, without a new version), whether its harness reaches a hosted model atmodel.base_url(Link's proxy, which then holds the key: keys on the wire by default), whether itsendpointproves it's the harness before it's sent the key (proof, below; left out, no), whether itsendpointreads which agent a request is for (agent, below; left out, no), and how many ports fromLNK_PORTits harness takes (ports; Link gives the agent that many and one more, for its proxy on a Mac; left out, 200), whether it takes the agent spec's[instructions](instructions; left out, no: a spec setting them is refused for it), whether itsendpointreads who a message is from (from, below; left out, no: it's never sent a message from anyone but the owner, and gets no scheduler), whether itsendpointhands over the answers of turns it finished after a crash (recover, below; left out, no: they stay in the conversation), whether itsendpointkeeps a conversation's tools (tools, below; left out, no: a subagent has all its parent's tools, and the bridge refuses to send a list), whether it keeps to the files folder's layout (folders: it writes onlyconversations/there itself, so its sandbox reads the files folder and writes onlyconversations/and the folders the agent's sandbox file's[files] writenames; left out, no: its sandbox writes the whole folder, and a start says[files]holds only for Link's tools), and whether it answersinstructions(prompt, below; left out, no:lnk agent instructionssays it can't). An adapter without it speaks version 0 and answers no, but for the three harnesses Link's releases once carried (lnk-openclaw,lnk-hermes,lnk-deepseek), which keep their keys on the wire as they did; a version 0 adapter also gets 180 s to answer after a start, andlnk agent startsays it gets no updates. Read bylnk-harness(the default oflnk agent keys) andlnk plugin add <url>, which installs only an adapter answering a version of at least 1.lnk-<name> install: Installs the harness with its own installer, or does nothing; asks nothing. Says so first when it installs, and shows progress. Read bylnk-harness.lnk-<name> configure: Readslink_plugin::harness::Settingsas JSON on stdin ({"files", "model": {"provider", "api_key", "name"?, "base_url"?}, "<channel>"?: {...}, "endpoint_key"?, "memory"?, "tools"?, "allowed"?, "unsandboxed"?, "proxied"?, "spec"?, "defaults"?, "model_facts"?, "environment"?}) and writes the harness's own config, leaving out a channel it doesn't speak. Each connected channel's section is under its name, as its plugin'sconnectmade it; Link's own:"telegram": {"bot_token", "allowed_users", "api_root"?},"slack": {"bot_token", "app_token", "allowed_users", "dm_channel"?},"discord": {"bot_token", "allowed_users", "dm_channel"?},"whatsapp": {"allowed_users", "self_chat"}and"signal": {"account", "allowed_users", "program"?}(typed inlink_plugin::harness;programthesignal-clilnk-signal cli installput in the home); a channel from outside Link is inSettings::channels, and no channel is namedfiles,model,endpoint_key,memory,tools,allowed,unsandboxed,proxied,spec,defaults,model_factsorenvironment.endpoint_key, for an adapter with anendpoint: what its endpoint takes (Authorization: Bearer <key>), refusing anything else; a channel Link carries for it is left out of the settings.telegram.api_root, with keys on the wire: where it must reach Telegram's Bot API, its proxy, which adds the real token tobot_token's placeholder (lnk-telegram rule --json).memory: the address of the agent's memory, an MCP server over HTTP through the harness's proxy (lnk agent memory), there only while the agent has one and its harness runs behind its proxy.tools: each tool that runs outside it may call (lnk agent allow tool), by name, at its address on its proxy (an MCP server over streamable HTTP). The adapter adds each oftools, andmemoryasmemory, to its harness's MCP servers (Settings::mcp_servers), and removes those it added before that are gone (link_plugin::harness::tools_gone).allowed: the risks itsneedsnamed that the user allowed, by name; the adapter leaves out what any other would open, or fails, saying how to allow it, when the harness can't run without it.unsandboxed:truewhen it runs with no sandbox at all, its ports on the machine's loopback.proxied:truewhen it runs behind its proxy (sandboxed, withnetworkbut notlan, or routed through an exit): on Linux its ports other than those its proxy serves (Launch.serve) then stay in the sandbox's own network; left out, they're on the machine's loopback, as they always are on macOS, and an adapter names a risk such a port opens. Link before it never sends it, so its absence is no proxy; an adapter from before it reads it as a channel it doesn't speak.spec: the agent's opinions, its spec as it runs it (Link's, its teams' and its own), which a harness takes in place of any of its own;model.nameis already its spec's default, and one itsallowlists.defaults: the keys ofspecthat are Link's, set by no team and not by the user (["window.max", ...]): a harness holds a default of Link's to what the model takes, and takes a key the user set as set.model_facts: what Link and the user know ofmodel.name(link_plugin::models::Known,{"name", "window"?, "answer"?, "thinking"?, "price"?: {"input", "output", "cached"?}, "yours"?}: its whole window and longest answer in tokens, how it's asked to think,adaptive,budget,effortorno, its price in dollars a million tokens, and which of these the user's~/.config/lnk/model-facts.tomlset), left out for a model neither knows (lnk model facts).environment: what the agent runs with here, as Link resolved it (link_plugin::harness::Environment,{"channels"?, "folders": {"write", "read"}}): the channels it's reached on, by their plugins' names, and its sandbox file's[files], the folders of the files folder it may write and those it may only read (link_plugin::harness::layout:instructions/,skills/,conversations/,work/), for a harness to tell its model. Prints nothing unless it fails. Read bylnk-harness.lnk-<name> instructions [--json]: For an adapter whose contract saysprompt: prints the system prompt its harness sends its model now, exactly, from whatconfigurelast kept and the files folder as it is; with--json,{"system", "sections": [{"name", "owner", "changes", "from", "bytes", "max", "cut"?}]}, each section of it in order. Runs in its home, the files folder read, nothing else of the agent's, with no network. Read bylnk agent instructions.lnk-<name> channels: A JSON list of the channels its harness speaks, by their plugins' names (["telegram", "slack", "discord", "whatsapp", "signal"]); an adapter without it speaks Telegram. Read bylnk agent connect(refuses one the harness lacks),lnk agent use,lnk agent status.lnk-<name> command:Launchas JSON:{"argv", "env", "write", "read", "ports", "serve"?, "services", "wait_secs"?}, how to run it in the foreground;lnk-harnessrefuses one that would widen the sandbox.serve: ofports, those Link reaches from outside the sandbox (its gateway or endpoint, its health check), the only ones served on this machine's loopback behind its proxy; left out, all ofports.wait_secs: how long it may take to answerhealthafter it starts, at most 600; left out, 60 (180 for a version 0 adapter). Read bylnk-harness.lnk-<name> health: Exits 0 when the running harness answers, reaching it on its ports on this machine's loopback, the only network it has in the sandbox. Read bylnk-harness(lnk agent status).lnk-<name> hosts: A JSON list of the hosts itsinstallandconfigurereach, aslnk agent allow hosttakes them (["registry.npmjs.org", "*.githubusercontent.com"]). With the sandbox andnetwork, every step runs behind a proxy of its own, direct, and those two reach only these hosts; an adapter without the command lets them reach any host on the internet.linkreaches what the agent may while it runs (lnk agent allow host,tcp; none listed: any host), andhealthonly the harness's ports on this machine's loopback. Read bylnk-harness.lnk-<name> needs: Readslink_plugin::harness::Settingson stdin asconfiguredoes, but with no key or token (the model'sapi_keyand each channel's secrets empty), and printslink_plugin::harness::Needsas JSON:{"permissions"?: [{"name", "why"}], "unsandboxed"?: {"why"}, "risks"?: [{"name", "why", "without"?}]}, what its harness needs beyond Link's defaults (network,developer-tools) with those settings (unsandboxedamong them:truewhen the user runs it with no sandbox; andproxied, with the permissions it has so far, so a harness that asks forlanitself counts it), each with why in its own words (one line, at most 200 characters).permissionsnames permissions (lan,localhost,read-home,open, ...); a name Link doesn't know, or one named twice, is refused, and the harness doesn't start.unsandboxedis only for a harness that can't run in any sandbox.risksnames what running it risks that no permission does (a port of its own that asks for no credential), each by a name of its own: lowercase letters, digits and dashes, at most 32, and nonelnk agent allowordenytakes for anything else; one withwithout(one line: what it does without it) runs either way, and only a yes in a terminal allows it; one withoutwithoutis asked as a permission is.configuregets those allowed inallowed.lnk agent use, a start andlnk agent moveask the user for each not answered yet, and grant what they allow to the harness (~/.config/lnk/harnesses/<name>.toml), held only while it runs, remembering the answer; a permission the agent has of its own (lnk agent allow) is had, and not asked. An adapter without the command, whatever version it speaks, needs nothing more: one that exits with an error (its answer to a command it doesn't know) or prints nothing. An answer that isn'tNeedsis refused. Read bylnk-harness.lnk-<name> endpoint: For a harness that speaks no channel itself: a JSON string, the address of its chat completions endpoint on this machine's loopback ("http://127.0.0.1:18800/v1/chat/completions"), at one of itsLaunch.ports,LNK_PORTtaken into account. Link then carries the channels whose plugins carry messages (describe'sbridged: all five of Link's) for it: each message from the owner is POSTed there alone,{"model", "messages": [{"role": "user", "content"}], "stream": true}, never the thread's history, withAuthorization: Bearer <endpoint_key>,Link-Thread: <channel>:<chat id>(telegram:123456789,slack:D0123ABCDEF,discord:<channel id>,whatsapp:15551234567,signal:15551234567) andLink-Agent: <agent>(main), withLink-Agent-Id: <its ID>when it has one; the answer is read as chat completions, streamed (text/event-stream) or whole.modelis the agent's model's name. With the contract'sproof, each message goes on a connection of its own that first proves it's the harness:GET /_link/proofat the endpoint's host and port, withLink-Challenge: <32 to 128 hex characters>, answered 200 withLink-Proof: <HMAC-SHA256 of "link endpoint proof", a zero byte and the challenge, keyed with endpoint_key, in hex>(link_plugin::harness::endpoint_proof); a connection that doesn't is sent neither the key nor the message. An endpoint that answers the proof setsTCP_NODELAYon the connections it accepts: each message is a new connection, and without it a short answer waits about 40 ms for the channel's delayed ACK. An answer, streamed or whole, and a line of a stream, are read up to 16 MiB; past that the owner is told the agent failed. Withoutproof, the key and the message go to whatever answers on the port. The proof's request carriesLink-Agenttoo. With the contract'sagent, the endpoint answers a request as the agentLink-Agentnames, with that agent's key, and one naming none as its own agent's (LNK_AGENT); one naming an agent it doesn't run is refused (404) before the model sees it, so one process could answer for many agents. Names are unique only to an owner: an endpoint answering for more than one owner's agents finds the agent byLink-Agent-Id, refuses one whose ID and name disagree (404), and one naming by name alone an agent two owners share (409). Withoutagent, every request is its own agent's. With the contract'sfrom, a message from someone who isn't the owner carriesLink-From: <sender>(schedule:<id>,task:<id>oragent:<id>,link_plugin::channel::FROM_HEADER,is_from) andLink-Hops: <n>, how many messages, each sent by an agent on its own, led to it; the endpoint records who it's from, tells the model who's speaking (never as the owner), refuses aLink-Fromnaming no such sender (400), and passes the hops on to its tools. Its harness's environment then holds$LNK_SCHEDULER(link_plugin::memory::SCHEDULER_ENV), the agent's scheduler, an MCP server over HTTP through its proxy, beside$LNK_MEMORY; a call to it carries_meta{"link.local/thread", "link.local/hops"}, the conversation it's made in and the hops of the message being answered. With the contract'stools, a message may carryLink-Tools: <names>(link_plugin::channel::TOOLS_HEADER,tools_header,tools_of): the tools its conversation may use from then on, by the names the harness offers them (memory__search), comma-separated, at most 256, each 1 to 128 letters, digits,_,-and., or empty for none. The endpoint keeps the list with the conversation and offers it only those of its tools from then on; a later list narrows it to the tools in both, never widens it; a message without one keeps it; one that isn't a list is refused (400) before the model sees it. A call to its tools then carries_meta"link.local/tools"too, the names of the tools the conversation has this turn, which the scheduler reads so a subagent gets its parent's tools or fewer. Link sends it only with a message from others whoseFiringhastools(a subagent's task). with202andLink-Steered: 1and no body (link_plugin::channel::STEERED_HEADER,channel::ask_for'sAsked::Steered): the answer on the request that started that turn answers it too, and the bridge marks the message as its spec'ssteeredsays, or not at all. With the contract'srecover,POST /_link/recoverat the endpoint's host and port (link_plugin::channel::RECOVER_PATH), with the sameLink-Agent,Link-Agent-Idand key as a message, answers{"answers": [{"thread", "text"}]}: each answer of a turn it finished after a crash, which nobody waited for, handed over once (channel::recovered); a request without the key is refused (401). Its body empty or{}, that is all. With{"two_phase": true}, each answer also has an"id"(an opaque string) and is kept, handed over again at each ask, until aPOSTwith{"done": ["<id>", ...]}, sent once it went to the chat, lets those go; that one answers{"answers": []}, and an id that isn't one is passed over. The harness finishes the crashed turns in a task of its own, so a bridge that stops waiting leaves none half done. The agent's bridge asks as it starts and every[turns] lapseafter, and sends each to the chat its thread is. An adapter without the command speaks its own channels. Read bylnk-harness(lnk agent start,connect,use,status).lnk-<name> state:link_plugin::harness::Stateas JSON:{"include", "exclude"}, paths~/...in its home that hold the agent, and those under them that are its install (programs, runtimes, logs, caches,.env). Read bylnk agent send.lnk-<name> link <channel> [--again]: For a linked channel (describe'slinked: WhatsApp, Signal), whose account the harness links itself: links it in the home, its QR code on the terminal, once configured. Exits 0 at once when it's linked (--again: links it anew), 3 when it isn't and there's no terminal. Read bylnk agent connect(--again),lnk agent startanduse(before starting the harness).lnk-<name> traces: A JSON list of paths (~/...or absolute,{uid}for the user's id) the harness writes when it isn't in its home. Read bylnk agent <harness> show,lnk agent <harness> remove.
Adapters from outside Link
Any lnk-<name> answering the commands above is a harness: lnk agent use <name> runs it like Link Harness. A harness Link doesn't write
lives in its own repository, and releases and signs itself:
- Releasing: a GitHub release holding
lnk-<name>-<target>.tar.gzfor each target it runs on (aarch64-apple-darwin,x86_64-apple-darwin,x86_64-unknown-linux-musl,aarch64-unknown-linux-musl;lnkon a machine whose target it lacks refuses it, and so does a box of that processor), each withlnk-<name>alone; aSHA256SUMSof them, signed with the repository's own Ed25519 key (ssh-keygen -Y sign -n link-release -f <key> SHA256SUMS, givingSHA256SUMS.sig); and the key's public half asrelease-key.pub.lnk-<name> --versionprintslnk-<name> <version>. - Installing:
lnk plugin add https://github.com/<owner>/<repo>takes the latest release, shows the key and asks, then pins both (Plugins). It installs only an adapter answeringcontractwith a version. - Updates:
lnk upgradetakes each one's latest release, signed by the pinned key, when it's newer. - Listing and boxes:
lnk agent uselists it with where it came from (lnk plugin list --json), andlnk agent moveadds it on the box from the same repository, with the same key. - Without a release, a
lnk-<name>you put on the PATH yourself works too, by name, unsigned, and yours to update and put on a box.
The smallest one answers contract, command (its Launch), health,
needs and endpoint, and serves chat completions at that address: Link
carries Telegram for it, and the sandbox, the model's key on the wire,
moving and switching work as for any harness.
Memory engines (lnk-<engine>)
The contract is memory.rs.
lnk agent memory use <engine> runs lnk-<plugin> (MEMORIES in the
harness plugin maps link to link-memory; any other name is its own
plugin, from Link or not).
lnk-<engine> install: Installs the engine, or leaves it; no questions. Run confined with HOME<state>, which it may write, and the network.lnk-<engine> command <folder> <state>: Printslink_plugin::memory::Serve,{"argv", "allow"?, "models"?, "model"?}: the command serving MCP over stdio for<folder>, keeping what it keeps in<state>; the calls that only read, which don't ask (allow); whether it reaches this machine's model runtimes (models, which gives its sandboxnetwork); and an embedding model to serve for it (model:lnk agent memory serverunslnk model serve <model> --jsonoutside its sandbox with each session, and gives the engine its URL as$LNK_MEMORY_MODEL, empty when no runtime here has it or it doesn't say where it listens within 10 s, so the engine starts well within a harness's wait for its tool). Every command of the engine's gets the agent's opinions for it as$LNK_MEMORY_SPEC: its spec's[tool.memory]as JSON,modelthe embedding model to rank by (none: by words only, nomodelprinted) andtokensthe most a search gives; an engine refuses one that isn't valid. Read bylnk-harness, which defines it as the agent's toolmemory-<agent id>at every start, run bylnk sandbox runreading<folder>and the program itself and writing<state>, and gives the harness its address as$LNK_MEMORYand, to its adapter'sconfigure, asmemory.
Channel plugins (lnk-<channel>)
The contract is channel.rs.
lnk-<channel> connect --agent <name> [--taken <json>]: Asks for the channel's token (itsLNK_<CHANNEL>_...variables, else on the terminal, questions on stderr), refuses a bot in--taken([{"id", "agent", "machine"?}], the bots other agents have, here and running on boxes), pairs whoever sends the bot its one-time code, and printslink_plugin::channel::Connectedon stdout:{"settings", "bot": {"id", "name", "team"?}},settingsthe channel's section oflink_plugin::harness::Settings,bot.idwhat another agent'sconnectrefuses (a Telegram bot's id, a Slack or Discord bot's user id, the agent's WhatsApp or Signal number). Read bylnk agent connect(which stops the agent meanwhile, keeps the settings and starts it again).lnk-<channel> describe --json: Printslink_plugin::channel::Describe:{"title", "secret_fields"?, "linked", "bridged", "cli"?, "hosts"?, "disconnect_hint"?}: its name as people write it, the fields of its settings a Mac keeps in the Keychain (asagent/<agent>/<channel>-<field>,_as-; Telegram'sbot_tokenasbot-token), whether its account is linked as a device (link,state), whether it carries messages (bridge), whether it installs a program for a harness speaking it (cli), the hosts a harness speaking it reaches, and whatlnk agent disconnectsays stays the user's. Read bylnk agent connect, which keeps it with the channel, so a plugin from outside Link needs nothing of Link's butPLUGINSto plug in.lnk-<channel> cli install --home <dir> --json: A channel whosedescribesayscli(lnk-signal) only. Reads its section of the settings on stdin (Signal's{"account", "allowed_users"}), installs the program a harness speaking it runs into that harness's home<dir>, and prints{"program"}, its path (<dir>/.signal-cli/signal-cli). Read bylnk-harness, which runs it in the harness's sandbox, reaching the adapter's hosts, beforeconfigure, and hands the adapter the path in the section ("signal": {..., "program"}).lnk-<channel> cli link --home <dir> [--again]: The same channels only. Links the account in<dir>with that program, its QR code on the terminal; exits 0 once linked (--again: anew), 3 when it isn't and there's no terminal. Read bylnk-harnessin place of the adapter'slinkfor such a channel, in the harness's sandbox.lnk-<channel> rule --json: Printslink_plugin::channel::Rule, ornullwhen the harness holds the channel's secret itself:{"field", "root", "host", "paths": [{"prefix", "rest"}], "header"?, "refuse"?, "methods"?, "public"?}.fieldis the field of the channel's settings holding the secret, androotthe field a harness reads the service's address from; the rest is the proxy's rule for the secret (link_plugin::sandbox::SecretRule:{secret}where the secret goes in a path'sprefix, else inheader;restnameorpath;refusepaths compared without case;publicthe separator ending the secret's part that isn't secret). Read bylnk-harnesswith keys on the wire (lnk agent keys wire): the harness is handed a placeholder infieldand its proxy'ssecret_urlinroot, and the proxy the secret. A plugin that can't answer keeps the agent from starting, so its secret never reaches the harness.lnk-<channel> bridge: A channel whosedescribesaysbridgedonly. Readslink_plugin::channel::Bridgeas one JSON line on stdin ({"agent", "agent_id"?, "endpoint", "key", "proof"?, "model", "settings", "state"},agent_idsent asLink-Agent-Id,specthe agent's opinions for the channel,{"working"?, "stream"?, "pace"?, "steered"?}(its spec's[channel.<name>]over[channels]): a reaction while it answers, whether the answer is shown as it's written, and how often, never more often than the platform documents a bot may write (Telegram: a second in a chat, 3 in a group; Slack: 1.2 seconds; Discord documents none, so the bridge follows the rate limits it's answered with); left out, the bridge's own way,proofwhether the endpoint proves it's the harness first,settingsthe channel's section,statea folder for this channel alone, mode 700, which no harness reaches), then carries messages until stdin closes: each direct message from an allowed user goes to the harness'sendpointas above (link_plugin::channel::ask, which asks for the proof), and the answer back. Read bylnk-harness(lnk agent bridge, andlnk agent start --foreground), for a harness with anendpoint.lnk-<channel> send: A channel that can send a message nobody asked for (lnk-telegram,lnk-slack,lnk-discord). Readslink_plugin::channel::Sendingas one JSON line on stdin ({"bridge", "chat", "text"}: the sameBridge, the chat as its thread names it after the channel,telegram:42's42) and sendstextthere: only to a chat of the owner's alone (Telegram: an allowed user's own chat; Slack: the owner's direct conversation with the app,dm_channel; Discord: the owner's direct messages,dm_channel; an app or bot connected before Link kept it sends nowhere until connected again), never a group's, and exits nonzero when it can't. Read by the bridge's way in (below), for an answer to a schedule or a subagent.- The bridge's way in: for a harness whose contract says
from,lnk agent bridgelistens at~/.config/lnk/agents/<agent>/way-in.sock(600,link_plugin::channel::way_in) for messages from others, one JSON line each (link_plugin::channel::Firing,{"thread", "from", "text", "answer", "hops", "tools"?},answerone of"chat",{"thread": "<name>"}or"nowhere"). It sends each to the harness's endpoint withLink-FromandLink-Hops, andLink-Toolswhen it hastools(refused, unsent, for a harness whose contract doesn't saytools), then the answer where it goes: the chat its thread is (lnk-<channel> send), another conversation as a message from the same sender, one hop further (a subagent's answer, to its parent's), or nowhere; and answers with one line,link_plugin::channel::Fired({"answer", "error"?}), once it went. It runs the agent's scheduler's runner (lnk-link-scheduler run) beside its channels, again each time it stops. The scheduler's tool isscheduler-<agent id>, its runner and tool given the agent's spec's[tool.scheduler]as flags (--per-day,--subagents,--hops,--late,--zone, and the runner's--timeout, fromfiring); withask, itstask,scheduleandcancelaren't allowed when it's defined, so each asks first (lnk sandbox asks). lnk-<channel> link [--again]: A channel whosedescribesayslinked(lnk-whatsapp,lnk-signal) only. Reads the sameBridgeon stdin and links the channel's account as a device into itsstate, from a QR code on stdout. Exits 0 at once when it's linked already (--again: links it anew), and 3 when it isn't and stdout is no terminal. Read bylnk-harness(lnk agent connect,lnk agent start), for a harness with anendpoint.lnk-<channel> state: A linked channel only. Printslink_plugin::harness::Stateas JSON: what in itsstatefolder is the link (~/-relative to that folder; Signal's account, WhatsApp'swhatsapp.db), never a program it installed. Read bylnk-harness(lnk agent send), which carries those paths aschannels/<channel>/..., named in the move's manifest (channels).lnk agent bridge(hidden): Carries agent-a's channels as~/.config/lnk/agents/<agent>/bridge.jsonsays ({"endpoint", "key", "proof", "channels", "from", "recover", "tools"}, written bylnk agent start), alnk-<channel> bridgefor each, started again when one stops. Read by the agent's bridge service (link.bridge.<agent id>,lnk-bridge-<agent id>.service).
Cloud adapters (lnk-<kind>)
What lnk-aws and lnk-gcp answer.
A cloud adapter's commands but connect each read one Request as JSON
on stdin ({"account", "place"?, "id"?, "spec"?, "owner"?, "places"?, "public_key"?, "host_keys"?, "tags"?, "bucket"?, "labels"?, "image"?}, account being lnk cloud env's) and print one Reply as JSON on stdout; errors go to
stderr with a nonzero exit, and a command the adapter doesn't have
fails saying has no command there. A box is an Ubuntu LTS machine (spec.os)
with a public address, reachable over SSH as ubuntu with
spec.public_key, running spec.user_data (cloud-init) at its first
boot. The contract's
source is src/local/plugin/src/cloud.rs.
lnk-<kind> connect [--name] [--region] [--profile] [--key-id] [--project] [--zone] [--key-file]: Signs in to an account (the cloud's own login, reused, else asking on the terminal, questions on stderr), checks it, and prints itsAccountas JSON. Itsenvis the account's one sign-in, which every plugin acts as. On Google Cloud that is one credential file,GOOGLE_APPLICATION_CREDENTIALS: a service account's key (--key-file, alsoCLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE), or your login's application default credentials. Read bylnk cloud connect.lnk-<kind> storage:{"key": {...}, "env"?: {...}}: the account's own sign-in for its buckets, as rclone's settings for them (typeincluded; never saved), and variables for the rclone run. An aws profile (env_auth,profile) or access key; a service account'sservice_account_file, or for a person's loginenv_authandGOOGLE_APPLICATION_CREDENTIALS. Needs no CLI. Read bylnk bucket(each run on an aws or gcp bucket).lnk-<kind> forget:{}once what the adapter kept for the account (Google Cloud: its credential file) is gone. Read bylnk cloud disconnect.lnk-<kind> defaults:{"place", "size"}: where and how big a box is by default. Read bylnk box start.lnk-<kind> sizes:{"sizes": [{"size", "cpus", "memory_mb", "gpus", "gpu_model"?, "arch", "price"?}]}: the machine types a box can be at the request'splace, in the adapter's order of preference (general purpose, current generation, cheapest family first).gpu_modelin Link's words (nvidia-l4);archx86_64orarm64;pricedollars an hour, only from a rate the cloud gives. AWS listsdescribe-instance-typesthe region offers (describe-instance-type-offerings), Google Cloud the zone'smachine-types list, with the GPUs built in, and each N1 machine again with the T4s the zone'saccelerator-types listattaches (n1-standard-4.nvidia-tesla-t4.1, a namelaunchtakes as its size); neither gives a price there. Read bylnk box sizes, and bylnk box startto match a spec's CPUs, memory and GPUs or for what a pinned size has, never for the default size; when it fails, its stderr is the error matching gives. An adapter without it makes a pinned or default size only.lnk-<kind> launch:{"id"}: starts making a box from the request'sspec({"name", "id"?, "tags"?, "size", "user_data", "public_key", "os", "arch", "disk_gb", "disk_type", "spot", "gpus", "gpu_model"?}): its machine namedlnk-<id>(lnk-<name>without one) and tagged withtags(lnk-box,lnk-machine,lnk-owner, and the spec's own). The machine fields default to the box Link made before specs (ubuntu-24.04,x86_64, 20,balanced, not spot, no GPUs), the only boxlnk boxasks of an adapter withoutsizes, which would ignore them; the adapter maps each to its cloud's (its image from a fixed table by publisher, NVIDIA's driver withgpus;balancedgp3 or pd-balanced,fastio2 or pd-ssd, at least the image's disk, saying so on stderr when that's larger; spot a persistent request or a spot VM, each stopping the machine when taken back) and refuses what it doesn't know (link_plugin::cloud::Spec::check),arm64with GPUs among them;arm64is Ubuntu's arm64 image. Withimage, an idimage-listgave, the box's disk starts from that image instead, one of the account's own and ready (an AWS AMI--owners self, a Google Cloud image of the project), or the launch fails. Read bylnk box start,lnk box start --image.lnk-<kind> wait:{"ip"}once the box runs. Read bylnk box start.lnk-<kind> start:{"ip"}: a stopped box, running again. Read bylnk box start <box>.lnk-<kind> stop,remove:{}once stopped, or gone with its disk (on AWS a spot box's request cancelled first). Read bylnk box stop,lnk box remove.lnk-<kind> state:{"state", "stopped_by_cloud"?}:running,stopped,gone, ..., and for a stopped box whether its cloud stopped it taking a spot machine back rather than a person, when the adapter can tell (AWS by the instance's state reason; Google Cloud can't). Read bylnk box list --refresh.lnk-<kind> describe:{"boxes": [{"id", "name", "machine_id", "place", "state", "ip"?, "size", "stopped_by_cloud"?, "spot"?, "disk_gb"?, "disk_type"?, "os"?, "arch"?}]}: the box atidas its cloud says it is, each machine field inlaunch's words where the cloud says it. Read bylnk box spec <box>.lnk-<kind> hostkeys:{"host_keys"}: the box's SSH host keys (<type> <key>) assave-hostkeyskept them on it, else as its first boot printed them on its console (link_plugin::cloud::host_keysreads cloud-init's block); none until it has. Read bylnk box start,lnk box(a box found in the cloud).lnk-<kind> save-hostkeys: Readshost_keys;{}once they're kept on the box's instance (Google Cloud metadatalnk-host-keys, an AWS taglnk-host-keywith the ed25519 key). Read bylnk box start.lnk-<kind> list: Readsownerandplaces(none: everywhere);{"boxes": [{"id", "name", "machine_id", "place", "state", "ip"?, "size", "stopped_by_cloud"?}]}, every box taggedlnk-owner=<owner>. Read bylnk box list,lnk box start(a name no box of yours has).lnk-<kind> add-key: Readspublic_key;{"temporary"?}once the key letsubuntuin: merged into the instance's keys (Google Cloud), or pushed with EC2 Instance Connect for 60 s (temporary: the caller appends it over SSH meanwhile). Read bylnk box(this computer's key, on a box found in the cloud).lnk-<kind> tag: Readstags;{}once added to the box's tags (labels). Read bylnk box list(a box from before tags).lnk-<kind> label: Readsbucketandlabels;{}once they're added to the bucket's labels (tags), the others kept. Read bylnk bucket(an agent's new bucket:lnk-agent,lnk-agent-name,lnk-owner).lnk-<kind> buckets: Readslabels;{"buckets": [{"bucket", "place", "labels"}]}, Link's buckets (local-link-*) with every one of them. Read bylnk bucket find,keys,revoke.lnk-<kind> bucket-key: Readsbucket;{"key": {...}}, rclone's settings (typeincluded) for a new key reaching only that bucket's objects: an IAM userlnk-<bucket>with an inline policy (AWS), a service accountlnk-<...>withroles/storage.objectAdminon the bucket (Google Cloud). Its keys from before are deleted. Read bylnk bucket keys.lnk-<kind> revoke-key: Readsbucket;{}once that user or service account is gone (gone already is done). Read bylnk bucket revoke.lnk-<kind> image-save: Readsid(a stopped box),image(its name: lowercase letters, digits and dashes,link_plugin::cloud::check_image) andtags(lnk-image,lnk-owner, andlnk-hashfor an image saved with a hash);{"images": [image]}once the box's disk is saved as imagelnk-<image>, tagged with every tag given, and ready: an AMI and its snapshot, both tagged (AWS,create-image, waited for), an image of its boot disk (Google Cloud,images create --source-disk). Each image is{"id", "name", "place", "state", "disk_gb"?, "arch"?, "created"?, "hash"?}: its id in the cloud, itslnk-imagetag, its region (empty on Google Cloud, where it's the project's),ready,pendingorfailed, its disk in GiB,x86_64orarm64, when it was made, itslnk-hashtag. An adapter that doesn't sayhashstill saves the tag, andlnk boxthen finds no image there by its hash. Read bylnk box image save.lnk-<kind> image-list: Readsownerandplaces(none: everywhere);{"images": [...]}, every image of the account's own taggedlnk-imageandlnk-owner=<owner>, asimage-saveprints one. Read bylnk box image list(and--hash, itshash),save(a name no image of yours has),remove, andlnk box start --imageand--image-hash(the newestreadyimage with thathash, bycreated); an adapter without it makes no image, andlnk boxsays so.lnk-<kind> image-remove: Readsimage(an idimage-listgave),placeandowner;{}once the image is gone with its snapshots (AWSderegister-image,delete-snapshot; Google Cloudimages delete), or was already. Refused for an image not tagged as Link's forowner. Read bylnk box image remove.lnk-<kind> cleanup:{}: deletes what the adapter made for boxes (a firewall rule, a key pair) once its cloud says no Link box uses it; else nothing. On AWS it first cancels Link's spot requests whose box is gone. Read bylnk box remove.
