Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
3bb9c54
feat(app): support managed cloud provider bootstrap
pascalandr Jun 10, 2026
68422bd
fix(cloud): redact managed worker tokens
pascalandr Jun 11, 2026
19157ef
fix(cloud): restrict worker token handoff
pascalandr Jun 11, 2026
5e1d204
fix(cloud): avoid exposing managed worker tokens
pascalandr Jun 11, 2026
0978662
fix(cloud): tighten managed credential access
pascalandr Jun 11, 2026
d629baa
fix(cloud): require worker ownership for mutations
pascalandr Jun 11, 2026
103dbe3
fix(cloud): hide provider credential actions for members
pascalandr Jun 11, 2026
ac20f6f
fix(cloud): harden managed provider auth gates
pascalandr Jun 11, 2026
0c2cacf
fix(cloud): isolate managed provider sync state
pascalandr Jun 11, 2026
1551035
fix(cloud): preserve remote workspace settings credentials
pascalandr Jun 11, 2026
cd10d7b
fix(cloud): sync providers by worker owner access
pascalandr Jun 11, 2026
340e40d
fix(cloud): revoke managed provider aliases
pascalandr Jun 11, 2026
4878e97
fix(integration): handle remote connect links safely
pascalandr Jun 11, 2026
90e4ecf
fix(integration): close remote token edge cases
pascalandr Jun 11, 2026
e2381df
fix(integration): require client tokens for remote connects
pascalandr Jun 11, 2026
0288d8f
fix(integration): prefer client tokens in remote links
pascalandr Jun 11, 2026
7684441
fix(integration): scope browser proxy auth
pascalandr Jun 11, 2026
d62152b
fix(app): remove duplicate Den auth import
pascalandr Jun 11, 2026
8287611
fix(cloud): address managed provider review findings
pascalandr Jun 11, 2026
31b350a
fix(app): strip workspace mounts with trailing paths
pascalandr Jun 11, 2026
4f31bdc
fix(cloud): finish managed provider 0.16.2 validation
pascalandr Jun 12, 2026
c8c4da7
fix(cloud): repair standalone worker ownership guards
pascalandr Jun 12, 2026
0328e38
Merge remote-tracking branch 'upstream/dev' into work/resolve-2175
pascalandr Jun 14, 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
34 changes: 34 additions & 0 deletions apps/app/scripts/workspace-endpoint.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import { describe, expect, test } from "bun:test";
import { resolveWorkspaceEndpoint } from "../src/app/lib/workspace-endpoint";

describe("resolveWorkspaceEndpoint", () => {
test("does not use remote host token as bearer authorization", () => {
const endpoint = resolveWorkspaceEndpoint({
id: "rem_ws_123",
workspaceType: "remote",
baseUrl: "https://worker.example.test",
openworkHostUrl: "https://worker.example.test",
openworkToken: null,
openworkClientToken: null,
openworkHostToken: "host-token-must-not-be-bearer",
openworkWorkspaceId: "ws_123",
} as never, { baseUrl: "http://127.0.0.1:8791", token: "local-token" });

expect(endpoint?.token).toBe("");
});

test("uses remote client token before local server token", () => {
const endpoint = resolveWorkspaceEndpoint({
id: "rem_ws_123",
workspaceType: "remote",
baseUrl: "https://worker.example.test",
openworkHostUrl: "https://worker.example.test",
openworkToken: null,
openworkClientToken: "remote-client-token",
openworkHostToken: "host-token",
openworkWorkspaceId: "ws_123",
} as never, { baseUrl: "http://127.0.0.1:8791", token: "local-token" });

expect(endpoint?.token).toBe("remote-client-token");
});
});
63 changes: 63 additions & 0 deletions apps/app/src/app/cloud/managed-provider-models.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
import type { CloudImportedProvider } from "./import-state";
import type { ModelOption, ProviderListItem } from "../types";

export function buildCloudManagedModelIdsByProvider(
importedCloudProviders: Record<string, CloudImportedProvider> | null | undefined,
): Map<string, Set<string>> {
const next = new Map<string, Set<string>>();
for (const imported of Object.values(importedCloudProviders ?? {})) {
const providerId = imported.providerId.trim();
if (!providerId) continue;
const modelIds = imported.modelIds.map((id) => id.trim()).filter(Boolean);
if (!modelIds.length) continue;
const merged = next.get(providerId) ?? new Set<string>();
for (const modelId of modelIds) merged.add(modelId);
next.set(providerId, merged);
}
return next;
}

export function isCloudManagedModelAllowed(
cloudManagedModelIdsByProvider: Map<string, Set<string>>,
providerId: string,
modelId: string,
) {
const allowedModelIds = cloudManagedModelIdsByProvider.get(providerId);
return !allowedModelIds || allowedModelIds.has(modelId);
}

export function hasCloudManagedModelAllowlist(
cloudManagedModelIdsByProvider: Map<string, Set<string>>,
providerId: string,
) {
return cloudManagedModelIdsByProvider.has(providerId);
}

export function buildCloudManagedModelOptions(input: {
providers: ProviderListItem[];
cloudManagedModelIdsByProvider: Map<string, Set<string>>;
isRecommendedProvider?: (providerId: string) => boolean;
}): ModelOption[] {
const options: ModelOption[] = [];
for (const provider of input.providers) {
const isCloudManaged = hasCloudManagedModelAllowlist(input.cloudManagedModelIdsByProvider, provider.id);
for (const [modelId, model] of Object.entries(provider.models)) {
if (!isCloudManagedModelAllowed(input.cloudManagedModelIdsByProvider, provider.id, modelId)) continue;
options.push({
providerID: provider.id,
modelID: modelId,
title: model.name || modelId,
description: provider.name,
behaviorTitle: "Reasoning",
behaviorLabel: "Default",
behaviorDescription: "",
behaviorValue: null,
isFree: false,
isConnected: true,
isRecommended: input.isRecommendedProvider?.(provider.id),
source: isCloudManaged || /^lpr_/i.test(provider.id) ? "cloud" : undefined,
});
}
}
return options;
}
152 changes: 148 additions & 4 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,20 @@ export type DenWorkerTokens = {
workspaceId: string | null;
};

export type DenStaticWorkerAttachInput = {
name: string;
description?: string | null;
url: string;
clientToken: string;
hostToken: string;
activityToken?: string | null;
};

export type DenWorkerLaunchInput = {
name: string;
source?: "manual" | "signup_auto";
};

export type DenMcpToken = {
token: string;
expiresAt: string;
Expand All @@ -134,17 +148,29 @@ export type DenOrgLlmProviderModel = {
export type DenOrgLlmProvider = {
id: string;
source: "models_dev" | "custom" | "openwork";
credentialKind: "api_key" | "opencode_oauth";
providerId: string;
name: string;
providerConfig: Record<string, unknown>;
hasApiKey: boolean;
hasOpencodeAuth: boolean;
hasCredential: boolean;
models: DenOrgLlmProviderModel[];
createdAt: string | null;
updatedAt: string | null;
};

export type DenOrgLlmProviderConnection = DenOrgLlmProvider & {
apiKey: string | null;
opencodeAuth: string | null;
};

export type DenManagedProviderSyncResult = {
status: "applied" | "failed";
providerCount: number;
revision: string;
providerIds?: string[];
reason?: string;
};

export type DenOrgMarketplaceResolved = {
Expand Down Expand Up @@ -588,8 +614,20 @@ function syncBootstrapSettingsToLocalStorage(config: DenBootstrapConfig) {
return;
}

const previousBaseUrl = window.localStorage.getItem(STORAGE_BASE_URL);
const previousOrigin = normalizeDenBaseUrl(previousBaseUrl) ?? "";
const nextOrigin = normalizeDenBaseUrl(config.baseUrl) ?? "";
const denOriginChanged = Boolean(previousOrigin && nextOrigin && previousOrigin !== nextOrigin);

window.localStorage.setItem(STORAGE_BASE_URL, config.baseUrl);
window.localStorage.setItem(STORAGE_API_BASE_URL, config.apiBaseUrl);

if (denOriginChanged) {
window.localStorage.removeItem(STORAGE_AUTH_TOKEN);
window.localStorage.removeItem(STORAGE_ACTIVE_ORG_ID);
window.localStorage.removeItem(STORAGE_ACTIVE_ORG_SLUG);
window.localStorage.removeItem(STORAGE_ACTIVE_ORG_NAME);
}
}

function getPendingBootstrapConfig(next: DenSettings): DenBootstrapConfig | null {
Expand Down Expand Up @@ -647,9 +685,11 @@ export async function initializeDenBootstrapConfig(): Promise<DenBootstrapConfig
// boot are more trustworthy than build defaults, and clobbering them
// silently reverted custom/self-hosted control planes to the production
// URL until a manual reload.
const storedBaseUrl = typeof window === "undefined" ? null : window.localStorage.getItem(STORAGE_BASE_URL);
const storedApiBaseUrl = typeof window === "undefined" ? null : window.localStorage.getItem(STORAGE_API_BASE_URL);
desktopBootstrapConfig = resolveDenBootstrapConfig({
baseUrl: BUILD_DEN_BASE_URL,
apiBaseUrl: BUILD_DEN_API_BASE_URL,
baseUrl: storedBaseUrl ?? BUILD_DEN_BASE_URL,
apiBaseUrl: storedApiBaseUrl ?? BUILD_DEN_API_BASE_URL,
requireSignin: BUILD_DEN_REQUIRE_SIGNIN,
});

Expand Down Expand Up @@ -1081,10 +1121,13 @@ function parseDenOrgLlmProvider(value: unknown): DenOrgLlmProvider | null {
return {
id: value.id,
source: value.source,
credentialKind: value.credentialKind === "opencode_oauth" ? "opencode_oauth" : "api_key",
providerId: value.providerId,
name: value.name,
providerConfig: parseJsonRecord(value.providerConfig),
hasApiKey: value.hasApiKey === true,
hasOpencodeAuth: value.hasOpencodeAuth === true,
hasCredential: value.hasCredential === true || value.hasApiKey === true || value.hasOpencodeAuth === true,
models: Array.isArray(value.models)
? value.models.flatMap((model) => {
const parsed = parseDenOrgLlmProviderModel(model);
Expand Down Expand Up @@ -1120,6 +1163,28 @@ function getDenOrgLlmProviderConnection(payload: unknown): DenOrgLlmProviderConn
return {
...provider,
apiKey: typeof payload.llmProvider.apiKey === "string" ? payload.llmProvider.apiKey : null,
opencodeAuth: typeof payload.llmProvider.opencodeAuth === "string" ? payload.llmProvider.opencodeAuth : null,
};
}

function getDenManagedProviderSyncResult(payload: unknown): DenManagedProviderSyncResult | null {
if (!isRecord(payload)) return null;
if (payload.status !== "applied" && payload.status !== "failed") return null;
if (typeof payload.providerCount !== "number" || !Number.isInteger(payload.providerCount) || payload.providerCount < 0) return null;
if (typeof payload.revision !== "string") return null;
const rawProviderIds = Array.isArray(payload.providerIds)
? payload.providerIds
: Array.isArray(payload.appliedProviderIds)
? payload.appliedProviderIds
: undefined;
const providerIds = rawProviderIds ? readStringArray(rawProviderIds) : undefined;
if (rawProviderIds && providerIds?.length !== payload.providerCount) return null;
return {
status: payload.status,
providerCount: payload.providerCount,
revision: payload.revision,
...(providerIds ? { providerIds } : {}),
...(typeof payload.reason === "string" ? { reason: payload.reason } : {}),
};
}

Expand Down Expand Up @@ -1845,6 +1910,31 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
return getWorkers(payload);
},

async createWorker(orgId: string, input: DenWorkerLaunchInput): Promise<DenWorkerSummary> {
const payload = await requestJson<unknown>(baseUrls, "/v1/workers", {
method: "POST",
token,
organizationId: orgId,
body: {
name: input.name,
destination: "cloud",
source: input.source ?? "manual",
},
});
const workers = getWorkers({
workers: isRecord(payload) && isRecord(payload.worker)
? [{ ...payload.worker, instance: isRecord(payload.instance) ? payload.instance : null }]
: isRecord(payload)
? [payload]
: [],
});
const worker = workers[0];
if (!worker) {
throw new DenApiError(500, "invalid_worker_create_payload", "Worker launch response was missing worker details.");
}
return worker;
},

async mintMcpToken(orgId: string): Promise<DenMcpToken> {
const payload = await requestJson<unknown>(baseUrls, "/v1/mcp/token", {
method: "POST",
Expand Down Expand Up @@ -1873,6 +1963,32 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
return tokens;
},

async attachStaticWorker(orgId: string, input: DenStaticWorkerAttachInput): Promise<DenWorkerSummary> {
const payload = await requestJson<unknown>(baseUrls, "/v1/workers/static-attach", {
method: "POST",
token,
organizationId: orgId,
body: {
name: input.name,
description: input.description ?? undefined,
url: input.url,
clientToken: input.clientToken,
hostToken: input.hostToken,
activityToken: input.activityToken ?? undefined,
},
});
const workers = getWorkers({
workers: isRecord(payload) && isRecord(payload.worker)
? [{ ...payload.worker, instance: isRecord(payload.instance) ? payload.instance : null }]
: [],
});
const worker = workers[0];
if (!worker) {
throw new DenApiError(500, "invalid_worker_attach_payload", "Static worker attach response was missing worker details.");
}
return worker;
},

async listOrgSkills(orgId: string): Promise<DenOrgSkillCard[]> {
const payload = await requestJson<unknown>(baseUrls, "/v1/skills", {
method: "GET",
Expand Down Expand Up @@ -1904,7 +2020,7 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
},

async listOrgLlmProviders(orgId: string): Promise<DenOrgLlmProvider[]> {
const payload = await requestJson<unknown>(baseUrls, "/v1/llm-providers", {
const payload = await requestJson<unknown>(baseUrls, "/v1/llm-providers?scope=usable", {
method: "GET",
token,
organizationId: orgId,
Expand All @@ -1915,7 +2031,7 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
async getOrgLlmProviderConnection(orgId: string, llmProviderId: string): Promise<DenOrgLlmProviderConnection> {
const payload = await requestJson<unknown>(
baseUrls,
`/v1/llm-providers/${encodeURIComponent(llmProviderId)}/connect`,
`/v1/llm-providers/${encodeURIComponent(llmProviderId)}/import-credential`,
{
method: "GET",
token,
Expand All @@ -1929,6 +2045,34 @@ export function createDenClient(options: { baseUrl: string; apiBaseUrl?: string
return provider;
},

async syncWorkerManagedProviders(orgId: string, workerId: string): Promise<DenManagedProviderSyncResult> {
const raw = await requestJsonRaw<unknown>(
baseUrls,
`/v1/workers/${encodeURIComponent(workerId)}/managed-providers/sync`,
{
method: "POST",
token,
organizationId: orgId,
body: {},
},
);
if (!raw.ok) {
const reason = isRecord(raw.json) && typeof raw.json.reason === "string" && raw.json.reason.trim()
? raw.json.reason.trim()
: getErrorMessage(raw.json, `Request failed with ${raw.status}.`);
throw new DenApiError(raw.status, "managed_provider_sync_failed", reason);
}

const result = getDenManagedProviderSyncResult(raw.json);
if (!result) {
throw new DenApiError(500, "invalid_managed_provider_sync_payload", "Managed provider sync response was invalid.");
}
if (result.status !== "applied") {
throw new DenApiError(502, "managed_provider_sync_failed", result.reason ?? "Managed provider sync failed.");
}
return result;
},

async listOrgMarketplaces(orgId: string): Promise<DenOrgMarketplace[]> {
const payload = await requestJson<unknown>(
baseUrls,
Expand Down
18 changes: 12 additions & 6 deletions apps/app/src/app/lib/desktop.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,12 @@ function isLoopbackUrl(input: RequestInfo | URL): boolean {
}
}

async function serializeFetchBody(body: RequestInit["body"] | null | undefined): Promise<string | undefined> {
if (body === null || body === undefined) return undefined;
if (typeof body === "string") return body;
return new Response(body).text();
}

export const desktopFetch: typeof globalThis.fetch = async (input, init) => {
if (isLoopbackUrl(input)) {
return globalThis.fetch(input, init);
Expand All @@ -263,8 +269,8 @@ export const desktopFetch: typeof globalThis.fetch = async (input, init) => {
method = init?.method ?? input.method;
const headersSource = init?.headers ? new Headers(init.headers) : input.headers;
headers = Object.fromEntries(headersSource.entries());
if (typeof init?.body === "string") {
body = init.body;
if (init?.body !== undefined) {
body = await serializeFetchBody(init.body);
} else if (input.body) {
// Request body is a stream — buffer to text so it survives the IPC hop
// to the Electron main process.
Expand All @@ -274,7 +280,7 @@ export const desktopFetch: typeof globalThis.fetch = async (input, init) => {
url = typeof input === "string" ? input : input.toString();
method = init?.method;
headers = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : undefined;
body = typeof init?.body === "string" ? init.body : undefined;
body = await serializeFetchBody(init?.body);
}

const result = await invokeElectronHelper("__fetch", url, { method, headers, body });
Expand Down Expand Up @@ -302,16 +308,16 @@ export async function desktopFetchViaMain(input: RequestInfo | URL, init?: Reque
method = init?.method ?? input.method;
const headersSource = init?.headers ? new Headers(init.headers) : input.headers;
headers = Object.fromEntries(headersSource.entries());
if (typeof init?.body === "string") {
body = init.body;
if (init?.body !== undefined) {
body = await serializeFetchBody(init.body);
} else if (input.body) {
body = await input.clone().text();
}
} else {
url = typeof input === "string" ? input : input.toString();
method = init?.method;
headers = init?.headers ? Object.fromEntries(new Headers(init.headers).entries()) : undefined;
body = typeof init?.body === "string" ? init.body : undefined;
body = await serializeFetchBody(init?.body);
}

const result = await invokeElectronHelper("__fetch", url, { method, headers, body, timeoutMs });
Expand Down
Loading