refactor(secrets): extracted buildSecretObfuscator from sdk session setup
Moved the obfuscator assembly (secrets.yml + env entries + built-in credential patterns, placeholder-key minting rules, redaction-only fallback) from createAgentSessionScoped into the secrets module so other entrypoints can build the same obfuscator.
This commit is contained in:
@@ -123,18 +123,12 @@ import lateDiagnosticTemplate from "./prompts/tools/lsp-late-diagnostic.md" with
|
||||
import { AgentLifecycleManager } from "./registry/agent-lifecycle";
|
||||
import { type AgentRef, AgentRegistry, MAIN_AGENT_ID } from "./registry/agent-registry";
|
||||
import {
|
||||
builtinCredentialSecretEntries,
|
||||
collectEnvSecrets,
|
||||
buildSecretObfuscator,
|
||||
deobfuscateSessionContext,
|
||||
deobfuscateToolArguments,
|
||||
getExistingSecretPlaceholderKey,
|
||||
getSecretPlaceholderKey,
|
||||
getSecretPlaceholderKeySync,
|
||||
loadSecrets,
|
||||
obfuscateMessages,
|
||||
obfuscateProviderContext,
|
||||
SecretObfuscator,
|
||||
secretEntriesNeedPlaceholderKey,
|
||||
type SecretObfuscator,
|
||||
} from "./secrets";
|
||||
import { AgentSession, type InitialRetryFallbackState, type PlanYolo, type Prewalk } from "./session/agent-session";
|
||||
import { discoverAuthStorage as discoverAuthStorageFromConfig } from "./session/auth-broker-config";
|
||||
@@ -1377,46 +1371,9 @@ async function createAgentSessionScoped(options: CreateAgentSessionOptions): Pro
|
||||
|
||||
// Load and create secret obfuscator early so resumed session state and prompt warnings
|
||||
// reflect actual loaded secrets, not just the setting toggle.
|
||||
let obfuscator: SecretObfuscator | undefined;
|
||||
if (settings.get("secrets.enabled")) {
|
||||
const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
|
||||
const envEntries = collectEnvSecrets();
|
||||
// Built-in credential-pattern entries come last so user-configured entries
|
||||
// (plain literals, custom regexes) take precedence in the scan order.
|
||||
const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
|
||||
// Only CONFIGURED entries force startup key creation: a configured
|
||||
// obfuscate-mode secret — or a default (no custom `replacement`)
|
||||
// replace-mode regex whose key-derived idempotent fallback marker needs a
|
||||
// stable key across restarts (see `secretEntryNeedsPlaceholderKey`) —
|
||||
// mints placeholders as soon as the obfuscator is built. The built-in
|
||||
// credential-pattern entry matches dynamically, so it resolves the
|
||||
// persisted key lazily on first match instead of creating the key file
|
||||
// for every secrets.enabled session; a session whose content never
|
||||
// contains a credential-shaped token must not require the key, otherwise a
|
||||
// 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(explicitAgentDir)
|
||||
: await getExistingSecretPlaceholderKey(explicitAgentDir);
|
||||
if (allEntries.length > 0) {
|
||||
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
|
||||
// plain entries, or no entries at all), but a persisted key exists. Build a
|
||||
// redaction-only obfuscator so a tool read of the key file does not ship the
|
||||
// reusable HMAC key to the provider.
|
||||
obfuscator = new SecretObfuscator(
|
||||
[{ type: "plain", mode: "replace", content: placeholderKey }],
|
||||
placeholderKey,
|
||||
);
|
||||
}
|
||||
}
|
||||
const obfuscator: SecretObfuscator | undefined = settings.get("secrets.enabled")
|
||||
? await buildSecretObfuscator(cwd, agentDir, options.agentDir)
|
||||
: undefined;
|
||||
const secretsEnabled = obfuscator?.hasSecrets() === true;
|
||||
|
||||
// An abnormal process exit after a non-terminal message tail is durable
|
||||
|
||||
@@ -4,7 +4,13 @@ import * as path from "node:path";
|
||||
import { SENSITIVE_TOKEN_RE } from "@oh-my-pi/pi-ai/providers/transform-messages";
|
||||
import { getSecretPlaceholderKeyPath, isEnoent, logger } from "@oh-my-pi/pi-utils";
|
||||
import { YAML } from "bun";
|
||||
import { regexHasUnresolvableShortMatchFallback, type SecretEntry, sanitizeSecretFriendlyName } from "./obfuscator";
|
||||
import {
|
||||
regexHasUnresolvableShortMatchFallback,
|
||||
type SecretEntry,
|
||||
SecretObfuscator,
|
||||
sanitizeSecretFriendlyName,
|
||||
secretEntriesNeedPlaceholderKey,
|
||||
} from "./obfuscator";
|
||||
import { compileSecretRegex } from "./regex";
|
||||
|
||||
const PLACEHOLDER_KEY_RE = /^[A-Za-z0-9_-]{43}$/;
|
||||
@@ -223,6 +229,51 @@ export function builtinCredentialSecretEntries(): SecretEntry[] {
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the session secret obfuscator from every configured source: secrets.yml
|
||||
* (project + global), secret-shaped environment variables, and the built-in
|
||||
* credential patterns. Callers gate on `secrets.enabled`.
|
||||
*
|
||||
* Only CONFIGURED entries force startup key creation: a configured
|
||||
* obfuscate-mode secret — or a default (no custom `replacement`) replace-mode
|
||||
* regex whose key-derived idempotent fallback marker needs a stable key across
|
||||
* restarts (see `secretEntryNeedsPlaceholderKey`) — mints placeholders as soon
|
||||
* as the obfuscator is built. The built-in credential-pattern entry matches
|
||||
* dynamically, so it resolves the persisted key lazily on first match instead
|
||||
* of creating the key file for every secrets-enabled session.
|
||||
*
|
||||
* When no configured entry produced an active secret but a persisted key
|
||||
* exists, returns a redaction-only obfuscator so a tool read of the key file
|
||||
* does not ship the reusable HMAC key to the provider. Returns undefined when
|
||||
* there is nothing to protect.
|
||||
*
|
||||
* `keyDir` is the explicit agent dir override for the placeholder-key file
|
||||
* (default XDG/agent location when omitted).
|
||||
*/
|
||||
export async function buildSecretObfuscator(
|
||||
cwd: string,
|
||||
agentDir: string,
|
||||
keyDir?: string,
|
||||
): Promise<SecretObfuscator | undefined> {
|
||||
const fileEntries = await logger.time("loadSecrets", loadSecrets, cwd, agentDir);
|
||||
const envEntries = collectEnvSecrets();
|
||||
// Built-in credential-pattern entries come last so user-configured entries
|
||||
// (plain literals, custom regexes) take precedence in the scan order.
|
||||
const allEntries = [...envEntries, ...fileEntries, ...builtinCredentialSecretEntries()];
|
||||
const needsPlaceholderKey = secretEntriesNeedPlaceholderKey([...envEntries, ...fileEntries]);
|
||||
const placeholderKey = needsPlaceholderKey
|
||||
? await getSecretPlaceholderKey(keyDir)
|
||||
: await getExistingSecretPlaceholderKey(keyDir);
|
||||
let obfuscator: SecretObfuscator | undefined;
|
||||
if (allEntries.length > 0) {
|
||||
obfuscator = new SecretObfuscator(allEntries, placeholderKey ?? (() => getSecretPlaceholderKeySync(keyDir)));
|
||||
}
|
||||
if (obfuscator?.hasSecrets() !== true && placeholderKey !== undefined) {
|
||||
obfuscator = new SecretObfuscator([{ type: "plain", mode: "replace", content: placeholderKey }], placeholderKey);
|
||||
}
|
||||
return obfuscator;
|
||||
}
|
||||
|
||||
async function loadSecretsFile(filePath: string): Promise<SecretEntry[]> {
|
||||
try {
|
||||
const text = await Bun.file(filePath).text();
|
||||
|
||||
Reference in New Issue
Block a user