baa-conductor


baa-conductor / scripts / runtime
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"