@rizom/ops 0.2.0-alpha.0

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 (38) hide show
  1. package/README.md +24 -0
  2. package/dist/brains-ops.js +174 -0
  3. package/dist/default-user-runner.d.ts +3 -0
  4. package/dist/deploy.js +171 -0
  5. package/dist/entries/deploy.d.ts +2 -0
  6. package/dist/entrypoint.d.ts +2 -0
  7. package/dist/index.d.ts +8 -0
  8. package/dist/index.js +173 -0
  9. package/dist/init.d.ts +1 -0
  10. package/dist/load-registry.d.ts +46 -0
  11. package/dist/onboard-user.d.ts +2 -0
  12. package/dist/parse-args.d.ts +9 -0
  13. package/dist/reconcile-all.d.ts +2 -0
  14. package/dist/reconcile-cohort.d.ts +2 -0
  15. package/dist/reconcile-lib.d.ts +16 -0
  16. package/dist/render-users-table.d.ts +5 -0
  17. package/dist/run-command.d.ts +11 -0
  18. package/dist/schema.d.ts +86 -0
  19. package/dist/user-runner.d.ts +6 -0
  20. package/dist/user-secret-names.d.ts +6 -0
  21. package/package.json +65 -0
  22. package/templates/rover-pilot/.env.schema +54 -0
  23. package/templates/rover-pilot/.github/workflows/build.yml +63 -0
  24. package/templates/rover-pilot/.github/workflows/deploy.yml +261 -0
  25. package/templates/rover-pilot/.github/workflows/reconcile.yml +45 -0
  26. package/templates/rover-pilot/.kamal/hooks/pre-deploy +9 -0
  27. package/templates/rover-pilot/README.md +42 -0
  28. package/templates/rover-pilot/cohorts/cohort-1.yaml +2 -0
  29. package/templates/rover-pilot/deploy/Dockerfile +15 -0
  30. package/templates/rover-pilot/deploy/kamal/deploy.yml +39 -0
  31. package/templates/rover-pilot/deploy/scripts/helpers.ts +10 -0
  32. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +59 -0
  33. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +49 -0
  34. package/templates/rover-pilot/docs/onboarding-checklist.md +10 -0
  35. package/templates/rover-pilot/docs/operator-playbook.md +47 -0
  36. package/templates/rover-pilot/package.json +9 -0
  37. package/templates/rover-pilot/pilot.yaml +8 -0
  38. package/templates/rover-pilot/users/alice.yaml +3 -0
@@ -0,0 +1,45 @@
1
+ name: Reconcile
2
+
3
+ on:
4
+ workflow_dispatch:
5
+ push:
6
+ branches: ["main"]
7
+ paths:
8
+ - pilot.yaml
9
+ - cohorts/**
10
+ - users/*.yaml
11
+ - .github/workflows/reconcile.yml
12
+
13
+ permissions:
14
+ contents: write
15
+
16
+ concurrency:
17
+ group: reconcile
18
+ cancel-in-progress: true
19
+
20
+ jobs:
21
+ reconcile:
22
+ runs-on: ubuntu-latest
23
+ steps:
24
+ - uses: actions/checkout@v5
25
+ with:
26
+ ref: ${{ github.sha }}
27
+
28
+ - uses: oven-sh/setup-bun@v2
29
+
30
+ - name: Install operator tooling
31
+ run: bun install
32
+
33
+ - name: Reconcile generated pilot outputs
34
+ run: bunx brains-ops reconcile-all "$GITHUB_WORKSPACE"
35
+
36
+ - name: Commit generated outputs
37
+ run: |
38
+ if git diff --quiet -- views users; then
39
+ exit 0
40
+ fi
41
+ git config user.name "github-actions[bot]"
42
+ git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
43
+ git add views users
44
+ git commit -m "chore(ops): reconcile pilot outputs"
45
+ git push
@@ -0,0 +1,9 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+
4
+ BRAIN_FILE="${BRAIN_YAML_PATH:-brain.yaml}"
5
+ SSH_USER="$(ruby -e 'require "yaml"; config = YAML.load_file("deploy/kamal/deploy.yml") || {}; puts(config.dig("ssh", "user") || "root")')"
6
+ IFS=',' read -ra HOSTS <<< "$KAMAL_HOSTS"
7
+ for host in "${HOSTS[@]}"; do
8
+ scp -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null "$BRAIN_FILE" "${SSH_USER}@${host}:/opt/brain.yaml"
9
+ done
@@ -0,0 +1,42 @@
1
+ # rover-pilot
2
+
3
+ Private desired-state repo for the rover pilot.
4
+
5
+ This is a single operator-owned repo. Pilot users do not get their own brain repos.
6
+ Per-user deploy config lives under `users/<handle>/`, while content stays in per-user content repos.
7
+
8
+ ## Operator tooling
9
+
10
+ This repo pins `@rizom/ops` in `package.json`.
11
+
12
+ Install it with:
13
+
14
+ ```sh
15
+ bun install
16
+ ```
17
+
18
+ Then run commands with:
19
+
20
+ ```sh
21
+ bunx brains-ops <command>
22
+ ```
23
+
24
+ The repo also checks in its deploy contract:
25
+
26
+ - `.env.schema`
27
+ - `deploy/kamal/deploy.yml`
28
+ - `deploy/scripts/`
29
+ - `.github/workflows/*`
30
+
31
+ `.env.schema` is the single source of truth for required and sensitive deploy vars.
32
+ The shared pilot image tag is `brain-${brainVersion}` end to end.
33
+ When `pilot.yaml.brainVersion` changes and you push, CI rebuilds the shared tag, refreshes generated user env files, and redeploys affected users.
34
+ When a push changes only deploy contract files, CI prints `No affected user configs; skipping deploy.` and stops before Kamal.
35
+
36
+ ## Commands
37
+
38
+ - `brains-ops init <repo>`
39
+ - `brains-ops render <repo>`
40
+ - `brains-ops onboard <repo> <handle>`
41
+ - `brains-ops reconcile-cohort <repo> <cohort>`
42
+ - `brains-ops reconcile-all <repo>`
@@ -0,0 +1,2 @@
1
+ members:
2
+ - alice
@@ -0,0 +1,15 @@
1
+ ARG BUN_VERSION=1.3.10
2
+ FROM oven/bun:${BUN_VERSION}-slim
3
+
4
+ ARG BRAIN_VERSION
5
+ WORKDIR /app
6
+
7
+ RUN test -n "$BRAIN_VERSION" \
8
+ && printf '{"name":"rover-pilot-runtime","private":true}\n' > package.json \
9
+ && bun add @rizom/brain@$BRAIN_VERSION
10
+
11
+ ENV XDG_DATA_HOME=/data
12
+ ENV XDG_CONFIG_HOME=/config
13
+ RUN mkdir -p /app/brain-data /data /config && chmod -R 777 /app/brain-data /data /config
14
+
15
+ CMD ["sh", "-c", "exec ./node_modules/.bin/brain start"]
@@ -0,0 +1,39 @@
1
+ service: rover
2
+ image: <%= ENV['IMAGE_REPOSITORY'] %>
3
+
4
+ servers:
5
+ mcp:
6
+ hosts:
7
+ - <%= ENV['SERVER_IP'] %>
8
+
9
+ proxy:
10
+ ssl:
11
+ certificate_pem: CERTIFICATE_PEM
12
+ private_key_pem: PRIVATE_KEY_PEM
13
+ hosts:
14
+ - <%= ENV['BRAIN_DOMAIN'] %>
15
+ app_port: 3333
16
+ healthcheck:
17
+ path: /health
18
+
19
+ registry:
20
+ server: ghcr.io
21
+ username: <%= ENV['REGISTRY_USERNAME'] %>
22
+ password:
23
+ - KAMAL_REGISTRY_PASSWORD
24
+
25
+ builder:
26
+ arch: amd64
27
+
28
+ env:
29
+ clear:
30
+ NODE_ENV: production
31
+ secret:
32
+ - AI_API_KEY
33
+ - GIT_SYNC_TOKEN
34
+ - MCP_AUTH_TOKEN
35
+ - DISCORD_BOT_TOKEN
36
+
37
+ volumes:
38
+ - /opt/brain-data:/app/brain-data
39
+ - /opt/brain.yaml:/app/brain.yaml
@@ -0,0 +1,10 @@
1
+ export {
2
+ readJsonResponse,
3
+ parseEnvFile,
4
+ parseEnvSchema,
5
+ parseEnvSchemaFile,
6
+ requireEnv,
7
+ writeGitHubOutput,
8
+ writeGitHubEnv,
9
+ } from "@rizom/ops/deploy";
10
+ export type { EnvSchemaEntry } from "@rizom/ops/deploy";
@@ -0,0 +1,59 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { requireEnv, writeGitHubOutput } from "./helpers";
3
+
4
+ const eventName = requireEnv("GITHUB_EVENT_NAME");
5
+
6
+ if (eventName === "workflow_dispatch") {
7
+ const handle = requireEnv("HANDLE_INPUT");
8
+ writeGitHubOutput("handles_json", JSON.stringify([handle]));
9
+ process.exit(0);
10
+ }
11
+
12
+ if (eventName !== "push") {
13
+ throw new Error(`Unsupported GITHUB_EVENT_NAME: ${eventName}`);
14
+ }
15
+
16
+ const beforeSha = requireEnv("BEFORE_SHA");
17
+ const currentSha = requireEnv("GITHUB_SHA");
18
+
19
+ if (!isUsableGitRevision(beforeSha) || !isUsableGitRevision(currentSha)) {
20
+ writeGitHubOutput("handles_json", JSON.stringify([]));
21
+ process.exit(0);
22
+ }
23
+
24
+ const diffOutput = execFileSync(
25
+ "git",
26
+ ["diff", "--name-only", beforeSha, currentSha],
27
+ { encoding: "utf8" },
28
+ );
29
+
30
+ const handles = [
31
+ ...new Set(
32
+ diffOutput
33
+ .split(/\r?\n/)
34
+ .map((path) => {
35
+ const match = path.match(/^users\/([^/]+)\/(?:\.env|brain\.yaml)$/);
36
+ return match?.[1] ?? null;
37
+ })
38
+ .filter((handle): handle is string => handle !== null)
39
+ .sort((left, right) => left.localeCompare(right)),
40
+ ),
41
+ ];
42
+
43
+ writeGitHubOutput("handles_json", JSON.stringify(handles));
44
+
45
+ function isUsableGitRevision(revision: string): boolean {
46
+ if (!revision || /^0+$/.test(revision)) {
47
+ return false;
48
+ }
49
+
50
+ try {
51
+ execFileSync("git", ["rev-parse", "--verify", revision], {
52
+ encoding: "utf8",
53
+ stdio: ["ignore", "pipe", "ignore"],
54
+ });
55
+ return true;
56
+ } catch {
57
+ return false;
58
+ }
59
+ }
@@ -0,0 +1,49 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { parseEnvFile, requireEnv, writeGitHubOutput } from "./helpers";
3
+
4
+ const handle = requireEnv("HANDLE");
5
+ const envPath = `users/${handle}/.env`;
6
+ const brainYamlPath = `users/${handle}/brain.yaml`;
7
+
8
+ const envEntries = parseEnvFile(envPath);
9
+ const repository = process.env["GITHUB_REPOSITORY"] ?? "";
10
+ const repositoryOwner = repository.split("/")[0] ?? "";
11
+
12
+ const brainYaml = readFileSync(brainYamlPath, "utf8");
13
+ const domainMatch = brainYaml.match(/^domain:\s*(.+)$/m);
14
+ const brainDomain = domainMatch?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? "";
15
+
16
+ if (!brainDomain) {
17
+ throw new Error(`Missing domain in ${brainYamlPath}`);
18
+ }
19
+
20
+ const outputs: Record<string, string> = {
21
+ brain_version: envEntries["BRAIN_VERSION"] ?? "",
22
+ ai_api_key_secret_name: envEntries["AI_API_KEY_SECRET"] ?? "",
23
+ git_sync_token_secret_name: envEntries["GIT_SYNC_TOKEN_SECRET"] ?? "",
24
+ mcp_auth_token_secret_name: envEntries["MCP_AUTH_TOKEN_SECRET"] ?? "",
25
+ discord_bot_token_secret_name: envEntries["DISCORD_BOT_TOKEN_SECRET"] ?? "",
26
+ content_repo: envEntries["CONTENT_REPO"] ?? "",
27
+ brain_domain: brainDomain,
28
+ brain_yaml_path: brainYamlPath,
29
+ instance_name: `rover-${handle}`,
30
+ image_repository: `ghcr.io/${repository}`,
31
+ registry_username: repositoryOwner,
32
+ };
33
+
34
+ const required = [
35
+ "brain_version",
36
+ "ai_api_key_secret_name",
37
+ "git_sync_token_secret_name",
38
+ "mcp_auth_token_secret_name",
39
+ "registry_username",
40
+ ];
41
+ for (const key of required) {
42
+ if (!outputs[key]) {
43
+ throw new Error(`Missing ${key} (derived from ${envPath})`);
44
+ }
45
+ }
46
+
47
+ for (const [key, value] of Object.entries(outputs)) {
48
+ writeGitHubOutput(key, value);
49
+ }
@@ -0,0 +1,10 @@
1
+ # Onboarding Checklist
2
+
3
+ 1. Run `bun install` so the repo uses its pinned `@rizom/ops` version.
4
+ 2. Fill in `pilot.yaml`.
5
+ 3. Add or edit `users/<handle>.yaml`.
6
+ 4. Add the user to a cohort in `cohorts/*.yaml`.
7
+ 5. Run `bunx brains-ops render <repo>`.
8
+ 6. Run `bunx brains-ops onboard <repo> <handle>`.
9
+ 7. 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.
10
+ 8. Hand the MCP connection details to the user.
@@ -0,0 +1,47 @@
1
+ # Operator Playbook
2
+
3
+ ## Deploy contract files
4
+
5
+ Treat these as checked-in deploy artifacts in the pilot repo:
6
+
7
+ - `.env.schema`
8
+ - `deploy/kamal/deploy.yml`
9
+ - `deploy/scripts/`
10
+ - `.github/workflows/build.yml`
11
+ - `.github/workflows/deploy.yml`
12
+ - `.github/workflows/reconcile.yml`
13
+
14
+ `.env.schema` is the single source of truth for required and sensitive deploy vars.
15
+ The deploy scripts and workflows should read from that contract instead of inventing a second list.
16
+
17
+ The shared pilot image tag is `brain-${brainVersion}`:
18
+
19
+ - build publishes `brain-${brainVersion}`
20
+ - generated `users/<handle>/.env` carries `BRAIN_VERSION=<brainVersion>`
21
+ - deploy sets `VERSION=brain-${brainVersion}`
22
+
23
+ ## Version bump flow
24
+
25
+ When `pilot.yaml.brainVersion` changes and you push:
26
+
27
+ 1. build publishes the new shared image tag
28
+ 2. reconcile refreshes generated `users/<handle>/.env`
29
+ 3. deploy runs for handles whose generated config changed
30
+ 4. generated file commits happen once in a final aggregation step after the deploy matrix finishes
31
+
32
+ When a push changes only deploy contract files and no generated `users/<handle>/.env` or `users/<handle>/brain.yaml` files, the deploy workflow exits through its explicit no-op path and prints `No affected user configs; skipping deploy.`
33
+
34
+ They are scaffolded from `@rizom/ops`, then versioned in this repo like any other deploy contract.
35
+
36
+ ## Upgrading operator behavior
37
+
38
+ When `@rizom/ops` changes the scaffolded deploy contract:
39
+
40
+ 1. bump `@rizom/ops` in `package.json`
41
+ 2. rerun the relevant scaffold/reconcile flow
42
+ 3. review the resulting changes to `.env.schema`, `deploy/scripts/`, and workflows in git
43
+ 4. commit the updated deploy artifacts together
44
+
45
+ ## Recovery notes
46
+
47
+ Document known failure modes, recovery steps, and operator notes here.
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "rover-pilot",
3
+ "private": true,
4
+ "type": "module",
5
+ "packageManager": "bun@__BUN_VERSION__",
6
+ "devDependencies": {
7
+ "@rizom/ops": "__BRAINS_OPS_VERSION__"
8
+ }
9
+ }
@@ -0,0 +1,8 @@
1
+ schemaVersion: 1
2
+ brainVersion: 0.1.1-alpha.14
3
+ model: rover
4
+ githubOrg: <github-org>
5
+ contentRepoPrefix: rover-
6
+ domainSuffix: .rizom.ai
7
+ preset: core
8
+ aiApiKey: AI_API_KEY
@@ -0,0 +1,3 @@
1
+ handle: alice
2
+ discord:
3
+ enabled: false