{"record":{"id":"10e09bfae70ac7b5","repo":"different-ai/openwork","slug":"managed-mcp-secure-storage-unavailable","errorCode":"managed_mcp_secure_storage_unavailable","errorMessage":"Secure storage for OpenWork-managed MCP credentials is unavailable. Start through OpenWork Desktop or set OPENWORK_ENCRYPTION_KEY.","messagePattern":"Secure storage for OpenWork-managed MCP credentials is unavailable\\. Start through OpenWork Desktop or set OPENWORK_ENCRYPTION_KEY\\.","errorType":"http","errorClass":"ApiError","httpStatus":503,"severity":"critical","filePath":"apps/server/src/local-managed-mcp.ts","lineNumber":200,"sourceCode":"  return join(runtimeStorageDir(config), \"local-managed-mcp-vault.json\");\n}\n\nfunction secureVaultStorageUnavailable(): ApiError {\n  return new ApiError(\n    503,\n    \"managed_mcp_secure_storage_unavailable\",\n    \"Secure storage for OpenWork-managed MCP credentials is unavailable. Start through OpenWork Desktop or set OPENWORK_ENCRYPTION_KEY.\",\n  );\n}\n\nasync function resolveVaultKey(config: ServerConfig): Promise<Buffer> {\n  if (config.localManagedMcpVaultKey) {\n    try {\n      const key = Buffer.from(await config.localManagedMcpVaultKey());\n      if (key.byteLength !== 32) throw new Error(\"invalid vault key length\");\n      return key;\n    } catch {\n      throw secureVaultStorageUnavailable();\n    }\n  }\n  const configured = process.env.OPENWORK_ENCRYPTION_KEY?.trim();\n  if (configured) return createHash(\"sha256\").update(configured).digest();\n  throw secureVaultStorageUnavailable();\n}\n\nasync function vaultKey(config: ServerConfig): Promise<Buffer> {\n  let pending = vaultKeyByConfig.get(config);\n  if (!pending) {\n    pending = resolveVaultKey(config);\n    vaultKeyByConfig.set(config, pending);\n  }\n  try {\n    return Buffer.from(await pending);\n  } catch (error) {\n    vaultKeyByConfig.delete(config);\n    throw error;","sourceCodeStart":182,"sourceCodeEnd":218,"githubUrl":"https://github.com/different-ai/openwork/blob/2b7df46e8ae1517d64c896c7793d2d52ec845669/apps/server/src/local-managed-mcp.ts#L182-L218","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nlocalManagedMcpVaultKey: async () => Buffer.from(process.env.VAULT_KEY_HEX!) // \"<64 hex chars>\" as UTF-8 = 64 bytes -> unavailable\n// after\nlocalManagedMcpVaultKey: async () => Buffer.from(process.env.VAULT_KEY_HEX!, \"hex\") // 32 bytes\n// or headless: export OPENWORK_ENCRYPTION_KEY=\"$(openssl rand -base64 32)\" before start","handlingStrategy":"validation","validationCode":"const raw = process.env.OPENWORK_ENCRYPTION_KEY;\nif (!raw?.trim()) throw new Error(\"set OPENWORK_ENCRYPTION_KEY or start via OpenWork Desktop\");\n// custom provider:\nconst key = await config.localManagedMcpVaultKey?.();\nif (key && key.byteLength !== 32) throw new Error(`vault key must be 32 bytes, got ${key.byteLength}`);","typeGuard":"function isVaultKey(v: unknown): v is Buffer {\n  return v instanceof Buffer && v.byteLength === 32;\n}","tryCatchPattern":"try {\n  await storeManagedMcpCredential(creds);\n} catch (e) {\n  if ((e as Error).code === \"managed_mcp_secure_storage_unavailable\") {\n    throw new Error(\"Provide OPENWORK_ENCRYPTION_KEY or run under OpenWork Desktop\");\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["encryption","mcp","configuration","keychain","key-length"],"backgroundTag":"missing-encryption-key","analyzedSha":"2b7df46e8ae1517d64c896c7793d2d52ec845669","analyzedAt":"2026-09-01T07:59:23.713Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-08T10:18:20.063Z"}