JuliusBrussee/caveman · error
runtimePolicy publicKey must be a base64-encoded 32-byte…
Error message
runtimePolicy publicKey must be a base64-encoded 32-byte Ed25519 key
What it means
The RuntimePolicy constructor validates options.publicKey by decoding it as base64 and requiring exactly 32 bytes (an Ed25519 public key size). Anything else — invalid base64 or wrong length — throws this configuration error before the client is usable.
Solutions
- Base64-encode the raw 32-byte Ed25519 public key (standard alphabet, padding allowed)
- Ensure you export the public key, not the private key or seed
- Convert hex to raw bytes then base64: Buffer.from(hexKey,'hex').toString('base64')
- Convert URL-safe base64 to standard by replacing -/_ with +/
Example fix
// before
new RuntimePolicyEnforcer(cave, { publicKey: "9d61b19deffd5a60ba844af492ec2cc4" }); // hex, 16 bytes
// after
new RuntimePolicyEnforcer(cave, { publicKey: "nWGJne/9WmC6hEr0kuwsxMA5G0zmGToLWM0vB1X0N60=" }); // base64, 32 bytes Defensive patterns
Strategy: validation
Validate before calling
function isValidPinnedKey(key: string): boolean {
if (!/^[A-Za-z0-9+/]*={0,2}$/.test(key)) return false;
const raw = Buffer.from(key, "base64");
return raw.length === 32;
}
if (!isValidPinnedKey(opts.publicKey)) throw new Error("publicKey must be base64 of a 32-byte Ed25519 public key"); Type guard
function isBase64Of32Bytes(v: unknown): v is string {
return typeof v === "string" && /^([A-Za-z0-9+/]{43}=?|)$/.test(v) && Buffer.from(v, "base64").length === 32;
} Try / catch
try {
const enforcer = new RuntimePolicyEnforcer(cave, { publicKey });
} catch (e) {
if (/publicKey/.test(e.message)) dieWithConfigHint("Invalid CAVEMAN_POLICY_PUBLIC_KEY: expected base64 32-byte Ed25519 key");
else throw e;
} Prevention
- Store the key as standard base64 of the raw 32-byte public key
- Never paste hex, PEM, or the private/seed key
- Validate env-provided keys at startup with Buffer.from(key,'base64').length === 32
When it happens
Trigger: Passing publicKey that is not valid base64, is base64 of a non-32-byte key (e.g. hex string, PEM, 64-byte private key, 65-byte uncompressed point), or a URL-safe base64 variant with -/_ characters.
Common situations: Copying a hex-encoded key instead of base64; pasting the Ed25519 private/seed key (64 bytes) instead of the public key; using a PEM-wrapped key; URL-safe base64 from another tool.
Understand the failure class
Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.
Related errors
- ed25519 public key must be 32 bytes, got
- apiKey, baseURL, and agent are required
- ASGI context resolver or request bounds are invalid
- bedrock endpoint must be runtime or mantle
- bundle completeness cannot be attested by unsigned export…
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/078f4c0f8423e1f1.
Report an issue: GitHub.
Appendix: source
Thrown at packages/sdk/typescript/src/index.ts:1082
* failure. `decide()` is synchronous, local-only, and never throws — with no
* bundle it returns the caller's baseline. Mirrors the Python
* `RuntimePolicyClient`.
*/
export class RuntimePolicyClient {
private bundle: Record<string, unknown> | null = null;
private signedBundle = false;
private pinnedKey: Uint8Array | null = null;
private killedLocally = false;
private timer: ReturnType<typeof setInterval> | null = null;
/** True while a `refresh()` is in flight; the background timer skips rather than pile on. */
private refreshing = false;
private readonly killEnv: string;
constructor(private cave: Cave, options: RuntimePolicyOptions = {}) {
this.killEnv = options.killEnv ?? "CAVEMAN_POLICY_KILL";
if (options.publicKey !== undefined) {
const pin = base64Bytes(options.publicKey);
if (pin === null || pin.length !== 32) throw new Error("runtimePolicy publicKey must be a base64-encoded 32-byte Ed25519 key");
this.pinnedKey = pin;
}
const seconds = options.autoRefreshSeconds;
if (typeof seconds === "number" && Number.isFinite(seconds) && seconds > 0) {
const timer = setInterval(() => {
// A slow or hung endpoint must not queue refreshes behind each other:
// one tick while a refresh is already in flight is skipped, not stacked.
if (this.refreshing) return;
void this.refresh();
}, seconds * 1000);
(timer as unknown as { unref?: () => void }).unref?.();
this.timer = timer;
}
}
/**
* Fetch and verify the current bundle (`GET /sdk/v1/runtime-policy`).
*View on GitHub (pinned to 3ee70a1026)