WolOS
Reference contents

Reference · Decisions

0041: Ruby scripts run like shell scripts

A Ruby script is checked and run the same way as a shell script, as the person, through the helper gate.

Status: accepted (main 2026-11-03, sandbox scope as stated below; lead orders 2026-11-03: "Ruby must be supported like .sh scripts", remote execution in a sandbox; drafted by a worker for main). Builds on 0020 (helpers and the gate), 0024 (first-use malware check), 0032 (optional modules), 0036 (Crystal userland).

On this page

Context

WolOS treats a shell script as a first-class program: it has a header rule, a lint pipeline (docs/RULES-system.md, sh -n, shellcheck), CI steps, exec-bit checks, and when it calls wol with WOL_HELPER=<name> it goes through the helper gate (policy, approval sheet, audit log; docs/modules/automation.md). Nothing in the product ran a script on request: there was no wol verb for a script file, and remote runs existed only in developer tooling. Ruby had none of this: tools/check-headers.sh refused a ruby shebang, the rule tests and CI ignored .rb, and no Ruby interpreter was on the system. ADR 0036 makes Crystal the language of apps and tools; Crystal compiles, so it is a poor fit for a file you edit and run, which is what a script is.

Decision

  • Ruby is the scripting language beside sh. Crystal stays the language of apps and tools (ADR 0036); Lua stays the plugin sandbox (ADR 0031). A .rb file is a script wherever a .sh file is: a bundle entry, a widget source, a plugin or hotkey command, a helper with WOL_HELPER, a file run over ssh. Ruby calls WolOS through the wol CLI with --json, as sh does; there is no Ruby library over libwol (ADR 0036, lock-in).
  • One runner for every script language: wol script (crates/controller/src/script.rs). wol script check <file> parses without running (sh -n, bash -n, ruby -wc), then runs the language's linter when it is installed (shellcheck; RuboCop with the explicit cop list RUBOCOP_COPS). wol script run <file> [args] runs it as the person, never as root, with an argv (never a shell string), a clean environment (env -i keeping only KEPT_ENV: who, where, language, terminal and the desktop session; a fixed PATH; no tokens or keys from the calling shell) and WOL_HELPER=<file name>, so every change the script asks of wol meets the gate. wol script on <host> <file> [args] sends the file on ssh's stdin (BatchMode, no terminal) and runs it on the other computer the same way: same clean environment, same helper name, so the other WolOS applies its own gate. Each run is one audit record (script run or script on, the exit code; the script's arguments are not recorded). What differs between languages is one table, KINDS (kind by shebang, else by file name ending). Helpers may list the kinds and check a file (Read); run and on execute code and stay for people (not in HELPER_VERBS, not offered by wol mcp).
  • What "sandboxed" means in this release, stated plainly: the clean environment, no root, no shell parsing, the helper gate for every wol change, and the audit record. It is not a file-system jail: the script can read and write what the person can. The workspace jail (wol jail run, ADR 0032) is the planned isolation and will wrap wol script run when it lands; until then a script is as trusted as the person running it, like a shell script.
  • The interpreter ships in the image; the linter in the developer module. ruby from the base distribution's repositories at the release snapshot (ADR 0016) in the desktop package list, so a fresh computer and an ssh session can run a .rb file; rubocop in the dev module (files/usr/share/wolos/modules/dev.json). Not from mise: a system script needs the pinned package, not a user-side build.
  • No Ruby root helpers. Scripts under files/usr/lib/wolos, units and hooks stay POSIX sh (STANDARDS §2). RB-030 keeps ROOT_RUBY empty; adding one needs a new ADR and ruby in wolos-system's depends.
  • Rules: docs/RULES-ruby.md (RB-nnn, crates/controller/tests/rules_ruby.rs): header, frozen string literals, argv-only commands, no eval, no object-building decoders, default gems only (STDLIB), timeouts, no fixed /tmp, no world-writable modes, ruby -wc clean, RuboCop with one explicit list (tools/ruby/rubocop.yml, DisabledByDefault, NewCops: disable), an interpreter census per folder. CI installs ruby and rubocop and sets WOLOS_REQUIRE_RUBOCOP=1, so a missing tool fails instead of skipping. tools/check-headers.sh accepts a ruby shebang and parses the file with ruby -wc.
  • Malware check: the background pass looks at .rb files as it looks at .sh files (xguard::sweep::in_scope). The first-use check scans the interpreter once, not each script it reads; that gap is the same for sh and Ruby (security.md, interpreters).

Consequences

the image grows by the interpreter (about 17 MiB installed for ruby 3.4 plus its rubygems dependency at the time of writing; the measured number belongs in the release notes) and gains a second scripting interpreter that the first-use check does not see into. wol script is the one place to add a language: PowerShell (planned) is one more row in KINDS. Ruby's default gems move between releases; RB-009 names the allowed list, so a snapshot bump that drops one fails loudly. RuboCop releases add cops; the explicit list keeps CI stable and RB-020 fails when a listed cop disappears. Phase 2 (not this ADR): the jail around wol script run, and completions for wol in the shells (there are none for any verb yet).

From the developer documentation, wonderlandos/wolos docs/adr/0041-ruby-scripts.md.