From 032229c1e209828343d837c3914b90c3f74468a4 Mon Sep 17 00:00:00 2001 From: Frank Date: Sun, 18 Oct 2026 16:26:37 -0700 Subject: [PATCH] =?UTF-8?q?feat(open-notebook):=20env=20contract=20?= =?UTF-8?q?=E2=80=94=20CPU=20guard,=20credential=20store,=20auth=20posture?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The two knobs that keep the box alive: TTS_BATCH_SIZE=1 and OPEN_NOTEBOOK_WORKER_MAX_TASKS=1. Upstream ships 5 x 5; frank-tts spawns one ~8 GiB, 8-thread engine process per synthesis request on a machine that also serves two LLMs, so a five-way fan-out is a self-inflicted shutdown. The worker knob is real, not decorative: the image's supervisord expands ${OPEN_NOTEBOOK_WORKER_MAX_TASKS:-5} into the worker command (supervisord.conf:25). Both ESPERANTO_* timeouts go above upstream's 300 s defaults, sized for CPU synthesis (SHIM_TIMEOUT=600, LLAMA_TTS_THREADS=8) — one episode is dozens of per-line /audio/speech calls and a timeout kills the episode, not the line. API_URL is forced for the 5055 -> 5056 remap; SURREAL_URL addresses the database by container name; CORS_ORIGINS is origin-form (scheme://host:port), the form the app compares against. OPEN_NOTEBOOK_PASSWORD stays absent here on purpose: the app treats a set-but-empty value as auth-on. Secrets stay placeholders in the tracked file and in dankclaw/.env.example, which lists SURREAL_PASSWORD rather than SURREAL_PASS — the deployer derives the surreal CLI's spelling from it when it seeds, so the app and the database cannot be handed different values. --- dankclaw/.env.example | 20 ++++++++ scripts/open-notebook.env | 99 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 scripts/open-notebook.env diff --git a/dankclaw/.env.example b/dankclaw/.env.example index 3affd70..c1fcd65 100644 --- a/dankclaw/.env.example +++ b/dankclaw/.env.example @@ -79,3 +79,23 @@ MARRETA_APP_KEY=changeme # account on first boot from these values. MARRETA_ADMIN_EMAIL=admin@franksdank.cloud MARRETA_ADMIN_PASSWORD=changeme + +# open-notebook (podcast generator; internal-only, no vhost). +# deploy-remote.sh seeds /home/dank/Containers/open-notebook/env from these +# values (only if that file does not already exist) and refuses to start the +# units while any of them is blank: a blank OPEN_NOTEBOOK_PASSWORD means +# authentication OFF, and a blank encryption key means no provider credential +# can be saved. The seed writes SURREAL_PASS too — the spelling the `surreal` +# CLI reads — from SURREAL_PASSWORD, so the app and the database cannot be +# given different values. +# OPEN_NOTEBOOK_ENCRYPTION_KEY: encrypts the stored AI provider keys at rest. +# Generate: openssl rand -hex 32 +# Changing it after credentials are saved makes them undecryptable. +OPEN_NOTEBOOK_ENCRYPTION_KEY=changeme +# UI/API password. Every API route — including the episode-audio download the +# app serves without a session — is open to the LAN while this is unset. +OPEN_NOTEBOOK_PASSWORD=changeme +# SurrealDB root password (the root user NAME comes from +# scripts/open-notebook.env, so this file holds only the secret). +# Generate: openssl rand -base64 32 +SURREAL_PASSWORD=changeme diff --git a/scripts/open-notebook.env b/scripts/open-notebook.env new file mode 100644 index 0000000..d987717 --- /dev/null +++ b/scripts/open-notebook.env @@ -0,0 +1,99 @@ +# open-notebook.env — configuration for **open-notebook**, the fleet's podcast +# generator on Dankclaw (UI host port **8502**, API host port **5056**). Read by +# BOTH Quadlet units (dankclaw/open-notebook/quadlet/) as the FIRST +# EnvironmentFile= line, which is why every path and host name below is the +# value the CONTAINER must see. +# +# TWO FILES, ONE ORDER. This file is tracked; it holds knobs, not credentials. +# The three secret keys below are declared EMPTY on purpose — the `scripts/*.env` +# gitignore exception exists for "paths and numbers, never credentials" — and the +# real values live in the host file the deployer seeds at +# /home/dank/Containers/open-notebook/env (mode 600, seeded from dankclaw/.env, +# the same convention grafana/marreta/pihole follow). Both files are handed to +# podman in EnvironmentFile= order and the LAST one wins (verified against the +# generated run line), so the host file overrides these blanks. Do not reorder +# the two lines. +# +# WHY THE DEPLOYER REFUSES TO START THESE UNITS WITHOUT THE SECRET FILE. An empty +# OPEN_NOTEBOOK_PASSWORD is not "unconfigured", it is **authentication off**, and +# the episode-audio download is one of the routes the API serves without a +# session; an empty SURREAL_PASS lets the database come up with different root +# credentials than the app will present; an empty OPEN_NOTEBOOK_ENCRYPTION_KEY +# means no provider credential can be saved at all. So the guard is in +# deploy-remote.sh (seed_open_notebook_env), not in the app's opinion. +# +# THE CPU-SHUTDOWN GUARD (the two knobs that matter most on this box). Defaults +# are TTS_BATCH_SIZE=5 and OPEN_NOTEBOOK_WORKER_MAX_TASKS=5 — five parallel +# synthesis requests from five parallel jobs. frank-tts answers each request by +# spawning its own engine process (~8 GiB resident, 8 threads, measured in the +# frank-tts CPU trial of 2026-10-18), so the defaults mean ~40 GiB and forty +# threads aimed at a machine that also serves frank-dragoon and frank-zealot and +# whose TTS engine runs on the SAME CPU. Both numbers are 1 here: one episode at +# a time, one voice line synthesized at a time. That is a throughput choice, not +# a correctness one — a podcast is a background job — and it is the difference +# between a slow render and a box that stops answering the text endpoints +# mid-sentence. Raise them only with a TTS engine that batches internally. +# +# THE TTS CLOCK. ESPERANTO_TTS_TIMEOUT is the worker's patience per DIALOGUE +# LINE, not per episode: one episode is dozens of POSTs to /v1/audio/speech +# (the app synthesises each speaker turn separately and concatenates the clips +# with ffmpeg afterwards), and a CPU engine takes single-digit seconds for a +# line, with the tail of a long turn well past the 300 s default. 600 matches +# SHIM_TIMEOUT in scripts/frank-tts.env — the shim's own per-request ceiling — +# so open-notebook never abandons a synthesis frank-tts is still finishing. An +# abandoned request does not cancel the engine process; it just leaves the +# episode stuck in "generating" while the audio arrives. +# +# THE SCRIPTWRITER. ESPERANTO_LLM_TIMEOUT is the app's own default (180 s), kept +# visible beside the TTS number because both belong to one podcast run: the +# outline call and the transcript call are the slow text work, and frank-zealot +# answers them well inside 180. Provider choice is made in the UI, not here — +# see [[Open Notebook]] for the one-time credential provisioning — and it is +# frank-zealot on purpose: script writing asks for a JSON-schema response, which +# the OpenAI-compatible llama.cpp endpoint is known to serve, while halogen's +# structured-output behaviour on frank-dragoon is UNVERIFIED. halogen remains +# the unproven alternative, not the fallback. +# +# THE DATABASE COORDINATES. SURREAL_URL addresses the sibling container BY NAME +# on systemd-open-notebook (container DNS is scoped to the owning network, so +# this is the only form that works). NAMESPACE/DATABASE/USER are plain names. +# SURREAL_PASSWORD and SURREAL_PASS are ONE value under two spellings: the app +# reads SURREAL_PASSWORD, the `surreal` CLI reads SURREAL_PASS. Keep them equal; +# the deployer writes both into the seeded file from a single SURREAL_PASSWORD in +# dankclaw/.env, so hand-editing one line is the only way to desync them. + +SURREAL_URL=ws://open-notebook-surrealdb:8000/rpc +SURREAL_NAMESPACE=open_notebook +SURREAL_DATABASE=open_notebook +SURREAL_USER=open_notebook +# Secrets — filled on the host, never here (see the header). +SURREAL_PASSWORD= +SURREAL_PASS= +OPEN_NOTEBOOK_ENCRYPTION_KEY= +OPEN_NOTEBOOK_PASSWORD= + +# WHERE THE BROWSER FINDS THE API. The UI auto-detects `:5055`, +# and 5055 on this host belongs to seerr, so the API leaves the container on 5056 +# and this line has to say so. Both origins are listed in CORS_ORIGINS because +# the API is called cross-origin from the UI's own port: the LAN origin is the +# normal one, the localhost one covers a browser opened on Dankclaw itself. The +# app logs a warning at start while CORS_ORIGINS is unset and defaults to `*`, +# which is a wildcard in front of an endpoint that serves every episode's audio. +API_URL=http://192.168.0.143:5056 +CORS_ORIGINS=http://192.168.0.143:8502,http://localhost:8502 + +# Concurrency: see "THE CPU-SHUTDOWN GUARD" above. Defaults are 5 and 5. +TTS_BATCH_SIZE=1 +OPEN_NOTEBOOK_WORKER_MAX_TASKS=1 + +# Timeouts: see "THE TTS CLOCK" and "THE SCRIPTWRITER" above. +ESPERANTO_TTS_TIMEOUT=600 +ESPERANTO_LLM_TIMEOUT=180 + +# Heavy extraction runtimes stay OFF, which is also what keeps boot fast: each +# one installs a multi-gigabyte ML/Chromium stack into the image's venv on first +# startup (cached on the /app/data volume after that). Podcast generation reads +# notes and sources that are already text, so neither is needed here; enable one +# deliberately, per the upstream docs, if source ingestion starts to matter. +# OPEN_NOTEBOOK_ENABLE_DOCLING=true +# OPEN_NOTEBOOK_ENABLE_CRAWL4AI=true