@skill-router/runtime
v0.1.14
Published
SkillRouter runtime — command registry, local daemon (127.0.0.1:19825) and browser bridge for sr-* CLI adapters. Fork of the published @jackwener/opencli 1.8.6 npm artifact (Apache-2.0, see NOTICE); wire-compatible with additive SkillRouter handoff and co
Readme
@skill-router/runtime (sr-runtime)
SkillRouter's local runtime: command registry, executor, plugin system, local daemon (127.0.0.1:19825) and browser bridge. Forked from the published @jackwener/opencli 1.8.6 npm artifact with selected 1.8.7/main framework changes ported (Apache-2.0, see NOTICE). It ships the bare runtime only, without the upstream site adapters; SkillRouter adapters are installed separately as plugins. Basic commands stay wire-compatible with the upstream protocol.
It is normally installed and upgraded together with @skill-router/cli by sr update; no separate steps are needed.
sr-runtime plugin install file:///abs/path/sr-arxiv # install an adapter
sr-runtime sr-arxiv paper 1706.03762 # run an adapter command directly
sr-runtime plugin migrate # adopt existing plugins from ~/.opencli/plugins
sr-runtime plugin verify [name] # check installed plugins against their content hashes
sr-runtime plugin lint /abs/path/sr-x # static pre-release checks for an adapter
sr-runtime sitedata sr-x --show # show the source and version of a site's calibration data
sr-runtime sitedata sr-x --refresh # refresh a site's calibration data now
sr-runtime verify sr-x # run the whole-site fixtures (write commands stop at prepare)
sr-runtime doctor # check daemon and Bridge connectivityplugin verify exits non-zero for tampered, missing and unverified plugins.
Site calibration data (sitedata)
Every plugin ships a sitedata.json that is data only: URLs, selectors, labels, mappings, wait limits,
fixtures and pace. After a command finishes, the runtime spawns at most one detached background process every
15 seconds to check for updates by ETag; the command itself never waits on the network. Selection order: a
fresh cache under 24 hours old → a valid cache under 30 days old → the copy bundled with the plugin; a change
in the bundled version discards the old cache. Remote documents must pass schema, regular-expression safety
and plugin domain allowlist validation; a failure never replaces the last-known-good copy, and a document
older than the bundled version is never accepted. SR_NO_SITEDATA_REFRESH=1 disables the background refresh;
SR_SITEDATA_REFRESH_INTERVAL_MS adjusts the check interval (5 seconds to 24 hours).
Sites that declare pace keep persistent accounting in ~/.skillrouter/state/pace/<site>.json; when that
state cannot be written, the command is refused rather than released. verify checks row counts, columns and
pattern/notEmpty for read commands, and for write commands strips the submit arguments and checks only
prepare_assert. plugin lint checks command/fixture alignment, literal sleeps, disabled automation surfaces
and out-of-domain URLs.
Browser sessions
Browser commands reuse a persistent site session by default; precedence is --site-session >
SR_SITE_SESSION > the command's declaration > the default. A read command first makes one non-waiting local
lease attempt: when idle it reuses site:<site>, when busy it switches to an isolated tab and prints
[sr] site:<site> busy → isolated tab (SR_QUIET=1 silences the notice). A read that loses its lease
mid-command discards its result and reruns once in full on an isolated tab; if that fails too, it exits 65
with the original error. A write command returns SESSION_BUSY (exit 75) immediately when the session is busy
and is never rerun automatically. After a process crash the lease is reclaimed by TTL within about 45 seconds.
Read commands must therefore be idempotent.
After a persistent command succeeds, the runtime sends control-release: the final page and the site-to-tab
mapping are kept, but debugger control is detached at once and the extension icon goes from LIVE to IDLE.
Only an explicit tabs close really closes the tab.
The browser subcommand picks its window default by host: a person in an interactive terminal gets the
foreground; when driven by an agent, MCP or a pipe it runs in the background and never steals focus.
--window, --show and login-wall takeover are unaffected; pick and blocking record are always foreground.
Failure handoff and login walls
When an adapter command fails hard, the runtime writes a handoff bundle to
~/.skillrouter/handoff/<timestamp>-p<PID>-<UUID>.json and refreshes latest.json (mode 0600, kept seven
days, never uploaded). When takeover is safe the bundle carries the adapter lease, the page target, a
screenshot and candidate elements; inspect the scene with
sr-runtime browser <session> --surface adapter state --tab <target>. Before taking over a write command,
confirm whether the first attempt partially succeeded; never blindly replay it. Login and CAPTCHA go to a
human; rate limits and risk-control decisions are never taken over.
On a login wall the runtime brings the tab to the foreground, shows a card asking a human to log in, and
reruns the command once after the login lands. SR_REQUEST_HELP=off disables this.
sr report-fix --pick lets you pick the repair target on the live page at the breakpoint and uploads the
chosen selector and its structural fingerprint to the SkillRouter repair service. Apart from the optional run
signal below, it is the only command in this runtime that sends data out on its own.
Before browser <session> network --detail <key> writes to its cache or output, sensitive headers such as
authorization, cookie, token, password, secret and session id, and credential-shaped values, are replaced with
<redacted>; URL query parameters are redacted the same way.
Optional anonymous run signal
Off by default. After sr signal on, the runtime asynchronously sends a fixed five-field record
{site, cmd, sitedata_version, ok, code} when a command completes; it carries no arguments, URLs, page
content, output or credentials, and a failed send never affects the command. SR_SIGNAL=1|0 switches it
explicitly. Details: https://skillrouter.org/us/privacy .
Action overlay, recording and exploration audit
SR_SHOW_ACTIONS=1 or --show displays a synthetic cursor, target highlight and action labels (decoration
only, not audit evidence).
sr-runtime browser work pick --prompt "click the publish button" # returns ref + selector (300 s, Esc cancels)
sr-runtime browser work record --purpose "demo a search" # writes trace.json + states/ after Finish
sr-runtime browser work record --values # record typed values (never passwords, OTP, card numbers, SSN)
sr-runtime browser work explore start # local black box: command names and redacted arguments only
sr-runtime browser work explore stop
sr-runtime browser audit <record-dir> --json # verdict: read_pure / write_actions / violationsaudit exits 1 on a violations verdict. network, analyze and wait xhr read the network capture buffer
and cannot leave complete network evidence, so an explore session that uses them is always judged violations.
Recordings are written with mode 0600 to ~/.skillrouter/records/<timestamp>-<host>/, kept seven days,
capped at 500 steps or 5 MiB. URLs keep origin, path and parameter names; sensitive parameter values are
redacted. trace.json is untrusted data supplied by the web page; states/*.txt are full-page text snapshots
that may contain personal information from the page and should not be uploaded. Google/Microsoft sign-in,
password managers, Alipay/Taobao login, PayPal, Stripe and any host containing bank refuse recording by
default; SR_RECORD_BLOCK_HOST=host,*.suffix extends the blocklist, and SR_RECORD_ALLOW_HOST=a,b is an
explicit risk acceptance that takes precedence over it. The bank substring rule has a clear ceiling: bank
sites whose domain lacks the word and punycode domains not on the list are not recognised automatically.
Recordings and exploration files stay on this machine, are never uploaded and must not be placed in the
four-field feedback body; they leave the machine only when the owner explicitly submits a candidate later.
Domain allowlist and plugin integrity
Every adapter command runs inside a domain allowlist: the plugin manifest domains ∪ the sitedata.json
domains. Navigations and requests the runtime itself initiates outside the allowlist are refused (exit 68,
handoff takeover_policy.mode = stop); navigations the page performs on its own are only recorded as landing
URLs. A plugin that declares no domains gets a single warning; SR_STRICT_DOMAINS=1 turns that into a refusal
and installs a page-side fetch/XHR observer (count only, never blocking). about:, data:, blob: and
chrome-extension: pass; javascript: and chrome: do not. A manifest whose domains contain illegal
entries (a scheme, path or port, loopback or private addresses, a public-suffix wildcard) is refused by
plugin install/update; already-installed plugins are enforced on their valid subset. Matching is exact: a
full hostname matches only that host, and *.example.com matches subdomains only.
plugin install/update records a content hash of the plugin directory in ~/.sr/plugins.lock.json
(excluding node_modules, .git and *.log); plugin verify compares against it, the executor warns once
per process about a mismatching plugin, and under SR_STRICT_INTEGRITY=1 only verified plugins run. A
local-path install is a symlink hashed through its real directory; after editing its code, run
plugin update <name> to re-record it. Threat model: the domain guard catches hijacked pages, poisoned
sitedata and honest adapter mistakes; adapter code runs inside the runtime process, so this is not a sandbox
against a malicious plugin author, which is what plugin lint and the integrity hash are for. The lock file
lives in the user's home directory and only detects one-sided tampering.
Directories and environment variables
The runtime's own state lives under ~/.sr/ (plugins, plugins.lock.json, monorepos, the update-check cache);
logins and configuration shared with upstream opencli stay under ~/.opencli/, so both can coexist.
SR_DAEMON_TOKEN optionally enables a daemon handshake token (off by default).
