import { SessionControlConflictError, WorkspacePauseTimerInputError } from "@opengeni/db"; import { updateWorkspaceSettingsWithToolDefaults } from "@opengeni/db/workspace-tool-defaults"; import { WorkspacePauseTimerRequest } from "@opengeni/contracts"; import { getWorkspaceConnectionModelRestrictions } from "@opengeni/db"; import { createHash } from "node:crypto"; import { AddWorkspaceMemberRequest, CreateWorkspaceRequest, EnsureWorkspaceRequest, EnsureWorkspaceResponse, ListWorkspaceMemberCandidatesResponse, ListWorkspaceMembersResponse, SetWorkspaceDefaultRigRequest, UpdateWorkspaceMemberRequest, CreateWorkspaceGatewayCustomModelRequest, CreateWorkspaceOpenRouterCustomModelRequest, DeleteWorkspaceGatewayCustomModelRequest, DeleteWorkspaceOpenRouterCustomModelRequest, UpdateWorkspaceModelPolicyRequest, UpdateWorkspaceRequest, UpdateWorkspaceSettingsRequest, WORKSPACE_CONTROL_ACTOR_MAX_BYTES, WorkspaceModelCatalogResponse, WorkspaceGatewayCustomModel, WorkspaceGatewayCustomModelsResponse, WorkspaceOpenRouterCustomModel, WorkspaceOpenRouterCustomModelsResponse, WorkspaceRealtimeModelCatalogResponse, WorkspaceInferenceControlRequest, Workspace, WorkspaceMember, workspaceControlUtf8Bytes, stableJson, type AccessContext, type Permission, type WorkspaceMemberCandidate, type WorkspaceMember as WorkspaceMemberValue, } from "@opengeni/contracts"; import { allWorkspacePermissions, createWorkspace, ensureWorkspaceByExternalIdentity, findWorkspaceByExternalIdentity, getManagedUserProfilesByIds, getWorkspaceModelPolicy, grantWorkspaceAccess, listWorkspaceMembers, listWorkspaceMemberManagementCandidates, normalizeWorkspaceMembershipPermissions, listWorkspaceControlEvents, listSharedWorkspacesForAccount, listWorkspaceGatewayCustomModels, listWorkspaceOpenRouterCustomModels, createWorkspaceGatewayCustomModel, createWorkspaceOpenRouterCustomModel, deleteWorkspaceGatewayCustomModel, deleteWorkspaceOpenRouterCustomModel, replayWorkspaceGatewayCustomModelCreate, replayWorkspaceOpenRouterCustomModelCreate, listWorkspacesForSubject, nestedPostgresSqlState, removeWorkspaceMember, requireWorkspace, updateWorkspaceSettings, getRig, setWorkspaceDefaultRig, updateWorkspace, upsertWorkspaceMemberAsWorkspaceManager, upsertWorkspaceModelPolicy, workspaceCodexSubscriptionActive, workspaceControlRequestLockTimeoutMs, workspaceXaiSubscriptionActive, workspaceVercelAiGatewayConnectionActive, workspaceOpenRouterConnectionActive, organizationModelProviderConnectionActiveForWorkspace, listOrganizationModelProviderCustomModelsForWorkspace, WorkspaceGatewayCustomModelHistoryLimitError, WorkspaceOpenRouterCustomModelHistoryLimitError, WorkspaceExternalIdentityConflictError, WorkspaceGatewayCustomModelLimitError, WorkspaceOpenRouterCustomModelLimitError, WorkspaceLimitExceededError, } from "@opengeni/db"; import { boundWorkspaceControlHttpPage } from "@opengeni/events"; import type { Hono } from "hono"; import { HTTPException } from "hono/http-exception"; import { getManagedAuthRequestActorEpoch, accountScopedApiKeyWorkspaceAuthority, hasPermission, requireAccessContext, listExternalActorWorkspaces, addExternalWorkspaceMemberForRequest, requireAccessGrant, requireWorkspaceSettingsGrant, requireFreshAccessGrant, resolveWorkspaceCatalogSettings, } from "@opengeni/core"; import { requireLimit } from "@opengeni/core"; import type { ApiRouteDeps } from "@opengeni/core"; import { assertWorkspaceMemberRemovable, assertWorkspaceMemberUpdateAllowed, controlHumanWorkspace, controlHumanWorkspaceTimer, } from "@opengeni/core"; import { boundedLimit } from "../http/common"; import { ApiHttpError } from "../http/api-error"; import { browserSseDeliveryOptions, sseWorkspaceControlStream } from "../http/sse"; import { buildWorkspaceModelCatalog } from "../model-catalog"; import { deleteWorkspaceForRequest } from "../workspace-deletion"; import { AI_GATEWAY_REALTIME_MODELS, CODEX_REALTIME_MODEL_ID, SUPERGROK_REALTIME_MODEL_ID, canonicalizeConfiguredModelId, configuredStaticUsageLimits, configuredGatewayUpstreamModelIds, configuredGatewayWorkspaceProductModelIds, configuredModelInputIdentities, configuredOpenRouterUpstreamModelIds, configuredOpenRouterWorkspaceProductModelIds, WORKSPACE_GATEWAY_MODEL_ID_PREFIX, WORKSPACE_OPENROUTER_MODEL_ID_PREFIX, type Settings, } from "@opengeni/config"; export function canonicalWorkspacePolicyModelIds( settings: Settings, modelIds: string[] | null | undefined, ): string[] | null { if (modelIds === null || modelIds === undefined) { return null; } return [...new Set(modelIds.map((modelId) => canonicalizeConfiguredModelId(settings, modelId)))]; } function projectWorkspaceGatewayCustomModel(model: { id: string; upstreamModelId: string; label: string | null; version: number; createdAt: Date; updatedAt: Date; }) { return WorkspaceGatewayCustomModel.parse({ id: model.id, upstreamModelId: model.upstreamModelId, label: model.label, version: model.version, createdAt: model.createdAt.toISOString(), updatedAt: model.updatedAt.toISOString(), }); } function projectWorkspaceOpenRouterCustomModel(model: { id: string; upstreamModelId: string; label: string | null; version: number; createdAt: Date; updatedAt: Date; }) { return WorkspaceOpenRouterCustomModel.parse({ id: model.id, upstreamModelId: model.upstreamModelId, label: model.label, version: model.version, createdAt: model.createdAt.toISOString(), updatedAt: model.updatedAt.toISOString(), }); } function workspaceCustomModelRequestHash(value: unknown): string { return createHash("sha256").update(stableJson(value), "utf8").digest("hex"); } type WorkspaceMemberProjectionInput = Omit & { permissions: unknown; }; /** * Keep the roster readable across stored-permission contract changes. Unknown * values are never re-authorized: they are omitted from the public projection, * while every currently recognized permission is preserved in stored order. */ export function workspaceMembersResponse(members: readonly WorkspaceMemberProjectionInput[]) { return ListWorkspaceMembersResponse.parse({ members: members.map((member) => ({ ...member, permissions: normalizeWorkspaceMembershipPermissions(member.permissions), })), }); } export function workspaceUpdateRequestsAccountTransfer(value: unknown): boolean { return ( typeof value === "object" && value !== null && Object.prototype.hasOwnProperty.call(value, "accountId") ); } export function registerWorkspaceRoutes(app: Hono, deps: ApiRouteDeps): void { app.post("/v1/workspaces/:workspaceId/external-members", async (c) => { return c.json( await addExternalWorkspaceMemberForRequest( c, deps, c.req.param("workspaceId"), await c.req.json(), ), ); }); app.get("/v1/access/me", async (c) => { return c.json(await requireAccessContext(c, deps)); }); app.get("/v1/workspaces", async (c) => { const context = await requireAccessContext(c, deps); const externalWorkspaces = await listExternalActorWorkspaces(context, deps); if (externalWorkspaces !== null) return c.json(externalWorkspaces.map((workspace) => Workspace.parse(workspace))); const accountScopedAuthority = accountScopedApiKeyWorkspaceAuthority(context); if ( accountScopedAuthority && hasPermission(accountScopedAuthority.permissions, "workspace:read") ) { return c.json( (await listSharedWorkspacesForAccount(deps.db, accountScopedAuthority.accountId)).map( (workspace) => Workspace.parse(workspace), ), ); } const readableWorkspaceIds = [ ...new Set( context.workspaceGrants .filter((grant) => hasPermission(grant.permissions, "workspace:read")) .map((grant) => grant.workspaceId), ), ]; if (readableWorkspaceIds.length > 0) { const workspaces = await Promise.all( readableWorkspaceIds.map((workspaceId) => requireWorkspace(deps.db, workspaceId)), ); return c.json(workspaces.map((workspace) => Workspace.parse(workspace))); } return c.json( (await listWorkspacesForSubject(deps.db, context.subjectId)).map((workspace) => Workspace.parse(workspace), ), ); }); app.put("/v1/workspaces/external", async (c) => { const context = await requireAccessContext(c, deps); const payload = EnsureWorkspaceRequest.parse(await c.req.json()); requireAccountPermission(context, payload.accountId, "workspace:create"); try { const existing = await findWorkspaceByExternalIdentity(deps.db, { accountId: payload.accountId, externalSource: payload.externalSource, externalId: payload.externalId, }); if (existing) { if (existing.accountId !== payload.accountId || existing.kind !== "shared") { throw new WorkspaceExternalIdentityConflictError(); } return c.json( EnsureWorkspaceResponse.parse({ workspace: existing, created: false, }), ); } await requireLimit(deps, { accountId: payload.accountId, action: "workspace:create", quantity: 1, }); const result = await ensureWorkspaceByExternalIdentity(deps.db, { accountId: payload.accountId, externalSource: payload.externalSource, externalId: payload.externalId, name: payload.name, slug: payload.slug ?? null, ...(payload.agentInstructions !== undefined ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions), } : {}), maxWorkspacesPerAccount: workspaceLimit(deps), }); const response = EnsureWorkspaceResponse.parse(result); return result.created ? c.json(response, 201) : c.json(response); } catch (error) { if (error instanceof WorkspaceExternalIdentityConflictError) { throw new HTTPException(409, { message: "external workspace identity is already in use", }); } if (error instanceof WorkspaceLimitExceededError) { throw new HTTPException(429, { message: error.message }); } throw error; } }); app.post("/v1/workspaces", async (c) => { const context = await requireAccessContext(c, deps); const payload = CreateWorkspaceRequest.parse(await c.req.json()); const accountId = payload.accountId ?? context.defaultAccountId; if (!accountId) { throw new HTTPException(409, { message: "account selection is required", }); } requireAccountPermission(context, accountId, "workspace:create"); await requireLimit(deps, { accountId, action: "workspace:create", quantity: 1, }); try { const workspace = await createWorkspace(deps.db, { accountId, name: payload.name.trim(), slug: payload.slug?.trim() || null, externalSource: payload.externalSource ?? null, externalId: payload.externalId ?? null, ...(payload.agentInstructions !== undefined ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions), } : {}), maxWorkspacesPerAccount: workspaceLimit(deps), }); await grantWorkspaceAccess(deps.db, { accountId, workspaceId: workspace.id, subjectId: context.subjectId, role: "owner", permissions: allWorkspacePermissions, ...(context.subjectLabel ? { subjectLabel: context.subjectLabel } : {}), }); return c.json(Workspace.parse(workspace), 201); } catch (error) { if (error instanceof WorkspaceLimitExceededError) { throw new HTTPException(429, { message: error.message }); } throw error; } }); app.get("/v1/workspaces/:workspaceId", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:read"); return c.json(Workspace.parse(await requireWorkspace(deps.db, workspaceId))); }); app.patch("/v1/workspaces/:workspaceId", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:admin"); const body = await c.req.json(); if (workspaceUpdateRequestsAccountTransfer(body)) { throw new ApiHttpError(409, { code: "conflict", message: "A workspace is permanently owned by one organization; use workspace grants for same-organization access handoff.", retryable: false, outcomeUnknown: false, details: { code: "workspace_transfer_unsupported" }, }); } const payload = UpdateWorkspaceRequest.parse(body); const workspace = await updateWorkspace(deps.db, workspaceId, { ...(payload.name !== undefined ? { name: payload.name.trim() } : {}), ...(payload.slug !== undefined ? { slug: payload.slug?.trim() || null } : {}), ...(payload.agentInstructions !== undefined ? { agentInstructions: normalizeAgentInstructions(payload.agentInstructions), } : {}), }); return c.json(Workspace.parse(workspace)); }); // Read is via GET /v1/workspaces/:workspaceId (Workspace.settings). This PATCH // deep-merges (top-level) a settings patch, preserving unknown/future keys. app.patch("/v1/workspaces/:workspaceId/settings", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireWorkspaceSettingsGrant(c, deps, workspaceId); const parsed = UpdateWorkspaceSettingsRequest.safeParse(await c.req.json()); if (!parsed.success) { throw new HTTPException(400, { message: "invalid workspace settings patch", }); } // Request-scoped: bound the exclusive control-prefix wait so a busy // workspace yields the retryable 503 instead of parking this request. try { const workspace = await updateWorkspaceSettingsWithToolDefaults( deps.db, workspaceId, parsed.data, { requireWorkspace, updateWorkspaceSettings }, { controlLockTimeoutMs: workspaceControlRequestLockTimeoutMs(), }, ); return c.json(Workspace.parse(workspace)); } catch (error) { if (nestedPostgresSqlState(error) === "0A000") { throw new HTTPException(409, { message: "Memory settings moved to Settings > Agent learning. Refresh the app to change them.", }); } throw error; } }); // Per-workspace model/provider availability policy (the HARD blocker over // which providers/models may serve a turn at all). Absent row reads as // unrestricted {null, null}. No Azure AD credential resolver is wired here, // so bearer/federated definitions intentionally fail closed as not ready. app.get("/v1/workspaces/:workspaceId/model-catalog", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const [ connectionModelRestrictions, resolvedCatalog, policy, codexSubscriptionActive, xaiSubscriptionActive, workspaceGatewayConnectionActive, workspaceGatewayCustomModels, openRouterConnectionActive, workspaceOpenRouterCustomModels, organizationGatewayConnectionActive, organizationOpenRouterConnectionActive, organizationGatewayCustomModels, organizationOpenRouterCustomModels, ] = await Promise.all([ getWorkspaceConnectionModelRestrictions(deps.db, workspaceId, grant.subjectId), deps.resolveCatalogSettings(), getWorkspaceModelPolicy(deps.db, workspaceId), workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId), workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId), workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId), listWorkspaceGatewayCustomModels(deps.db, { accountId: grant.accountId, workspaceId, }), workspaceOpenRouterConnectionActive(deps.db, workspaceId), listWorkspaceOpenRouterCustomModels(deps.db, { accountId: grant.accountId, workspaceId, }), organizationModelProviderConnectionActiveForWorkspace(deps.db, { accountId: grant.accountId, workspaceId, providerKind: "vercel_gateway", }), organizationModelProviderConnectionActiveForWorkspace(deps.db, { accountId: grant.accountId, workspaceId, providerKind: "openrouter", }), listOrganizationModelProviderCustomModelsForWorkspace(deps.db, { accountId: grant.accountId, workspaceId, providerKind: "vercel_gateway", }), listOrganizationModelProviderCustomModelsForWorkspace(deps.db, { accountId: grant.accountId, workspaceId, providerKind: "openrouter", }), ]); c.header("cache-control", "private, no-store"); return c.json( WorkspaceModelCatalogResponse.parse( buildWorkspaceModelCatalog({ connectionModelRestrictions, settings: resolvedCatalog.settings, policy, codexSubscriptionActive, xaiSubscriptionActive, workspaceGatewayConnectionActive, workspaceGatewayCustomModels, workspaceOpenRouterConnectionActive: openRouterConnectionActive, workspaceOpenRouterCustomModels, organizationGatewayConnectionActive, organizationOpenRouterConnectionActive, organizationGatewayCustomModels, organizationOpenRouterCustomModels, }), ), ); }); app.get("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const models = await listWorkspaceGatewayCustomModels(deps.db, { accountId: grant.accountId, workspaceId, }); c.header("cache-control", "private, no-store"); return c.json( WorkspaceGatewayCustomModelsResponse.parse({ models: models.map(projectWorkspaceGatewayCustomModel), }), ); }); app.post("/v1/workspaces/:workspaceId/gateway-custom-models", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); const parsed = CreateWorkspaceGatewayCustomModelRequest.safeParse( await c.req.json().catch(() => null), ); if (!parsed.success) { throw new HTTPException(422, { message: "invalid Gateway custom model" }); } const requestHash = workspaceCustomModelRequestHash({ action: "create", upstreamModelId: parsed.data.upstreamModelId, label: parsed.data.label ?? null, }); const replay = await replayWorkspaceGatewayCustomModelCreate(deps.db, { accountId: grant.accountId, workspaceId, operationId: parsed.data.operationId, requestHash, }); if (replay.outcome === "conflict") { throw new HTTPException(409, { message: "Gateway custom model operation conflicts with current state", }); } if (replay.outcome === "success") { return c.json(projectWorkspaceGatewayCustomModel(replay.model), 201); } const catalog = await deps.resolveCatalogSettings(); if (configuredGatewayUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId)) { throw new HTTPException(422, { message: "Gateway model is already included in the deployment catalog", }); } const customProductId = `${WORKSPACE_GATEWAY_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`; const deploymentProductIds = new Set([ ...configuredModelInputIdentities(catalog.settings), ...configuredGatewayWorkspaceProductModelIds(catalog.settings), ]); if (deploymentProductIds.has(customProductId)) { throw new HTTPException(422, { message: "Gateway model product id conflicts with the deployment catalog", }); } try { const model = await createWorkspaceGatewayCustomModel(deps.db, { accountId: grant.accountId, workspaceId, upstreamModelId: parsed.data.upstreamModelId, label: parsed.data.label ?? null, operationId: parsed.data.operationId, requestHash, createdBySubjectId: grant.subjectId, }); if (!model || model.retiredAt) { throw new HTTPException(409, { message: "Gateway custom model operation conflicts with current state", }); } return c.json(projectWorkspaceGatewayCustomModel(model), 201); } catch (error) { if ( error instanceof WorkspaceGatewayCustomModelLimitError || error instanceof WorkspaceGatewayCustomModelHistoryLimitError ) { throw new HTTPException(422, { message: error.message }); } if (nestedPostgresSqlState(error) === "23505") { throw new HTTPException(422, { message: "Gateway custom model already exists", }); } throw error; } }); app.delete("/v1/workspaces/:workspaceId/gateway-custom-models/:customModelId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); const customModelId = c.req.param("customModelId"); if ( !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test( customModelId, ) ) { throw new HTTPException(422, { message: "invalid Gateway custom model id", }); } const parsed = DeleteWorkspaceGatewayCustomModelRequest.safeParse( await c.req.json().catch(() => null), ); if (!parsed.success) { throw new HTTPException(422, { message: "invalid Gateway custom model deletion", }); } const removed = await deleteWorkspaceGatewayCustomModel(deps.db, { accountId: grant.accountId, workspaceId, customModelId, expectedVersion: parsed.data.expectedVersion, operationId: parsed.data.operationId, requestHash: workspaceCustomModelRequestHash({ action: "delete", customModelId, expectedVersion: parsed.data.expectedVersion, }), }); if (removed.outcome === "not_found") { throw new HTTPException(404, { message: "Gateway custom model not found", }); } if (removed.outcome === "conflict") { throw new HTTPException(409, { message: "Gateway custom model changed; reload and retry", }); } return c.body(null, 204); }); app.get("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const models = await listWorkspaceOpenRouterCustomModels(deps.db, { accountId: grant.accountId, workspaceId, }); c.header("cache-control", "private, no-store"); return c.json( WorkspaceOpenRouterCustomModelsResponse.parse({ models: models.map(projectWorkspaceOpenRouterCustomModel), }), ); }); app.post("/v1/workspaces/:workspaceId/openrouter-custom-models", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); const parsed = CreateWorkspaceOpenRouterCustomModelRequest.safeParse( await c.req.json().catch(() => null), ); if (!parsed.success) { throw new HTTPException(422, { message: "invalid OpenRouter custom model" }); } const requestHash = workspaceCustomModelRequestHash({ action: "create", upstreamModelId: parsed.data.upstreamModelId, label: parsed.data.label ?? null, }); const replay = await replayWorkspaceOpenRouterCustomModelCreate(deps.db, { accountId: grant.accountId, workspaceId, operationId: parsed.data.operationId, requestHash, }); if (replay.outcome === "conflict") { throw new HTTPException(409, { message: "OpenRouter custom model operation conflicts with current state", }); } if (replay.outcome === "success") { return c.json(projectWorkspaceOpenRouterCustomModel(replay.model), 201); } const catalog = await deps.resolveCatalogSettings(); if ( configuredOpenRouterUpstreamModelIds(catalog.settings).includes(parsed.data.upstreamModelId) ) { throw new HTTPException(422, { message: "OpenRouter model is already included in the deployment catalog", }); } const customProductId = `${WORKSPACE_OPENROUTER_MODEL_ID_PREFIX}${parsed.data.upstreamModelId}`; const deploymentProductIds = new Set([ ...configuredModelInputIdentities(catalog.settings), ...configuredOpenRouterWorkspaceProductModelIds(catalog.settings), ]); if (deploymentProductIds.has(customProductId)) { throw new HTTPException(422, { message: "OpenRouter model product id conflicts with the deployment catalog", }); } try { const model = await createWorkspaceOpenRouterCustomModel(deps.db, { accountId: grant.accountId, workspaceId, upstreamModelId: parsed.data.upstreamModelId, label: parsed.data.label ?? null, operationId: parsed.data.operationId, requestHash, createdBySubjectId: grant.subjectId, }); if (!model || model.retiredAt) { throw new HTTPException(409, { message: "OpenRouter custom model operation conflicts with current state", }); } return c.json(projectWorkspaceOpenRouterCustomModel(model), 201); } catch (error) { if ( error instanceof WorkspaceOpenRouterCustomModelLimitError || error instanceof WorkspaceOpenRouterCustomModelHistoryLimitError ) { throw new HTTPException(422, { message: error.message }); } if (nestedPostgresSqlState(error) === "23505") { throw new HTTPException(422, { message: "OpenRouter custom model already exists", }); } throw error; } }); app.delete("/v1/workspaces/:workspaceId/openrouter-custom-models/:customModelId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); const customModelId = c.req.param("customModelId"); if ( !/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu.test( customModelId, ) ) { throw new HTTPException(422, { message: "invalid OpenRouter custom model id", }); } const parsed = DeleteWorkspaceOpenRouterCustomModelRequest.safeParse( await c.req.json().catch(() => null), ); if (!parsed.success) { throw new HTTPException(422, { message: "invalid OpenRouter custom model deletion", }); } const removed = await deleteWorkspaceOpenRouterCustomModel(deps.db, { accountId: grant.accountId, workspaceId, customModelId, expectedVersion: parsed.data.expectedVersion, operationId: parsed.data.operationId, requestHash: workspaceCustomModelRequestHash({ action: "delete", customModelId, expectedVersion: parsed.data.expectedVersion, }), }); if (removed.outcome === "not_found") { throw new HTTPException(404, { message: "OpenRouter custom model not found", }); } if (removed.outcome === "conflict") { throw new HTTPException(409, { message: "OpenRouter custom model changed; reload and retry", }); } return c.body(null, 204); }); app.get("/v1/workspaces/:workspaceId/realtime-model-catalog", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const [codexConnected, supergrokConnected, workspaceGatewayConnected] = await Promise.all([ workspaceCodexSubscriptionActive(deps.db, deps.settings, workspaceId), workspaceXaiSubscriptionActive(deps.db, deps.settings, workspaceId, grant.subjectId), workspaceVercelAiGatewayConnectionActive(deps.db, workspaceId), ]); const availability = ( credentialReady: boolean, credentialReason: string, ): { available: boolean; unavailableReason: string | null } => { return credentialReady ? { available: true, unavailableReason: null } : { available: false, unavailableReason: credentialReason }; }; const gatewayModels = Object.values(AI_GATEWAY_REALTIME_MODELS); const models = [ ...gatewayModels.map((model, index) => ({ id: model.managedModelId, label: model.label, provider: "OpenGeni" as const, description: model.description, ...availability( Boolean(deps.settings.vercelAiGatewayApiKey), "OpenGeni Gateway voice is not configured", ), recommended: index === 0, })), { id: CODEX_REALTIME_MODEL_ID, label: "Codex Live", provider: "Connected Codex" as const, description: "Deep session integration", ...availability(codexConnected, "Connect Codex to use this voice model"), recommended: false, }, { id: SUPERGROK_REALTIME_MODEL_ID, label: "Grok Voice Think Fast 2.0", provider: "Connected SuperGrok" as const, description: "Direct SuperGrok speech-to-speech", ...availability(supergrokConnected, "Connect SuperGrok to use this voice model"), recommended: false, }, ...gatewayModels.map((model) => ({ id: model.workspaceModelId, label: model.label, provider: "Your Gateway" as const, description: model.description, ...availability(workspaceGatewayConnected, "Connect a workspace AI Gateway key"), recommended: false, })), ]; c.header("cache-control", "private, no-store"); return c.json(WorkspaceRealtimeModelCatalogResponse.parse({ models })); }); app.get("/v1/workspaces/:workspaceId/model-policy", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const policy = await getWorkspaceModelPolicy(deps.db, workspaceId); return c.json({ allowedProviders: policy?.allowedProviders ?? null, allowedModels: policy?.allowedModels ?? null, }); }); // Full replace (PUT, not merge): null/omitted = unrestricted for that // dimension; an empty array is a valid explicit total block. Settings access // admits workspace administrators and the verified Personal owner, without // widening membership or API-key delegation authority. app.put("/v1/workspaces/:workspaceId/model-policy", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); const payload = UpdateWorkspaceModelPolicyRequest.parse(await c.req.json()); const catalog = await resolveWorkspaceCatalogSettings(deps.db, deps.settings, { accountId: grant.accountId, workspaceId, }); const policy = await upsertWorkspaceModelPolicy(deps.db, { accountId: grant.accountId, workspaceId, allowedProviders: payload.allowedProviders ?? null, allowedModels: canonicalWorkspacePolicyModelIds(catalog.settings, payload.allowedModels), }); return c.json(policy); }); app.post("/v1/workspaces/:workspaceId/pause-timer", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) { throw new HTTPException(400, { message: "workspace-control actor is too large" }); } const parsed = WorkspacePauseTimerRequest.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) throw new HTTPException(400, { message: "Invalid pause timer" }); try { await controlHumanWorkspaceTimer( { db: deps.db, bus: deps.bus, workflowClient: deps.workflowClient, ...(deps.schedulePromptPostCommit ? { schedulePromptPostCommit: deps.schedulePromptPostCommit } : {}), }, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId }, parsed.data, ); } catch (error) { if (error instanceof SessionControlConflictError) throw new HTTPException(409, { message: "Workspace changed. Reopen the timer and try again.", }); if (error instanceof WorkspacePauseTimerInputError) throw new HTTPException(400, { message: error.message }); throw error; } return c.json({ ok: true }); }); app.post("/v1/workspaces/:workspaceId/inference-control", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireWorkspaceSettingsGrant(c, deps, workspaceId); if (workspaceControlUtf8Bytes(grant.subjectId) > WORKSPACE_CONTROL_ACTOR_MAX_BYTES) { throw new HTTPException(400, { message: "workspace-control actor is too large", }); } const parsed = WorkspaceInferenceControlRequest.safeParse(await c.req.json().catch(() => null)); if (!parsed.success) { throw new HTTPException(400, { message: "invalid workspace inference-control request", }); } return c.json( await controlHumanWorkspace( { db: deps.db, bus: deps.bus, workflowClient: deps.workflowClient, ...(deps.schedulePromptPostCommit ? { schedulePromptPostCommit: deps.schedulePromptPostCommit } : {}), }, { accountId: grant.accountId, workspaceId, subjectId: grant.subjectId }, parsed.data, ), ); }); app.get("/v1/workspaces/:workspaceId/control-events", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0); const limit = boundedLimit(c.req.query("limit")); const fetched = await listWorkspaceControlEvents(deps.db, workspaceId, after, limit + 1); const countHasMore = fetched.length > limit; const page = boundWorkspaceControlHttpPage(fetched.slice(0, limit)); const truncated = countHasMore || page.truncated; c.header("X-OpenGeni-Page-Bytes", String(page.bytes)); c.header("X-OpenGeni-Page-Truncated", String(truncated)); if (page.nextSequence !== null) { c.header("X-OpenGeni-Next-After", String(page.nextSequence)); } return c.json(page.events); }); app.get("/v1/workspaces/:workspaceId/control-events/stream", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const after = Math.max(0, Number.parseInt(c.req.query("after") ?? "0", 10) || 0); return await sseWorkspaceControlStream( deps.db, deps.bus, workspaceId, after, c.req.raw.signal, { ...browserSseDeliveryOptions(c.req.query("transport")), observability: deps.observability, actorEpoch: getManagedAuthRequestActorEpoch(c.req.raw) ?? undefined, reauthorize: async () => { await requireFreshAccessGrant(c, deps, workspaceId, "workspace:read"); }, }, ); }); app.put("/v1/workspaces/:workspaceId/default-rig", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "rigs:manage"); const payload = SetWorkspaceDefaultRigRequest.parse(await c.req.json()); if (payload.rigId) { const rig = await getRig(deps.db, workspaceId, payload.rigId); if (!rig) { throw new HTTPException(422, { message: `unknown rigId: ${payload.rigId}`, }); } } const workspace = await setWorkspaceDefaultRig(deps.db, workspaceId, payload.rigId); return c.json(Workspace.parse(workspace)); }); app.delete("/v1/workspaces/:workspaceId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "workspace:admin"); await deleteWorkspaceForRequest(deps, { accountId: grant.accountId, workspaceId, }); return c.body(null, 204); }); // --- Members ("People with access") --------------------------------------- app.get("/v1/workspaces/:workspaceId/members", async (c) => { const workspaceId = c.req.param("workspaceId"); await requireAccessGrant(c, deps, workspaceId, "workspace:read"); const members = await listWorkspacePeople(deps, workspaceId); return c.json(workspaceMembersResponse(members)); }); app.get("/v1/workspaces/:workspaceId/member-candidates", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage"); try { return c.json( ListWorkspaceMemberCandidatesResponse.parse({ members: await listWorkspaceMemberManagementCandidates(deps.db, { accountId: grant.accountId, workspaceId, actorSubjectId: grant.subjectId, }), }), ); } catch (error) { rethrowWorkspaceMemberCandidateError(error); } }); app.post("/v1/workspaces/:workspaceId/members", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage"); const payload = AddWorkspaceMemberRequest.parse(await c.req.json()); let candidates: WorkspaceMemberCandidate[]; try { candidates = await listWorkspaceMemberManagementCandidates(deps.db, { accountId: grant.accountId, workspaceId, actorSubjectId: grant.subjectId, }); } catch (error) { rethrowWorkspaceMemberCandidateError(error); } const candidate = candidates.find( (entry) => entry.organizationMembershipId === payload.organizationMembershipId, ); if (!candidate) { throw new HTTPException(404, { message: "this organization member is not available to add", }); } const subjectId = candidate.subjectId; const existing = await listWorkspaceMembers(deps.db, workspaceId); if (existing.some((member) => member.subjectId === subjectId)) { throw new HTTPException(409, { message: "this person already has access to the workspace", }); } try { await upsertWorkspaceMemberAsWorkspaceManager(deps.db, { accountId: grant.accountId, workspaceId, actorSubjectId: grant.subjectId, targetSubjectId: subjectId, mode: "add", subjectLabel: candidate.name?.trim() || candidate.email || subjectId, role: payload.role ?? "member", permissions: payload.permissions, }); } catch (error) { rethrowWorkspaceMemberCandidateError(error); } const members = await listWorkspacePeople(deps, workspaceId); const member = members.find((addedMember) => addedMember.subjectId === subjectId); if (!member) { throw new HTTPException(500, { message: "failed to add member" }); } return c.json(WorkspaceMember.parse(member), 201); }); app.patch("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage"); const subjectId = decodeURIComponent(c.req.param("subjectId")); const payload = UpdateWorkspaceMemberRequest.parse(await c.req.json()); const existing = await listWorkspacePeople(deps, workspaceId); const current = existing.find((member) => member.subjectId === subjectId); if (!current) { throw new HTTPException(404, { message: "member not found" }); } assertWorkspaceMemberUpdateAllowed({ members: existing, subjectId, callerSubjectId: grant.subjectId, nextPermissions: payload.permissions, }); try { await upsertWorkspaceMemberAsWorkspaceManager(deps.db, { accountId: grant.accountId, workspaceId, actorSubjectId: grant.subjectId, targetSubjectId: subjectId, mode: "update", ...(current.subjectLabel ? { subjectLabel: current.subjectLabel } : {}), role: payload.role ?? current.role, permissions: payload.permissions, }); } catch (error) { rethrowWorkspaceMemberCandidateError(error); } const members = await listWorkspacePeople(deps, workspaceId); const member = members.find((candidate) => candidate.subjectId === subjectId); if (!member) { throw new HTTPException(500, { message: "failed to update member" }); } return c.json(WorkspaceMember.parse(member)); }); app.delete("/v1/workspaces/:workspaceId/members/:subjectId", async (c) => { const workspaceId = c.req.param("workspaceId"); const grant = await requireAccessGrant(c, deps, workspaceId, "members:manage"); const subjectId = decodeURIComponent(c.req.param("subjectId")); const members = await listWorkspaceMembers(deps.db, workspaceId); // Never remove yourself, and never remove the last administering member. // The fenced removal command re-enforces both guards fail-closed. assertWorkspaceMemberRemovable({ members, subjectId, callerSubjectId: grant.subjectId, }); await removeWorkspaceMember(deps.db, { accountId: grant.accountId, workspaceId, actorSubjectId: grant.subjectId, targetSubjectId: subjectId, }); return c.body(null, 204); }); } function workspaceLimit(deps: ApiRouteDeps): number | null { if (deps.settings.usageLimitsMode !== "static" && deps.settings.usageLimitsMode !== "managed") { return null; } return configuredStaticUsageLimits(deps.settings).maxWorkspacesPerAccount ?? null; } async function listWorkspacePeople( deps: ApiRouteDeps, workspaceId: string, ): Promise { const members = await listWorkspaceMembers(deps.db, workspaceId); const userIds = members.flatMap((member) => member.subjectId.startsWith("user:") ? [member.subjectId.slice("user:".length)] : [], ); const profiles = await getManagedUserProfilesByIds(deps.db, userIds); const profileBySubject = new Map( profiles.map((profile) => [`user:${profile.id}`, profile.name?.trim() || profile.email]), ); return members.map((member) => ({ ...member, subjectLabel: profileBySubject.get(member.subjectId) ?? member.subjectLabel, })); } function rethrowWorkspaceMemberCandidateError(error: unknown): never { const managementCode = error && typeof error === "object" && "code" in error ? error.code : undefined; if (managementCode === "WORKSPACE_MEMBER_ALREADY_EXISTS") { throw new HTTPException(409, { message: "this person already has access to the workspace", }); } if (managementCode === "WORKSPACE_MEMBER_NOT_FOUND") { throw new HTTPException(404, { message: "member not found" }); } if (managementCode === "WORKSPACE_MEMBER_SELF_UPDATE") { throw new HTTPException(409, { message: "you cannot change your own workspace access", }); } if (managementCode === "WORKSPACE_MEMBER_LAST_ADMIN") { throw new HTTPException(409, { message: "the workspace must keep at least one administrator", }); } const sqlState = nestedPostgresSqlState(error); if (sqlState === "P0002") { throw new HTTPException(404, { message: "workspace not found" }); } if (sqlState === "42501") { throw new HTTPException(403, { message: "workspace member management is not allowed", }); } if (sqlState === "54000") { throw new HTTPException(409, { message: "this organization has too many members to show here", }); } throw error; } // A persona override that is null or trims to empty collapses to null (use the // deployment default). Otherwise the template is stored verbatim so the runtime // can substitute the non-bypassable CORE at its {{core}} marker. function normalizeAgentInstructions(value: string | null): string | null { if (value === null) { return null; } const trimmed = value.trim(); return trimmed.length > 0 ? trimmed : null; } function requireAccountPermission( context: AccessContext, accountId: string, permission: Permission, ): void { const grant = context.accountGrants.find((candidate) => candidate.accountId === accountId); if ( !grant || (!grant.permissions.includes(permission) && !grant.permissions.includes("account:admin")) ) { throw new HTTPException(403, { message: `missing permission: ${permission}`, }); } }