{"record":{"id":"04d01def57d5ed95","repo":"ruvnet/ruflo","slug":"native-keychain-backend-unavailable","errorCode":null,"errorMessage":"native keychain backend unavailable","messagePattern":"native keychain backend unavailable","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"v3/@claude-flow/security/src/keychain-adapter.ts","lineNumber":101,"sourceCode":"  }\n\n  async isAvailable(): Promise<boolean> {\n    const Entry = await this.resolveEntryCtor();\n    if (!Entry) return false;\n    try {\n      const entry = new Entry(CANARY_SERVICE, CANARY_ACCOUNT);\n      entry.setPassword('canary');\n      const readBack = entry.getPassword();\n      entry.deletePassword();\n      return readBack === 'canary';\n    } catch {\n      return false; // binding loaded, but no reachable backend (e.g. headless Linux, no D-Bus)\n    }\n  }\n\n  async setSecret(service: string, account: string, secret: string): Promise<void> {\n    const Entry = await this.resolveEntryCtor();\n    if (!Entry) throw new Error('native keychain backend unavailable');\n    new Entry(service, account).setPassword(secret);\n  }\n\n  async getSecret(service: string, account: string): Promise<string | null> {\n    const Entry = await this.resolveEntryCtor();\n    if (!Entry) return null;\n    try {\n      return new Entry(service, account).getPassword();\n    } catch {\n      return null; // no matching entry, or backend unavailable\n    }\n  }\n\n  async deleteSecret(service: string, account: string): Promise<void> {\n    const Entry = await this.resolveEntryCtor();\n    if (!Entry) return;\n    try {\n      new Entry(service, account).deletePassword();","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/ruvnet/ruflo/blob/fa13ee4ad60ac2090b1480656eb233521790d640/v3/@claude-flow/security/src/keychain-adapter.ts#L83-L119","documentation":"NativeKeychainAdapter.setSecret throws this plain Error when resolveEntryCtor() returns null — the dynamic import('@napi-rs/keyring') failed because the package isn't installed or has no prebuilt binary for the platform. ADR-306 defines the intended degrade path: check isAvailable() and fall back to SessionOnlyKeychainAdapter (memory-only, never persisted) rather than writing secrets to disk unencrypted.","triggerScenarios":"Calling setSecret after an install skipped optional dependencies (npm --omit=optional, yarn selective resolution); a bundler statically pruning the dynamic import; unsupported platform/arch with no prebuilt napi binary.","commonSituations":"Slim CI containers and Docker images without optional deps; Alpine/musl or exotic architectures lacking prebuilts; esbuild/webpack builds marking the dynamic import dead code; headless Linux where the module loads but no D-Bus Secret Service exists (canary path).","solutions":["Call await adapter.isAvailable() first; when false use SessionOnlyKeychainAdapter — the documented, deliberate fallback","Install the binding: npm install @napi-rs/keyring; avoid --omit=optional in production images","On headless Linux, also ensure a Secret Service backend (gnome-keyring + D-Bus) so the canary round-trip succeeds","For bundlers, mark @napi-rs/keyring as external so the dynamic import survives packaging"],"exampleFix":"// before\nawait nativeAdapter.setSecret('ruflo', 'refresh-token', token); // throws: native keychain backend unavailable\n\n// after\nconst adapter = (await nativeAdapter.isAvailable())\n  ? nativeAdapter\n  : new SessionOnlyKeychainAdapter(); // ADR-306 degrade path: memory-only, never persisted\nawait adapter.setSecret('ruflo', 'refresh-token', token);","handlingStrategy":"fallback","validationCode":"import { NativeKeychainAdapter, SessionOnlyKeychainAdapter, KeychainAdapter } from './keychain-adapter.js';\nconst native = new NativeKeychainAdapter();\nconst adapter: KeychainAdapter = (await native.isAvailable())\n  ? native\n  : new SessionOnlyKeychainAdapter(); // memory-only — ADR-306 degrade path\nawait adapter.setSecret(service, account, secret);","typeGuard":"function isKeychainUnavailable(e: unknown): boolean {\n  return e instanceof Error && e.message === 'native keychain backend unavailable';\n}","tryCatchPattern":"try {\n  await nativeAdapter.setSecret(service, account, secret);\n} catch (e) {\n  if (isKeychainUnavailable(e)) {\n    const session = new SessionOnlyKeychainAdapter();\n    await session.setSecret(service, account, secret); // warn: lost on exit, never persisted\n  } else {\n    throw e;\n  }\n}","preventionTips":["Always gate native keychain use behind isAvailable() — it runs a real write/read/delete canary","Never write refresh tokens to disk as a 'workaround'; use the session-only adapter","Install @napi-rs/keyring as a real dependency in production images; mark it external when bundling"],"tags":["keychain","native-module","optional-dependency","headless","credentials"],"backgroundTag":"os-keychain-unavailable","analyzedSha":"fa13ee4ad60ac2090b1480656eb233521790d640","analyzedAt":"2026-08-18T21:34:22.708Z","contentChangedAt":"2026-08-18T21:34:22.708Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}