@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.
- package/README.md +3 -2
- package/dist/age-key-bootstrap.d.ts +17 -0
- package/dist/brains-ops.js +172 -149
- package/dist/cert-bootstrap.d.ts +2 -2
- package/dist/content-repo.d.ts +12 -0
- package/dist/default-user-runner.d.ts +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.js +172 -149
- package/dist/load-registry.d.ts +19 -3
- package/dist/onboard-user.d.ts +2 -2
- package/dist/push-secrets.d.ts +1 -1
- package/dist/reconcile-all.d.ts +2 -2
- package/dist/reconcile-cohort.d.ts +2 -2
- package/dist/reconcile-lib.d.ts +4 -2
- package/dist/run-command.d.ts +0 -1
- package/dist/run-subprocess.d.ts +1 -0
- package/dist/schema.d.ts +97 -0
- package/dist/secrets-encrypt.d.ts +32 -0
- package/dist/user-runner.d.ts +5 -0
- package/package.json +5 -3
- package/templates/rover-pilot/.env.schema +5 -0
- package/templates/rover-pilot/.github/workflows/deploy.yml +45 -13
- 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 -1
- package/templates/rover-pilot/deploy/scripts/resolve-user-config.ts +12 -12
- package/templates/rover-pilot/deploy/scripts/sync-content-repo.ts +179 -0
- 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 +5 -1
- package/dist/secrets-push.d.ts +0 -13
- package/dist/user-secret-names.d.ts +0 -6
package/dist/load-registry.d.ts
CHANGED
|
@@ -13,6 +13,21 @@ export interface ResolvedCohort {
|
|
|
13
13
|
brainVersionOverride?: string;
|
|
14
14
|
presetOverride?: PilotPreset;
|
|
15
15
|
aiApiKeyOverride?: string;
|
|
16
|
+
gitSyncTokenOverride?: string;
|
|
17
|
+
mcpAuthTokenOverride?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface ResolvedAnchorProfileSocialLink {
|
|
20
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
21
|
+
url: string;
|
|
22
|
+
label?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface ResolvedAnchorProfile {
|
|
25
|
+
name: string;
|
|
26
|
+
description?: string;
|
|
27
|
+
website?: string;
|
|
28
|
+
email?: string;
|
|
29
|
+
story?: string;
|
|
30
|
+
socialLinks?: ResolvedAnchorProfileSocialLink[];
|
|
16
31
|
}
|
|
17
32
|
export interface ResolvedUserIdentity {
|
|
18
33
|
handle: string;
|
|
@@ -23,7 +38,11 @@ export interface ResolvedUserIdentity {
|
|
|
23
38
|
domain: string;
|
|
24
39
|
contentRepo: string;
|
|
25
40
|
discordEnabled: boolean;
|
|
41
|
+
discordAnchorUserId?: string;
|
|
26
42
|
effectiveAiApiKey: string;
|
|
43
|
+
effectiveGitSyncToken: string;
|
|
44
|
+
effectiveMcpAuthToken: string;
|
|
45
|
+
anchorProfile: ResolvedAnchorProfile;
|
|
27
46
|
snapshotStatus: SnapshotStatus;
|
|
28
47
|
}
|
|
29
48
|
export interface ResolvedUser extends ResolvedUserIdentity {
|
|
@@ -40,7 +59,4 @@ export interface PilotRegistry {
|
|
|
40
59
|
cohorts: ResolvedCohort[];
|
|
41
60
|
users: ResolvedUser[];
|
|
42
61
|
}
|
|
43
|
-
declare class PilotRegistryError extends Error {
|
|
44
|
-
}
|
|
45
62
|
export declare function loadPilotRegistry(rootDir: string, options?: LoadPilotRegistryOptions): Promise<PilotRegistry>;
|
|
46
|
-
export { PilotRegistryError };
|
package/dist/onboard-user.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function onboardUser(rootDir: string, handle: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
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/reconcile-all.d.ts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function reconcileAll(rootDir: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function reconcileAll(rootDir: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { type UserRunner } from "./reconcile-lib";
|
|
2
|
-
export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner): Promise<void>;
|
|
1
|
+
import { type UserRunner, type ContentRepoSyncOptions } from "./reconcile-lib";
|
|
2
|
+
export declare function reconcileCohort(rootDir: string, cohortId: string, runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
package/dist/reconcile-lib.d.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { type ContentRepoSyncOptions } from "./content-repo";
|
|
1
2
|
import { type PilotRegistry, type ResolvedUser } from "./load-registry";
|
|
2
3
|
import type { UserRunner } from "./user-runner";
|
|
3
|
-
export type {
|
|
4
|
-
export
|
|
4
|
+
export type { ContentRepoSyncOptions } from "./content-repo";
|
|
5
|
+
export type { ContentRepoFile, UserRunResult, UserRunner } from "./user-runner";
|
|
6
|
+
export declare function runUsers(rootDir: string, registry: PilotRegistry, users: ResolvedUser[], runner?: UserRunner, contentRepoOptions?: ContentRepoSyncOptions): Promise<void>;
|
|
5
7
|
export declare function findUser(rootDir: string, handle: string): Promise<{
|
|
6
8
|
registry: PilotRegistry;
|
|
7
9
|
user: ResolvedUser;
|
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/run-subprocess.d.ts
CHANGED
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,53 +38,142 @@ 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>;
|
|
59
|
+
anchorProfile: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
name: z.ZodOptional<z.ZodString>;
|
|
61
|
+
description: z.ZodOptional<z.ZodString>;
|
|
62
|
+
website: z.ZodOptional<z.ZodString>;
|
|
63
|
+
email: z.ZodOptional<z.ZodString>;
|
|
64
|
+
story: z.ZodOptional<z.ZodString>;
|
|
65
|
+
socialLinks: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
66
|
+
platform: z.ZodEnum<["github", "instagram", "linkedin", "email", "website"]>;
|
|
67
|
+
url: z.ZodString;
|
|
68
|
+
label: z.ZodOptional<z.ZodString>;
|
|
69
|
+
}, "strict", z.ZodTypeAny, {
|
|
70
|
+
url: string;
|
|
71
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
72
|
+
label?: string | undefined;
|
|
73
|
+
}, {
|
|
74
|
+
url: string;
|
|
75
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
76
|
+
label?: string | undefined;
|
|
77
|
+
}>, "many">>;
|
|
78
|
+
}, "strict", z.ZodTypeAny, {
|
|
79
|
+
name?: string | undefined;
|
|
80
|
+
email?: string | undefined;
|
|
81
|
+
website?: string | undefined;
|
|
82
|
+
description?: string | undefined;
|
|
83
|
+
story?: string | undefined;
|
|
84
|
+
socialLinks?: {
|
|
85
|
+
url: string;
|
|
86
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
87
|
+
label?: string | undefined;
|
|
88
|
+
}[] | undefined;
|
|
89
|
+
}, {
|
|
90
|
+
name?: string | undefined;
|
|
91
|
+
email?: string | undefined;
|
|
92
|
+
website?: string | undefined;
|
|
93
|
+
description?: string | undefined;
|
|
94
|
+
story?: string | undefined;
|
|
95
|
+
socialLinks?: {
|
|
96
|
+
url: string;
|
|
97
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
98
|
+
label?: string | undefined;
|
|
99
|
+
}[] | undefined;
|
|
100
|
+
}>>;
|
|
44
101
|
}, "strict", z.ZodTypeAny, {
|
|
45
102
|
handle: string;
|
|
46
103
|
discord: {
|
|
47
104
|
enabled: boolean;
|
|
105
|
+
anchorUserId?: string | undefined;
|
|
48
106
|
};
|
|
49
107
|
aiApiKeyOverride?: string | undefined;
|
|
108
|
+
gitSyncTokenOverride?: string | undefined;
|
|
109
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
110
|
+
anchorProfile?: {
|
|
111
|
+
name?: string | undefined;
|
|
112
|
+
email?: string | undefined;
|
|
113
|
+
website?: string | undefined;
|
|
114
|
+
description?: string | undefined;
|
|
115
|
+
story?: string | undefined;
|
|
116
|
+
socialLinks?: {
|
|
117
|
+
url: string;
|
|
118
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
119
|
+
label?: string | undefined;
|
|
120
|
+
}[] | undefined;
|
|
121
|
+
} | undefined;
|
|
50
122
|
}, {
|
|
51
123
|
handle: string;
|
|
52
124
|
discord: {
|
|
53
125
|
enabled: boolean;
|
|
126
|
+
anchorUserId?: string | undefined;
|
|
54
127
|
};
|
|
55
128
|
aiApiKeyOverride?: string | undefined;
|
|
129
|
+
gitSyncTokenOverride?: string | undefined;
|
|
130
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
131
|
+
anchorProfile?: {
|
|
132
|
+
name?: string | undefined;
|
|
133
|
+
email?: string | undefined;
|
|
134
|
+
website?: string | undefined;
|
|
135
|
+
description?: string | undefined;
|
|
136
|
+
story?: string | undefined;
|
|
137
|
+
socialLinks?: {
|
|
138
|
+
url: string;
|
|
139
|
+
platform: "github" | "instagram" | "linkedin" | "email" | "website";
|
|
140
|
+
label?: string | undefined;
|
|
141
|
+
}[] | undefined;
|
|
142
|
+
} | undefined;
|
|
56
143
|
}>;
|
|
57
144
|
export declare const cohortSchema: z.ZodEffects<z.ZodObject<{
|
|
58
145
|
members: z.ZodArray<z.ZodString, "many">;
|
|
59
146
|
brainVersionOverride: z.ZodOptional<z.ZodString>;
|
|
60
147
|
presetOverride: z.ZodOptional<z.ZodEnum<["core", "default", "pro"]>>;
|
|
61
148
|
aiApiKeyOverride: z.ZodOptional<z.ZodString>;
|
|
149
|
+
gitSyncTokenOverride: z.ZodOptional<z.ZodString>;
|
|
150
|
+
mcpAuthTokenOverride: z.ZodOptional<z.ZodString>;
|
|
62
151
|
}, "strict", z.ZodTypeAny, {
|
|
63
152
|
members: string[];
|
|
64
153
|
aiApiKeyOverride?: string | undefined;
|
|
154
|
+
gitSyncTokenOverride?: string | undefined;
|
|
155
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
65
156
|
brainVersionOverride?: string | undefined;
|
|
66
157
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
67
158
|
}, {
|
|
68
159
|
members: string[];
|
|
69
160
|
aiApiKeyOverride?: string | undefined;
|
|
161
|
+
gitSyncTokenOverride?: string | undefined;
|
|
162
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
70
163
|
brainVersionOverride?: string | undefined;
|
|
71
164
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
72
165
|
}>, {
|
|
73
166
|
members: string[];
|
|
74
167
|
aiApiKeyOverride?: string | undefined;
|
|
168
|
+
gitSyncTokenOverride?: string | undefined;
|
|
169
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
75
170
|
brainVersionOverride?: string | undefined;
|
|
76
171
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
77
172
|
}, {
|
|
78
173
|
members: string[];
|
|
79
174
|
aiApiKeyOverride?: string | undefined;
|
|
175
|
+
gitSyncTokenOverride?: string | undefined;
|
|
176
|
+
mcpAuthTokenOverride?: string | undefined;
|
|
80
177
|
brainVersionOverride?: string | undefined;
|
|
81
178
|
presetOverride?: "default" | "core" | "pro" | undefined;
|
|
82
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/user-runner.d.ts
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import type { ResolvedUser } from "./load-registry";
|
|
2
|
+
export interface ContentRepoFile {
|
|
3
|
+
path: string;
|
|
4
|
+
content: string;
|
|
5
|
+
}
|
|
2
6
|
export interface UserRunResult {
|
|
3
7
|
brainYaml?: string;
|
|
4
8
|
envFile?: string;
|
|
9
|
+
contentRepoFiles?: ContentRepoFile[];
|
|
5
10
|
}
|
|
6
11
|
export type UserRunner = (user: ResolvedUser) => Promise<UserRunResult | void>;
|
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.13",
|
|
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
|
|
|
@@ -12,6 +12,8 @@ on:
|
|
|
12
12
|
paths:
|
|
13
13
|
- users/*/.env
|
|
14
14
|
- users/*/brain.yaml
|
|
15
|
+
- users/*/content/**
|
|
16
|
+
- users/*.secrets.yaml.age
|
|
15
17
|
- deploy/**
|
|
16
18
|
- .github/workflows/deploy.yml
|
|
17
19
|
|
|
@@ -73,8 +75,18 @@ jobs:
|
|
|
73
75
|
- name: Install operator tooling
|
|
74
76
|
run: bun install
|
|
75
77
|
|
|
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"
|
|
83
|
+
|
|
76
84
|
- name: Reconcile selected user config
|
|
77
|
-
|
|
85
|
+
env:
|
|
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"
|
|
78
90
|
|
|
79
91
|
- name: Resolve generated user config
|
|
80
92
|
id: user_config
|
|
@@ -82,10 +94,9 @@ jobs:
|
|
|
82
94
|
|
|
83
95
|
- name: Validate selected secrets
|
|
84
96
|
env:
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
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] }}
|
|
89
100
|
HCLOUD_TOKEN: ${{ secrets.HCLOUD_TOKEN }}
|
|
90
101
|
HCLOUD_SSH_KEY_NAME: ${{ secrets.HCLOUD_SSH_KEY_NAME }}
|
|
91
102
|
HCLOUD_SERVER_TYPE: ${{ secrets.HCLOUD_SERVER_TYPE }}
|
|
@@ -96,7 +107,19 @@ jobs:
|
|
|
96
107
|
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
97
108
|
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
98
109
|
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
99
|
-
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
|
|
115
|
+
|
|
116
|
+
- name: Seed content repo
|
|
117
|
+
env:
|
|
118
|
+
CONTENT_REPO: ${{ steps.user_config.outputs.content_repo }}
|
|
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
|
|
100
123
|
|
|
101
124
|
- name: Log in to GHCR
|
|
102
125
|
uses: docker/login-action@v3
|
|
@@ -143,14 +166,17 @@ jobs:
|
|
|
143
166
|
|
|
144
167
|
- name: Write .kamal/secrets
|
|
145
168
|
env:
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
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] }}
|
|
150
172
|
KAMAL_REGISTRY_PASSWORD: ${{ secrets.KAMAL_REGISTRY_PASSWORD }}
|
|
151
173
|
CERTIFICATE_PEM: ${{ secrets.CERTIFICATE_PEM }}
|
|
152
174
|
PRIVATE_KEY_PEM: ${{ secrets.PRIVATE_KEY_PEM }}
|
|
153
|
-
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
|
|
154
180
|
|
|
155
181
|
- name: Provision server
|
|
156
182
|
id: provision
|
|
@@ -167,10 +193,11 @@ jobs:
|
|
|
167
193
|
CF_API_TOKEN: ${{ secrets.CF_API_TOKEN }}
|
|
168
194
|
CF_ZONE_ID: ${{ secrets.CF_ZONE_ID }}
|
|
169
195
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
196
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
170
197
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
171
198
|
run: |
|
|
172
199
|
bun deploy/scripts/update-dns.ts
|
|
173
|
-
BRAIN_DOMAIN="
|
|
200
|
+
BRAIN_DOMAIN="$PREVIEW_DOMAIN" bun deploy/scripts/update-dns.ts
|
|
174
201
|
|
|
175
202
|
- name: Validate SSH key
|
|
176
203
|
run: ssh-keygen -y -f ~/.ssh/id_ed25519 >/dev/null
|
|
@@ -197,6 +224,7 @@ jobs:
|
|
|
197
224
|
IMAGE_REPOSITORY: ${{ steps.user_config.outputs.image_repository }}
|
|
198
225
|
REGISTRY_USERNAME: ${{ steps.user_config.outputs.registry_username }}
|
|
199
226
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
227
|
+
PREVIEW_DOMAIN: ${{ steps.user_config.outputs.preview_domain }}
|
|
200
228
|
BRAIN_YAML_PATH: ${{ steps.user_config.outputs.brain_yaml_path }}
|
|
201
229
|
run: kamal setup --skip-push -c deploy/kamal/deploy.yml
|
|
202
230
|
|
|
@@ -204,7 +232,10 @@ jobs:
|
|
|
204
232
|
env:
|
|
205
233
|
SERVER_IP: ${{ steps.provision.outputs.server_ip }}
|
|
206
234
|
BRAIN_DOMAIN: ${{ steps.user_config.outputs.brain_domain }}
|
|
207
|
-
|
|
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/"
|
|
208
239
|
|
|
209
240
|
- name: Upload generated config
|
|
210
241
|
uses: actions/upload-artifact@v4
|
|
@@ -213,6 +244,7 @@ jobs:
|
|
|
213
244
|
path: |
|
|
214
245
|
users/${{ matrix.handle }}/brain.yaml
|
|
215
246
|
users/${{ matrix.handle }}/.env
|
|
247
|
+
users/${{ matrix.handle }}/content
|
|
216
248
|
|
|
217
249
|
- name: Dump remote proxy diagnostics
|
|
218
250
|
if: failure()
|
|
@@ -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,7 +32,9 @@ const handles = [
|
|
|
32
32
|
diffOutput
|
|
33
33
|
.split(/\r?\n/)
|
|
34
34
|
.map((path) => {
|
|
35
|
-
const match =
|
|
35
|
+
const match =
|
|
36
|
+
path.match(/^users\/([^/]+)\/(?:\.env|brain\.yaml|content\/.*)$/) ??
|
|
37
|
+
path.match(/^users\/([^/]+)\.secrets\.yaml\.age$/);
|
|
36
38
|
return match?.[1] ?? null;
|
|
37
39
|
})
|
|
38
40
|
.filter((handle): handle is string => handle !== null)
|