import { Effect } from "effect"; import { definePlugin, type PluginCtx } from "@executor/sdk"; import { deletePassword, displayName, getPassword, isSupportedPlatform, resolveServiceName, setPassword, } from "./keyring"; import { makeKeychainProvider } from "./provider"; // Probe the keychain by writing and then deleting a sentinel entry. A // read-only probe isn't enough — on some Linux environments (WSL2, // headless CI) `getPassword` for a missing key returns null without // error, but `setPassword` fails because the secret-service backend // isn't actually reachable. Writing is the capability the executor // cares about, so test it directly. const PROBE_ACCOUNT = "__executor_keychain_probe__"; const PROBE_VALUE = "probe"; // --------------------------------------------------------------------------- // Re-exports // --------------------------------------------------------------------------- export { KeychainError } from "./errors"; export { makeKeychainProvider } from "./provider"; export { isSupportedPlatform, displayName } from "./keyring"; // --------------------------------------------------------------------------- // Plugin config // --------------------------------------------------------------------------- export interface KeychainPluginConfig { /** Override the keychain service name (default: "executor") */ readonly serviceName?: string; } // --------------------------------------------------------------------------- // Plugin extension — public API on executor.keychain // --------------------------------------------------------------------------- export interface KeychainExtension { /** Human-readable name for the keychain on this platform */ readonly displayName: string; /** Whether the current platform supports system keychain */ readonly isSupported: boolean; /** Check if a secret exists in the system keychain */ readonly has: (id: string) => Effect.Effect; } // --------------------------------------------------------------------------- // Plugin definition // --------------------------------------------------------------------------- // Scope the keychain service name to the current executor scope so each // folder / workspace gets its own set of keychain entries. Computed // identically in `extension` and `secretProviders` — both receive ctx and // both are called once per createExecutor, so the derivation stays pure. const scopedServiceName = ( ctx: PluginCtx, options: KeychainPluginConfig | undefined, ): string => `${resolveServiceName(options?.serviceName)}/${ctx.scopes[0]!.id as string}`; export const keychainPlugin = definePlugin( (options?: KeychainPluginConfig) => ({ id: "keychain" as const, storage: () => ({}), extension: (ctx): KeychainExtension => { const serviceName = scopedServiceName(ctx, options); return { displayName: displayName(), isSupported: isSupportedPlatform(), has: (id) => getPassword(serviceName, id).pipe( Effect.map((v) => v !== null), Effect.orElseSucceed(() => false), ), }; }, secretProviders: (ctx) => Effect.gen(function* () { const serviceName = scopedServiceName(ctx, options); const reachable = yield* setPassword( serviceName, PROBE_ACCOUNT, PROBE_VALUE, ).pipe( Effect.andThen( deletePassword(serviceName, PROBE_ACCOUNT).pipe( Effect.catchAll(() => Effect.void), ), ), Effect.as(true), Effect.catchAll((cause) => Effect.logWarning( `keychain unavailable, skipping provider registration: ${cause.message}`, ).pipe(Effect.as(false)), ), ); return reachable ? [makeKeychainProvider(serviceName)] : []; }), }), );