Skip to content

Host entries

A host loads a remote plugin through its contract. The contract is the only module the host imports; the implementation runs in the plugin process.

Define the shared protocol in a contract. Select its implementation independently in the entry.

Use captured artifacts to keep a built implementation separate from later source edits.

For captured direct deployments that keep consumer service objects across upgrades, use managed Remote plugins. The module-based configuration below uses ordinary restart behavior.

Replace the plugin’s entry with a remote entry. name: remote loads @graftland/plugin-remote, so install it in the application.

graft.yml
- id: greeter
name: remote
config:
contract: ./dist/contract.js
implementation: ./dist/greeter.js
connect:
local: {}
config:
greeting: Hello
Field Meaning
contract A path relative to the config file, a file: URL, or a package name. Through a runner, a package name only.
implementation Built implementation module. Direct execution accepts a config-relative path, a file URL, or a package export; runners use a package export.
connect Where the plugin runs. One of the forms below.
config The plugin’s config in encoded JSON form, checked against the contract’s schema before anything starts. Default {}.
connect form Runs the plugin Other fields
local: {} In a child process on the host. local.command replaces the bundled child entry, such as [node, /path/to/childMain.js].
worker: {} In a Node worker in the host process. worker.execArgv: Node options for the worker.
url: wss://… or ws://… On a runner’s WebSocket listener. ca: CA file to trust instead of Node’s. servername: name to verify instead of the URL host.
unix: path On a runner’s Unix socket. —
command: [program, args…] On a runner started by a command, such as SSH. cwd, env.
transport: { name, config } Through a custom connector. —
  • local and worker take no other fields: no auth, instance, lease, margin or expect.
  • Runner forms also take auth (Transport and authentication), instance and expect (Runners), and lease and margin (Replacement and stopping).
  • Paths are relative to the config file, not the working directory.
  • A YAML entry has a fixed 5 s shutdownTimeout and no retry.

Remote.plugin builds the same plugin in code, for Graft.plugin:

src/entry.ts
import { Effect, Schema } from "effect"
import { Graft } from "@graftland/graft"
import * as Remote from "@graftland/plugin-remote"
declare const contract: Remote.Contract.Contract<
Schema.Struct<{ greeting: typeof Schema.String }>
>
const greeter = Remote.plugin(contract, {
contractModule: new URL("./contract.js", import.meta.url).href,
implementation: new URL("./greeter.js", import.meta.url).href,
connect: { local: {} },
shutdownTimeout: "5 seconds",
})
export const program = Effect.gen(function* () {
const fork = yield* Graft.plugin(greeter, { greeting: "Hello" })
yield* fork.await
yield* fork.update({ greeting: "Hi" })
yield* fork.await
})

The factory fixes deployment options. Pass business config to Graft.plugin; fork.update stops the old execution unit and starts a new one with the next config.

Option Meaning
contractModule Where the execution unit imports the contract.
implementation Built implementation module. Direct execution accepts a config-relative path, a file URL, or a package export; runners use a package export.
connect As in YAML, plus credential effects.
shutdownTimeout Bound for the graceful stop. Default 5 seconds.
retry A Schedule for restarting the plugin after a failure. Calls themselves are never retried.

graft start provides everything Remote needs. If you build Graft.layer yourself, provide NodeServices.layer and SecureRandom.layerWebCrypto in its environment, and optionally the loader’s ModuleResolver and ConfigDirectory.

Rule Detail
Provides The implementation publishes these with Graft.provide; host plugins inject them as usual.
Inject The implementation injects these host services and gets a client that calls the host.
Waiting The remote entry waits for its inject services like any plugin with dependencies.
Host withdraws When a host service in inject is withdrawn, the remote plugin stops until it returns.
Plugin withdraws Host consumers stop. Calls made after the withdrawal fail with RemoteWithdrawn.
Stopping order Consumers stop before the services are withdrawn, as in withdrawal.
Broken connections Can interrupt calls and finalizers at any point.
Only declared Services not listed in the contract do not cross.

Callers and fork.await see these:

Error Meaning What to do
A declared error The plugin failed with an error in the contract’s error schema. Handle it like any plugin error.
Contract.UndeclaredFailure The plugin failed with an undeclared error. original holds it. Declare it in error, or handle it in the plugin.
RemoteError Remote itself failed. cause holds the error; runner errors arrive here too, see below. Read cause.
RemoteWithdrawn The plugin’s services were withdrawn while in use. Normal during stops and restarts.
MissingInstance A runner connection has neither connect.instance nor an entry id. Set connect.instance.
RemoteEntryError A YAML entry is invalid. entry, directory and field locate it. Fix that field: contract, config, connect, connect.transport, connect.auth, or a runner’s listen[n].transport or listen[n].auth.

Public errors above are exported from the package root. Defects stay defects. The following nested tags are diagnostics carried in RemoteError.cause:

Error Meaning What to do
TransportError The connection failed. cause holds the underlying error. Check the network, address and TLS settings.
CredentialsError Reading the host’s credentials failed, such as a missing token file. Check the source in cause.
ProcessExited The plugin process or worker exited unexpectedly, with exit code and stderr. Read stderr and statusCause.
CommandExited The command program exited, with its code and stderr. Check SSH access and the forced command.
CapabilityCombinationError Transport and authentication do not fit, such as mTLS over ws. See combinations.
PackageError The runner could not find, read or verify the contract or implementation package, or expect failed. Install the package under root, or fix expect.
SessionAccessDenied The session belongs to another identity. Give each host its own credentials.
StaleSession Another start of the instance won, or the runner no longer knows the session. Check for two hosts sharing identity and instance.
SessionConflict Conflicting requests on one session. Same as above.
LeaseExpired The lease ran out before the host renewed it. Check the network, or use a longer lease.
SessionStopped The session is stopping, stopped, or was retired during runner recovery. Expected during replacement or recovery.
TerminationUnconfirmed The runner could not confirm a plugin process exited. Check the runner machine. Replacement waits.
RunnerClosing The runner is shutting down. Wait for it to return.
StateDirectoryLocked Another runner owns the state directory, or it cannot be locked. Run one runner per state directory.
StateStorageError The runner could not read or write its state. Check the disk and permissions.
ExecutionError The runner could not start, watch or stop a plugin process. Read detail and the runner’s logs.
You change What happens
Business config through fork.update or YAML The execution unit restarts. Remote never updates in place.
implementation or connect in YAML The old execution unit stops, then the new connection starts.
A local/worker contract or implementation file under graft start --watch The matching remote fork restarts with fresh code. Imported contract dependencies are observed too. Rebuild first when the entry points at built JavaScript.
A network runner’s implementation The host does not watch it. Install the new code on the runner and restart the entry.

Observation lasts until the entry is removed or disabled, even if importing, validating or starting it fails. Fixing a known contract, dependency or implementation file retries the entry. A successful import replaces its old dependencies. Watcher failures are logged and retried against the previous file digests, so edits made during recovery still trigger a restart.

Without --watch, restart the host after rebuilding its contract.

With trust policy installed, use captured managed entries. Untrusted entries require a sandboxed child process; Worker placement is rejected. Ordinary module deployments and runner targets cannot bypass this boundary.