Writes .env, and the whole point of the section is the two values it must not write twice. JWT_SECRET and VAULT_STORE_KEY are read back from any existing .env and kept. The original script reminted JWT_SECRET on every run that agreed to regenerate .env, which logs every device out with no stated reason, and never wrote VAULT_STORE_KEY at all — so a scripted install had no at-rest key and the vault and wallet refused to store anything. VAULT_STORE_KEY is the more dangerous of the two now that it is being written. It is not Vaultwarden's despite the name: it encrypts every secret column in Postgres, and the wallet seed envelope on top of the owner passphrase. Changing it is unrecoverable for the seed, because the passphrase opens the inner envelope and that is the outer one. Said in the section, in the file it writes, and in .env.example, which described it as Vaultwarden's and understated it. DATA_PATH and OFFICER_ITEMS_DIR are derived from $OFFICER_ROOT rather than asked — two questions that had to agree with each other and with the app store. ALLOW_ANY_ORIGIN is written explicitly from whether tailscale0 exists, rather than left to the platform default. The default is ON, which CLAUDE.md says is only defensible because the tailnet is the perimeter; with no tailnet there is no perimeter, so it goes out as false. Added to .env.example, which omitted it. PORT defaults to 9000, matching .env.example. The old script used 9010; nothing depends on either, and it is a prompt. write_env restores the prior umask. It was set to 077 so the secrets are never briefly world-readable, but umask is not scoped to a function and would have made every file the later sections create owner-only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
565 lines
22 KiB
Bash
Executable File
565 lines
22 KiB
Bash
Executable File
#!/bin/bash
|
|
set -e
|
|
|
|
# =============================================================================
|
|
# officer-setup — the platform, on a machine that is already provisioned
|
|
#
|
|
# The second half of the install. machine-setup/ brings a blank box up to a
|
|
# usable machine; this puts Officer on top of it.
|
|
#
|
|
# Run as root: sudo scripts/setup/officer-setup.sh
|
|
# =============================================================================
|
|
|
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
PROGRESS_FILE="$SCRIPT_DIR/officer-setup/.setup-progress"
|
|
|
|
ONLY_STEP=""
|
|
while [[ $# -gt 0 ]]; do
|
|
case "$1" in
|
|
--only)
|
|
ONLY_STEP="${2:-}"
|
|
shift 2
|
|
;;
|
|
--only=*)
|
|
ONLY_STEP="${1#*=}"
|
|
shift
|
|
;;
|
|
-l | --list)
|
|
grep -oP '^step "\K[^"]+' "${BASH_SOURCE[0]}"
|
|
exit 0
|
|
;;
|
|
-h | --help)
|
|
echo "usage: officer-setup.sh [--only <step>] [--list]"
|
|
exit 0
|
|
;;
|
|
*) echo "unknown option: $1" >&2 && exit 2 ;;
|
|
esac
|
|
done
|
|
|
|
# shellcheck source=officer-setup/lib/base.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/base.sh"
|
|
# shellcheck source=officer-setup/lib/preflight.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/preflight.sh"
|
|
# shellcheck source=officer-setup/lib/repo.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/repo.sh"
|
|
# shellcheck source=officer-setup/lib/layout.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/layout.sh"
|
|
# shellcheck source=officer-setup/lib/postgres.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/postgres.sh"
|
|
# shellcheck source=officer-setup/lib/env.sh
|
|
source "$SCRIPT_DIR/officer-setup/lib/env.sh"
|
|
|
|
trap 'echo ""; echo -e "${RED}╔══════════════════════════════════════════════════╗${NC}"; echo -e "${RED}║ OFFICER SETUP FAILED${NC}"; echo -e "${RED}║ Step: ${CURRENT_STEP:-unknown}${NC}"; echo -e "${RED}║ Line: $LINENO${NC}"; echo -e "${RED}║ Command: $BASH_COMMAND${NC}"; echo -e "${RED}╚══════════════════════════════════════════════════╝${NC}"' ERR
|
|
|
|
# =============================================================================
|
|
# 1. Pre-flight
|
|
# =============================================================================
|
|
|
|
echo ""
|
|
echo -e "${BOLD}╔══════════════════════════════════════════════════╗${NC}"
|
|
echo -e "${BOLD}║ Officer Setup ║${NC}"
|
|
echo -e "${BOLD}╚══════════════════════════════════════════════════╝${NC}"
|
|
|
|
if [[ "$EUID" -ne 0 ]]; then
|
|
fail "Please run as root: sudo ./officer-setup.sh"
|
|
fi
|
|
|
|
# ── what machine-setup already established ──
|
|
echo ""
|
|
if load_machine_answers; then
|
|
info "Read from machine-setup: ${MACHINE_ANSWERS}"
|
|
else
|
|
warn "machine-setup has not run on this machine"
|
|
echo " That is fine if you provisioned it another way — the questions it"
|
|
echo " would have answered are asked below instead."
|
|
fi
|
|
|
|
# ── the account ──
|
|
#
|
|
# A remembered answer can go stale: the account it names may have been renamed or
|
|
# removed since machine-setup ran. That is a reason to ask again, not a reason to
|
|
# stop — so the remembered value is checked before it is trusted, and a bad one
|
|
# is reported and replaced rather than ending the run.
|
|
if [[ -n "$USERNAME" ]] && ! owner_exists; then
|
|
warn "the remembered account '${USERNAME}' does not exist on this machine any more"
|
|
USERNAME=""
|
|
fi
|
|
|
|
while [[ -z "$USERNAME" ]] || ! owner_exists; do
|
|
echo ""
|
|
info "Which account owns this Officer install?"
|
|
echo " Its files, its node_modules and its pm2 process list all belong to"
|
|
echo " this account rather than to root."
|
|
echo ""
|
|
ask_required USERNAME "Username" "${SUDO_USER:-}"
|
|
owner_exists || warn "There is no account called '${USERNAME}' on this machine."
|
|
done
|
|
|
|
resolve_user_home
|
|
|
|
# ── where it goes ──
|
|
if [[ -z "$OFFICER_ROOT" ]]; then
|
|
echo ""
|
|
info "Where should Officer be installed?"
|
|
echo " One directory holding the app, its data, the item store and any"
|
|
echo " containers the app store provisions."
|
|
echo ""
|
|
ask_required OFFICER_ROOT "Path" "${USER_HOME}/officerdev"
|
|
fi
|
|
OFFICER_ROOT="${OFFICER_ROOT/#\~/$USER_HOME}"
|
|
[[ "$OFFICER_ROOT" == /* ]] || fail "That needs to be an absolute path — got '${OFFICER_ROOT}'"
|
|
OFFICER_ROOT="${OFFICER_ROOT%/}"
|
|
|
|
info "Account: ${USERNAME} (home ${USER_HOME})"
|
|
info "Officer: ${OFFICER_ROOT}"
|
|
[[ -n "$MACHINE_ROLE" ]] && info "Role: ${MACHINE_ROLE}"
|
|
|
|
# ── is the machine actually ready ──
|
|
#
|
|
# Checked and reported together. Finding out about a missing bun three sections
|
|
# in, after a repository has been cloned and a database started, is a worse way
|
|
# to learn it.
|
|
echo ""
|
|
info "What Officer needs from this machine"
|
|
|
|
mapfile -t MISSING < <(missing_tools)
|
|
mapfile -t MISSING_OPT < <(missing_optional_tools)
|
|
|
|
for t in "${REQUIRED_TOOLS[@]}"; do
|
|
if command -v "$t" &>/dev/null; then
|
|
printf ' %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
|
|
else
|
|
printf ' %-6s %-10s %s\n' "$t" "MISSING" "$(tool_why "$t")"
|
|
fi
|
|
done
|
|
for t in "${OPTIONAL_TOOLS[@]}"; do
|
|
if command -v "$t" &>/dev/null; then
|
|
printf ' %-6s %-10s %s\n' "$t" "ok" "$(tool_why "$t")"
|
|
else
|
|
printf ' %-6s %-10s %s\n' "$t" "absent" "$(tool_why "$t") — optional"
|
|
fi
|
|
done
|
|
|
|
if ((${#MISSING[@]} > 0)); then
|
|
echo ""
|
|
fail "Missing: ${MISSING[*]}. Run scripts/setup/machine-setup/machine-setup.sh first, or install them yourself."
|
|
fi
|
|
|
|
if ((${#MISSING_OPT[@]} > 0)); then
|
|
echo ""
|
|
warn "No Docker. Postgres will have to be one you already run, and the app"
|
|
echo " store cannot provision anything until Docker is installed."
|
|
fi
|
|
|
|
if [[ -f "$PROGRESS_FILE" ]]; then
|
|
echo ""
|
|
info "Resuming — $(wc -l <"$PROGRESS_FILE") step(s) already done, and they will be skipped"
|
|
echo " To start over instead: sudo rm ${PROGRESS_FILE}"
|
|
else
|
|
echo ""
|
|
echo " This can be stopped at any point and run again later. Completed"
|
|
echo " steps are remembered and skipped."
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 2. Layout
|
|
# =============================================================================
|
|
#
|
|
# Before the repository, because the repository is cloned into it.
|
|
|
|
step "Layout"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Layout — everything Officer owns, under one root"
|
|
echo " ${OFFICER_ROOT}/"
|
|
echo " platform/ the app"
|
|
echo " data/ managed homes, attachments, job logs"
|
|
echo " dockers/ anything the app store provisions"
|
|
echo " capabilities/ skills, tools, tasks, processes"
|
|
echo ""
|
|
echo " Nothing here is configurable. The original asked separately for the"
|
|
echo " data directory and the item store, which were two answers that had"
|
|
echo " to agree with each other. One root now, and the rest follows."
|
|
echo ""
|
|
echo " To put data/ on a bigger volume later, symlink it — that is a"
|
|
echo " decision about storage rather than about how Officer is laid out."
|
|
|
|
mapfile -t WRONG_OWNER < <(layout_wrong_owner)
|
|
if ((${#WRONG_OWNER[@]} > 0)); then
|
|
echo ""
|
|
warn "these exist but do not belong to ${USERNAME}:"
|
|
printf ' %s\n' "${WRONG_OWNER[@]}"
|
|
echo " Everything that writes into them runs as ${USERNAME} — the platform"
|
|
echo " under pm2, the app store's compose files, the item store the agent"
|
|
echo " authors into. Left as they are, those writes fail in a way that"
|
|
echo " reads as a bug in the platform."
|
|
if confirm "Give them to ${USERNAME}?"; then
|
|
for d in "${WRONG_OWNER[@]}"; do chown -R "${USERNAME}:$(user_group)" "$d"; done
|
|
ok "ownership corrected"
|
|
SUMMARY+=("Layout: ownership corrected on ${#WRONG_OWNER[@]} directory(ies)")
|
|
fi
|
|
fi
|
|
|
|
create_layout
|
|
ok "layout in place under ${OFFICER_ROOT}"
|
|
SUMMARY+=("Layout: ${OFFICER_ROOT} (data, dockers, capabilities)")
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 3. Repository
|
|
# =============================================================================
|
|
|
|
step "Repository"
|
|
if ! skip; then
|
|
PLATFORM_DIR="$(platform_dir)"
|
|
|
|
echo ""
|
|
info "Repository — where the platform's code lives"
|
|
echo " path: ${PLATFORM_DIR}"
|
|
|
|
if repo_exists; then
|
|
echo " remote: $(repo_remote)"
|
|
echo " branch: $(repo_branch)"
|
|
echo " working: $(repo_is_dirty && echo 'has uncommitted changes' || echo 'clean')"
|
|
|
|
# Reported, never silently corrected. Repointing somebody's remote is a
|
|
# decision about where their work goes, and this script is not entitled to
|
|
# make it quietly.
|
|
if [[ -n "$(repo_remote)" && "$(repo_remote)" != "$OFFICER_REPO" ]]; then
|
|
echo ""
|
|
warn "this checkout points somewhere other than ${OFFICER_REPO}"
|
|
echo " Left alone. To move it:"
|
|
echo " git -C ${PLATFORM_DIR} remote set-url origin ${OFFICER_REPO}"
|
|
fi
|
|
|
|
if repo_is_dirty; then
|
|
echo ""
|
|
echo " not pulling — there are uncommitted changes here, and a pull"
|
|
echo " would either fail or bury them"
|
|
SUMMARY+=("Repository: present at ${PLATFORM_DIR}, left alone (uncommitted changes)")
|
|
elif confirm "Pull the latest changes?"; then
|
|
if pull_repo; then
|
|
ok "up to date on $(repo_branch)"
|
|
SUMMARY+=("Repository: pulled, on $(repo_branch)")
|
|
else
|
|
# --ff-only, so this means the branch has diverged rather than that the
|
|
# network failed. Saying which matters.
|
|
warn "could not fast-forward — the local branch has diverged from the remote"
|
|
ERRORS+=("Repository: pull refused, branch diverged")
|
|
SUMMARY+=("Repository: present, pull refused (diverged)")
|
|
fi
|
|
else
|
|
SUMMARY+=("Repository: present at ${PLATFORM_DIR}")
|
|
fi
|
|
|
|
else
|
|
echo " nothing there yet"
|
|
echo ""
|
|
info "Clone from ${OFFICER_REPO}?"
|
|
echo " Cloned as ${USERNAME}, not as root — a repository owned by root is"
|
|
echo " one you cannot pull, commit in, or install into."
|
|
|
|
CLONE_URL="$OFFICER_REPO"
|
|
|
|
if confirm "Clone it now?"; then
|
|
if clone_repo "$CLONE_URL"; then
|
|
ok "cloned to ${PLATFORM_DIR} on $(repo_branch)"
|
|
SUMMARY+=("Repository: cloned from ${CLONE_URL}")
|
|
else
|
|
# GIT_TERMINAL_PROMPT=0 in clone_repo means this is a real failure rather
|
|
# than a prompt nobody answered.
|
|
fail "could not clone ${CLONE_URL} — nothing below can run without it."
|
|
fi
|
|
else
|
|
fail "Nothing below can run without the repository."
|
|
fi
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 4. Dependencies
|
|
# =============================================================================
|
|
|
|
step "Dependencies"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Dependencies — bun install, as ${USERNAME}"
|
|
echo " node_modules: $(deps_installed && echo present || echo 'not there')"
|
|
echo " node-pty: $(node_pty_built && echo built || echo 'not built')"
|
|
echo ""
|
|
echo " The lockfile is frozen: bun resolves from bun.lock and nothing else,"
|
|
echo " so a package.json that disagrees with it fails rather than quietly"
|
|
echo " picking newer versions. That friction is deliberate."
|
|
echo ""
|
|
echo " node-pty has no Linux prebuild, so this compiles it from source"
|
|
echo " every time — which is what build-essential and python3 are for."
|
|
|
|
if deps_installed && node_pty_built; then
|
|
ok "already installed, and node-pty is built"
|
|
SUMMARY+=("Dependencies: already installed")
|
|
elif confirm "Install them?"; then
|
|
if install_deps; then
|
|
if node_pty_built; then
|
|
ok "installed, node-pty built"
|
|
SUMMARY+=("Dependencies: installed")
|
|
else
|
|
# The install can succeed while the native module does not get built —
|
|
# bun skips a dependency's lifecycle scripts unless it trusts the
|
|
# package. Worth naming, because the symptom is a terminal that never
|
|
# comes up rather than an install error.
|
|
warn "installed, but node-pty has no built module at node_modules/node-pty/build/Release/"
|
|
echo " The terminal sidecar cannot start without it. Try:"
|
|
echo " cd $(platform_dir) && bun install --force"
|
|
ERRORS+=("Dependencies: node-pty not built")
|
|
SUMMARY+=("Dependencies: installed, node-pty NOT built")
|
|
fi
|
|
else
|
|
warn "bun install failed"
|
|
echo " If it complained about the lockfile, package.json and bun.lock"
|
|
echo " disagree — that is the frozen lockfile doing its job, and it"
|
|
echo " wants a human to look at the diff."
|
|
ERRORS+=("Dependencies: bun install failed")
|
|
SUMMARY+=("Dependencies: FAILED")
|
|
fi
|
|
else
|
|
warn "skipped by request"
|
|
SUMMARY+=("Dependencies: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 5. Database
|
|
# =============================================================================
|
|
#
|
|
# POSTGRES_URL is set here and written by the environment section below.
|
|
|
|
step "Database"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Database — Postgres, the only one Officer has"
|
|
echo " It holds the account, passkeys, settings, dashboards, email"
|
|
echo " accounts and the job queue. Nothing else in the platform is a"
|
|
echo " database."
|
|
echo ""
|
|
# One network for everything Officer provisions. Created before the compose
|
|
# file references it, since it is declared external there.
|
|
if ensure_docker_network; then
|
|
ok "docker network '${OFFICER_NETWORK}' created"
|
|
SUMMARY+=("Docker network: ${OFFICER_NETWORK} created")
|
|
elif docker_network_exists; then
|
|
echo " network: ${OFFICER_NETWORK} (already there)"
|
|
fi
|
|
|
|
echo " compose file: $(pg_compose_exists && echo "$(pg_compose_file)" || echo 'not written yet')"
|
|
echo " container: $(pg_container_running && echo "${PG_CONTAINER} running" || echo 'not running')"
|
|
echo " port ${PG_PORT}: $(pg_port_in_use && echo 'something is listening' || echo 'free')"
|
|
|
|
POSTGRES_URL=""
|
|
|
|
# An existing compose file means this ran before. Reuse its password rather
|
|
# than minting a new one, which would leave the container and the URL
|
|
# disagreeing about the credential.
|
|
if pg_compose_exists && PG_EXISTING_PASSWORD="$(pg_password_from_env_file)"; then
|
|
POSTGRES_URL="$(pg_url "$PG_EXISTING_PASSWORD")"
|
|
echo ""
|
|
echo " already provisioned here — reusing the password from $(pg_env_file)"
|
|
pg_container_running || {
|
|
info " starting it"
|
|
pg_compose_up >/dev/null 2>&1 || true
|
|
}
|
|
if pg_wait_ready; then
|
|
ok "postgres answering on 127.0.0.1:${PG_PORT}"
|
|
SUMMARY+=("Database: existing Postgres at ${PG_CONTAINER}")
|
|
else
|
|
warn "the container is not answering — check: docker logs ${PG_CONTAINER}"
|
|
ERRORS+=("Database: provisioned but not answering")
|
|
fi
|
|
|
|
else
|
|
echo ""
|
|
info "Which Postgres should Officer use?"
|
|
echo ""
|
|
echo " [1] provision one here"
|
|
echo " ${PG_IMAGE} in $(pg_service_dir), bound to 127.0.0.1 only."
|
|
echo " Docker publishes ports by writing iptables rules beneath ufw,"
|
|
echo " so a database published to every interface is reachable from"
|
|
echo " the internet whatever the firewall says. Loopback is all the"
|
|
echo " platform needs — it runs on this machine."
|
|
echo ""
|
|
echo " [2] use one you already run"
|
|
echo " Give the connection URL. Nothing is provisioned."
|
|
echo ""
|
|
|
|
DB_PICK=""
|
|
while [[ -z "$DB_PICK" ]]; do
|
|
if ! read -rp " Which one? (1/2) [1]: " DB_CHOICE; then
|
|
echo ""
|
|
fail "No answer."
|
|
fi
|
|
case "${DB_CHOICE:-1}" in
|
|
1)
|
|
if ! command -v docker &>/dev/null; then
|
|
warn "Docker is not installed, so there is nothing to provision into."
|
|
continue
|
|
fi
|
|
if pg_port_in_use; then
|
|
warn "something is already listening on ${PG_PORT} — provisioning here would fail to bind"
|
|
echo " If that is a Postgres you already run, pick 2 and give its URL."
|
|
continue
|
|
fi
|
|
DB_PICK=provision
|
|
;;
|
|
2) DB_PICK=existing ;;
|
|
*) warn "Pick 1 or 2." ;;
|
|
esac
|
|
done
|
|
|
|
if [[ "$DB_PICK" == provision ]]; then
|
|
PG_PASSWORD="$(openssl rand -base64 32 | tr -d '/+=' | head -c 32)"
|
|
write_pg_compose "$PG_PASSWORD"
|
|
ok "compose written to $(pg_compose_file)"
|
|
if pg_compose_up && pg_wait_ready; then
|
|
POSTGRES_URL="$(pg_url "$PG_PASSWORD")"
|
|
ok "postgres answering on 127.0.0.1:${PG_PORT}, database '${PG_DATABASE}'"
|
|
SUMMARY+=("Database: provisioned at $(pg_service_dir)")
|
|
else
|
|
warn "the container did not come up — check: docker logs ${PG_CONTAINER}"
|
|
ERRORS+=("Database: container did not start")
|
|
SUMMARY+=("Database: provisioning FAILED")
|
|
fi
|
|
else
|
|
echo ""
|
|
ask_required POSTGRES_URL "Connection URL" "postgresql://user:password@host:5432/officer"
|
|
if pg_url_works "$POSTGRES_URL"; then
|
|
ok "reachable"
|
|
SUMMARY+=("Database: existing, ${POSTGRES_URL%%:*}://…")
|
|
else
|
|
# Not fatal. The URL may be right and the database not started yet, and
|
|
# refusing to continue over that would be worse than saying so.
|
|
warn "could not connect with that URL"
|
|
echo " Kept anyway — check it before running the schema step."
|
|
ERRORS+=("Database: the given URL did not answer")
|
|
SUMMARY+=("Database: existing URL kept, did not answer")
|
|
fi
|
|
fi
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# 6. Environment
|
|
# =============================================================================
|
|
|
|
step "Environment"
|
|
if ! skip; then
|
|
echo ""
|
|
info "Environment — $(env_file)"
|
|
|
|
# Read back before anything is asked. The two secrets below are kept, never
|
|
# reminted, and everything else becomes the default for its question.
|
|
ENV_JWT_SECRET="$(env_get JWT_SECRET)"
|
|
ENV_VAULT_STORE_KEY="$(env_get VAULT_STORE_KEY)"
|
|
ENV_PORT="$(env_get PORT)"
|
|
ENV_PUBLIC_URL="$(env_get PUBLIC_URL)"
|
|
ENV_MAIL_TRANSPORT="$(env_get MAIL_TRANSPORT)"
|
|
ENV_DISCORD_WEBHOOK="$(env_get DISCORD_BUG_REPORT_WEBHOOK)"
|
|
ENV_BROWSER_RELAY_PORT="$(env_get BROWSER_RELAY_PORT)"
|
|
|
|
if env_exists; then
|
|
echo " exists — its values are the defaults below, and the two secrets are kept"
|
|
else
|
|
echo " does not exist yet"
|
|
fi
|
|
|
|
# ── the secrets ──
|
|
if [[ -n "$ENV_JWT_SECRET" ]]; then
|
|
echo " JWT_SECRET: kept (regenerating it logs everybody out)"
|
|
else
|
|
ENV_JWT_SECRET="$(generate_secret)"
|
|
echo " JWT_SECRET: generated"
|
|
fi
|
|
|
|
if [[ -n "$ENV_VAULT_STORE_KEY" ]]; then
|
|
echo " VAULT_STORE_KEY: kept"
|
|
else
|
|
ENV_VAULT_STORE_KEY="$(generate_secret)"
|
|
echo " VAULT_STORE_KEY: generated"
|
|
echo ""
|
|
warn "back up VAULT_STORE_KEY somewhere safe, now."
|
|
echo " It encrypts every upstream credential the platform stores, and the"
|
|
echo " wallet's seed on top of your passphrase. Lose it and those are gone"
|
|
echo " — the passphrase does not help, because it opens the inner envelope"
|
|
echo " and this is the outer one."
|
|
fi
|
|
|
|
# ── what is asked ──
|
|
echo ""
|
|
ask_required ENV_PORT "Port Officer listens on" "${ENV_PORT:-9000}"
|
|
ENV_BROWSER_RELAY_PORT="${ENV_BROWSER_RELAY_PORT:-18792}"
|
|
|
|
echo ""
|
|
echo " PUBLIC_URL is where Officer is reached from a browser. Allowed"
|
|
echo " origins are derived from it, and passkeys are bound to its host —"
|
|
echo " so it has to be the address you actually use, not localhost, unless"
|
|
echo " localhost is genuinely it."
|
|
ask_required ENV_PUBLIC_URL "Public URL" "${ENV_PUBLIC_URL:-http://localhost:${ENV_PORT}}"
|
|
|
|
# ── origin checking, decided by the machine rather than by a default ──
|
|
#
|
|
# ALLOW_ANY_ORIGIN defaults to ON inside the platform, which CLAUDE.md says is
|
|
# only defensible because the tailnet is the perimeter. So the value is written
|
|
# explicitly here, from whether this machine actually has one.
|
|
if tailnet_present; then
|
|
ENV_ALLOW_ANY_ORIGIN="true"
|
|
ORIGIN_WHY="tailscale0 is up, so the tailnet is the perimeter"
|
|
else
|
|
ENV_ALLOW_ANY_ORIGIN="false"
|
|
ORIGIN_WHY="no tailnet on this machine, so origin checking is left ON"
|
|
fi
|
|
echo ""
|
|
echo " ALLOW_ANY_ORIGIN=${ENV_ALLOW_ANY_ORIGIN} — ${ORIGIN_WHY}"
|
|
|
|
echo ""
|
|
ENV_MAIL_TRANSPORT="${ENV_MAIL_TRANSPORT:-}"
|
|
read -rp " Mail transport, blank for none [${ENV_MAIL_TRANSPORT}]: " REPLY_MAIL || true
|
|
ENV_MAIL_TRANSPORT="${REPLY_MAIL:-$ENV_MAIL_TRANSPORT}"
|
|
|
|
echo ""
|
|
echo " to write:"
|
|
echo " PORT=${ENV_PORT} BROWSER_RELAY_PORT=${ENV_BROWSER_RELAY_PORT}"
|
|
echo " PUBLIC_URL=${ENV_PUBLIC_URL}"
|
|
echo " ALLOW_ANY_ORIGIN=${ENV_ALLOW_ANY_ORIGIN}"
|
|
echo " DATA_PATH=${OFFICER_ROOT}/data"
|
|
echo " OFFICER_ITEMS_DIR=${OFFICER_ROOT}/capabilities"
|
|
echo " HOME_DIR=${USER_HOME}"
|
|
echo " POSTGRES_URL=${POSTGRES_URL%%:*}://…"
|
|
echo " JWT_SECRET, VAULT_STORE_KEY — not shown"
|
|
echo ""
|
|
|
|
if confirm "Write it?"; then
|
|
write_env
|
|
ok "written, 0600, owned by ${USERNAME}"
|
|
[[ -f "$(env_file).before-officer-setup" ]] && echo " previous kept as $(env_file).before-officer-setup"
|
|
SUMMARY+=("Environment: $(env_file)")
|
|
else
|
|
warn "skipped by request"
|
|
SUMMARY+=("Environment: SKIPPED by request")
|
|
fi
|
|
step_ok
|
|
fi
|
|
|
|
# =============================================================================
|
|
# NOT BUILT YET
|
|
# =============================================================================
|
|
# 6 Schema db:push
|
|
# 7 Build gen:index
|
|
# 8 Services pm2 startOrRestart · save · startup
|
|
# 9 Verify are the processes actually up
|
|
|
|
echo ""
|
|
echo -e "${BOLD} Pre-flight complete.${NC} The remaining sections are not built yet."
|
|
echo ""
|