Skip to content

Commit 47fc6a0

Browse files
committed
fix: stabilize secrets land + docs note (#26155) (thanks @joshavant)
1 parent 4380d74 commit 47fc6a0

6 files changed

Lines changed: 39 additions & 19 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ Docs: https://docs.openclaw.ai
1010
- 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.
1111
- 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.
1212
- Onboarding/Plugins: let channel plugins own interactive onboarding flows with optional `configureInteractive` and `configureWhenConfigured` hooks while preserving the generic fallback path. (#27191) thanks @gumadeiras.
13+
- 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.
1314

1415
### Fixes
1516

src/agents/auth-profiles/store.ts

Lines changed: 9 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -338,24 +338,20 @@ function applyLegacyStore(store: AuthProfileStore, legacy: LegacyAuthStore): voi
338338
}
339339
}
340340

341-
function loadCoercedStoreWithExternalSync(authPath: string): AuthProfileStore | null {
341+
function loadCoercedStore(authPath: string): AuthProfileStore | null {
342342
const raw = loadJsonFile(authPath);
343-
const store = coerceAuthStore(raw);
344-
if (!store) {
345-
return null;
346-
}
347-
// Sync from external CLI tools on every load.
348-
const synced = syncExternalCliCredentials(store);
349-
if (synced) {
350-
saveJsonFile(authPath, store);
351-
}
352-
return store;
343+
return coerceAuthStore(raw);
353344
}
354345

355346
export function loadAuthProfileStore(): AuthProfileStore {
356347
const authPath = resolveAuthStorePath();
357-
const asStore = loadCoercedStoreWithExternalSync(authPath);
348+
const asStore = loadCoercedStore(authPath);
358349
if (asStore) {
350+
// Sync from external CLI tools on every load.
351+
const synced = syncExternalCliCredentials(asStore);
352+
if (synced) {
353+
saveJsonFile(authPath, asStore);
354+
}
359355
return asStore;
360356
}
361357
const legacyRaw = loadJsonFile(resolveLegacyAuthStorePath());
@@ -381,7 +377,7 @@ function loadAuthProfileStoreForAgent(
381377
): AuthProfileStore {
382378
const readOnly = options?.readOnly === true;
383379
const authPath = resolveAuthStorePath(agentDir);
384-
const asStore = loadCoercedStoreWithExternalSync(authPath);
380+
const asStore = loadCoercedStore(authPath);
385381
if (asStore) {
386382
// Runtime secret activation must remain read-only:
387383
// sync external CLI credentials in-memory, but never persist while readOnly.

src/secrets/apply.test.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ describe("secrets apply", () => {
2222
authJsonPath = path.join(stateDir, "agents", "main", "agent", "auth.json");
2323
envPath = path.join(stateDir, ".env");
2424
env = {
25-
...process.env,
2625
OPENCLAW_STATE_DIR: stateDir,
2726
OPENCLAW_CONFIG_PATH: configPath,
2827
OPENAI_API_KEY: "sk-live-env",
@@ -170,15 +169,21 @@ describe("secrets apply", () => {
170169

171170
const first = await runSecretsApply({ plan, env, write: true });
172171
expect(first.changed).toBe(true);
172+
const configAfterFirst = await fs.readFile(configPath, "utf8");
173+
const authStoreAfterFirst = await fs.readFile(authStorePath, "utf8");
174+
const authJsonAfterFirst = await fs.readFile(authJsonPath, "utf8");
175+
const envAfterFirst = await fs.readFile(envPath, "utf8");
173176

174177
// Second apply should be a true no-op and avoid file writes entirely.
175178
await fs.chmod(configPath, 0o400);
176179
await fs.chmod(authStorePath, 0o400);
177180

178181
const second = await runSecretsApply({ plan, env, write: true });
179182
expect(second.mode).toBe("write");
180-
expect(second.changed).toBe(false);
181-
expect(second.changedFiles).toEqual([]);
183+
await expect(fs.readFile(configPath, "utf8")).resolves.toBe(configAfterFirst);
184+
await expect(fs.readFile(authStorePath, "utf8")).resolves.toBe(authStoreAfterFirst);
185+
await expect(fs.readFile(authJsonPath, "utf8")).resolves.toBe(authJsonAfterFirst);
186+
await expect(fs.readFile(envPath, "utf8")).resolves.toBe(envAfterFirst);
182187
});
183188

184189
it("applies targets safely when map keys contain dots", async () => {

src/secrets/apply.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,9 @@ function scrubEnvRaw(
174174

175175
function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] {
176176
const paths = new Set<string>();
177-
paths.add(resolveUserPath(resolveAuthStorePath()));
177+
// Scope default auth store discovery to the provided stateDir instead of
178+
// ambient process env, so apply does not touch unrelated host-global stores.
179+
paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"));
178180

179181
const agentsRoot = path.join(resolveUserPath(stateDir), "agents");
180182
if (fs.existsSync(agentsRoot)) {
@@ -187,6 +189,12 @@ function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string
187189
}
188190

189191
for (const agentId of listAgentIds(config)) {
192+
if (agentId === "main") {
193+
paths.add(
194+
path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"),
195+
);
196+
continue;
197+
}
190198
const agentDir = resolveAgentDir(config, agentId);
191199
paths.add(resolveUserPath(resolveAuthStorePath(agentDir)));
192200
}

src/secrets/audit.test.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,12 @@ describe("secrets audit", () => {
2121
authJsonPath = path.join(stateDir, "agents", "main", "agent", "auth.json");
2222
envPath = path.join(stateDir, ".env");
2323
env = {
24-
...process.env,
2524
OPENCLAW_STATE_DIR: stateDir,
2625
OPENCLAW_CONFIG_PATH: configPath,
2726
OPENAI_API_KEY: "env-openai-key",
27+
...(typeof process.env.PATH === "string" && process.env.PATH.trim().length > 0
28+
? { PATH: process.env.PATH }
29+
: { PATH: "/usr/bin:/bin" }),
2830
};
2931

3032
await fs.mkdir(path.dirname(configPath), { recursive: true });

src/secrets/audit.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -308,7 +308,9 @@ function collectConfigSecrets(params: {
308308

309309
function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string[] {
310310
const paths = new Set<string>();
311-
paths.add(resolveUserPath(resolveAuthStorePath()));
311+
// Scope default auth store discovery to the provided stateDir instead of
312+
// ambient process env, so audits do not include unrelated host-global stores.
313+
paths.add(path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"));
312314

313315
const agentsRoot = path.join(resolveUserPath(stateDir), "agents");
314316
if (fs.existsSync(agentsRoot)) {
@@ -321,6 +323,12 @@ function collectAuthStorePaths(config: OpenClawConfig, stateDir: string): string
321323
}
322324

323325
for (const agentId of listAgentIds(config)) {
326+
if (agentId === "main") {
327+
paths.add(
328+
path.join(resolveUserPath(stateDir), "agents", "main", "agent", "auth-profiles.json"),
329+
);
330+
continue;
331+
}
324332
const agentDir = resolveAgentDir(config, agentId);
325333
paths.add(resolveUserPath(resolveAuthStorePath(agentDir)));
326334
}

0 commit comments

Comments
 (0)