@rizom/ops 0.2.0-alpha.12 → 0.2.0-alpha.14
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.
- package/README.md +4 -2
- package/dist/age-key-bootstrap.d.ts +17 -0
- package/dist/brains-ops.js +170 -152
- package/dist/cert-bootstrap.d.ts +2 -2
- package/dist/content-repo.d.ts +3 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +169 -151
- package/dist/load-registry.d.ts +5 -3
- package/dist/push-secrets.d.ts +1 -1
- package/dist/run-command.d.ts +0 -1
- package/dist/schema.d.ts +35 -4
- package/dist/secrets-encrypt.d.ts +32 -0
- package/dist/secrets-push.d.ts +1 -1
- package/package.json +5 -3
- package/templates/rover-pilot/.env.schema +5 -0
- package/templates/rover-pilot/.github/workflows/deploy.yml +37 -21
- package/templates/rover-pilot/README.md +3 -2
- package/templates/rover-pilot/deploy/Caddyfile +1 -1
- package/templates/rover-pilot/deploy/kamal/deploy.yml +1 -1
- package/templates/rover-pilot/deploy/scripts/decrypt-user-secrets.ts +83 -0
- package/templates/rover-pilot/deploy/scripts/provision-server.ts +1 -1
- package/templates/rover-pilot/deploy/scripts/resolve-deploy-handles.ts +3 -3
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +12 -12
- package/templates/rover-pilot/docs/onboarding-checklist.md +21 -12
- package/templates/rover-pilot/docs/operator-playbook.md +43 -5
- package/templates/rover-pilot/docs/user-onboarding.md +86 -58
- package/templates/rover-pilot/package.json +3 -0
- package/templates/rover-pilot/pilot.yaml +3 -0
- package/templates/rover-pilot/users/alice.yaml +2 -1
- package/dist/user-secret-names.d.ts +0 -6
package/dist/load-registry.d.ts
CHANGED
|
@@ -13,6 +13,8 @@ export interface ResolvedCohort {
|
|
|
13
13
|
brainVersionOverride?: string;
|
|
14
14
|
presetOverride?: PilotPreset;
|
|
15
15
|
aiApiKeyOverride?: string;
|
|
16
|
+
gitSyncTokenOverride?: string;
|
|
17
|
+
mcpAuthTokenOverride?: string;
|
|
16
18
|
}
|
|
17
19
|
export interface ResolvedAnchorProfileSocialLink {
|
|
18
20
|
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
@@ -36,7 +38,10 @@ export interface ResolvedUserIdentity {
|
|
|
36
38
|
domain: string;
|
|
37
39
|
contentRepo: string;
|
|
38
40
|
discordEnabled: boolean;
|
|
41
|
+
discordAnchorUserId?: string;
|
|
39
42
|
effectiveAiApiKey: string;
|
|
43
|
+
effectiveGitSyncToken: string;
|
|
44
|
+
effectiveMcpAuthToken: string;
|
|
40
45
|
anchorProfile: ResolvedAnchorProfile;
|
|
41
46
|
snapshotStatus: SnapshotStatus;
|
|
42
47
|
}
|
|
@@ -54,7 +59,4 @@ export interface PilotRegistry {
|
|
|
54
59
|
cohorts: ResolvedCohort[];
|
|
55
60
|
users: ResolvedUser[];
|
|
56
61
|
}
|
|
57
|
-
declare class PilotRegistryError extends Error {
|
|
58
|
-
}
|
|
59
62
|
export declare function loadPilotRegistry(rootDir: string, options?: LoadPilotRegistryOptions): Promise<PilotRegistry>;
|
|
60
|
-
export { PilotRegistryError };
|
package/dist/push-secrets.d.ts
CHANGED
|
@@ -5,5 +5,5 @@ export interface PushSecretsOptions {
|
|
|
5
5
|
runCommand?: RunCommand | undefined;
|
|
6
6
|
logger?: ((message: string) => void) | undefined;
|
|
7
7
|
}
|
|
8
|
-
export declare function pushSecretsToBackend(
|
|
8
|
+
export declare function pushSecretsToBackend(_target: PushTarget, secrets: readonly SecretPair[], options?: PushSecretsOptions): Promise<void>;
|
|
9
9
|
export { normalizePushTarget };
|
package/dist/run-command.d.ts
CHANGED
|
@@ -15,7 +15,6 @@ export interface CommandDependencies extends LoadPilotRegistryOptions {
|
|
|
15
15
|
logger?: ((message: string) => void) | undefined;
|
|
16
16
|
fetchImpl?: FetchLike | undefined;
|
|
17
17
|
lookupHost?: LookupHost | undefined;
|
|
18
|
-
secretRunCommand?: OpsRunCommand | undefined;
|
|
19
18
|
bootstrapRunCommand?: OpsRunCommand | undefined;
|
|
20
19
|
sshKeygen?: SshKeygen | undefined;
|
|
21
20
|
}
|
package/dist/schema.d.ts
CHANGED
|
@@ -3,6 +3,7 @@ export declare const presetSchema: z.ZodEnum<["core", "default", "pro"]>;
|
|
|
3
3
|
export declare const exactVersionSchema: z.ZodString;
|
|
4
4
|
export declare const handleSchema: z.ZodString;
|
|
5
5
|
export declare const secretNameSchema: z.ZodString;
|
|
6
|
+
export declare const agePublicKeySchema: z.ZodString;
|
|
6
7
|
export declare const pilotSchema: z.ZodObject<{
|
|
7
8
|
schemaVersion: z.ZodLiteral<1>;
|
|
8
9
|
brainVersion: z.ZodString;
|
|
@@ -12,7 +13,11 @@ export declare const pilotSchema: z.ZodObject<{
|
|
|
12
13
|
domainSuffix: z.ZodString;
|
|
13
14
|
preset: z.ZodEnum<["core", "default", "pro"]>;
|
|
14
15
|
aiApiKey: z.ZodString;
|
|
16
|
+
gitSyncToken: z.ZodString;
|
|
17
|
+
mcpAuthToken: z.ZodString;
|
|
18
|
+
agePublicKey: z.ZodString;
|
|
15
19
|
}, "strict", z.ZodTypeAny, {
|
|
20
|
+
agePublicKey: string;
|
|
16
21
|
schemaVersion: 1;
|
|
17
22
|
brainVersion: string;
|
|
18
23
|
model: "rover";
|
|
@@ -21,7 +26,10 @@ export declare const pilotSchema: z.ZodObject<{
|
|
|
21
26
|
domainSuffix: string;
|
|
22
27
|
preset: "default" | "core" | "pro";
|
|
23
28
|
aiApiKey: string;
|
|
29
|
+
gitSyncToken: string;
|
|
30
|
+
mcpAuthToken: string;
|
|
24
31
|
}, {
|
|
32
|
+
agePublicKey: string;
|
|
25
33
|
schemaVersion: 1;
|
|
26
34
|
brainVersion: string;
|
|
27
35
|
model: "rover";
|
|
@@ -30,17 +38,24 @@ export declare const pilotSchema: z.ZodObject<{
|
|
|
30
38
|
domainSuffix: string;
|
|
31
39
|
preset: "default" | "core" | "pro";
|
|
32
40
|
aiApiKey: string;
|
|
41
|
+
gitSyncToken: string;
|
|
42
|
+
mcpAuthToken: string;
|
|
33
43
|
}>;
|
|
34
44
|
export declare const userSchema: z.ZodObject<{
|
|
35
45
|
handle: z.ZodString;
|
|
36
46
|
discord: z.ZodObject<{
|
|
37
47
|
enabled: z.ZodBoolean;
|
|
48
|
+
anchorUserId: z.ZodOptional<z.ZodString>;
|
|
38
49
|
}, "strict", z.ZodTypeAny, {
|
|
39
50
|
enabled: boolean;
|
|
51
|
+
anchorUserId?: string | undefined;
|
|
40
52
|
}, {
|
|
41
53
|
enabled: boolean;
|
|
54
|
+
anchorUserId?: string | undefined;
|
|
42
55
|
}>;
|
|
43
56
|
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
57
|
+
gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
|
|
58
|
+
mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
|
|
44
59
|
anchorProfile: z.ZodOptional<z.ZodObject<{
|
|
45
60
|
name: z.ZodOptional<z.ZodString>;
|
|
46
61
|
description: z.ZodOptional<z.ZodString>;
|
|
@@ -61,9 +76,9 @@ export declare const userSchema: z.ZodObject<{
|
|
|
61
76
|
label?: string | undefined;
|
|
62
77
|
}>, "many">>;
|
|
63
78
|
}, "strict", z.ZodTypeAny, {
|
|
79
|
+
name?: string | undefined;
|
|
64
80
|
email?: string | undefined;
|
|
65
81
|
website?: string | undefined;
|
|
66
|
-
name?: string | undefined;
|
|
67
82
|
description?: string | undefined;
|
|
68
83
|
story?: string | undefined;
|
|
69
84
|
socialLinks?: {
|
|
@@ -72,9 +87,9 @@ export declare const userSchema: z.ZodObject<{
|
|
|
72
87
|
label?: string | undefined;
|
|
73
88
|
}[] | undefined;
|
|
74
89
|
}, {
|
|
90
|
+
name?: string | undefined;
|
|
75
91
|
email?: string | undefined;
|
|
76
92
|
website?: string | undefined;
|
|
77
|
-
name?: string | undefined;
|
|
78
93
|
description?: string | undefined;
|
|
79
94
|
story?: string | undefined;
|
|
80
95
|
socialLinks?: {
|
|
@@ -87,12 +102,15 @@ export declare const userSchema: z.ZodObject<{
|
|
|
87
102
|
handle: string;
|
|
88
103
|
discord: {
|
|
89
104
|
enabled: boolean;
|
|
105
|
+
anchorUserId?: string | undefined;
|
|
90
106
|
};
|
|
91
107
|
aiApiKeyOverride?: string | undefined;
|
|
108
|
+
gitSyncTokenOverride?: string | undefined;
|
|
109
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
92
110
|
anchorProfile?: {
|
|
111
|
+
name?: string | undefined;
|
|
93
112
|
email?: string | undefined;
|
|
94
113
|
website?: string | undefined;
|
|
95
|
-
name?: string | undefined;
|
|
96
114
|
description?: string | undefined;
|
|
97
115
|
story?: string | undefined;
|
|
98
116
|
socialLinks?: {
|
|
@@ -105,12 +123,15 @@ export declare const userSchema: z.ZodObject<{
|
|
|
105
123
|
handle: string;
|
|
106
124
|
discord: {
|
|
107
125
|
enabled: boolean;
|
|
126
|
+
anchorUserId?: string | undefined;
|
|
108
127
|
};
|
|
109
128
|
aiApiKeyOverride?: string | undefined;
|
|
129
|
+
gitSyncTokenOverride?: string | undefined;
|
|
130
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
110
131
|
anchorProfile?: {
|
|
132
|
+
name?: string | undefined;
|
|
111
133
|
email?: string | undefined;
|
|
112
134
|
website?: string | undefined;
|
|
113
|
-
name?: string | undefined;
|
|
114
135
|
description?: string | undefined;
|
|
115
136
|
story?: string | undefined;
|
|
116
137
|
socialLinks?: {
|
|
@@ -125,24 +146,34 @@ export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
|
|
|
125
146
|
brainVersionOverride: z.ZodOptional<z.ZodString>;
|
|
126
147
|
presetOverride: z.ZodOptional<z.ZodEnum<["core", "default", "pro"]>>;
|
|
127
148
|
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
149
|
+
gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
|
|
150
|
+
mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
|
|
128
151
|
}, "strict", z.ZodTypeAny, {
|
|
129
152
|
members: string[];
|
|
130
153
|
aiApiKeyOverride?: string | undefined;
|
|
154
|
+
gitSyncTokenOverride?: string | undefined;
|
|
155
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
131
156
|
brainVersionOverride?: string | undefined;
|
|
132
157
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
133
158
|
}, {
|
|
134
159
|
members: string[];
|
|
135
160
|
aiApiKeyOverride?: string | undefined;
|
|
161
|
+
gitSyncTokenOverride?: string | undefined;
|
|
162
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
136
163
|
brainVersionOverride?: string | undefined;
|
|
137
164
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
138
165
|
}>, {
|
|
139
166
|
members: string[];
|
|
140
167
|
aiApiKeyOverride?: string | undefined;
|
|
168
|
+
gitSyncTokenOverride?: string | undefined;
|
|
169
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
141
170
|
brainVersionOverride?: string | undefined;
|
|
142
171
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
143
172
|
}, {
|
|
144
173
|
members: string[];
|
|
145
174
|
aiApiKeyOverride?: string | undefined;
|
|
175
|
+
gitSyncTokenOverride?: string | undefined;
|
|
176
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
146
177
|
brainVersionOverride?: string | undefined;
|
|
147
178
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
148
179
|
}>;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { z } from "@brains/utils";
|
|
2
|
+
declare const encryptedUserSecretsSchema: z.ZodObject<{
|
|
3
|
+
gitSyncToken: z.ZodOptional<z.ZodString>;
|
|
4
|
+
mcpAuthToken: z.ZodOptional<z.ZodString>;
|
|
5
|
+
discordBotToken: z.ZodOptional<z.ZodString>;
|
|
6
|
+
aiApiKey: z.ZodOptional<z.ZodString>;
|
|
7
|
+
}, "strict", z.ZodTypeAny, {
|
|
8
|
+
aiApiKey?: string | undefined;
|
|
9
|
+
gitSyncToken?: string | undefined;
|
|
10
|
+
mcpAuthToken?: string | undefined;
|
|
11
|
+
discordBotToken?: string | undefined;
|
|
12
|
+
}, {
|
|
13
|
+
aiApiKey?: string | undefined;
|
|
14
|
+
gitSyncToken?: string | undefined;
|
|
15
|
+
mcpAuthToken?: string | undefined;
|
|
16
|
+
discordBotToken?: string | undefined;
|
|
17
|
+
}>;
|
|
18
|
+
export type EncryptedUserSecrets = z.infer<typeof encryptedUserSecretsSchema>;
|
|
19
|
+
export interface SecretsEncryptOptions {
|
|
20
|
+
env?: NodeJS.ProcessEnv | undefined;
|
|
21
|
+
logger?: ((message: string) => void) | undefined;
|
|
22
|
+
dryRun?: boolean | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface SecretsEncryptResult {
|
|
25
|
+
encryptedPath: string;
|
|
26
|
+
plaintextPath: string;
|
|
27
|
+
deletedPlaintext: boolean;
|
|
28
|
+
encryptedKeys: Array<keyof EncryptedUserSecrets>;
|
|
29
|
+
dryRun?: boolean | undefined;
|
|
30
|
+
}
|
|
31
|
+
export declare function encryptPilotSecrets(rootDir: string, handle: string, options?: SecretsEncryptOptions): Promise<SecretsEncryptResult>;
|
|
32
|
+
export {};
|
package/dist/secrets-push.d.ts
CHANGED
|
@@ -10,4 +10,4 @@ export interface SecretsPushResult {
|
|
|
10
10
|
skippedKeys: string[];
|
|
11
11
|
dryRun?: boolean | undefined;
|
|
12
12
|
}
|
|
13
|
-
export declare function pushPilotSecrets(rootDir: string,
|
|
13
|
+
export declare function pushPilotSecrets(rootDir: string, options?: SecretsPushOptions): Promise<SecretsPushResult>;
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.2.0-alpha.
|
|
7
|
+
"version": "0.2.0-alpha.14",
|
|
8
8
|
"type": "module",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
@@ -32,9 +32,11 @@
|
|
|
32
32
|
"typecheck": "tsc --noEmit",
|
|
33
33
|
"lint": "eslint . --ext .ts",
|
|
34
34
|
"lint:fix": "eslint . --ext .ts --fix",
|
|
35
|
-
"test": "bun test"
|
|
35
|
+
"test": "bun test --timeout 20000"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"age-encryption": "^0.3.0"
|
|
36
39
|
},
|
|
37
|
-
"dependencies": {},
|
|
38
40
|
"devDependencies": {
|
|
39
41
|
"@brains/eslint-config": "workspace:*",
|
|
40
42
|
"@brains/typescript-config": "workspace:*",
|
|
@@ -4,18 +4,23 @@
|
|
|
4
4
|
# ----------
|
|
5
5
|
|
|
6
6
|
# AI provider
|
|
7
|
+
# Shared GitHub secret by default; a per-user override may come from the decrypted
|
|
8
|
+
# users/<handle>.secrets.yaml.age file at deploy time.
|
|
7
9
|
# @required @sensitive
|
|
8
10
|
AI_API_KEY=
|
|
9
11
|
|
|
10
12
|
# Git sync
|
|
13
|
+
# Comes from the decrypted users/<handle>.secrets.yaml.age file.
|
|
11
14
|
# @required @sensitive
|
|
12
15
|
GIT_SYNC_TOKEN=
|
|
13
16
|
|
|
14
17
|
# MCP interface
|
|
18
|
+
# Comes from the decrypted users/<handle>.secrets.yaml.age file.
|
|
15
19
|
# @required @sensitive
|
|
16
20
|
MCP_AUTH_TOKEN=
|
|
17
21
|
|
|
18
22
|
# Discord (optional, per-user)
|
|
23
|
+
# Comes from the decrypted users/<handle>.secrets.yaml.age file when enabled.
|
|
19
24
|
# @sensitive
|
|
20
25
|
DISCORD_BOT_TOKEN=
|
|
21
26
|
|
|
@@ -13,6 +13,7 @@ on:
|
|
|
13
13
|
- users/*/.env
|
|
14
14
|
- users/*/brain.yaml
|
|
15
15
|
- users/*/content/**
|
|
16
|
+
- users/*.secrets.yaml.age
|
|
16
17
|
- deploy/**
|
|
17
18
|
- .github/workflows/deploy.yml
|
|
18
19
|
|
|
@@ -74,16 +75,18 @@ jobs:
|
|
|
74
75
|
- name: Install operator tooling
|
|
75
76
|
run: bun install
|
|
76
77
|
|
|
77
|
-
- name:
|
|
78
|
-
id:
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
78
|
+
- name: Decrypt user secrets
|
|
79
|
+
id: user_secrets
|
|
80
|
+
env:
|
|
81
|
+
AGE_SECRET_KEY: ${{ secrets.AGE_SECRET_KEY }}
|
|
82
|
+
run: bun deploy/scripts/decrypt-user-secrets.ts "$HANDLE"
|
|
82
83
|
|
|
83
84
|
- name: Reconcile selected user config
|
|
84
85
|
env:
|
|
85
|
-
|
|
86
|
-
run:
|
|
86
|
+
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
87
|
+
run: |
|
|
88
|
+
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
89
|
+
bunx brains-ops onboard "$GITHUB_WORKSPACE" "$HANDLE"
|
|
87
90
|
|
|
88
91
|
- name: Resolve generated user config
|
|
89
92
|
id: user_config
|
|
@@ -91,10 +94,9 @@ jobs:
|
|
|
91
94
|
|
|
92
95
|
- name: Validate selected secrets
|
|
93
96
|
env:
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
|
|
97
|
+
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
98
|
+
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
99
|
+
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
98
100
|
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
99
101
|
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
100
102
|
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
@@ -105,13 +107,19 @@ jobs:
|
|
|
105
107
|
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
106
108
|
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
107
109
|
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
108
|
-
run:
|
|
110
|
+
run: |
|
|
111
|
+
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
112
|
+
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
113
|
+
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
114
|
+
bun deploy/scripts/validate-secrets.ts
|
|
109
115
|
|
|
110
116
|
- name: Seed content repo
|
|
111
117
|
env:
|
|
112
118
|
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
113
|
-
|
|
114
|
-
run:
|
|
119
|
+
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
120
|
+
run: |
|
|
121
|
+
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
122
|
+
bun deploy/scripts/sync-content-repo.ts
|
|
115
123
|
|
|
116
124
|
- name: Log in to GHCR
|
|
117
125
|
uses: docker/login-action@v3
|
|
@@ -158,14 +166,17 @@ jobs:
|
|
|
158
166
|
|
|
159
167
|
- name: Write .kamal/secrets
|
|
160
168
|
env:
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
DISCORD_BOT_TOKEN: ${{ steps.user_config.outputs.discord_bot_token_secret_name != '' && secrets[steps.user_config.outputs.discord_bot_token_secret_name] || '' }}
|
|
169
|
+
SHARED_AI_API_KEY: ${{ secrets[steps.user_secrets.outputs.shared_ai_api_key_secret_name] }}
|
|
170
|
+
SHARED_GIT_SYNC_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_git_sync_token_secret_name] }}
|
|
171
|
+
SHARED_MCP_AUTH_TOKEN: ${{ secrets[steps.user_secrets.outputs.shared_mcp_auth_token_secret_name] }}
|
|
165
172
|
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
166
173
|
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
167
174
|
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
168
|
-
run:
|
|
175
|
+
run: |
|
|
176
|
+
export AI_API_KEY="${AI_API_KEY:-$SHARED_AI_API_KEY}"
|
|
177
|
+
export GIT_SYNC_TOKEN="${GIT_SYNC_TOKEN:-$SHARED_GIT_SYNC_TOKEN}"
|
|
178
|
+
export MCP_AUTH_TOKEN="${MCP_AUTH_TOKEN:-$SHARED_MCP_AUTH_TOKEN}"
|
|
179
|
+
bun deploy/scripts/write-kamal-secrets.ts
|
|
169
180
|
|
|
170
181
|
- name: Provision server
|
|
171
182
|
id: provision
|
|
@@ -182,10 +193,11 @@ jobs:
|
|
|
182
193
|
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
183
194
|
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
184
195
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
196
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
185
197
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
186
198
|
run: |
|
|
187
199
|
bun deploy/scripts/update-dns.ts
|
|
188
|
-
BRAIN_DOMAIN="
|
|
200
|
+
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
189
201
|
|
|
190
202
|
- name: Validate SSH key
|
|
191
203
|
run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
|
@@ -212,6 +224,7 @@ jobs:
|
|
|
212
224
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
213
225
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
214
226
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
227
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
215
228
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
216
229
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
217
230
|
|
|
@@ -219,7 +232,10 @@ jobs:
|
|
|
219
232
|
env:
|
|
220
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
221
234
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
222
|
-
|
|
235
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
236
|
+
run: |
|
|
237
|
+
curl -I -k --max-time 20 --resolve "$BRAIN_DOMAIN:443:$SERVER_IP" "https://$BRAIN_DOMAIN/health"
|
|
238
|
+
curl -I -k --max-time 20 --resolve "$PREVIEW_DOMAIN:443:$SERVER_IP" "https://$PREVIEW_DOMAIN/"
|
|
223
239
|
|
|
224
240
|
- name: Upload generated config
|
|
225
241
|
uses: actions/upload-artifact@v4
|
|
@@ -38,8 +38,9 @@ When a push changes only deploy contract files, CI prints `No affected user conf
|
|
|
38
38
|
- `brains-ops init <repo>`
|
|
39
39
|
- `brains-ops render <repo>` — regenerates `views/users.md` with live DNS, `/health`, and unauthenticated `/mcp` status checks
|
|
40
40
|
- `brains-ops onboard <repo> <handle>`
|
|
41
|
+
- `brains-ops age-key:bootstrap <repo>`
|
|
41
42
|
- `brains-ops ssh-key:bootstrap <repo>`
|
|
42
|
-
- `brains-ops cert:bootstrap <repo
|
|
43
|
-
- `brains-ops secrets:
|
|
43
|
+
- `brains-ops cert:bootstrap <repo>`
|
|
44
|
+
- `brains-ops secrets:encrypt <repo> <handle>`
|
|
44
45
|
- `brains-ops reconcile-cohort <repo> <cohort>`
|
|
45
46
|
- `brains-ops reconcile-all <repo>`
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
|
|
3
|
+
import { Decrypter, armor } from "age-encryption";
|
|
4
|
+
|
|
5
|
+
import { requireEnv, writeGitHubEnv, writeGitHubOutput } from "./helpers";
|
|
6
|
+
|
|
7
|
+
const handle = process.argv[2] ?? requireEnv("HANDLE");
|
|
8
|
+
const ageSecretKey = extractAgeIdentity(requireEnv("AGE_SECRET_KEY"));
|
|
9
|
+
const encryptedPath = `users/${handle}.secrets.yaml.age`;
|
|
10
|
+
|
|
11
|
+
const armored = readFileSync(encryptedPath, "utf8");
|
|
12
|
+
const decoded = armor.decode(armored);
|
|
13
|
+
|
|
14
|
+
const decrypter = new Decrypter();
|
|
15
|
+
decrypter.addIdentity(ageSecretKey);
|
|
16
|
+
|
|
17
|
+
const plaintext = await decrypter.decrypt(decoded, "text");
|
|
18
|
+
const secrets = parseFlatYaml(plaintext);
|
|
19
|
+
const pilot = parseFlatYaml(readFileSync("pilot.yaml", "utf8"));
|
|
20
|
+
|
|
21
|
+
writeGitHubEnv("AI_API_KEY", secrets["aiApiKey"] ?? "");
|
|
22
|
+
writeGitHubEnv("GIT_SYNC_TOKEN", secrets["gitSyncToken"] ?? "");
|
|
23
|
+
writeGitHubEnv("MCP_AUTH_TOKEN", secrets["mcpAuthToken"] ?? "");
|
|
24
|
+
writeGitHubEnv("DISCORD_BOT_TOKEN", secrets["discordBotToken"] ?? "");
|
|
25
|
+
|
|
26
|
+
writeGitHubOutput(
|
|
27
|
+
"shared_ai_api_key_secret_name",
|
|
28
|
+
requireFlatValue(pilot, "aiApiKey", "pilot.yaml"),
|
|
29
|
+
);
|
|
30
|
+
writeGitHubOutput(
|
|
31
|
+
"shared_git_sync_token_secret_name",
|
|
32
|
+
requireFlatValue(pilot, "gitSyncToken", "pilot.yaml"),
|
|
33
|
+
);
|
|
34
|
+
writeGitHubOutput(
|
|
35
|
+
"shared_mcp_auth_token_secret_name",
|
|
36
|
+
requireFlatValue(pilot, "mcpAuthToken", "pilot.yaml"),
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
function extractAgeIdentity(contents: string): string {
|
|
40
|
+
const line = contents
|
|
41
|
+
.split(/\r?\n/)
|
|
42
|
+
.map((entry) => entry.trim())
|
|
43
|
+
.find((entry) => entry.startsWith("AGE-SECRET-KEY-"));
|
|
44
|
+
|
|
45
|
+
if (!line) {
|
|
46
|
+
throw new Error("Missing AGE-SECRET-KEY in AGE_SECRET_KEY");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return line;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function parseFlatYaml(contents: string): Record<string, string> {
|
|
53
|
+
const result: Record<string, string> = {};
|
|
54
|
+
|
|
55
|
+
for (const rawLine of contents.split(/\r?\n/)) {
|
|
56
|
+
const line = rawLine.trim();
|
|
57
|
+
if (!line || line.startsWith("#")) {
|
|
58
|
+
continue;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const match = line.match(/^([A-Za-z0-9_]+):\s*(.*)$/);
|
|
62
|
+
if (!match) {
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const [, key, rawValue] = match;
|
|
67
|
+
result[key] = rawValue.replace(/^['"]|['"]$/g, "");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return result;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function requireFlatValue(
|
|
74
|
+
values: Record<string, string>,
|
|
75
|
+
key: string,
|
|
76
|
+
label: string,
|
|
77
|
+
): string {
|
|
78
|
+
const value = values[key];
|
|
79
|
+
if (!value) {
|
|
80
|
+
throw new Error(`Missing ${key} in ${label}`);
|
|
81
|
+
}
|
|
82
|
+
return value;
|
|
83
|
+
}
|
|
@@ -101,7 +101,7 @@ while (server.status !== "running" || !server.public_net?.ipv4?.ip) {
|
|
|
101
101
|
server = await getServer(server.id);
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
const serverIp = server.public_net
|
|
104
|
+
const serverIp = server.public_net.ipv4.ip;
|
|
105
105
|
if (!serverIp) {
|
|
106
106
|
throw new Error(`Server ${server.id} running but has no IPv4 address`);
|
|
107
107
|
}
|
|
@@ -32,9 +32,9 @@ const handles = [
|
|
|
32
32
|
diffOutput
|
|
33
33
|
.split(/\r?\n/)
|
|
34
34
|
.map((path) => {
|
|
35
|
-
const match =
|
|
36
|
-
/^users\/([^/]+)\/(?:\.env|brain\.yaml|content\/.*)
|
|
37
|
-
|
|
35
|
+
const match =
|
|
36
|
+
path.match(/^users\/([^/]+)\/(?:\.env|brain\.yaml|content\/.*)$/) ??
|
|
37
|
+
path.match(/^users\/([^/]+)\.secrets\.yaml\.age$/);
|
|
38
38
|
return match?.[1] ?? null;
|
|
39
39
|
})
|
|
40
40
|
.filter((handle): handle is string => handle !== null)
|
|
@@ -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})`);
|
|
@@ -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.
|
|
5
|
-
3.
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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.
|