diff --git a/packages/ai/src/stream.ts b/packages/ai/src/stream.ts index 36213199b..1ad774e90 100644 --- a/packages/ai/src/stream.ts +++ b/packages/ai/src/stream.ts @@ -15,7 +15,7 @@ import { } from "@oh-my-pi/pi-catalog/model-thinking"; import { CATALOG_PROVIDERS, type ProviderCatalogEntry } from "@oh-my-pi/pi-catalog/provider-models"; import { CODEX_BASE_URL } from "@oh-my-pi/pi-catalog/wire/codex"; -import { $env, $pickenv, getConfigRootDir, isEnoent, logger, withExtraCaFetch } from "@oh-my-pi/pi-utils"; +import { $env, $pickenv, getProviderInFlightRoot, isEnoent, logger, withExtraCaFetch } from "@oh-my-pi/pi-utils"; import { getCustomApi } from "./api-registry"; import { createAuthRetryKeyState, isApiKeyResolver, resolveNextAuthRetryKey } from "./auth-retry"; import * as AIError from "./error"; @@ -189,7 +189,7 @@ function resolveProviderInFlightLimit( function providerInFlightRoot(): string { if (providerInFlightRootOverride) return providerInFlightRootOverride; - return path.join(getConfigRootDir(), "run", "provider-inflight"); + return getProviderInFlightRoot(); } function providerInFlightSegment(provider: string): string { diff --git a/packages/coding-agent/CHANGELOG.md b/packages/coding-agent/CHANGELOG.md index 1b632e9c5..295af56d6 100644 --- a/packages/coding-agent/CHANGELOG.md +++ b/packages/coding-agent/CHANGELOG.md @@ -2,6 +2,12 @@ ## [Unreleased] +### Changed + +- `secret-placeholder.key` now resolves under XDG state (`$XDG_STATE_HOME/omp/secret-placeholder.key`) instead of the agent config directory, so it follows the same XDG layout as other state files. +- Daemon runtime directories (`run/daemons/`) and provider in-flight tracking (`run/provider-inflight`) now resolve under XDG state (`$XDG_STATE_HOME/omp/run/`) instead of the config root, keeping ephemeral runtime state out of `~/.config`. +- `marketplaces.json` now resolves under XDG data (`$XDG_DATA_HOME/omp/marketplaces.json`) instead of the config root, aligning with the XDG data category for user-scoped registry files. + ## [17.2.3] - 2026-08-01 ### Changed diff --git a/packages/coding-agent/src/extensibility/plugins/marketplace/registry.ts b/packages/coding-agent/src/extensibility/plugins/marketplace/registry.ts index b48a08e24..3e193fce9 100644 --- a/packages/coding-agent/src/extensibility/plugins/marketplace/registry.ts +++ b/packages/coding-agent/src/extensibility/plugins/marketplace/registry.ts @@ -15,7 +15,9 @@ import * as fs from "node:fs/promises"; import * as path from "node:path"; -import { getConfigRootDir, getPluginsDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils"; +import { getPluginsDir, isEnoent, logger, tryParseJson } from "@oh-my-pi/pi-utils"; + +export { getMarketplacesRegistryPath } from "@oh-my-pi/pi-utils"; import type { InstalledPluginEntry, @@ -24,12 +26,6 @@ import type { MarketplacesRegistry, } from "./types"; -// ── Path helpers ───────────────────────────────────────────────────── - -export function getMarketplacesRegistryPath(): string { - return path.join(getConfigRootDir(), "marketplaces.json"); -} - export function getInstalledPluginsRegistryPath(): string { return path.join(getPluginsDir(), "installed_plugins.json"); } diff --git a/packages/coding-agent/src/launch/paths.ts b/packages/coding-agent/src/launch/paths.ts index 2426da642..00951ef6c 100644 --- a/packages/coding-agent/src/launch/paths.ts +++ b/packages/coding-agent/src/launch/paths.ts @@ -1,11 +1,8 @@ import * as path from "node:path"; -import { getConfigRootDir } from "@oh-my-pi/pi-utils"; +import { getDaemonRuntimeDir } from "@oh-my-pi/pi-utils"; /** Resolve the private runtime directory shared by omp processes in one project directory. */ -export function daemonRuntimeDir(projectDir: string, configRoot: string = getConfigRootDir()): string { - const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0"); - return path.join(configRoot, "run", "daemons", key); -} +export { getDaemonRuntimeDir as daemonRuntimeDir }; /** Resolve the Unix socket or Windows named pipe used by one project broker. */ export function daemonBrokerEndpoint(projectDir: string, runtimeDir: string): string { diff --git a/packages/coding-agent/src/sdk.ts b/packages/coding-agent/src/sdk.ts index 393044ead..66e73260c 100644 --- a/packages/coding-agent/src/sdk.ts +++ b/packages/coding-agent/src/sdk.ts @@ -1377,11 +1377,15 @@ export async function createAgentSession(options: CreateAgentSessionOptions = {} // headless run on an unwritable default config root pays for a feature it // does not use. const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]); + const explicitAgentDir = options.agentDir; const placeholderKey = needsPlaceholderKey - ? await getSecretPlaceholderKey(agentDir) - : await getExistingSecretPlaceholderKey(agentDir); + ? await getSecretPlaceholderKey(explicitAgentDir) + : await getExistingSecretPlaceholderKey(explicitAgentDir); if (allEntries.length > 0) { - obfuscator = new SecretObfuscator(allEntries, placeholderKey ?? (() => getSecretPlaceholderKeySync(agentDir))); + obfuscator = new SecretObfuscator( + allEntries, + placeholderKey ?? (() => getSecretPlaceholderKeySync(explicitAgentDir)), + ); } if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) { // No configured entry produced an active secret (e.g. only ignored short diff --git a/packages/coding-agent/src/secrets/index.ts b/packages/coding-agent/src/secrets/index.ts index e94eba24e..2ce028751 100644 --- a/packages/coding-agent/src/secrets/index.ts +++ b/packages/coding-agent/src/secrets/index.ts @@ -2,7 +2,7 @@ import * as crypto from "node:crypto"; import * as fs from "node:fs"; import * as path from "node:path"; import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages"; -import { getAgentDir, isEnoent, logger } from "@oh-my-pi/pi-utils"; +import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils"; import { YAML } from "bun"; import { regexHasUnresolvableShortMatchFallback, type SecretEntry, sanitizeSecretFriendlyName } from "./obfuscator"; import { compileSecretRegex } from "./regex"; @@ -11,17 +11,15 @@ const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/; const cachedPlaceholderKeys = new Map(); /** - * Per-install secret key for the placeholder digest. Persisted under the agent - * config directory and never sent to a provider, so model-visible placeholders - * cannot be reversed by dictionary-hashing candidate secrets. Stable across - * sessions so persisted transcripts deobfuscate consistently. Defaults to - * `getAgentDir()` — the same directory `createAgentSession()` passes as - * `agentDir` — so a caller relying on the default reads/writes the identical - * key file live sessions use, per `~/.omp/agent/secret-placeholder.key` in - * docs/secrets.md. + * Per-install secret key for the placeholder digest. Persisted under XDG state + * and never sent to a provider, so model-visible placeholders cannot be reversed + * by dictionary-hashing candidate secrets. Stable across sessions so persisted + * transcripts deobfuscate consistently. Defaults to `getSecretPlaceholderKeyPath()` + * — `$XDG_STATE_HOME/omp/secret-placeholder.key` (or `~/.omp/agent/secret-placeholder.key` + * without XDG), per docs/secrets.md. */ -export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): Promise { - const keyPath = path.join(keyDir, "secret-placeholder.key"); +export async function getSecretPlaceholderKey(keyDir?: string): Promise { + const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath(); const cached = cachedPlaceholderKeys.get(keyPath); if (cached !== undefined) return cached; @@ -32,7 +30,7 @@ export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): P } const generated = crypto.randomBytes(32).toString("base64url"); - await fs.promises.mkdir(keyDir, { recursive: true }); + await fs.promises.mkdir(path.dirname(keyPath), { recursive: true }); try { await fs.promises.writeFile(keyPath, generated, { flag: "wx", mode: 0o600 }); cachedPlaceholderKeys.set(keyPath, generated); @@ -53,8 +51,8 @@ export async function getSecretPlaceholderKey(keyDir: string = getAgentDir()): P } /** Return an existing placeholder key for redaction without creating a new key file. */ -export async function getExistingSecretPlaceholderKey(keyDir: string = getAgentDir()): Promise { - const keyPath = path.join(keyDir, "secret-placeholder.key"); +export async function getExistingSecretPlaceholderKey(keyDir?: string): Promise { + const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath(); const cached = cachedPlaceholderKeys.get(keyPath); if (cached !== undefined) return cached; // Redaction-only: this key is loaded solely to redact an existing key file from @@ -85,8 +83,8 @@ let ephemeralSyncPlaceholderKey: string | undefined; * throws: an unreadable or unwritable key file degrades to a process-ephemeral * key (with a warning) instead of breaking the session. */ -export function getSecretPlaceholderKeySync(keyDir: string = getAgentDir()): string { - const keyPath = path.join(keyDir, "secret-placeholder.key"); +export function getSecretPlaceholderKeySync(keyDir?: string): string { + const keyPath = keyDir ? path.join(keyDir, "secret-placeholder.key") : getSecretPlaceholderKeyPath(); const cached = cachedPlaceholderKeys.get(keyPath); if (cached !== undefined) return cached; try { @@ -100,7 +98,7 @@ export function getSecretPlaceholderKeySync(keyDir: string = getAgentDir()): str } const generated = crypto.randomBytes(32).toString("base64url"); try { - fs.mkdirSync(keyDir, { recursive: true }); + fs.mkdirSync(path.dirname(keyPath), { recursive: true }); fs.writeFileSync(keyPath, generated, { flag: "wx", mode: 0o600 }); cachedPlaceholderKeys.set(keyPath, generated); return generated; diff --git a/packages/utils/CHANGELOG.md b/packages/utils/CHANGELOG.md index 27466e466..6740bb20c 100644 --- a/packages/utils/CHANGELOG.md +++ b/packages/utils/CHANGELOG.md @@ -2,6 +2,10 @@ ## [Unreleased] +### Added + +- Added `getSecretPlaceholderKeyPath()`, `getDaemonRuntimeDir()`, `getProviderInFlightRoot()`, and `getMarketplacesRegistryPath()` to resolve secret key, daemon runtime, provider in-flight, and marketplace registry paths under their respective XDG categories (state, data) instead of the config root. + ## [17.2.1] - 2026-07-30 ### Added diff --git a/packages/utils/src/dirs.ts b/packages/utils/src/dirs.ts index b3474ccca..9463f1271 100644 --- a/packages/utils/src/dirs.ts +++ b/packages/utils/src/dirs.ts @@ -820,6 +820,27 @@ export function getDebugLogPath(agentDir?: string): string { return dirs.agentSubdir(agentDir, `${APP_NAME}-debug.log`, "state"); } +/** Get the secret placeholder key path (~/.omp/agent/secret-placeholder.key; XDG default: $XDG_STATE_HOME/omp/secret-placeholder.key). */ +export function getSecretPlaceholderKeyPath(): string { + return dirs.agentSubdir(undefined, "secret-placeholder.key", "state"); +} + +/** Get the daemon runtime directory for a project (~/.omp/run/daemons/; XDG default: $XDG_STATE_HOME/omp/run/daemons/). */ +export function getDaemonRuntimeDir(projectDir: string): string { + const key = Bun.hash.wyhash(path.resolve(projectDir)).toString(16).padStart(16, "0"); + return dirs.rootSubdir(path.join("run", "daemons", key), "state"); +} + +/** Get the provider in-flight root directory (~/.omp/run/provider-inflight; XDG default: $XDG_STATE_HOME/omp/run/provider-inflight). */ +export function getProviderInFlightRoot(): string { + return dirs.rootSubdir(path.join("run", "provider-inflight"), "state"); +} + +/** Get the marketplaces registry path (~/.omp/marketplaces.json; XDG default: $XDG_DATA_HOME/omp/marketplaces.json). */ +export function getMarketplacesRegistryPath(): string { + return dirs.rootSubdir("marketplaces.json", "data"); +} + // ============================================================================= // Project subdirectories (.omp/*) // =============================================================================