diff --git a/scripts/deploy-remote.sh b/scripts/deploy-remote.sh index d3d3e6d..717605d 100755 --- a/scripts/deploy-remote.sh +++ b/scripts/deploy-remote.sh @@ -1348,6 +1348,7 @@ LLM_DRAGOON_NPU_MODELS="${LLM_DRAGOON_NPU_MODELS:-qwen3-embedding-0.6b}" LLM_DRAGOON_NPU_DIR="${LLM_DRAGOON_NPU_DIR:-/home/dank/models/halogen-npu}" LLM_DRAGOON_DEV_ACCEL="${LLM_DRAGOON_DEV_ACCEL:-/dev/accel/accel0}" LLM_DRAGOON_XRT_DIR="${LLM_DRAGOON_XRT_DIR:-/usr/lib/x86_64-linux-gnu}" +LLM_DRAGOON_NETWORK="${LLM_DRAGOON_NETWORK:-dankclaw_default}" retire_halogen_dragoon_quadlet() { local trash="$HOME/.AGENTTRASH/20260919-orca-cutover/quadlet" @@ -1426,6 +1427,14 @@ deploy_llm_dragoon() { fi done + # The container network has to exist before the run line names it (a typo + # there is a restart-looping unit, not a config error anyone reads). + if [[ -n "$LLM_DRAGOON_NETWORK" ]] && ! podman network exists "$LLM_DRAGOON_NETWORK"; then + echo "ERROR: podman network $LLM_DRAGOON_NETWORK does not exist" >&2 + echo "frank-dragoon: create it or blank FRANK_DRAGOON_NETWORK (pasta default)" >&2 + return 1 + fi + # --- NPU lane: the same three predicates the serve script checks --------- if [[ -n "$LLM_DRAGOON_NPU_MODELS" ]]; then if [[ ! -e "$LLM_DRAGOON_DEV_ACCEL" ]]; then diff --git a/scripts/frank-dragoon.env b/scripts/frank-dragoon.env index 61dd3bb..ee67a94 100644 --- a/scripts/frank-dragoon.env +++ b/scripts/frank-dragoon.env @@ -110,3 +110,12 @@ FRANK_DRAGOON_XRT_DIR=/usr/lib/x86_64-linux-gnu # sysfs root the fabric-clock preflight reads (same knob the vendor's # halogen-fabric-clock status reads; the seam points it at a fixture). FRANK_DRAGOON_SYSFS=/sys +# Container network. Empty = podman's rootless default (`pasta`): a private +# netns that nothing else can join — podman refuses a second network while the +# primary is pasta, the published port is refused from dankclaw_default +# containers, and host.containers.internal resolves link-local, which +# open-notebook's URL guard refuses to store. Named = the app bridge, so +# open-notebook can dial http://frank-dragoon-halogen:8731 for the NPU +# endpoints. The engine's unauthenticated 8730 stays private either way (it +# binds the container's own loopback); only the API port becomes name-resolvable. +FRANK_DRAGOON_NETWORK=dankclaw_default diff --git a/scripts/serve-frank-dragoon.sh b/scripts/serve-frank-dragoon.sh index 4c121db..9f90f27 100755 --- a/scripts/serve-frank-dragoon.sh +++ b/scripts/serve-frank-dragoon.sh @@ -187,6 +187,24 @@ if [ -n "$NPU_MODELS" ]; then -e HALOGEN_NPU_MODELS="$NPU_MODELS") echo "frank-dragoon: NPU lane armed: $NPU_MODELS on $DEV_ACCEL (fabric clock held)" >&2 fi + +# Container network. Empty = podman's rootless default (`pasta`), which is a +# private netns and CANNOT be joined by anything else: podman refuses a second +# network while the primary is pasta ("cannot set multiple networks without +# bridge network mode"), the endpoint's published port answers 200 on the host +# and on the LAN but is REFUSED from a dankclaw_default container (measured +# 2026-10-19), and `host.containers.internal:7770` answers 200 yet resolves to +# 169.254.1.2 — link-local, which open-notebook's URL guard refuses to store. +# So an endpoint on pasta has no address an app container can use; naming the +# app bridge here is the only shape that lets open-notebook dial +# http://frank-dragoon-halogen:8731 for the NPU endpoints. It does not widen +# the surface: the engine's unauthenticated 8730 is bound to the container's +# OWN loopback (/proc/net/tcp: 127.0.0.1:8730 vs 0.0.0.0:8731, checked the same +# day), so only the API port is reachable by name. +NET_ARGS=() +if [ -n "${FRANK_DRAGOON_NETWORK:-}" ]; then + NET_ARGS=(--network "$FRANK_DRAGOON_NETWORK") +fi # Host-RAM preflight — the engine's pin gate, checked BEFORE podman run. # The engine refuses to pin when MemAvailable is below the checkpoint plus # a 16 GiB floor: pinned pages cannot be swapped, and under the floor the @@ -261,6 +279,7 @@ podman run --name "$FRANK_DRAGOON_CONTAINER" \ ${HALOGEN_WEIGHTS_LOCK:+-e HALOGEN_WEIGHTS_LOCK="$HALOGEN_WEIGHTS_LOCK"} \ -v "$FRANK_DRAGOON_MODEL_DIR:/models:ro" \ -v "$FRANK_DRAGOON_MODEL_DIR/tokenizer:/tokenizer:ro" \ + "${NET_ARGS[@]}" \ "${NPU_ARGS[@]}" \ "$FRANK_DRAGOON_IMAGE" \ > "$LOG" 2>&1 diff --git a/tests/frank_dragoon_unit.bats b/tests/frank_dragoon_unit.bats index 76f1115..f0e35e3 100755 --- a/tests/frank_dragoon_unit.bats +++ b/tests/frank_dragoon_unit.bats @@ -357,6 +357,8 @@ $(cat "$TEST_TMP/serve.log")" npu_models="$(sed -nE 's/^HALOGEN_NPU_MODELS=(.*)$/\1/p' "$ENV_FILE")" grep -qF "LLM_DRAGOON_NPU_DIR=\"\${LLM_DRAGOON_NPU_DIR:-$npu_dir}\"" "$DEPLOYER" grep -qF "LLM_DRAGOON_NPU_MODELS=\"\${LLM_DRAGOON_NPU_MODELS:-$npu_models}\"" "$DEPLOYER" + net="$(sed -nE 's/^FRANK_DRAGOON_NETWORK=(.*)$/\1/p' "$ENV_FILE")" + grep -qF "LLM_DRAGOON_NETWORK=\"\${LLM_DRAGOON_NETWORK:-$net}\"" "$DEPLOYER" } @test "the deployer points a missing artifact at the v2abl fetch script" { @@ -583,3 +585,24 @@ $(cat "$TEST_TMP/serve.log")" # and curl drops Range across it (the sibling 62 GiB fetch, 2026-10-18). grep -qE 'url_effective' "$NPU_FETCH" } + +@test "the container network is podman's default when unnamed and the app bridge when named" { + # The app-to-endpoint leg hangs on this: pasta is a private netns nothing + # else can join (podman refuses a second network, the published port is + # refused from app containers, and host.containers.internal is link-local, + # which open-notebook refuses to store), so reaching the NPU endpoints by + # name means the container lives on dankclaw_default. Both arms are + # observable in the command podman would run. + podman_stub + good_env + run_with_log FRANK_DRAGOON_NETWORK= + [ "$status" -eq 0 ] + ! grep -q -- '--network' "$TEST_TMP/serve.log" + run_with_log FRANK_DRAGOON_NETWORK=dankclaw_default + [ "$status" -eq 0 ] + [[ "$output" == *"--network dankclaw_default"* ]] + # The deployer refuses a network that does not exist rather than starting a + # unit that restart-loops on it. + grep -qE 'podman network exists' "$DEPLOYER" + grep -qE '^FRANK_DRAGOON_NETWORK=dankclaw_default$' "$ENV_FILE" +}