diff --git a/hub/.gitignore b/hub/.gitignore index ed814f3..f02a5e5 100644 --- a/hub/.gitignore +++ b/hub/.gitignore @@ -4,6 +4,7 @@ dist/ .env .env.* !.env.example +.secrets/ admin-web/node_modules/ admin-web/build/ admin-web/.svelte-kit/ diff --git a/hub/admin-web/src/lib/api.ts b/hub/admin-web/src/lib/api.ts index 076f7d0..4c63ff4 100644 --- a/hub/admin-web/src/lib/api.ts +++ b/hub/admin-web/src/lib/api.ts @@ -145,27 +145,6 @@ export interface SessionSummary { updatedAt: string; } -export interface OrgModelRow { - id: string; - modelId: string; - label: string; - toolCapable: boolean; - sortKey: string; - createdAt: string; - updatedAt: string; -} - -export interface OrgRoleRow { - id: string; - roleId: string; - label: string; - defaultModelId: string | null; - systemPrompt: string | null; - tools: string[] | null; - createdAt: string; - updatedAt: string; -} - export interface ProviderConnection { organizationId: string; providerId: string; @@ -269,22 +248,6 @@ export const api = { return get(`${orgBase(slug)}/usage${qs ? `?${qs}` : ''}`) as Promise; }, - models: (slug: string) => get(`${orgBase(slug)}/models`) as Promise<{ models: OrgModelRow[] }>, - createModel: (slug: string, body: { modelId: string; label: string; toolCapable?: boolean; sortKey?: string }) => - post(`${orgBase(slug)}/models`, body) as Promise, - updateModel: (slug: string, id: string, body: { label?: string; toolCapable?: boolean; sortKey?: string; modelId?: string }) => - patch(`${orgBase(slug)}/models/${id}`, body) as Promise, - deleteModel: (slug: string, id: string) => - del(`${orgBase(slug)}/models/${id}`), - - roles: (slug: string) => get(`${orgBase(slug)}/roles`) as Promise<{ roles: OrgRoleRow[] }>, - createRole: (slug: string, body: { roleId: string; label: string; defaultModelId?: string | null; systemPrompt?: string | null; tools?: string[] | null }) => - post(`${orgBase(slug)}/roles`, body) as Promise, - updateRole: (slug: string, id: string, body: { label?: string; defaultModelId?: string | null; systemPrompt?: string | null; tools?: string[] | null; roleId?: string }) => - patch(`${orgBase(slug)}/roles/${id}`, body) as Promise, - deleteRole: (slug: string, id: string) => - del(`${orgBase(slug)}/roles/${id}`), - provider: (slug: string) => get(`${orgBase(slug)}/provider-connection`) as Promise, setProvider: (slug: string, body: { mode: string; providerId?: string; baseUrl?: string | null; authToken?: string | null }) => put(`${orgBase(slug)}/provider-connection`, body) as Promise diff --git a/hub/admin-web/src/lib/components/Icon.svelte b/hub/admin-web/src/lib/components/Icon.svelte index 9241400..a9a6ac3 100644 --- a/hub/admin-web/src/lib/components/Icon.svelte +++ b/hub/admin-web/src/lib/components/Icon.svelte @@ -9,8 +9,6 @@ | 'members' | 'teams' | 'projects' - | 'models' - | 'roles' | 'provider' | 'menu' | 'logout' @@ -51,18 +49,6 @@ d="M3.75 6.75A2.25 2.25 0 016 4.5h3.379c.6 0 1.175.238 1.6.66l.842.84c.424.423 1 .66 1.6.66H18A2.25 2.25 0 0120.25 9v8.25A2.25 2.25 0 0118 19.5H6a2.25 2.25 0 01-2.25-2.25V6.75z" /> -{:else if name === 'models'} - - - -{:else if name === 'roles'} - - - {:else if name === 'provider'} diff --git a/hub/admin-web/src/lib/components/RoleCard.svelte b/hub/admin-web/src/lib/components/RoleCard.svelte deleted file mode 100644 index 67fed54..0000000 --- a/hub/admin-web/src/lib/components/RoleCard.svelte +++ /dev/null @@ -1,148 +0,0 @@ - - -
-
- /{roleId || r.roleId} - {label || r.label} -
- -
-
- 角色 ID - -
-
- 显示名 - -
-
- -
-

默认模型

- -
- -
- 工具白名单 - -
- - {#each Object.entries(groupedTools) as [group, tools]} -
-

{group}

-
- {#each tools as t} - - {/each} -
-
- {/each} -
-
-
- -
- 系统提示词 - -
- -
- 更新于 {fmtDate(r.updatedAt)} -
- - -
-
diff --git a/hub/admin-web/src/routes/+layout.svelte b/hub/admin-web/src/routes/+layout.svelte index 2bb3bd5..7f1b5f4 100644 --- a/hub/admin-web/src/routes/+layout.svelte +++ b/hub/admin-web/src/routes/+layout.svelte @@ -23,8 +23,6 @@ { key: 'members', label: '成员', icon: 'members' as const }, { key: 'teams', label: '团队', icon: 'teams' as const }, { key: 'projects', label: '项目', icon: 'projects' as const }, - { key: 'models', label: '模型', icon: 'models' as const }, - { key: 'roles', label: '角色', icon: 'roles' as const }, { key: 'provider', label: '供应方', icon: 'provider' as const } ]; diff --git a/hub/package.json b/hub/package.json index 03fbed8..e2c2116 100644 --- a/hub/package.json +++ b/hub/package.json @@ -35,14 +35,16 @@ "check": "tsc -p tsconfig.json --noEmit", "audit:production": "npm audit --omit=dev --audit-level=high", "prisma:generate": "prisma generate --schema prisma/schema.prisma", - "prisma:validate": "DATABASE_URL=${DATABASE_URL:-postgresql://stub:stub@127.0.0.1:5432/stub} prisma validate --schema prisma/schema.prisma", - "prisma:migrate": "DATABASE_URL=${DATABASE_URL:-postgresql://paradigm:paradigm@127.0.0.1:5432/paradigm} prisma migrate deploy --schema prisma/schema.prisma", + "prisma:validate": "prisma validate --schema prisma/schema.prisma", + "prisma:migrate": "prisma migrate deploy --schema prisma/schema.prisma", "secrets:rotate-kek": "node dist/deployment/rotate-secret-kek.js", "agent-config": "node dist/deployment/agent-config-cli.js", "silo:bootstrap": "node dist/deployment/bootstrap-silo-cli.js", "silo:restore-preflight": "node dist/deployment/restore-preflight.js", "deploy": "bash deploy/deploy_platform.sh", "test": "vitest run", - "test:watch": "vitest" + "test:watch": "vitest", + "admin:dev": "npm run dev --prefix admin-web", + "admin:build": "npm run build --prefix admin-web" } } diff --git a/hub/scripts/dev-bootstrap-silo.ts b/hub/scripts/dev-bootstrap-silo.ts new file mode 100644 index 0000000..d0f74bd --- /dev/null +++ b/hub/scripts/dev-bootstrap-silo.ts @@ -0,0 +1,76 @@ +/** + * Local dev bootstrap for an Alpha Silo on Windows / non-systemd hosts. + * + * The production bootstrap-silo CLI is Linux-only (requires root uid 0, + * systemctl, root-owned files). This script calls the same + * `bootstrapAlphaSilo` invariants directly, sourcing credentials from the + * local `.env` and the dev keyring. Idempotent: re-running is a no-op once + * the Silo Organization exists. + * + * Usage: npx tsx scripts/dev-bootstrap-silo.ts + */ +import "dotenv/config"; +import { PrismaClient } from "@prisma/client"; +import { bootstrapAlphaSilo } from "../src/deployment/bootstrap-silo.js"; +import { loadLocalSecretKeyring, LocalSecretEnvelope } from "../src/security/secretEnvelope.js"; + +function requiredEnv(name: string): string { + const v = process.env[name]?.trim(); + if (v === undefined || v === "") throw new Error(`missing required env: ${name}`); + return v; +} + +async function main(): Promise { + const databaseUrl = requiredEnv("DATABASE_URL"); + const keyring = await loadLocalSecretKeyring(); + const secrets = new LocalSecretEnvelope(keyring); + const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } }, log: [] }); + + const organizationId = requiredEnv("HUB_SILO_ORGANIZATION_ID"); + const feishuAppId = requiredEnv("FEISHU_APP_ID"); + const feishuAppSecret = requiredEnv("FEISHU_APP_SECRET"); + const feishuBotOpenId = requiredEnv("FEISHU_BOT_OPEN_ID"); + const providerAuthToken = requiredEnv("ANTHROPIC_AUTH_TOKEN"); + const providerBaseUrl = process.env["ANTHROPIC_BASE_URL"]?.trim() || "https://openrouter.ai/api"; + const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || ""; + + try { + const result = await bootstrapAlphaSilo( + prisma, + secrets, + { + organization: { + id: organizationId, + slug: "local-dev", + name: "Local Dev Silo", + }, + owner: { + openId: feishuBotOpenId, + displayName: "Local Dev Owner", + }, + feishu: { + appId: feishuAppId, + appSecret: feishuAppSecret, + botOpenId: feishuBotOpenId, + }, + provider: { + providerId: "openrouter", + baseUrl: providerBaseUrl, + authToken: providerAuthToken, + ...(anthropicApiKey !== "" ? { anthropicApiKey } : {}), + }, + }, + // Skip live network probes in local dev — Feishu/OpenRouter reachability + // is not required to seed the encrypted envelope rows. + { feishu: async () => {}, provider: async () => {} }, + ); + console.log("bootstrap result:", JSON.stringify(result, null, 2)); + } finally { + await prisma.$disconnect(); + } +} + +main().catch((error: unknown) => { + console.error("[dev-bootstrap-silo] failed:", error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}); diff --git a/hub/scripts/dev-seed-connections.ts b/hub/scripts/dev-seed-connections.ts new file mode 100644 index 0000000..158a098 --- /dev/null +++ b/hub/scripts/dev-seed-connections.ts @@ -0,0 +1,84 @@ +/** + * Local dev seed for an existing Organization that predates the ADR-0024 + * secret-envelope plane (e.g. the legacy `org_default` from the tenant-root + * migration). Creates ACTIVE Feishu Application + Provider (BYOK openrouter) + * connections with encrypted envelopes, using the local dev keyring. + * + * Idempotent: re-running rotates a new secret version if the connection + * already exists. + * + * Usage: npx tsx scripts/dev-seed-connections.ts + */ +import "dotenv/config"; +import { PrismaClient } from "@prisma/client"; +import { FeishuApplicationConnectionService } from "../src/connections/feishuApplicationConnections.js"; +import { ProviderConnectionService } from "../src/connections/providerConnections.js"; +import { loadLocalSecretKeyring, LocalSecretEnvelope } from "../src/security/secretEnvelope.js"; + +function requiredEnv(name: string): string { + const v = process.env[name]?.trim(); + if (v === undefined || v === "") throw new Error(`missing required env: ${name}`); + return v; +} + +async function main(): Promise { + const databaseUrl = requiredEnv("DATABASE_URL"); + const organizationId = requiredEnv("HUB_SILO_ORGANIZATION_ID"); + const keyring = await loadLocalSecretKeyring(); + const secrets = new LocalSecretEnvelope(keyring); + const prisma = new PrismaClient({ datasources: { db: { url: databaseUrl } }, log: [] }); + + const feishuAppId = requiredEnv("FEISHU_APP_ID"); + const feishuAppSecret = requiredEnv("FEISHU_APP_SECRET"); + const feishuBotOpenId = requiredEnv("FEISHU_BOT_OPEN_ID"); + const providerAuthToken = requiredEnv("ANTHROPIC_AUTH_TOKEN"); + const providerBaseUrl = process.env["ANTHROPIC_BASE_URL"]?.trim() || "https://openrouter.ai/api"; + const anthropicApiKey = process.env["ANTHROPIC_API_KEY"]?.trim() || ""; + + // Pick an existing active OWNER/ADMIN as the actor for the audit rows. + const actor = await prisma.organizationMembership.findFirst({ + where: { organizationId, role: { in: ["OWNER", "ADMIN"] }, revokedAt: null }, + select: { userId: true }, + }); + if (actor === null) throw new Error(`no active OWNER/ADMIN membership on ${organizationId}; seed a member first`); + const actorUserId = actor.userId; + console.log("actor:", actorUserId); + + try { + const feishuService = new FeishuApplicationConnectionService( + prisma, + secrets, + async () => {}, // skip live Feishu probe in local dev + ); + const feishuResult = await feishuService.rotateCustomerApplication({ + organizationId, + actorUserId, + appId: feishuAppId, + appSecret: feishuAppSecret, + botOpenId: feishuBotOpenId, + }); + console.log("feishu connection:", JSON.stringify(feishuResult, null, 2)); + + const providerService = new ProviderConnectionService( + prisma, + secrets, + async () => {}, // skip live OpenRouter probe in local dev + ); + const providerResult = await providerService.rotateByok({ + organizationId, + actorUserId, + providerId: "openrouter", + baseUrl: providerBaseUrl, + authToken: providerAuthToken, + ...(anthropicApiKey !== "" ? { anthropicApiKey } : {}), + }); + console.log("provider connection:", JSON.stringify(providerResult, null, 2)); + } finally { + await prisma.$disconnect(); + } +} + +main().catch((error: unknown) => { + console.error("[dev-seed-connections] failed:", error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +});