@rizom/ops 0.2.0-alpha.11 → 0.2.0-alpha.13

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 +3 -2
  2. package/dist/age-key-bootstrap.d.ts +17 -0
  3. package/dist/brains-ops.js +172 -149
  4. package/dist/cert-bootstrap.d.ts +2 -2
  5. package/dist/content-repo.d.ts +12 -0
  6. package/dist/default-user-runner.d.ts +1 -1
  7. package/dist/index.d.ts +2 -1
  8. package/dist/index.js +172 -149
  9. package/dist/load-registry.d.ts +19 -3
  10. package/dist/onboard-user.d.ts +2 -2
  11. package/dist/push-secrets.d.ts +1 -1
  12. package/dist/reconcile-all.d.ts +2 -2
  13. package/dist/reconcile-cohort.d.ts +2 -2
  14. package/dist/reconcile-lib.d.ts +4 -2
  15. package/dist/run-command.d.ts +0 -1
  16. package/dist/run-subprocess.d.ts +1 -0
  17. package/dist/schema.d.ts +97 -0
  18. package/dist/secrets-encrypt.d.ts +32 -0
  19. package/dist/user-runner.d.ts +5 -0
  20. package/package.json +5 -3
  21. package/templates/rover-pilot/.env.schema +5 -0
  22. package/templates/rover-pilot/.github/workflows/deploy.yml +45 -13
  23. package/templates/rover-pilot/README.md +3 -2
  24. package/templates/rover-pilot/deploy/Caddyfile +1 -1
  25. package/templates/rover-pilot/deploy/kamal/deploy.yml +1 -1
  26. package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +83 -0
  27. package/templates/rover-pilot/deploy/scripts/provision-server.ts +1 -1
  28. package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +3 -1
  29. package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +12 -12
  30. package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +179 -0
  31. package/templates/rover-pilot/docs/onboarding-checklist.md +21 -12
  32. package/templates/rover-pilot/docs/operator-playbook.md +43 -5
  33. package/templates/rover-pilot/docs/user-onboarding.md +86 -58
  34. package/templates/rover-pilot/package.json +3 -0
  35. package/templates/rover-pilot/pilot.yaml +3 -0
  36. package/templates/rover-pilot/users/alice.yaml +5 -1
  37. package/dist/secrets-push.d.ts +0 -13
  38. package/dist/user-secret-names.d.ts +0 -6
@@ -1,4 +1,5 @@
1
1
  import { readFileSync } from "node:fs";
2
+
2
3
  import { parseEnvFile, requireEnv, writeGitHubOutput } from "./helpers";
3
4
 
4
5
  const handle = requireEnv("HANDLE");
@@ -12,32 +13,31 @@ const repositoryOwner = repository.split("/")[0] ?? "";
12
13
  const brainYaml = readFileSync(brainYamlPath, "utf8");
13
14
  const domainMatch = brainYaml.match(/^domain:\s*(.+)$/m);
14
15
  const brainDomain = domainMatch?.[1]?.trim().replace(/^['"]|['"]$/g, "") ?? "";
15
-
16
16
  if (!brainDomain) {
17
17
  throw new Error(`Missing domain in ${brainYamlPath}`);
18
18
  }
19
19
 
20
+ const zone =
21
+ brainDomain.startsWith(`${handle}.`) && brainDomain.length > handle.length + 1
22
+ ? brainDomain.slice(handle.length + 1)
23
+ : "";
24
+ if (!zone) {
25
+ throw new Error(`Could not derive preview domain from ${brainDomain}`);
26
+ }
27
+ const previewDomain = `${handle}-preview.${zone}`;
28
+
20
29
  const outputs: Record<string, string> = {
21
30
  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
31
  content_repo: envEntries["CONTENT_REPO"] ?? "",
27
32
  brain_domain: brainDomain,
33
+ preview_domain: previewDomain,
28
34
  brain_yaml_path: brainYamlPath,
29
35
  instance_name: `rover-${handle}`,
30
36
  image_repository: `ghcr.io/${repository}`,
31
37
  registry_username: repositoryOwner,
32
38
  };
33
39
 
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
- ];
40
+ const required = ["brain_version", "registry_username"];
41
41
  for (const key of required) {
42
42
  if (!outputs[key]) {
43
43
  throw new Error(`Missing ${key} (derived from ${envPath})`);
@@ -0,0 +1,179 @@
1
+ import { cp, mkdtemp, mkdir, readFile, readdir } from "node:fs/promises";
2
+ import { existsSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { dirname, join } from "node:path";
5
+ import { execFileSync } from "node:child_process";
6
+ import { readJsonResponse, requireEnv } from "./helpers";
7
+
8
+ const handle = requireEnv("HANDLE");
9
+ const contentRepo = requireEnv("CONTENT_REPO");
10
+ const token = requireEnv("GIT_SYNC_TOKEN");
11
+ const sourceDir = join("users", handle, "content");
12
+
13
+ if (!existsSync(sourceDir)) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const { owner, repo } = parseRepoSlug(contentRepo);
18
+ await ensureGitHubRepo({ owner, repo, token });
19
+
20
+ const tempRoot = await mkdtemp(join(tmpdir(), "brains-ops-content-"));
21
+ const checkoutDir = join(tempRoot, "repo");
22
+ const remoteUrl = buildAuthenticatedRemoteUrl(owner, repo, token);
23
+
24
+ runGit(["clone", remoteUrl, checkoutDir]);
25
+ runGit(["-C", checkoutDir, "checkout", "-B", "main"]);
26
+
27
+ const copiedFiles = await copyMissingFiles(sourceDir, checkoutDir);
28
+ if (copiedFiles === 0) {
29
+ process.exit(0);
30
+ }
31
+
32
+ runGit(["-C", checkoutDir, "config", "user.name", "brains-ops[bot]"]);
33
+ runGit([
34
+ "-C",
35
+ checkoutDir,
36
+ "config",
37
+ "user.email",
38
+ "41898282+github-actions[bot]@users.noreply.github.com",
39
+ ]);
40
+ runGit(["-C", checkoutDir, "add", "."]);
41
+
42
+ if (hasNoStagedChanges(checkoutDir)) {
43
+ process.exit(0);
44
+ }
45
+
46
+ runGit([
47
+ "-C",
48
+ checkoutDir,
49
+ "commit",
50
+ "-m",
51
+ `chore(content): seed ${handle} anchor profile`,
52
+ ]);
53
+ runGit(["-C", checkoutDir, "push", "origin", "HEAD:main"]);
54
+
55
+ const STALE_ANCHOR_PROFILE_MARKERS = [
56
+ "name: Your Name Here",
57
+ "Delete this and write your own",
58
+ ];
59
+
60
+ interface EnsureGitHubRepoOptions {
61
+ owner: string;
62
+ repo: string;
63
+ token: string;
64
+ }
65
+
66
+ interface GitHubRepoResponse {
67
+ clone_url?: string;
68
+ private?: boolean;
69
+ }
70
+
71
+ async function ensureGitHubRepo(
72
+ options: EnsureGitHubRepoOptions,
73
+ ): Promise<void> {
74
+ const headers = {
75
+ Authorization: `Bearer ${options.token}`,
76
+ Accept: "application/vnd.github+json",
77
+ "Content-Type": "application/json",
78
+ };
79
+ const repoUrl = `https://api.github.com/repos/${options.owner}/${options.repo}`;
80
+ const repoResponse = await fetch(repoUrl, { headers });
81
+
82
+ if (repoResponse.ok) {
83
+ await readJsonResponse(repoResponse, "GitHub repo lookup");
84
+ return;
85
+ }
86
+
87
+ if (repoResponse.status !== 404) {
88
+ const payload = await readJsonResponse(repoResponse, "GitHub repo lookup");
89
+ throw new Error(`GitHub repo lookup failed: ${JSON.stringify(payload)}`);
90
+ }
91
+
92
+ const createResponse = await fetch(
93
+ `https://api.github.com/orgs/${options.owner}/repos`,
94
+ {
95
+ method: "POST",
96
+ headers,
97
+ body: JSON.stringify({
98
+ name: options.repo,
99
+ private: true,
100
+ auto_init: false,
101
+ }),
102
+ },
103
+ );
104
+ const payload = (await readJsonResponse(
105
+ createResponse,
106
+ "GitHub repo create",
107
+ )) as GitHubRepoResponse;
108
+
109
+ if (!createResponse.ok) {
110
+ throw new Error(`GitHub repo create failed: ${JSON.stringify(payload)}`);
111
+ }
112
+ }
113
+
114
+ function parseRepoSlug(contentRepo: string): { owner: string; repo: string } {
115
+ const [owner, repo] = contentRepo.split("/");
116
+ if (!owner || !repo) {
117
+ throw new Error(`Invalid CONTENT_REPO: ${contentRepo}`);
118
+ }
119
+ return { owner, repo };
120
+ }
121
+
122
+ function buildAuthenticatedRemoteUrl(
123
+ owner: string,
124
+ repo: string,
125
+ token: string,
126
+ ): string {
127
+ return `https://x-access-token:${encodeURIComponent(token)}@github.com/${owner}/${repo}.git`;
128
+ }
129
+
130
+ function runGit(args: string[]): void {
131
+ execFileSync("git", args, { stdio: "inherit" });
132
+ }
133
+
134
+ function hasNoStagedChanges(checkoutDir: string): boolean {
135
+ try {
136
+ execFileSync("git", ["-C", checkoutDir, "diff", "--cached", "--quiet"], {
137
+ stdio: "ignore",
138
+ });
139
+ return true;
140
+ } catch {
141
+ return false;
142
+ }
143
+ }
144
+
145
+ async function copyMissingFiles(
146
+ sourceDir: string,
147
+ targetDir: string,
148
+ ): Promise<number> {
149
+ const entries = await readdir(sourceDir, { withFileTypes: true });
150
+ let copiedFiles = 0;
151
+
152
+ for (const entry of entries) {
153
+ const sourcePath = join(sourceDir, entry.name);
154
+ const targetPath = join(targetDir, entry.name);
155
+
156
+ if (entry.isDirectory()) {
157
+ await mkdir(targetPath, { recursive: true });
158
+ copiedFiles += await copyMissingFiles(sourcePath, targetPath);
159
+ continue;
160
+ }
161
+
162
+ const existing = await readFile(targetPath, "utf8").catch(() => undefined);
163
+ if (existing !== undefined && !isStaleAnchorProfile(existing)) {
164
+ continue;
165
+ }
166
+
167
+ await mkdir(dirname(targetPath), { recursive: true });
168
+ await cp(sourcePath, targetPath, { force: true });
169
+ copiedFiles += existing === (await readFile(sourcePath, "utf8")) ? 0 : 1;
170
+ }
171
+
172
+ return copiedFiles;
173
+ }
174
+
175
+ function isStaleAnchorProfile(content: string): boolean {
176
+ return STALE_ANCHOR_PROFILE_MARKERS.some((marker) =>
177
+ content.includes(marker),
178
+ );
179
+ }
@@ -1,17 +1,26 @@
1
1
  # Onboarding Checklist
2
2
 
3
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 ssh-key:bootstrap <repo> --push-to gh`.
9
- 7. Run `bunx brains-ops cert:bootstrap <repo> <handle> --push-to gh`.
10
- 8. Run `bunx brains-ops secrets:push <repo> <handle>`.
11
- 9. Run `bunx brains-ops onboard <repo> <handle>`.
12
- 10. Verify the deployed rover core contract:
4
+ 2. Run `bunx brains-ops age-key:bootstrap <repo> --push-to gh`.
5
+ 3. Fill in `pilot.yaml`.
6
+ - keep your pinned `brainVersion`
7
+ - confirm shared selectors for `aiApiKey`, `gitSyncToken`, and `mcpAuthToken`
8
+ - confirm `agePublicKey`
9
+ 4. Add or edit `users/<handle>.yaml`.
10
+ - Discord is enabled by default for pilot users
11
+ - if the user should be an anchor there, set `discord.anchorUserId` to their Discord user ID
12
+ 5. Add the user to a cohort in `cohorts/*.yaml`.
13
+ 6. Run `bunx brains-ops render <repo>`.
14
+ 7. Run `bunx brains-ops ssh-key:bootstrap <repo> --push-to gh`.
15
+ 8. Run `bunx brains-ops cert:bootstrap <repo> --push-to gh`.
16
+ 9. Keep raw user secret material locally for now (`.env.local`, file-backed env vars, or equivalent local inputs).
17
+ 10. Run `bunx brains-ops secrets:encrypt <repo> <handle>`.
18
+ 11. Commit and push `users/<handle>.secrets.yaml.age`.
19
+ 12. Run `bunx brains-ops onboard <repo> <handle>`.
20
+ 13. Verify the deployed rover core contract:
13
21
  - `https://<handle>.rizom.ai/health` returns `200`
14
22
  - unauthenticated `POST https://<handle>.rizom.ai/mcp` returns `401`
15
- 11. 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.
16
- 12. Hand the MCP connection details to the user.
17
- 13. Send `docs/user-onboarding.md` to the user as the pilot handoff guide.
23
+ 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.
24
+ 15. Hand the Discord setup details to the user. If they need direct client access, also hand over the MCP connection details.
25
+ 16. If you are also giving them a content repo workflow, describe it first as a normal git repo of markdown/text files; mention Obsidian only as an optional editor.
26
+ 17. Send `docs/user-onboarding.md` to the user as the pilot handoff guide.
@@ -35,14 +35,21 @@ They are scaffolded from `@rizom/ops`, then versioned in this repo like any othe
35
35
 
36
36
  ## Bootstrap flow
37
37
 
38
+ For this fleet, operator-local secret material remains the source of truth during onboarding and rotation. The repo stores encrypted per-user secrets, not raw values.
39
+
38
40
  For a new pilot user, the operator bootstrap order is:
39
41
 
40
- 1. `bunx brains-ops ssh-key:bootstrap <repo> --push-to gh`
41
- 2. `bunx brains-ops cert:bootstrap <repo> <handle> --push-to gh`
42
- 3. `bunx brains-ops secrets:push <repo> <handle>`
43
- 4. `bunx brains-ops onboard <repo> <handle>`
42
+ 1. `bunx brains-ops age-key:bootstrap <repo> --push-to gh`
43
+ 2. `bunx brains-ops ssh-key:bootstrap <repo> --push-to gh`
44
+ 3. `bunx brains-ops cert:bootstrap <repo> --push-to gh`
45
+ 4. `bunx brains-ops secrets:encrypt <repo> <handle>`
46
+ 5. `bunx brains-ops onboard <repo> <handle>`
47
+
48
+ `age-key:bootstrap` keeps a repo-local canonical age identity under `.brains-ops/age/identity.txt`, writes the matching public recipient to `pilot.yaml.agePublicKey`, and can push the private key to GitHub as `AGE_SECRET_KEY`.
49
+
50
+ The shared cert bootstrap writes local cert artifacts under `.brains-ops/certs/shared/`, which stays repo-local and ignored by git.
44
51
 
45
- `brains-ops cert:bootstrap` writes local cert artifacts under `.brains-ops/`, which stays repo-local and ignored by git.
52
+ Preview hosts use the shape `<handle>-preview.rizom.ai`, so one wildcard origin cert for `*.rizom.ai` covers both the primary and preview hosts for every pilot user.
46
53
 
47
54
  ## Upgrading operator behavior
48
55
 
@@ -63,6 +70,37 @@ Use these checks after deploy:
63
70
  - unauthenticated `POST https://<handle>.rizom.ai/mcp` should return `401 Unauthorized: Bearer token required`
64
71
  - a bare `GET /` may also return `401`; that is expected for rover core and does not indicate a bad deploy
65
72
 
73
+ ## Discord bot token checklist
74
+
75
+ Use this when enabling Discord for a pilot user.
76
+
77
+ 1. Pick the user handle (for example `smoke`).
78
+ 2. Open the Discord Developer Portal.
79
+ 3. Create a **new application** for that user's rover.
80
+ 4. Add a **Bot** to the application.
81
+ 5. Copy the bot token.
82
+ 6. Put that value in `.env` or `.env.local` in this repo as `DISCORD_BOT_TOKEN=...` while onboarding that user.
83
+ 7. Keep `discord.enabled: true` in `users/<handle>.yaml` unless you explicitly want to disable the primary pilot interface.
84
+ 8. Encrypt the current per-user secret payload:
85
+ - `bunx brains-ops secrets:encrypt . <handle>`
86
+ 9. Reconcile/deploy the user or cohort:
87
+
88
+ - `bunx brains-ops onboard . <handle>`
89
+ - or `bunx brains-ops reconcile-cohort . <cohort>`
90
+
91
+ 11. In the Discord Developer Portal, generate an install URL and invite the bot to the right server.
92
+ 12. Send a test message in Discord and confirm the rover responds.
93
+
94
+ Notes:
95
+
96
+ - Use **one bot token per user/rover**.
97
+ - Do not reuse the same Discord bot token across multiple pilot users.
98
+ - Discord is the default pilot interface moving forward.
99
+ - The encrypted `users/<handle>.secrets.yaml.age` file is the durable checked-in deploy input; your local env is only the operator staging source.
100
+ - MCP is optional and mainly for direct client access or specific testing workflows.
101
+ - When explaining the content workflow, describe it first as a normal **git repo** of **markdown/text files**.
102
+ - Position **Obsidian** as optional: it is just one possible editor for those same files, not the default requirement.
103
+
66
104
  ## Recovery notes
67
105
 
68
106
  Document known failure modes, recovery steps, and operator notes here.
@@ -10,10 +10,10 @@ Rover is your private AI assistant for working with your own notes, links, and i
10
10
 
11
11
  In this pilot, Rover is intentionally simple:
12
12
 
13
- - you talk to it through an **MCP client**
13
+ - you will usually talk to it in **Discord**
14
14
  - **there is no website to browse**
15
- - Discord is optional
16
- - Obsidian fits through the git-sync/content-repo workflow, not as the main chat interface
15
+ - **MCP is optional** and only needed for direct client access or specific testing workflows
16
+ - your content can also live in a normal git repo of markdown/text files; **Obsidian is optional** if you want a nicer note-editing interface
17
17
 
18
18
  You can think of Rover as a private knowledge companion that helps you:
19
19
 
@@ -25,45 +25,66 @@ You can think of Rover as a private knowledge companion that helps you:
25
25
 
26
26
  ## What you will receive from us
27
27
 
28
- We will send you the details you need to connect.
28
+ We will send you the details you need to get started.
29
29
 
30
30
  That usually includes:
31
31
 
32
- - your Rover URL: `https://<handle>.rizom.ai/mcp`
33
- - your **Bearer token**
34
- - confirmation of whether Discord is enabled for you
32
+ - confirmation that Discord is enabled for you, plus the invite/setup steps
33
+ - if needed, your Rover MCP URL: `https://<handle>.rizom.ai/mcp`
34
+ - if needed, your **Bearer token**
35
35
  - if needed, an invite to your **private** Rover content repo
36
36
  - any extra instructions if we are testing a specific workflow with your cohort
37
37
 
38
- Treat the **Bearer token** like a password. Do not share it.
38
+ If we give you a **Bearer token**, treat it like a password. Do not share it.
39
39
 
40
- ## One important idea: MCP is just the connection method
40
+ ## One important idea: Discord is the default, MCP is optional
41
41
 
42
- If you have never used MCP before, the shortest explanation is:
42
+ If you are new to Rover, the shortest explanation is:
43
43
 
44
44
  - **Rover** is the assistant
45
- - **MCP** is the way your AI client connects to Rover
45
+ - **Discord** is the default way most pilot users will talk to it
46
+ - **MCP** is an optional direct connection method for supported AI clients
46
47
 
47
- You do not need to understand the protocol details.
48
+ You do not need to understand the protocol details unless we specifically ask you to use MCP.
48
49
 
49
- For the pilot, the practical meaning is simple:
50
+ For most users, the practical meaning is simple:
50
51
 
51
- - open a supported client
52
- - add your Rover URL
53
- - paste your Bearer token
54
- - start talking to Rover
52
+ - join Discord
53
+ - message Rover there
54
+ - start using it
55
+
56
+ If your cohort is also testing MCP, we will send the URL, Bearer token, and setup help separately.
55
57
 
56
58
  ## What to use first
57
59
 
58
60
  For most users, the easiest first setup is:
59
61
 
60
- - **Claude Desktop** for talking to Rover
61
- - **Obsidian** only if you also want to work directly with markdown files later
62
- - **Discord** only if we explicitly enable it for you
62
+ - **Discord** for talking to Rover
63
+ - a normal **git repo of markdown/text files** only if you also want to work directly with your content later
64
+ - **Obsidian** only if you want a friendlier interface for those same files
65
+ - **Claude Desktop** or another MCP client only if we explicitly ask you to test a direct MCP workflow
66
+
67
+ ## Default setup: Discord
68
+
69
+ For most users, getting started means:
70
+
71
+ - join the Discord server we send you
72
+ - open the Rover channel or DM
73
+ - send a first message
74
+
75
+ Try a first message like:
76
+
77
+ > What can you help me do, and what should I use you for?
78
+
79
+ Or:
80
+
81
+ > Help me save my first note.
82
+
83
+ If Discord is not enabled for you yet, tell us and we will share the right next step.
63
84
 
64
- ## How to connect
85
+ ## Optional: direct MCP access
65
86
 
66
- Use an MCP client that supports:
87
+ If we have asked you to use an MCP client, use one that supports:
67
88
 
68
89
  - **HTTP / Streamable HTTP MCP**
69
90
  - **Bearer token authentication**
@@ -78,9 +99,9 @@ If the client asks for a name, use something simple like:
78
99
 
79
100
  - `Rover (<handle>)`
80
101
 
81
- ## Claude Desktop setup
102
+ ## Optional: Claude Desktop setup
82
103
 
83
- If your Claude Desktop version supports connecting to a **remote HTTP / Streamable HTTP MCP server**, enter:
104
+ If we ask you to connect through Claude Desktop and your version supports a **remote HTTP / Streamable HTTP MCP server**, enter:
84
105
 
85
106
  - **Server URL:** `https://<handle>.rizom.ai/mcp`
86
107
  - **Authentication:** Bearer token
@@ -186,34 +207,38 @@ If Rover cannot do what you asked, a good response from Rover is something like:
186
207
 
187
208
  If that does **not** happen, that is useful feedback for us too.
188
209
 
189
- ## Obsidian
210
+ ## Git, text files, and Obsidian
190
211
 
191
- Obsidian is part of the pilot through the **git-synced content repo workflow**, not through MCP.
212
+ The underlying content workflow is a normal **git repo** with normal **markdown/text files**.
213
+
214
+ Obsidian is optional. It is just one possible editor for those files.
192
215
 
193
216
  That means:
194
217
 
195
- - use **Claude Desktop** as the main way to talk to Rover
196
- - use **Obsidian** if you want to browse, draft, and edit markdown files directly
218
+ - use **Discord** as the main way to talk to Rover
219
+ - use a normal editor plus **git** if you want to browse, draft, and edit your files directly
220
+ - use **Obsidian** only if you want a more note-focused interface for the same files
197
221
  - Rover can pick up those file changes through the normal git-sync / directory-sync flow
198
222
 
199
223
  A simple mental model:
200
224
 
201
- - **Claude Desktop** = talk to Rover
202
- - **Obsidian** = edit the underlying notes
225
+ - **Discord** = talk to Rover
226
+ - **git repo + text files** = the underlying content
227
+ - **Obsidian** = an optional editor for that content
203
228
 
204
229
  ### Important: your content repo is private
205
230
 
206
- If you use the Obsidian/git workflow, you will be working in your own **private** GitHub repo.
231
+ If you use the git/text-file workflow, you will be working in your own **private** GitHub repo.
207
232
 
208
233
  That means:
209
234
 
210
- - you do **not** need repo access just to use Rover through MCP
235
+ - you do **not** need repo access just to use Rover in Discord or through MCP
211
236
  - you **do** need GitHub access if you want to clone, edit, and push to your content repo
212
237
  - we will invite you only to **your own** content repo, not to the operator repo and not to other users' repos
213
238
 
214
239
  ### How you get access
215
240
 
216
- If you want the Obsidian/git workflow, we will:
241
+ If you want the git/text-file workflow, we will:
217
242
 
218
243
  1. create or confirm your private content repo
219
244
  2. invite your GitHub account to that repo
@@ -227,10 +252,9 @@ The easiest path for most first-time users is:
227
252
  1. install **GitHub Desktop**
228
253
  2. accept the repo invite in GitHub
229
254
  3. clone the private repo with GitHub Desktop
230
- 4. open the cloned folder as an Obsidian vault
231
- 5. optionally install the **Obsidian Git** plugin if you want in-app commit/push/pull support
232
- 6. edit your markdown notes
233
- 7. commit and push your changes
255
+ 4. open the cloned folder in your normal editor and edit the markdown/text files directly
256
+ 5. optionally open that same folder as an **Obsidian** vault if you prefer
257
+ 6. commit and push your changes
234
258
 
235
259
  ### Authentication options
236
260
 
@@ -242,38 +266,38 @@ Usually the easiest order is:
242
266
  2. **SSH key** if you already use git that way
243
267
  3. a **fine-grained personal access token** only if another tool specifically requires it
244
268
 
245
- You do **not** need a personal access token just to use Rover through MCP.
269
+ You do **not** need a personal access token just to use Rover in Discord or through MCP.
246
270
 
247
271
  If we have already shared your content repo workflow with you, the normal setup is:
248
272
 
249
273
  1. clone your Rover content repo locally
250
- 2. open that folder as an Obsidian vault
274
+ 2. edit the markdown/text files in your normal editor, or open that same folder as an Obsidian vault if you prefer
251
275
  3. optionally install the **Obsidian Git** plugin if you want in-app commit/push/pull support
252
- 4. edit or organize your markdown notes there
253
- 5. commit and push your changes through normal git or the Obsidian Git plugin
276
+ 4. edit or organize your notes there
277
+ 5. commit and push your changes through normal git, GitHub Desktop, or the Obsidian Git plugin
254
278
  6. let the normal git-sync flow carry those changes into Rover
255
279
 
256
- If we have **not** given you a direct content repo workflow yet, that is fine. You can ignore Obsidian for now and use Rover purely through MCP.
280
+ If we have **not** given you a direct content repo workflow yet, that is fine. You can ignore git, text files, and Obsidian for now and use Rover in Discord. If we have also asked you to test MCP, you can use that too.
257
281
 
258
- ## Discord (optional)
282
+ ## Discord (default)
259
283
 
260
- Discord is optional in this pilot.
284
+ Discord is the default interface for this pilot.
261
285
 
262
- If it is enabled for you, think of it as a lightweight secondary interface for:
286
+ Think of it as the main place to:
263
287
 
264
- - quick note capture
265
- - dropping in links to save
266
- - short questions when you do not want to open Claude Desktop
288
+ - save quick notes
289
+ - drop in links to save
290
+ - ask short or long questions
291
+ - use Rover day to day without setting up a separate client
267
292
 
268
293
  Important:
269
294
 
270
- - **MCP remains the main interface**
271
- - Discord is **off by default**
272
- - if you want Discord, tell us explicitly
273
- - for this pilot, Discord-enabled users may need to supply their own bot token
295
+ - **Discord is the main pilot interface moving forward**
296
+ - MCP is **optional**
274
297
  - if Discord is enabled, we will send the exact invite/setup steps separately
298
+ - for some pilot setups, Discord-enabled users may need to supply their own bot token
275
299
 
276
- If Discord is **not** enabled for you, that is completely normal.
300
+ If Discord is **not** enabled for you yet, ask us and we will tell you whether your cohort is on the Discord-first workflow.
277
301
 
278
302
  ## What to expect in the pilot
279
303
 
@@ -294,7 +318,7 @@ That is normal. The point of the pilot is to learn from real use.
294
318
  For the pilot:
295
319
 
296
320
  - your Rover is deployed specifically for you
297
- - access to `/mcp` is protected by your Bearer token
321
+ - if you are using MCP, access to `/mcp` is protected by your Bearer token
298
322
  - you should avoid putting highly sensitive material into the pilot unless we have explicitly agreed that it is in scope
299
323
 
300
324
  If you are unsure whether something belongs in Rover, ask us first.
@@ -303,9 +327,9 @@ If you are unsure whether something belongs in Rover, ask us first.
303
327
 
304
328
  ### I opened the domain and it does not look like a normal site
305
329
 
306
- That is expected. In this pilot, **there is no website to browse**. Rover core is MCP-first.
330
+ That is expected. In this pilot, **there is no website to browse**. Rover runs through Discord and, optionally, a direct MCP endpoint.
307
331
 
308
- ### I got an authentication error
332
+ ### I got an authentication error in MCP
309
333
 
310
334
  Usually this means one of three things:
311
335
 
@@ -347,10 +371,14 @@ Short, honest feedback is perfect.
347
371
  When we onboard you, the message will look roughly like this:
348
372
 
349
373
  ```text
350
- Rover URL: https://<handle>.rizom.ai/mcp
374
+ Discord enabled: yes/no
375
+ Discord setup: <invite link or setup steps>
376
+ MCP access: optional / enabled / not enabled
377
+
378
+ If MCP is enabled:
379
+ MCP URL: https://<handle>.rizom.ai/mcp
351
380
  Auth type: Bearer token
352
381
  Bearer token: <token>
353
- Discord enabled: yes/no
354
382
  ```
355
383
 
356
384
  If anything is unclear, reply with the exact error text or a screenshot and we will help.
@@ -3,6 +3,9 @@
3
3
  "private": true,
4
4
  "type": "module",
5
5
  "packageManager": "bun@__BUN_VERSION__",
6
+ "dependencies": {
7
+ "age-encryption": "^0.3.0"
8
+ },
6
9
  "devDependencies": {
7
10
  "@rizom/ops": "__BRAINS_OPS_VERSION__"
8
11
  }
@@ -6,3 +6,6 @@ contentRepoPrefix: rover-
6
6
  domainSuffix: .rizom.ai
7
7
  preset: core
8
8
  aiApiKey: AI_API_KEY
9
+ gitSyncToken: GIT_SYNC_TOKEN
10
+ mcpAuthToken: MCP_AUTH_TOKEN
11
+ agePublicKey: age1replace-with-your-public-key
@@ -1,3 +1,7 @@
1
1
  handle: alice
2
+ anchorProfile:
3
+ name: Alice Example
4
+ description: Replace this with Alice's real public profile summary.
2
5
  discord:
3
- enabled: false
6
+ enabled: true
7
+ # anchorUserId: "123456789012345678"
@@ -1,13 +0,0 @@
1
- import { type RunCommand } from "./run-subprocess";
2
- export interface SecretsPushOptions {
3
- env?: NodeJS.ProcessEnv | undefined;
4
- logger?: ((message: string) => void) | undefined;
5
- dryRun?: boolean | undefined;
6
- runCommand?: RunCommand | undefined;
7
- }
8
- export interface SecretsPushResult {
9
- pushedKeys: string[];
10
- skippedKeys: string[];
11
- dryRun?: boolean | undefined;
12
- }
13
- export declare function pushPilotSecrets(rootDir: string, handle: string, options?: SecretsPushOptions): Promise<SecretsPushResult>;
@@ -1,6 +0,0 @@
1
- export interface UserSecretNames {
2
- gitSyncTokenSecretName: string;
3
- mcpAuthTokenSecretName: string;
4
- discordBotTokenSecretName: string;
5
- }
6
- export declare function deriveUserSecretNames(handle: string): UserSecretNames;