Skip to content

Commit e3a2485

Browse files
committed
fix(cli): preserve scoped secret resolution
Co-authored-by: wuyangfan <[email protected]> # Conflicts: # src/cli/capability-cli.test.ts # src/cli/capability-cli.ts
1 parent fe680e4 commit e3a2485

13 files changed

Lines changed: 924 additions & 63 deletions

scripts/repro/cli-web-search-secret-refs-live-proof.mjs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -38,8 +38,5 @@ console.log(
3838
"resolveCommandConfigWithSecrets apiKey is string =",
3939
typeof apiKey === "string" && apiKey.length > 0,
4040
);
41-
console.log(
42-
"resolved apiKey prefix =",
43-
typeof apiKey === "string" ? `${apiKey.slice(0, 8)}…` : apiKey,
44-
);
41+
console.log("resolved apiKey remains redacted =", typeof apiKey === "string");
4542
console.log("diagnostics count =", diagnostics.length);

src/cli/capability-cli.test.ts

Lines changed: 104 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ const mocks = vi.hoisted(() => ({
2020
writeStdout: vi.fn(),
2121
},
2222
loadConfig: vi.fn(() => ({})),
23+
getRuntimeConfigSourceSnapshot: vi.fn(() => null),
24+
setRuntimeConfigSnapshot: vi.fn(),
2325
loadAuthProfileStoreForRuntime: vi.fn(() => ({ profiles: {}, order: {} })),
2426
listProfilesForProvider: vi.fn(() => []),
2527
updateAuthProfileStoreWithLock: vi.fn(
@@ -154,8 +156,12 @@ vi.mock("../runtime.js", () => ({
154156
}));
155157

156158
vi.mock("../config/config.js", () => ({
159+
getRuntimeConfigSourceSnapshot:
160+
mocks.getRuntimeConfigSourceSnapshot as typeof import("../config/config.js").getRuntimeConfigSourceSnapshot,
157161
getRuntimeConfig: mocks.loadConfig as typeof import("../config/config.js").getRuntimeConfig,
158162
loadConfig: mocks.loadConfig as typeof import("../config/config.js").loadConfig,
163+
setRuntimeConfigSnapshot:
164+
mocks.setRuntimeConfigSnapshot as typeof import("../config/config.js").setRuntimeConfigSnapshot,
159165
}));
160166

161167
vi.mock("./command-config-resolution.js", () => ({
@@ -395,6 +401,8 @@ describe("capability cli", () => {
395401
.mockResolvedValue([{ id: "gpt-5.4", provider: "openai", name: "GPT-5.4" }] as never);
396402
mocks.loadAuthProfileStoreForRuntime.mockReset().mockReturnValue({ profiles: {}, order: {} });
397403
mocks.listProfilesForProvider.mockReset().mockReturnValue([]);
404+
mocks.getRuntimeConfigSourceSnapshot.mockReset().mockReturnValue(null);
405+
mocks.setRuntimeConfigSnapshot.mockClear();
398406
mocks.updateAuthProfileStoreWithLock
399407
.mockReset()
400408
.mockImplementation(async ({ updater }: { updater: (store: any) => boolean }) => {
@@ -440,6 +448,13 @@ describe("capability cli", () => {
440448
mocks.registerBuiltInMemoryEmbeddingProviders.mockClear();
441449
mocks.isWebSearchProviderConfigured.mockReset().mockReturnValue(false);
442450
mocks.isWebFetchProviderConfigured.mockReset().mockReturnValue(false);
451+
mocks.resolveCommandConfigWithSecrets
452+
.mockReset()
453+
.mockImplementation(async ({ config }: { config: Record<string, unknown> }) => ({
454+
resolvedConfig: config,
455+
effectiveConfig: config,
456+
diagnostics: [],
457+
}));
443458
mocks.modelsStatusCommand.mockClear();
444459
mocks.callGateway.mockImplementation((async ({ method }: { method: string }) => {
445460
if (method === "tts.status") {
@@ -498,6 +513,7 @@ describe("capability cli", () => {
498513
};
499514
type ImageDescribeParams = {
500515
filePath?: string;
516+
mediaUrl?: string;
501517
model?: unknown;
502518
prompt?: unknown;
503519
provider?: unknown;
@@ -528,6 +544,13 @@ describe("capability cli", () => {
528544
return calls[0]?.[0];
529545
}
530546

547+
function firstCommandConfigResolutionCall() {
548+
const calls = mocks.resolveCommandConfigWithSecrets.mock.calls as unknown as Array<
549+
[Record<string, unknown>]
550+
>;
551+
return calls[0]?.[0];
552+
}
553+
531554
function firstRegisteredEmbeddingBootstrapArg() {
532555
const calls = mocks.registerBuiltInMemoryEmbeddingProviders.mock.calls as unknown as Array<
533556
[{ registerMemoryEmbeddingProvider?: unknown }]
@@ -559,7 +582,7 @@ describe("capability cli", () => {
559582

560583
function firstAudioTranscriptionCall() {
561584
const calls = mocks.transcribeAudioFile.mock.calls as unknown as Array<
562-
[{ filePath?: string; language?: unknown; prompt?: unknown }]
585+
[{ cfg?: unknown; filePath?: string; language?: unknown; prompt?: unknown }]
563586
>;
564587
return calls[0]?.[0];
565588
}
@@ -1204,6 +1227,26 @@ describe("capability cli", () => {
12041227
expect(describeCall?.timeoutMs).toBe(90000);
12051228
});
12061229

1230+
it("keeps image describe URL files as remote media references", async () => {
1231+
await runRegisteredCli({
1232+
register: registerCapabilityCli as (program: Command) => void,
1233+
argv: [
1234+
"capability",
1235+
"image",
1236+
"describe",
1237+
"--file",
1238+
"https://example.com/photo.png",
1239+
"--json",
1240+
],
1241+
});
1242+
1243+
const describeCall = imageDescribeCall();
1244+
expect(describeCall?.filePath).toBe("https://example.com/photo.png");
1245+
expect(describeCall?.mediaUrl).toBe("https://example.com/photo.png");
1246+
const outputs = firstJsonOutput()?.outputs as Array<Record<string, unknown>>;
1247+
expect(outputs[0]?.path).toBe("https://example.com/photo.png");
1248+
});
1249+
12071250
it("uses the explicit media-understanding provider for image describe model overrides", async () => {
12081251
await runRegisteredCli({
12091252
register: registerCapabilityCli as (program: Command) => void,
@@ -1808,6 +1851,35 @@ describe("capability cli", () => {
18081851
expect(outputs[0]?.kind).toBe("audio.transcription");
18091852
});
18101853

1854+
it("resolves command SecretRefs before local audio transcription", async () => {
1855+
const rawConfig = { models: { providers: { openai: { apiKey: "raw-ref" } } } };
1856+
const resolvedConfig = { models: { providers: { openai: { apiKey: "resolved-key" } } } };
1857+
mocks.loadConfig.mockReturnValue(rawConfig);
1858+
mocks.resolveCommandConfigWithSecrets.mockResolvedValueOnce({
1859+
resolvedConfig,
1860+
effectiveConfig: resolvedConfig,
1861+
diagnostics: [],
1862+
} as never);
1863+
1864+
await runRegisteredCli({
1865+
register: registerCapabilityCli as (program: Command) => void,
1866+
argv: ["capability", "audio", "transcribe", "--file", "memo.m4a", "--json"],
1867+
});
1868+
1869+
expect(firstCommandConfigResolutionCall()).toEqual(
1870+
expect.objectContaining({
1871+
config: rawConfig,
1872+
commandName: "infer audio transcribe",
1873+
}),
1874+
);
1875+
expect(
1876+
(firstCommandConfigResolutionCall()?.targetIds as Set<string>).has(
1877+
"models.providers.*.apiKey",
1878+
),
1879+
).toBe(true);
1880+
expect(firstAudioTranscriptionCall()?.cfg).toBe(resolvedConfig);
1881+
});
1882+
18111883
it("fails audio transcribe when no transcript text is returned", async () => {
18121884
mocks.transcribeAudioFile.mockResolvedValueOnce({ text: undefined } as never);
18131885

@@ -1991,6 +2063,37 @@ describe("capability cli", () => {
19912063
expect(firstJsonOutput()?.model).toBe("text-embedding-3-small");
19922064
});
19932065

2066+
it("resolves command SecretRefs before local model capability execution", async () => {
2067+
const rawConfig = { agents: { defaults: { model: "openai/gpt-5.4" } } };
2068+
const resolvedConfig = { agents: { defaults: { model: "openai/gpt-5.4" } }, resolved: true };
2069+
mocks.loadConfig.mockReturnValue(rawConfig);
2070+
mocks.resolveCommandConfigWithSecrets.mockResolvedValueOnce({
2071+
resolvedConfig,
2072+
effectiveConfig: resolvedConfig,
2073+
diagnostics: [],
2074+
} as never);
2075+
2076+
await runRegisteredCli({
2077+
register: registerCapabilityCli as (program: Command) => void,
2078+
argv: ["capability", "model", "run", "--prompt", "hello", "--json"],
2079+
});
2080+
2081+
expect(firstCommandConfigResolutionCall()).toEqual(
2082+
expect.objectContaining({
2083+
config: rawConfig,
2084+
commandName: "infer model run",
2085+
runtime: mocks.runtime,
2086+
}),
2087+
);
2088+
expect(
2089+
(firstCommandConfigResolutionCall()?.targetIds as Set<string>).has(
2090+
"models.providers.*.apiKey",
2091+
),
2092+
).toBe(true);
2093+
expect(firstPreparedModelParams()?.cfg).toBe(resolvedConfig);
2094+
expect(mocks.setRuntimeConfigSnapshot).toHaveBeenCalledWith(resolvedConfig);
2095+
});
2096+
19942097
it("derives the embedding provider from a provider/model override", async () => {
19952098
await runRegisteredCli({
19962099
register: registerCapabilityCli as (program: Command) => void,

0 commit comments

Comments
 (0)