fix: stabilize secrets land + docs note (#26155) (thanks @joshavant)
This commit is contained in:
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Agents/Routing CLI: add `openclaw agents bindings`, `openclaw agents bind`, and `openclaw agents unbind` for account-scoped route management, including channel-only to account-scoped binding upgrades, role-aware binding identity handling, plugin-resolved binding account IDs, and optional account-binding prompts in `openclaw channels add`. (#27195) thanks @gumadeiras.
|
||||
- Android/Nodes: add `notifications.list` support on Android nodes and expose `nodes notifications_list` in agent tooling for listing active device notifications. (#27344) thanks @obviyus.
|
||||
- Onboarding/Plugins: let channel plugins own interactive onboarding flows with optional `configureInteractive` and `configureWhenConfigured` hooks while preserving the generic fallback path. (#27191) thanks @gumadeiras.
|
||||
- Secrets/External management: add external secrets runtime activation, migration/apply safety hardening, and dedicated docs for strict `secrets apply` target-path rules and ref-only auth-profile behavior. (#26155) Thanks @joshavant.
|
||||
|
||||
### Fixes
|
||||
|
||||
|
||||
@@ -338,24 +338,20 @@ function applyLegacyStore(store: AuthProfileStore, legacy: LegacyAuthStore): voi
|
||||
}
|
||||
}
|
||||
|
||||
function loadCoercedStoreWithExternalSync(authPath: string): AuthProfileStore | null {
|
||||
function loadCoercedStore(authPath: string): AuthProfileStore | null {
|
||||
const raw = loadJsonFile(authPath);
|
||||
const store = coerceAuthStore(raw);
|
||||
if (!store) {
|
||||
return null;
|
||||
}
|
||||
// Sync from external CLI tools on every load.
|
||||
const synced = syncExternalCliCredentials(store);
|
||||
if (synced) {
|
||||
saveJsonFile(authPath, store);
|
||||
}
|
||||
return store;
|
||||
return coerceAuthStore(raw);
|
||||
}
|
||||
|
||||
export function loadAuthProfileStore(): AuthProfileStore {
|
||||
const authPath = resolveAuthStorePath();
|
||||
const asStore = loadCoercedStoreWithExternalSync(authPath);
|
||||
const asStore = loadCoercedStore(authPath);
|
||||
if (asStore) {
|
||||
// Sync from external CLI tools on every load.
|
||||
const synced = syncExternalCliCredentials(asStore);
|
||||
if (synced) {
|
||||
saveJsonFile(authPath, asStore);
|
||||
}
|
||||
return asStore;
|
||||
}
|
||||
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
|
||||
@@ -381,7 +377,7 @@ function loadAuthProfileStoreForAgent(
|
||||
): AuthProfileStore {
|
||||
const readOnly = options?.readOnly === true;
|
||||
const authPath = resolveAuthStorePath(agentDir);
|
||||
const asStore = loadCoercedStoreWithExternalSync(authPath);
|
||||
const asStore = loadCoercedStore(authPath);
|
||||
if (asStore) {
|
||||
// Runtime secret activation must remain read-only:
|
||||
// sync external CLI credentials in-memory, but never persist while readOnly.
|
||||
|
||||
@@ -22,7 +22,6 @@ describe("secrets apply", () => {
|
||||
authJsonPath = path.join(stateDir, "agents", "main", "agent", "auth.json");
|
||||
envPath = path.join(stateDir, ".env");
|
||||
env = {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENAI_API_KEY: "sk-live-env",
|
||||
@@ -170,6 +169,10 @@ describe("secrets apply", () => {
|
||||
|
||||
const first = await runSecretsApply({ plan, env, write: true });
|
||||
expect(first.changed).toBe(true);
|
||||
const configAfterFirst = await fs.readFile(configPath, "utf8");
|
||||
const authStoreAfterFirst = await fs.readFile(authStorePath, "utf8");
|
||||
const authJsonAfterFirst = await fs.readFile(authJsonPath, "utf8");
|
||||
const envAfterFirst = await fs.readFile(envPath, "utf8");
|
||||
|
||||
// Second apply should be a true no-op and avoid file writes entirely.
|
||||
await fs.chmod(configPath, 0o400);
|
||||
@@ -177,8 +180,10 @@ describe("secrets apply", () => {
|
||||
|
||||
const second = await runSecretsApply({ plan, env, write: true });
|
||||
expect(second.mode).toBe("write");
|
||||
expect(second.changed).toBe(false);
|
||||
expect(second.changedFiles).toEqual([]);
|
||||
await expect(fs.readFile(configPath, "utf8")).resolves.toBe(configAfterFirst);
|
||||
await expect(fs.readFile(authStorePath, "utf8")).resolves.toBe(authStoreAfterFirst);
|
||||
await expect(fs.readFile(authJsonPath, "utf8")).resolves.toBe(authJsonAfterFirst);
|
||||
await expect(fs.readFile(envPath, "utf8")).resolves.toBe(envAfterFirst);
|
||||
});
|
||||
|
||||
it("applies targets safely when map keys contain dots", async () => {
|
||||
|
||||
@@ -174,7 +174,9 @@ function scrubEnvRaw(
|
||||
|
||||
function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] {
|
||||
const paths = new Set<string>();
|
||||
paths.add(resolveUserPath(resolveAuthStorePath()));
|
||||
// Scope default auth store discovery to the provided stateDir instead of
|
||||
// ambient process env, so apply does not touch unrelated host-global stores.
|
||||
paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"));
|
||||
|
||||
const agentsRoot = path.join(resolveUserPath(stateDir), "agents");
|
||||
if (fs.existsSync(agentsRoot)) {
|
||||
@@ -187,6 +189,12 @@ function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string
|
||||
}
|
||||
|
||||
for (const agentId of listAgentIds(config)) {
|
||||
if (agentId === "main") {
|
||||
paths.add(
|
||||
path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const agentDir = resolveAgentDir(config, agentId);
|
||||
paths.add(resolveUserPath(resolveAuthStorePath(agentDir)));
|
||||
}
|
||||
|
||||
@@ -21,10 +21,12 @@ describe("secrets audit", () => {
|
||||
authJsonPath = path.join(stateDir, "agents", "main", "agent", "auth.json");
|
||||
envPath = path.join(stateDir, ".env");
|
||||
env = {
|
||||
...process.env,
|
||||
OPENCLAW_STATE_DIR: stateDir,
|
||||
OPENCLAW_CONFIG_PATH: configPath,
|
||||
OPENAI_API_KEY: "env-openai-key",
|
||||
...(typeof process.env.PATH === "string" && process.env.PATH.trim().length > 0
|
||||
? { PATH: process.env.PATH }
|
||||
: { PATH: "/usr/bin:/bin" }),
|
||||
};
|
||||
|
||||
await fs.mkdir(path.dirname(configPath), { recursive: true });
|
||||
|
||||
@@ -308,7 +308,9 @@ function collectConfigSecrets(params: {
|
||||
|
||||
function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] {
|
||||
const paths = new Set<string>();
|
||||
paths.add(resolveUserPath(resolveAuthStorePath()));
|
||||
// Scope default auth store discovery to the provided stateDir instead of
|
||||
// ambient process env, so audits do not include unrelated host-global stores.
|
||||
paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"));
|
||||
|
||||
const agentsRoot = path.join(resolveUserPath(stateDir), "agents");
|
||||
if (fs.existsSync(agentsRoot)) {
|
||||
@@ -321,6 +323,12 @@ function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string
|
||||
}
|
||||
|
||||
for (const agentId of listAgentIds(config)) {
|
||||
if (agentId === "main") {
|
||||
paths.add(
|
||||
path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"),
|
||||
);
|
||||
continue;
|
||||
}
|
||||
const agentDir = resolveAgentDir(config, agentId);
|
||||
paths.add(resolveUserPath(resolveAuthStorePath(agentDir)));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user