Miner secret service and scoped inference proxy for Kata. See
../miner-secret-proxy-plan.md for the design
and ../miner-secret-proxy-implementation-steps.md
for the build order.
This is Step 2 of that plan: the secret service + proxy. It stores each miner's real provider key encrypted at rest and hands the sandboxed agent only a short-lived, PR/head-SHA-scoped run token — the raw key never reaches PRs, the validator, logs, or other miners.
Built so far:
run_token.py— scoped, signed, expiring run tokens (condition b: tamper-proof per-request scope). Issue/verify with HMAC-SHA256;enforce_scoperejects replay against a different PR / head SHA / submission.spend.py— per-run request/cost caps (condition c), reserved before the provider call and atomic under concurrency, keyed by the token's unique id.crypto.py— at-rest encryption viacryptographyMultiFernet(versioned rotation); non-reversible key fingerprint for duplicate detection.store.py— encrypted, file-backed provider-key store keyed by the exact scope (repo + pull_number + head_sha + subnet_pack + mode + submission_id); register / status / revoke / internalget_provider_key. Only ciphertext and fingerprints touch disk.credentials.py— run-credential issuance: verifies a key is registered and not revoked for the exact scope and provider, then mints a short-lived, scope-bound run token and returns the proxy endpoint + token. Never returns the raw provider key.proxy.py—/inferencerequest logic: verify token → provider/model allowlist → per-run spend/request caps (keyed by the submission-evaluation scope, reserved before forwarding) → per-request revocation check → decrypt key → forward (injected) → metadata-only log. Never logs the key, prompt, or response body.service.py— HTTP router + server exposingPOST /api/miner-secrets,GET /api/miner-secrets/status,POST /api/internal/run-credentials, andPOST /inference, with injected miner-auth / PR-author-verifier seams.provider_forwarder.py— real Chutes/OpenRouter forwarder matching the SN60 Bitsec proxy (endpoints, auth headers, key prefixes, payload defaults), with an injected HTTP transport; plus a coarse prompt-token estimator for the cost cap.github.py— GitHub-backed auth seams:build_github_miner_authenticator(miner GitHub-OAuth token → identity via/user) andbuild_github_pr_author_verifier(service token confirms the miner authored the PR). Injected transport.app.py— env-driven assembly:load_secrets_config(pure) +build_router(wires all modules; network edges injectable) +main. Runnable aspython -m kata_secrets.
The run token is scoped to the submission evaluation (repo + pull_number + head_sha + subnet_pack + mode + submission_id + provider) — the unit kata-bot knows before it invokes Kata. There is no separate run id.
Remaining before deploy: optional wallet-signature auth and retry/backoff on transient provider errors (Step 4 hardening). Conditions a (agent egress isolation) and d (secret service not reachable from the agent network) are deployment/topology controls.
Required env for python -m kata_secrets:
KATA_SECRETS_MASTER_KEYS— comma-separated Fernet keys (newest first)KATA_SECRETS_STORE_PATH— encrypted key-store pathKATA_SECRETS_TOKEN_SECRET— HMAC secret for run tokensKATA_SECRETS_INTERNAL_TOKEN— bearer for/api/internal/run-credentials(matches kata-bot'sKATA_SECRET_SERVICE_TOKEN)KATA_SECRETS_PROXY_URL— public proxy base returned to agentsKATA_SECRETS_GITHUB_TOKEN— service token for PR-author verification
Optional: KATA_SECRETS_TOKEN_TTL_SECONDS (1800), KATA_SECRETS_ALLOWED_PROVIDERS
(chutes,openrouter), KATA_SECRETS_ALLOWED_MODELS, KATA_SECRETS_MAX_REQUESTS_PER_RUN
(200), KATA_SECRETS_MAX_COST_PER_RUN, KATA_SECRETS_GITHUB_API_URL,
KATA_SECRETS_HOST (0.0.0.0), KATA_SECRETS_PORT (8099).
The service hosts a small registration page at GET /miner/secrets (same
origin as the API, strict Content-Security-Policy, no inline scripts). Point
kata-bot's KATA_SECRET_REGISTRATION_URL at it; the bot's PR comment links a
miner straight to a pre-filled form:
https://<secret-service-host>/miner/secrets?repo=<owner/repo>&pr=<number>&submission_id=<id>
On the page the miner enters a GitHub token (identity only — a token with
no scopes/permissions is enough; the service reads /user to get their login
and its own token to confirm they authored the PR), picks the provider, and
pastes the provider API key. The page can auto-fill the PR head SHA from GitHub.
Nothing is stored in the PR; the key is sent over HTTPS to /api/miner-secrets,
encrypted at rest, and never displayed again.
Serve this page over HTTPS in production, and make sure only the
public-facing secret service (not the inference proxy on bitsec-net) exposes
it.
The page is a thin client over the same API a miner can call directly:
curl -X POST https://<secret-service-host>/api/miner-secrets \
-H "Authorization: Bearer <miner-github-token>" \
-H "Content-Type: application/json" \
-d '{"repo":"owner/kata","pull_number":42,
"head_sha":"<PR head SHA>","subnet_pack":"sn60__bitsec","mode":"miner",
"submission_id":"<id>","provider":"chutes","api_key":"<provider key>"}'Check status with GET /api/miner-secrets/status?<scope query> and revoke with
POST /api/miner-secrets/revoke (same auth + scope body).
In
KATA_SECRETS_TEE_MODE=requiredthe raw-key form is rejected; miners usepython -m kata_secrets.client register(enclave-encrypted) instead. See the TEE plan.
uv sync --extra dev
uv run --extra dev python -m pytest
uv run --extra dev ruff check