@rizom/ops 0.2.0-alpha.45 → 0.2.0-alpha.450

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/README.md +82 -1
  2. package/dist/age-key-bootstrap.d.ts +1 -1
  3. package/dist/brains-ops.js +736 -335
  4. package/dist/capability-bundle-migration.d.ts +22 -0
  5. package/dist/cert-bootstrap.d.ts +4 -2
  6. package/dist/content-repo-ref.d.ts +10 -0
  7. package/dist/content-repo.d.ts +1 -1
  8. package/dist/deploy.js +110 -166
  9. package/dist/directory-sync-stress-system.d.ts +75 -0
  10. package/dist/directory-sync-stress.d.ts +117 -0
  11. package/dist/entries/deploy.d.ts +4 -2
  12. package/dist/health-watchdog-smoke.d.ts +54 -0
  13. package/dist/image-inventory.d.ts +4 -0
  14. package/dist/image-types.d.ts +7 -0
  15. package/dist/images.d.ts +82 -0
  16. package/dist/index.d.ts +8 -0
  17. package/dist/index.js +736 -323
  18. package/dist/legacy-pilot-migration.d.ts +11 -0
  19. package/dist/legacy-projection-job-recovery.d.ts +29 -0
  20. package/dist/load-registry.d.ts +61 -6
  21. package/dist/observed-status.d.ts +1 -1
  22. package/dist/parse-args.d.ts +2 -12
  23. package/dist/preview-domain.d.ts +9 -0
  24. package/dist/reconcile-dry-run.d.ts +9 -0
  25. package/dist/run-command.d.ts +20 -4
  26. package/dist/schema.d.ts +126 -163
  27. package/dist/secrets-encrypt.d.ts +7 -13
  28. package/dist/secrets-push.d.ts +1 -1
  29. package/dist/ssh-key-bootstrap.d.ts +1 -26
  30. package/dist/stage-legacy-crossover.d.ts +25 -0
  31. package/dist/stress-command.d.ts +14 -0
  32. package/dist/stress-git-checkout.d.ts +21 -0
  33. package/dist/stress-health-monitor.d.ts +46 -0
  34. package/dist/upgrade.d.ts +10 -0
  35. package/dist/user-offboard.d.ts +61 -0
  36. package/dist/verify-user.d.ts +22 -0
  37. package/package.json +50 -42
  38. package/templates/rover-pilot/.env.schema +20 -5
  39. package/templates/rover-pilot/.github/actions/varlock-env/action.yml +47 -0
  40. package/templates/rover-pilot/.github/workflows/build.yml +70 -17
  41. package/templates/rover-pilot/.github/workflows/deploy.yml +74 -57
  42. package/templates/rover-pilot/.github/workflows/directory-sync-stress.yml +119 -0
  43. package/templates/rover-pilot/.github/workflows/health-watchdog-smoke.yml +95 -0
  44. package/templates/rover-pilot/.github/workflows/offboard.yml +107 -0
  45. package/templates/rover-pilot/.github/workflows/reconcile.yml +10 -4
  46. package/templates/rover-pilot/.github/workflows/upgrade.yml +104 -0
  47. package/templates/rover-pilot/README.md +13 -5
  48. package/templates/rover-pilot/deploy/scripts/create-predeploy-backup.ts +936 -0
  49. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +80 -24
  50. package/templates/rover-pilot/deploy/scripts/helpers.ts +2 -0
  51. package/templates/rover-pilot/deploy/scripts/install-health-watchdog.ts +144 -0
  52. package/templates/rover-pilot/deploy/scripts/provision-server.ts +51 -21
  53. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +4 -0
  54. package/templates/rover-pilot/deploy/scripts/resolve-missing-images.ts +13 -0
  55. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +40 -9
  56. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +51 -47
  57. package/templates/rover-pilot/deploy/scripts/update-dns.ts +72 -16
  58. package/templates/rover-pilot/deploy/scripts/validate-secrets.ts +12 -1
  59. package/templates/rover-pilot/deploy/scripts/verify-runtime-image.ts +18 -0
  60. package/templates/rover-pilot/docs/canonical-crossover-record.md +107 -0
  61. package/templates/rover-pilot/docs/onboarding-checklist.md +23 -14
  62. package/templates/rover-pilot/docs/operator-playbook.md +357 -29
  63. package/templates/rover-pilot/docs/user-onboarding.md +48 -463
  64. package/templates/rover-pilot/package.json +1 -0
  65. package/templates/rover-pilot/pilot.yaml +6 -4
  66. package/dist/origin-ca.d.ts +0 -1
  67. package/dist/push-secrets.d.ts +0 -9
  68. package/dist/push-target.d.ts +0 -2
  69. package/dist/run-subprocess.d.ts +0 -6
  70. package/templates/rover-pilot/.kamal/hooks/pre-deploy +0 -9
@@ -13,23 +13,81 @@ const baseUrl = "https://api.cloudflare.com/client/v4";
13
13
 
14
14
  interface CloudflareResult {
15
15
  success: boolean;
16
+ }
17
+
18
+ interface CloudflareRecordList extends CloudflareResult {
16
19
  result?: Array<{ id: string }>;
17
20
  }
18
21
 
19
- async function upsertRecord(name: string): Promise<void> {
20
- const lookupUrl = `${baseUrl}/zones/${zoneId}/dns_records?type=A&name=${encodeURIComponent(name)}`;
22
+ /**
23
+ * Copied verbatim into generated projects, so this stays dependency-free and
24
+ * checks the payload by hand. readJsonResponse returns unknown; asserting a
25
+ * response type onto it would let malformed error bodies pass silently.
26
+ */
27
+ function isRecord(value: unknown): value is Record<string, unknown> {
28
+ return typeof value === "object" && value !== null && !Array.isArray(value);
29
+ }
30
+
31
+ function readCloudflareSuccess(payload: unknown): boolean | undefined {
32
+ if (!isRecord(payload)) return undefined;
33
+ const success = payload["success"];
34
+ return typeof success === "boolean" ? success : undefined;
35
+ }
36
+
37
+ function readCloudflareRecordList(
38
+ payload: unknown,
39
+ ): CloudflareRecordList | undefined {
40
+ const success = readCloudflareSuccess(payload);
41
+ if (success === undefined) return undefined;
42
+ if (!success) return { success };
43
+ if (!isRecord(payload)) return undefined;
44
+ const result = payload["result"];
45
+ if (
46
+ !Array.isArray(result) ||
47
+ !result.every(
48
+ (entry): entry is { id: string } =>
49
+ isRecord(entry) && typeof entry["id"] === "string",
50
+ )
51
+ ) {
52
+ return undefined;
53
+ }
54
+ return { success, result };
55
+ }
56
+
57
+ function readCloudflareMutation(
58
+ payload: unknown,
59
+ ): CloudflareResult | undefined {
60
+ const success = readCloudflareSuccess(payload);
61
+ if (success === undefined) return undefined;
62
+ if (!success) return { success };
63
+ if (!isRecord(payload)) return undefined;
64
+ const result = payload["result"];
65
+ if (!isRecord(result) || typeof result["id"] !== "string") return undefined;
66
+ return { success };
67
+ }
68
+
69
+ async function findRecordId(
70
+ name: string,
71
+ type: "A" | "CNAME",
72
+ ): Promise<string | undefined> {
73
+ const lookupUrl = `${baseUrl}/zones/${zoneId}/dns_records?type=${type}&name=${encodeURIComponent(name)}`;
21
74
  const lookup = await fetch(lookupUrl, { headers });
22
- const payload = (await readJsonResponse(
23
- lookup,
24
- "Cloudflare DNS lookup",
25
- )) as CloudflareResult;
26
- if (!lookup.ok || !payload.success) {
27
- throw new Error(`Cloudflare DNS lookup failed: ${JSON.stringify(payload)}`);
75
+ const raw = await readJsonResponse(lookup, "Cloudflare DNS lookup");
76
+ const payload = readCloudflareRecordList(raw);
77
+ if (!lookup.ok || !payload?.success) {
78
+ throw new Error(`Cloudflare DNS lookup failed: ${JSON.stringify(raw)}`);
28
79
  }
29
80
 
30
- const existing = payload.result?.[0];
81
+ return payload.result?.[0]?.id;
82
+ }
83
+
84
+ async function upsertRecord(name: string): Promise<void> {
85
+ // Prefer an existing A record. If the hostname currently has a CNAME,
86
+ // replace that CNAME in-place so deploys can claim legacy www aliases.
87
+ const existing =
88
+ (await findRecordId(name, "A")) ?? (await findRecordId(name, "CNAME"));
31
89
  const url = existing
32
- ? `${baseUrl}/zones/${zoneId}/dns_records/${existing.id}`
90
+ ? `${baseUrl}/zones/${zoneId}/dns_records/${existing}`
33
91
  : `${baseUrl}/zones/${zoneId}/dns_records`;
34
92
 
35
93
  const response = await fetch(url, {
@@ -43,12 +101,10 @@ async function upsertRecord(name: string): Promise<void> {
43
101
  proxied: true,
44
102
  }),
45
103
  });
46
- const result = (await readJsonResponse(
47
- response,
48
- "Cloudflare DNS upsert",
49
- )) as CloudflareResult;
50
- if (!response.ok || !result.success) {
51
- throw new Error(`Cloudflare DNS upsert failed: ${JSON.stringify(result)}`);
104
+ const raw = await readJsonResponse(response, "Cloudflare DNS upsert");
105
+ const result = readCloudflareMutation(raw);
106
+ if (!response.ok || !result?.success) {
107
+ throw new Error(`Cloudflare DNS upsert failed: ${JSON.stringify(raw)}`);
52
108
  }
53
109
  }
54
110
 
@@ -4,7 +4,7 @@ import { parseEnvSchema } from "./helpers";
4
4
  const envSchemaPath = ".env.schema";
5
5
  const schema = parseEnvSchema(readFileSync(envSchemaPath, "utf8"));
6
6
  const requiredKeys = schema
7
- .filter((entry) => entry.required)
7
+ .filter((entry) => entry.required && entry.key !== "BWS_ACCESS_TOKEN")
8
8
  .map((entry) => entry.key);
9
9
 
10
10
  const missing: string[] = [];
@@ -14,6 +14,17 @@ for (const key of requiredKeys) {
14
14
  }
15
15
  }
16
16
 
17
+ const discordKeys = [
18
+ "DISCORD_BOT_TOKEN",
19
+ "DISCORD_PUBLIC_KEY",
20
+ "DISCORD_APPLICATION_ID",
21
+ ];
22
+ if (discordKeys.some((key) => process.env[key])) {
23
+ for (const key of discordKeys) {
24
+ if (!process.env[key]) missing.push(key);
25
+ }
26
+ }
27
+
17
28
  if (missing.length > 0) {
18
29
  throw new Error(`Missing required secrets: ${missing.join(", ")}`);
19
30
  }
@@ -0,0 +1,18 @@
1
+ import { loadPilotRegistry } from "@rizom/ops";
2
+ import {
3
+ requireEnv,
4
+ runtimeImageTag,
5
+ sitePackagesFor,
6
+ verifyRuntimeImage,
7
+ } from "@rizom/ops/deploy";
8
+
9
+ const handle = requireEnv("HANDLE");
10
+ const registry = await loadPilotRegistry(process.cwd());
11
+ const user = registry.users.find((entry) => entry.handle === handle);
12
+ if (!user) throw new Error(`Unknown fleet handle: ${handle}`);
13
+
14
+ await verifyRuntimeImage(`ghcr.io/${requireEnv("GITHUB_REPOSITORY")}`, {
15
+ tag: runtimeImageTag(user.brainVersion),
16
+ brainVersion: user.brainVersion,
17
+ sitePackages: sitePackagesFor(user.siteOverride),
18
+ });
@@ -0,0 +1,107 @@
1
+ # Canonical Crossover Approval Record
2
+
3
+ > Draft evidence record only. Completing this file does not authorize a merge, publish, reconcile, or deployment.
4
+
5
+ Do not include secret values, private keys, access tokens, or decrypted user configuration.
6
+
7
+ ## Authorization and freeze
8
+
9
+ - Operator:
10
+ - Approval reference:
11
+ - Approved window:
12
+ - Freeze start:
13
+ - Build workflow disabled and idle:
14
+ - Reconcile workflow disabled and idle:
15
+ - Deploy workflow disabled and idle:
16
+
17
+ ## Reviewed inputs
18
+
19
+ - `brains` crossover commit:
20
+ - Private-pilot source commit used for staging:
21
+ - Canonical review commit:
22
+ - Secret-free review diff SHA-256:
23
+ - Identity-review evidence SHA-256:
24
+ - Reviewed source-to-target bundle manifest SHA-256:
25
+ - Reviewed hosted-site pin manifest SHA-256:
26
+ - Source and review worktrees clean:
27
+
28
+ ## Hosted site and theme pins
29
+
30
+ List every hosted site. External site and theme package versions must be exact and must
31
+ match the staged user desired state; do not infer them from the brain or from each other.
32
+
33
+ | Handle | Site package | Exact site version | Theme package | Exact theme version | Package/image evidence |
34
+ | ------ | ------------ | ------------------ | ------------- | ------------------- | ---------------------- |
35
+ | | | | | | |
36
+
37
+ ## Forward artifact pins
38
+
39
+ | Artifact | Exact version | Registry integrity or digest | Verified installable |
40
+ | ---------------------- | ------------- | ---------------------------- | -------------------- |
41
+ | `@rizom/brain` | | | |
42
+ | `@rizom/ops` | | | |
43
+ | private-pilot lockfile | n/a | | |
44
+
45
+ Record every image. Tags alone are not immutable evidence.
46
+
47
+ | Cohort/handle | Config commit | Image repository | Image tag | Image digest |
48
+ | ------------- | ------------- | ---------------- | --------- | ------------ |
49
+ | | | | | |
50
+
51
+ ## Rollback pair
52
+
53
+ - Prior private-pilot commit:
54
+ - Prior `@rizom/ops` version and registry integrity:
55
+
56
+ | Cohort/handle | Prior config commit | Prior image repository | Prior image tag | Prior image digest |
57
+ | ------------- | ------------------- | ---------------------- | --------------- | ------------------ |
58
+ | | | | | |
59
+
60
+ ## Identity review
61
+
62
+ - Repository and image names unchanged or explicitly approved:
63
+ - GitHub organization and content repository identities unchanged:
64
+ - Server, domain, Cloudflare zone, and ATProto identities unchanged:
65
+ - Secret selector names and encrypted secret artifacts unchanged:
66
+ - Per-user runtime-version and site-package tag inputs reviewed:
67
+ - No plaintext source secrets copied into the review artifact:
68
+ - Every generated config declares `bundleContract: capability-bundles-v1`:
69
+ - Pilot and explicit cohort source selections exactly matched the reviewed manifest:
70
+ - No removed model, preset, or old-format schema discriminator remains:
71
+ - Every external hosted site and theme package has an exact reviewed pin:
72
+
73
+ ## Offline convergence
74
+
75
+ Command:
76
+
77
+ ```sh
78
+ bunx brains-ops reconcile-all <canonical-review-copy> --dry-run
79
+ ```
80
+
81
+ - First-pass reconciler-owned changed files:
82
+ - Second-pass reconciler-owned changed files (must be zero):
83
+ - Observational `views/users.md` unchanged by reconciliation:
84
+ - Review copy unchanged:
85
+ - External content-repository access blocked:
86
+
87
+ ## Pre-window validation
88
+
89
+ - Package tests, typecheck, lint, build, and packed-consumer startup:
90
+ - Architecture and dependency boundaries:
91
+ - Environment-schema and workspace checks:
92
+ - Crossover migration and comment preservation:
93
+ - Canary-first order and health checks reviewed:
94
+ - Paired rollback reviewed:
95
+
96
+ ## Execution record
97
+
98
+ Complete only inside the explicitly approved maintenance window.
99
+
100
+ - Unified packages published and verified:
101
+ - Canonical desired state committed while automation remained frozen:
102
+ - Image digest set matched this record:
103
+ - First reconcile reviewed:
104
+ - Per-instance health, MCP authorization, identity, content, and site checks:
105
+ - Second reconcile produced zero reconciler-owned drift and no deploy work:
106
+ - Freeze lifted:
107
+ - One-week soak start:
@@ -4,14 +4,18 @@
4
4
  2. Run `bunx brains-ops age-key:bootstrap <repo> --push-to gh`.
5
5
  3. Fill in `pilot.yaml`.
6
6
  - keep your pinned `brainVersion`
7
- - confirm shared selectors for `aiApiKey`, `gitSyncToken`, `contentRepoAdminToken`, and `mcpAuthToken`
7
+ - confirm shared selectors for `aiApiKey`, `gitSyncToken`, and `contentRepoAdminToken`
8
8
  - use different tokens for `contentRepoAdminToken` and `gitSyncToken`: admin creates/checks content repos; sync is used by runtime directory-sync
9
9
  - confirm `agePublicKey`
10
10
  4. Run `bunx brains-ops user:add <repo> <handle> --cohort <cohort>`.
11
- - Discord is enabled by default for pilot users.
12
- - if the user should be an anchor there, add `--anchor-id <discord-user-id>`.
11
+ - Web chat is the primary interface; it needs no per-user setup beyond the passkey.
12
+ - `user:add` currently writes `discord: enabled: true`; set it to `false` unless the user's cohort actually uses Discord.
13
+ - if the user should be an anchor on Discord, add `--anchor-id <discord-user-id>`.
13
14
  - the command creates `users/<handle>.yaml`, `users/<handle>.secrets.yaml`, and the cohort membership without duplicating existing entries.
14
15
  5. Edit the generated user file if the anchor profile needs richer metadata.
16
+ - Set `setup.delivery: email` and `setup.email` so the user gets the passkey setup email — this is the default onboarding path.
17
+ - For ATProto publishing, add `atproto.identifier` to the user file; put only `atprotoAppPassword` in the per-user secrets file.
18
+ - Ensure `SETUP_EMAIL_API_KEY` and `SETUP_EMAIL_FROM` exist as GitHub Secrets before deploying any email-setup user.
15
19
  6. Run `bunx brains-ops render <repo>`.
16
20
  7. Run `bunx brains-ops ssh-key:bootstrap <repo> --push-to gh`.
17
21
  8. Run `bunx brains-ops cert:bootstrap <repo> --push-to gh`.
@@ -19,15 +23,20 @@
19
23
  10. Run `bunx brains-ops secrets:encrypt <repo> <handle>`.
20
24
  11. Commit and push `users/<handle>.secrets.yaml.age`.
21
25
  12. Run `bunx brains-ops onboard <repo> <handle>`.
22
- 13. Verify the deployed rover core contract:
23
- - `https://<handle>.rizom.ai/health` returns `200`
24
- - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns `401`
25
- 14. For fleet upgrades, edit `pilot.yaml.brainVersion` and push once; CI rebuilds the shared image tag, refreshes generated user env files, and redeploys affected users.
26
- 15. Hand the Discord setup details to the user.
27
- 16. Hand over the browser defaults:
26
+ 13. Verify the deployed canonical contract:
27
+ - `https://<handle>.rizom.ai/health/operate` returns `200`
28
+ - `https://<handle>.rizom.ai/chat` loads the web chat and accepts passkey sign-in
29
+ - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns the expected auth failure
30
+ - content repo exists and runtime sync is healthy
31
+ - background jobs are not repeatedly failing, except for expected missing optional integrations
32
+ - when `site` is selected, the browser/Studio surfaces load and the initial app-managed site build completes
33
+ 14. For fleet upgrades, edit `pilot.yaml.brainVersion` and push once; CI rebuilds the required default/site image tags, refreshes generated user env files, and redeploys affected users. Every external site and theme package keeps its own required exact version pin and never follows the brain version implicitly.
34
+ 15. Confirm the user received the setup email, registered their passkey, and can sign in to web chat at `https://<handle>.rizom.ai/chat`. That completes the default onboarding; everything below is per-cohort extras.
35
+ 16. Hand over the browser surfaces:
36
+ - Chat (primary): `https://<handle>.rizom.ai/chat`
28
37
  - Dashboard: `https://<handle>.rizom.ai/`
29
- - CMS: `https://<handle>.rizom.ai/cms`
30
- - GitHub token guidance for CMS access to the user's private content repo
31
- 17. If they need direct client access, also hand over the MCP connection details.
32
- 18. If you are also giving them a content repo workflow, describe it as optional and frame git/Obsidian as an advanced file-based path, not the default.
33
- 19. Send `docs/user-onboarding.md` to the user as the pilot handoff guide.
38
+ - Studio: `https://<handle>.rizom.ai/studio`, plus GitHub token guidance if Studio editing is part of their cohort
39
+ 17. For Discord-enabled cohorts, hand the Discord setup details to the user as a secondary chat surface.
40
+ 18. If they need direct client access (MCP), use OAuth/passkey-capable clients where possible.
41
+ 19. If you are also giving them a content repo workflow, describe it as optional and frame git/Obsidian as an advanced file-based path, not the default.
42
+ 20. Send `docs/user-onboarding.md` to the user as the pilot handoff guide.