Files
rohitg00__agentmemory/deploy/fly/entrypoint.sh
yinhewang f40631e492 fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST (#442)
* fix(viewer): allow non-loopback bind via AGENTMEMORY_VIEWER_HOST

`startViewerServer` hardcoded `server.listen(port, "127.0.0.1")`, so
Fly's `fly-local-6pn` traffic was RST and `fly proxy 3113:3113` reported
"Recv failure: Connection reset by peer" (#434).

Bind host is now read from `AGENTMEMORY_VIEWER_HOST` (default
`127.0.0.1`). To keep the viewer's bearer-authorized proxy from
silently exposing itself on non-loopback binds, the same code path now:

  - drops loopback hostnames from the default Host allowlist when bind
    is non-loopback;
  - refuses to start unless both `VIEWER_ALLOWED_HOSTS` and
    `AGENTMEMORY_SECRET` are set in that mode;
  - requires `Authorization: Bearer $AGENTMEMORY_SECRET` (timing-safe
    compare from src/auth.ts) on every /agentmemory/* request; HTML
    and the favicon stay open.

`deploy/fly/Dockerfile` no longer bakes `AGENTMEMORY_VIEWER_HOST=::`.
The entrypoint exports it (and a `VIEWER_ALLOWED_HOSTS` default scoped
to the Host headers `fly proxy` emits) only when `FLY_APP_NAME` or
`FLY_ALLOC_ID` is present, so a `docker run -p 3113:3113` of the image
elsewhere stays loopback-only.

Tests: 29 cases in test/viewer-host.test.ts.

* fix(viewer): secure Fly proxy access

* fix(viewer): address CodeRabbit review on PR #442

- Add aria-label + aria-describedby to the unlock input so screen
  readers announce 'AGENTMEMORY_SECRET' instead of relying on placeholder
  text alone (major a11y gap on the only path through the auth gate).
- Call fetchFlags() after a successful unlock so footer version + flag
  banners refresh without a full page reload; bootstrap-only fetch left
  the UI stale when config/flags was the first request that 401'd.
- Log the actual bound port in the listening handler. server.listen(0)
  callers (tests + ephemeral-port users) saw 'http://localhost:0' in
  the startup log because currentPort stayed 0 while server.address()
  already exposed the real port.
- Cache the favicon buffer at module load instead of doing a synchronous
  readFileSync on every /favicon.svg request.
- README: 'preseeds' -> 'pre-seeds'.
- Clarify the VIEWER_ALLOWED_HOSTS cache comment: readAllowedHostsOverride
  is called per request, but buildAllowedHosts caches the result for the
  lifetime of the listener — production env changes after the first
  request require a restart.

---------

Co-authored-by: Rohit Ghumare <ghumare64@gmail.com>
2026-06-02 10:17:33 +01:00

113 lines
3.5 KiB
Bash
Executable File

#!/bin/sh
# agentmemory first-boot entrypoint.
#
# Runs as root so it can:
# 1. Overwrite the npm-bundled iii-config.yaml (which binds 127.0.0.1
# and uses relative ./data paths) with a deploy-tuned version that
# binds 0.0.0.0 and uses absolute /data paths.
# 2. chown the platform-mounted /data volume to the runtime user
# (managed platforms mount volumes root-owned 755 by default).
# 3. Generate the HMAC secret on first boot and persist it to
# /data/.hmac (chmod 600) so the secret survives restarts.
#
# Then it execs the agentmemory CLI under gosu as the unprivileged
# `node` user.
set -eu
DATA_DIR="${AGENTMEMORY_DATA_DIR:-/data}"
HMAC_FILE="${AGENTMEMORY_HMAC_FILE:-/data/.hmac}"
RUN_AS="node:node"
III_CONFIG="/opt/agentmemory/node_modules/@agentmemory/agentmemory/dist/iii-config.yaml"
mkdir -p "$DATA_DIR"
chown -R "$RUN_AS" "$DATA_DIR"
cat > "$III_CONFIG" <<'EOF'
workers:
- name: iii-http
config:
port: 3111
host: 0.0.0.0
default_timeout: 180000
cors:
allowed_origins:
- "http://localhost:3111"
- "http://localhost:3113"
- "http://127.0.0.1:3111"
- "http://127.0.0.1:3113"
allowed_methods: [GET, POST, PUT, DELETE, OPTIONS]
- name: iii-state
config:
adapter:
name: kv
config:
store_method: file_based
file_path: /data/state_store.db
- name: iii-queue
config:
adapter:
name: builtin
- name: iii-pubsub
config:
adapter:
name: local
- name: iii-cron
config:
adapter:
name: kv
- name: iii-stream
config:
port: 3112
host: 0.0.0.0
adapter:
name: kv
config:
store_method: file_based
file_path: /data/stream_store
- name: iii-observability
config:
enabled: true
service_name: agentmemory
exporter: memory
sampling_ratio: 1.0
metrics_enabled: true
logs_enabled: true
logs_console_output: true
EOF
chown "$RUN_AS" "$III_CONFIG"
if [ ! -s "$HMAC_FILE" ]; then
SECRET="$(openssl rand -hex 32)"
umask 077
printf '%s\n' "$SECRET" > "$HMAC_FILE"
chmod 600 "$HMAC_FILE"
chown "$RUN_AS" "$HMAC_FILE"
echo "================================================================"
echo "agentmemory: generated HMAC secret on first boot"
echo "AGENTMEMORY_SECRET=$SECRET"
echo "Copy this value now. It will not be printed again."
echo "Stored at: $HMAC_FILE (chmod 600)"
echo "To rotate: delete $HMAC_FILE on the persistent volume and restart."
echo "================================================================"
fi
AGENTMEMORY_SECRET="$(cat "$HMAC_FILE")"
export AGENTMEMORY_SECRET
# The viewer's default 127.0.0.1 bind is unreachable through fly proxy,
# which enters the machine via fly-local-6pn (IPv6). Opt into a
# non-loopback bind ONLY when we're actually inside Fly (detected via
# Fly's runtime variables). A plain `docker run` of this image will not
# see these variables and will keep the safe-by-default loopback bind,
# so it can't silently expose the viewer's bearer-authorized proxy to
# the LAN. VIEWER_ALLOWED_HOSTS is preseeded to the Host headers that
# `fly proxy 3113:3113` actually produces on the operator's laptop.
if [ -n "${FLY_APP_NAME:-}" ] || [ -n "${FLY_ALLOC_ID:-}" ]; then
: "${AGENTMEMORY_VIEWER_HOST:=::}"
: "${VIEWER_ALLOWED_HOSTS:=localhost:3113,127.0.0.1:3113,[::1]:3113}"
export AGENTMEMORY_VIEWER_HOST VIEWER_ALLOWED_HOSTS
fi
exec gosu "$RUN_AS" agentmemory "$@"