baa-conductor


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