fix(xdg): move secret-placeholder.key, marketplaces.json, and run/ out of config root

These four paths bypassed DirResolver's XDG-aware rootSubdir/agentSubdir
hooks, resolving directly against getConfigRootDir()/getAgentDir() and
ignoring XDG state/data layout. Add XDG-aware path helpers in dirs.ts
and route all four through them:

- secret-placeholder.key → $XDG_STATE_HOME/omp/ (state, agent flattened)
- marketplaces.json      → $XDG_DATA_HOME/omp/  (data)
- run/daemons/<hash>/    → $XDG_STATE_HOME/omp/run/ (state)
- run/provider-inflight/ → $XDG_STATE_HOME/omp/run/ (state)

omp config init-xdg migrates secret-placeholder.key and marketplaces.json
from their legacy locations; run/ is ephemeral and rebuilds on restart.
This commit is contained in:
Parsifa1
2026-07-30 06:21:16 +00:00
parent 8baa3300bc
commit ea437745a3
8 changed files with 60 additions and 34 deletions
+2 -2
View File
@@ -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 {
+6
View File
@@ -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/<hash>`) 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
@@ -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");
}
+2 -5
View File
@@ -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 {
+7 -3
View File
@@ -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
+15 -17
View File
@@ -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<string, string>();
/**
* 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<string> {
const keyPath = path.join(keyDir, "secret-placeholder.key");
export async function getSecretPlaceholderKey(keyDir?: string): Promise<string> {
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<string | undefined> {
const keyPath = path.join(keyDir, "secret-placeholder.key");
export async function getExistingSecretPlaceholderKey(keyDir?: string): Promise<string | undefined> {
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;
+4
View File
@@ -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
+21
View File
@@ -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/<hash>; XDG default: $XDG_STATE_HOME/omp/run/daemons/<hash>). */
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/*)
// =============================================================================