5.8 KiB
5.8 KiB
paths
| paths | |
|---|---|
|
FastAPI image (image/)
Standalone FastAPI app (Python ≥3.11; ships on python:3.12-slim; UI on port 9999; vanilla HTML/CSS/JS, no framework). Python has no configured linter/formatter — match the style of the file you're editing.
Local dev (no StartOS)
cd image
python3 -m venv .venv && source .venv/bin/activate # one-time
pip install -e .
export SPARK1_HOST=<ip> SPARK1_USER=<user> SPARK2_HOST=<ip> SPARK2_USER=<user> SSH_KEY_PATH=<private-key>
# Required outside the container — these default to paths under /data, which only exists in the image
# (missing REDACTION_MAP_DB crashes startup; missing CONNECTIVITY_LOG 500s /api/status):
export REDACTION_MAP_DB=/tmp/redaction_maps.db CONNECTIVITY_LOG=/tmp/connectivity.json
uvicorn app.server:app --host 0.0.0.0 --port 9999 --reload
Other env vars: BIND_PORT, MODELS_YAML, SSH_DIR, SSH_KNOWN_HOSTS, MODELS_OVERRIDES, SERVICES_OVERRIDES.
Tests
Two kinds, both run with the image/.venv interpreter (system python3 has no deps):
- pytest unit suite — offline, pure functions, no cluster.
.venv/bin/python -m pytestfromimage/. Lives inimage/tests/; currently coversbuild_launch_command(incl. the shell-injection /shlexround-trip invariant) and the transcript↔diarizer label-merge (_merge_words_with_speakers). Install the test dep once withpip install -e '.[dev]'. Add new pure-function coverage here. - Standalone scripts — the redaction suites and the live-cluster audio e2e are run directly (not via pytest). See the redaction and audio rules.
Conventions
- Pydantic request models go at module scope, never inside a
build_router()body (FastAPI silently 422s otherwise). - New external-facing endpoints get documented in
docs/(AUDIO_API.md,EMBEDDINGS.md,REDACTION_GATEWAY.md) and noted in release notes. - SSH-input safety: any user-supplied value that reaches an SSH command on the Sparks MUST go through
app/shellsafe.py— validate against a whitelist at the API boundary, thenquote_arg/quote_args(shlex.quote) at the sink. Never raw f-string a user value into a command string. Existing sinks:models.build_launch_command,download,nim,services;disk.pykeeps its own_SAFE_DIRNAMEbecause it needs$HOMEto expand server-side. The vLLM pre-flight (validate.py) relies onshlex.splitcleanly reversing this quoting — preserve that invariant. - CSRF / same-origin: state-mutating control endpoints are guarded by the
csrf_guardmiddleware inserver.py(rejects requests whoseOrigin/Refererhost ≠ the served host). A new endpoint meant to be called cross-origin by downstream apps (a proxy/data endpoint) must be added to_CSRF_EXEMPT_PREFIXES, or browser POSTs from those apps will 403. No app-layer token auth by design (LAN/VPN-only; would break consumers). - Settings split (gear vs StartOS action): only the four required fields (both Spark IPs + SSH users) live in the StartOS "Configure Sparks" action →
config.yaml→ env. Every optional knob (ports, container names, support-service hosts, integrations, webhook) is edited in the dashboard's ⚙ Settings gear, backed by the/data/app_settings.jsonoverlay (app_settings.py), keyed by the same env-var names. Precedence (config._effective_env):os.environfirst, overlay on top.app_settings.seed_from_envruns once at startup to migrate a pre-gear install's env values into the overlay (don't move seeding intofrom_env/reload— it writes, andfrom_envruns on every build → it would clobber across calls, which it did once already).Settingsis deliberately not frozen: one shared instance is threaded by reference into every router closure/manager, andSettings.reload()(called after a gear save) recomputes its fields in place so changes apply live with no restart and no call-site changes. Gotcha: this only reaches holders that keep the object (self.settings = settings); anything that snapshots a value at construction is invisible toreload()and must be re-synced explicitly. The one such holder isWebhookNotifier, which copiesurl/secret—post_settingscallsswap_webhook.update(...)right afterreload(). Any future component that caches a gear-managed value (rather than readingsettings.xat use time) needs the same treatment. A new gear knob = add one entry toapp_settings.FIELDS(the front-end renders it generically); the matchingconfig.Settingsfield must already read that env var.
Layout
image/app/server.py— FastAPI entry; routers live in sibling modules (audio_proxy.py,llm_proxy.py,embeddings_proxy.py,redaction_gateway.py,swap.py,health.py,deep_health.py,connectivity.py, …).image/app/discovery.py— the disk-driven model menu./api/modelslists what's actually downloaded on the Sparks (viadisk.list_cached_models);models.yaml/overrides are launch recipes matched by repo, not the menu. An on-disk model with no recipe isneeds_setup→infer_recipereads itsconfig.jsonto prefill a setup form the operator confirms once.image/app/app_settings.py— the in-app settings overlay backing the ⚙ gear:FIELDSmetadata (drives/api/settings+ the UI form),load_overlay()(pure read),seed_from_env()(one-time migration),apply()(validate + persist).GET/POST /api/settingsinserver.pyread/write it, thensettings.reload().image/app/static/— the dashboard UI.image/models.yaml— bundled vLLM launch recipes (how to launch a known model), NOT the dashboard menu — the menu is the on-disk scan.image/spark_embed/— Dockerfile + app for the embeddings container; built ON a Spark (ARM64, NGC PyTorch base — see the audio/cluster rule for NGC torch-pinning caveats).