Remote executors

Give an agent a real computer to work on — on Linux, macOS or Windows.

What a remote executor is

An agent in Nessie has no computer. It can read what is in Nessie, talk to people and call connected tools over the network — but it cannot open a file on a laptop, run a command, or look at a page that only exists behind somebody’s login. A remote executor is how you give one a machine to work on.

An executor is a small daemon you install on a computer you control. It pairs outwards with your Nessie instance, proves which machine it is with a key that never leaves the host, and then offers a fixed, named set of operations — nothing else. Work arrives as individual signed commands; results come back as bounded, structured receipts. The machine never accepts an inbound connection from Nessie.

It is worth being precise about what an executor is not, because most remote-access tools are one of these things and an executor is deliberately none of them. There is no SSH, no ambient remote shell, no attaching to a terminal multiplexer you already have open, no cloud-side standard input and no general local-network proxy. It is not an MCP server, and it is not a container or cloud execution environment — Nessie has those already, and they are a different thing. An executor is a capability-bearing endpoint with a closed list of operations, and that list is the entire surface.

Who an executor belongs to

Every executor is created with exactly one scope, and that scope cannot be changed afterwards. It decides who may invoke the executor and who may administer it.

ScopeWho may invoke itWho manages access
PrivateThe requesting person and the exact invoked agent must both be named assignees. Neither alone is enough.An assigned human administrator. Agents can only use a private executor, never administer it.
ProjectA person entitled to that exact project, and only from a run in that project.An organisation manager, or a project owner or admin.
OrganisationA person with organisation entitlement.An organisation manager.

A private executor must always have at least one active human administrator. Removing the last one directly fails; if an offboarding transaction would leave none, the executor stops accepting new work, drains what it has and revokes itself. An organisation owner has a revocation-only break-glass path that ends an executor they do not otherwise manage, without showing them its private roster, its sessions or its content — private access is never silently transferred to somebody else.

What an agent can actually do with one

The operation vocabulary is fixed in code, not configured per deployment. It divides in two, and the division decides what you need to install: some operations the daemon serves from its own scratch directory with no virtual machine at all, and the rest need a per-session guest VM on the host.

OperationWhat it doesGuest VM
file.list, file.readList and read files beneath the one workspace root chosen when pairing.No
file.writeWrite into a copy-on-write draft of that workspace. It never opens the real root for writing.No
workspace.reviewReport what the draft changed — up to 100 relative paths with kind and byte count, plus a manifest digest. It carries no file contents.No
sandbox.stopTear down the session this run owns.No
command.runRun one command as a shell-free argument vector in a guest with no egress. There is no shell string anywhere in this path.Yes
browser.open, browser.observe, browser.actDrive a browser inside the guest against an origin list configured on the machine. Observation returns a bounded accessibility tree and an optional downscaled image; actions are navigate, click, type, press and scroll against a node the agent has just observed — never a selector, a coordinate or a script.Yes
coding.launch, coding.observeStart a managed Codex session in the guest and read back typed lifecycle state. There is no terminal attach and no prompt channel: the agent learns whether the session is running, not what is on its screen.Yes
workspace.promoteApply a reviewed draft to the real workspace. This is the only operation that writes to the host, and it is deliberately not offered to models at all.No

The operations that need no guest are the workspace bundle. A host that cannot start a guest is not a broken executor: it pairs normally, reports its sandbox backend as none, and advertises the workspace bundle and nothing else. That is the state a Windows edition without Hyper-V, or a Linux machine whose user cannot use /dev/kvm, ends up in, and the interface names it rather than failing obscurely.

Browser work is never something an agent starts on its own initiative. The ordinary single-operation binding route refuses browser.open and browser.observe outright; a browser run is launched by a person from a channel, as one bundle of four operations on a fresh run, and no further binding can be added to it afterwards. Managed coding sessions work the same way.

Two further groups exist in the code and are deliberately unavailable. Operations for driving a Chrome tab you are already signed into have a complete local implementation and a native-messaging extension, but the control plane refuses to advertise them until the server can prove the originating conversation was private and record who disclosed what — the foundation is inert on purpose, and the extension is not shipped in either installer. And several coding operations — attach, prompt, interrupt and close — are named in the schema but have no implementation, so they cannot be granted.

How it works

The connection is outbound, always

The daemon makes outbound HTTPS and WSS connections to your Nessie API and nothing else. Nessie never dials a laptop, never holds an address for one, and never treats any catalogue endpoint as a machine transport. That is why an executor works from a home network, behind NAT, on a corporate VPN, with no port forward and no inbound firewall rule — and why stopping the daemon is a complete and immediate disconnection.

The API origin must be HTTPS. The single exception is a loopback address for people running Nessie on their own machine, behind its own environment variable; no ordinary flag turns a production origin into plain HTTP.

Pairing proves a machine, and a person confirms it

Pairing is a two-sided proof. An entitled person creates the executor in Nessie with its scope, and Nessie issues an enrolment: a 256-bit random challenge of which it stores only a SHA-256 verifier, valid for ten minutes and usable once.

On the machine, the daemon generates an Ed25519 key pair, stores it in owner-only OS-protected storage, and writes the exact signed request to disk before it makes any network call — so a lost response can be retried with the same prepared key rather than quietly pairing twice. It then submits the enrolment id, the challenge, its public key and a proof signature. Nessie stores the public key and its fingerprint. It never stores the private key.

Nessie then shows that fingerprint to a person, who confirms it. Until that confirmation and a subsequent proof of possession, the executor stays offline. Pairing can never be completed from chat text or a terminal transcript: it takes a deliberate human action in the web or desktop interface.

A machine that loses its key cannot reclaim its executor record. It is revoked and enrolled again as a new executor. Key rotation, by contrast, is a single transaction signed by both the old key and the new one.

The descriptor is the machine’s signed claim about itself

A paired daemon publishes a descriptor: a signed, monotonically revised statement of structural facts only — which operation schemas it supports, its resource and network limits, its platform, and a digest of its local policy. It carries no host paths and creates no authority in the cloud.

A new revision is never activated on arrival. It is stored pending review, and an entitled person has to see it and confirm it before it can affect what is available. That is what stops a capability refresh from silently granting new power: a newly advertised operation starts denied.

Be clear about what the signature proves. A descriptor signed by a paired executor key proves that key made the claim. It does not prove the host is uncompromised, and the interface is required to say so.

Dispatch: candidate, binding, command, receipt

An agent never names an executor. Before a run can use one, Nessie resolves the logical operation into an opaque, short-lived availability candidate. The response contains no executor id; the database keeps only a digest of the handle alongside who and what it was resolved for. Models and clients receive a handle, not a selectable machine.

Availability is one resolver, used identically by the API, the worker, the UI and the assistant:

available(operation, human, agent, immutableRunContext) =
  human may discover executor
  ∩ exact executor-agent-operation grant allows operation
  ∩ stable logical tool grant allows operation
  ∩ scope matches immutable run context
  ∩ executor is online and descriptor is approved
  ∩ local policy permits operation

Consuming the candidate creates a binding under a database lock that rechecks every one of those gates a second time and advances a monotonic fence. Dispatch rechecks them again. The consequence is that a grant withdrawn, a policy narrowed or an executor revoked between two steps stops the work rather than being noticed afterwards — and a retry can never land on a different machine than the one the run was bound to.

The command itself rides on Nessie’s ordinary queue job and tool-call lifecycle rather than a second one of its own. The daemon polls for work over its authenticated control channel; it is never pushed to. A command moves through four receipts and no others:

leased → accepted → started → result-acknowledged

Every frame binds the command id, the run provenance, the binding fence, the capability revision, an expiry, an idempotency key and digests of both the arguments and the payload. The daemon refuses a mismatch, a stale policy, a replay, or a second result for a command it has already completed. Arguments and terminal results are encrypted with AES-256-GCM at rest; the database keeps bounded ciphertext and canonical digests, not content.

If an acknowledgement is lost, Nessie does not assume success. The command is recorded as an unknown outcome and the run follows the normal recovery path; the daemon, which journals each delivered command locally before it starts work, will not repeat the side effect. A late receipt can still resolve the ambiguity honestly.

Trust boundary

PrincipalCanCannot
PersonPair an executor, manage it when entitled, confirm access and policy changes.Widen a policy the machine itself denies, or act without current entitlement.
Agent, including the personal assistantInvoke an operation already available to it; prepare a proposed access change for the person who asked.Administer access, approve itself, pick a machine, change local policy, or apply a change it prepared.
Nessie control planeResolve availability, bind a run, lease commands, keep redacted audit facts.Dial the machine, widen local policy, reach host credentials, or treat terminal text as an authorisation or an outcome.
Executor daemonEnforce local policy, pair outward, run the guest and gateway, acknowledge commands.Expand its own scope, accept stale or replayed work, expose the host workspace or credentials directly, or send raw local data into the audit trail.
Guest VM and the tools inside itWork in a copy-on-write sandbox, reaching the network only through the gateway.Reach host files, host credentials, direct network or DNS, or promote a host change.

Installing and pairing an executor

Install the Mac CLI with Homebrew, the Mac apps with Homebrew casks, the Linux CLI through APT or DNF, or the Windows executor through WinGet. Each paired team has separate permissions stored on the computer. Nessie controls access within the team; it cannot change local resources.

The installation and pairing guide has exact commands, the two-sided eight-digit code flow, local settings, startup, updates and removal.

How far each platform has actually been proven

The state of the three guest sandboxes differs, and the difference is material. macOS is the backend with a working, exercised guest path. The Firecracker backend on Linux and the Hyper-V backend on Windows are implemented and covered by host-side tests — the launch sequence, the disk images, the control and egress transports, the draft protocol — but no guest has yet booted on real hardware in this project’s continuous integration, because that needs a machine with KVM or Hyper-V and the jobs that would prove it have never had one. The workspace bundle, which needs no guest, is exercised end to end on Windows against a real control loop. Treat guest-backed operations on Linux and Windows as unproven until you have proven them on your own hardware. The pairing screen in Nessie says the same thing in its own words.

Security and containment

One workspace root, and its path stays on the machine

Pairing selects exactly one canonical workspace root: one directory, not a set of folders, and never their nearest common parent. Nessie is told the root’s basename, so a person can recognise the boundary in the interface; the full path stays in owner-only machine state and is never uploaded. Changing the root is possible, but only after every local draft and sandbox has been removed, and it produces a new descriptor revision that goes through the same human review as any other policy change. It can never silently widen to a parent directory.

File operations accept relative paths only. They reject traversal and every symbolic-link component, re-check that the configured root is still an ordinary directory, and return bounded structured results containing no host path.

Writes go to a copy, and only a person promotes them

An agent never writes to your files. file.write creates a bounded copy-on-write tree under the daemon’s own state directory, keyed to the run, and later reads and listings for that run see the draft. Guest work, including coding sessions, uses the same copy-on-write scratch.

Applying a draft to the real workspace is a single operation — workspace.promote — and it is the only host write in the system. It has no model-facing schema at all: no agent, no assistant and no caller-supplied path can turn a draft into a host write. Instead a person opens Your reviewed drafts, presses Review promotion, and confirms with their current password. The server then rechecks the acknowledged review digest, the manifest digest, the originating run, user and agent, the authorisation revision, the operation grant and the active local policy before it creates the command. On the machine, the daemon rebuilds the manifest itself and refuses a digest that has changed — so an edit made after the review cannot ride in on the review’s approval.

The apply itself is descriptor-based and journalled. The native helper receives the workspace root and the draft as already-open directory descriptors, never as paths; it validates every component with no-follow operations, refuses symbolic links, hard links, special files and mount crossings, and keeps an all-or-recover journal so that a crash either finishes the validated manifest or restores the original before anything else runs.

The guest has no network, and egress is forced

When an operation does need a guest, it is a per-session Linux micro-VM with no host home, no Docker socket, no SSH agent, no inherited environment and no host mount other than the workspace share. The executable runtime mount is read-only; the writable workspace mount forbids execution, so anything that runs comes from the verified runtime payload rather than from something an agent wrote. The guest drops to an unprivileged identity before it opens any socket, and a root-owned share fails the boot rather than leaving a privileged workload running.

Most importantly, the guest has no network interface. On Linux the launch sequence deliberately never configures one and no TAP device is created; on Windows the virtual network adapter Hyper-V adds by default is removed during setup. The guest’s only route off the machine is a loopback proxy inside it, tunnelled over a virtual socket to a bridge on the host, which hands the bytes to the daemon’s own gateway — an owner-only local socket listener with no TCP listener and no general forwarding mode. That gateway is where HTTP, CONNECT, WebSocket, redirect, origin, download and upload policy is enforced, using Nessie’s existing pinned-fetch machinery rather than a second implementation of the same rules. There is no NAT bridge and no direct DNS resolver: alternate DNS, raw TCP, QUIC and proxy bypass are all denied.

The origin list that gateway enforces is configured on the machine, by its owner, and is deliberately never uploaded. The interface names that absence rather than pretending Nessie is an origin-policy editor: to know what a browser executor may reach, ask the person who owns the machine.

Credentials

No host credential store is ever mounted into a guest. Not the keychain, not ~/.codex or ~/.claude, not a global CLI token, not an environment variable carrying an API key. The only supported login for a managed coding session is an owner-private authentication file whose contents are read solely by the owner-controlled initrd builder — never by Nessie, never by the descriptor, never by a log or a command argument — and moved once into a private home inside the guest, which is destroyed with the VM. That guest can reach exactly one origin.

The browser executor is stricter still: each session gets a fresh private browser profile and refuses one prepared by the workspace, so it starts with no cookies, no extensions and no ambient signed-in state.

Raw local data can reach a model provider only inside an explicit, bounded run. What Nessie persists is redacted manifests, argument and policy digests, result digests and structured status. File contents, terminal output, browser DOM, credentials and authentication factors are forbidden from database records, audit entries, logs, realtime events and error reports.

What an operator grants, and how they take it back

Permission to use an executor is not one switch. An agent can invoke an operation only when it holds a grant for that exact executor and that exact operation, and the ordinary logical tool policy allows it, and the run’s scope matches, and the machine’s own local policy permits it. Granting one executor an operation therefore authorises nothing on another, and a capability refresh cannot widen anything, because new operations arrive denied.

Agent access and sharing both take effect at once on the page itself, with no approval continuation, password prompt or fresh-authentication step. On the executor’s Agents tab a manager turns an agent’s access to the machine on or off directly; on Sharing they give a person Can use or Admin, or extend use to a project or to everyone on the current team, the same way. In your own assistant conversation — only there, never in a shared channel, a scheduled job or another agent’s run — the assistant can prepare an agent-access or assignment change instead, which posts a confirmation card in that conversation; nothing changes until you press Allow access on it, and chat text is never a confirmation.

Three changes still require you to re-enter your current password at the moment of confirmation: activating a descriptor revision; an assistant-prepared assignment that gives a person, rather than an agent, the machine; and a standing policy that lets colleagues start commands on it as its owner, the unattended ticket-work policy. Disconnecting or deleting a machine deliberately asks for neither, since a kill switch must never be harder to reach than what it stops. Promotion of a reviewed draft always does. The audit trail records the acting person and the assistant’s involvement separately.

An executor moves through a small, explicit set of states:

pending_pairing → online ↔ offline
        │             ↓
        └──────────→ draining → revoked
                         ↓
                       error

The header’s Computer menu is where a manager prepares each lifecycle change: pause it, resume it once paused, disconnect it, or delete it. Pausing leaves an executor online but stops it accepting commands. Disconnecting invalidates its client certificates and every outstanding candidate handle, and cannot be undone — a disconnected executor is re-paired as a new one. Delete does the same and also removes the executor from the list; unlike disconnect, it is offered whatever the executor’s state, pending pairing and already disconnected included, because it is the only way one leaves the list. Draining — finishing the work it already has before it stops taking more — is a real third state in the protocol, and the interface can show a computer in it, but nothing here starts one: only a direct call to the API does. Every confirmed access change, descriptor review and lifecycle transition, a drain included, also advances the daemon’s connection epoch, so its next control poll — at most a second later — fails closed and stops every live guest before it can reconnect. Revocation is not a request the machine may ignore.

Operating an executor

Checking it is healthy

Nessie’s own answer is liveness. A daemon signs a heartbeat, and an executor whose last authenticated activity is older than sixty seconds is durably marked offline before it can be listed, selected or dispatched to. An executor that shows as online has proven itself within the last minute.

There is no health endpoint on the machine — the daemon listens on no operator-facing port at all, by design. On macOS and Linux, ask the CLI directly:

nessie-executor status                # every paired executor, plus linger state
nessie-executor status <executorId>
systemctl --user status nessie-executor@<executorId>

On macOS, status reports each team’s launchd agent. The systemctl command is Linux-only. On Windows the tray icon is the at-a-glance answer, its menu names the reason, and the service log carries the detail. On either platform the executor’s Overview tab in Nessie shows its approved profiles, its data boundary, when it was last seen and any pending local policy proposal awaiting review.

Logs

PlatformWhere
Linuxjournalctl --user -u nessie-executor@<executorId>; the daemon keeps no log file of its own
Windows%ProgramData%\Nessie Executor\logs\service.log, which the tray’s Open logs folder opens

Every refusal a person can act on lands there: an unsigned or tampered runtime, a state directory that would not secure, an executor that did not start at boot. What never lands there is a pairing challenge, a key, or a child process’s output. There is no log-level setting to raise, because there is nothing more verbose to turn on.

The Mac CLI writes each team’s service log to ~/Library/Logs/NessieExecutor/<executorId>.log.

Upgrading

Upgrade the CLI through Homebrew, APT or DNF, then restart each team’s service with disable followed by enable. WinGet upgrades the Windows executor. Direct Nessie Desktop installers offer signed in-app updates; MSI and Debian installs use their package manager. Pairings and local permissions survive upgrades.

Stopping and removing

Stopping is a graceful path the daemon owns, not a kill. It receives a termination signal, stops every guest session, releases its lease and exits. On Linux the unit waits fifteen seconds for a ten-second teardown budget and deliberately never escalates to an uncatchable kill — because a daemon killed mid-teardown strands a micro-VM and a workspace overlay with no process left to clean them up. A stop that hangs is therefore visible in systemctl --user status rather than silent, and escalating is an explicit human decision.

To remove the Linux package:

nessie-executor disable <executorId>   # stops and disables the service
sudo apt remove nessie-executor
loginctl disable-linger                # only if your user manager should stop running at boot too

On Windows, msiexec /x stops and removes the service, its registry entries and the tray’s startup entry. On both platforms the paired state is deliberately left behind — ~/.local/state/nessie-executor/ on Linux and %ProgramData%\Nessie Executor on Windows. A pairing is a machine key and a signed policy revision, and uninstalling a program is not a request to destroy them; reinstalling finds them where they were. Delete the directory by hand to forget a pairing, and revoke the executor in Nessie to end it from the other side.

The Windows tray and the desktop companion both offer Forget pairing on this computer as a first-class action: it stops the local daemon, removes the machine key and the folder selection, and permanently deletes the local draft copies after one native confirmation — while leaving the server-side executor and its audit history for an owner to keep or revoke.

Common failures

SymptomWhat it means
The executor pairs but offers only file operationsThe host reported no usable sandbox backend. On Linux, check whether you can read and write /dev/kvm and whether you are in the kvm group — log out and back in after adding yourself. On Windows, the edition has no Hyper-V; it is an edition, not a setting.
Executor controls are missing entirelyThe build is not verifiably from the expected publisher. On Linux the runtime must be root-owned with no group or world write bit; on Windows and macOS the signature must match the publisher or team pinned into the build. Each of these refuses in words and names the remedy rather than hiding the controls.
The service runs but starts no daemon and refuses every commandSomething in the install was replaced after packaging. This is the intended behaviour: a tampered install answers with the reason instead of disappearing, because a stopped service would make the tray name the wrong remedy.
A run ends with an unknown outcomeA terminal acknowledgement was lost. Nessie will not report a result it did not receive: the command is marked unknown, the run takes the recovery path, and the side effect is not repeated. A late receipt from the daemon can still resolve it.
An operation that worked yesterday is refused todayA gate changed between binding and dispatch, and the checks are repeated at every step: a grant withdrawn, a descriptor revision awaiting review, a narrowed local policy, a scope that no longer matches the run, or a revocation. The refusal names which.
An agent asks for a browser and is told it cannotBrowser and coding work are launched by a person from a channel as a whole bundle on a fresh run. The ordinary binding route refuses them by design, and no browser operation can be added to a run that is already doing something else.