mirror of
https://github.com/akitaonrails/ai-memory.git
synced 2026-10-02 03:24:46 +08:00
fix(upgrade): reconstruct the docker run for non-compose containers
`ai-memory upgrade` could not recreate a container started with plain `docker run`, so it told the operator to stop and remove it and rebuild the command from memory — losing ports, mounts and environment in the process. The old comment claimed the original args were unknowable; they are not, `docker inspect` has them. Reconstructs that container's own stop/remove/run — name, restart policy, published ports, mounts, operator-set environment, and an overridden command — and writes it for review before it is run. Written to a 0600 file rather than echoed: a real install's environment carries provider API keys and AI_MEMORY_AUTH_TOKEN, and printing those into terminal scrollback or a piped install log is the kind of leak this project exists to prevent. Environment already baked into the image is omitted, so the new image's defaults are not frozen to the old ones. Verified round-trip against live containers: the generated command recreates an identical container, including an environment value containing a space, a double quote and a `$`. Refs #407 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
b3d1c222d5
commit
789b65e196
@@ -44,6 +44,37 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
## [1.28.0] - 2026-08-17
|
||||
|
||||
### Fixed
|
||||
- Docker containers no longer refuse to start unauthenticated, which had left
|
||||
every container from the README Quick start crash-looping since v1.27.0
|
||||
(#407). The v1.27.0 bind guard reads a non-loopback bind as evidence of
|
||||
network exposure and refuses without a token. That inference holds on a
|
||||
host, but not inside a container: publishing a port with `-p` *requires*
|
||||
binding `0.0.0.0` in the namespace, and whether that port reaches the
|
||||
network is decided by the host-side publish spec — `-p 127.0.0.1:49374:49374`
|
||||
versus `-p 0.0.0.0:49374:49374` — which the process cannot observe. The
|
||||
documented Quick start passes no token and published to loopback, so it was
|
||||
safe and refused anyway; with the documented `--restart unless-stopped` that
|
||||
became a restart loop. Containers now log a loud warning naming the publish
|
||||
spec as the thing to check, instead of refusing. **The host rule is
|
||||
unchanged** — an unauthenticated non-loopback bind outside a container is
|
||||
still refused. Containers are detected via `/.dockerenv` (Docker),
|
||||
`/run/.containerenv` (Podman), or `AI_MEMORY_IN_CONTAINER`, which the
|
||||
official image now sets.
|
||||
|
||||
Note the `Host` allowlist does not substitute for a token here: it defends
|
||||
against DNS rebinding, where a *browser* sets the header. A client that can
|
||||
route to the port sets `Host` freely. If you publish ai-memory beyond
|
||||
loopback, set `AI_MEMORY_AUTH_TOKEN`.
|
||||
- `ai-memory upgrade` no longer tells non-compose Docker users to delete their
|
||||
container and rebuild the `docker run` from memory (#407). It now reconstructs
|
||||
that container's own stop/remove/run — name, restart policy, published ports,
|
||||
mounts, operator-set environment, and an overridden command — and writes it to
|
||||
`${XDG_CACHE_HOME:-~/.cache}/ai-memory/recreate-ai-memory.sh` for review before
|
||||
you run it. The script is written mode 0600 and never echoed, because a real
|
||||
install's environment carries provider API keys and `AI_MEMORY_AUTH_TOKEN`.
|
||||
Environment already baked into the image is deliberately omitted, so the new
|
||||
image's own defaults are not frozen to the old ones. Compose-based installs
|
||||
are unaffected and still upgrade via `docker compose up -d`.
|
||||
- `install-hooks --agent codex` and `uninstall` now honor `CODEX_HOME`, instead
|
||||
of always writing to `~/.codex/hooks.json`. Codex loads hooks from its
|
||||
configured home, so on an install with `CODEX_HOME` set the hooks landed
|
||||
|
||||
+101
-4
@@ -139,6 +139,91 @@ self_upgrade_script() {
|
||||
AI_MEMORY_SKIP_SELF_UPGRADE=1 exec "${script_path}" upgrade
|
||||
}
|
||||
|
||||
|
||||
# Reconstruct the `docker run` that created a running container, so a
|
||||
# non-compose install can be recreated on the freshly pulled image without
|
||||
# the operator having to remember their original flags (issue #407).
|
||||
#
|
||||
# Written to a 0600 file rather than echoed: the environment of a real
|
||||
# install carries provider API keys and AI_MEMORY_AUTH_TOKEN, and printing
|
||||
# those into terminal scrollback — or into a piped install log — is exactly
|
||||
# the kind of leak this project exists to prevent.
|
||||
#
|
||||
# Reconstructs the flags that matter for an ai-memory container: name,
|
||||
# restart policy, published ports, mounts, operator-supplied environment,
|
||||
# and an overridden command. It does NOT reproduce every possible `docker
|
||||
# run` flag (networks, capabilities, resource limits, devices); the script
|
||||
# says so in its own header so a reader can add anything exotic back.
|
||||
emit_docker_run_script() {
|
||||
local container="$1" out="$2" image
|
||||
image="$("${DOCKER}" inspect "${container}" --format '{{.Config.Image}}' 2>/dev/null)" || return 1
|
||||
[ -n "${image}" ] || return 1
|
||||
|
||||
local ports volumes restart cmd
|
||||
ports="$("${DOCKER}" inspect "${container}" --format \
|
||||
'{{range $p, $bindings := .HostConfig.PortBindings}}{{range $bindings}}-p {{if .HostIp}}{{.HostIp}}:{{end}}{{.HostPort}}:{{$p}} {{end}}{{end}}' 2>/dev/null)"
|
||||
volumes="$("${DOCKER}" inspect "${container}" --format \
|
||||
'{{range .Mounts}}{{if eq .Type "volume"}}-v {{.Name}}:{{.Destination}} {{else}}-v {{.Source}}:{{.Destination}} {{end}}{{end}}' 2>/dev/null)"
|
||||
restart="$("${DOCKER}" inspect "${container}" --format \
|
||||
'{{with .HostConfig.RestartPolicy.Name}}{{if ne . "no"}}--restart {{.}}{{end}}{{end}}' 2>/dev/null)"
|
||||
|
||||
# Only the command if the operator overrode the image default; otherwise
|
||||
# let the new image supply its own (that is the point of upgrading).
|
||||
local image_cmd container_cmd
|
||||
image_cmd="$("${DOCKER}" inspect "${image}" --format '{{json .Config.Cmd}}' 2>/dev/null)"
|
||||
container_cmd="$("${DOCKER}" inspect "${container}" --format '{{json .Config.Cmd}}' 2>/dev/null)"
|
||||
cmd=""
|
||||
if [ "${image_cmd}" != "${container_cmd}" ]; then
|
||||
cmd="$("${DOCKER}" inspect "${container}" --format \
|
||||
'{{range .Config.Cmd}}{{printf "%q " .}}{{end}}' 2>/dev/null)"
|
||||
fi
|
||||
|
||||
# Likewise, carry only environment the operator actually passed. Anything
|
||||
# baked into the image is re-applied by the new image anyway, and pinning
|
||||
# it here would freeze a value this upgrade is meant to move forward.
|
||||
local image_env container_env env_flags=""
|
||||
image_env="$("${DOCKER}" inspect "${image}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null)"
|
||||
container_env="$("${DOCKER}" inspect "${container}" --format '{{range .Config.Env}}{{println .}}{{end}}' 2>/dev/null)"
|
||||
local kv
|
||||
while IFS= read -r kv; do
|
||||
[ -n "${kv}" ] || continue
|
||||
if printf '%s\n' "${image_env}" | grep -qxF -- "${kv}"; then
|
||||
continue
|
||||
fi
|
||||
env_flags="${env_flags} -e $(printf '%q' "${kv}")"
|
||||
done <<ENVEOF
|
||||
${container_env}
|
||||
ENVEOF
|
||||
|
||||
( umask 077 && : > "${out}" ) || return 1
|
||||
{
|
||||
echo "#!/usr/bin/env bash"
|
||||
echo "# Reconstructed by \`ai-memory upgrade\` from the running '${container}'"
|
||||
echo "# container, before it was removed. Review it, then run it."
|
||||
echo "#"
|
||||
echo "# Contains this install's environment (API keys, auth token) — it is"
|
||||
echo "# mode 0600 for that reason. Delete it once the container is back up."
|
||||
echo "#"
|
||||
echo "# Covers name, restart policy, ports, mounts, operator-set environment"
|
||||
echo "# and an overridden command. If your original command used anything"
|
||||
echo "# else (custom network, capabilities, resource limits), re-add it."
|
||||
echo "set -euo pipefail"
|
||||
echo
|
||||
echo "docker stop ${container}"
|
||||
echo "docker rm ${container}"
|
||||
printf 'docker run -d --name %s' "${container}"
|
||||
[ -n "${restart}" ] && printf ' %s' "${restart}"
|
||||
[ -n "${ports}" ] && printf ' %s' "${ports% }"
|
||||
[ -n "${volumes}" ] && printf ' %s' "${volumes% }"
|
||||
[ -n "${env_flags}" ] && printf '%s' "${env_flags}"
|
||||
printf ' %s' "${image}"
|
||||
[ -n "${cmd}" ] && printf ' %s' "${cmd% }"
|
||||
printf '\n'
|
||||
} >> "${out}"
|
||||
chmod 600 "${out}" 2>/dev/null || true
|
||||
return 0
|
||||
}
|
||||
|
||||
cmd_upgrade() {
|
||||
if [ -z "${AI_MEMORY_SKIP_SELF_UPGRADE:-}" ]; then
|
||||
self_upgrade_script
|
||||
@@ -206,10 +291,22 @@ cmd_upgrade() {
|
||||
else
|
||||
echo "→ a local ai-memory container is running but no compose file"
|
||||
echo " was found in \$PWD/docker-compose.yml, ./docker/docker-compose.yml,"
|
||||
echo " or ~/deploy/ai-memory/docker-compose.yml. Restart it manually so"
|
||||
echo " the new image takes effect:"
|
||||
echo " docker stop ai-memory && docker rm ai-memory"
|
||||
echo " # then re-run your docker-run command from the README Quick start"
|
||||
echo " or ~/deploy/ai-memory/docker-compose.yml."
|
||||
local recreate_script="${CACHE_DIR}/recreate-ai-memory.sh"
|
||||
mkdir -p "${CACHE_DIR}" 2>/dev/null || true
|
||||
if emit_docker_run_script "ai-memory" "${recreate_script}"; then
|
||||
echo " Wrote the equivalent stop/remove/run for THIS container to:"
|
||||
echo " ${recreate_script}"
|
||||
echo " Review it and run it so the new image takes effect:"
|
||||
echo " less ${recreate_script} && bash ${recreate_script}"
|
||||
echo " (mode 0600 — it carries the environment of this install, including"
|
||||
echo " any API keys and AI_MEMORY_AUTH_TOKEN. Delete it once you are up.)"
|
||||
else
|
||||
echo " Could not inspect the running container to reconstruct its"
|
||||
echo " command. Restart it manually so the new image takes effect:"
|
||||
echo " docker stop ai-memory && docker rm ai-memory"
|
||||
echo " # then re-run your docker-run command from the README Quick start"
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
Reference in New Issue
Block a user