The per-connector "enabled/disabled model" settings lived under a
global key in the settings key-value table
(`disabled_models:${connectorId}`). No company dimension. Deleting a
company left the pool choices behind, and creating a new company
silently inherited them — the visible symptom was "onboarding's team-
model configuration survives company deletion and pollutes the next
company's CEO model list."
Root fix, not a cleanup hook: the storage becomes a real relational
table with `(company_id, connector_id)` primary key and an
`ON DELETE CASCADE` FK from `companies`. Company deletion wipes pool
config by construction; no cleanup step in `deleteCompany()` can
drift.
Schema (migration v42):
- CREATE TABLE connector_model_pool_settings (company_id, connector_id,
disabled_models_json, updated_at), PK (company_id, connector_id),
FK companies(id) ON DELETE CASCADE.
- Drop every `settings.disabled_models:*` key — these were the bug's
residue; there is no right company to attribute them to, so the
migration clears them. Users re-select on next onboarding.
API surgery — every reader/writer now takes companyId:
- db.getDisabledModels(companyId, connectorId)
- db.setDisabledModels(companyId, connectorId, models)
- db.hasModelPoolSettings(companyId, connectorId)
- connector-model-policy: getConnectorModelPolicy(deps, companyId,
connectorId); validateConnectorModelSelection(deps, companyId,
connectorId, model)
- model-dispatch: listEnabledModels(companyId, deps);
pickModelForAgent resolves companyId from agent.companyId
- orchestration heartbeat/task-runner/turn-context/run-dispatch pass
the agent's or run's companyId through to the policy deps
- IPC schema: connectorDisabledModelsSchema gains companyId; new
connectorModelPoolQuerySchema pairs companyId+connectorId for reads
Onboarding becomes commit-on-launch:
- useConnectorModelSelection no longer persists via IPC during
onboarding. Toggles mutate local state; it exposes `poolSelections`
to the wizard.
- bootstrapOnboardingSchema gains connectorPoolSelections. The
onboarding service saves the company first, then persists each
per-connector disabled list scoped to the new company id — CEO
model validation runs against the company's freshly-persisted pool,
not some stale global.
- IPC setConnectorDisabledModels now requires companyId; the only
consumer was onboarding, which no longer calls it at all.
- Main-side listConnectorModels returns the raw provider catalog
(no pool filtering), since that IPC is used from onboarding where
no company exists yet; pool filtering happens at dispatch time.
saveConnector IPC drops the per-company pool validation — connector
config is a global hint, company-scoped enforcement belongs at run
dispatch where the company is known.
Tests: 603/603 green. Typecheck clean. Integration tests rewritten
to set the pool via connectorPoolSelections on bootstrap input, not
a pre-bootstrap global setDisabledModels call.
135 lines
4.2 KiB
TypeScript
135 lines
4.2 KiB
TypeScript
import { z } from "zod";
|
|
import { optionalId } from "./helpers";
|
|
|
|
export const companyInputSchema = z.object({
|
|
id: optionalId,
|
|
name: z.string().min(2).max(80),
|
|
description: z.string().max(500).default(""),
|
|
status: z.enum(["active", "paused", "archived"]).default("active"),
|
|
autoApproveHires: z.boolean().default(false),
|
|
reviewDeliverables: z.boolean().default(false),
|
|
executionMode: z.enum(["ai_ceo", "ai_cofounder"]).default("ai_ceo"),
|
|
});
|
|
|
|
/**
|
|
* Pure pi-mono connector catalog ids. Single source of truth: this zod enum.
|
|
* The TS `ConnectorId` alias (`shared/types/enums.ts`) and the catalog
|
|
* (`shared/connector-catalog.ts`) both derive from it. Adding a connector
|
|
* requires editing this list only.
|
|
*/
|
|
export const connectorIdEnum = z.enum([
|
|
// OAuth (5)
|
|
"claude-pro",
|
|
"openai-codex",
|
|
"github-copilot",
|
|
"gemini-cli",
|
|
"antigravity",
|
|
// API key (18)
|
|
"anthropic",
|
|
"openai",
|
|
"google",
|
|
"mistral",
|
|
"groq",
|
|
"cerebras",
|
|
"xai",
|
|
"openrouter",
|
|
"vercel-ai-gateway",
|
|
"zai",
|
|
"opencode",
|
|
"opencode-go",
|
|
"huggingface",
|
|
"kimi-coding",
|
|
"minimax",
|
|
"minimax-cn",
|
|
"fireworks",
|
|
"fireworks-firepass",
|
|
// Cloud (3)
|
|
"amazon-bedrock",
|
|
"azure-openai",
|
|
"google-vertex",
|
|
]);
|
|
|
|
export type ConnectorId = z.infer<typeof connectorIdEnum>;
|
|
|
|
export const connectorInputSchema = z.object({
|
|
id: connectorIdEnum,
|
|
command: z.string().min(1),
|
|
model: z.string().nullable().optional(),
|
|
envBindingText: z.string().default(""),
|
|
notes: z.string().default(""),
|
|
});
|
|
|
|
export const connectorDisabledModelsSchema = z.object({
|
|
companyId: z.string().uuid(),
|
|
connectorId: connectorInputSchema.shape.id,
|
|
disabledModels: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
});
|
|
|
|
export const connectorModelPoolQuerySchema = z.object({
|
|
companyId: z.string().uuid(),
|
|
connectorId: connectorInputSchema.shape.id,
|
|
});
|
|
|
|
export const workspaceInputSchema = z.object({
|
|
id: optionalId,
|
|
companyId: z.string().uuid(),
|
|
name: z.string().min(2).max(80),
|
|
localPath: z.string().min(1),
|
|
repoUrl: z.string().default(""),
|
|
repoRef: z.string().default(""),
|
|
projectId: optionalId,
|
|
isPrimary: z.boolean().default(false),
|
|
});
|
|
|
|
export const settingsSchema = z.object({
|
|
theme: z.enum(["system", "light", "dark"]).optional(),
|
|
locale: z.enum(["en", "zh"]).optional(),
|
|
updatesEnabled: z.boolean().optional(),
|
|
logRetentionDays: z.number().int().min(1).max(365).optional(),
|
|
});
|
|
|
|
export const bootstrapOnboardingSchema = z.object({
|
|
companyName: z.string().min(2).max(80),
|
|
companyDescription: z.string().max(500).default(""),
|
|
workspacePath: z.string().default(""),
|
|
autoApproveHires: z.boolean().default(true),
|
|
reviewDeliverables: z.boolean().default(false),
|
|
executionMode: z.enum(["ai_ceo", "ai_cofounder"]).default("ai_ceo"),
|
|
goalTitle: z.string().default(""),
|
|
goalDescription: z.string().default(""),
|
|
connectorId: connectorInputSchema.shape.id.optional(),
|
|
connectorModel: z.string().default(""),
|
|
// Per-connector disabled-model selections gathered during onboarding.
|
|
// Persisted against the new company's id once it is created, so they
|
|
// are scoped correctly from the first moment the company exists.
|
|
connectorPoolSelections: z
|
|
.array(
|
|
z.object({
|
|
connectorId: connectorInputSchema.shape.id,
|
|
disabledModels: z.array(z.object({ id: z.string(), label: z.string() })),
|
|
}),
|
|
)
|
|
.default([]),
|
|
});
|
|
|
|
export const socialAccountInputSchema = z.object({
|
|
id: optionalId,
|
|
companyId: z.string().uuid(),
|
|
platform: z.enum(["twitter", "linkedin", "instagram", "facebook", "reddit", "youtube", "tiktok", "bluesky", "discord", "threads", "pinterest", "telegram", "whatsapp", "wechat", "weibo", "xiaohongshu", "douyin", "bilibili", "zhihu", "other"]),
|
|
accountName: z.string().min(1).max(120),
|
|
displayName: z.string().max(120).default(""),
|
|
profileUrl: z.string().max(500).default(""),
|
|
credentialSecretId: optionalId,
|
|
status: z.enum(["active", "logged_in", "login_required", "suspended", "paused"]).default("login_required"),
|
|
requireApproval: z.boolean().default(true),
|
|
metadataJson: z.string().default("{}"),
|
|
});
|
|
|
|
export const secretInputSchema = z.object({
|
|
id: optionalId,
|
|
companyId: z.string().uuid(),
|
|
name: z.string().min(2).max(80),
|
|
description: z.string().default(""),
|
|
value: z.string().min(1).max(2000),
|
|
});
|