Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
2860334
feat(den): add provider credential contract base
pascalandr May 23, 2026
d28b45f
feat(den): sync managed providers to workers
pascalandr May 22, 2026
f592203
fix(server): restore managed provider auth apply
pascalandr May 23, 2026
542a3dc
fix(den): harden provider sync verification
pascalandr May 22, 2026
9776240
fix(den): treat empty provider sync as applied
pascalandr May 23, 2026
c53a9cf
fix: type worker organization context
pascalandr May 23, 2026
bf09ed3
fix(server): restore managed provider auth apply
pascalandr May 23, 2026
f7ce71c
fix: TASK-2026-05-26-017 sanitize managed provider model config
pascalandr May 27, 2026
2a26ba3
fix: TASK-2026-05-26-020 filter managed OAuth models
pascalandr May 27, 2026
3690f38
fix: TASK-2026-05-26-021 handle live provider shapes
pascalandr May 28, 2026
5f52352
merge: TASK-2026-05-26-023 resolve PR 1939 with upstream dev
pascalandr May 28, 2026
596108c
fix: TASK-2026-05-26-024 surface managed provider connections
pascalandr May 28, 2026
396af65
fix: TASK-2026-05-26-024 apply managed provider auth
pascalandr May 28, 2026
9fba3bf
fix: TASK-2026-05-28-008 address PR 1939 reviews
pascalandr May 28, 2026
08aa051
Merge remote-tracking branch 'upstream/dev' into pr/credential-contra…
pascalandr Jun 4, 2026
c29208e
fix: TASK-2026-06-05-001 restore managed provider config writer
pascalandr Jun 5, 2026
0938ba5
Merge remote-tracking branch 'upstream/dev' into pr/credential-contra…
pascalandr Jun 9, 2026
07474f0
fix: TASK-2026-06-09-002 make managed provider sync authoritative
pascalandr Jun 9, 2026
4a245ad
test: TASK-2026-06-09-002 align provider sync proxy auth
pascalandr Jun 9, 2026
fc10acb
fix: TASK-2026-06-10-005 defer stale auth deletion
pascalandr Jun 10, 2026
79913b5
fix: TASK-2026-06-10-005 retry stale auth cleanup
pascalandr Jun 10, 2026
c4fcfbf
fix: TASK-2026-06-10-008 harden managed provider sync
pascalandr Jun 10, 2026
cb25538
fix: TASK-2026-06-10-008 reject duplicate managed provider runtime ids
pascalandr Jun 10, 2026
9e1ee05
fix: TASK-2026-06-10-008 guard array provider models
pascalandr Jun 10, 2026
eeca524
fix: TASK-2026-06-10-008 filter array provider models
pascalandr Jun 10, 2026
89b3f9d
fix: TASK-2026-06-10-009 harden managed provider sync
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
567 changes: 567 additions & 0 deletions apps/server/src/managed-provider-sync.e2e.test.ts

Large diffs are not rendered by default.

603 changes: 594 additions & 9 deletions apps/server/src/server.ts

Large diffs are not rendered by default.

36 changes: 32 additions & 4 deletions ee/apps/den-api/src/routes/org/llm-providers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -145,6 +145,24 @@ function isOrganizationAdmin(payload: { currentMember: { isOwner: boolean; role:
return payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin")
}

export function getCredentialFlags(provider: Pick<LlmProviderRow, "credentialKind" | "apiKey" | "opencodeAuth">) {
const hasApiKey = Boolean(provider.apiKey && provider.apiKey.trim().length > 0)
const hasOpencodeAuth = Boolean(provider.opencodeAuth && provider.opencodeAuth.trim().length > 0)
return {
hasApiKey,
hasOpencodeAuth,
hasCredential: provider.credentialKind === "opencode_oauth" ? hasOpencodeAuth : hasApiKey,
}
}

export function redactLlmProviderCredentials<T extends { apiKey?: unknown; opencodeAuth?: unknown }>(provider: T): Omit<T, "apiKey" | "opencodeAuth"> & { apiKey: undefined; opencodeAuth: undefined } {
return {
...provider,
apiKey: undefined,
opencodeAuth: undefined,
}
}

function canManageLlmProvider(
payload: { currentMember: { id: MemberId; isOwner: boolean; role: string } },
provider: LlmProviderRow,
Expand Down Expand Up @@ -464,7 +482,7 @@ async function loadLlmProviders(input: {

return providers.map((provider) => ({
...provider,
hasApiKey: Boolean(provider.apiKey && provider.apiKey.trim().length > 0),
...getCredentialFlags(provider),
models: (modelsByProviderId.get(provider.id) ?? [])
.map((model) => ({
id: model.modelId,
Expand Down Expand Up @@ -607,8 +625,7 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari

return c.json({
llmProviders: providers.map((provider) => ({
...provider,
apiKey: undefined,
...redactLlmProviderCredentials(provider),
canManage: canManageLlmProvider(payload, provider),
})),
})
Expand Down Expand Up @@ -678,6 +695,8 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
return c.json({
llmProvider: {
...provider,
opencodeAuth: undefined,
...getCredentialFlags(provider),
models: models
.map((model) => ({
id: model.modelId,
Expand Down Expand Up @@ -781,10 +800,13 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari
organizationId: payload.organization.id,
createdByOrgMembershipId: payload.currentMember.id,
source: normalized.source,
credentialKind: "api_key",
providerId: normalized.providerId,
name: normalized.name,
providerConfig: normalized.providerConfig,
hasApiKey: Boolean(normalized.apiKey),
hasOpencodeAuth: false,
hasCredential: Boolean(normalized.apiKey),
createdAt: now,
updatedAt: now,
},
Expand Down Expand Up @@ -916,12 +938,18 @@ export function registerOrgLlmProviderRoutes<T extends { Variables: OrgRouteVari

return c.json({
llmProvider: {
...provider,
...redactLlmProviderCredentials(provider),
source: normalized.source,
providerId: normalized.providerId,
name: normalized.name,
providerConfig: normalized.providerConfig,
credentialKind: provider.credentialKind,
hasApiKey: input.apiKey === undefined ? Boolean(provider.apiKey) : Boolean(normalized.apiKey),
hasOpencodeAuth: Boolean(provider.opencodeAuth),
hasCredential: getCredentialFlags({
...provider,
apiKey: input.apiKey === undefined ? provider.apiKey : normalized.apiKey,
}).hasCredential,
updatedAt,
},
})
Expand Down
2 changes: 2 additions & 0 deletions ee/apps/den-api/src/routes/workers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,13 @@ import type { WorkerRouteVariables } from "./shared.js"
import { registerWorkerActivityRoutes } from "./activity.js"
import { registerWorkerBillingRoutes } from "./billing.js"
import { registerWorkerCoreRoutes } from "./core.js"
import { registerManagedProviderSyncRoutes } from "./managed-providers.js"
import { registerWorkerRuntimeRoutes } from "./runtime.js"

export function registerWorkerRoutes<T extends { Variables: WorkerRouteVariables }>(app: Hono<T>) {
registerWorkerActivityRoutes(app)
registerWorkerBillingRoutes(app)
registerWorkerCoreRoutes(app)
registerManagedProviderSyncRoutes(app as unknown as Hono<{ Variables: WorkerRouteVariables }>)
registerWorkerRuntimeRoutes(app)
}
213 changes: 213 additions & 0 deletions ee/apps/den-api/src/routes/workers/managed-providers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
import { and, eq, inArray, or } from "@openwork-ee/den-db/drizzle"
import { LlmProviderAccessTable, LlmProviderModelTable, LlmProviderTable } from "@openwork-ee/den-db/schema"
import { normalizeDenTypeId } from "@openwork-ee/utils/typeid"
import type { Hono } from "hono"
import { describeRoute } from "hono-openapi"
import { z } from "zod"
import { db } from "../../db.js"
import { paramValidator, requireUserMiddleware, resolveMemberTeamsMiddleware, resolveOrganizationContextMiddleware } from "../../middleware/index.js"
import { forbiddenSchema, invalidRequestSchema, jsonResponse, notFoundSchema, unauthorizedSchema } from "../../openapi.js"
import { memberHasRole } from "../org/shared.js"
import { fetchWorkerRuntimeJson, getWorkerByIdForOrg, parseWorkerIdParam, type WorkerId, type WorkerRouteVariables, workerIdParamSchema } from "./shared.js"

type LlmProviderRow = typeof LlmProviderTable.$inferSelect
type LlmProviderModelRow = typeof LlmProviderModelTable.$inferSelect
type OrganizationId = LlmProviderRow["organizationId"]
type MemberId = typeof LlmProviderAccessTable.$inferSelect.orgMembershipId
type TeamId = typeof LlmProviderAccessTable.$inferSelect.teamId

export type ManagedProviderSyncProvider = {
id: string
providerId: string
name: string
source: LlmProviderRow["source"]
credentialKind: LlmProviderRow["credentialKind"]
providerConfig: Record<string, unknown>
models: Array<{ id: string; name: string; config: Record<string, unknown> }>
apiKey?: string
opencodeAuth?: string
revision: string
}

type ManagedProviderRouteDeps = {
middlewares?: never[]
getWorker?: (workerId: WorkerId, orgId: OrganizationId) => Promise<{ id: WorkerId } | null>
listProviders?: (orgId: OrganizationId) => Promise<ManagedProviderSyncProvider[]>
pushRuntime?: (workerId: WorkerId, payload: { providers: ManagedProviderSyncProvider[]; revision: string }) => Promise<{ ok: boolean; status: number; payload: unknown }>
}

const managedProviderSyncResponseSchema = z.object({
status: z.enum(["applied", "failed"]),
providerCount: z.number().int().min(0),
revision: z.string(),
reason: z.string().optional(),
}).meta({ ref: "ManagedProviderSyncResponse" })

export function canSyncManagedProviders(payload: { currentMember: { isOwner: boolean; role: string } }) {
return payload.currentMember.isOwner || memberHasRole(payload.currentMember.role, "admin")
}

function credentialPresent(provider: Pick<LlmProviderRow, "credentialKind" | "apiKey" | "opencodeAuth">) {
return provider.credentialKind === "opencode_oauth"
? Boolean(provider.opencodeAuth?.trim())
: Boolean(provider.apiKey?.trim())
}

function revisionForProvider(provider: Pick<LlmProviderRow, "id" | "updatedAt" | "credentialKind">, models: LlmProviderModelRow[]) {
return [
provider.id,
provider.credentialKind,
provider.updatedAt instanceof Date ? provider.updatedAt.toISOString() : String(provider.updatedAt),
models.map((model) => `${model.modelId}:${model.name}`).sort().join(","),
].join(":")
}

export function computeManagedProviderRevision(providers: Pick<ManagedProviderSyncProvider, "id" | "revision">[]) {
return providers.map((provider) => `${provider.id}:${provider.revision}`).sort().join("|") || "empty"
}

export function sanitizeManagedProviderSyncFailure(payload: unknown) {
return "Worker provider sync failed."
}

async function listAccessibleManagedProviderIds(input: {
organizationId: OrganizationId
currentMemberId: NonNullable<MemberId>
memberTeamIds: NonNullable<TeamId>[]
}) {
const rows = await db
.select({ llmProviderId: LlmProviderAccessTable.llmProviderId })
.from(LlmProviderAccessTable)
.innerJoin(LlmProviderTable, eq(LlmProviderAccessTable.llmProviderId, LlmProviderTable.id))
.where(input.memberTeamIds.length > 0
? and(
eq(LlmProviderTable.organizationId, input.organizationId),
or(
eq(LlmProviderAccessTable.orgMembershipId, input.currentMemberId),
inArray(LlmProviderAccessTable.teamId, input.memberTeamIds),
),
)
: and(
eq(LlmProviderTable.organizationId, input.organizationId),
eq(LlmProviderAccessTable.orgMembershipId, input.currentMemberId),
))

return [...new Set(rows.map((row) => row.llmProviderId))]
}

export async function listManagedProviderSyncProviders(input: OrganizationId | {
organizationId: OrganizationId
currentMemberId: NonNullable<MemberId>
memberTeamIds: NonNullable<TeamId>[]
}) {
const organizationId = typeof input === "string" ? input : input.organizationId
const accessibleProviderIds = typeof input === "string"
? null
: await listAccessibleManagedProviderIds(input)

if (accessibleProviderIds && accessibleProviderIds.length === 0) return []

const providers = await db
.select()
.from(LlmProviderTable)
.where(accessibleProviderIds
? and(eq(LlmProviderTable.organizationId, organizationId), inArray(LlmProviderTable.id, accessibleProviderIds))
: eq(LlmProviderTable.organizationId, organizationId))

const eligible = providers.filter(credentialPresent)
if (!eligible.length) return []

const models = await db
.select()
.from(LlmProviderModelTable)
.where(inArray(LlmProviderModelTable.llmProviderId, eligible.map((provider) => provider.id)))

return eligible.map((provider) => {
const providerModels = models.filter((model) => model.llmProviderId === provider.id)
return {
id: provider.id,
providerId: provider.providerId,
name: provider.name,
source: provider.source,
credentialKind: provider.credentialKind,
providerConfig: provider.providerConfig,
models: providerModels.map((model) => ({ id: model.modelId, name: model.name, config: model.modelConfig })),
...(provider.credentialKind === "api_key" && provider.apiKey ? { apiKey: provider.apiKey } : {}),
...(provider.credentialKind === "opencode_oauth" && provider.opencodeAuth ? { opencodeAuth: provider.opencodeAuth } : {}),
revision: revisionForProvider(provider, providerModels),
}
})
}

export function registerManagedProviderSyncRoutes(app: Hono<{ Variables: WorkerRouteVariables }>, deps: ManagedProviderRouteDeps = {}) {
const routeMiddlewares = deps.middlewares ?? [requireUserMiddleware, resolveOrganizationContextMiddleware, resolveMemberTeamsMiddleware, paramValidator(workerIdParamSchema)]
const getWorker = deps.getWorker ?? getWorkerByIdForOrg
const listProviders = deps.listProviders ?? listManagedProviderSyncProviders
const pushRuntime = deps.pushRuntime ?? ((workerId, payload) => fetchWorkerRuntimeJson({
workerId,
path: "/managed-providers/sync",
method: "POST",
body: payload,
}))

app.post(
"/v1/workers/:id/managed-providers/sync",
describeRoute({
tags: ["Workers", "Managed Providers"],
summary: "Sync managed providers to worker runtime",
description: "Applies organization-managed provider config/auth to a static worker through the host-token runtime channel.",
responses: {
Comment thread
cubic-dev-ai[bot] marked this conversation as resolved.
200: jsonResponse("Managed providers applied successfully.", managedProviderSyncResponseSchema),
400: jsonResponse("The worker path parameters were invalid.", invalidRequestSchema),
401: jsonResponse("The caller must be signed in to sync providers.", unauthorizedSchema),
403: jsonResponse("Only organization owners and admins can sync providers.", forbiddenSchema),
404: jsonResponse("The worker could not be found.", notFoundSchema),
502: jsonResponse("The worker runtime failed to apply managed providers.", managedProviderSyncResponseSchema),
},
}),
...(routeMiddlewares as never[]),
async (c) => {
const orgId = c.get("activeOrganizationId")
const organizationContext = c.get("organizationContext")
const memberTeams = c.get("memberTeams") ?? []
const params = c.req.valid("param" as never) as { id: string }

if (!orgId) return c.json({ error: "worker_not_found" }, 404)
if (!organizationContext || !canSyncManagedProviders(organizationContext)) {
return c.json({ error: "forbidden", message: "Only organization owners and admins can sync managed providers." }, 403)
}

let workerId: WorkerId
try {
workerId = parseWorkerIdParam(params.id)
} catch {
return c.json({ error: "worker_not_found" }, 404)
}

const normalizedOrgId = normalizeDenTypeId("organization", orgId)
const worker = await getWorker(workerId, normalizedOrgId)
if (!worker) return c.json({ error: "worker_not_found" }, 404)

const providers = deps.listProviders
? await listProviders(normalizedOrgId)
: await listManagedProviderSyncProviders({
organizationId: normalizedOrgId,
currentMemberId: organizationContext.currentMember.id,
memberTeamIds: memberTeams.map((team) => team.id),
})
const revision = computeManagedProviderRevision(providers)

const runtime = await pushRuntime(worker.id, { providers, revision })
if (!runtime.ok) {
return c.json({
status: "failed",
providerCount: providers.length,
revision,
reason: sanitizeManagedProviderSyncFailure(runtime.payload),
}, 502)
}

return c.json({ status: "applied", providerCount: providers.length, revision })
},
)
}
33 changes: 30 additions & 3 deletions ee/apps/den-api/src/routes/workers/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import { z } from "zod"
import { requireCloudWorkerAccess } from "../../billing/polar.js"
import { db } from "../../db.js"
import { env } from "../../env.js"
import type { UserOrganizationsContext } from "../../middleware/index.js"
import type { MemberTeamsContext, OrganizationContextVariables, UserOrganizationsContext } from "../../middleware/index.js"
import { denTypeIdSchema } from "../../openapi.js"
import type { AuthContextVariables } from "../../session.js"
import { deprovisionWorker, provisionWorker } from "../../workers/provisioner.js"
Expand Down Expand Up @@ -49,7 +49,7 @@ export const workerIdParamSchema = z.object({
id: denTypeIdSchema("worker"),
})

export type WorkerRouteVariables = AuthContextVariables & Partial<UserOrganizationsContext>
export type WorkerRouteVariables = AuthContextVariables & Partial<UserOrganizationsContext> & Partial<OrganizationContextVariables> & Partial<MemberTeamsContext>

type WorkerRow = typeof WorkerTable.$inferSelect
type WorkerInstanceRow = typeof WorkerInstanceTable.$inferSelect
Expand Down Expand Up @@ -192,7 +192,16 @@ async function resolveConnectUrlFromCandidates(workerId: WorkerId, instanceUrl:
}

async function getWorkerRuntimeAccess(workerId: WorkerId) {
const instance = await getLatestWorkerInstance(workerId)
const workerRows = await db
.select({ status: WorkerTable.status })
.from(WorkerTable)
.where(eq(WorkerTable.id, workerId))
.limit(1)
if (workerRows[0]?.status !== "healthy") {
return null
}

const instance = await getLatestHealthyWorkerInstance(workerId)
const tokenRows = await db
.select()
.from(WorkerTokenTable)
Expand Down Expand Up @@ -284,6 +293,24 @@ export async function getLatestWorkerInstance(workerId: WorkerId) {
return rows[0] ?? null
}

export async function getLatestHealthyWorkerInstance(workerId: WorkerId) {
const rows = await db
.select()
.from(WorkerInstanceTable)
.where(and(eq(WorkerInstanceTable.worker_id, workerId), eq(WorkerInstanceTable.status, "healthy")))
.orderBy(desc(WorkerInstanceTable.created_at))
.limit(1)

return rows[0] ?? null
}

export function isWorkerRuntimeSyncTarget(input: { workerStatus?: string | null; instanceStatus?: string | null; instanceUrl?: string | null; hostToken?: string | null }) {
return input.workerStatus === "healthy"
&& input.instanceStatus === "healthy"
&& Boolean(input.instanceUrl?.trim())
&& Boolean(input.hostToken?.trim())
}

export function toInstanceResponse(instance: WorkerInstanceRow | null) {
if (!instance) {
return null
Expand Down
Loading