Skip to content
Closed
Show file tree
Hide file tree
Changes from 81 commits
Commits
Show all changes
84 commits
Select commit Hold shift + click to select a range
48719e3
feat(daemon): resolve deployment provider credentials
dredozubov Jun 19, 2026
f03bb54
feat(web): surface deployment provider credentials
dredozubov Jun 19, 2026
a36d2ea
feat(cli): expose deployment provider status
dredozubov Jun 19, 2026
87764d2
fix(web): preserve initial provider model fetch
dredozubov Jun 19, 2026
7e3df44
docs: document deployment provider configuration
dredozubov Jun 19, 2026
0d5fc23
fix(web): auto-verify deployment provider onboarding
dredozubov Jun 19, 2026
5b2e96f
fix(web): handle empty deployment provider catalog
dredozubov Jun 19, 2026
7920ed1
chore: refresh provider validation
dredozubov Jun 19, 2026
a7ac544
feat: add deployment provider run sessions
dredozubov Jun 19, 2026
80358fa
fix: allow deployment provider service-network endpoints
dredozubov Jun 19, 2026
300deb9
Merge origin/main into provider orchestrator runtime
dredozubov Jun 20, 2026
5e301b7
fix(web): skip empty terminal run replay
dredozubov Jun 20, 2026
02cb542
Merge remote-tracking branch 'origin/main' into provider-orchestrator…
dredozubov Jun 20, 2026
eec98e1
fix(daemon): allow realpath opencode cwd aliases
dredozubov Jun 20, 2026
0ad9878
chore: scope deployment provider routing to daemon APIs
dredozubov Jun 21, 2026
8fc03ad
test: stabilize AMR logout P0 agent discovery
dredozubov Jun 21, 2026
5d2a651
fix(daemon): harden deployment provider bootstrap
dredozubov Jun 21, 2026
9652cfc
fix: harden deployment provider credential mode
dredozubov Jun 21, 2026
2b7b5d8
fix(daemon): allow private deployment gateways
dredozubov Jun 21, 2026
4e56ac0
fix(web): support deployment provider onboarding
dredozubov Jun 21, 2026
b7b0944
fix: close deployment provider review gaps
dredozubov Jun 21, 2026
2ba7a4e
fix(web): harden deployment provider model selection
dredozubov Jun 21, 2026
0e0cae0
fix(web): preserve deployment provider drafts
dredozubov Jun 21, 2026
7aa9f4c
fix(daemon): reject deployment provider URL userinfo
dredozubov Jun 21, 2026
dd50040
fix(web): use live deployment finalize model
dredozubov Jun 21, 2026
5fb1b72
fix(daemon): align deployment run-session error codes
dredozubov Jun 21, 2026
5e82ddc
fix: share deployment provider run sessions
dredozubov Jun 21, 2026
952c5f5
fix: defer deployment run sessions until egress approval
dredozubov Jun 21, 2026
17f3e67
Merge origin/main into provider orchestrator runtime
dredozubov Jun 22, 2026
25eea46
fix(daemon): require stable deployment run ids
dredozubov Jun 22, 2026
93ea16b
Merge origin/main into provider-orchestrator-runtime
dredozubov Jun 25, 2026
99ce320
fix: close deployment provider review gaps
dredozubov Jun 25, 2026
2299f43
fix: preserve deployment onboarding drafts
dredozubov Jun 25, 2026
4be4340
Merge origin/main into provider orchestrator runtime
dredozubov Jun 25, 2026
afd723c
fix: restore deployment provider byok draft
dredozubov Jun 25, 2026
2e7ebda
Merge provider orchestrator follow-up
dredozubov Jun 25, 2026
7e98e30
Merge remote-tracking branch 'fork/provider-orchestrator-runtime' int…
dredozubov Jun 25, 2026
037250b
Merge origin/main into provider-orchestrator-runtime
dredozubov Jun 25, 2026
b001a5e
Merge remote provider branch
dredozubov Jun 25, 2026
9657ece
fix: align onboarding tests with current flow
dredozubov Jun 25, 2026
028f2f0
fix: align onboarding tests with current flow
dredozubov Jun 25, 2026
2cc3393
fix(web): preserve deployment onboarding state
dredozubov Jun 25, 2026
d01df07
Fix AMR onboarding e2e finish button
dredozubov Jun 25, 2026
074729f
Merge remote-tracking branch 'fork/provider-orchestrator-runtime' int…
dredozubov Jun 25, 2026
630e8e7
Merge remote-tracking branch 'fork/provider-orchestrator-runtime' int…
dredozubov Jun 25, 2026
c51587a
Fix deployment provider review regressions
dredozubov Jun 25, 2026
bd1375d
fix(daemon): refresh deployment run session fallback ids
dredozubov Jun 25, 2026
82a4ed4
fix: close deployment provider review blockers
dredozubov Jun 25, 2026
2cda113
Merge main into provider orchestrator runtime
dredozubov Jun 25, 2026
709f456
fix(web): keep deployment model switcher configured
dredozubov Jun 27, 2026
a8e8667
Merge main into provider orchestrator runtime
dredozubov Jun 30, 2026
8862063
fix(web): allow AIHubMix avatar model fetch without key
dredozubov Jun 30, 2026
03a5c99
fix(web): polish deployment provider layout
dredozubov Jun 30, 2026
698276a
fix(daemon): redact deployment stream errors
dredozubov Jun 30, 2026
adb8892
fix(web): tighten deployment provider layout
dredozubov Jul 2, 2026
6fe51b6
fix(web): stack onboarding runtime alternatives
dredozubov Jul 3, 2026
3036834
fix: require deployment run session context
dredozubov Jul 3, 2026
211443a
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 3, 2026
e0199af
fix: synthesize deployment run session context
dredozubov Jul 3, 2026
7ef1f67
Merge origin/main into provider orchestrator runtime
dredozubov Jul 5, 2026
df964c6
Merge origin/main into provider orchestrator runtime
dredozubov Jul 6, 2026
2582ee1
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 6, 2026
301d78b
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 7, 2026
21fb35d
test(web): stabilize onboarding localStorage setup
dredozubov Jul 7, 2026
dde045b
Merge remote-tracking branch 'origin/main' into maintain-dredozubov-4…
dredozubov Jul 7, 2026
7e0d830
Merge remote-tracking branch 'origin/main' into maintain-dredozubov-4…
dredozubov Jul 7, 2026
c7bb4f0
Merge origin/main into provider-orchestrator-runtime
dredozubov Jul 9, 2026
dc5b7a5
test(web): read deployment model option labels
dredozubov Jul 9, 2026
6b63b87
fix: close deployment provider review follow-ups
dredozubov Jul 9, 2026
70a507d
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 9, 2026
8626021
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 10, 2026
d02f543
Merge remote-tracking branch 'origin/main' into HEAD
dredozubov Jul 10, 2026
3ede5f3
fix(daemon): launch Kimi connection tests with stream json
dredozubov Jul 10, 2026
b97dc14
test(daemon): pin Kimi direct MCP behavior
dredozubov Jul 10, 2026
24d1c99
Merge main into provider orchestrator runtime
dredozubov Jul 13, 2026
133e945
fix(web): block onboarding after deployment config save failure
dredozubov Jul 13, 2026
068a697
Merge main into provider-orchestrator-runtime
dredozubov Jul 18, 2026
1586e09
Merge main into provider-orchestrator-runtime
dredozubov Jul 20, 2026
b4712fa
Fix deployment connection metadata for OpenAI responses
dredozubov Jul 20, 2026
b547e70
Merge main into provider-orchestrator-runtime
dredozubov Jul 21, 2026
bbb463c
Keep inline switcher open without layout
dredozubov Jul 21, 2026
e20092e
Stabilize question form readiness test
dredozubov Jul 21, 2026
afedb3a
Keep topbar model switcher open
dredozubov Jul 21, 2026
5c3d770
Merge main into provider-orchestrator-runtime
dredozubov Jul 22, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@ Inside a project's Studio, the same design system streams out multiple artifact
<img src="https://repo-assets.open-design.ai/resources/images/coding-agents.png" alt="The 25 coding-agent CLIs Open Design supports — Claude Code · Codex · OpenCode · Hermes · Antigravity · Vela · Grok Build · Kimi · Cursor Agent · Qwen · Qoder · GitHub Copilot · Pi · Kiro · Kilo · Mistral Vibe · DeepSeek · Reasonix · Aider · Amp · CodeBuddy · Mimo · AtomCode · Devin · Trae" width="100%" />
</p>

**No CLI installed?** The BYOK proxy at `POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream` gives you the same loop (no process spawn) — paste `baseUrl` + `apiKey` + `model`, with presets for OpenAI, Atlas Cloud, Anthropic, Azure OpenAI, Google Gemini, Ollama, LM Studio, vLLM, or any OpenAI-compatible endpoint. Atlas Cloud uses `https://api.atlascloud.ai/v1` with your own key and OpenAI-compatible model ids such as `qwen/qwen3.5-flash`. Per-target SSRF protection blocks internal IPs / link-local / CGNAT at the daemon edge.
**No CLI installed?** The BYOK proxy at `POST /api/proxy/{anthropic,openai,azure,google,ollama,senseaudio}/stream` gives you the same loop (no process spawn) — paste `baseUrl` + `apiKey` + `model`, with presets for OpenAI, Atlas Cloud, Anthropic, Azure OpenAI, Google Gemini, Ollama, LM Studio, vLLM, or any OpenAI-compatible endpoint. Atlas Cloud uses `https://api.atlascloud.ai/v1` with your own key and OpenAI-compatible model ids such as `qwen/qwen3.5-flash`. Per-target SSRF protection blocks internal IPs / link-local / CGNAT at the daemon edge. Self-host administrators can also preconfigure an OpenAI-compatible provider orchestrator for deployment-owned credentials; see [`deploy/README.md`](deploy/README.md#deployment-provider-gateway).

Runtime definitions live in [`apps/daemon/src/runtimes/defs/`](apps/daemon/src/runtimes/defs/), with registration and shared stream handling under [`apps/daemon/src/runtimes/`](apps/daemon/src/runtimes/). See [`docs/agent-adapters.md`](docs/agent-adapters.md) for the adapter contract.

Expand Down
22 changes: 22 additions & 0 deletions apps/daemon/src/api-token-auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,25 @@ export function apiTokenFromEnv(env: NodeJS.ProcessEnv = process.env): string {
export function isApiTokenMiddlewareEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
return apiTokenFromEnv(env).length > 0 && !isApiAuthDisabled(env);
}

const apiTokenOpenProbePaths = new Set([
'/health',
'/api/health',
'/ready',
'/api/ready',
'/version',
'/api/version',
]);

const apiTokenOpenGetPaths = new Set([
// Redacted admin-managed provider discovery. The route exposes availability
// and display metadata only; credentials remain daemon-side.
'/provider-orchestrator/config',
'/api/provider-orchestrator/config',
]);

export function isApiTokenExemptRequest(method: string, path: string): boolean {
if (apiTokenOpenProbePaths.has(path)) return true;
if (method.toUpperCase() === 'GET' && apiTokenOpenGetPaths.has(path)) return true;
return false;
}
79 changes: 79 additions & 0 deletions apps/daemon/src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,14 @@ const RESEARCH_SEARCH_BOOLEAN_FLAGS = new Set([
'help',
'h',
]);
const PROVIDER_STRING_FLAGS = new Set([
'daemon-url',
]);
const PROVIDER_BOOLEAN_FLAGS = new Set([
'help',
'h',
'json',
]);

const PLUGIN_STRING_FLAGS = new Set([
'daemon-url',
Expand Down Expand Up @@ -324,6 +332,7 @@ const PLUGIN_LIST_BOOLEAN_FLAGS = new Set([
const SUBCOMMAND_MAP = {
artifacts: runArtifacts,
media: runMedia,
provider: runProvider,
mcp: runMcp,
amr: runAmr,
research: runResearch,
Expand Down Expand Up @@ -641,6 +650,9 @@ function printRootHelp() {
od research search --query <text> [--max-sources 5] [--daemon-url <url>]
Run agent-callable Tavily research through the local daemon.

od provider config [--json] [--daemon-url <url>]
Inspect daemon-managed provider availability without printing credentials.

od plugin <list|info|install|uninstall|apply|doctor|replay|trust> [args]
Discover, install, and apply plugins through the local daemon.
od plugin publish-repo <folder>
Expand Down Expand Up @@ -709,6 +721,73 @@ What the daemon does:
dispatcher that the agent calls via \`od media generate\`.`);
}

// ---------------------------------------------------------------------------
// Subcommand: od provider …
// ---------------------------------------------------------------------------

async function runProvider(args) {
const sub = args.find((a) => !a.startsWith('-')) || '';
if (!sub || sub === 'help' || args.includes('--help') || args.includes('-h')) {
printProviderHelp();
process.exit(sub === 'help' || args.includes('--help') || args.includes('-h') ? 0 : 2);
}
if (sub !== 'config') {
console.error(`unknown subcommand: od provider ${sub}`);
printProviderHelp();
process.exit(2);
}
const idx = args.indexOf(sub);
const rest = [...args.slice(0, idx), ...args.slice(idx + 1)];
let flags;
try {
flags = parseFlags(rest, {
string: PROVIDER_STRING_FLAGS,
boolean: PROVIDER_BOOLEAN_FLAGS,
});
} catch (err) {
console.error(err.message);
printProviderHelp();
process.exit(2);
}
const base = await cliDaemonBaseUrl(flags);
let resp;
try {
resp = await fetch(`${base}/api/provider-orchestrator/config`);
} catch (err) {
return exitWithStructuredError({
code: 'daemon-not-running',
message: `Cannot reach daemon at ${base}: ${err?.message ?? err}`,
});
}
if (!resp.ok) return structuredHttpFailure(resp);
const data = await resp.json();
if (flags.json) {
process.stdout.write(JSON.stringify(data, null, 2) + '\n');
return;
}
console.log(`[provider] ${data.available ? 'available' : 'unavailable'} (${data.kind ?? 'unknown'})`);
console.log(` source: ${data.credentialSource ?? 'deployment'}`);
console.log(` protocol: ${data.protocol ?? 'openai'}`);
console.log(` label: ${data.label ?? 'Provider orchestrator'}`);
if (data.displayHost) console.log(` host: ${data.displayHost}`);
if (data.defaultModel) console.log(` model: ${data.defaultModel}`);
if (data.detail) console.log(` detail: ${data.detail}`);
}

function printProviderHelp() {
console.log(`Usage:
od provider config [--json] [--daemon-url <url>]

Inspects daemon-managed provider configuration. This mirrors the deployment
provider status endpoint: it reports whether a deployment provider is available,
which protocol it fronts, its display label/host, and the default model.
Provider credentials are never printed.

Common options:
--daemon-url <url> Open Design daemon HTTP base.
--json Emit the raw redacted JSON response.`);
}

// ---------------------------------------------------------------------------
// Subcommand: od amr …
// ---------------------------------------------------------------------------
Expand Down
58 changes: 50 additions & 8 deletions apps/daemon/src/connectionTest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,9 @@ export { validateBaseUrl } from '@open-design/contracts/api/connectionTest';

export type DnsLookupAddress = { address: string; family: number };
export type DnsLookupFn = (hostname: string) => Promise<DnsLookupAddress[]>;
export interface ResolvedBaseUrlValidationOptions extends ValidateBaseUrlOptions {
allowPrivateNetwork?: boolean;
}

const defaultDnsLookup: DnsLookupFn = async (hostname) => {
const result = await dnsPromises.lookup(hostname, { all: true, family: 0 });
Expand All @@ -131,13 +134,29 @@ function looksLikeIpLiteral(hostname: string): boolean {
return host.includes(':');
}

function validateConfiguredBaseUrl(baseUrl: string): BaseUrlValidationResult {
let parsed: ParsedBaseUrl;
try {
parsed = new URL(String(baseUrl).replace(/\/+$/, ''));
} catch {
return { error: 'Invalid baseUrl' };
}
if (!['http:', 'https:'].includes(parsed.protocol)) {
return { error: 'Only http/https allowed' };
}
return { parsed };
}

export async function validateBaseUrlResolved(
baseUrl: string,
lookup: DnsLookupFn = defaultDnsLookup,
options: ValidateBaseUrlOptions = {},
options: ResolvedBaseUrlValidationOptions = {},
): Promise<BaseUrlValidationResult> {
const sync = validateBaseUrl(baseUrl, options);
const sync = options.allowPrivateNetwork
? validateConfiguredBaseUrl(baseUrl)
: validateBaseUrl(baseUrl, options);
if (sync.error || !sync.parsed) return sync;
if (options.allowPrivateNetwork) return sync;

const hostname = sync.parsed.hostname.toLowerCase();
if (isLoopbackApiHost(hostname)) return sync;
Expand Down Expand Up @@ -729,7 +748,13 @@ export function redactSecrets(
return redacted;
}

type ProviderConnectionInput = ProviderTestRequest & { signal?: AbortSignal };
type ProviderConnectionInput = Omit<ProviderTestRequest, 'baseUrl' | 'apiKey'> & {
baseUrl: string;
apiKey: string;
metadata?: Record<string, unknown>;
signal?: AbortSignal;
allowPrivateNetworkBaseUrl?: boolean;
};
type AgentConnectionInput = AgentTestRequest & { signal?: AbortSignal };

function appendVersionedApiPath(baseUrl: string, suffix: string): string {
Expand Down Expand Up @@ -1146,8 +1171,15 @@ interface ProviderCallShape {
retryBodyOnUnsupportedMaxTokens?: unknown;
}

function metadataField(value: unknown): { metadata?: Record<string, unknown> } {
if (!value || typeof value !== 'object' || Array.isArray(value)) return {};
return { metadata: value as Record<string, unknown> };
}

export function resolveOpenAIConnectionTestRunProviderPackage(
input: Pick<ProviderTestRequest, 'protocol' | 'baseUrl' | 'apiKey' | 'apiVersion' | 'model'>,
input: Pick<ProviderTestRequest, 'protocol' | 'baseUrl' | 'apiKey' | 'model'> & {
apiVersion?: ProviderTestRequest['apiVersion'];
},
): string | null {
if (input.protocol !== 'openai') return null;
const providerConfig = {
Expand All @@ -1172,6 +1204,7 @@ function openAIChatCompletionsProviderCall(
baseUrl: string,
apiKey: string,
model: string,
metadata?: unknown,
): ProviderCallShape {
return {
url: appendVersionedApiPath(baseUrl, '/chat/completions'),
Expand All @@ -1185,6 +1218,7 @@ function openAIChatCompletionsProviderCall(
},
body: {
model,
...metadataField(metadata),
...buildOpenAIChatTokenParam(model, PROVIDER_MAX_TOKENS),
messages: [{ role: 'user', content: SMOKE_PROMPT }],
stream: false,
Expand All @@ -1197,6 +1231,7 @@ function openAIResponsesProviderCall(
baseUrl: string,
apiKey: string,
model: string,
metadata?: unknown,
): ProviderCallShape {
return {
url: appendVersionedApiPath(baseUrl, '/responses'),
Expand All @@ -1206,6 +1241,7 @@ function openAIResponsesProviderCall(
},
body: {
model,
...metadataField(metadata),
input: SMOKE_PROMPT,
max_output_tokens: PROVIDER_MAX_TOKENS,
},
Expand Down Expand Up @@ -1260,6 +1296,7 @@ function buildProviderCall(input: ProviderTestRequest): ProviderCallShape {
},
body: {
model,
...metadataField(input.metadata),
...buildOpenAIChatTokenParam(model, PROVIDER_MAX_TOKENS),
messages: [{ role: 'user', content: SMOKE_PROMPT }],
stream: false,
Expand All @@ -1276,13 +1313,13 @@ function buildProviderCall(input: ProviderTestRequest): ProviderCallShape {
if (input.protocol === 'openai') {
const runProviderPackage = resolveOpenAIConnectionTestRunProviderPackage(input);
if (runProviderPackage === '@ai-sdk/openai') {
return openAIResponsesProviderCall(baseUrl, apiKey, model);
return openAIResponsesProviderCall(baseUrl, apiKey, model, input.metadata);
}
if (runProviderPackage === '@ai-sdk/openai-compatible') {
return openAIChatCompletionsProviderCall(baseUrl, apiKey, model);
return openAIChatCompletionsProviderCall(baseUrl, apiKey, model, input.metadata);
}
}
return openAIChatCompletionsProviderCall(baseUrl, apiKey, model);
return openAIChatCompletionsProviderCall(baseUrl, apiKey, model, input.metadata);
case 'azure': {
const url = new URL(baseUrl);
const basePath = url.pathname.replace(/\/+$/, '');
Expand Down Expand Up @@ -1418,7 +1455,12 @@ export async function testProviderConnection(
const start = Date.now();
const model = String(input.model ?? '');
const normalizedInput = normalizeProviderTestInput(input);
const validated = await validateUserProviderBaseUrl(normalizedInput.baseUrl);
const validated =
input.allowPrivateNetworkBaseUrl === true
? await validateBaseUrlResolved(normalizedInput.baseUrl, undefined, {
allowPrivateNetwork: true,
})
: await validateUserProviderBaseUrl(normalizedInput.baseUrl);
if (validated.error || !validated.parsed) {
const kind: ConnectionTestKind = validated.forbidden ? 'forbidden' : 'invalid_base_url';
return {
Expand Down
Loading
Loading