2026-06-16 03:40:31 +00:00
|
|
|
#!/bin/bash
|
|
|
|
|
# MeshAI Docker Entrypoint
|
2026-07-05 16:21:32 -06:00
|
|
|
# Writes a default config on first run, then runs the bot
|
2026-06-16 03:40:31 +00:00
|
|
|
|
|
|
|
|
export MESHAI_CONFIG="/data/config.yaml"
|
|
|
|
|
export TERM="${TERM:-xterm-256color}"
|
|
|
|
|
|
|
|
|
|
# First run - no config exists, create defaults
|
|
|
|
|
if [ ! -f "$MESHAI_CONFIG" ]; then
|
|
|
|
|
mkdir -p /data
|
|
|
|
|
cat > "$MESHAI_CONFIG" << 'EOF'
|
|
|
|
|
# MeshAI Configuration
|
2026-07-05 16:21:32 -06:00
|
|
|
# Edit this file directly, or configure via the dashboard
|
2026-06-16 03:40:31 +00:00
|
|
|
|
|
|
|
|
bot:
|
|
|
|
|
name: ai
|
|
|
|
|
owner: ""
|
|
|
|
|
respond_to_dms: true
|
|
|
|
|
filter_bbs_protocols: true
|
|
|
|
|
|
|
|
|
|
connection:
|
|
|
|
|
type: tcp
|
|
|
|
|
serial_port: /dev/ttyUSB0
|
|
|
|
|
tcp_host: localhost
|
|
|
|
|
tcp_port: 4403
|
|
|
|
|
|
|
|
|
|
response:
|
|
|
|
|
delay_min: 2.2
|
|
|
|
|
delay_max: 3.0
|
|
|
|
|
max_length: 150
|
|
|
|
|
max_messages: 2
|
|
|
|
|
|
|
|
|
|
history:
|
|
|
|
|
database: /data/conversations.db
|
|
|
|
|
max_messages_per_user: 50
|
|
|
|
|
conversation_timeout: 86400
|
|
|
|
|
|
|
|
|
|
memory:
|
|
|
|
|
enabled: true
|
|
|
|
|
window_size: 4
|
|
|
|
|
summarize_threshold: 8
|
|
|
|
|
|
|
|
|
|
context:
|
|
|
|
|
enabled: true
|
|
|
|
|
observe_channels: []
|
|
|
|
|
ignore_nodes: []
|
2026-07-03 18:39:26 -06:00
|
|
|
max_age: 1209600
|
2026-06-16 03:40:31 +00:00
|
|
|
max_context_items: 20
|
|
|
|
|
|
|
|
|
|
llm:
|
|
|
|
|
backend: openai
|
feat(secrets): GUI-managed .env secrets store — keys are config, but gitignored (#47)
API keys/secrets now live in /data/secrets/.env (gitignored, never in config
YAML), while remaining fully editable from the dashboard. Config YAML holds
only ${VAR} references.
Backend:
- meshai/secrets_store.py: get_status (SET/NOT-SET, never values), set_secret,
delete_secret over /data/secrets/.env (resolved like load_config); authoritative
SECRET_FIELD_TO_ENV map (traffic→TOMTOM_API_KEY, firms→FIRMS_MAP_KEY,
roads511→ROADS511_API_KEY, wzdx→WZDX_API_KEY, smtp→SMTP_PASSWORD,
mesh_sources→MESHMONITOR_API_TOKEN) + backend-dependent llm_env_var
- dashboard/api/secrets_routes.py: GET /api/secrets (status only), PUT/DELETE
/api/secrets/{env_var} (validated, restart_required); registered in server.py
- config_loader: save_section preserves ${VAR} secret refs on section save
(never rejects them); EXPECTED_SECRETS += ROADS511_API_KEY, WZDX_API_KEY
- config.example.yaml + docker-entrypoint default config use ${VAR} refs;
first-run bootstraps /data/secrets/.env; .gitignore covers it
Frontend:
- components/ManagedSecret.tsx: masked, Set/Not-set badge, reveal, Save->PUT,
"restart required"; carries no config value so secrets never enter a section
save payload
- wired into Environment (tomtom/roads511/wzdx/firms), Config LLM tab
(env var by backend), Notifications (smtp)
Restart required after a secret change (env read at config-load). 11 store
tests; suite at 10-failure baseline (1714 passed).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:57:45 -06:00
|
|
|
api_key: ${GOOGLE_API_KEY} # secret: set via dashboard or /data/secrets/.env
|
2026-06-16 03:40:31 +00:00
|
|
|
base_url: https://api.openai.com/v1
|
|
|
|
|
model: gpt-4o-mini
|
|
|
|
|
timeout: 30
|
|
|
|
|
system_prompt: >-
|
|
|
|
|
You are a helpful assistant on a Meshtastic mesh network.
|
|
|
|
|
Keep responses VERY brief - under 250 characters total.
|
|
|
|
|
Be concise but friendly. No markdown formatting.
|
|
|
|
|
google_grounding: false
|
|
|
|
|
|
|
|
|
|
meshmonitor:
|
|
|
|
|
enabled: false
|
|
|
|
|
inject_into_prompt: true
|
|
|
|
|
EOF
|
feat(secrets): GUI-managed .env secrets store — keys are config, but gitignored (#47)
API keys/secrets now live in /data/secrets/.env (gitignored, never in config
YAML), while remaining fully editable from the dashboard. Config YAML holds
only ${VAR} references.
Backend:
- meshai/secrets_store.py: get_status (SET/NOT-SET, never values), set_secret,
delete_secret over /data/secrets/.env (resolved like load_config); authoritative
SECRET_FIELD_TO_ENV map (traffic→TOMTOM_API_KEY, firms→FIRMS_MAP_KEY,
roads511→ROADS511_API_KEY, wzdx→WZDX_API_KEY, smtp→SMTP_PASSWORD,
mesh_sources→MESHMONITOR_API_TOKEN) + backend-dependent llm_env_var
- dashboard/api/secrets_routes.py: GET /api/secrets (status only), PUT/DELETE
/api/secrets/{env_var} (validated, restart_required); registered in server.py
- config_loader: save_section preserves ${VAR} secret refs on section save
(never rejects them); EXPECTED_SECRETS += ROADS511_API_KEY, WZDX_API_KEY
- config.example.yaml + docker-entrypoint default config use ${VAR} refs;
first-run bootstraps /data/secrets/.env; .gitignore covers it
Frontend:
- components/ManagedSecret.tsx: masked, Set/Not-set badge, reveal, Save->PUT,
"restart required"; carries no config value so secrets never enter a section
save payload
- wired into Environment (tomtom/roads511/wzdx/firms), Config LLM tab
(env var by backend), Notifications (smtp)
Restart required after a secret change (env read at config-load). 11 store
tests; suite at 10-failure baseline (1714 passed).
Co-authored-by: Matt Johnson <mj@k7zvx.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-05 17:57:45 -06:00
|
|
|
mkdir -p /data/secrets
|
|
|
|
|
[ -f /data/secrets/.env ] || : > /data/secrets/.env
|
2026-07-05 16:21:32 -06:00
|
|
|
echo "Default config created at $MESHAI_CONFIG. Edit it or configure via the dashboard."
|
2026-06-16 03:40:31 +00:00
|
|
|
fi
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Kill bot gracefully with SIGKILL fallback
|
|
|
|
|
kill_bot() {
|
|
|
|
|
local pid=$1
|
|
|
|
|
if ! kill -0 "$pid" 2>/dev/null; then
|
|
|
|
|
return
|
|
|
|
|
fi
|
|
|
|
|
kill "$pid" 2>/dev/null || true
|
|
|
|
|
echo "Sent SIGTERM to bot (PID $pid)"
|
|
|
|
|
# Wait up to 5 seconds for graceful shutdown
|
|
|
|
|
for i in 1 2 3 4 5; do
|
|
|
|
|
kill -0 "$pid" 2>/dev/null || return
|
|
|
|
|
sleep 1
|
|
|
|
|
done
|
|
|
|
|
# Force kill if still alive
|
|
|
|
|
if kill -0 "$pid" 2>/dev/null; then
|
|
|
|
|
kill -9 "$pid" 2>/dev/null || true
|
|
|
|
|
echo "Sent SIGKILL to bot (PID $pid)"
|
|
|
|
|
fi
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
# Start the bot in a loop with integrated restart watcher
|
|
|
|
|
echo "Starting MeshAI..."
|
|
|
|
|
rm -f /tmp/meshai_restart
|
|
|
|
|
while true; do
|
|
|
|
|
python -m meshai -v --config-file "$MESHAI_CONFIG" &
|
|
|
|
|
BOT_PID=$!
|
|
|
|
|
echo "$BOT_PID" > /tmp/meshai.pid
|
|
|
|
|
echo "Bot started (PID $BOT_PID)"
|
|
|
|
|
|
|
|
|
|
# Poll: wait for bot to exit OR restart signal
|
|
|
|
|
while kill -0 $BOT_PID 2>/dev/null; do
|
|
|
|
|
if [ -f /tmp/meshai_restart ]; then
|
|
|
|
|
rm -f /tmp/meshai_restart
|
|
|
|
|
echo "Restart signal received, restarting bot..."
|
|
|
|
|
kill_bot $BOT_PID
|
|
|
|
|
break
|
|
|
|
|
fi
|
|
|
|
|
sleep 1
|
|
|
|
|
done
|
|
|
|
|
|
|
|
|
|
wait $BOT_PID 2>/dev/null || true
|
|
|
|
|
rm -f /tmp/meshai.pid
|
|
|
|
|
echo "Bot exited. Restarting in 3s..."
|
|
|
|
|
sleep 3
|
|
|
|
|
done
|