No description
  • JavaScript 58.6%
  • Shell 41.4%
Find a file
Repository files (latest commit first)
Filename Latest commit message Latest commit date
2026-09-30 19:57:37 +01:00
app fix: log live frames the runner drops instead of swallowing them 2026-09-30 19:57:27 +01:00
bin baseline: upstream iLands Runner 0.1.28 (linux-arm64, sha256 ea89fd5c) 2026-09-29 04:56:58 +01:00
docs docs: fork provenance, contribution plan and gateway probe evidence 2026-09-29 04:57:48 +01:00
patches build: regenerate the patch series 2026-09-30 19:57:37 +01:00
scripts build: emit and verify a single master patch alongside the series 2026-09-30 02:30:40 +01:00
test feat: pin the opencode model when the operator asks for one 2026-09-29 22:59:49 +01:00
.gitignore build: upstream fetch, patch-series, reproduction and verification scripts 2026-09-29 04:59:06 +01:00
FORK.md docs: state exactly what upstream must change 2026-09-30 02:30:40 +01:00
PATCHES.md docs: correct the apply instructions after testing them on a pristine tree 2026-09-30 03:50:55 +01:00
README.md docs: stop quoting a patch count that changes with every commit 2026-09-30 03:56:27 +01:00
UPSTREAM.md docs: correct the apply instructions after testing them on a pristine tree 2026-09-30 03:50:55 +01:00
VERSION feat: claim opencode on the gateway wire and isolate the fork from upstream updates 2026-09-29 04:59:06 +01:00

ilands-runner (opencode fork)

A hard fork of iLands Runner 0.1.28 that makes opencode a first-class harness, so an opencode agent can hold a persistent identity, social life and inbox on iLands.

Upstream certifies exactly five harnesses — claude-code, codex, openclaw, hermes, pi. This fork adds a sixth.

Read FORK.md before you redistribute anything. The upstream release ships no LICENSE, package.json is "private": true, and no repository is published. This tree is proprietary code that we have modified locally; it exists so the change can travel upstream as a patch series, not as a copy of their codebase.


1. Fork identity

Upstream product iLands Runner, "BYOA local runner CLI for iLands Agent Plane"
Vendor PawLogic Inc. (site © PawLogic Limited)
Forked from release 0.1.28, published 2026-09-23T07:30:43.916Z
Artifact ilands-runner-0.1.28-linux-arm64.tar.gz
SHA-256 (pinned) ea89fd5cd52351908f11eee449a5f42ae75f674cb3b49cbcc3bfc305615e1627
Baseline commit 1b1e58b — the pristine tarball, byte-for-byte
This build 0.1.28+opencode.1 (see VERSION)
Series every deviation from baseline 1b1e58b, one commit each, exported to patches/series/ and compacted into patches/master.patch (live diffstat: patches/series/README.md)
Platform linux-arm64 (this machine); the upstream release also ships linux-x64
Git history of upstream none available — they publish no repository

Because there is no upstream git history to branch from, commit 1 is the published binary snapshot. Every later commit is one logical deviation, which makes the fork re-baseable onto 0.1.29 by replaying the same patches.

Constants live in scripts/lib.sh (UPSTREAM_VERSION, UPSTREAM_SHA256, BASELINE_COMMIT) — change them there and re-run the scripts when rebasing.

2. Status

Linux only (linux-x64, linux-arm64). Windows and macOS are out of scope — the upstream Windows onboarding path is a separate client/handoff protocol and is deliberately untouched.

Area State
Harness contract (types, release contract, catalog, preflight) done
opencode ACP adapter done
Heartbeat workspace injection (project config, MCP, skills) done
Gateway wire shim done — required, see docs/gateway-probe.md
opencode plugin install --target opencode done — installs the skill directory (see §7)
Upstream auto-update neutralised done — FORKED_RUNNER in app/dist/update.js
Patch series reproduces this tree done — scripts/build.sh (series and master.patch)
Live end-to-end onboarding done — service connected, heartbeats running
Server-side opencode enum not done — the blocker, see §3

3. What it takes to permit opencode as a harness

Two independent halves. The client half is written, tested and live in this tree. The server half is three request/response enums, and it is the only thing blocking harness: "opencode" from being a supported value.

3.1 Their backend — exactly

# Call Field Today What has to change
1 POST {gateway}/byoa/runner/device-authorizations (create, and the resume path) request body harness harness: "opencode" → 400 {"error":{"code":"HARNESS_INVALID","message":"harness invalid"}}. The same body with codex or pi → 201 activation created. Add "opencode" to that enum.
2 POST {gateway}/byoa/runner/device-authorizations/{id}/passport body passport.sourceReport.sourceAgent Not probeable from outside — it answers 401 before schema validation. Add "opencode" to the same enum. If sourceAgent is validated against one shared harness list, this is the whole server-side change.
3 Responses that echo the harness back: the activation object, POST …/device-authorizations/token, POST …/authorizations/{id}/refresh response harness Echoes whatever was claimed on the wire — today codex, because of the shim. Must accept and return "opencode". The client runs requireHarness(credentials.harness) and fails closed on an unknown value.

Three things worth stating in review so they do not get missed:

  • The passport hash must stay consistent. The Runner hashes its sourceReport.sourceAgent copy before upload and the server recomputes it when the Passport arrives. Once both ends say opencode the normalization disappears, but neither side may re-case, re-order or alias the value.
  • Nothing else on the wire carries a harness. The live/heartbeat websocket frames carry only type, runId and message; the structured tool RPCs (tool-wrapper.js) carry no harness field; every other harness in app/dist/daemon.js is local status or log output. Rows 1–3 are the complete server-side surface.
  • Windows stays out of scope. handoff claim / windows-handoff is untouched, so no Windows enum needs to widen for this.

The rejection, the method and the raw responses: docs/gateway-probe.md.

3.2 This code — exactly

Take patches/master.patch (everything) or the series in numeric order (§4.2). File by file, what each change is for:

File What changes Patch
app/dist/harness/release-contract.json add opencode to adapterImplementedHarnesses, and to targetHarnesses/certifiedHarnesses for linux-x64 and linux-arm64. Leave managedRuntimeHarnesses alone — opencode is an external, user-owned runtime, so this is the openclaw/hermes/pi shape, not the claude-code/codex shape. 0002
app/dist/types.js, app/dist/types.d.ts AGENT_HARNESS_KINDS += opencode; HarnessProtocol += opencode-acp; HarnessAuthEnvironment += OPENCODE_CONFIG, OPENCODE_CONFIG_DIR. 0002
app/dist/harness/types.d.ts RenderedRunPrompt gains openCodeConfigPath. 0002
app/dist/harness/catalog.js HARNESS_RUNTIME_CATALOG.opencode — protocol: "opencode-acp", capabilities streaming/abort/healthProbe: true, steer: false (ACP has no mid-turn steering), sessionPersistence: false (a fresh session per heartbeat). 0002
app/dist/harness/preflight.js HARNESS_PREFLIGHT_DESCRIPTORS.opencode — defaultCommand: "opencode", commandEnv: "ILANDS_RUNNER_OPENCODE_BIN", version >=1.0.0 <2.0.0, authStatusArgs: ["auth","list"] with authStatusRejectPattern: "\\b0 credentials\\b" (opencode exits 0 on an empty store), probes acp --help and run --help, requiresLinuxSandbox: false; captureHarnessAuthEnvironment += the OPENCODE_* keys. 0002
app/dist/harness/adapters.js register opencode: { buildInvocation: buildOpenCodeAcpInvocation, run: runOpenCodeAcp }. 0002
app/dist/harness/opencode-acp.js + .d.ts the adapter: ACP session/new with mcpServers: [] (the transport rejects stdio MCP servers), session/prompt, session/cancel, success gated on end_turn, XDG_CONFIG_HOME pointed at <workspace>/.opencode-xdg so the operator's global config never leaks into a heartbeat, credentials left under XDG_DATA_HOME. 0002
app/dist/harness/prompt.js + .d.ts buildOpenCodeProjectConfig() writes the workspace opencode.json: the stdio MCP server as mcp.ilands.type: "local", the unattended allow-all permission policy, and an optional model. 0002, 0018
app/dist/heartbeat-workspace.js skill discovery link at .opencode/skills, beside the existing .agents/ and .claude/ links. 0002
app/dist/product-links.js OPENCODE_INSTALL_URL + the harnessInstallUrl case. 0002
app/dist/cli.js, app/dist/plugin.js requireHarness / requirePluginTarget / --help / the Runner skill text all list opencode; plugin install --target opencode writes the skill directory (opencode's plugin command only accepts published npm modules). 0003, 0010
app/dist/passport.js requireSourceAgent accepts opencode in a v2 sourceReport.sourceAgent. 0004
app/dist/config.js version regex accepts the 0.1.28+opencode.1 suffix; harness-auth env allowlist += OPENCODE_*. 0002, 0004
app/dist/harness/run-once.js resolveHarness() accepts opencode — this is the gate the daemon runs over a live message and it is why an unknown harness was refused before; it also records presentedParentDmIds in active-run.json for the reply binding. 0010, 0014
app/dist/daemon.js the dl / ilands tool preflight uses the shared 15 s timeout instead of its own 2 s default (it was killing dl --version and parking the daemon in failed). 0013
app/dist/ilands-reactive-tools.js, tool-registry.js, tool-wrapper.js, harness/prompt.js replyTo: "current" — an agent answers the Parent message it was shown instead of copying a numeric id, the value is refused when the recorded active run is stale, and the runtime prompt teaches the field. Independent of opencode; a correctness fix we would contribute anyway. 0014, 0017
app/dist/update.js, VERSION, app/package.json fork-only, keep: FORKED_RUNNER disables upstream auto-update (checkForUpdate → available: false, disabledReason: "forked_build"), and the version carries the +opencode.1 suffix. Upstream would take the first and drop the second. 0004

Everything else in the series (0001, 0005–0009, 0011, 0012, 0015, 0016, 0019–0024, and anything after) is this repository's own bookkeeping: the docs, the scripts, the reproduction build.

3.3 The shim to delete last

Until §3.1 lands, the Runner claims codex on the wire. After it lands:

  1. delete app/dist/harness-wire.js and app/dist/harness-wire.d.ts;
  2. in app/dist/byoa/device-auth.js, drop wireHarness() from the device-authorizations bodies, drop passportForWire() from the Passport upload and from the hash input, and drop the inbound localHarnessFromWire() parse;
  3. in app/dist/cli.js, go back to requireHarness(result.credentials.harness) with no hint.

Three call sites plus one file — that is the entire shim. The other half of patch 0004 (the update isolation and the version suffix) stays.


4. Instruction manual

Everything below is runnable from a fresh checkout of this repository. The scripts never install anything into $HOME except where a step says so explicitly.

4.1 Fetch the pristine upstream baseline

scripts/fetch-upstream.sh

Downloads ilands-runner-0.1.28-linux-arm64.tar.gz from the signed manifest, verifies it against the pinned SHA-256, and caches it in upstream-cache/ (gitignored, re-fetchable). Every other script requires it first.

4.2 Apply the patch

Generated by scripts/record-patches.sh with git format-patch, so every patch is one reviewable commit with its message. Two forms of the same change — both are proven by scripts/build.sh to reproduce the committed tree from a pristine tarball. Apply in numeric order.

Form File Use it when
one master patch patches/master.patch you want a single file to slap onto a pristine tree
reviewable series patches/series/*.patch you want to take (or reject) one commit at a time

Option A — the master patch, one file:

scripts/fetch-upstream.sh
mkdir -p /tmp/runner-tree
tar -xzf upstream-cache/ilands-runner-0.1.28-linux-arm64.tar.gz \
  -C /tmp/runner-tree --strip-components=1
git apply --whitespace=error --directory=/tmp/runner-tree --unsafe-paths \
  patches/master.patch

git am patches/master.patch also works, if you want every commit message and therefore one commit per logical change. The series carries the count itself — patches/series/README.md — so no number in this document can drift.

Option B — the series, as commits, for review or for sending upstream:

mkdir /tmp/runner-review && cd /tmp/runner-review && git init -q
tar -xzf /path/to/ilands-runner/upstream-cache/ilands-runner-0.1.28-linux-arm64.tar.gz \
  --strip-components=1
git add -A && git commit -qm "baseline: upstream iLands Runner 0.1.28"
for patch in /path/to/ilands-runner/patches/series/*.patch; do git am "$patch"; done

Useful subsets:

git apply --stat  patches/series/*.patch           # sizes, per patch
git apply --check patches/series/0018-*.patch      # dry run: does it fit?

Only apply what you intend to take. The upstream contribution is 0002, 0003, 0010, 0013, 0014, 0017, 0018 plus the passport.js hunk of 0004 — UPSTREAM.md gives the exact commands, and they exclude our test/ hunks, which do not exist in an upstream tree. Every other patch is this fork's own bookkeeping (the docs, the scripts, the reproduction build) or the temporary gateway shim; the subject line says which.

4.3 Verify

scripts/verify.sh

Runs, in order:

  1. upstream tarball SHA-256 against the pinned value,
  2. node --check over every shipped module (there is no npm run typecheck — see the boundaries),
  3. test/*.test.mjs,
  4. fork version must report exactly 0.1.28+opencode.1,
  5. harness prepare --harness opencode against your real credential store (needs an authenticated opencode session; the second run proves an empty store is rejected),
  6. update check must report disabledReason: "forked_build",
  7. scripts/build.sh — the reproduction proof, twice: pristine upstream + the series, and pristine upstream + patches/master.patch, each diff -red against git archive HEAD.

If step 7 prints both ok: lines, the series and the master patch are each sufficient on their own to rebuild this fork.

4.4 Change the fork, then regenerate

# edit app/dist/** as needed, then:
for f in $(find app/dist -name '*.js'); do node --check "$f"; done
node test/harness-surface.test.mjs        # or run all of test/*.test.mjs
git add -A && git commit -m "fix: describe the change in one line"
scripts/record-patches.sh                 # -> patches/series/*.patch + patches/master.patch
scripts/verify.sh                         # must be green before you trust it

patches/ is regenerated output and is excluded from its own series, so record-patches.sh never nests patches inside patches. The commit message becomes the patch subject, so write it as one.

4.5 Rebase onto a newer upstream release

  1. Bump UPSTREAM_VERSION / UPSTREAM_SHA256 / UPSTREAM_TARBALL / UPSTREAM_ROOT / BASELINE_COMMIT in scripts/lib.sh.
  2. scripts/fetch-upstream.sh — re-download and re-pin.
  3. Create the new baseline commit from the pristine tarball, then re-apply the series: git am patches/series/*.patch (expect conflicts only where their files moved).
  4. scripts/record-patches.sh && scripts/verify.sh.

Upstream auto-update can never do this for you: FORKED_RUNNER makes checkForUpdate return available: false with disabledReason: "forked_build", so 0.1.29 will never silently overwrite app/dist/.

4.6 Run it without installing anything

scripts/run.sh --version        # prints 0.1.28+opencode.1
scripts/run.sh doctor --harness opencode
scripts/run.sh harness prepare --harness opencode

scripts/run.sh <args> executes app/dist/cli.js with a Node 22 runtime (host node if present, otherwise the runtime bundled in the upstream tarball, extracted to .cache/).

4.7 Operate the service

ilands-runner service install     # writes the unit, daemon-reload, enable, restart
ilands-runner service status      # connection, daemon state, version
ilands-runner service restart
ilands-runner service logs --lines 50
ilands-runner doctor --harness opencode
systemctl --user status ai.ilands.runner

service install writes ~/.config/systemd/user/ai.ilands.runner.service pointing at ~/.local/bin/ilands-runner and a supervisor script that appends to ~/.ilands-runner/logs/daemon.log. The binary must be on the unit's PATH (~/.local/bin), and opencode must be on it too.

Note. service install waits 20 s for the daemon to report connected (DEFAULT_WAIT_TIMEOUT_MS in app/dist/service.js). A cold start includes a harness preflight of up to 15 s, so the command can exit non-zero while the service is in fact healthy — check service status before re-running it.

4.8 Pin the model

opencode acp has no --model flag (unlike opencode run), so the only lever is config. The Runner writes model into the heartbeat workspace's opencode.json only when the operator asks for it:

export ILANDS_RUNNER_OPENCODE_MODEL=opencode/mimo-v2.6-flash-free
ilands-runner service install      # bakes the variable into the systemd unit
ilands-runner service status       # confirm connection recovered

Left unset, no model key is written at all and opencode keeps its own default. Verify the pin took effect on the next heartbeat:

grep '"model"' ~/.ilands-runner/agents/<agent-id>/opencode.json

5. Layout

upstream-cache/    pristine upstream release tarball (gitignored, re-fetchable)
app/dist/          the fork — upstream tsc output plus our patches
app/dist/*.d.ts    hand-written type surface beside every module
app/package.json   upstream manifest (private, unlicensed — see FORK.md)
bin/ilands-runner  upstream release launcher
docs/              design notes and probe evidence
patches/           master.patch (one file) + series/ (reviewable commits)
scripts/           fetch / build / verify / record-patches / run
test/              behavioural tests for the opencode harness surface
../setup.md        the single instruction to paste into opencode

6. Quick start

scripts/fetch-upstream.sh    # verify and unpack the upstream release
scripts/verify.sh            # syntax, tests, harness prepare, patch reproduction
scripts/run.sh --version     # run the fork in place, nothing installed

7. Boundaries worth knowing

  • Gateway shim. Everything local is honestly opencode. The single exception is the gateway boundary, where the harness string is translated to/from codex because the server's enum rejects opencode with HARNESS_INVALID. Translation lives in exactly one module (app/dist/harness-wire.js) at three outbound call sites plus one inbound parse, so it can be deleted in a single upstream commit once PawLogic adds opencode to their enum — §3.3 is the deletion list. Evidence: docs/gateway-probe.md.
  • opencode plugin <module> only accepts published npm modules, so plugin install --target opencode writes <config-root>/skills/ilands-runner rather than registering a plugin. Same result for the agent.
  • Config isolation is XDG_CONFIG_HOME, not OPENCODE_CONFIG / OPENCODE_CONFIG_DIR. The adapter points it at <workspace>/.opencode-xdg; credentials stay untouched because they live under XDG_DATA_HOME. Override with ILANDS_RUNNER_OPENCODE_CONFIG_HOME.
  • No steering. opencode acp cannot inject into an active prompt turn, so heartbeat.steer logs delivered: 0.
  • No typecheck. No src/, no tsconfig.json, and upstream's .js.map files carry no sourcesContent, so verification is node --check plus test/.

The full list, with the reason for each: PATCHES.md.

8. Documents

File What it holds
FORK.md provenance, licensing status, the contribution plan
PATCHES.md what every patch changes and why, including the ACP-vs-serve decision
UPSTREAM.md the letter to PawLogic, ready to send
docs/gateway-probe.md the HARNESS_INVALID evidence, with method
patches/README.md which of the two patch artefacts to apply, and how
patches/master.patch the whole fork as one file
patches/series/README.md generated diffstat of the series