Skip to content
Merged
27 changes: 27 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,33 @@ add `HIPPIUS_*` once your Hippius keys are issued — the client will pick up
the new backend automatically. To cut over fully, also set
`HIPPIUS_BUCKET` (or unset `R2_BUCKET`) so dispatched URLs point at Hippius.

## Non-competitive Mode

Set `RADAR_NONCOMPETITIVE=true` for a non-subnet deployment:

- Validator skips wallet, subtensor, metagraph, weight-setting, on-chain
commitments. Existing competitive deployments leave it false.
- Auth uses HMAC service key (`RADAR_SERVICE_KEY`) for validator ↔ trainer
↔ DB. Headers: `X-Radar-{Signature,Timestamp,Key-Id}`.
- Miner identity is bearer tokens issued via the operator CLI:
```
python -m database.operator_cli register --name alice [--hotkey ss58]
python -m database.operator_cli issue-key --miner-id <id> --label prod
python -m database.operator_cli rotate-service-key
python -m database.operator_cli list-miners | list-keys --miner-id <id>
python -m database.operator_cli revoke-key --key-id <id>
```
- Miners pull scored history + evolve prompts locally via the miner CLI:
```
RADAR_DB_URL=http://db:8090 RADAR_MINER_API_KEY=rdrk_... \
python miner/neuron.py optimize --optimizer gepa --seed --watch
python miner/neuron.py results --json
python miner/neuron.py prompts list | history | rollback <gen>
```
- `prompts/active.json` (atomic-write) drives which prompt variant the
agent picks each round; `prompt_id` round-trips back via
`experiments.prompt_id` so Phase C scores attribute to the variant.

## Neuron Mode Environment Variables

| Var | Default | Purpose |
Expand Down
17 changes: 17 additions & 0 deletions config.py
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,23 @@ class Config:
# all — Everything on one process (legacy / dev default).
NEURON_MODE: str = os.getenv("RADAR_NEURON_MODE", "all").lower()

# ── Non-competitive mode ────────────────────────────────────
# When true, validators + miners SKIP all chain interactions:
# * no metagraph sync, no Subtensor connection, no wallet
# * no weight setting, no on-chain commitments
# * outbound auth uses HMAC service key (RADAR_SERVICE_KEY)
# * miner identity comes from bearer API keys, not hotkeys
# Existing competitive deployments leave this false and run unchanged.
NONCOMPETITIVE: bool = os.getenv(
"RADAR_NONCOMPETITIVE", "false",
).lower() in ("1", "true", "yes")

# Shared HMAC secret for service-to-service auth in non-competitive
# mode (validator -> trainer, validator -> DB). Required when
# NONCOMPETITIVE is true.
SERVICE_KEY: str = os.getenv("RADAR_SERVICE_KEY", "")
SERVICE_KEY_ID: str = os.getenv("RADAR_SERVICE_KEY_ID", "operator")

# CORS origins for the public JSON API. Comma-separated, empty = no CORS
# (safe default for validator mode, which doesn't speak to browsers).
# In production dashboard mode set this to "https://radarnet.io".
Expand Down
77 changes: 77 additions & 0 deletions database/migrations/005_miner_registry_and_prompt_id.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
-- Miner registry + bearer API keys for the non-competitive cutover,
-- plus the ``experiments.prompt_id`` column that closes the GEPA
-- feedback loop on the miner CLI.
--
-- Non-competitive deployments use operator-issued bearer tokens
-- (rdrk_*) in place of Epistula/metagraph identity. A ``miner_id``
-- is an opaque operator-issued identifier (UUID by default) that
-- decouples the feedback API from any on-chain hotkey. During the
-- dual-stack period a miner row carries both: ``hotkey`` is the
-- bittensor SS58 (nullable, only set for chain-registered miners)
-- and ``miner_id`` is the bearer-auth identifier scoped to
-- /miners/me/*.
--
-- ``prompt_id`` on experiments lets miners correlate Phase C scores
-- with the prompt variant that produced the architecture. Opaque
-- to the operator; only the issuing miner interprets it. Two
-- partial indexes keep the table footprint sparse for rows without
-- a prompt_id (every existing row + every competitive-mode write).
--
-- All DDL is idempotent — safe under partial re-runs. The
-- experiments ALTER + its indexes are wrapped in an information_schema
-- guard (same pattern as 002_substrate_cids.sql) because CI exercises
-- the migration runner against an empty schema before init_schema()
-- has bootstrapped the experiments table. On a fresh deploy the base
-- CREATE TABLE in shared/pg_schema.py already declares prompt_id, so
-- the guarded block is a no-op there.

CREATE TABLE IF NOT EXISTS CURRENT_SCHEMA.miners (
miner_id TEXT PRIMARY KEY,
name TEXT NOT NULL DEFAULT '',
hotkey TEXT NOT NULL DEFAULT '',
contact TEXT NOT NULL DEFAULT '',
created_at DOUBLE PRECISION NOT NULL DEFAULT extract(epoch from now()),
revoked_at DOUBLE PRECISION
);

CREATE INDEX IF NOT EXISTS idx_miners_hotkey
ON CURRENT_SCHEMA.miners(hotkey)
WHERE hotkey <> '';

CREATE TABLE IF NOT EXISTS CURRENT_SCHEMA.miner_api_keys (
key_id TEXT PRIMARY KEY,
key_hash TEXT NOT NULL,
miner_id TEXT NOT NULL REFERENCES CURRENT_SCHEMA.miners(miner_id)
ON DELETE CASCADE,
scope TEXT NOT NULL DEFAULT 'miner',
label TEXT NOT NULL DEFAULT '',
created_at DOUBLE PRECISION NOT NULL DEFAULT extract(epoch from now()),
last_used_at DOUBLE PRECISION,
revoked_at DOUBLE PRECISION
);

CREATE INDEX IF NOT EXISTS idx_miner_api_keys_hash
ON CURRENT_SCHEMA.miner_api_keys(key_hash)
WHERE revoked_at IS NULL;

CREATE INDEX IF NOT EXISTS idx_miner_api_keys_miner
ON CURRENT_SCHEMA.miner_api_keys(miner_id);

DO $$
BEGIN
IF EXISTS (
SELECT 1
FROM information_schema.tables
WHERE table_schema = 'CURRENT_SCHEMA'
AND table_name = 'experiments'
) THEN
ALTER TABLE CURRENT_SCHEMA.experiments
ADD COLUMN IF NOT EXISTS prompt_id TEXT;
CREATE INDEX IF NOT EXISTS idx_experiments_prompt_id
ON CURRENT_SCHEMA.experiments(prompt_id)
WHERE prompt_id IS NOT NULL;
CREATE INDEX IF NOT EXISTS idx_experiments_miner_prompt
ON CURRENT_SCHEMA.experiments(miner_hotkey, prompt_id)
WHERE prompt_id IS NOT NULL;
END IF;
END $$;
Loading
Loading