mirror of
https://github.com/openclaw/openclaw.git
synced 2026-03-12 07:20:45 +00:00
CLI: prevent agent-path models.json secret persistence
This commit is contained in:
@@ -22,3 +22,7 @@ openclaw agent --agent ops --message "Summarize logs"
|
|||||||
openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium
|
openclaw agent --session-id 1234 --message "Summarize inbox" --thinking medium
|
||||||
openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"
|
openclaw agent --agent ops --message "Generate report" --deliver --reply-channel slack --reply-to "#reports"
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- When this command triggers `models.json` regeneration, SecretRef-managed provider credentials are persisted as non-secret markers (for example env var names or `secretref-managed`), not resolved secret plaintext.
|
||||||
|
|||||||
@@ -217,3 +217,5 @@ Merge mode precedence for matching provider IDs:
|
|||||||
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
|
- SecretRef-managed provider `apiKey` values are refreshed from source markers (`ENV_VAR_NAME` for env refs, `secretref-managed` for file/exec refs) instead of persisting resolved secrets.
|
||||||
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
|
- Empty or missing agent `apiKey`/`baseUrl` fall back to config `models.providers`.
|
||||||
- Other provider fields are refreshed from config and normalized catalog data.
|
- Other provider fields are refreshed from config and normalized catalog data.
|
||||||
|
|
||||||
|
This marker-based persistence applies whenever OpenClaw regenerates `models.json`, including command-driven paths like `openclaw agent`.
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import { FailoverError } from "../agents/failover-error.js";
|
|||||||
import { loadModelCatalog } from "../agents/model-catalog.js";
|
import { loadModelCatalog } from "../agents/model-catalog.js";
|
||||||
import * as modelSelectionModule from "../agents/model-selection.js";
|
import * as modelSelectionModule from "../agents/model-selection.js";
|
||||||
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
import { runEmbeddedPiAgent } from "../agents/pi-embedded.js";
|
||||||
|
import * as commandSecretGatewayModule from "../cli/command-secret-gateway.js";
|
||||||
import type { OpenClawConfig } from "../config/config.js";
|
import type { OpenClawConfig } from "../config/config.js";
|
||||||
import * as configModule from "../config/config.js";
|
import * as configModule from "../config/config.js";
|
||||||
import * as sessionsModule from "../config/sessions.js";
|
import * as sessionsModule from "../config/sessions.js";
|
||||||
@@ -51,6 +52,8 @@ const runtime: RuntimeEnv = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const configSpy = vi.spyOn(configModule, "loadConfig");
|
const configSpy = vi.spyOn(configModule, "loadConfig");
|
||||||
|
const readConfigFileSnapshotForWriteSpy = vi.spyOn(configModule, "readConfigFileSnapshotForWrite");
|
||||||
|
const setRuntimeConfigSnapshotSpy = vi.spyOn(configModule, "setRuntimeConfigSnapshot");
|
||||||
const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent");
|
const runCliAgentSpy = vi.spyOn(cliRunnerModule, "runCliAgent");
|
||||||
const deliverAgentCommandResultSpy = vi.spyOn(agentDeliveryModule, "deliverAgentCommandResult");
|
const deliverAgentCommandResultSpy = vi.spyOn(agentDeliveryModule, "deliverAgentCommandResult");
|
||||||
|
|
||||||
@@ -256,13 +259,91 @@ function createTelegramOutboundPlugin() {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
vi.clearAllMocks();
|
vi.clearAllMocks();
|
||||||
|
configModule.clearRuntimeConfigSnapshot();
|
||||||
runCliAgentSpy.mockResolvedValue(createDefaultAgentResult() as never);
|
runCliAgentSpy.mockResolvedValue(createDefaultAgentResult() as never);
|
||||||
vi.mocked(runEmbeddedPiAgent).mockResolvedValue(createDefaultAgentResult());
|
vi.mocked(runEmbeddedPiAgent).mockResolvedValue(createDefaultAgentResult());
|
||||||
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
vi.mocked(loadModelCatalog).mockResolvedValue([]);
|
||||||
vi.mocked(modelSelectionModule.isCliProvider).mockImplementation(() => false);
|
vi.mocked(modelSelectionModule.isCliProvider).mockImplementation(() => false);
|
||||||
|
readConfigFileSnapshotForWriteSpy.mockResolvedValue({
|
||||||
|
snapshot: { valid: false, resolved: {} as OpenClawConfig },
|
||||||
|
writeOptions: {},
|
||||||
|
} as Awaited<ReturnType<typeof configModule.readConfigFileSnapshotForWrite>>);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("agentCommand", () => {
|
describe("agentCommand", () => {
|
||||||
|
it("sets runtime snapshots from source config before embedded agent run", async () => {
|
||||||
|
await withTempHome(async (home) => {
|
||||||
|
const store = path.join(home, "sessions.json");
|
||||||
|
const loadedConfig = {
|
||||||
|
agents: {
|
||||||
|
defaults: {
|
||||||
|
model: { primary: "anthropic/claude-opus-4-5" },
|
||||||
|
models: { "anthropic/claude-opus-4-5": {} },
|
||||||
|
workspace: path.join(home, "openclaw"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
session: { store, mainKey: "main" },
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
baseUrl: "https://api.openai.com/v1",
|
||||||
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpenClawConfig;
|
||||||
|
const sourceConfig = {
|
||||||
|
...loadedConfig,
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
baseUrl: "https://api.openai.com/v1",
|
||||||
|
apiKey: { source: "env", provider: "default", id: "OPENAI_API_KEY" },
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpenClawConfig;
|
||||||
|
const resolvedConfig = {
|
||||||
|
...loadedConfig,
|
||||||
|
models: {
|
||||||
|
providers: {
|
||||||
|
openai: {
|
||||||
|
baseUrl: "https://api.openai.com/v1",
|
||||||
|
apiKey: "sk-resolved-runtime",
|
||||||
|
models: [],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} as unknown as OpenClawConfig;
|
||||||
|
|
||||||
|
configSpy.mockReturnValue(loadedConfig);
|
||||||
|
readConfigFileSnapshotForWriteSpy.mockResolvedValue({
|
||||||
|
snapshot: { valid: true, resolved: sourceConfig },
|
||||||
|
writeOptions: {},
|
||||||
|
} as Awaited<ReturnType<typeof configModule.readConfigFileSnapshotForWrite>>);
|
||||||
|
const resolveSecretsSpy = vi
|
||||||
|
.spyOn(commandSecretGatewayModule, "resolveCommandSecretRefsViaGateway")
|
||||||
|
.mockResolvedValueOnce({
|
||||||
|
resolvedConfig,
|
||||||
|
diagnostics: [],
|
||||||
|
targetStatesByPath: {},
|
||||||
|
hadUnresolvedTargets: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
await agentCommand({ message: "hello", to: "+1555" }, runtime);
|
||||||
|
|
||||||
|
expect(resolveSecretsSpy).toHaveBeenCalledWith({
|
||||||
|
config: loadedConfig,
|
||||||
|
commandName: "agent",
|
||||||
|
targetIds: expect.any(Set),
|
||||||
|
});
|
||||||
|
expect(setRuntimeConfigSnapshotSpy).toHaveBeenCalledWith(resolvedConfig, sourceConfig);
|
||||||
|
expect(vi.mocked(runEmbeddedPiAgent).mock.calls.at(-1)?.[0]?.config).toBe(resolvedConfig);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("creates a session entry when deriving from --to", async () => {
|
it("creates a session entry when deriving from --to", async () => {
|
||||||
await withTempHome(async (home) => {
|
await withTempHome(async (home) => {
|
||||||
const store = path.join(home, "sessions.json");
|
const store = path.join(home, "sessions.json");
|
||||||
|
|||||||
@@ -57,7 +57,11 @@ import { formatCliCommand } from "../cli/command-format.js";
|
|||||||
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js";
|
||||||
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
import { getAgentRuntimeCommandSecretTargetIds } from "../cli/command-secret-targets.js";
|
||||||
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
|
import { type CliDeps, createDefaultDeps } from "../cli/deps.js";
|
||||||
import { loadConfig } from "../config/config.js";
|
import {
|
||||||
|
loadConfig,
|
||||||
|
readConfigFileSnapshotForWrite,
|
||||||
|
setRuntimeConfigSnapshot,
|
||||||
|
} from "../config/config.js";
|
||||||
import {
|
import {
|
||||||
mergeSessionEntry,
|
mergeSessionEntry,
|
||||||
parseSessionThreadInfo,
|
parseSessionThreadInfo,
|
||||||
@@ -427,11 +431,23 @@ async function agentCommandInternal(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const loadedRaw = loadConfig();
|
const loadedRaw = loadConfig();
|
||||||
|
const sourceConfig = await (async () => {
|
||||||
|
try {
|
||||||
|
const { snapshot } = await readConfigFileSnapshotForWrite();
|
||||||
|
if (snapshot.valid) {
|
||||||
|
return snapshot.resolved;
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Fall back to runtime-loaded config when source snapshot is unavailable.
|
||||||
|
}
|
||||||
|
return loadedRaw;
|
||||||
|
})();
|
||||||
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
const { resolvedConfig: cfg, diagnostics } = await resolveCommandSecretRefsViaGateway({
|
||||||
config: loadedRaw,
|
config: loadedRaw,
|
||||||
commandName: "agent",
|
commandName: "agent",
|
||||||
targetIds: getAgentRuntimeCommandSecretTargetIds(),
|
targetIds: getAgentRuntimeCommandSecretTargetIds(),
|
||||||
});
|
});
|
||||||
|
setRuntimeConfigSnapshot(cfg, sourceConfig);
|
||||||
for (const entry of diagnostics) {
|
for (const entry of diagnostics) {
|
||||||
runtime.log(`[secrets] ${entry}`);
|
runtime.log(`[secrets] ${entry}`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -51,6 +51,10 @@ function sanitizeProviderHeaders(
|
|||||||
if (typeof value !== "string") {
|
if (typeof value !== "string") {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
// Intentionally preserve marker-shaped values here. This path handles
|
||||||
|
// explicit config/runtime provider headers, where literal values may
|
||||||
|
// legitimately match marker patterns; discovered models.json entries are
|
||||||
|
// sanitized separately in the model registry path.
|
||||||
next[key] = value;
|
next[key] = value;
|
||||||
}
|
}
|
||||||
return Object.keys(next).length > 0 ? next : undefined;
|
return Object.keys(next).length > 0 ? next : undefined;
|
||||||
|
|||||||
Reference in New Issue
Block a user