Settings
What a harness adapter is told of the agent it runs, and what it says
its harness needs: lnk-<name> configure writes the harness's own
config from the settings, and lnk-<name> needs reads them to say what
the harness needs beyond Link's defaults. lnk-agent runs both.
lnk-<name> configure
Reads link_harness_contract::harness::Settings as JSON on stdin
({"files", "agent"?, "model": {"provider", "api_key", "name"?, "base_url"?}, "<connection>"?: {...}, "endpoint_key"?, "store"?, "storage"?, "memory"?, "tools"?, "allowed"?, "unsandboxed"?, "proxied"?, "spec"?, "defaults"?, "model_facts"?, "environment"?, "fallback"?, "fallback_facts"?, "identity"?, "remembered"?}) and writes the
harness's own config, leaving out a connection it doesn't speak. It
prints nothing unless it fails.
Each connection's section
Each connected connection's section is under its name, as its plugin's
connect made it, typed in link_harness_contract::harness. Link's
own are "telegram": {"bot_token", "allowed_users", "api_root"?},
"slack": {"bot_token", "app_token", "allowed_users", "dm_channel"?},
"discord": {"bot_token", "allowed_users", "dm_channel"?} and
"signal": {"account", "allowed_users", "program"?}, program the
signal-cli lnk-signal cli install put in the home.
A connection from outside Link
A connection from outside Link is in Settings::connections, and no
connection is named as one of the settings' own keys, such as files,
model, endpoint_key, store, storage, memory, tools or
spec.
Keys and bridges
What the adapter is handed to reach the harness and the agent's things: its endpoint's key and the storage bridges.
endpoint_key
For an adapter with an endpoint: what its endpoint takes
(Authorization: Bearer <key>), refusing anything else, and a
connection Link carries for it is left out of the settings. It's also
the agent's key for this start, which proves the agent's calls to its
bridges
(Bridges).
store
For an agent whose spec keeps its conversations with a driver:
{"driver", "socket"}, where the agent's store bridge listens
(link_store_contract::store::BridgeAt). A harness that keeps
conversations calls that driver through it
(link_store_contract::store::Bridged) and never runs it itself.
storage
Where the agent's machine resolved each of its collections is kept
(link_store_contract::storage::StorageAt, {"places": {"<collection>": {"place", "overridden"?}}, "socket"?},
Agent Spec).
Left out, each is in the files folder.
A collection kept elsewhere
A collection at a place other than file is the driver of that
place's, reached only through the storage bridge at socket: its files
with the file_* calls, its conversations with the store's calls, and
the tools each was narrowed to with tools_read and tools_narrow.
It's never on this machine's disk, and nothing is written there to say
where it is.
What the harness holds in memory
Nothing of a conversation kept elsewhere is written to the machine's disk: a turn asked to stop, or an answer of a crashed turn not yet sent, is held in the harness's memory.
A collection at file
One at file is in the agent's own folder (agent, Agent/), kept as
lnk-file-store keeps it, which the harness reaches itself, as its
sandbox lets it.
How Link hands it
lnk agent start hands it on every machine to a harness that keeps
Link's collections (its contract says chats), each place
resolved from the agent's [storage] and the machine's machine.toml.
It installs the needed drivers and serves the bridge, each call
answered by the driver of the
place its collection is kept at
(link_store_contract::store::serve_storage, Request::collection).
telegram.api_root
With keys on the wire: where the harness must reach Telegram's Bot API,
its proxy, which adds the real token to bot_token's placeholder
(lnk-telegram rule --json).
Tools and grants
The agent's tools, and what the harness may do beyond Link's defaults.
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 the harness that 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, and memory as memory,
to its harness's MCP servers (Settings::mcp_servers), and removes
those it added before that are gone
(link_harness_contract::harness::tools_gone).
allowed
The risks its needs named 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
true when the harness runs with no sandbox at all, its ports on the
machine's loopback.
proxied
true when it runs behind its proxy: sandboxed, with [internet] and
not [network] mode = "host", 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.
Opinions and models
The agent's spec, and what Link knows of its model and its fallback.
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.name is already its spec's default,
and one its allow lists.
defaults
The keys of spec that 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 of model.name
(link_model_contract::models::Known, {"name", "window"?, "answer"?, "thinking"?, "price"?: {"input", "output", "cached"?}, "yours"?}), left
out for a model neither knows (lnk model show).
What each fact is
window and answer are the model's whole window and longest answer
in tokens, thinking how it's asked to think (adaptive, budget,
effort or no), price dollars a million tokens, and yours which
of these the user's ~/.lnk/model-facts.toml set.
fallback
A second model (lnk agent start --fallback), in model's shape and
reached as it is, with keys on the wire at the proxy with a placeholder
key. A harness tries it for a turn model can't answer: its runtime
off, the turn's prompt past its window, or its provider busy or failing
(429, or a 5xx) before any of its answer. A harness that can't fall back
ignores it.
fallback_facts
What Link and the user know of the fallback model, in model_facts's
shape, left out for a model neither knows.
Who it is and where it runs
Who the agent is, and what it runs with on this machine.
identity and remembered
Who the agent is and what it remembers, for a harness whose contract
says identity (Who the agent is).
environment
What the agent runs with here, as Link resolved it
(link_harness_contract::harness::Environment, {"connections"?, "folders": {"write", "read", "teams"?}, "commands"?, "teams"?}), for a
harness to tell its model. connections names the connections it's
reached on, by their plugins' names.
Its folders
folders is its sandbox file's [files]: the folders it may write and
those it may only read, by name (link_harness_contract::harness::layout:
Home's work/ and memory/, and the own folder's instructions/,
skills/ and chats/; . all of Home), and of each of its
teams' shared folders (folders.teams, {"<team>": {"write"?, "read"?}}, folders' names in it or . for all of it).
Its programs
commands names the programs its run may start here, those of its
sandbox file's [commands] this machine has, by name (["sh", "python3"], or ["*"] for any). Left out, it has no run.
Its teams
teams is the teams it's on, [{"name", "id", "member", "purpose"?, "teammates"?: [{"name", "id", "purpose"?}], "shared"?}]: its own name
in each, the others by name and agent ID, and the shared folder where
it's granted some of it. A harness whose sandbox Link makes is given
its teams' folders as granted, whatever harness it is.
lnk-<name> needs
Reads link_harness_contract::harness::Settings on stdin as
configure does, but with no key or token (the model's api_key and
each connection's secrets empty), and prints
link_harness_contract::harness::Needs as JSON: {"needs"?: [{"spec", "why"}], "unsandboxed"?: {"why"}, "risks"?: [{"name", "why", "without"?}]}. Read by lnk-agent.
What it says it needs
What its harness needs beyond Link's defaults with those settings, each
with why in its own words, one line of at most 200 characters. The
settings carry unsandboxed, true when the user runs it with no
sandbox, and proxied, with the grants it has so far, so a harness
that asks for [network] mode = "host" itself counts it.
Each need is a spec
Each of needs is a sandbox's spec's sections (Sandbox
Spec),
such as {"spec": {"network": {"mode": "host"}}, "why": "Slack's websocket ignores Link's proxy (HTTPS_PROXY)"}.
The programs it runs
Its harness runs no program but the one its launch names (and that
one's #! interpreter) and those [commands] allows: a harness that
starts another (node, git, a program it installed) names it,
{"spec": {"commands": {"allow": ["node"]}}, ...}, or any, ["*"]. Its
commands that read the agent's files (instructions, chats)
run only its own program and the store drivers it's handed.
What a need may not say
Each need is a spec with no name, version, relative path or limit
of calls (per_minute, the agent's own), opening something on some OS,
and taken as it is on this OS: one that opens nothing here is passed
over. It's shown and taken back as the flags lnk agent allow takes
(--network-mode host), and a key Link doesn't know is refused, the
harness not started.
unsandboxed
unsandboxed is only for a harness that can't run in any sandbox: it's
never asked or granted, and a start is refused unless the agent runs
with no sandbox (lnk agent start --no-sandbox).
risks
risks names what running it risks that no spec 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 none that unsandboxed or
a name of a sandbox setting (network, lan, ...).
A risk it runs without
One with without, a line saying what it does without it, runs either
way, and only a yes in a terminal allows it. One without without is
asked as a need is. configure gets those allowed in allowed.
How each is asked
lnk agent use, a start and lnk agent move ask the user for each not
answered yet, and grant what they allow to the harness
(~/.lnk/harnesses/<name>.toml), held only while it runs, remembering
the answer. What the agent has of its own (lnk agent allow) is had,
and not asked.
An adapter without it
An adapter without needs 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't Needs is refused.