Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
636c597
feat(desktop): support managed bootstrap state
pascalandr May 22, 2026
af01579
fix(desktop): update bootstrap dependency lockfile
pascalandr May 22, 2026
7238bb5
chore: resolve pr 1891 conflicts
pascalandr May 28, 2026
4112a09
fix: TASK-2026-05-26-024 persist Den remote workspace metadata
pascalandr May 28, 2026
0a9862b
fix: TASK-2026-05-28-002 address browser MCP review
pascalandr May 28, 2026
1db5746
fix(desktop): support Den cloud handoff on custom hosts
pascalandr Jun 4, 2026
da26fff
Merge remote-tracking branch 'upstream/dev' into pr/managed-desktop-b…
pascalandr Jun 4, 2026
33a1f4f
fix: TASK-2026-06-05-001 resolve managed desktop PR review
pascalandr Jun 5, 2026
09c4461
fix: TASK-2026-06-05-001 remove duplicate bootstrap normalizer
pascalandr Jun 5, 2026
fe3229d
Merge remote-tracking branch 'upstream/dev' into pr/managed-desktop-b…
pascalandr Jun 9, 2026
370948e
fix(desktop): connect cloud onboarding to remote workers
pascalandr Jun 9, 2026
aea30ac
fix: TASK-2026-06-09-002 scope host-token bootstrap access
pascalandr Jun 9, 2026
5514598
test: TASK-2026-06-09-002 cover browser page callbacks
pascalandr Jun 9, 2026
f5bde5d
test: TASK-2026-06-09-002 typecheck browser callback test
pascalandr Jun 9, 2026
efcb923
fix: TASK-2026-06-09-002 keep host token off workspace discovery
pascalandr Jun 9, 2026
463df77
fix: TASK-2026-06-10-004 resolve PR 1891 dev conflicts
pascalandr Jun 10, 2026
9eaa69e
test: TASK-2026-06-10-006 tolerate fetch response headers
pascalandr Jun 10, 2026
413cb0d
fix: TASK-2026-06-10-008 clean managed bootstrap workspace discovery
pascalandr Jun 10, 2026
576cf60
fix: TASK-2026-06-10-008 filter managed Den runtime workspaces
pascalandr Jun 10, 2026
871e0b3
fix: TASK-2026-06-10-008 preserve remote workspace secrets
pascalandr Jun 10, 2026
2b607e4
fix: TASK-2026-06-10-009 preserve remote Den metadata
pascalandr Jun 10, 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
63 changes: 60 additions & 3 deletions apps/app/src/app/lib/den.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,15 @@ 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 DenMcpToken = {
token: string;
expiresAt: string;
Expand All @@ -132,14 +141,18 @@ export type DenOrgLlmProvider = {
providerId: string;
name: string;
providerConfig: Record<string, unknown>;
credentialKind: "api_key" | "opencode_oauth";
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 DenPluginConfigObjectType = "skill" | "agent" | "command" | "tool" | "mcp" | "hook" | "context" | "custom";
Expand Down Expand Up @@ -590,8 +603,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 @@ -649,9 +674,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 @@ -1113,7 +1140,10 @@ function parseDenOrgLlmProvider(value: unknown): DenOrgLlmProvider | null {
providerId: value.providerId,
name: value.name,
providerConfig: parseJsonRecord(value.providerConfig),
credentialKind: value.credentialKind === "opencode_oauth" ? "opencode_oauth" : "api_key",
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 @@ -1149,6 +1179,7 @@ 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,
};
}

Expand Down Expand Up @@ -1914,6 +1945,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 @@ -1987,7 +2044,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 Down
4 changes: 4 additions & 0 deletions apps/app/src/app/lib/desktop-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,10 @@ export type WorkspaceInfo = {
openworkHostToken?: string | null;
openworkWorkspaceId?: string | null;
openworkWorkspaceName?: string | null;
openworkDenBaseUrl?: string | null;
openworkDenApiBaseUrl?: string | null;
openworkDenOrgId?: string | null;
openworkDenWorkerId?: string | null;
sandboxBackend?: "docker" | "microsandbox" | null;
sandboxRunId?: string | null;
sandboxContainerName?: string | null;
Expand Down
19 changes: 19 additions & 0 deletions apps/app/src/app/lib/openwork-links.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import { normalizeOpenworkServerUrl } from "./openwork-server";
export type RemoteWorkspaceDefaults = {
openworkHostUrl?: string | null;
openworkToken?: string | null;
openworkClientToken?: string | null;
openworkHostToken?: string | null;
openworkDenBaseUrl?: string | null;
openworkDenApiBaseUrl?: string | null;
openworkDenOrgId?: string | null;
openworkDenWorkerId?: string | null;
directory?: string | null;
displayName?: string | null;
autoConnect?: boolean;
Expand Down Expand Up @@ -44,6 +50,7 @@ export function parseRemoteConnectDeepLink(rawUrl: string): RemoteWorkspaceDefau
const tokenRaw = url.searchParams.get("openworkToken") ?? url.searchParams.get("accessToken") ?? "";
const normalizedHostUrl = normalizeOpenworkServerUrl(hostUrlRaw);
const token = tokenRaw.trim();
const clientToken = url.searchParams.get("openworkClientToken")?.trim() || token;
if (!normalizedHostUrl || !token) {
return null;
}
Expand All @@ -61,6 +68,12 @@ export function parseRemoteConnectDeepLink(rawUrl: string): RemoteWorkspaceDefau
return {
openworkHostUrl: normalizedHostUrl,
openworkToken: token,
openworkClientToken: clientToken || null,
openworkHostToken: url.searchParams.get("openworkHostToken")?.trim() || null,
openworkDenBaseUrl: url.searchParams.get("openworkDenBaseUrl")?.trim() || null,
openworkDenApiBaseUrl: url.searchParams.get("openworkDenApiBaseUrl")?.trim() || null,
openworkDenOrgId: url.searchParams.get("openworkDenOrgId")?.trim() || null,
openworkDenWorkerId: url.searchParams.get("openworkDenWorkerId")?.trim() || workerId || null,
directory: null,
displayName: displayName || null,
autoConnect,
Expand All @@ -80,6 +93,12 @@ export function stripRemoteConnectQuery(rawUrl: string): string | null {
"openworkHostUrl",
"openworkUrl",
"openworkToken",
"openworkClientToken",
"openworkHostToken",
"openworkDenBaseUrl",
"openworkDenApiBaseUrl",
"openworkDenOrgId",
"openworkDenWorkerId",
"accessToken",
"workerId",
"workerName",
Expand Down
26 changes: 24 additions & 2 deletions apps/app/src/app/lib/openwork-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -532,6 +532,28 @@ export function parseOpenworkWorkspaceIdFromUrl(input: string) {
}
}

export function stripOpenworkWorkspaceMount(input: string) {
const normalized = normalizeOpenworkServerUrl(input) ?? "";
if (!normalized) return "";

try {
const url = new URL(normalized);
const segments = url.pathname.split("/").filter(Boolean);
const workspaceIndex = segments.indexOf("workspace");
const legacyIndex = segments.indexOf("w");
const mountIndex = workspaceIndex >= 0 ? workspaceIndex : legacyIndex;
if (mountIndex >= 0 && segments[mountIndex + 1]) {
const prefix = segments.slice(0, mountIndex).join("/");
url.pathname = prefix ? `/${prefix}` : "/";
return url.toString().replace(/\/+$/, "");
}
} catch {
// Fall through to the normalized value below.
}

return normalized.replace(/\/+$/, "");
}

export function buildOpenworkWorkspaceBaseUrl(hostUrl: string, workspaceId?: string | null) {
const normalized = normalizeOpenworkServerUrl(hostUrl) ?? "";
if (!normalized) return null;
Expand Down Expand Up @@ -650,7 +672,7 @@ export function stripOpenworkConnectInviteFromUrl(input: string) {
export function readOpenworkServerSettings(): OpenworkServerSettings {
if (typeof window === "undefined") return {};
try {
const urlOverride = normalizeOpenworkServerUrl(
const urlOverride = stripOpenworkWorkspaceMount(
window.localStorage.getItem(STORAGE_URL_OVERRIDE) ?? "",
);
const portRaw = window.localStorage.getItem(STORAGE_PORT_OVERRIDE) ?? "";
Expand All @@ -659,7 +681,7 @@ export function readOpenworkServerSettings(): OpenworkServerSettings {
const hostToken = window.localStorage.getItem(STORAGE_HOST_AUTH_KEY) ?? undefined;
const remoteAccessRaw = window.localStorage.getItem(STORAGE_REMOTE_ACCESS) ?? "";
return {
urlOverride: urlOverride ?? undefined,
urlOverride: urlOverride || undefined,
portOverride: Number.isNaN(portOverride) ? undefined : portOverride,
token: token?.trim() || undefined,
hostToken: hostToken?.trim() || undefined,
Expand Down
13 changes: 12 additions & 1 deletion apps/app/src/app/lib/workspace-endpoint.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export type ResolvedWorkspaceEndpoint = {
baseUrl: string;
/** Auth token for that server. May be empty for unauthenticated local servers. */
token: string;
/** Host/admin token for routes that require worker mutation privileges. */
hostToken: string;
/** Workspace id as the owning server expects it in URL paths. No `rem_` prefix. */
workspaceId: string;
/** True when the workspace lives on a remote OpenWork worker, not the user's local server. */
Expand Down Expand Up @@ -93,13 +95,18 @@ function pickRemoteBaseUrl(workspace: WorkspaceEndpointInput): string {
function pickRemoteToken(workspace: WorkspaceEndpointInput): string {
if (!workspace) return "";
return (
workspace.openworkToken ??
workspace.openworkClientToken ??
workspace.openworkToken ??
workspace.openworkHostToken ??
""
).trim();
}

function pickRemoteHostToken(workspace: WorkspaceEndpointInput): string {
if (!workspace) return "";
return (workspace.openworkHostToken ?? "").trim();
}

/**
* Resolve the right server endpoint for a workspace. Returns null when the
* workspace can't be reached (remote with no baseUrl, or local with no local
Expand All @@ -116,17 +123,20 @@ export function resolveWorkspaceEndpoint(
const baseUrl = pickRemoteBaseUrl(workspace);
if (!baseUrl) return null;
const token = pickRemoteToken(workspace);
const hostToken = pickRemoteHostToken(workspace);
const workspaceId = workspaceServerId(workspace);
const client = createOpenworkServerClient({
baseUrl,
token: token || undefined,
hostToken: hostToken || undefined,
});
const mountedBaseUrl = (
buildOpenworkWorkspaceBaseUrl(baseUrl, workspaceId) ?? baseUrl
).replace(/\/+$/, "");
return {
baseUrl,
token,
hostToken,
workspaceId,
isRemote: true,
client,
Expand All @@ -149,6 +159,7 @@ export function resolveWorkspaceEndpoint(
return {
baseUrl: localBaseUrl,
token: localToken,
hostToken: "",
workspaceId,
isRemote: false,
client,
Expand Down
Loading