im_wower
·
2026-03-29
check-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/check-launchd.sh [options]
12
13Options:
14 --node mini Select node defaults. Defaults to mini.
15 --scope agent|daemon Expected launchd scope for install copies. Defaults to agent.
16 --service NAME Add one service to the check set. Repeatable.
17 --all-services Check conductor, codexd, worker-runner, and status-api.
18 --repo-dir PATH Repo root used to derive runtime paths.
19 --home-dir PATH HOME value expected in installed plist files.
20 --install-dir PATH Validate installed copies under this directory.
21 --shared-token TOKEN Expect this exact token in installed copies.
22 --shared-token-file PATH Read the expected token from a file.
23 --d1-account-id ID Expect this exact D1_ACCOUNT_ID in conductor.
24 --d1-database-id ID Expect this exact D1_DATABASE_ID in conductor.
25 --cloudflare-api-token TOKEN
26 Expect this exact CLOUDFLARE_API_TOKEN in conductor.
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 Expected conductor BAA_CONDUCTOR_PUBLIC_API_BASE.
31 --control-api-base URL Legacy alias for --public-api-base.
32 --local-api-base URL Expected BAA_CONDUCTOR_LOCAL_API.
33 --local-api-allowed-hosts CSV
34 Expected BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS.
35 --codexd-local-api-base URL
36 Expected BAA_CODEXD_LOCAL_API_BASE.
37 --codexd-event-stream-path PATH
38 Expected BAA_CODEXD_EVENT_STREAM_PATH.
39 --codexd-server-command COMMAND
40 Expected BAA_CODEXD_SERVER_COMMAND.
41 --codexd-server-cwd PATH Expected BAA_CODEXD_SERVER_CWD.
42 --status-api-host HOST Expected BAA_STATUS_API_HOST.
43 --username NAME Expected UserName for LaunchDaemons.
44 --skip-dist-check Skip dist/index.js existence checks.
45 --check-loaded Also require launchctl print to succeed for each selected service.
46 --domain TARGET Override launchctl domain target for --check-loaded.
47 --help Show this help text.
48
49Notes:
50 If no service is specified, conductor + codexd are checked.
51 Use --service status-api to validate the optional local read-only observer.
52 D1 sync is optional. When expected for conductor, D1_ACCOUNT_ID,
53 D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN must be provided together.
54 status-api defaults to BAA_CONDUCTOR_LOCAL_API /v1/system/state; launchd no
55 longer writes conductor public-api base env vars for it.
56 --public-api-base only applies when conductor is part of the checked set.
57 check mode accepts legacy conductor copies that only carry
58 BAA_CONTROL_API_BASE, but new installs are expected to write both names.
59 codexd static checks only validate app-server launchd wiring; they do not
60 require /v1/codexd/runs* or codex exec as formal runtime capabilities.
61EOF
62}
63
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=""
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}"
82codexd_local_api_base="${BAA_CODEXD_LOCAL_API_BASE:-${BAA_RUNTIME_DEFAULT_CODEXD_LOCAL_API}}"
83codexd_event_stream_path="${BAA_CODEXD_EVENT_STREAM_PATH:-${BAA_RUNTIME_DEFAULT_CODEXD_EVENT_STREAM_PATH}}"
84codexd_server_command="${BAA_CODEXD_SERVER_COMMAND:-${BAA_RUNTIME_DEFAULT_CODEXD_SERVER_COMMAND}}"
85codexd_server_cwd="${BAA_CODEXD_SERVER_CWD:-}"
86status_api_host="${BAA_STATUS_API_HOST:-100.71.210.78}"
87username="$(default_username)"
88skip_dist_check="0"
89check_loaded="0"
90domain_target=""
91services=()
92
93while [[ $# -gt 0 ]]; do
94 case "$1" in
95 --node)
96 node="$2"
97 shift 2
98 ;;
99 --scope)
100 scope="$2"
101 shift 2
102 ;;
103 --service)
104 validate_service "$2"
105 if ! contains_value "$2" "${services[@]-}"; then
106 services+=("$2")
107 fi
108 shift 2
109 ;;
110 --all-services)
111 while IFS= read -r service; do
112 if ! contains_value "$service" "${services[@]-}"; then
113 services+=("$service")
114 fi
115 done < <(all_services)
116 shift
117 ;;
118 --repo-dir)
119 repo_dir="$2"
120 shift 2
121 ;;
122 --home-dir)
123 home_dir="$2"
124 shift 2
125 ;;
126 --install-dir)
127 install_dir="$2"
128 shift 2
129 ;;
130 --shared-token)
131 shared_token="$2"
132 shift 2
133 ;;
134 --shared-token-file)
135 shared_token_file="$2"
136 shift 2
137 ;;
138 --d1-account-id)
139 d1_account_id="$2"
140 shift 2
141 ;;
142 --d1-database-id)
143 d1_database_id="$2"
144 shift 2
145 ;;
146 --cloudflare-api-token)
147 cloudflare_api_token="$2"
148 shift 2
149 ;;
150 --d1-secrets-env)
151 d1_secrets_env="$2"
152 shift 2
153 ;;
154 --public-api-base)
155 public_api_base="$2"
156 shift 2
157 ;;
158 --control-api-base)
159 legacy_control_api_base="$2"
160 shift 2
161 ;;
162 --local-api-base)
163 local_api_base="$2"
164 shift 2
165 ;;
166 --local-api-allowed-hosts)
167 local_api_allowed_hosts="$2"
168 shift 2
169 ;;
170 --codexd-local-api-base)
171 codexd_local_api_base="$2"
172 shift 2
173 ;;
174 --codexd-event-stream-path)
175 codexd_event_stream_path="$2"
176 shift 2
177 ;;
178 --codexd-server-command)
179 codexd_server_command="$2"
180 shift 2
181 ;;
182 --codexd-server-cwd)
183 codexd_server_cwd="$2"
184 shift 2
185 ;;
186 --status-api-host)
187 status_api_host="$2"
188 shift 2
189 ;;
190 --username)
191 username="$2"
192 shift 2
193 ;;
194 --skip-dist-check)
195 skip_dist_check="1"
196 shift
197 ;;
198 --check-loaded)
199 check_loaded="1"
200 shift
201 ;;
202 --domain)
203 domain_target="$2"
204 shift 2
205 ;;
206 --help)
207 usage
208 exit 0
209 ;;
210 *)
211 die "Unknown option: $1"
212 ;;
213 esac
214done
215
216validate_node "$node"
217validate_scope "$scope"
218
219if [[ -z "$public_api_base" ]]; then
220 if [[ -n "$legacy_control_api_base" ]]; then
221 public_api_base="$legacy_control_api_base"
222 elif [[ -n "${BAA_CONDUCTOR_PUBLIC_API_BASE:-}" ]]; then
223 public_api_base="${BAA_CONDUCTOR_PUBLIC_API_BASE}"
224 elif [[ -n "${BAA_CONTROL_API_BASE:-}" ]]; then
225 public_api_base="${BAA_CONTROL_API_BASE}"
226 else
227 public_api_base="${BAA_RUNTIME_DEFAULT_PUBLIC_API_BASE}"
228 fi
229fi
230
231if [[ "${#services[@]}" -eq 0 ]]; then
232 while IFS= read -r service; do
233 services+=("$service")
234 done < <(default_services)
235fi
236
237if [[ -z "$codexd_server_cwd" ]]; then
238 codexd_server_cwd="$repo_dir"
239fi
240
241services_require_shared_token() {
242 local service
243
244 for service in "${services[@]}"; do
245 if service_requires_shared_token "$service"; then
246 return 0
247 fi
248 done
249
250 return 1
251}
252
253if services_require_shared_token && [[ -n "$shared_token" || -n "$shared_token_file" ]]; then
254 shared_token="$(load_shared_token "$shared_token" "$shared_token_file")"
255fi
256
257d1_account_id="$(resolve_env_or_file_value "$d1_account_id" "D1_ACCOUNT_ID" "$d1_secrets_env")"
258d1_database_id="$(resolve_env_or_file_value "$d1_database_id" "D1_DATABASE_ID" "$d1_secrets_env")"
259cloudflare_api_token="$(
260 resolve_env_or_file_value "$cloudflare_api_token" "CLOUDFLARE_API_TOKEN" "$d1_secrets_env"
261)"
262
263d1_config_count=0
264if [[ -n "$d1_account_id" ]]; then
265 d1_config_count=$((d1_config_count + 1))
266fi
267if [[ -n "$d1_database_id" ]]; then
268 d1_config_count=$((d1_config_count + 1))
269fi
270if [[ -n "$cloudflare_api_token" ]]; then
271 d1_config_count=$((d1_config_count + 1))
272fi
273
274if [[ "$d1_config_count" != "0" && "$d1_config_count" != "3" ]]; then
275 die "Partial D1 config is not allowed. Provide D1_ACCOUNT_ID, D1_DATABASE_ID, and CLOUDFLARE_API_TOKEN together, or omit all three."
276fi
277
278set -- $(resolve_node_defaults "$node")
279conductor_host="$1"
280conductor_role="$2"
281node_id="$3"
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"
289
290assert_directory "$state_dir"
291assert_directory "$runs_dir"
292assert_directory "$worktrees_dir"
293assert_directory "$logs_dir"
294assert_directory "$logs_launchd_dir"
295assert_directory "$tmp_dir"
296
297check_string_equals() {
298 local name="$1"
299 local actual="$2"
300 local expected="$3"
301
302 if [[ "$actual" != "$expected" ]]; then
303 die "${name} mismatch: expected '${expected}', got '${actual}'"
304 fi
305}
306
307check_key_missing() {
308 local name="$1"
309 local plist_path="$2"
310 local key="$3"
311
312 if plist_has_key "$plist_path" "$key"; then
313 die "${name} should be absent"
314 fi
315}
316
317check_public_api_base_keys() {
318 local service="$1"
319 local plist_path="$2"
320 local found_any="0"
321
322 if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"; then
323 found_any="1"
324 check_string_equals \
325 "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" \
326 "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE")" \
327 "$public_api_base"
328 fi
329
330 if plist_has_key "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"; then
331 found_any="1"
332 check_string_equals \
333 "${service}:BAA_CONTROL_API_BASE" \
334 "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE")" \
335 "$public_api_base"
336 fi
337
338 if [[ "$found_any" != "1" ]]; then
339 die "${service}: missing conductor public API base env keys"
340 fi
341}
342
343check_conductor_d1_keys() {
344 local service="$1"
345 local plist_path="$2"
346
347 if [[ "$d1_config_count" == "3" ]]; then
348 check_string_equals \
349 "${service}:D1_ACCOUNT_ID" \
350 "$(plist_print_value "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID")" \
351 "$d1_account_id"
352 check_string_equals \
353 "${service}:D1_DATABASE_ID" \
354 "$(plist_print_value "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID")" \
355 "$d1_database_id"
356 check_string_equals \
357 "${service}:CLOUDFLARE_API_TOKEN" \
358 "$(plist_print_value "$plist_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN")" \
359 "$cloudflare_api_token"
360 return 0
361 fi
362
363 check_key_missing "${service}:D1_ACCOUNT_ID" "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
364 check_key_missing "${service}:D1_DATABASE_ID" "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID"
365 check_key_missing \
366 "${service}:CLOUDFLARE_API_TOKEN" \
367 "$plist_path" \
368 ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
369}
370
371check_installed_plist() {
372 local service="$1"
373 local plist_path="$2"
374 local stdout_path="$3"
375 local stderr_path="$4"
376 local dist_entry="$5"
377
378 assert_file "$plist_path"
379 plutil -lint "$plist_path" >/dev/null
380
381 check_string_equals "${service}:WorkingDirectory" "$(plist_print_value "$plist_path" ":WorkingDirectory")" "$repo_dir"
382 check_string_equals "${service}:PATH" "$(plist_print_value "$plist_path" ":EnvironmentVariables:PATH")" "$launchd_path"
383 check_string_equals "${service}:HOME" "$(plist_print_value "$plist_path" ":EnvironmentVariables:HOME")" "$home_dir"
384 check_string_equals "${service}:BAA_CONDUCTOR_HOST" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_HOST")" "$conductor_host"
385 check_string_equals "${service}:BAA_CONDUCTOR_ROLE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_ROLE")" "$conductor_role"
386 check_string_equals "${service}:BAA_CONDUCTOR_LOCAL_API" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API")" "$local_api_base"
387 check_string_equals "${service}:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_LOCAL_API_ALLOWED_HOSTS")" "$local_api_allowed_hosts"
388 check_string_equals "${service}:BAA_RUNS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_RUNS_DIR")" "$runs_dir"
389 check_string_equals "${service}:BAA_WORKTREES_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_WORKTREES_DIR")" "$worktrees_dir"
390 check_string_equals "${service}:BAA_LOGS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_LOGS_DIR")" "$logs_dir"
391 check_string_equals "${service}:BAA_TMP_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_TMP_DIR")" "$tmp_dir"
392 check_string_equals "${service}:BAA_STATE_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_STATE_DIR")" "$state_dir"
393 check_string_equals "${service}:BAA_NODE_ID" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_NODE_ID")" "$node_id"
394 check_string_equals "${service}:stdout" "$(plist_print_value "$plist_path" ":StandardOutPath")" "$stdout_path"
395 check_string_equals "${service}:stderr" "$(plist_print_value "$plist_path" ":StandardErrorPath")" "$stderr_path"
396 check_string_equals "${service}:entry" "$(plist_print_value "$plist_path" ":ProgramArguments:2")" "$dist_entry"
397
398 if service_uses_public_api_base "$service"; then
399 check_public_api_base_keys "$service" "$plist_path"
400 else
401 check_key_missing "${service}:BAA_CONDUCTOR_PUBLIC_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONDUCTOR_PUBLIC_API_BASE"
402 check_key_missing "${service}:BAA_CONTROL_API_BASE" "$plist_path" ":EnvironmentVariables:BAA_CONTROL_API_BASE"
403 fi
404
405 if service_requires_shared_token "$service"; then
406 local actual_shared_token
407
408 actual_shared_token="$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_SHARED_TOKEN")"
409 if [[ -n "$shared_token" ]]; then
410 check_string_equals "${service}:BAA_SHARED_TOKEN" "$actual_shared_token" "$shared_token"
411 elif [[ -z "$actual_shared_token" || "$actual_shared_token" == "replace-me" ]]; then
412 die "${service}: BAA_SHARED_TOKEN is empty or still replace-me"
413 fi
414 fi
415
416 if [[ "$service" == "conductor" ]]; then
417 check_string_equals "${service}:host-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:4")" "$conductor_host"
418 check_string_equals "${service}:role-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:6")" "$conductor_role"
419 check_string_equals "${service}:BAA_CODEXD_LOCAL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE")" "$codexd_local_api_base"
420 check_conductor_d1_keys "$service" "$plist_path"
421 else
422 check_key_missing "${service}:D1_ACCOUNT_ID" "$plist_path" ":EnvironmentVariables:D1_ACCOUNT_ID"
423 check_key_missing "${service}:D1_DATABASE_ID" "$plist_path" ":EnvironmentVariables:D1_DATABASE_ID"
424 check_key_missing "${service}:CLOUDFLARE_API_TOKEN" "$plist_path" ":EnvironmentVariables:CLOUDFLARE_API_TOKEN"
425 fi
426
427 if [[ "$service" == "codexd" ]]; then
428 check_string_equals "${service}:start-arg" "$(plist_print_value "$plist_path" ":ProgramArguments:3")" "start"
429 check_string_equals "${service}:BAA_CODEXD_REPO_ROOT" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_REPO_ROOT")" "$repo_dir"
430 check_string_equals "${service}:BAA_CODEXD_LOGS_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_LOGS_DIR")" "${repo_dir}/logs/codexd"
431 check_string_equals "${service}:BAA_CODEXD_STATE_DIR" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_STATE_DIR")" "${repo_dir}/state/codexd"
432 check_string_equals "${service}:BAA_CODEXD_MODE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_MODE")" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_MODE"
433 check_string_equals "${service}:BAA_CODEXD_LOCAL_API_BASE" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_LOCAL_API_BASE")" "$codexd_local_api_base"
434 check_string_equals "${service}:BAA_CODEXD_EVENT_STREAM_PATH" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_EVENT_STREAM_PATH")" "$codexd_event_stream_path"
435 check_string_equals "${service}:BAA_CODEXD_SERVER_STRATEGY" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_STRATEGY")" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_STRATEGY"
436 check_string_equals "${service}:BAA_CODEXD_SERVER_COMMAND" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_COMMAND")" "$codexd_server_command"
437 check_string_equals "${service}:BAA_CODEXD_SERVER_ARGS" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_ARGS")" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_ARGS"
438 check_string_equals "${service}:BAA_CODEXD_SERVER_CWD" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_CWD")" "$codexd_server_cwd"
439 check_string_equals "${service}:BAA_CODEXD_SERVER_ENDPOINT" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_CODEXD_SERVER_ENDPOINT")" "$BAA_RUNTIME_DEFAULT_CODEXD_SERVER_ENDPOINT"
440 fi
441
442 if [[ "$service" == "status-api" ]]; then
443 check_string_equals "${service}:BAA_STATUS_API_HOST" "$(plist_print_value "$plist_path" ":EnvironmentVariables:BAA_STATUS_API_HOST")" "$status_api_host"
444 fi
445
446 if [[ "$scope" == "daemon" ]]; then
447 check_string_equals "${service}:UserName" "$(plist_print_value "$plist_path" ":UserName")" "$username"
448 fi
449}
450
451for service in "${services[@]}"; do
452 template_path="$(service_template_path "$repo_dir" "$service")"
453 dist_entry="${repo_dir}/$(service_dist_entry_relative "$service")"
454 stdout_path="$(service_stdout_path "$logs_launchd_dir" "$service")"
455 stderr_path="$(service_stderr_path "$logs_launchd_dir" "$service")"
456
457 assert_file "$template_path"
458 plutil -lint "$template_path" >/dev/null
459
460 if [[ "$skip_dist_check" != "1" ]]; then
461 assert_file "$dist_entry"
462 fi
463
464 if [[ -n "$install_dir" ]]; then
465 check_installed_plist "$service" "$(service_install_path "$install_dir" "$service")" "$stdout_path" "$stderr_path" "$dist_entry"
466 fi
467done
468
469if [[ "$check_loaded" == "1" ]]; then
470 require_command launchctl
471
472 if [[ -z "$domain_target" ]]; then
473 domain_target="$(default_domain_target "$scope")"
474 fi
475
476 for service in "${services[@]}"; do
477 launchctl print "${domain_target}/$(service_label "$service")" >/dev/null
478 done
479fi
480
481runtime_log "launchd checks passed"