feat: POST /api/admin/agent-keys — system-keyed raw key minting

New admin endpoint for provisioning per-agent dialectic API keys
during recruitment. Auth via separate x-dialectic-admin-key header
matching env DIALECTIC_ADMIN_API_KEY (not bearer — admin lifecycle
is independent of agent identity).

Behavior:
- Body {agent_id, force?}; generates 32-byte hex raw key; stores
  sha256-peppered hash in agent_keys; returns raw key (ONLY time
  exposed — caller stores in agent secret-mgr)
- 409 on existing agent_id unless force:true (rotates the hash,
  clears last_used_at + revoked_at)
- Closed-by-default: if DIALECTIC_ADMIN_API_KEY env is empty, every
  request 401s

Caller pattern: skills/dialectic-hangman-lab/scripts/dialectic-ctrl
(to be added) reads admin key from
/root/.openclaw/system-secrets/dialectic-admin-key on the openclaw
host, POSTs to admin endpoint, stores returned raw key in the proxy-
for agent secret-mgr (inherits the proxy-pcexec context from
recruitment/onboard).

Unblocks Phase 3.5 plan to provision all prod agents and integrate
into recruitment skill.
This commit is contained in:
h z
2026-05-23 14:53:39 +01:00
parent 03b89a547c
commit 15bb942d9b
4 changed files with 139 additions and 1 deletions

View File

@@ -52,6 +52,13 @@ type Config struct {
AgentAPIKeyPepper string
OIDCDevBypassToken string
// DialecticAdminAPIKey gates POST /api/admin/agent-keys (raw key
// minting). Held on the operator side only — kept on the openclaw
// host at /root/.openclaw/system-secrets/dialectic-admin-key for
// `dialectic-ctrl` script to read. Empty in env → admin endpoint
// fully closed.
DialecticAdminAPIKey string
// OIDC issuer URL (Keycloak realm endpoint). e.g.
// https://auth.hangman-lab.top/realms/hangman-lab
// Phase 2C ships this as configured-but-not-verified; Phase 4 wires
@@ -85,6 +92,7 @@ func LoadFromEnv() (*Config, error) {
SystemAPIKey: os.Getenv("SYSTEM_API_KEY"),
AgentAPIKeyPepper: os.Getenv("AGENT_API_KEY_PEPPER"),
OIDCDevBypassToken: os.Getenv("OIDC_DEV_BYPASS_TOKEN"),
DialecticAdminAPIKey: os.Getenv("DIALECTIC_ADMIN_API_KEY"),
OIDCIssuer: os.Getenv("OIDC_ISSUER"),
OIDCClientID: os.Getenv("OIDC_CLIENT_ID"),
FabricGuildBaseURL: os.Getenv("FABRIC_GUILD_BASE_URL"),