Author SHA1 Message Date
AVB 000792f702 ai: persist FlowAgent MSAL token cache via mcpo bind mount (#18)
ci/woodpecker/push/deploy Pipeline was successful
https://ai.bryanmail.net/s/0255f5cf-cb2a-4f8d-a782-caacc9dfd62f
2026-09-09 08:45:02 -07:00
Bot 9316a32ba5 ai: persist FlowAgent MSAL token cache via mcpo bind mount
Adds the missing bind mount for FlowAgent's delegated-user MSAL disk
token cache:

    /volume1/docker/mcpo/flowagent-auth:/app/flowagent-auth

Confirmed via `docker service inspect ai_mcpo --format '{{json .Spec.TaskTemplate.ContainerSpec.Mounts}}'`-equivalent review of the current
compose that mcpo's mounts were only: ssh_keys, config.json, mcpo_data,
uv-cache -- flowagent-auth (set as HOME for the flowagent subprocess via
config.json.template's env.HOME=/app/flowagent-auth) was NOT persisted,
so any successful delegated-user MSAL sign-in (list_connections /
create_connection / resolve_refs / pick_or_create_connection /
resolve_params -- the tools that use MsalTokenProvider, see companion
homelab/mcp-config PR #9) would be silently lost on every mcpo restart,
forcing a fresh interactive sign-in every time.

Host dir already exists (pre-created on shared CephFS, identical across
all three docker LXCs):
    mkdir -p /volume1/docker/mcpo/flowagent-auth

This does NOT change the flowagent image tag/pin -- that stays
git.bryanmail.net/homelab/flowagent-mcpo:c5b22618404a pending the
companion mcp-config PR merging and its Woodpecker build producing a new
commit-sha tag to bump to.

No other changes to ai.yaml in this commit -- diff is mount-list addition
only.
2026-09-09 00:16:35 -07:00
AVB 5a5c8e56a9 Merge pull request 'Fix phantom verify failure: secrets/ is not a stack + errexit-proof verify' (#17) from fix-secrets-folder-stack-detection into main
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/renovate Pipeline failed
Reviewed-on: #17
https://ai.bryanmail.net/c/9f4c84c7-7aa1-4b24-a27e-98be6a5e737e
2026-09-07 22:28:36 -07:00
Bot eee6e543b1 Fix phantom verify failure: exclude secrets/ from stack detection + errexit-proof verify's stack-ps
Root cause of PR #16's pipeline failure (#398-#401): secrets/ is a
tooling folder (secrets-map.yaml, *.secrets.example), not a stack, but
folder-detection (cut -d/ -f1, excluding only deploy/) computed
ALL_STACKS="ai secrets" the first time a commit touched secrets/. The
deploy step survived only because 'secrets' coincidentally sits in the
bootstrap-tier skip list; verify had no guard — `docker stack ps
secrets` exited non-zero and, under errexit, the OUTPUT=$(...)
assignment killed the step before the designed WARNING path could run.
The ai deploy itself was fully successful (complete 21-key render, all
6 services healthy) — the failure was pure phantom. Hazard first
flagged July 2026 in PR #3 (closed unmerged).

Changes:
- grep -v '^deploy$'  ->  grep -vE '^(deploy|secrets)$' in ALL five
  folder-detection sites (validate, provision-secrets x2, deploy,
  verify)
- verify: `|| true` inside the stack-ps command substitution so a
  genuinely missing stack warns instead of aborting (comment added)
- deploy step: also rsync secrets/ tooling to the host mirror
  unconditionally, same treatment as deploy/
- header: 2026-09-08 incident note

Everything else byte-identical to main. Dollar-escape audit done.
2026-09-07 22:26:43 -07:00
AVB be587de5be Merge pull request 'Provisioning v2 (take 2): data-only manifest + full-file env template + generic provisioner — replaces #15' (#16) from secrets-provisioning-v3 into main
ci/woodpecker/push/deploy Pipeline failed
Reviewed-on: #16
https://ai.bryanmail.net/c/9f4c84c7-7aa1-4b24-a27e-98be6a5e737e
2026-09-07 22:06:03 -07:00
Bot ef7e3d015e provision-secrets: ai) case -> provision-stack.py + normalized env block (branch rebuilt on current main)
Same two scoped changes as PR #15's rebase, now applied on a branch cut
from TODAY's main so the merge base is current (PR #15 became
unmergeable — its 09-03 merge base predated main's heavy hand-edits):

1. ai) case -> `python3 deploy/provision-stack.py ai` (one line). Retires
   main's heredoc with its live drift: AI_LITELLM_POSTGRES_PASSWORD
   printed twice (one copy from retired AI_LITELLM_DB_PASSWORD), stray
   legacy AI_WEBUI_SECRET_KEY, and the WEB_UI/WEBUI shell-var mismatch
   that renders an EMPTY OAuth client secret into ai.env.
2. env block ai section: normalized to exact template placeholder names,
   added AI_MCPO_API_KEY (NEW Woodpecker secret required pre-merge),
   pruned now-template-literal vars.

apk gains python3 py3-yaml. Everything else byte-matches current main.
Dollar-escape audit done per header lesson.
2026-09-07 21:47:04 -07:00
Bot 89a1e030c0 Add deploy/provision-stack.py — generic manifest-driven provisioner
Carried over from secrets-provisioning-v2 (PR #15), content identical.
Whole-file template render (hard-fail naming missing vars), env + Docker
secrets shipped via ssh stdin only, sha256-checksum skip-if-unchanged,
never prints a value. Stacks absent from the manifest exit 0 so legacy
case-entries keep handling them.
2026-09-07 21:43:57 -07:00
Bot e82b754386 Add ai/ai.env.template — full-file authoritative env template (AI_<SERVICE>_* naming)
Carried over from secrets-provisioning-v2 (PR #15) post-rebase. Key names
match exactly what current ai/ai.yaml references; MCPO_API_KEY stays
unprefixed (ai.yaml consumes it unprefixed for both mcpo services).
Placeholder names match the CI env vars declared in deploy.yml's
provision-secrets block, including the normalized
AI_OPEN_WEBUI_OAUTH_CLIENT_SECRET (fixing main's WEB_UI/WEBUI mismatch
that currently renders an EMPTY OAuth client secret into ai.env).
NEW Woodpecker secret required before merge: ai_mcpo_api_key.
2026-09-07 21:43:05 -07:00
Bot 85734f4601 Add secrets/secrets-map.yaml — data-only provisioning manifest (ai stack first)
Carried over from secrets-provisioning-v2 (PR #15), which became
unmergeable after heavy parallel hand-edits to main moved the merge base
too far. Content identical to v2.
2026-09-07 21:43:04 -07:00
AVB 264f4e7379 Change AWS_REGION_NAME variable
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-09-06 20:33:56 -07:00
AVB 9bbbd5fae3 Add AWS Region Name variable
ci/woodpecker/push/deploy Pipeline was successful
2026-09-06 20:33:08 -07:00
AVB e33fd8695f Update ai/ai.yaml
ci/woodpecker/push/deploy Pipeline was successful
ci/woodpecker/cron/renovate Pipeline was successful
2026-09-06 10:59:05 -07:00
AVB 1828c89194 Update .woodpecker/deploy.yml
ci/woodpecker/push/deploy Pipeline was successful
2026-09-06 10:53:55 -07:00
AVB 9ec5353d7e Fixed LITELLM Database URL
ci/woodpecker/push/deploy Pipeline was successful
2026-09-06 10:44:53 -07:00
AVB dfa0d34a83 Update ai/ai.yaml
ci/woodpecker/push/deploy Pipeline was successful
2026-09-06 10:29:27 -07:00
AVB 66638dd743 Update ai/ai.yaml
ci/woodpecker/push/deploy Pipeline was successful
2026-09-06 01:29:02 -07:00
2 changed files with 50 additions and 27 deletions
+30 -8
View File
@@ -59,6 +59,17 @@ when:
# render, hard failure naming any missing value). Migrated stacks call
# the script; unmigrated stacks keep legacy case entries until their own
# PR. See the "Secrets & Deployment Architecture — Global Direction" note.
#
# 2026-09-08 FIX: secrets/ is a tooling/docs folder (secrets-map.yaml +
# *.secrets.example), not a stack — but folder-detection treated it as one
# the first time a commit touched it (PR #16). deploy survived only because
# 'secrets' sits in the bootstrap-tier skip list; verify had no guard and
# died on `docker stack ps secrets` failing under errexit (assignment from
# a failing command substitution aborts the step). Fixed by excluding
# secrets/ alongside deploy/ in ALL folder-detection sites, and by
# tolerating a failing stack-ps in verify (|| true) so a genuinely missing
# stack produces the designed WARNING instead of killing the step. This
# hazard was first flagged in July (PR #3, closed unmerged).
# ─────────────────────────────────────────────────────────────────────────────
steps:
@@ -76,9 +87,9 @@ steps:
FLAT=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.ya?ml$' || true)
# Folder: any file under a subfolder (e.g. immich/immich.yml).
# Exclude dotfolders (.woodpecker, .git, .gitea, etc.) and deploy/
# (shared tooling, not a stack — see note above).
FOLDERS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -v '^deploy$' | sort -u || true)
# Exclude dotfolders (.woodpecker, .git, .gitea, etc.) and the
# non-stack tooling folders deploy/ and secrets/ (see notes above).
FOLDERS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -vE '^(deploy|secrets)$' | sort -u || true)
[ -z "$FLAT" ] && [ -z "$FOLDERS" ] && echo "No stacks changed" && exit 0
@@ -219,14 +230,14 @@ steps:
- |
CHANGED_FILES=$(echo "${CI_PIPELINE_FILES}" | tr -d '[]"' | tr ',' '\n')
FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -v '^deploy$' | sort -u || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -vE '^(deploy|secrets)$' | sort -u || true)
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
[ -z "$ALL_STACKS" ] && echo "No stacks changed, skipping" && exit 0
- scp -o StrictHostKeyChecking=no deploy/create-secrets.sh root@$${SWARM_MANAGER_IP}:/tmp/cs.sh
- |
CHANGED_FILES=$(echo "${CI_PIPELINE_FILES}" | tr -d '[]"' | tr ',' '\n')
FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -v '^deploy$' | sort -u || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -vE '^(deploy|secrets)$' | sort -u || true)
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
for STACK in $ALL_STACKS; do
@@ -360,7 +371,7 @@ steps:
- |
CHANGED_FILES=$(echo "${CI_PIPELINE_FILES}" | tr -d '[]"' | tr ',' '\n')
FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -v '^deploy$' | sort -u || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -vE '^(deploy|secrets)$' | sort -u || true)
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
[ -z "$ALL_STACKS" ] && echo "No stacks changed" && exit 0
@@ -368,6 +379,13 @@ steps:
rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
deploy/ root@$${SWARM_MANAGER_IP}:/volume1/docker/compose-files/deploy/
# Sync secrets/ tooling (manifest + examples) alongside deploy/ —
# provision-stack.py reads secrets/secrets-map.yaml from the CI
# checkout, but the host mirror should stay complete for emergency
# manual provisioning runs.
rsync -av -e "ssh -o StrictHostKeyChecking=no -i ~/.ssh/id_rsa" \
secrets/ root@$${SWARM_MANAGER_IP}:/volume1/docker/compose-files/secrets/
for STACK in $ALL_STACKS; do
echo "--- Deploying: $STACK ---"
# Sync files to host first (always, even for bootstrap stacks)
@@ -414,7 +432,7 @@ steps:
- |
CHANGED_FILES=$(echo "${CI_PIPELINE_FILES}" | tr -d '[]"' | tr ',' '\n')
FLAT_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/]+\.yaml$' | sed 's/\.yaml$//' || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -v '^deploy$' | sort -u || true)
FOLDER_STACKS=$(echo "$CHANGED_FILES" | grep -E '^[^/.][^/]*/' | cut -d/ -f1 | grep -vE '^(deploy|secrets)$' | sort -u || true)
ALL_STACKS=$(printf '%s\n%s' "$FLAT_STACKS" "$FOLDER_STACKS" | grep -v '^$' | sort -u)
[ -z "$ALL_STACKS" ] && exit 0
@@ -425,6 +443,10 @@ steps:
# "nothing found in stack" output on ordinary deploys (e.g. vaultwarden,
# 2026-08-25). Retry with backoff instead of a single fixed sleep, and
# only warn (don't fail the pipeline) if tasks never show up.
# 2026-09-08: `|| true` inside the command substitution is REQUIRED —
# this step runs under errexit, and an assignment from a failing
# command substitution (e.g. `docker stack ps` on a stack that doesn't
# exist) kills the whole step before the WARNING path can run.
ATTEMPTS=6
DELAY=5
for STACK in $ALL_STACKS; do
@@ -433,7 +455,7 @@ steps:
while [ "$i" -le "$ATTEMPTS" ]; do
OUTPUT=$(ssh -o StrictHostKeyChecking=no root@$${SWARM_MANAGER_IP} \
"docker stack ps $STACK --filter desired-state=running \
--format ' {{.Name}} {{.CurrentState}}'" 2>/dev/null)
--format ' {{.Name}} {{.CurrentState}}'" 2>/dev/null || true)
if [ -n "$OUTPUT" ]; then
echo "$OUTPUT"
break
+20 -19
View File
@@ -16,20 +16,20 @@ services:
start_period: 60s
environment:
- OLLAMA_BASE_URL=http://ollama-intel-arc:11434
- WEBUI_SECRET_KEY=${WEBUI_SECRET_KEY}
- WEBUI_SECRET_KEY=${AI_OPEN_WEBUI_SECRET_KEY}
- WEBUI_DB_HOST=postgresql
- DATABASE_URL=${OPEN_WEBUI_DATABASE_URL}
- DATABASE_URL=${AI_OPEN_WEBUI_DATABASE_URL}
- ENABLE_TOOL_SERVER_CALLING=true
- TOOL_SERVER_CALLING=true
- WEBUI_URL=${WEBUI_URL}
- ENABLE_OAUTH_SIGNUP=${ENABLE_OAUTH_SIGNUP}
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=${OAUTH_MERGE_ACCOUNTS_BY_EMAIL}
- OAUTH_PROVIDER_NAME=${OAUTH_PROVIDER_NAME}
- OPENID_PROVIDER_URL=${OPENID_PROVIDER_URL}
- OAUTH_CLIENT_ID=${OAUTH_CLIENT_ID}
- OAUTH_CLIENT_SECRET=${OAUTH_CLIENT_SECRET}
- OAUTH_SCOPES=${OAUTH_SCOPES}
- OPENID_REDIRECT_URI=${OPENID_REDIRECT_URI}
- WEBUI_URL=${AI_OPEN_WEBUI_URL}
- ENABLE_OAUTH_SIGNUP=${AI_OPEN_WEBUI_ENABLE_OAUTH_SIGNUP}
- OAUTH_MERGE_ACCOUNTS_BY_EMAIL=${AI_OPEN_WEBUI_OAUTH_MERGE_ACCOUNTS_BY_EMAIL}
- OAUTH_PROVIDER_NAME=${AI_OPEN_WEBUI_OAUTH_PROVIDER_NAME}
- OPENID_PROVIDER_URL=${AI_OPEN_WEBUI_OPENID_PROVIDER_URL}
- OAUTH_CLIENT_ID=${AI_OPEN_WEBUI_OAUTH_CLIENT_ID}
- OAUTH_CLIENT_SECRET=${AI_OPEN_WEBUI_OAUTH_CLIENT_SECRET}
- OAUTH_SCOPES=${AI_OPEN_WEBUI_OAUTH_SCOPES}
- OPENID_REDIRECT_URI=${AI_OPEN_WEBUI_OPENID_REDIRECT_URI}
networks:
- traefik_backend
- postgresql_db-backend
@@ -62,13 +62,13 @@ services:
environment:
- AWS_ACCESS_KEY_ID=${AI_AWS_ACCESS_KEY_ID}
- AWS_SECRET_ACCESS_KEY=${AI_AWS_SECRET_ACCESS_KEY}
- AWS_REGION_NAME=${AWS_REGION_NAME}
- LITELLM_MASTER_KEY=${LITELLM_MASTER_KEY}
- LITELLM_SALT_KEY=${LITELLM_SALT_KEY}
- DATABASE_URL=${DATABASE_URL}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- LITELLM_MODIFY_PARAMS=${LITELLM_MODIFY_PARAMS}
- LITELLM_DATABASE_MIGRATIONS=${LITELLM_DATABASE_MIGRATIONS}
- AWS_REGION_NAME=${AI_AWS_REGION_NAME}
- LITELLM_MASTER_KEY=${AI_LITELLM_MASTER_KEY}
- LITELLM_SALT_KEY=${AI_LITELLM_SALT_KEY}
- DATABASE_URL=${AI_LITELLM_DATABASE_URL}
- POSTGRES_PASSWORD=${AI_LITELLM_POSTGRES_PASSWORD}
- LITELLM_MODIFY_PARAMS=${AI_LITELLM_MODIFY_PARAMS}
- LITELLM_DATABASE_MIGRATIONS=${AI_LITELLM_DATABASE_MIGRATIONS}
networks:
- traefik_backend
- postgresql_db-backend
@@ -127,7 +127,7 @@ services:
labels:
- traefik.enable=false
mcpo:
image: git.bryanmail.net/homelab/flowagent-mcpo:916164714429
image: git.bryanmail.net/homelab/flowagent-mcpo:c5b22618404a
command:
- --config
- /app/config/config.json
@@ -141,6 +141,7 @@ services:
- /volume1/docker/mcpo/data:/mcpo_data
- /volume1/docker/cronicle/ssh_keys:/app/ssh_keys:ro
- /volume1/docker/mcpo/uv-cache:/app/uv-cache
- /volume1/docker/mcpo/flowagent-auth:/app/flowagent-auth
secrets:
- source: flowagent_azure_client_id
target: flowagent_azure_client_id