Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,12 @@ UPSTREAM_MODELS_PATH=/providers-models/openrouter.json
NODE_A_ADMIN_PASSWORD=test-admin-pw
NODE_B_ADMIN_PASSWORD=test-admin-pw

# Fernet key each node uses to encrypt secrets at rest (routstr-core #553). A
# committed default is baked into compose.yml; override per node here if desired.
# Must be a urlsafe-base64 32-byte key (python: Fernet.generate_key()).
NODE_A_ROUTSTR_SECRET_KEY=W5PvCGEnbMTde00OFubyfhPPO2-f6aQP5ullyqoBfRQ=
NODE_B_ROUTSTR_SECRET_KEY=W5PvCGEnbMTde00OFubyfhPPO2-f6aQP5ullyqoBfRQ=

# Paid-scenario auth (per run, not stored):
# NODE_A_API_KEY — funded node api-key/cashu token for `real_inference`
# X_CASHU_TOKENS — comma-separated funded tokens for `xcashu`
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Set in `.env` (all gitignored):
- `CASHU_MINTS` — the mint the token is from (e.g. `https://mint.chorus.community`). Both nodes must trust it.
- `OPENROUTER_API_KEY` (+ optional `OPENROUTER_REFERER`) — to point the nodes at a real upstream. The node auto-seeds an `openrouter` provider at startup when this is present.
- `NODE_A_ADMIN_PASSWORD` / `NODE_B_ADMIN_PASSWORD` — admin password per node (default `test-admin-pw`), needed for `routstr-cli` config.
- `NODE_A_ROUTSTR_SECRET_KEY` / `NODE_B_ROUTSTR_SECRET_KEY` — Fernet key each node uses to encrypt secrets at rest (routstr-core #553). A committed default is baked into `compose.yml`; override only if you want a per-run key (must be a urlsafe-base64 32-byte key from `Fernet.generate_key()`).

### 3. Start services

Expand Down
7 changes: 7 additions & 0 deletions compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,11 @@ services:
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
- OPENROUTER_REFERER=${OPENROUTER_REFERER:-https://routstr-testing.local}
- ADMIN_PASSWORD=${NODE_A_ADMIN_PASSWORD:-test-admin-pw}
# Mandatory for #553 secret-at-rest: the node refuses to boot without it and
# uses it to Fernet-encrypt the stored nsec. A fixed key keeps the encrypted
# blob readable across restarts within the volume's lifetime. Older nodes that
# don't read this var simply ignore it.
- ROUTSTR_SECRET_KEY=${NODE_A_ROUTSTR_SECRET_KEY:-W5PvCGEnbMTde00OFubyfhPPO2-f6aQP5ullyqoBfRQ=}
- NAME=NodeA
- DESCRIPTION=Test node A
- NPUB=${NODE_A_NPUB:-npub10xlxvlhemja6c4dqv22uapctqupfhlxm9h8z3k2e72q4k9hcz7vqpkge6d}
Expand Down Expand Up @@ -204,6 +209,8 @@ services:
- OPENROUTER_API_KEY=${OPENROUTER_API_KEY:-}
- OPENROUTER_REFERER=${OPENROUTER_REFERER:-https://routstr-testing.local}
- ADMIN_PASSWORD=${NODE_B_ADMIN_PASSWORD:-test-admin-pw}
# See node-a: mandatory for #553 secret-at-rest, ignored by older nodes.
- ROUTSTR_SECRET_KEY=${NODE_B_ROUTSTR_SECRET_KEY:-W5PvCGEnbMTde00OFubyfhPPO2-f6aQP5ullyqoBfRQ=}
- NAME=NodeB
- DESCRIPTION=Test node B
- NPUB=${NODE_B_NPUB:-npub1ccz8l9zpa47k6vz9gphftsrumpw80rjt3nhnefat4symjhrsnmjs38mnyd}
Expand Down
32 changes: 32 additions & 0 deletions scenarios/secret_lifecycle.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
id: secret_lifecycle
name: Config-ownership secret lifecycle (boot-time)
description: >
Whole-system boot-time secret behaviours for routstr-core #553 that a single
long-lived node cannot show. Each test boots a throwaway node (via docker run,
on the compose network, against the built node image) with tailored env: a node
with a Nostr identity refuses to start without ROUTSTR_SECRET_KEY (and prints
the gen command); a fresh node generates + logs a first-run admin password that
then logs in; changing ROUTSTR_SECRET_KEY under an encrypted nsec bricks the
node; and an encrypted nsec survives a later boot after NSEC has left the env.
RED against a pre-#553 node (ignores the key, no generated-password log, no
brick); GREEN once secrets move into the Fernet/scrypt Secret store. No Cashu,
no real sats.

# Bring up the stack so the node IMAGE is built and relay/mock-openai/primary-mint
# are reachable by the ephemeral nodes; a missing service then fails the run
# instead of silently skipping (SERVICES_REQUIRED=1).
services_required: true
target_profile: local
upstream_profile: mock

selection:
paths:
- tests/integration/test_secret_lifecycle.py
markers:
- destructive

expected_cost_sats: 0
estimated_upstream_cost_usd: 0.0
# Each test boots one or two throwaway nodes (~20s to healthy each), so the suite
# runs several sequential container boots.
timeout_seconds: 600
30 changes: 30 additions & 0 deletions scenarios/secrets_at_rest.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
id: secrets_at_rest
name: Config-ownership secret storage at rest
description: >
Whole-system check for routstr-core #553 (config ownership / secret storage).
A real node is booted with the admin password and Nostr identity supplied the
way operators supply them — plain container env (ADMIN_PASSWORD, NSEC) — plus a
mandatory ROUTSTR_SECRET_KEY. The scenario asserts those secrets are migrated
into the encrypted Secret store at boot rather than persisted in the plaintext
settings blob: the env password still logs in, admin_password is no longer an
editable settings field, the node exposes a dedicated PATCH /admin/api/nsec
rotation endpoint, and neither the plaintext password nor the bech32 nsec
survives in the node's on-disk SQLite database. RED against a pre-#553 node that
keeps secrets in the settings blob; GREEN once they move into the
Fernet/scrypt-backed Secret store. No Cashu, no real sats.

# Orchestrator provisions the stack; a missing service then fails the run instead
# of silently skipping (SERVICES_REQUIRED=1).
services_required: true
target_profile: local
upstream_profile: mock

selection:
paths:
- tests/integration/test_secrets_at_rest.py
markers:
- destructive

expected_cost_sats: 0
estimated_upstream_cost_usd: 0.0
timeout_seconds: 180
Loading