Skip to content

fix(standard): give the relay's container its key as a file - #44

Merged
harenome merged 1 commit into
ThinkFlowLab:mainfrom
harenome:fix/relay-key-file
Nov 10, 2026
Merged

harenome merged 1 commit into
ThinkFlowLab:mainfrom
harenome:fix/relay-key-file

Conversation

@harenome

@harenome harenome commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Closes #42

What

On the docker backend, the relay sidecar got the provider's key as -e NULLIUS_RELAY_UPSTREAM_KEY, so docker inspect of the relay showed it. Now Unit.start writes the key to a 0600 file in a new 0700 folder under relay-keys/ in the user's cache (paths.cache_dir(), so NULLIUS_HOME and XDG apply), never in the record folder. Colima and Lima share the user's home with their engine by default, but not the system's temporary folder. relay_argv bind-mounts that one file read-only at /opt/nullius/relay-key with --mount, which fails at start when the engine cannot see the source, and passes --key-file. relay serve reads the key from that file and refuses a missing or empty file. Unit removes the folder once the relay listens, and in stop, which a failed start also calls. container.sweep(job), which runs at the end of every job on the docker backend, removes the job's key folders by name. Each unit's start and each sweep also remove any key folder older than KEY_STALE (10 minutes). The local backend keeps the key in memory and does not change.

Touches

backend · the standard · docs and site · CI and skills

  • app/nullius/standard/container.py: relay_argv(key_file=...), Unit._key_file, Unit._forget_key, key_root, forget_stale_keys, key_prefix, sweep, LISTEN_TIMEOUT, KEY_STALE, module docstring
  • app/nullius/standard/relay.py: serve --key-file
  • docs/standard.md: Safety table, the relay row
  • docs/builder.md: Key and endpoint row
  • docs/settings/model-providers.md: A run row
  • app/nullius/execution.py: module docstring, how keys reach a run
  • .claude/rules/running.md: Keys
  • app/tests/test_standard_container.py, app/tests/test_standard_relay.py

Verified

All in throwaway rootless containers, on this branch and on main (22140a6).

Search for a fake key after one container.Unit relay starts with it, on rootful Docker 29.9.3 in docker:dind:

== main
docker inspect (whole)                   key visible: True
Config.Env: ['PYTHONPATH', 'PYTHONDONTWRITEBYTECODE', 'NULLIUS_RELAY_UPSTREAM_KEY=sk-****', 'PATH', 'LANG', 'GPG_KEY', 'PYTHON_VERSION', 'PYTHON_SHA256']
relay /proc/*/environ+cmdline            key visible: True
docker logs                              key visible: False
record folder                            key visible: False
== branch
docker inspect (whole)                   key visible: False
Config.Env: ['PYTHONDONTWRITEBYTECODE', 'PYTHONPATH', 'PATH', 'LANG', 'GPG_KEY', 'PYTHON_VERSION', 'PYTHON_SHA256']
relay /proc/*/environ+cmdline            key visible: False
docker logs                              key visible: False
record folder                            key visible: False

The run left no container behind.

Tests:

  • New tests:
    • The relay's arguments name the key's file, bind-mounted read-only, and hold no key.
    • No docker call's arguments or environment hold the key.
    • The key's file is 0600, in a 0700 folder, under key_root() (0700). It is gone once the relay listens.
    • A failed start removes it.
    • A job's sweep removes its own key folders and not another job's.
    • A start removes a key folder older than KEY_STALE and keeps a fresh one.
    • relay serve uses the key from its file and refuses a missing or empty file.
    • The docker tier checks that docker inspect of the relay shows no key.
  • Backend suite (no docker tier): 1607 tests in 102 modules: OK
  • Route suite: 262 tests in 35 modules: OK
  • Docker tier, test_standard_container and test_standard_relay:
    • rootful Docker 29.9.3: Ran 51 tests ... OK
    • rootless Docker 29.9.3: Ran 51 tests, the two relay tests of DockerTest fail. The relay runs as uid:gid, a subordinate id there, and stops at start with cannot read --key-file: [Errno 13] Permission denied. On main that engine already cannot run units (the same user cannot write the record folder).
  • check_docs.py: 0 findings.

Risk

The engine must see the user's cache folder, as it must already see the run's folders. If NULLIUS_HOME or XDG_CACHE_HOME moves the cache to a place the engine cannot see, the relay's docker run stops with "bind source path does not exist", and the unit fails before any call. It fails closed: the key is not exposed.

The key stays readable inside the running relay's container, at /opt/nullius/relay-key, until that container stops. Only access that is already root-equivalent (docker exec into the relay) reads it there.

A Nullius process killed while a relay starts (SIGKILL, OOM) leaves one owner-only <cache>/relay-keys/nullius-relay-key-<hash>-<random>/key per unit that was starting. The next unit's start, or the next sweep, of any job removes it once it is older than KEY_STALE (10 minutes).

An engine that maps the container's user to another id (rootless Docker, or userns-remap) cannot read the 0600 file. The relay stops at start with cannot read --key-file: [Errno 13] Permission denied, and no call is made. On main, such an engine already cannot write the record folder.

Conflicts: this branch conflicts textually with #43 in app/tests/test_standard_container.py (tests added side by side). Whichever merges second needs a rebase that keeps both sides. The merge passes the backend suite (1614 tests in 103 modules: OK) and the route suite.

Undo with a revert.

The docker backend passed the provider's key to each unit's relay as an
environment variable, so docker inspect, and anything that dumps container
configurations, showed it. The key is now written owner-only in a folder
of its own under relay-keys/ in the user's cache, outside the unit's
record. Colima and Lima share the user's home with their engine by
default, but not the system's temporary folder, so that folder is not
used. The file is bind-mounted alone and read-only into the relay's
container and passed to the relay with --key-file. The relay no longer
reads NULLIUS_RELAY_UPSTREAM_KEY.

Nullius removes the folder once the relay listens, and on stop, which a
failed start also calls. A job's sweep removes the folders its relays
left. Each unit's start and each sweep also remove any key folder older
than 10 minutes, which no start takes: a killed run leaves such a folder.
@harenome
harenome merged commit 01dc1cf into ThinkFlowLab:main Nov 10, 2026
2 checks passed
@harenome
harenome deleted the fix/relay-key-file branch November 10, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Standard: keep the provider's key out of the relay container's configuration

1 participant