different-ai/openwork · critical · ApiError
managed_mcp_secure_storage_unavailable
managed_mcp_secure_storage_unavailable
Error message
Secure storage for OpenWork-managed MCP credentials is unavailable. Start through OpenWork Desktop or set OPENWORK_ENCRYPTION_KEY.
What it means
resolveVaultKey builds the key used to encrypt OpenWork-managed MCP credentials. When config.localManagedMcpVaultKey is provided, it must resolve to exactly 32 bytes; any failure (callback missing/throwing or wrong length) is converted to managed_mcp_secure_storage_unavailable here. Without it the key is derived from OPENWORK_ENCRYPTION_KEY, and if neither source exists the same error is thrown, because the server refuses to store MCP credentials without secure key material.
Source
Thrown at apps/server/src/local-managed-mcp.ts:200
return join(runtimeStorageDir(config), "local-managed-mcp-vault.json");
}
function secureVaultStorageUnavailable(): ApiError {
return new ApiError(
503,
"managed_mcp_secure_storage_unavailable",
"Secure storage for OpenWork-managed MCP credentials is unavailable. Start through OpenWork Desktop or set OPENWORK_ENCRYPTION_KEY.",
);
}
async function resolveVaultKey(config: ServerConfig): Promise<Buffer> {
if (config.localManagedMcpVaultKey) {
try {
const key = Buffer.from(await config.localManagedMcpVaultKey());
if (key.byteLength !== 32) throw new Error("invalid vault key length");
return key;
} catch {
throw secureVaultStorageUnavailable();
}
}
const configured = process.env.OPENWORK_ENCRYPTION_KEY?.trim();
if (configured) return createHash("sha256").update(configured).digest();
throw secureVaultStorageUnavailable();
}
async function vaultKey(config: ServerConfig): Promise<Buffer> {
let pending = vaultKeyByConfig.get(config);
if (!pending) {
pending = resolveVaultKey(config);
vaultKeyByConfig.set(config, pending);
}
try {
return Buffer.from(await pending);
} catch (error) {
vaultKeyByConfig.delete(config);
throw error;View on GitHub (pinned to 2b7df46e8a)
Solutions
- Set OPENWORK_ENCRYPTION_KEY to a strong secret in the server's environment before startup.
- Start the server through OpenWork Desktop so the OS keychain-backed vault key is provided.
- If supplying config.localManagedMcpVaultKey, return exactly 32 bytes (e.g. Buffer.from(hex, "hex") for a 64-char hex key, not the hex string as UTF-8).
- Persist the chosen key — credentials encrypted under one key cannot be decrypted after it changes.
Example fix
// before localManagedMcpVaultKey: async () => Buffer.from(process.env.VAULT_KEY_HEX!) // "<64 hex chars>" as UTF-8 = 64 bytes -> unavailable // after localManagedMcpVaultKey: async () => Buffer.from(process.env.VAULT_KEY_HEX!, "hex") // 32 bytes // or headless: export OPENWORK_ENCRYPTION_KEY="$(openssl rand -base64 32)" before start
Defensive patterns
Strategy: validation
Validate before calling
const raw = process.env.OPENWORK_ENCRYPTION_KEY;
if (!raw?.trim()) throw new Error("set OPENWORK_ENCRYPTION_KEY or start via OpenWork Desktop");
// custom provider:
const key = await config.localManagedMcpVaultKey?.();
if (key && key.byteLength !== 32) throw new Error(`vault key must be 32 bytes, got ${key.byteLength}`); Type guard
function isVaultKey(v: unknown): v is Buffer {
return v instanceof Buffer && v.byteLength === 32;
} Try / catch
try {
await storeManagedMcpCredential(creds);
} catch (e) {
if ((e as Error).code === "managed_mcp_secure_storage_unavailable") {
throw new Error("Provide OPENWORK_ENCRYPTION_KEY or run under OpenWork Desktop");
}
throw e;
} Prevention
- Provision OPENWORK_ENCRYPTION_KEY in every headless environment before startup.
- Return exactly 32 bytes from custom vault-key providers (decode hex/base64, don't pass strings).
- Persist the key in a secret manager; never rotate casually (old ciphertext becomes unreadable).
- Smoke-test managed-MCP credential storage in CI with the same env shape as production.
When it happens
Trigger: Server started outside OpenWork Desktop (no keychain-backed key callback) and OPENWORK_ENCRYPTION_KEY unset; or config.localManagedMcpVaultKey supplied but returns a key whose byteLength !== 32 or throws — the catch converts any such failure to this error.
Common situations: Headless/CLI/Docker/CI runs without Desktop's keychain; a custom vault-key provider returning a hex/base64 string as UTF-8 (64-char hex string as UTF-8 is 64 bytes, not 32); provider reads a secret that is unavailable at startup.
Related errors
- unsupported_transport
- MCP_OAUTH_CONFIGURATION_REQUIRED
- mcp_oauth_configuration_required
- MCP_HTTP_404
- MCP_URL_BLOCKED
AI-assisted analysis of different-ai/openwork@2b7df46e8a (2026-09-01).
Data as JSON: /api/errors/10e09bfae70ac7b5.
Report an issue: GitHub.