Run the same binaries that ship to the board on a developer machine: HAL with virtual (or host) peripherals, os-server, the agent bridge, and the web UI.
There is no build tag and no second code path. Only the device-absolute paths
move, through one env var each (system/lib/syspath on the Go side, HAL_* on
the Python side). Unset env = the board's behaviour, byte for byte — which is
what makes the tested binary the shipped binary.
Source of truth: this doc reflects the code. If they disagree, the code wins.
- Not a physics simulator. No mass, inertia, collision or torque. A pose that would jam on a real body succeeds here.
- Not a claim that rendered geometry is correct. The repo carries no calibrated joint hierarchy, pivots or CAD zero offsets.
- Not
robots/sim. That is a separate minimal body (motion+systemonly) used to prove HAL mounts exactly what aROBOT.mddeclares — seerobots/sim/ROBOT.md. The laptop simulator boots the full lamp body.
| Need | Check with | If missing |
|---|---|---|
codex CLI |
codex --version |
Install it yourself — nothing here installs it |
| codex logged in | ls ~/.codex/auth.json |
codex login |
ffmpeg |
ffmpeg -version |
Needed for music playback |
uv |
uv --version |
Builds HAL's hal/.venv. make sim creates it on first run and re-syncs it whenever hal/uv.lock or hal/pyproject.toml moves ahead of it. If that first sync fails compiling insightface, see insightface fails to build below |
node + npm |
node --version |
Needed for make web-dev only |
Only codex works off-device. Other runtimes have no *-dev target.
mkdir -p /tmp/autonomous-os/config
cp scripts/dev/config.example.json /tmp/autonomous-os/config/config.json
chmod 600 /tmp/autonomous-os/config/config.json$EDITOR /tmp/autonomous-os/config/config.json| Key | Value | Missing → |
|---|---|---|
llm_api_key |
Your provider key | No TTS, no STT, no Gemini Live, no image description. The agent still answers text |
llm_base_url |
OpenAI-compatible base, e.g. https://…/api/v1/ai/v1 |
Same as above |
| Key | Value |
|---|---|
admin_password_hash |
bcrypt hash (cost 10) of your login password — not the password |
session_secret |
Leave empty — os-server writes a random one on first login (system/server/session/session.go) |
| Key | Default | Effect |
|---|---|---|
deepgram_api_key |
"" |
Set it to use Deepgram STT instead of AutonomousSTT |
realtime.enabled |
true |
false → every turn goes to the main agent (slower, still works) |
wakeword |
true |
false → always-listening, no wake phrase needed |
tts_voice |
Rachel |
ElevenLabs voice |
stt_language |
en |
STT language |
timezone |
Asia/Ho_Chi_Minh |
IANA zone |
device_type · agent_runtime · set_up_completed — rewritten by
os-dev-seed.sh on every run.
device_id · mqtt_endpoint · mqtt_username · mqtt_password ·
fa_channel · fd_channel — the backend uplink is off — see The backend uplink is off.
telegram_bot_token — if you copied a real device's config, blank this. One
bot token cannot have two pollers; the laptop would steal the device's messages.
Four terminals, in this order.
make sim SIM_MEDIA=host # 1. HAL :5001
make codex-dev CODEX_PORT=18892 # 2. agent bridge :18892
make os-dev CODEX_PORT=18892 # 3. os-server :5000
make web-dev # 4. web UI :5173 (optional)| # | Wait for this line before starting the next |
|---|---|
| 1 | Simulation mode enabled for device 'lamp' (media=host) |
| 2 | [codex-gatewayd] listening on ws://127.0.0.1:18892/codex/ws/ |
| 3 | Codex connected |
| 4 | VITE … ready |
Rules:
- Start
make simfirst — os-server waits up to 120s on HAL's/health. codex-devandos-devmust use the sameCODEX_PORT. The default18792collides with an openclaw gateway if one is installed.- macOS asks for Microphone and Camera on the first
SIM_MEDIA=hostrun. Grant them, then re-runmake sim. - Drop
SIM_MEDIA=hostif you do not need voice — the stack still runs, silently.
# 1. os-server is up
curl -s :5000/api/health/live
# 2. HAL is up and using host devices
curl -s :5001/simulator/state | jq # expect media:"host", media_reasons:{}
# 3. The os-server → HAL path works (no LLM, ~50ms)
curl -s -X POST :5000/api/sensing/event -H 'Content-Type: application/json' \
-d '{"type":"voice_command","message":"turn on the light"}'
# 4. The agent answers (~15s) — reply appears in terminal 3
curl -s -X POST :5000/api/sensing/event -H 'Content-Type: application/json' \
-d '{"type":"voice_command","message":"introduce yourself"}'
# 5. Speak into the mic: say "hey lamp, what time is it"
grep '\[turn\] route=' /tmp/autonomous-sim/log/server.log | tailOpen:
http://127.0.0.1:5001/simulator— the 3D bodyhttp://localhost:5173/monitor— Flow Monitor (localhost, not127.0.0.1)
Wake words follow the agent name. Write:
echo '- **Name:** Lumi' > ~/.codex/workspace/IDENTITY.mdPicked up within 5 seconds, no restart. Now hey lumi works, alongside
hey lamp and hey autonomous.
| Target | Does | Does not |
|---|---|---|
make sim |
Boots HAL on the lamp body with virtual (or host) peripherals | — |
make hal-install |
uv sync — an exact reconcile of hal/.venv, dropping anything absent from uv.lock. make sim syncs by itself with --inexact, which keeps a hand-installed pytest |
Does not install the dev extra (pyflakes); add --extra dev by hand |
make codex-dev |
Only runs os-server codex-gatewayd: a loopback WebSocket listener that spawns one codex exec per turn |
No onboarding, no presync, no skill sync |
make os-dev |
Three things in sequence: os-dev-build (compile), os-dev-seed (prepare the state dir), then run the API — which also does all agent provisioning: presync.sh, seeding AGENTS.md/SOUL.md/KNOWLEDGE.md/HEARTBEAT.md, downloadSkills(), the skill watcher |
— |
make web-dev |
Runs Vite in nginx's place (os-server serves no HTML) | — |
If the workspace is empty or the agent has no persona, look at os-dev, not
codex-dev.
os-dev-seed is a prerequisite target of os-dev, not a separate step you
normally run. It touches only the state dir, never starts a process:
| It does | It does not |
|---|---|
Refuses to continue if config.json is missing, printing the cp command |
Create or overwrite config.json — that file is yours |
Rewrites device_type, agent_runtime, set_up_completed in it |
Touch any other key |
Warns about empty llm_api_key / admin_password_hash |
— |
Seeds config/bootstrap.json (once) so skills can download |
— |
Backs up an existing config.toml to config.toml.pre-os-dev (once) |
— |
make os-dev-seed runs it alone — useful to re-check the state dir without
booting the server.
| Path | |
|---|---|
| Template (in the repo) | scripts/dev/config.example.json |
| Live config | $OS_STATE_DIR/config/config.json — default /tmp/autonomous-os/config/config.json |
| OTA metadata (auto-seeded) | $OS_STATE_DIR/config/bootstrap.json |
| Agent workspace | $CODEX_HOME/workspace/ |
One config file serves both HAL and os-server, the same role
/root/config/config.json plays on a board: os-server resolves
config/config.json relative to its cwd (make os-dev cds into the state dir),
HAL reads OS_CONFIG_PATH.
The seed copies the template once. Later runs only rewrite device_type,
agent_runtime and set_up_completed; your edits survive.
| You want | You need |
|---|---|
| The agent answers text | nothing — codex uses its own login |
| Skills install automatically | nothing — the CDN objects are public |
| The device speaks (TTS) | llm_api_key + llm_base_url |
| Voice input (STT) | llm_api_key, or deepgram_api_key |
| Sub-second voice (Gemini Live) | llm_api_key + realtime.enabled |
| The agent sees images | llm_api_key |
One key covers all of it: the realtime key falls back to llm_api_key and its
endpoint to llm_base_url + /ws/gemini (hal/config.py), so no separate Google
credential is involved.
SIM_MEDIA decides whether HAL opens the developer machine's peripherals.
virtual (default) |
host |
|
|---|---|---|
| Camera | synthetic calibration scene, deterministic | the Mac's webcam (AVFoundation) |
| Mic / speaker | virtual device ids, never passed to sounddevice |
real devices via PortAudio |
| Voice pipeline | inert stub | the real one — VAD, Silero, STT, realtime agent, wake word, [turn] route=… dispatch |
| Music | full pipeline into a null sink | audible |
| Permissions | none needed | macOS asks for Microphone + Camera |
| Suitable for | tests, CI, offline work | manual end-to-end checks |
The gate is state.simulation_audio (hal/server.py), not the raw media string.
That matters: _sim_audio_probe() runs first and flips the flag back to
virtual when a device is missing, busy or permission-denied, so a refused
microphone lands on the stub with a logged [sim-media] reason instead of a real
pipeline reading a dead device. routes/voice.py keys off the same flag, so the
boot path and POST /voice/start can never disagree.
Host mode never hard-fails. GET /simulator/state reports the outcome per
subsystem:
{"media":"host","media_camera":"host","media_audio":"host","media_reasons":{}}media is "host" only when both are; media_reasons carries the actionable
why for each downgrade. On macOS the permissions live under
System Settings → Privacy & Security → Camera / Microphone and must be granted to
the terminal app running HAL. Enumeration is not permission — the device list is
populated either way and only the first real read fails, which is why HAL probes
at boot rather than failing a turn later.
With "wakeword": true you must address the device. Prefixes are
hello hey hi alo okay ok wake up, combined with:
- the agent name from
$CODEX_HOME/workspace/IDENTITY.md - the device type (
lamp) - the permanent alias
autonomous
To name the agent, write:
- **Name:** Lumiinto $CODEX_HOME/workspace/IDENTITY.md. WatchIdentity polls that file every 5
seconds and pushes fresh wake words to HAL — no restart needed. HAL merges
them with the permanent set, so hey lumi, hey lamp and hey autonomous all
work. With no IDENTITY.md, the name falls back to device_type.
A wake phrase is accepted at the start or end of any sentence in the turn. Mid-sentence is rejected — a device name inside a sentence is people talking about the device.
After an authorized turn a follow-up window opens
(HAL_WAKEWORD_FOLLOWUP_TIMEOUT_S, code default 20s; the lamp image sets 60)
and resets after every authorized turn, so a continuous conversation never
needs the name again. Set "wakeword": false for always-listening.
They are different things and easy to confuse.
Served by HAL. Only when HAL_SIMULATE=1 and the body is lamp; otherwise
404. An orbitable view of the lamp's rigged GLB — five joint nodes named exactly
as HAL names them (base_yaw, base_pitch, elbow_pitch, wrist_pitch,
wrist_roll) — driven by live joint values, plus recording playback and LED
controls that call the same /servo/* and /led/* endpoints a skill calls.
Drag to orbit, scroll to zoom, double-click to reset. The camera opens on the lamp's face,
three quarters round from dead-on, aims at the centre of whatever geometry
loaded and backs off far enough to frame it, so the head does
not run off the canvas on a wide window. Zoom spans 0.3x-4x of that distance
through a 38 deg lens, and elevation runs from level with the floor to just
short of straight overhead. The body stands on a
1.6 m ground grid (50 mm cells, brighter every 250 mm, fading to a circular
edge) so pan and lean are readable against something; the canvas renders at the
display's pixel ratio, capped at 2x. The LED ring reads
/simulator/pixels (the strip buffer itself), not /led/color, because the
latter reports an effect's static base colour and would render breathing, candle
and rainbow as one dead colour.
The rendered pose is a visual response to live joint values, not a claim that it is physically correct.
Flow Monitor, Settings, Logs — the same SPA that runs on a board.
os-server serves no HTML: on a device nginx serves web/dist and proxies
/api and /hw to :5000. make web-dev puts Vite in nginx's place, with
LAMP_PROXY naming the device the SPA talks to. A .env in system/web/ still
wins (vite.config reads it before process.env), so pointing at a real Pi is
unchanged.
Vite binds
[::1]only —127.0.0.1:5173is refused and looks like the server never started. Uselocalhost.
Log in with the password whose bcrypt hash is in admin_password_hash.
Alternatively append ?llm_api_key=<the key in config.json> — but note this
misses on the first load of a fresh tab (api.ts initialises its token from
sessionStorage at module load, and AuthGate's effect runs before App's
useBearerFromQuery), so navigate to /monitor a second time in the same tab.
There is no third way: an empty admin_password_hash is not an open door.
VerifyAdminPassword (system/device/config_update.go) refuses outright when
no hash is set, and it refuses off-device exactly as it does on a board — the
simulator runs the shipped binary, so it has no auth bypass to enable.
A config copied from a device carries that device's hash. To log in with a
password of your own instead, generate a cost-10 hash and put it in
admin_password_hash:
htpasswd -bnBC 10 "" 'your-password' | tr -d ':\n'htpasswd ships with macOS. It emits a $2y$ prefix where Go's bcrypt writes
$2a$; both verify against the same algorithm and CompareHashAndPassword
accepts either, so paste the output as-is. The leading "" is the username
field, which os-server does not use — tr strips it along with the newline.
Boot on the lamp body mounts 12 routes and skips one:
mounted=['audio','bluetooth','camera','emotion','led','music','scene',
'sensing','servo','speaker','system','voice'] skipped=['buddy'] failed_required=[]
- the whole route surface — still decided by
ROBOT.md, not by the simulator - safety gates —
motion.max_speed, the whole-body stability gate (motion.max_cog_offset_mm), the LED brightness ceiling./servo/moverejects unknown joint names, but no bound clamps a joint angle to a range. - the declaration-driven mount plan
- emotion presets, scene, music, bluetooth, system routes
TrackerService, volume, user/stranger stores
| Subsystem | Board | Simulator |
|---|---|---|
| Servo | feetech over a serial bus |
MockMotionService — joints are floats in a dict; moves interpolate over their commanded duration and block until arrival; aim/nudge obey SAFETY.md's max_speed; CSV recordings replay through the same 30 Hz stretch-and-resample timing, so an animation takes the same wall-clock time — and through the same resample_recording, so the stability gate refuses in make sim exactly the clip a physical body refuses |
| LED | WS2812 over SPI | _MemoryStrip — a real pixel buffer in RAM, effects run for real, still funnelled through the same brightness clamp |
| Camera | opencv/V4L2 |
virtual (synthetic scene) or host (the Mac's webcam) |
| Sensing | SensingService + face recognition |
VirtualSensingService — keeps presence state and the route contract; no perception-service calls, no face identity |
| Board profile | reads the device tree | the inert sim profile |
| Music output | aplay (ALSA) or paplay (PulseAudio) |
ffmpeg's AudioToolbox output device on macOS |
| Voice enroll capture | arecord over the ALSA alias |
PortAudio (sounddevice) on the same input device the voice pipeline records with; the WAV carries its own rate and the recognizer resamples |
| GELF logging | ships to the log server | off |
| GPIO button / touchpad | real | skipped (_board_id != "sim" gate) |
HAL_BOARD=sim is refused without HAL_SIMULATE=1 — HAL will not boot physical
drivers on a virtual board.
Every accessor keeps its production default and is overridden by exactly one env
var. make os-dev and make codex-dev share one set so the bridge and its
client can never disagree.
| Env | Board default | Purpose |
|---|---|---|
CODEX_HOME |
/root/.codex |
Root of every codex path, in the client and the gatewayd |
CODEX_PORT |
18792 |
Bridge listener + WSURL |
CODEX_WS_TOKEN |
autonomous_codex_token |
Bearer token between os-server and the bridge |
OS_AGENT_HOME |
/root |
Root a Telegram coding session resolves ~ against |
OS_AGENT_STATE_PATH |
/root/config/agent_state.json |
Runtime-switch history |
OS_BOOTSTRAP_CONFIG |
/root/config/bootstrap.json |
Source of metadata_url — the skill-zip base |
OS_LOG_FILE |
/var/log/os-server.log |
os-server's rotating log |
OS_BACKEND_UPLINK |
on |
Backend ping + MQTT. make os-dev sets off |
OS_HAL_LOG_FILE |
/var/log/hal/server.log |
Where the web UI reads HAL's log from |
OS_AGENT_BRIDGE_LOG |
"" (use the journal) |
File to read the bridge from when there is no systemd |
DEVICES_DIR |
/opt/devices |
Root of robots/<type>/ |
Only "off" disables OS_BACKEND_UPLINK — any other value, including a typo,
keeps it on, so a fleet upgrade can never silently take devices off their uplink.
HAL already read all of these; the sim target is what points them somewhere a
laptop can write. Their failures surface far from their cause — the TTS cache one
appeared as POST /voice/speak 409 with the real PermissionError buried in a
background thread's traceback — which is why they are set as a block.
| Env | Board default | make sim |
|---|---|---|
OS_CONFIG_PATH |
/root/config/config.json |
$OS_STATE_DIR/config/config.json — the file shared with os-server |
HAL_SNAPSHOT_DIR |
/root/.<runtime>/media/hal-snapshots |
$CODEX_HOME/media/hal-snapshots — must sit under the agent's own home, or it cannot read the frame back and os-server cannot serve its thumbnail |
HAL_CODEX_WORKSPACE_DIR |
/root/.codex/workspace |
$CODEX_HOME/workspace — the realtime agent's memory.jsonl derives from it |
HAL_LOG_DIR |
/var/log/hal |
$SIM_STATE_DIR/log |
HAL_SNAPSHOT_PERSIST_DIR |
/var/lib/hal/snapshots |
$SIM_STATE_DIR/snapshots |
HAL_TTS_CACHE_DIR |
/var/lib/hal/tts_cache |
$SIM_STATE_DIR/tts_cache |
HAL_CALIBRATION_DIR |
/var/lib/hal/calibration/… |
$SIM_STATE_DIR/calibration/… |
HAL_USER_BEARING_PATH |
/var/lib/hal/user_bearing.json |
$SIM_STATE_DIR/user_bearing.json |
HAL_FACE_HEIGHT_PATH |
/var/lib/hal/face_height.json |
$SIM_STATE_DIR/face_height.json |
HAL_USERS_DIR / HAL_STRANGERS_DIR / HAL_VOICE_STRANGERS_DIR |
/root/local/… |
$SIM_STATE_DIR/… |
HAL_BT_STATE_DIR / HAL_VOLUME_STATE_PATH |
/var/lib/hal, …/.volume |
$SIM_STATE_DIR/… |
HAL_DL_STALL_LOG |
/root/local/dl_ws_stall.log |
$SIM_STATE_DIR/dl_ws_stall.log |
HAL_SIMULATE / HAL_BOARD / HAL_SIM_MEDIA |
unset | 1 / sim / $(SIM_MEDIA) |
Two device defaults are deliberately left alone: /root/local/models and
/opt/piper are read-only model paths. Absent on a laptop, the feature that
needs them simply stays off.
| Knob | Default |
|---|---|
DEVICE_TYPE |
lamp |
SIM_MEDIA |
virtual |
SIM_STATE_DIR |
/tmp/autonomous-sim |
OS_STATE_DIR |
/tmp/autonomous-os |
OS_AGENT_RUNTIME |
codex |
CODEX_HOME |
$HOME/.codex |
CODEX_PORT |
18792 |
CODEX_BIN |
first codex on PATH |
OS_BACKEND_UPLINK |
off |
HAL_PORT |
5001 |
LAMP_PROXY |
http://127.0.0.1:5000 |
Every path in the environment tables above is a knob too — each is its own ?=
variable, so one can move without touching the rest:
make sim HAL_TTS_CACHE_DIR=/Volumes/sd/tts # one path
make sim SIM_STATE_DIR=~/work/sim-a # all of themAn exported shell variable wins over the default for the same reason. Two pairs
stay coupled through a variable rather than a repeated literal, so overriding one
half moves the other: OS_HAL_LOG_FILE derives from HAL_LOG_DIR (HAL writes,
os-server reads it for the web UI's HAL tab), and codex-dev's tee target is
OS_AGENT_BRIDGE_LOG itself (bridge writes, os-server reads it for the Agent tab).
make os-dev sets OS_BACKEND_UPLINK=off, which stops two things: the 15s
status ping (system/device/status_reporter.go) and the MQTT command channel
(system/server/mqtt.go).
This is not a simulator preference — it is a safety interlock. The backend
identifies a device by its llm_api_key, not by device_id, so a laptop
holding a copy of a device's config is indistinguishable from that device.
Measured with both running:
- the ping overwrote the real device's
local_ip,mac,versionandskillsevery 15 seconds - the MQTT client ids — derived from the
device_idthe backend hands back — collided, and the two clients evicted each other from the broker about once a second, indefinitely
Config edits cannot avoid this: blanking device_id gets the same id back, and
blanking mqtt_endpoint is undone by the ping response, which writes the broker
config to disk and triggers restartMQTT. Blocking the subscriber is enough for
the whole chain — both publisher clients connect lazily, and the chat stream only
publishes for runs a backend chat.send created.
Nothing a developer needs goes through the uplink: the web UI, Flow Monitor, voice pipeline, agent, skills and Telegram (polled directly by the device) are all local. What is lost is remote control: phone-app chat, remote OTA, remote skill installs, and the Slack webhook proxy.
make os-dev OS_BACKEND_UPLINK=on re-enables it. Only do that when the laptop is
not carrying a live device's credentials.
| Web UI tab | Off-device |
|---|---|
| HAL | ✅ $SIM_STATE_DIR/log/server.log |
| OS | ✅ $OS_STATE_DIR/os-server.log |
| Agent / Agent Service | ✅ $OS_STATE_DIR/codex-gatewayd.log — make codex-dev tees the bridge to a file (2>&1, because Go's slog writes to stderr) since a laptop has no journal |
| Bootstrap | ❌ the OTA worker has no off-device target |
| Claude Desktop Buddy | ❌ a separate Mac app that writes no log here |
| Symptom | Cause |
|---|---|
codex CLI not found on PATH |
The agent is not installed — see Step 1 |
curl :5000 returns a binary plist or 403 |
os-server is not running — macOS AirPlay Receiver answers on *:5000. Turn it off (System Settings → General → AirDrop & Handoff) or change httpPort |
listen failed: address already in use |
CODEX_PORT left at 18792, which an openclaw gateway holds |
bad handshake (status 404) |
codex-dev and os-dev disagree on CODEX_PORT |
dial 127.0.0.1:5001: connection refused |
HAL is not up yet |
127.0.0.1:5173 refuses the connection |
Vite binds [::1] — use localhost:5173 |
| Speaking does nothing | Missing SIM_MEDIA=host, or macOS denied the microphone. Check media_reasons in /simulator/state |
Voice enroll returns 503 needs a real microphone |
SIM_MEDIA=virtual — enroll refuses to open the host mic in a mode that promises not to |
Voice enroll returns 400 vad_removed_all |
The clip held no speech. Read the phrases aloud, closer to the mic, for the full countdown |
| STT hears the wrong name | flux-general-en mis-hears proper nouns; "hi lamp" has come back as "hi lance", and a miss drops the whole turn silently. Wake terms are sent as STT boost terms, but say the name clearly |
POST /voice/speak 409 + PermissionError: /var/lib/hal |
HAL_TTS_CACHE_DIR not set — an old sim target |
| "Sorry, I can't play that right now" | Music: macOS has no aplay/paplay. Needs ffmpeg on PATH for the AudioToolbox route |
POST /audio/volume returns 503 |
Expected — macOS has no ALSA mixer |
| Agent calls itself "Codex", no persona | $CODEX_HOME/workspace must hold AGENTS.md, SOUL.md, KNOWLEDGE.md, HEARTBEAT.md. These come from os-dev, not codex-dev |
Empty workspace, no seeded file log |
set_up_completed is not true, so the startup sequence never ran |
skill download skipped: no ota_metadata_url |
config/bootstrap.json missing |
uv sync fails building insightface, ld: library 'c++' not found |
See below |
insightface compiles C++, and the interpreter uv picked decides which SDK
that compile targets. A Homebrew Python bakes the SDK path it was built against
into its own sysconfig, so on a machine whose macOS has moved on, the build
points -isysroot at an SDK that is no longer installed:
Compiling with an SDK that doesn't seem to exist:
/Library/Developer/CommandLineTools/SDKs/MacOSX13.sdk
ld: library 'c++' not found
Setting SDKROOT does not help — the stale flag comes from sysconfig, not the
environment. Build the venv against a uv-managed interpreter instead, which
carries no such baked-in path:
uv python install 3.12
cd hal && uv sync --inexact --python-preference only-managed -p 3.12make sim reuses the resulting hal/.venv, so this is a one-time fix.
| Why | |
|---|---|
| Backend uplink | Off by design — see The backend uplink is off |
| Bootstrap / OTA | bootstrap-server has no off-device target |
| Claude Desktop Buddy logs | Separate Mac app |
| Any runtime other than codex | No *-dev target; never exercised on a laptop |
Real SensingService (face, motion perception) |
Its module imports the Feetech driver, which pulls lerobot. VirtualSensingService stands in |
| Face recognition, speech emotion, speaker ID | Need the perception service / embedding endpoint |
| GPIO button, touchpad, mic button | No hardware; skipped at boot |
| SoC temperature | temp_c: null, so the thermal-throttle path is untestable |
Every off-device behaviour sits behind an env var whose unset value is the board's, or behind a platform check a board never satisfies:
| Change | Board |
|---|---|
| Voice-pipeline gate | state.simulation_audio is False when HAL_SIMULATE is unset — identical to the _simulation check it replaced |
| macOS music route | Guarded by sys.platform == "darwin" |
record-enroll capture backend |
shutil.which("arecord") finds it on the board, so the PortAudio fallback never runs |
BackendUplink() |
Defaults on; the var appears in no unit file, rootfs or image script |
Every syspath accessor |
Unset env returns the literal it replaced |
| Makefile, docs | Not shipped to the device |
Guarded by tests that assert the board contract, not the laptop one:
system/lib/syspath/syspath_test.go (TestDeviceDefaults, TestAgentRuntimeHome,
TestBackendUplink), system/server/logs_source_test.go
(TestResolveLogSourceBoardDefaults) and runtimes/codex/paths_default_test.go.
Makefile—── Off-device run (laptop) ──,SIM_HAL_ENV,sim,web-devscripts/dev/os-dev-seed.sh— config.json + bootstrap.json seedingsystem/lib/syspath/syspath.go— every Go-side env overridesystem/server/logs.go—resolveLogSourceruntimes/codex/gatewayd/gatewayd.go— the bridgecodex-devrunsruntimes/codex/onboarding.go— whatos-devseedshal/server.py— simulation gates, mount plan,_sim_audio_probehal/drivers/motors/mock_service.py— the mock bodyhal/drivers/camera/host_capture_device.py— the host webcam backendhal/static/lamp-simulator.html— the 3D pagerobots/sim/ROBOT.md— the minimal contract-test body
Related: overview.md · os-server.md · agentic/codex.md · realtime-voice.md