im_wower
·
2026-03-29
install-launchd.sh
1#!/usr/bin/env bash
2set -euo pipefail
3
4SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
5# shellcheck source=./common.sh
6source "${SCRIPT_DIR}/common.sh"
7
8usage() {
9 cat <<'EOF'
10Usage:
11 scripts/runtime/install-launchd.sh [options]
12
13Options:
14 --node mini Select node defaults. Defaults to mini.
15 --scope agent|daemon Install under LaunchAgents or LaunchDaemons. Defaults to agent.
16 --service NAME Add one service to the install set. Repeatable.
17 --all-services Install conductor, codexd, worker-runner, and status-api templates.
18 --repo-dir PATH Repo root used for WorkingDirectory and runtime paths.
19 --home-dir PATH HOME value written into plist files.
20 --install-dir PATH Override launchd install directory.
21 --shared-token TOKEN Shared token written into the install copy.
22 --shared-token-file PATH Read the shared token from a file.
23 --d1-account-id ID Write D1_ACCOUNT_ID into the conductor install copy.
24 --d1-database-id ID Write D1_DATABASE_ID into the conductor install copy.
25 --cloudflare-api-token TOKEN
26 Write CLOUDFLARE_API_TOKEN into the conductor install copy.
27 --d1-secrets-env PATH Read D1_ACCOUNT_ID, D1_DATABASE_ID, and
28 CLOUDFLARE_API_TOKEN from an env file when not
29 provided explicitly.
30 --public-api-base URL Override conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
31 --control-api-base URL Legacy alias for --public-api-base.
32 --local-api-base URL Override BAA_CONDUCTOR_LOCAL_API.
33 --local-api-allowed-hosts CSV
34 Override BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
35 --codexd-local-api-base URL
36 Override BAA_CODEXD_LOCAL_API_BASE.
37 --codexd-event-stream-path PATH
38 Override BAA_CODEXD_EVENT_STREAM_PATH.
39 --codexd-server-command COMMAND
40 Override BAA_CODEXD_SERVER_COMMAND while keeping app-server mode.
41 --codexd-server-cwd PATH Override BAA_CODEXD_SERVER_CWD.
42 --status-api-host HOST Override BAA_STATUS_API_HOST.
43 --username NAME UserName for LaunchDaemons. Defaults to the current user.
44 --help Show this help text.
45
46Notes:
47 If no service is specified, conductor + codexd are installed.
48 Use --service codexd to render codexd independently; it does not require a
49 shared token.
50 D1 sync is optional. When enabled for conductor, D1_ACCOUNT_ID,
51 D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN must be provided together.
52 Use --service status-api to install the optional local read-only observer.
53 status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
54 longer writes conductor public-api base env vars for it.
55 --public-api-base only affects conductor install copies, and the default is
56 already https://conductor.makefile.so. install copies write both
57 BAA_CONDUCTOR_PUBLIC_API_BASE and legacy BAA_CONTROL_API_BASE.
58 codexd launchd wiring stays on app-server mode and does not expose
59 /v1/codexd/runs* or codex exec as a formal service contract.
60EOF
61}
62
63require_command cp
64require_command plutil
65assert_file /usr/libexec/PlistBuddy
66
67node="mini"
68scope="agent"
69repo_dir="${BAA_RUNTIME_REPO_DIR_DEFAULT}"
70home_dir="$(default_home_dir)"
71install_dir=""
72shared_token="${BAA_SHARED_TOKEN:-}"
73shared_token_file=""
74d1_account_id="${D1_ACCOUNT_ID:-}"
75d1_database_id="${D1_DATABASE_ID:-}"
76cloudflare_api_token="${CLOUDFLARE_API_TOKEN:-}"
77d1_secrets_env=""
78public_api_base=""
79legacy_control_api_base=""
80local_api_base="http://100.71.210.78:4317"
81local_api_allowed_hosts="${BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS:-100.71.210.78}"
82claude_coded_local_api_base="${BAA_CLAUDE_CODED_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CLAUDE_CODED_LOCAL_API}}"
83codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
84codexd_event_stream_path="${BAA_CODEXD_EVENT_STREAM_PATH:-${BAA_RUNTIME_DEFAULT_CODEXD_EVENT_STREAM_PATH}}"
85codexd_server_command="${BAA_CODEXD_SERVER_COMMAND:-${BAA_RUNTIME_DEFAULT_CODEXD_SERVER_COMMAND}}"
86codexd_server_cwd="${BAA_CODEXD_SERVER_CWD:-}"
87codexd_server_cwd_set="0"
88status_api_host="${BAA_STATUS_API_HOST:-100.71.210.78}"
89username="$(default_username)"
90services=()
91
92while [[ $# -gt 0 ]]; do
93 case "$1" in
94 --node)
95 node="$2"
96 shift 2
97 ;;
98 --scope)
99 scope="$2"
100 shift 2
101 ;;
102 --service)
103 validate_service "$2"
104 if ! contains_value "$2" "${services[@]-}"; then
105 services+=("$2")
106 fi
107 shift 2
108 ;;
109 --all-services)
110 while IFS= read -r service; do
111 if ! contains_value "$service" "${services[@]-}"; then
112 services+=("$service")
113 fi
114 done < <(all_services)
115 shift
116 ;;
117 --repo-dir)
118 repo_dir="$2"
119 shift 2
120 ;;
121 --home-dir)
122 home_dir="$2"
123 shift 2
124 ;;
125 --install-dir)
126 install_dir="$2"
127 shift 2
128 ;;
129 --shared-token)
130 shared_token="$2"
131 shift 2
132 ;;
133 --shared-token-file)
134 shared_token_file="$2"
135 shift 2
136 ;;
137 --d1-account-id)
138 d1_account_id="$2"
139 shift 2
140 ;;
141 --d1-database-id)
142 d1_database_id="$2"
143 shift 2
144 ;;
145 --cloudflare-api-token)
146 cloudflare_api_token="$2"
147 shift 2
148 ;;
149 --d1-secrets-env)
150 d1_secrets_env="$2"
151 shift 2
152 ;;
153 --public-api-base)
154 public_api_base="$2"
155 shift 2
156 ;;
157 --control-api-base)
158 legacy_control_api_base="$2"
159 shift 2
160 ;;
161 --local-api-base)
162 local_api_base="$2"
163 shift 2
164 ;;
165 --local-api-allowed-hosts)
166 local_api_allowed_hosts="$2"
167 shift 2
168 ;;
169 --claude-coded-local-api-base)
170 claude_coded_local_api_base="$2"
171 shift 2
172 ;;
173 --codexd-local-api-base)
174 codexd_local_api_base="$2"
175 shift 2
176 ;;
177 --codexd-event-stream-path)
178 codexd_event_stream_path="$2"
179 shift 2
180 ;;
181 --codexd-server-command)
182 codexd_server_command="$2"
183 shift 2
184 ;;
185 --codexd-server-cwd)
186 codexd_server_cwd="$2"
187 codexd_server_cwd_set="1"
188 shift 2
189 ;;
190 --status-api-host)
191 status_api_host="$2"
192 shift 2
193 ;;
194 --username)
195 username="$2"
196 shift 2
197 ;;
198 --help)
199 usage
200 exit 0
201 ;;
202 *)
203 die "Unknown option: $1"
204 ;;
205 esac
206done
207
208validate_node "$node"
209validate_scope "$scope"
210
211if [[ -z "$public_api_base" ]]; then
212 if [[ -n "$legacy_control_api_base" ]]; then
213 public_api_base="$legacy_control_api_base"
214 elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
215 public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
216 elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
217 public_api_base="${BAA_CONTROL_API_BASE}"
218 else
219 public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
220 fi
221fi
222
223if [[ "${#services[@]}" -eq 0 ]]; then
224 while IFS= read -r service; do
225 services+=("$service")
226 done < <(default_services)
227fi
228
229services_require_shared_token() {
230 local service
231
232 for service in "${services[@]}"; do
233 if service_requires_shared_token "$service"; then
234 return 0
235 fi
236 done
237
238 return 1
239}
240
241if services_require_shared_token; then
242 shared_token="$(load_shared_token "$shared_token" "$shared_token_file")"
243 if [[ -z "$shared_token" ]]; then
244 die "A shared token is required. Use --shared-token, --shared-token-file, or BAA_SHARED_TOKEN."
245 fi
246fi
247
248d1_account_id="$(resolve_env_or_file_value "$d1_account_id" "D1_ACCOUNT_ID" "$d1_secrets_env")"
249d1_database_id="$(resolve_env_or_file_value "$d1_database_id" "D1_DATABASE_ID" "$d1_secrets_env")"
250cloudflare_api_token="$(
251 resolve_env_or_file_value "$cloudflare_api_token" "CLOUDFLARE_API_TOKEN" "$d1_secrets_env"
252)"
253
254d1_config_count=0
255if [[ -n "$d1_account_id" ]]; then
256 d1_config_count=$((d1_config_count + 1))
257fi
258if [[ -n "$d1_database_id" ]]; then
259 d1_config_count=$((d1_config_count + 1))
260fi
261if [[ -n "$cloudflare_api_token" ]]; then
262 d1_config_count=$((d1_config_count + 1))
263fi
264
265if [[ "$d1_config_count" != "0" && "$d1_config_count" != "3" ]]; then
266 die "Partial D1 config is not allowed. Provide D1_ACCOUNT_ID, D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN together, or omit all three."
267fi
268
269if [[ -z "$install_dir" ]]; then
270 install_dir="$(default_install_dir "$scope" "$home_dir")"
271fi
272
273if [[ "$codexd_server_cwd_set" != "1" && -z "$codexd_server_cwd" ]]; then
274 codexd_server_cwd="$repo_dir"
275fi
276
277set -- $(resolve_node_defaults "$node")
278conductor_host="$1"
279conductor_role="$2"
280node_id="$3"
281
282launchd_path="$(default_launchd_path "$home_dir")"
283state_dir="${repo_dir}/state"
284runs_dir="${repo_dir}/runs"
285worktrees_dir="${repo_dir}/worktrees"
286logs_dir="${repo_dir}/logs"
287logs_launchd_dir="${logs_dir}/launchd"
288tmp_dir="${repo_dir}/tmp"
289claude_coded_logs_dir="${logs_dir}/claude-coded"
290claude_coded_state_dir="${state_dir}/claude-coded"
291codexd_logs_dir="${logs_dir}/codexd"
292codexd_state_dir="${state_dir}/codexd"
293
294assert_directory "$state_dir"
295assert_directory "$runs_dir"
296assert_directory "$worktrees_dir"
297assert_directory "$logs_dir"
298assert_directory "$logs_launchd_dir"
299assert_directory "$tmp_dir"
300
301ensure_directory "$install_dir" "755"
302ensure_directory "$claude_coded_logs_dir" "700"
303ensure_directory "$claude_coded_state_dir" "700"
304ensure_directory "$codexd_logs_dir" "700"
305ensure_directory "$codexd_state_dir" "700"
306
307for service in "${services[@]}"; do
308 template_path="$(service_template_path "$repo_dir" "$service")"
309 install_path="$(service_install_path "$install_dir" "$service")"
310 stdout_path="$(service_stdout_path "$logs_launchd_dir" "$service")"
311 stderr_path="$(service_stderr_path "$logs_launchd_dir" "$service")"
312 dist_entry="${repo_dir}/$(service_dist_entry_relative "$service")"
313
314 assert_file "$template_path"
315 cp "$template_path" "$install_path"
316
317 plist_set_string "$install_path" ":WorkingDirectory" "$repo_dir"
318 plist_set_string "$install_path" ":EnvironmentVariables:PATH" "$launchd_path"
319 plist_set_string "$install_path" ":EnvironmentVariables:HOME" "$home_dir"
320 plist_set_string "$install_path" ":EnvironmentVariables:LANG" "$BAA_RUNTIME_DEFAULT_LOCALE"
321 plist_set_string "$install_path" ":EnvironmentVariables:LC_ALL" "$BAA_RUNTIME_DEFAULT_LOCALE"
322 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST" "$conductor_host"
323 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE" "$conductor_role"
324 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API" "$local_api_base"
325 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS" "$local_api_allowed_hosts"
326 plist_set_string "$install_path" ":EnvironmentVariables:BAA_RUNS_DIR" "$runs_dir"
327 plist_set_string "$install_path" ":EnvironmentVariables:BAA_WORKTREES_DIR" "$worktrees_dir"
328 plist_set_string "$install_path" ":EnvironmentVariables:BAA_LOGS_DIR" "$logs_dir"
329 plist_set_string "$install_path" ":EnvironmentVariables:BAA_TMP_DIR" "$tmp_dir"
330 plist_set_string "$install_path" ":EnvironmentVariables:BAA_STATE_DIR" "$state_dir"
331 plist_set_string "$install_path" ":EnvironmentVariables:BAA_NODE_ID" "$node_id"
332 plist_set_string "$install_path" ":StandardOutPath" "$stdout_path"
333 plist_set_string "$install_path" ":StandardErrorPath" "$stderr_path"
334 plist_set_string "$install_path" ":ProgramArguments:2" "$dist_entry"
335
336 if service_requires_shared_token "$service"; then
337 plist_set_string "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN" "$shared_token"
338 else
339 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_SHARED_TOKEN"
340 fi
341
342 if service_uses_public_api_base "$service"; then
343 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE" "$public_api_base"
344 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE" "$public_api_base"
345 else
346 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
347 plist_delete_key "$install_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
348 fi
349
350 if [[ "$service" == "conductor" ]]; then
351 plist_set_string "$install_path" ":ProgramArguments:4" "$conductor_host"
352 plist_set_string "$install_path" ":ProgramArguments:6" "$conductor_role"
353 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE" "$codexd_local_api_base"
354 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_LOCAL_API_BASE" "$claude_coded_local_api_base"
355
356 if [[ "$d1_config_count" == "3" ]]; then
357 plist_set_string "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID" "$d1_account_id"
358 plist_set_string "$install_path" ":EnvironmentVariables:D1_DATABASE_ID" "$d1_database_id"
359 plist_set_string "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN" "$cloudflare_api_token"
360 else
361 plist_delete_key "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
362 plist_delete_key "$install_path" ":EnvironmentVariables:D1_DATABASE_ID"
363 plist_delete_key "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
364 fi
365 else
366 plist_delete_key "$install_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
367 plist_delete_key "$install_path" ":EnvironmentVariables:D1_DATABASE_ID"
368 plist_delete_key "$install_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
369 fi
370
371 if [[ "$service" == "codexd" ]]; then
372 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_REPO_ROOT" "$repo_dir"
373 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_LOGS_DIR" "$codexd_logs_dir"
374 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_STATE_DIR" "$codexd_state_dir"
375 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_MODE" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_MODE"
376 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE" "$codexd_local_api_base"
377 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_EVENT_STREAM_PATH" "$codexd_event_stream_path"
378 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_STRATEGY" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_STRATEGY"
379 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_COMMAND" "$codexd_server_command"
380 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_ARGS" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_ARGS"
381 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_CWD" "$codexd_server_cwd"
382 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_ENDPOINT" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_ENDPOINT"
383 fi
384
385 if [[ "$service" == "claude-coded" ]]; then
386 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_REPO_ROOT" "$repo_dir"
387 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_LOGS_DIR" "$claude_coded_logs_dir"
388 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_STATE_DIR" "$claude_coded_state_dir"
389 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_LOCAL_API_BASE" "$claude_coded_local_api_base"
390 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_CHILD_COMMAND" "claude"
391 plist_set_string "$install_path" ":EnvironmentVariables:BAA_CLAUDE_CODED_CHILD_CWD" "$repo_dir"
392 fi
393
394 if [[ "$service" == "status-api" ]]; then
395 plist_set_string "$install_path" ":EnvironmentVariables:BAA_STATUS_API_HOST" "$status_api_host"
396 fi
397
398 if [[ "$scope" == "daemon" ]]; then
399 plist_set_string "$install_path" ":UserName" "$username"
400 else
401 plist_delete_key "$install_path" ":UserName"
402 fi
403
404 chmod 644 "$install_path"
405 plutil -lint "$install_path" >/dev/null
406
407 runtime_log "installed ${service} template -> ${install_path}"
408done
409
410runtime_log "launchd install copies rendered for ${node} (${scope})"