Files
curriculum-project-hub/hub/deploy
6990082247 build(deploy): 部署与限流配置切换到 filelib-web,并加共存回归测试
三处引用旧工程名/旧资源路径的地方一并更新,它们必须同时改 —— 少改一处
就是静默故障,而不是构建期报错:

1. 部署脚本(deploy_platform.sh / deploy_fleet_release.sh):npm ci 的
   prefix、rsync 排除项、构建产物存在性检查从 database-admin 换成
   filelib-web。最后一项是真门禁:static.ts 缺产物时只 warn 不注册路由,
   漏改会让 /app 与 /database 静默 404 —— 恰是 database-admin 长期处于
   禁用状态的原因。

2. silo 限流豁免:资源路径随 appDir 改名而变(/database/_app/* 已不存在,
   现为 /_filelib/*);/app/* 此前不在豁免列表,它现在也是 SPA 外壳,
   客户端路由无法预先枚举。
   注:/database/* 是整体豁免,filelib 的 JSON API 也绕过限流预算。这是
   迁移前就有的行为,原样保留,但覆盖面因多了 /app/* 而变宽。

3. 回归测试:把 registerStaticSpa 与 registerDatabaseSpa 挂到同一个
   Fastify 实例,断言 ready() 不因重复路由抛错 —— appDir 若用回默认的
   _app,这里会红(ADR-0029 的承重约束)。另断言 /app 与
   /database/dashboard/users 返回同一份字节(SPA 回退不读请求)、body 含
   /_filelib/。构建产物缺失时不 skip 而是直接失败:那说明该先跑
   filelib-web 的 build,不是测试不适用。
2026-07-26 20:23:15 +08:00
..

Alpha Silo service installation

For a brand-new Organization, start with the repeatable wizard and end-to-end operator runbook:

bash hub/deploy/new_silo.sh

See NEW_SILO_RUNBOOK.md. The remainder of this document describes the individual installer and maintenance primitives used by the generated bundle.

CI: roll Hub + admin SPA by fleet (educraft / educraft-dev)

.gitea/workflows/deploy-admin.yml deploys Hub releases (including the org-admin SPA) to the managed Alpha host via deploy_fleet_release.sh. Fleets share the host and are selected by the middle DNS label of HUB_PUBLIC_BASE_URL:

Fleet Domain CI trigger
dev https://<slug>.educraft-dev.paradigm-edu.net every push + PR (paths under hub/)
prod https://<slug>.educraft.paradigm-edu.net push to main + tags

Example prod tenant: https://para-26071100.educraft.paradigm-edu.net/auth/feishu/para-26071100.

Required Gitea secret: DEPLOY_SSH_KEY (private key for root@39.107.254.4). Optional: DEPLOY_HOST / DEPLOY_USER / DEPLOY_SSH_PORT / DEPLOY_BASE. Set repository var ALLOW_EMPTY_FLEET=1 until the first silo exists for a fleet.

Local dry-run against the host:

CPH_FLEET=dev \
  PLATFORM_DEPLOY_SSH_KEY=$HOME/.ssh/id_ed25519 \
  bash hub/deploy/deploy_fleet_release.sh

The supervised alpha runs one Organization per named Silo. The supported host has systemd, PostgreSQL, Node.js 24+, pg_isready, runuser, setpriv, bubblewrap, socat, pg_dump, tar, sha256sum, and a compatible cph.

Each Silo needs its own logical database/role, instance id, service user, environment, keyring, workspace, port, domain, and host filesystem quota. Application code lives in immutable versioned directories under /srv/curriculum-project-hub/releases/; releases contain no customer state.

Install an instance

sudo BASE=/srv/curriculum-project-hub \
  HUB_DIR=/srv/curriculum-project-hub/releases/<release-id>/hub \
  INSTANCE_ID=org-a \
  WORKSPACE_ROOT=/w/997 \
  PORT=8788 \
  MEMORY_MAX=16G CPU_QUOTA=400% TASKS_MAX=512 \
  bash /srv/curriculum-project-hub/releases/<release-id>/hub/deploy/install_service.sh

The first invocation creates root-owned 0600 files below /srv/curriculum-project-hub/.secrets/org-a/ and exits 78. Copy the keyring to a separately protected recovery destination and fill every blank in platform.env. Before rerunning the installer, migrate the empty Silo database:

sudo bash -c '
  set -euo pipefail
  set -a; . /srv/curriculum-project-hub/.secrets/org-a/platform.env; set +a
  node /srv/curriculum-project-hub/releases/<release-id>/hub/node_modules/prisma/build/index.js \
    migrate deploy \
    --schema /srv/curriculum-project-hub/releases/<release-id>/hub/prisma/schema.prisma
'

Then rerun the installer so it can complete preflight and install the stopped unit. Resource numbers above are examples, not defaults; the operator must set measured ceilings explicitly.

The installer refuses overlapping release/persistent paths, root service users, missing Node/cph/PostgreSQL/bubblewrap prerequisites, invalid credentials, and unreachable database roles. The installed unit is cph-hub-org-a.service and uses systemd LoadCredential; the service user cannot read the source keyring.

Default state paths are:

/var/lib/cph-hub/org-a/home
/var/lib/cph-hub/org-a/state
/w/997
/var/cache/cph-hub/org-a

Organization Agent roles and skills are runtime configuration. Skill versions are stored below the Silo state directory (state/skills) and are included in backup_silo.sh as agent-skills.tar; PostgreSQL stores role bundles, skill metadata and role-to-skill selection. Operate them as the Silo service user so content ownership remains correct:

sudo INSTANCE_ID=org-a \
  ENV_FILE=/srv/curriculum-project-hub/.secrets/org-a/platform.env \
  bash hub/deploy/agent_config.sh install-skill \
  --organization org-a --source /staging/typst --version 1
sudo INSTANCE_ID=org-a \
  ENV_FILE=/srv/curriculum-project-hub/.secrets/org-a/platform.env \
  bash hub/deploy/agent_config.sh upsert-role \
  --organization org-a --role draft --label 草稿 --tools-json null
sudo INSTANCE_ID=org-a \
  ENV_FILE=/srv/curriculum-project-hub/.secrets/org-a/platform.env \
  bash hub/deploy/agent_config.sh set-role-skills \
  --organization org-a --role draft --skills outline,lesson-project,typst
sudo INSTANCE_ID=org-a \
  ENV_FILE=/srv/curriculum-project-hub/.secrets/org-a/platform.env \
  bash hub/deploy/agent_config.sh list --organization org-a

--tools-json null means the full registered tool surface; [] means no ordinary tools. SDK-bundled skills and workspace/user setting sources remain disabled regardless of runtime configuration.

Bootstrap the only Organization

Prepare a root-owned 0600 JSON file containing organization, owner, feishu, provider, and optionally teams. Secrets never appear in command-line arguments.

{
  "organization": { "id": "org_a", "slug": "org-a", "name": "Org A" },
  "owner": { "openId": "ou_owner", "displayName": "Owner" },
  "feishu": {
    "appId": "cli_xxx",
    "appSecret": "...",
    "botOpenId": "ou_bot"
  },
  "provider": {
    "providerId": "openrouter",
    "baseUrl": "https://openrouter.ai/api",
    "authToken": "..."
  },
  "teams": [{ "slug": "teachers", "name": "Teachers" }]
}
sudo bash -c '
  set -euo pipefail
  set -a; . /srv/curriculum-project-hub/.secrets/org-a/platform.env; set +a
  node /srv/curriculum-project-hub/releases/<release-id>/hub/dist/deployment/bootstrap-silo-cli.js \
    --config-file /root/org-a-bootstrap.json \
    --keyring-file /srv/curriculum-project-hub/.secrets/org-a/secret-keyring.json
'

Bootstrap encrypts and activates both connection records and is rerunnable when provider configuration fails after Organization creation. Hub startup refuses to become healthy unless the sole Organization, active provider, active Feishu application, and Feishu listener are ready. Delete the bootstrap input after the recovery copy is current. Production has no process-global Feishu/provider credential fallback.

Start and rotate

sudo systemctl start cph-hub-org-a.service
sudo systemctl status cph-hub-org-a.service

KEK rotation requires the named unit to be stopped. Source its instance env so HUB_SYSTEMD_UNIT and DATABASE_URL identify the same Silo:

sudo bash -c '
  set -euo pipefail
  systemctl stop cph-hub-org-a.service
  set -a; . /srv/curriculum-project-hub/.secrets/org-a/platform.env; set +a
  node /srv/curriculum-project-hub/hub/dist/deployment/rotate-secret-kek.js \
    --keyring-file /srv/curriculum-project-hub/.secrets/org-a/secret-keyring.json
  systemctl start cph-hub-org-a.service
'

Stop the unit and run backup_silo.sh with distinct root-owned destinations:

sudo INSTANCE_ID=org-a \
  ENV_FILE=/srv/curriculum-project-hub/.secrets/org-a/platform.env \
  KEYRING_FILE=/srv/curriculum-project-hub/.secrets/org-a/secret-keyring.json \
  BUSINESS_BACKUP_DIR=/backup/business \
  RECOVERY_BACKUP_DIR=/separate-recovery \
  bash hub/deploy/backup_silo.sh

The business set contains the PostgreSQL custom dump, workspace archive and agent-skills.tar. The separate recovery set contains the keyring and environment. Both include checksums; neither destination may be the live host's only disk.

Restore into a separate drill database, workspace and skill-store directory; verify checksums before extracting both tar archives, then run:

set -a; . /path/to/restored/platform.env; set +a
mkdir -p "$HUB_PROJECT_WORKSPACE_ROOT" "$HUB_SKILL_STORE_ROOT"
tar -xf /path/to/business/workspaces.tar -C "$HUB_PROJECT_WORKSPACE_ROOT"
tar -xf /path/to/business/agent-skills.tar -C "$HUB_SKILL_STORE_ROOT"
node hub/dist/deployment/restore-preflight.js \
  --keyring-file /path/to/restored/secret-keyring.json
sudo INSTANCE_ID=org-a ENV_FILE=/path/to/restored/platform.env \
  HUB_DIR=/path/to/restored/release/hub \
  bash hub/deploy/agent_config.sh verify-store --organization org-a

Traffic stays disabled until the sole Organization and every Feishu/provider envelope authenticate, the workspace exists, and an end-to-end test run passes.

For the first supervised demo, install → bootstrap → start → health check is the deployment gate. A completed off-host restore drill is an Alpha hardening item, not a prerequisite for bringing up that first controlled Silo.

The default bind remains loopback; expose it only through a TLS reverse proxy. The platform admin surface is not part of the alpha and must not be exposed.