Skip to content

Commit 5a788e4

Browse files
committed
fix(daemon): tighten deployment credential boundaries
1 parent 7e9419e commit 5a788e4

5 files changed

Lines changed: 52 additions & 4 deletions

File tree

apps/daemon/src/cli.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6357,7 +6357,8 @@ Common options:
63576357
--daemon-url <url> Open Design daemon HTTP base.
63586358
--deployment-credential
63596359
Use the daemon-managed deployment credential with the
6360-
BYOK OpenCode agent; no browser credential is sent.
6360+
BYOK OpenCode agent; requires --model <id> and sends no
6361+
browser credential.
63616362
--json Emit raw JSON.`);
63626363
process.exit(args.length === 0 ? 2 : 0);
63636364
}
@@ -6564,6 +6565,10 @@ Common options:
65646565
console.error('--deployment-credential requires --agent byok-opencode when --agent is set');
65656566
process.exit(2);
65666567
}
6568+
if (!flags.model) {
6569+
console.error('--deployment-credential requires --model <id>');
6570+
process.exit(2);
6571+
}
65676572
body.agentId = 'byok-opencode';
65686573
body.byokCredentialSource = 'deployment';
65696574
}

apps/daemon/tests/run-cli.test.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ describe('od run CLI', () => {
117117
'--message',
118118
'Build the settings screen',
119119
'--deployment-credential',
120+
'--model',
121+
'gpt-5.4-mini',
120122
'--json',
121123
'--daemon-url',
122124
stub.baseUrl,
@@ -132,16 +134,37 @@ describe('od run CLI', () => {
132134
projectId: 'project-1',
133135
message: 'Build the settings screen',
134136
agentId: 'byok-opencode',
137+
model: 'gpt-5.4-mini',
135138
byokCredentialSource: 'deployment',
136139
});
137140
});
138141

142+
it('rejects a deployment credential run without an explicit model', async () => {
143+
stub = await startRunStubServer(true);
144+
145+
const result = await runCli([
146+
'run',
147+
'start',
148+
'--project',
149+
'project-1',
150+
'--deployment-credential',
151+
'--json',
152+
'--daemon-url',
153+
stub.baseUrl,
154+
]);
155+
156+
expect(result.code).toBe(2);
157+
expect(result.stderr).toContain('--deployment-credential requires --model <id>');
158+
expect(stub.requests).toEqual([]);
159+
});
160+
139161
it('documents the deployment credential selector in run help', async () => {
140162
const result = await runCli(['run', '--help']);
141163

142164
expect(result.code).toBe(0);
143165
expect(result.stdout).toContain('--deployment-credential');
144166
expect(result.stdout).toContain('BYOK OpenCode agent');
167+
expect(result.stdout).toContain('requires --model <id>');
145168
});
146169

147170
it('continues a resumable run through the normal run creation API', async () => {

apps/web/src/providers/daemon.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,6 @@ import type {
3131
DaemonAgentPayload,
3232
AmrModelsResponse,
3333
AmrWalletSnapshot,
34-
ByokChatProviderConfig,
3534
MediaExecutionPolicy,
3635
ResearchOptions,
3736
RunContextSelection,
@@ -312,7 +311,7 @@ export interface DaemonStreamOptions {
312311
model?: string | null;
313312
reasoning?: string | null;
314313
serviceTier?: string | null;
315-
byokProvider?: ByokChatProviderConfig;
314+
byokProvider?: ChatRequest['byokProvider'];
316315
/** Selects the daemon-managed deployment provider without exposing its secret. */
317316
byokCredentialSource?: 'deployment';
318317
byokMediaDefaults?: ChatRequest['byokMediaDefaults'];

packages/contracts/src/api/chat.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ export interface ChatRequest {
113113
* adapter. The daemon must not persist this object; it is translated into
114114
* child env + OPENCODE_CONFIG_CONTENT for the current run only.
115115
*/
116-
byokProvider?: ByokChatProviderConfig;
116+
byokProvider?: UserByokChatProviderConfig;
117117
/**
118118
* Selects the administrator-managed deployment provider for a BYOK OpenCode
119119
* run. This is deliberately a discriminator only: no credential or endpoint
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
import { describe, expect, it } from 'vitest';
2+
3+
import type { ChatRequest } from '../src/api/chat.js';
4+
5+
describe('ChatRequest BYOK credential boundary', () => {
6+
it('keeps deployment selection out of the run-scoped provider object', () => {
7+
const userProvider: NonNullable<ChatRequest['byokProvider']> = {
8+
protocol: 'openai',
9+
apiKey: 'run-scoped-secret',
10+
};
11+
12+
const nestedDeployment: NonNullable<ChatRequest['byokProvider']> = {
13+
protocol: 'openai',
14+
// @ts-expect-error Deployment selection belongs in byokCredentialSource.
15+
credentialSource: 'deployment',
16+
};
17+
18+
expect(userProvider.credentialSource).toBeUndefined();
19+
expect(nestedDeployment.credentialSource).toBe('deployment');
20+
});
21+
});

0 commit comments

Comments
 (0)