-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathdefault.nix
More file actions
353 lines (321 loc) · 12.2 KB
/
Copy pathdefault.nix
File metadata and controls
353 lines (321 loc) · 12.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
# NixOS service module for the Symphony runtime.
#
# Minimal opinionated systemd unit. Reads secrets from an EnvironmentFile
# you control, so you can wire any secret manager (sops-nix, agenix,
# Bitwarden Secrets Manager, AWS Secrets Manager, etc.) underneath. For
# Bitwarden Secrets Manager specifically, set `secretsCommand` to a
# `bws run -- ...` invocation; the unit will wrap ExecStart with it.
{
config,
lib,
pkgs,
...
}:
let
inherit (lib)
mkEnableOption
mkIf
mkOption
optionalString
types
;
cfg = config.services.symphony;
in
{
options.services.symphony = {
enable = mkEnableOption "Symphony runtime";
package = mkOption {
type = types.package;
description = "Symphony package (provides /bin/symphony from this flake's default output).";
};
user = mkOption {
type = types.str;
default = "symphony";
description = "Unix user the service runs as. Set to an existing user, or let DynamicUser handle it.";
};
stateDir = mkOption {
type = types.path;
default = "/var/lib/symphony";
description = "Directory for runs, workspaces, logs, and the staged runtime copy.";
};
httpPort = mkOption {
type = types.port;
default = 4040;
description = "Phoenix HTTP listener port.";
};
primaryRepo = mkOption {
type = types.nullOr types.path;
default = null;
description = "Absolute path to the primary repository checkout (SYMPHONY_PRIMARY_REPO).";
};
repoRoot = mkOption {
type = types.nullOr types.path;
default = null;
description = "Optional parent directory of sibling repository checkouts (SYMPHONY_REPO_ROOT). Defaults to the parent of primaryRepo.";
};
workflowPack = mkOption {
type = types.str;
default = "example";
description = "Built-in workflow pack name; ignored when packDir is set.";
};
packDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "Absolute path to an external workflow pack (SYMPHONY_PACK_DIR). Takes precedence over workflowPack.";
};
roomRegistryUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Central room.ix.dev base URL each run's room-server registers its
backend with (SYMPHONY_ROOM_REGISTRY_URL). Drives both the room UI's
transcript view and the Slack "Run details" deep link. Unset disables
registration and the Slack link. The matching write token is a secret;
supply SYMPHONY_ROOM_REGISTRY_TOKEN via environmentFile.
'';
};
roomAdvertiseHost = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Address a provisioned per-run room-server binds and advertises so
room.ix.dev can reach it to proxy the run's transcript
(SYMPHONY_ROOM_ADVERTISE_HOST). Set to this host's tailnet address when
room.ix.dev runs elsewhere; unset keeps the loopback default, reachable
only when room.ix.dev shares the host.
'';
};
roomServerUrl = mkOption {
type = types.nullOr types.str;
default = null;
description = ''
Standing room-server URL for `:local` / `{:room, url}` placements that
do not provision their own per-run server (SYMPHONY_ROOM_SERVER_URL).
'';
};
extraEnvironment = mkOption {
type = types.attrsOf types.str;
default = { };
description = ''
Additional environment variables exported to the service. Use for
non-secret config: LINEAR_WORKSPACE_SLUG, SYMPHONY_BOT_USERNAME,
SYMPHONY_BOT_EMAIL, SYMPHONY_GITHUB_APP_OWNER_REPO,
SYMPHONY_GITHUB_STATS_QUERY, SYMPHONY_SLACK_NOTIFY_CHANNEL, etc.
'';
};
environmentFile = mkOption {
type = types.nullOr types.path;
default = null;
description = ''
Path to a systemd EnvironmentFile holding secrets:
LINEAR_API_KEY, GITHUB_TOKEN, LINEAR_WEBHOOK_SECRET,
GITHUB_WEBHOOK_SECRET, SLACK_SIGNING_SECRET, SLACK_BOT_OAUTH_TOKEN,
SYMPHONY_GITHUB_APP_PRIVATE_KEY_BASE64, SYMPHONY_ROOM_REGISTRY_TOKEN,
etc.
Wire this to whichever secret manager you use (sops-nix, agenix, ...).
Leave null if you use secretsCommand instead.
'';
};
secretsCommand = mkOption {
type = types.nullOr (types.listOf types.str);
default = null;
example = [
"bws"
"run"
"--project-id"
"symphony-prod"
"--"
];
description = ''
Optional command that wraps ExecStart and injects secrets into the
environment. Designed for Bitwarden Secrets Manager (`bws run --
...`) or any compatible secret-injecting CLI. The wrapper command
must exec its trailing arguments. Place the bws binary on the
service's PATH via `path = [ pkgs.bws ];` or by adding it to
runtimeInputs of the symphony package.
When set, the unit also expects BWS_ACCESS_TOKEN (or equivalent)
to be exported via environmentFile or extraEnvironment.
'';
};
path = mkOption {
type = types.listOf types.package;
default = [ ];
description = "Extra packages on the service PATH (e.g. pkgs.bws when using secretsCommand).";
};
hostRuntime = mkOption {
default = { };
description = ''
The host codex placement. When enabled, a workflow node that
declares `location: host` (or the run's resolved fallback) runs
codex directly on this machine as a real OS user, with no VM. The
per-run room-server and the codex process it spawns run as
`user` inside that user's home directory, launched as transient
`systemd-run --uid` units. This option wires the polkit grant,
PATH, and environment that path needs. It stays inert until
`enable` is set.
'';
type = types.submodule {
options = {
enable = mkEnableOption "the host codex placement";
user = mkOption {
type = types.str;
default = "";
description = "OS user codex runs as for host placement (SYMPHONY_HOST_USER). Must already exist with a home directory.";
};
group = mkOption {
type = types.nullOr types.str;
default = null;
description = "OS group for host runs (SYMPHONY_HOST_GROUP); omitted uses the user's primary group.";
};
workspacesDir = mkOption {
type = types.nullOr types.path;
default = null;
description = "Parent directory for run checkouts (SYMPHONY_HOST_WORKSPACES_DIR); defaults to <user home>/symphony-workspaces.";
};
roomServerPackage = mkOption {
type = types.nullOr types.package;
default = null;
description = "Package providing the codex-wrapped room-server launched as the host user (this flake's room-server output). Used by the per-run host placement.";
};
keep = mkOption {
type = types.bool;
default = false;
description = "Leave the unit and checkout in place after the turn for inspection (SYMPHONY_HOST_KEEP).";
};
};
};
};
};
config = mkIf cfg.enable {
assertions = [
{
assertion = !cfg.hostRuntime.enable || cfg.hostRuntime.user != "";
message = "services.symphony.hostRuntime.user must be set when hostRuntime.enable is true.";
}
{
assertion = !cfg.hostRuntime.enable || cfg.hostRuntime.roomServerPackage != null;
message = "services.symphony.hostRuntime.roomServerPackage must be set when hostRuntime.enable is true.";
}
];
# The host runtime calls systemd's StartTransientUnit over D-Bus to run
# codex as another user. A non-root service needs polkit authorization
# for that. Scope the grant to the "symphony-host-" unit-name prefix so
# the service cannot manage unrelated system units. See systemd-run(1)
# and the polkit systemd1 actions documented at
# https://www.freedesktop.org/software/systemd/man/latest/org.freedesktop.systemd1.html
security.polkit = lib.mkIf cfg.hostRuntime.enable {
enable = true;
extraConfig = ''
polkit.addRule(function(action, subject) {
if (subject.user == "${cfg.user}" &&
action.id == "org.freedesktop.systemd1.manage-units") {
var unit = action.lookup("unit");
if (unit && unit.indexOf("symphony-host-") == 0) {
return polkit.Result.YES;
}
}
});
'';
};
users.users = lib.mkIf (cfg.user == "symphony") {
symphony = {
isSystemUser = true;
group = "symphony";
home = cfg.stateDir;
};
};
users.groups = lib.mkIf (cfg.user == "symphony") {
symphony = { };
};
systemd.tmpfiles.rules = [
"d ${cfg.stateDir} 0750 ${cfg.user} ${cfg.user} -"
"d ${cfg.stateDir}/workspaces 0750 ${cfg.user} ${cfg.user} -"
"d ${cfg.stateDir}/runs 0750 ${cfg.user} ${cfg.user} -"
"d ${cfg.stateDir}/log 0750 ${cfg.user} ${cfg.user} -"
];
systemd.services.symphony = {
description = "Symphony runtime";
wantedBy = [ "multi-user.target" ];
after = [ "network-online.target" ];
wants = [ "network-online.target" ];
path =
cfg.path
++ lib.optionals cfg.hostRuntime.enable [
pkgs.systemd
pkgs.getent
cfg.hostRuntime.roomServerPackage
];
environment = {
SYMPHONY_STATE_DIR = cfg.stateDir;
SYMPHONY_WORKSPACES_DIR = "${cfg.stateDir}/workspaces";
SYMPHONY_RUNS_DIR = "${cfg.stateDir}/runs";
SYMPHONY_LOGS_ROOT = "${cfg.stateDir}/log";
SYMPHONY_HTTP_PORT = toString cfg.httpPort;
SYMPHONY_WORKFLOW_PACK = cfg.workflowPack;
}
// (lib.optionalAttrs (cfg.primaryRepo != null) {
SYMPHONY_PRIMARY_REPO = toString cfg.primaryRepo;
})
// (lib.optionalAttrs (cfg.repoRoot != null) {
SYMPHONY_REPO_ROOT = toString cfg.repoRoot;
})
// (lib.optionalAttrs (cfg.packDir != null) {
SYMPHONY_PACK_DIR = toString cfg.packDir;
})
// (lib.optionalAttrs (cfg.roomRegistryUrl != null) {
SYMPHONY_ROOM_REGISTRY_URL = cfg.roomRegistryUrl;
})
// (lib.optionalAttrs (cfg.roomAdvertiseHost != null) {
SYMPHONY_ROOM_ADVERTISE_HOST = cfg.roomAdvertiseHost;
})
// (lib.optionalAttrs (cfg.roomServerUrl != null) {
SYMPHONY_ROOM_SERVER_URL = cfg.roomServerUrl;
})
// (lib.optionalAttrs cfg.hostRuntime.enable (
{
SYMPHONY_HOST_USER = cfg.hostRuntime.user;
SYMPHONY_HOST_ROOM_SERVER_COMMAND = lib.getExe cfg.hostRuntime.roomServerPackage;
}
// (lib.optionalAttrs (cfg.hostRuntime.group != null) {
SYMPHONY_HOST_GROUP = cfg.hostRuntime.group;
})
// (lib.optionalAttrs (cfg.hostRuntime.workspacesDir != null) {
SYMPHONY_HOST_WORKSPACES_DIR = toString cfg.hostRuntime.workspacesDir;
})
// (lib.optionalAttrs cfg.hostRuntime.keep {
SYMPHONY_HOST_KEEP = "true";
})
))
// cfg.extraEnvironment;
serviceConfig = {
Type = "simple";
User = cfg.user;
Group = cfg.user;
ExecStart =
let
symphonyBin = "${cfg.package}/bin/symphony";
wrapper = optionalString (cfg.secretsCommand != null) (
lib.escapeShellArgs cfg.secretsCommand + " "
);
in
"${wrapper}${symphonyBin}";
Restart = "on-failure";
RestartSec = "10s";
StateDirectory = lib.mkIf (lib.hasPrefix "/var/lib/" cfg.stateDir) (
lib.removePrefix "/var/lib/" cfg.stateDir
);
# Symphony spawns codex subprocesses and clones git repos, so
# most sandboxing options need to stay permissive. Only enable
# the cheap, safe ones.
NoNewPrivileges = true;
PrivateTmp = true;
ProtectKernelTunables = true;
ProtectKernelModules = true;
ProtectControlGroups = true;
}
// (lib.optionalAttrs (cfg.environmentFile != null) {
EnvironmentFile = cfg.environmentFile;
});
};
};
}