the install root is derived, not configured
Seven variables out of .env. DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone from the code entirely; PUBLIC_URL, PUBLIC_BUILD_ENV, JWT_SECRET and VAULT_STORE_KEY are no longer written by the setup script. data-path.ts now derives OFFICER_ROOT as dirname(process.cwd()), with data/, capabilities/ and dockers/ as fixed names under it. The direction used to run the other way — DATA_PATH from env, then OFFICER_ROOT = dirname(DATA_PATH) in app-store/paths.ts — which meant three environment variables that had to agree with each other and with the tree on disk. Eight files re-read process.env.DATA_PATH independently, each with its own `?? cwd()/data` fallback. They import the one value now, which is what made removing it safe: otherwise each would have derived its own and drifted. Three things this turned up. The cwd pin in ecosystem.profile.cjs was broken. It set `cwd: __dirname` under a comment asserting "__dirname is the repo root — this file sits beside ecosystem.config.cjs", which stopped being true when these files moved into ecosystem-files/. It walks up to the platform's package.json now, which holds wherever the file lives. That was a live bug before this change and a load-bearing one after it, since cwd now decides where the install is. assertInstallLayout joins the other two boot assertions. A wrong cwd does not error — it computes a plausible root somewhere else and writes managed homes and agent runs into it, so the install looks empty and the data looks lost with nothing naming the cause. It throws before serve(), first of the three, because a wrong answer there makes the other two check the wrong files. getOwnerHomeDir captures homedir() once at module load rather than per call. Measured on bun 1.3.10: both os.homedir() and os.userInfo().homedir return $HOME when set rather than reading passwd, and user-instance.ts assigns process.env.HOME on its way to spawning an agent. A lazy read would have returned the owner's home on the first call and a member's afterwards. data-path.ts imports only node builtins, so it is evaluated before any of that runs. JWT_SECRET and VAULT_STORE_KEY leaving .env means an install made by this script does not boot — jwt.ts throws at module load without one. That is the agreed sequencing: they move to the SQLite store (docs/secret-store.md), and writing them here meanwhile would create a second origin for a secret the store then has to be reconciled with. Said plainly in .env.example and in lib/env.sh rather than left to be discovered. Not typechecked: node_modules is empty here and installs are frozen. Every edited file parses under `bun build --no-bundle`; the profile loads and pins the right cwd; assertInstallLayout was exercised from both the repo and /tmp; the setup section was run and writes five variables. Prettier was NOT run — 3.9.6 via bunx is not the pinned resolution and reformatted unrelated unions and line wraps in six files, so those were reverted and the edits re-applied by hand. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -458,54 +458,22 @@ 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)"
|
||||
# Read back before anything is asked; existing values become the defaults.
|
||||
ENV_PORT="$(env_get PORT)"
|
||||
ENV_PUBLIC_URL="$(env_get PUBLIC_URL)"
|
||||
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"
|
||||
echo " exists — its values are the defaults below"
|
||||
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
|
||||
@@ -524,13 +492,12 @@ if ! skip; then
|
||||
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 ""
|
||||
echo " the install root is not written here — the platform derives it as the"
|
||||
echo " parent of the repo, so data/, capabilities/ and dockers/ follow from"
|
||||
echo " ${OFFICER_ROOT} without anything having to agree with anything."
|
||||
echo ""
|
||||
|
||||
if confirm "Write it?"; then
|
||||
|
||||
@@ -5,30 +5,24 @@
|
||||
#
|
||||
# Definitions only.
|
||||
#
|
||||
# ── Two secrets that must never be regenerated ──
|
||||
# ── What is NOT here ──
|
||||
#
|
||||
# JWT_SECRET signs every session token. Minting a new one logs everybody out of
|
||||
# every device, silently — the symptom is people being signed out for no stated
|
||||
# reason. The original regenerated it on every run that answered "yes" to
|
||||
# regenerating .env.
|
||||
# JWT_SECRET and VAULT_STORE_KEY are not written. They are moving into the SQLite
|
||||
# key store (docs/secret-store.md), and writing them here in the meantime would
|
||||
# mean generating a value that the store then has to be reconciled with — two
|
||||
# origins for one secret, which is the failure the store exists to end.
|
||||
#
|
||||
# VAULT_STORE_KEY is worse, and the original never wrote it at all — so a
|
||||
# scripted install had no key and the vault and wallet refused to store anything.
|
||||
# It encrypts every upstream credential the platform holds (see docs/secret-store.md
|
||||
# for the full list) and, on top of the owner passphrase, the BIP39 seed envelope.
|
||||
# Changing it makes all of them unreadable, and for the seed that is unrecoverable:
|
||||
# the passphrase opens the inner envelope, and the outer one is gone. Unless the
|
||||
# mnemonic was written down offline, so are the coins.
|
||||
#
|
||||
# Both are read back from an existing .env and kept. Both are slated to move into
|
||||
# the secret store — docs/secret-store.md — which is what makes changing them an
|
||||
# operation rather than data loss.
|
||||
# The consequence is honest and deliberate: jwt.ts throws at module load without
|
||||
# JWT_SECRET, so an install made by this script does not boot until the store
|
||||
# lands. That sequencing was chosen rather than stumbled into.
|
||||
#
|
||||
# ── Derived, not asked ──
|
||||
#
|
||||
# DATA_PATH and OFFICER_ITEMS_DIR come from $OFFICER_ROOT. They were two separate
|
||||
# questions in the original, which had to agree with each other and with where the
|
||||
# app store looks.
|
||||
# DATA_PATH, OFFICER_ITEMS_DIR and HOME_DIR are gone too, and this time nothing
|
||||
# replaces them. The platform derives the install root as the parent of its own
|
||||
# working directory, so data/, capabilities/ and dockers/ follow from the layout
|
||||
# on disk, and the owner's home comes from the OS. They were three environment
|
||||
# variables that had to agree with each other and with the directory tree.
|
||||
|
||||
[[ -n "${OFFICER_SETUP_ENV_LOADED:-}" ]] && return 0
|
||||
OFFICER_SETUP_ENV_LOADED=1
|
||||
@@ -50,10 +44,6 @@ env_get() {
|
||||
}' "$(env_file)"
|
||||
}
|
||||
|
||||
# Long enough to be worth having, and stripped of characters that would need
|
||||
# quoting in a file everything reads with a naive parser.
|
||||
generate_secret() { openssl rand -base64 48 | tr -d '/+=\n' | head -c 48; }
|
||||
|
||||
# Origin checking is OFF unless this is explicitly false — CLAUDE.md is explicit
|
||||
# that the inversion is deliberate and is only defensible because the tailnet is
|
||||
# the perimeter. With no tailnet there is no perimeter, so the default stops
|
||||
@@ -80,33 +70,12 @@ write_env() {
|
||||
# holds the token-signing secret and the database credential.
|
||||
|
||||
PORT="${ENV_PORT}"
|
||||
PUBLIC_URL="${ENV_PUBLIC_URL}"
|
||||
PUBLIC_BUILD_ENV="production"
|
||||
|
||||
# The browser relay listens on its own port, separate from the app.
|
||||
BROWSER_RELAY_PORT="${ENV_BROWSER_RELAY_PORT}"
|
||||
|
||||
# ── Do not regenerate either of these ──
|
||||
#
|
||||
# JWT_SECRET signs every session token. A new one logs everybody out, everywhere.
|
||||
JWT_SECRET="${ENV_JWT_SECRET}"
|
||||
|
||||
# VAULT_STORE_KEY encrypts every upstream credential in Postgres, and encrypts
|
||||
# the wallet's seed envelope on top of the owner passphrase. Changing it makes
|
||||
# all of them unreadable — and for the seed that is unrecoverable, passphrase or
|
||||
# not. Back it up with the same seriousness as the mnemonics.
|
||||
VAULT_STORE_KEY="${ENV_VAULT_STORE_KEY}"
|
||||
|
||||
POSTGRES_URL="${POSTGRES_URL}"
|
||||
|
||||
# Derived from the install root — see scripts/setup/officer-setup/lib/layout.sh.
|
||||
DATA_PATH="${OFFICER_ROOT}/data"
|
||||
OFFICER_ITEMS_DIR="${OFFICER_ROOT}/capabilities"
|
||||
|
||||
# The owner's real login home, which is where terminals, chats and task runs
|
||||
# actually execute — as opposed to the managed home under DATA_PATH.
|
||||
HOME_DIR="${USER_HOME}"
|
||||
|
||||
# Origin checking. Off by default in the platform, which is only safe behind a
|
||||
# tailnet; written explicitly here so the machine's actual situation decides it.
|
||||
ALLOW_ANY_ORIGIN="${ENV_ALLOW_ANY_ORIGIN}"
|
||||
|
||||
Reference in New Issue
Block a user