Files
curriculum-project-hub/hub/deploy
hongjr03 78f94fcc8c fix(ci): sync npm lockfiles so fleet deploy npm ci succeeds
Hub and admin-web package-lock.json were missing @emnapi/* entries that
npm 11 on the Alpha host requires, so deploy_fleet_release failed at
npm ci. Regenerate both lockfiles and retry incomplete release trees.
2026-07-15 00:18:22 +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.