Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
18a29c40d2 | ||
|
|
130e9d739f | ||
|
|
854890a155 | ||
|
|
d9f3d1be1e | ||
|
|
afa3a4521c | ||
|
|
9cc8ca1c88 | ||
|
|
9c05c40aa0 | ||
|
|
63219787af | ||
|
|
e914ecbca6 | ||
|
|
1eebf3b129 | ||
|
|
af5a1f69a2 | ||
|
|
0930510f9b | ||
|
|
af327bcc5e | ||
|
|
861920797d |
No files matched your search
@@ -803,6 +803,20 @@ dies, its page is destroyed. Rollback is a commit, not a document.
|
|||||||
`serverAppConfigs-docs/index.md` is the locator for every page that survives;
|
`serverAppConfigs-docs/index.md` is the locator for every page that survives;
|
||||||
retired scripts live in `old/` and `.AGENTTRASH/`.
|
retired scripts live in `old/` and `.AGENTTRASH/`.
|
||||||
|
|
||||||
|
## Agent skills
|
||||||
|
|
||||||
|
### Issue tracker
|
||||||
|
|
||||||
|
Issues live in Gitea (`dank/serverAppConfigs` on Dankclaw), via its REST API with the stored credential. See `serverAppConfigs-docs/Agent/Issue Tracker.md`.
|
||||||
|
|
||||||
|
### Triage labels
|
||||||
|
|
||||||
|
The five canonical roles keep their default strings (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `serverAppConfigs-docs/Agent/Triage Labels.md`.
|
||||||
|
|
||||||
|
### Domain docs
|
||||||
|
|
||||||
|
Single-context: root `GLOSSARY.md` + `docs/adr/`, created lazily; this vault stays the owner of operational facts. See `serverAppConfigs-docs/Agent/Domain Docs.md`.
|
||||||
|
|
||||||
## Why
|
## Why
|
||||||
|
|
||||||
Direct edits on the remote drift from version control, and future agents
|
Direct edits on the remote drift from version control, and future agents
|
||||||
|
|||||||
+47
@@ -0,0 +1,47 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Set the authentik login-session TTL on Dankclaw.
|
||||||
|
#
|
||||||
|
# The login-session lifetime is governed by the `default-authentication-login`
|
||||||
|
# stage's `session_duration`, NOT a global setting and NOT SESSION_COOKIE_AGE
|
||||||
|
# (that is the pre-login session, sessions.unauthenticated_age). Authentik does
|
||||||
|
# not expose SESSION_SAVE_EVERY_REQUEST at all, so the session is an absolute
|
||||||
|
# window from login, not a sliding one.
|
||||||
|
#
|
||||||
|
# The field is a STRING in authentik's own idiom ("days=7", "seconds=30",
|
||||||
|
# "hours=12") — not a Python timedelta. Writing str(timedelta(days=7)) stores
|
||||||
|
# "7 days, 0:00:00", which authentik's timedelta_from_string parser REJECTS
|
||||||
|
# (ValueError), and login then crashes computing the session expiry. Always pass
|
||||||
|
# the "key=value" form; this script validates it before writing.
|
||||||
|
#
|
||||||
|
# Existing sessions keep their old expiry; the new TTL applies from the next
|
||||||
|
# login. This is DB state — it is NOT repo-managed and a blueprint re-import or
|
||||||
|
# rebuild-from-scratch reverts it. Run this again after such a rebuild.
|
||||||
|
#
|
||||||
|
# Usage: authentik-set-login-session-duration.sh ["days=7"] (default days=7)
|
||||||
|
set -euo pipefail
|
||||||
|
|
||||||
|
DUR="${1:-days=7}"
|
||||||
|
STAGE="default-authentication-login"
|
||||||
|
HOST="${AUTHENTIK_HOST:-dankclaw}"
|
||||||
|
|
||||||
|
# Refuse anything that is not the safe key=value duration idiom.
|
||||||
|
if ! printf '%s' "$DUR" | grep -Eq '^[A-Za-z]+=[0-9]+([ ]+[A-Za-z]+=[0-9]+)*$'; then
|
||||||
|
echo "invalid duration '$DUR' — use authentik form, e.g. 'days=7' or 'seconds=30'" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
ssh "$HOST" 'podman exec -i authentik-server ak shell' <<PYEOF
|
||||||
|
from django.apps import apps
|
||||||
|
from authentik.lib.utils.time import timedelta_from_string
|
||||||
|
dur = "$DUR"
|
||||||
|
timedelta_from_string(dur) # validate the form before touching the row
|
||||||
|
ULS = apps.get_model("authentik_stages_user_login", "UserLoginStage")
|
||||||
|
s = ULS.objects.get(name="$STAGE")
|
||||||
|
s.session_duration = dur
|
||||||
|
s.save(update_fields=["session_duration"])
|
||||||
|
back = ULS.objects.get(name="$STAGE").session_duration
|
||||||
|
assert back == dur, "persisted value mismatch: %r" % back
|
||||||
|
print("session_duration set to", repr(back), "->", timedelta_from_string(back))
|
||||||
|
PYEOF
|
||||||
|
|
||||||
|
echo "Login-session TTL for '$STAGE' is now $DUR (applies on next login)."
|
||||||
@@ -7,7 +7,7 @@
|
|||||||
# whichever TTS engine wins the engine decision (dankclaw/voiceservice/app.py
|
# whichever TTS engine wins the engine decision (dankclaw/voiceservice/app.py
|
||||||
# is a single stdlib-only file in a locally built image). Consumers — omp's
|
# is a single stdlib-only file in a locally built image). Consumers — omp's
|
||||||
# TTS device, OpenWebUI, open-notebook — address a VOICE BY NAME
|
# TTS device, OpenWebUI, open-notebook — address a VOICE BY NAME
|
||||||
# (voice=narrator1, or the harness' voice_id=narrator1) and never see a path:
|
# (voice=wilwheaton, or the harness' voice_id=wilwheaton) and never see a path:
|
||||||
# a name is real exactly when /voices/<name>/reference.wav exists, and
|
# a name is real exactly when /voices/<name>/reference.wav exists, and
|
||||||
# anything else is a 400 that lists the names that are.
|
# anything else is a 400 that lists the names that are.
|
||||||
#
|
#
|
||||||
@@ -65,7 +65,7 @@ TTS_UPSTREAM_MODEL=voxcpm2
|
|||||||
# name still 400s with the list that exists — this covers the absent case only —
|
# name still 400s with the list that exists — this covers the absent case only —
|
||||||
# and the journal marks such a line `(default)` so an unrequested voice never
|
# and the journal marks such a line `(default)` so an unrequested voice never
|
||||||
# reads as a requested one. Any pack directory in /home/dank/voices is legal.
|
# reads as a requested one. Any pack directory in /home/dank/voices is legal.
|
||||||
DEFAULT_VOICE=narrator1
|
DEFAULT_VOICE=wilwheaton
|
||||||
|
|
||||||
# CLI-backend knobs, inert while TTS_BACKEND=omni-http. Empty means "not
|
# CLI-backend knobs, inert while TTS_BACKEND=omni-http. Empty means "not
|
||||||
# configured": the endpoint answers a 500 that says so instead of running
|
# configured": the endpoint answers a 500 that says so instead of running
|
||||||
|
|||||||
@@ -0,0 +1,55 @@
|
|||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Domain Docs
|
||||||
|
description: "Consumer rules for GLOSSARY.md and docs/adr/, and where this vault out-ranks them on operational facts."
|
||||||
|
tags: [agent, glossary, adr, documentation]
|
||||||
|
timestamp: 2026-10-20T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Domain Docs
|
||||||
|
|
||||||
|
How the engineering skills (`/grill-with-docs`, `/domain-modeling`,
|
||||||
|
`/improve-codebase-architecture`, `/to-spec`) should consume this repo's design
|
||||||
|
vocabulary when exploring the codebase.
|
||||||
|
|
||||||
|
## Before exploring, read these
|
||||||
|
|
||||||
|
- **`GLOSSARY.md`** at the repo root
|
||||||
|
- **`docs/adr/`**: read ADRs that touch the area you're about to work in
|
||||||
|
|
||||||
|
If either doesn't exist, **proceed silently**. Don't flag their absence; don't
|
||||||
|
suggest creating them upfront. `/domain-modeling` (reached via
|
||||||
|
`/grill-with-docs` and `/improve-codebase-architecture`) creates them lazily
|
||||||
|
when terms or decisions actually get resolved. As of 2026-10-20 neither exists;
|
||||||
|
this page is the pointer, not a promise.
|
||||||
|
|
||||||
|
## Ownership against this vault
|
||||||
|
|
||||||
|
This repo already carries `serverAppConfigs-docs/` — the operational knowledge
|
||||||
|
base and, per repo law, the owner of operational facts (endpoint shapes, Eras,
|
||||||
|
Laws, runbooks). `GLOSSARY.md` / `docs/adr/` hold **design vocabulary and
|
||||||
|
decisions**. Where a term is defined in both, the vault page that owns the fact
|
||||||
|
wins — one fact, one owner. A glossary entry restating an endpoint number is
|
||||||
|
the bug, not the second source.
|
||||||
|
|
||||||
|
## Use the glossary's vocabulary
|
||||||
|
|
||||||
|
When output names a domain concept (an issue title, a refactor proposal, a
|
||||||
|
hypothesis, a test name), use the term as defined in `GLOSSARY.md`. Don't drift
|
||||||
|
to synonyms the glossary explicitly avoids.
|
||||||
|
|
||||||
|
If the concept you need isn't in the glossary yet, that's a signal: either
|
||||||
|
you're inventing language the project doesn't use (reconsider) or there's a
|
||||||
|
real gap (note it for `/domain-modeling`).
|
||||||
|
|
||||||
|
## Flag ADR conflicts
|
||||||
|
|
||||||
|
If output contradicts an existing ADR, surface it explicitly rather than
|
||||||
|
silently overriding:
|
||||||
|
|
||||||
|
> _Contradicts ADR-0007 (…), but worth reopening because…_
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [[Issue Tracker]] — where specs and tickets built on this vocabulary are published.
|
||||||
|
- [[Agent Docs and Edits]] — the edit-safety rules that apply while touching any doc in this checkout.
|
||||||
@@ -0,0 +1,85 @@
|
|||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Issue Tracker
|
||||||
|
description: "How agent skills read and write Gitea issues here: credential-safe curl, label IDs, and wayfinding without sub-issues."
|
||||||
|
tags: [agent, gitea, issues, triage, api]
|
||||||
|
timestamp: 2026-10-20T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Issue Tracker
|
||||||
|
|
||||||
|
Issues and specs for this repo live as Gitea issues in `dank/serverAppConfigs`
|
||||||
|
on `http://192.168.0.143:3000` (Dankclaw). There is no `gh`/`glab` CLI here —
|
||||||
|
the door is the GitHub-compatible REST API driven with `curl`. The
|
||||||
|
mattpocock-lineage skills (`/triage`, `/to-tickets`, `/to-spec`, `/wayfinder`)
|
||||||
|
read this page as their tracker contract. The git workflow around issues and
|
||||||
|
PRs (branches, PR-only merges, attribution footer) is owned by
|
||||||
|
[[Deployment Flow]]; this page owns only the API shape.
|
||||||
|
|
||||||
|
## Auth
|
||||||
|
|
||||||
|
The token lives in the git credential file `~/.config/secrets/gitea`: one
|
||||||
|
line in git-credential URL form, `http://user:TOKEN@192.168.0.143`. Extract it
|
||||||
|
without echoing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
tok=$(sed -nE 's#^http://[^:/@]+:([^@]+)@192\.168\.0\.143.*#\1#p' ~/.config/secrets/gitea | head -1)
|
||||||
|
```
|
||||||
|
|
||||||
|
**A token on a command line is world-readable in `/proc/<pid>/cmdline`** (the
|
||||||
|
same leak class AGENTS.md records for HF tokens). Never pass it with `-H` —
|
||||||
|
put it in a mode-600 curl config and pass `-K`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cfg=$(mktemp); chmod 600 "$cfg"
|
||||||
|
printf 'header = "Authorization: token %s"\n' "$tok" > "$cfg"
|
||||||
|
# ... every curl below uses -K "$cfg"; rm -f "$cfg" when done
|
||||||
|
```
|
||||||
|
|
||||||
|
**GETs also accept HTTP Basic; writes do not** — a POST/PATCH with Basic auth
|
||||||
|
is refused with `422 {"message":"token is required"}` (measured 2026-10-20).
|
||||||
|
Use the `Authorization: token` header for everything.
|
||||||
|
|
||||||
|
## Conventions
|
||||||
|
|
||||||
|
`$API=http://192.168.0.143:3000/api/v1/repos/dank/serverAppConfigs`
|
||||||
|
|
||||||
|
- **Create an issue**: `curl -s -K "$cfg" -X POST -H 'Content-Type: application/json' -d "$(jq -n --arg t "$TITLE" --rawfile b "$BODY" '{title:$t, body:$b}')" $API/issues`
|
||||||
|
- **Read an issue**: `curl -s -K "$cfg" $API/issues/<number>` (fields: `number`, `title`, `body`, `labels[].name`, `state`)
|
||||||
|
- **List issues**: `curl -s -K "$cfg" "$API/issues?state=open&limit=50"` — add `&labels=<id>` to filter
|
||||||
|
- **Comment**: `curl -s -K "$cfg" -X POST -d "$(jq -n --rawfile b "$BODY" '{body:$b}')" $API/issues/<number>/comments`
|
||||||
|
- **Apply labels**: `POST $API/issues/<number>/labels` with `{"labels":[<label-ID numbers>]}` — Gitea wants numeric label **IDs**, not names; resolve them from `GET $API/labels` (see [[Triage Labels]] for which labels exist)
|
||||||
|
- **Remove a label**: `DELETE $API/issues/<number>/labels/<label-id>`
|
||||||
|
- **Close**: `PATCH $API/issues/<number>` with `{"state":"closed"}`
|
||||||
|
- **Blocking relationships**: this instance has no native sub-issues or dependencies — put `Part of #<parent>` / `Blocked by: #<n>, #<n>` lines at the top of the child body. A ticket is unblocked when every listed blocker is closed.
|
||||||
|
- **Attribution footer (repo law)**: every issue body you open ends with `cwd: <working directory> · session: <omp session id>` — full rule in [[Deployment Flow]].
|
||||||
|
|
||||||
|
**PRs as a request surface: no.** Issues and PRs have separate number spaces on
|
||||||
|
Gitea; a bare `#42` is an issue, a PR is spelled "PR #42".
|
||||||
|
|
||||||
|
## When a skill says "publish to the issue tracker"
|
||||||
|
|
||||||
|
Create a Gitea issue as above.
|
||||||
|
|
||||||
|
## When a skill says "fetch the relevant ticket"
|
||||||
|
|
||||||
|
`GET $API/issues/<number>` and read `title` + `body` + `labels[].name` +
|
||||||
|
comments.
|
||||||
|
|
||||||
|
## Wayfinding operations
|
||||||
|
|
||||||
|
Used by `/wayfinder`. The **map** is a single issue labelled `wayfinder:map`,
|
||||||
|
holding the Notes / Decisions-so-far / Fog body.
|
||||||
|
|
||||||
|
- **Map**: create an issue, apply the `wayfinder:map` label (create it via `POST $API/labels` `{"name":"wayfinder:map","color":"#6f42c1"}` if missing).
|
||||||
|
- **Child ticket**: an issue carrying `Part of #<map>` at the top of its body, plus a `wayfinder:<type>` label (`research`/`prototype`/`grilling`/`task`).
|
||||||
|
- **Blocking**: the `Blocked by:` body line (see Conventions).
|
||||||
|
- **Frontier**: list open issues (`?state=open`), drop any with an open `Blocked by` target or an assignee; lowest `number` wins.
|
||||||
|
- **Claim**: `POST $API/issues/<n>/assignees` with `{"assignees":["dank"]}` — actually assign it or it stays in the frontier; the session's first write.
|
||||||
|
- **Resolve**: post the answer as a comment, `PATCH` to `closed`, then update the map issue's body (GET → edit Decisions-so-far → PATCH) with a context pointer.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [[Triage Labels]] — the role strings these operations apply.
|
||||||
|
- [[Domain Docs]] — what a skill must read before touching the codebase.
|
||||||
|
- [[SSH and Remote Access]] — reaching the host when an issue needs live evidence.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
type: reference
|
||||||
|
title: Triage Labels
|
||||||
|
description: "The five canonical triage roles mapped to this repo's Gitea label strings, plus the numeric-ID quirk."
|
||||||
|
tags: [agent, gitea, triage, labels]
|
||||||
|
timestamp: 2026-10-20T00:00:00Z
|
||||||
|
---
|
||||||
|
|
||||||
|
# Triage Labels
|
||||||
|
|
||||||
|
The mattpocock-lineage skills speak in terms of five canonical triage roles.
|
||||||
|
This page maps those roles to the actual label strings used in this repo's
|
||||||
|
tracker ([[Issue Tracker]] owns the API shape).
|
||||||
|
|
||||||
|
| Label in mattpocock/skills | Label in our tracker | Meaning |
|
||||||
|
| -------------------------- | -------------------- | ---------------------------------------- |
|
||||||
|
| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue |
|
||||||
|
| `needs-info` | `needs-info` | Waiting on reporter for more information |
|
||||||
|
| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent |
|
||||||
|
| `ready-for-human` | `ready-for-human` | Requires human implementation |
|
||||||
|
| `wontfix` | `wontfix` | Will not be actioned |
|
||||||
|
|
||||||
|
When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the
|
||||||
|
corresponding label string from this table. Edit the "Label in our tracker"
|
||||||
|
column to match whatever vocabulary you actually use.
|
||||||
|
|
||||||
|
## Instance quirks (Dankclaw Gitea)
|
||||||
|
|
||||||
|
- Labels are applied by **numeric ID**, never by name: `POST /issues/<n>/labels`
|
||||||
|
with `{"labels":[<id>]}`; resolve names via `GET /labels`.
|
||||||
|
- The repo also carries the pre-existing `human-only` label from the manual era
|
||||||
|
(e.g. the podcast-destination issues). `ready-for-human` maps to itself; the
|
||||||
|
`(HUMAN ONLY)` title prefixes on existing issues stay as they are — the label
|
||||||
|
is what the skills read.
|
||||||
|
- An issue with **no** triage label is simply untriaged; absence is not a state.
|
||||||
|
|
||||||
|
## Related
|
||||||
|
|
||||||
|
- [[Issue Tracker]] — the auth-safe curl shape that applies these labels.
|
||||||
@@ -6,6 +6,9 @@ docs get edited safely, and how the agent's home is deployed.
|
|||||||
- [[OMP Harness]] — The omp harness as configured here: compaction chain arithmetic, per-model sampler routing, and the context-lean audits.
|
- [[OMP Harness]] — The omp harness as configured here: compaction chain arithmetic, per-model sampler routing, and the context-lean audits.
|
||||||
- [[Agent Docs and Edits]] — Two self-inflicted doc damage classes from parallel agent edits, with the audit checks and recovery order for each.
|
- [[Agent Docs and Edits]] — Two self-inflicted doc damage classes from parallel agent edits, with the audit checks and recovery order for each.
|
||||||
- [[Command Center Dotfiles]] — How the dotfiles repo deploys: stow-equivalent packages, full-replica rsync, the pre-overwrite audit, transport gotchas.
|
- [[Command Center Dotfiles]] — How the dotfiles repo deploys: stow-equivalent packages, full-replica rsync, the pre-overwrite audit, transport gotchas.
|
||||||
|
- [[Issue Tracker]] — How agent skills read and write Gitea issues here: credential-safe curl, label IDs, and wayfinding without sub-issues.
|
||||||
|
- [[Triage Labels]] — The five canonical triage roles mapped to this repo's Gitea label strings, plus the numeric-ID quirk.
|
||||||
|
- [[Domain Docs]] — Consumer rules for GLOSSARY.md and docs/adr/, and where this vault out-ranks them on operational facts.
|
||||||
|
|
||||||
## Related
|
## Related
|
||||||
|
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ tags:
|
|||||||
- netavark
|
- netavark
|
||||||
- rootless
|
- rootless
|
||||||
- dankclaw
|
- dankclaw
|
||||||
timestamp: 2026-09-02
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# Container Network Namespaces
|
# Container Network Namespaces
|
||||||
@@ -106,8 +106,8 @@ private `systemd-open-notebook`), each address aimed at a live listener:
|
|||||||
| `host.containers.internal:7771` (`169.254.1.2`) | **200** | the gateway alias is the one address that carries a rootless container to a host-netns listener. Why `hermes-dashboard` timed out on this alias in 2026-09-02 while `open-notebook` answers today is **not established**; the difference to check first is the second network and where the default route lands |
|
| `host.containers.internal:7771` (`169.254.1.2`) | **200** | the gateway alias is the one address that carries a rootless container to a host-netns listener. Why `hermes-dashboard` timed out on this alias in 2026-09-02 while `open-notebook` answers today is **not established**; the difference to check first is the second network and where the default route lands |
|
||||||
| `192.168.0.235:7770` (`frank-archon`) | **200** | a different machine: plain routing, no DNAT in the path |
|
| `192.168.0.235:7770` (`frank-archon`) | **200** | a different machine: plain routing, no DNAT in the path |
|
||||||
|
|
||||||
The consequence that changes designs: **a rootless container's menu of local model
|
The consequence that changed designs: **a rootless container's menu of local model
|
||||||
endpoints is `frank-tts` (container DNS, same network) and `frank-archon` (LAN
|
endpoints was only `frank-tts` (container DNS, same network) and `frank-archon` (LAN
|
||||||
address on another host).** A `pasta` endpoint has no address an app container can
|
address on another host).** A `pasta` endpoint has no address an app container can
|
||||||
use: the LAN address is refused, and the one alias that answers is link-local,
|
use: the LAN address is refused, and the one alias that answers is link-local,
|
||||||
which some applications refuse to store at all ([[Open Notebook]] meets exactly
|
which some applications refuse to store at all ([[Open Notebook]] meets exactly
|
||||||
@@ -127,6 +127,15 @@ API port becomes name-resolvable, which is the same trust the published 7770
|
|||||||
already carries on the LAN. `frank-zealot` (7771) stays a host-netns binary and
|
already carries on the LAN. `frank-zealot` (7771) stays a host-netns binary and
|
||||||
remains unreachable except through the link-local alias.
|
remains unreachable except through the link-local alias.
|
||||||
|
|
||||||
|
The move is verified from the consumer side, so the table above reads as the
|
||||||
|
**pre-move** census and not as today's menu: inside `open-notebook`,
|
||||||
|
`getent hosts frank-dragoon-halogen` → `10.89.1.39`, `GET :8731/v1/models`
|
||||||
|
lists both served models, and the engine's log carries the app's
|
||||||
|
`POST /v1/embeddings 200` from that address (2026-10-19) — see
|
||||||
|
[[Open Notebook]] for what runs over it. What the table still owns is the
|
||||||
|
pasta rule itself: nothing about pasta's forwarding changed, the endpoint was
|
||||||
|
moved out of it.
|
||||||
|
|
||||||
## Operational rules
|
## Operational rules
|
||||||
|
|
||||||
- Health checks for a bridge-network service must target a **published** port (or
|
- Health checks for a bridge-network service must target a **published** port (or
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ never learn a path.
|
|||||||
**Deployed 2026-10-18** — `./scripts/deploy.sh dankclaw --service frank-tts`
|
**Deployed 2026-10-18** — `./scripts/deploy.sh dankclaw --service frank-tts`
|
||||||
both units `active`, no drop-ins, and the homepage card's `wip` gate came off in
|
both units `active`, no drop-ins, and the homepage card's `wip` gate came off in
|
||||||
the same change. Probes on the deployed endpoint: `/health` answers
|
the same change. Probes on the deployed endpoint: `/health` answers
|
||||||
`{"status":"ok","backend":"omni-http","voices":["narrator1","narrator2","narrator3"]}`
|
`{"status":"ok","backend":"omni-http","voices":["dr.kern","garrickhagon","jeffhays","vaati","wilwheaton"]}`
|
||||||
and two different voices rendered the same sentence into **different** bytes
|
and two different voices rendered the same sentence into **different** bytes
|
||||||
(24 kHz mono PCM16, 4.24 s, healthy RMS) — so name resolution is real, not a
|
(24 kHz mono PCM16, 4.24 s, healthy RMS) — so name resolution is real, not a
|
||||||
pass-through of one default. The operator harness routes its `speech` role here;
|
pass-through of one default. The operator harness routes its `speech` role here;
|
||||||
@@ -42,8 +42,8 @@ render needs the app installed, not this endpoint.
|
|||||||
|
|
||||||
## Naming a voice
|
## Naming a voice
|
||||||
|
|
||||||
A voice **is** a directory name. `voice=narrator1` resolves to
|
A voice **is** a directory name. `voice=wilwheaton` resolves to
|
||||||
`/voices/narrator1/reference.wav`, and that file — not a manifest, not a config
|
`/voices/wilwheaton/reference.wav`, and that file — not a manifest, not a config
|
||||||
entry — is what makes the name real. omp's TTS device may send `voice_id`; the
|
entry — is what makes the name real. omp's TTS device may send `voice_id`; the
|
||||||
shim accepts either. A call that sends **neither** — the harness's normal shape,
|
shim accepts either. A call that sends **neither** — the harness's normal shape,
|
||||||
because no harness setting carries a voice for this transport — speaks
|
because no harness setting carries a voice for this transport — speaks
|
||||||
@@ -59,10 +59,11 @@ loudly instead of letting it half-work.
|
|||||||
|
|
||||||
Names are user-visible: an unknown voice comes back as
|
Names are user-visible: an unknown voice comes back as
|
||||||
`400 unknown voice 'x'; available voices: …`, and that list is the honest
|
`400 unknown voice 'x'; available voices: …`, and that list is the honest
|
||||||
census. The podcast speakers are `narrator1`, `narrator2`, `narrator3` —
|
census. As of 2026-10-20 the live packs are `dr.kern`, `garrickhagon`,
|
||||||
[[Open Notebook]] is configured with exactly those three, so renaming them
|
`jeffhays`, `vaati`, `wilwheaton`. [[Open Notebook]]'s `dank-duo` profile speaks
|
||||||
breaks episode rendering, not just speech. A personal voice gets the person's
|
`wilwheaton` (Frank) and `vaati` (Comrade), and `DEFAULT_VOICE` is `wilwheaton`.
|
||||||
name, lowercase.
|
Renaming a pack that a speaker profile names breaks episode rendering, not just
|
||||||
|
speech. A personal voice gets the person's name, lowercase.
|
||||||
|
|
||||||
## Voice packs
|
## Voice packs
|
||||||
|
|
||||||
@@ -221,5 +222,5 @@ harness bundle and measured the day of the flip:
|
|||||||
| Endpoint answers, synthesis fails | the engine: `podman healthcheck run frank-tts-engine`, then `systemctl --user status frank-tts-engine.service` |
|
| Endpoint answers, synthesis fails | the engine: `podman healthcheck run frank-tts-engine`, then `systemctl --user status frank-tts-engine.service` |
|
||||||
| First request of the day is ~0.6 s slower than the rest — or every name returns 400 | the warm pass did not run; it carries `-`, so it cannot fail the unit. `journalctl --user -u frank-tts-engine` for the warmer's lines, then `podman exec frank-tts-engine /app/warm-voices.sh` — the registry is memory-only (law 1 of [[Frank TTS Engine]]) |
|
| First request of the day is ~0.6 s slower than the rest — or every name returns 400 | the warm pass did not run; it carries `-`, so it cannot fail the unit. `journalctl --user -u frank-tts-engine` for the warmer's lines, then `podman exec frank-tts-engine /app/warm-voices.sh` — the registry is memory-only (law 1 of [[Frank TTS Engine]]) |
|
||||||
| The unit never appears after a deploy | the generator dropped it: `frank-tts-engine.network` is missing from `~/.config/containers/systemd/` |
|
| The unit never appears after a deploy | the generator dropped it: `frank-tts-engine.network` is missing from `~/.config/containers/systemd/` |
|
||||||
| A consumer rejects `narrator1` by listing 29 names (`af_heart … bm_fable`) and no request ever reaches this endpoint | that consumer is synthesizing **in the browser** with Kokoro.js, whose voice pack is fixed — OpenWebUI 0.11.4 ships it as the `browser-kokoro` TTS engine; see [[OpenWebUI]] |
|
| A consumer rejects `wilwheaton` by listing 29 names (`af_heart … bm_fable`) and no request ever reaches this endpoint | that consumer is synthesizing **in the browser** with Kokoro.js, whose voice pack is fixed — OpenWebUI 0.11.4 ships it as the `browser-kokoro` TTS engine; see [[OpenWebUI]] |
|
||||||
---
|
---
|
||||||
@@ -45,6 +45,8 @@ open-notebook ([lfnovo/open-notebook](https://github.com/lfnovo/open-notebook))
|
|||||||
|
|
||||||
The app's own login stays on behind the chain. It is not redundant: publishing the API on loopback does **not** hide it from this host's other containers, which can dial `open-notebook:5055` by name on `dankclaw_default`. The chain decides *who from outside* gets in; `OPEN_NOTEBOOK_PASSWORD` decides what anything that already reached the port may do.
|
The app's own login stays on behind the chain. It is not redundant: publishing the API on loopback does **not** hide it from this host's other containers, which can dial `open-notebook:5055` by name on `dankclaw_default`. The chain decides *who from outside* gets in; `OPEN_NOTEBOOK_PASSWORD` decides what anything that already reached the port may do.
|
||||||
|
|
||||||
|
**Single tenant — there are no user accounts, so this is an admin-side tool.** Upstream ships one global password and nothing else: `PasswordAuthMiddleware` compares `Bearer <OPEN_NOTEBOOK_PASSWORD>`, and the SurrealDB schema has no user/account/owner table (verified in the running image, 2026-10-20). One shared password therefore means **one shared workspace** — the guard chain can gate the door but cannot partition the data, and Authentik identity is never consumed. Multi-user is *approved but unbuilt* upstream (issue **#712**, "design phase, targeted post-v2"); OIDC/SSO was declined for now (**#607** closed). Real per-person isolation means **one deployment per person**, not a flag to flip. For that reason the homepage lists open-notebook under **admin**, not as a user-facing app.
|
||||||
|
|
||||||
## What the image actually runs
|
## What the image actually runs
|
||||||
|
|
||||||
One container, three processes under supervisord (`supervisord.conf` upstream): `uvicorn api.main:app --port 5055` (`:12`), the job worker (`:25`), and the Next.js frontend (`:39`). Two consequences the config depends on:
|
One container, three processes under supervisord (`supervisord.conf` upstream): `uvicorn api.main:app --port 5055` (`:12`), the job worker (`:25`), and the Next.js frontend (`:39`). Two consequences the config depends on:
|
||||||
@@ -64,9 +66,11 @@ Credentials live in the database, encrypted with `OPEN_NOTEBOOK_ENCRYPTION_KEY`,
|
|||||||
|--------|-------|
|
|--------|-------|
|
||||||
| Credential `frank-tts` | modality `text_to_speech`, base URL `http://frank-tts:7772/v1`, no API key (the shim does not authenticate) |
|
| Credential `frank-tts` | modality `text_to_speech`, base URL `http://frank-tts:7772/v1`, no API key (the shim does not authenticate) |
|
||||||
| Credential `frank-archon` | modality `language`, base URL `http://192.168.0.235:7770/v1` |
|
| Credential `frank-archon` | modality `language`, base URL `http://192.168.0.235:7770/v1` |
|
||||||
|
| Credential `frank-dragoon NPU` | modality `embedding`, base URL `http://frank-dragoon-halogen:8731/v1`, dummy API key (the lane does not authenticate) — a container name on `dankclaw_default`, which is the address shape that clears the URL guard below |
|
||||||
| Model row `voxcpm2` | type `text_to_speech` on the frank-tts credential. The name is decorative: frank-tts ignores the request's `model` field and synthesizes whatever `voice` names ([[Frank TTS]]) |
|
| Model row `voxcpm2` | type `text_to_speech` on the frank-tts credential. The name is decorative: frank-tts ignores the request's `model` field and synthesizes whatever `voice` names ([[Frank TTS]]) |
|
||||||
| Model row `frank-archon` | type `language`; `PUT /api/models/defaults` points chat, transformation and large-context at it. **The embedding default stays null — nothing in the fleet serves `/v1/embeddings`** |
|
| Model row `frank-archon` | type `language`; `PUT /api/models/defaults` points chat, transformation and large-context at it |
|
||||||
| Speaker profile `dank-duo` | two speakers, Frank → `voice_id: narrator1`, Comrade → `narrator2`, `voice_model` = the TTS row |
|
| Model row `qwen3-embedding-0.6b` | type `embedding` on the NPU-lane credential; `default_embedding_model` points at it, so every note save embeds through the XDNA2 lane ([[frank-dragoon]]) |
|
||||||
|
| Speaker profile `dank-duo` | two speakers, Frank → `voice_id: wilwheaton`, Comrade → `vaati`, `voice_model` = the TTS row |
|
||||||
| Episode profile `dank-daily` | `num_segments: 4`, `speaker_config` = `dank-duo`, outline and transcript LLM = the archon row, briefing that ends "Never invent numbers." |
|
| Episode profile `dank-daily` | `num_segments: 4`, `speaker_config` = `dank-duo`, outline and transcript LLM = the archon row, briefing that ends "Never invent numbers." |
|
||||||
|
|
||||||
Order that works over REST: credentials → model rows (`POST /api/models` with `credential: <credential id>`) → `PUT /api/models/defaults` → speaker profile (`voice_model: <model id>`) → episode profile (`speaker_config` / `outline_llm` / `transcript_llm`). `POST /api/models/{id}/test` proves each row: the language row answered with a real completion from frank-archon; the TTS row reached frank-tts and came back `unknown voice 'alloy'; available voices: narrator1, narrator2, narrator3` — the app's own test path hard-codes an OpenAI voice, so that message *is* the proof the leg works.
|
Order that works over REST: credentials → model rows (`POST /api/models` with `credential: <credential id>`) → `PUT /api/models/defaults` → speaker profile (`voice_model: <model id>`) → episode profile (`speaker_config` / `outline_llm` / `transcript_llm`). `POST /api/models/{id}/test` proves each row: the language row answered with a real completion from frank-archon; the TTS row reached frank-tts and came back `unknown voice 'alloy'; available voices: narrator1, narrator2, narrator3` — the app's own test path hard-codes an OpenAI voice, so that message *is* the proof the leg works.
|
||||||
@@ -78,11 +82,16 @@ Order that works over REST: credentials → model rows (`POST /api/models` with
|
|||||||
- `192.168.0.143:7771` (frank-zealot, a host-netns listener) and `192.168.0.143:7770` (frank-dragoon, a **root**-published port) both refuse the connection; `host.containers.internal:7771` answers. Full census: [[Container Network Namespaces]].
|
- `192.168.0.143:7771` (frank-zealot, a host-netns listener) and `192.168.0.143:7770` (frank-dragoon, a **root**-published port) both refuse the connection; `host.containers.internal:7771` answers. Full census: [[Container Network Namespaces]].
|
||||||
- The app refuses to **store** that one working address: its URL guard rejects any base URL that resolves inside link-local `169.254.0.0/16` (`open_notebook/utils/url_validation.py`, "Link-local addresses are not allowed (metadata endpoints)"), unconditionally — no env var touches it, and `AddHost=` cannot dodge it because the check runs on the resolved address.
|
- The app refuses to **store** that one working address: its URL guard rejects any base URL that resolves inside link-local `169.254.0.0/16` (`open_notebook/utils/url_validation.py`, "Link-local addresses are not allowed (metadata endpoints)"), unconditionally — no env var touches it, and `AddHost=` cannot dodge it because the check runs on the resolved address.
|
||||||
|
|
||||||
`http://192.168.0.235:7770/v1` — another machine, plain RFC1918 — is the only fleet LLM address that passes both tests. Cost: frank-archon runs `--max-num-seqs 2` and the operator session itself lives there, so a render is a slow-down for whoever else is talking to that endpoint, not a stall; `ESPERANTO_LLM_TIMEOUT=180` covers an outline or transcript call comfortably.
|
`http://192.168.0.235:7770/v1` — another machine, plain RFC1918 — was, when this was first written, the only fleet LLM address that passed both tests; a container name on `dankclaw_default` passes them as well (the embedding credential below is stored that way), but frank-archon stays the scriptwriter for the reason in the next paragraph. Cost: frank-archon runs `--max-num-seqs 2` and the operator session itself lives there, so a render is a slow-down for whoever else is talking to that endpoint, not a stall; `ESPERANTO_LLM_TIMEOUT=180` covers an outline or transcript call comfortably.
|
||||||
|
|
||||||
**frank-dragoon can write transcripts, but only greedily — and open-notebook cannot ask for greedy.** Measured 2026-10-19 against the live endpoint: `response_format` as `json_schema` or `json_object` returns schema-valid JSON at `temperature: 0` and a **400** at any sampled temperature — halogen decodes greedy under a schema, and because its sampler default is server-side, omitting `temperature` is sampled, not greedy. That 400 is the trap: esperanto's `is_json_schema_unsupported_error` matches the words *not implemented* in it, so `_SchemaFallbackModel` in `podcast_creator/nodes.py` reads it as "this endpoint has no schema support", downgrades to `json_object`, gets the same 400, and **fails the run** — esperanto's own rescue (drop `response_format` entirely) fires only on the different message `'response_format.type' must be 'json_schema'`. The full law is on [[frank-dragoon]]. And the fix is not reachable from here: a model record is `{credential, name, provider, type}` and a model response adds only timestamps (`GET /openapi.json`, `ModelCreate`/`ModelResponse`) — **there is no per-row config field**, no way to attach `temperature: 0` to one row from the UI or the API. Making `frank-dragoon` the writer would therefore need something in the path that rewrites request bodies, which is a heavier tenant than the contention it removes. **frank-archon stays the scriptwriter.** Reachability would be a separate problem anyway: `frank-dragoon` is not on `dankclaw_default` (the serve script passes no `--network`), so nothing on this network has a name for it, and the one address that does answer — `host.containers.internal:7770` — is the link-local the guard refuses to store ([[Container Network Namespaces]]).
|
**frank-dragoon can write transcripts, but only greedily — and open-notebook cannot ask for greedy.** Measured 2026-10-19 against the live endpoint: `response_format` as `json_schema` or `json_object` returns schema-valid JSON at `temperature: 0` and a **400** at any sampled temperature — halogen decodes greedy under a schema, and because its sampler default is server-side, omitting `temperature` is sampled, not greedy. That 400 is the trap: esperanto's `is_json_schema_unsupported_error` matches the words *not implemented* in it, so `_SchemaFallbackModel` in `podcast_creator/nodes.py` reads it as "this endpoint has no schema support", downgrades to `json_object`, gets the same 400, and **fails the run** — esperanto's own rescue (drop `response_format` entirely) fires only on the different message `'response_format.type' must be 'json_schema'`. The full law is on [[frank-dragoon]]. And the fix is not reachable from here: a model record is `{credential, name, provider, type}` and a model response adds only timestamps (`GET /openapi.json`, `ModelCreate`/`ModelResponse`) — **there is no per-row config field**, no way to attach `temperature: 0` to one row from the UI or the API. Making `frank-dragoon` the writer would therefore need something in the path that rewrites request bodies, which is a heavier tenant than the contention it removes. **frank-archon stays the scriptwriter.** Reachability stopped being the objection on 2026-10-19 — the endpoint joined `dankclaw_default`, which is exactly why the embedding lane below is dialable by name — but no network change reaches the no-per-row-config wall.
|
||||||
|
|
||||||
**No embedding model is deployed, and saving a note is where that shows up.** Every note save submits an `embed_note` command that fails in the worker with `No embedding model configured. Please configure one in the Models section.` The note itself is stored and readable; semantic search, source ingestion and notebook-context questions stay dead until something serves `/v1/embeddings`. Podcast generation from `content` does not need embeddings, which is why renders work today. The cheapest door is **halogen's NPU lane, not a new endpoint**: `qwen3-embedding-0.6b` is already in the catalog of the image `frank-dragoon` runs and answers `/v1/embeddings` on the same port (1024 unit-length floats; the instruct prefix goes in the text, and `check_embedding_ctx_length=False` for LangChain). What enabling it costs — the accel device, the host XRT mount, and the fabric-clock hold that GPU+NPU co-run requires — is on [[frank-dragoon]]; after that, the embedding row still has to clear the reachability rule above.
|
**Embeddings run on the XDNA2 lane — provisioned 2026-10-19.** `qwen3-embedding-0.6b` answers `/v1/embeddings` at `http://frank-dragoon-halogen:8731/v1`, the flash container's own bridge address; a container name on `dankclaw_default` resolves to `10.89.1.39`, plain RFC1918, so it clears the URL guard that ruled out `host.containers.internal` above. The lane's shape (one program, the token ceiling, what it costs the Flash model) is owned by [[frank-dragoon]]; the two shapes that bite on the app side are open-notebook's own:
|
||||||
|
|
||||||
|
- **`PUT /api/models/defaults` stores the record ID, not the name.** `"default_embedding_model": "qwen3-embedding-0.6b"` is accepted and echoed back, and then every embed dies in the worker with `Model with ID qwen3-embedding-0.6b not found`. The value has to be the row's `model:…` id — the other defaults in that same object are ids, so mirror them.
|
||||||
|
- **`POST /api/embed` answers 500 while succeeding.** Upstream's `EmbedResponse.command_id` declares `str` and the worker hands it a `RecordID`, so FastAPI raises on serialization *after* the vector is written. The truth is the worker line `Successfully embedded note … in 0.19s`, never the HTTP code. Saving a note (`POST /api/notes`) submits `embed_note` by itself, so that route is only needed to backfill old rows.
|
||||||
|
|
||||||
|
Proof from the app's own paths, 2026-10-19: `POST /api/models/{id}/test` → `Embedding dimensions: 1024`; two notes with nothing in common, then `POST /api/search` with `type: "vector"` (the enum is `text` or `vector`; the score field is `similarity`) — "a yellow waterbird beside a glowing beacon tower" put the grebe note at **0.659** and the boiler note at 0.292, and "quarterly depreciation schedule for industrial boilers" inverted it to **0.906** / 0.318. Embed cost 0.19–0.43 s per note. Semantic search, source ingestion and notebook-context questions are live; podcast generation never needed any of it.
|
||||||
|
|
||||||
**Secrets never go in a command line.** The API's bearer token *is* `OPEN_NOTEBOOK_PASSWORD`, and a `-H "Authorization: Bearer …"` argument is world-readable in `/proc/<pid>/cmdline`; put it in a mode-600 curl config and pass `-K`:
|
**Secrets never go in a command line.** The API's bearer token *is* `OPEN_NOTEBOOK_PASSWORD`, and a `-H "Authorization: Bearer …"` argument is world-readable in `/proc/<pid>/cmdline`; put it in a mode-600 curl config and pass `-K`:
|
||||||
|
|
||||||
@@ -99,13 +108,13 @@ A **speaker profile** carries one to four speakers (`docs/3-USER-GUIDE/creating-
|
|||||||
|
|
||||||
| Speaker | Voice ID to type |
|
| Speaker | Voice ID to type |
|
||||||
|---------|------------------|
|
|---------|------------------|
|
||||||
| Speaker 1 | `narrator1` |
|
| Speaker 1 | `wilwheaton` |
|
||||||
| Speaker 2 | `narrator2` |
|
| Speaker 2 | `vaati` |
|
||||||
| Speaker 3 | `narrator3` |
|
| Speaker 3 | `dr.kern` |
|
||||||
|
|
||||||
Those are the pack names frank-tts resolves from `/home/dank/voices` (`scripts/frank-tts.env` header: a voice is real exactly when `/voices/<name>/reference.wav` exists). Two traps, both from upstream's shipped defaults:
|
Those are the pack names frank-tts resolves from `/home/dank/voices` (`scripts/frank-tts.env` header: a voice is real exactly when `/voices/<name>/reference.wav` exists). Two traps, both from upstream's shipped defaults:
|
||||||
|
|
||||||
- The three seeded speaker profiles carry **OpenAI voice names** (`nova`, `alloy`, …) and **no voice model at all** — they show a "Setup required" badge, and generation fails with a voice error until they are edited (`creating-podcasts.md:13`, `:33`; `local-tts.md:89`). Add a `narrator1`/`narrator2` pair (or a third/fourth) rather than editing the seeded ones in place.
|
- The three seeded speaker profiles carry **OpenAI voice names** (`nova`, `alloy`, …) and **no voice model at all** — they show a "Setup required" badge, and generation fails with a voice error until they are edited (`creating-podcasts.md:13`, `:33`; `local-tts.md:89`). Add a `wilwheaton`/`vaati` pair (or a third/fourth) rather than editing the seeded ones in place.
|
||||||
- Two speakers with the same voice are hard to follow (`creating-podcasts.md:89`). Distinct packs per speaker is the point of the 1–4 shape.
|
- Two speakers with the same voice are hard to follow (`creating-podcasts.md:89`). Distinct packs per speaker is the point of the 1–4 shape.
|
||||||
|
|
||||||
## Two buttons that fail against frank-tts, and why that is cosmetic
|
## Two buttons that fail against frank-tts, and why that is cosmetic
|
||||||
@@ -164,6 +173,7 @@ podman pull docker.io/lfnovo/open_notebook:v1-latest@sha256:54b2025a4a4853c188f7
|
|||||||
- Local measurements (2026-10-18, workstation, pinned digests): `surreal start --help` (credential and bind options carry `[env: SURREAL_*]`); running the pinned SurrealDB digest with env-only credentials (credential init in the log, correct login authenticates, wrong password refused, `/surreal is-ready` → `OK` exit 0, named volume owned by the mapped uid); the v1-compatible Quadlet generator dry-run of all three unit files (two `--env-file` in order, two `--network` in order, named volume + host bind, both health probes, `--pull never`)
|
- Local measurements (2026-10-18, workstation, pinned digests): `surreal start --help` (credential and bind options carry `[env: SURREAL_*]`); running the pinned SurrealDB digest with env-only credentials (credential init in the log, correct login authenticates, wrong password refused, `/surreal is-ready` → `OK` exit 0, named volume owned by the mapped uid); the v1-compatible Quadlet generator dry-run of all three unit files (two `--env-file` in order, two `--network` in order, named volume + host bind, both health probes, `--pull never`)
|
||||||
- Host evidence (Dankclaw, 2026-10-18, read-only): `ss -ltn` (`127.0.0.1:5055` held by seerr, `0.0.0.0:7771` a host listener, nothing on 7772/8502/5056/8000), `podman ps -a` (no open-notebook container, `dankclaw_default` present), subnet registry from `dankclaw/open-webui/quadlet/open-webui.network`
|
- Host evidence (Dankclaw, 2026-10-18, read-only): `ss -ltn` (`127.0.0.1:5055` held by seerr, `0.0.0.0:7771` a host listener, nothing on 7772/8502/5056/8000), `podman ps -a` (no open-notebook container, `dankclaw_default` present), subnet registry from `dankclaw/open-webui/quadlet/open-webui.network`
|
||||||
- Deploy + provisioning (Dankclaw, 2026-10-19): first-boot log — migrations 1→25, `Credentials were provided, and no root users were found. The root user 'open_notebook' will be created`, `api`/`worker`/`frontend` all RUNNING under supervisord; `GET :8502/config` → the `API_URL` value; `GET /api/providers` (the `openai_compatible` modality list); `GET /openapi.json` schemas for `CreateCredentialRequest` / `ModelCreate` / `SpeakerProfileCreate` / `PodcastGenerateRequest`; the POSTs listed above; the note-save worker error `No embedding model configured.`
|
- Deploy + provisioning (Dankclaw, 2026-10-19): first-boot log — migrations 1→25, `Credentials were provided, and no root users were found. The root user 'open_notebook' will be created`, `api`/`worker`/`frontend` all RUNNING under supervisord; `GET :8502/config` → the `API_URL` value; `GET /api/providers` (the `openai_compatible` modality list); `GET /openapi.json` schemas for `CreateCredentialRequest` / `ModelCreate` / `SpeakerProfileCreate` / `PodcastGenerateRequest`; the POSTs listed above; the note-save worker error `No embedding model configured.`
|
||||||
|
- Embedding lane (Dankclaw, 2026-10-19): `POST /api/credentials` → `credential:7fdqz09tt0c4xp68x7cm`, `POST /api/models` → `model:p3ki598dydfinu93tbny`, `POST /api/models/{id}/test` → `Embedding dimensions: 1024`; worker lines `Successfully embedded note … in 0.43s` / `0.19s`; the name-not-id failure `Model with ID qwen3-embedding-0.6b not found`; the `/api/embed` 500 body (`EmbedResponse.command_id`, `input_value=RecordID(table_name=command…)`); `POST /api/search` `similarity` 0.6590/0.2921 and 0.9059/0.3176 for the two-note pair; `podman logs frank-dragoon-halogen` showing `POST /v1/embeddings 200` from `10.89.1.21`. Both probe notes deleted afterwards.
|
||||||
- First render (2026-10-19): job `command:9rdojq5ne7dj9caicq20`, `execution_time: 301.21 s`, `episode:7ft03thj98sn0ek9xf29`; `episodes/d5fed29b-33e9-40ac-b46a-e0537c56c256/clips/0001.mp3 … 0023.mp3`; `…/audio/d5fed29b-….mp3` = 11,957,420 bytes, mp3 24 kHz mono, `ffprobe` duration 747.31 s; `GET /api/podcasts/episodes/{id}/audio` → 200 `audio/mpeg` with the bearer, 401 without.
|
- First render (2026-10-19): job `command:9rdojq5ne7dj9caicq20`, `execution_time: 301.21 s`, `episode:7ft03thj98sn0ek9xf29`; `episodes/d5fed29b-33e9-40ac-b46a-e0537c56c256/clips/0001.mp3 … 0023.mp3`; `…/audio/d5fed29b-….mp3` = 11,957,420 bytes, mp3 24 kHz mono, `ffprobe` duration 747.31 s; `GET /api/podcasts/episodes/{id}/audio` → 200 `audio/mpeg` with the bearer, 401 without.
|
||||||
- Upstream code read inside the pinned image (2026-10-19): `frontend/next.config.ts:26-43` (the `/api/*` rewrite and the one-port claim), `frontend/src/app/config/route.ts:122-173` (`/config` priority order; autodetect appends `:5055`), `frontend/src/app/api/_sse-proxy.ts` (SSE proxied with `no-transform`), `open_notebook/utils/url_validation.py` (link-local refusal, `is_blocked_ip`, no environment override anywhere in the file).
|
- Upstream code read inside the pinned image (2026-10-19): `frontend/next.config.ts:26-43` (the `/api/*` rewrite and the one-port claim), `frontend/src/app/config/route.ts:122-173` (`/config` priority order; autodetect appends `:5055`), `frontend/src/app/api/_sse-proxy.ts` (SSE proxied with `no-transform`), `open_notebook/utils/url_validation.py` (link-local refusal, `is_blocked_ip`, no environment override anywhere in the file).
|
||||||
- Guard chain (2026-10-19): provider `notebook-proxy` created through `podman exec -i authentik-server ak shell` with the flows copied from `embedded-outpost` (pk 1), Application `Open Notebook`/`notebook-guard`, embedded-outpost provider set 9 → 10; afterwards the outpost logged zero `Failed to fetch outpost from API` and `seerr`/`grafana` still answered 200. Before the route existed, `notebook.franksdank.cloud` resolved to nothing (`000`).
|
- Guard chain (2026-10-19): provider `notebook-proxy` created through `podman exec -i authentik-server ak shell` with the flows copied from `embedded-outpost` (pk 1), Application `Open Notebook`/`notebook-guard`, embedded-outpost provider set 9 → 10; afterwards the outpost logged zero `Failed to fetch outpost from API` and `seerr`/`grafana` still answered 200. Before the route existed, `notebook.franksdank.cloud` resolved to nothing (`000`).
|
||||||
@@ -290,9 +290,12 @@ on large sizes ⇒ check ComfyUI resources; 401 ⇒ the token is bad.
|
|||||||
|
|
||||||
## Text-to-speech (frank-tts)
|
## Text-to-speech (frank-tts)
|
||||||
|
|
||||||
TTS is the fleet's one OpenAI-compatible speech endpoint —
|
TTS is the fleet's one OpenAI-compatible speech endpoint. The config DB
|
||||||
`http://host.containers.internal:7772/v1` (`audio.tts.openai.api_base_url`
|
`audio.tts.*` is set to engine `openai`, `audio.tts.openai.api_base_url`
|
||||||
in the config DB, 2026-10-18), model `frank-tts`, voice `narrator1`. That
|
`http://192.168.0.143:7772/v1`, `audio.tts.voice` `wilwheaton` (re-keyed
|
||||||
|
2026-10-20 from the retired `narrator1`; 7772 is a published port so
|
||||||
|
`host.containers.internal` also reaches it, but the LAN IP is the canonical
|
||||||
|
on-host form). That
|
||||||
port is the rootless quadlet `frank-tts` (this repo:
|
port is the rootless quadlet `frank-tts` (this repo:
|
||||||
`dankclaw/voiceservice/quadlet/frank-tts.container`
|
`dankclaw/voiceservice/quadlet/frank-tts.container`
|
||||||
`scripts/frank-tts.env`), and its shim resolves `voice=<name>` against
|
`scripts/frank-tts.env`), and its shim resolves `voice=<name>` against
|
||||||
@@ -306,8 +309,8 @@ ships an in-page Kokoro implementation (chunk carrying a frozen voice table
|
|||||||
`onnx-community/Kokoro-82M-v1.0-ONNX`, plus a personal *Kokoro.js dtype*
|
`onnx-community/Kokoro-82M-v1.0-ONNX`, plus a personal *Kokoro.js dtype*
|
||||||
setting). Its engine select takes `""` (Default) or `browser-kokoro`; when
|
setting). Its engine select takes `""` (Default) or `browser-kokoro`; when
|
||||||
`browser-kokoro` is chosen the page synthesizes locally and rejects any
|
`browser-kokoro` is chosen the page synthesizes locally and rejects any
|
||||||
voice outside its fixed pack — so `narrator1` fails with
|
voice outside its fixed pack — so `wilwheaton` fails with
|
||||||
`Voice: "narrator1" not found. Should be one of: af_heart, …` **without a
|
`Voice: "wilwheaton" not found. Should be one of: af_heart, …` **without a
|
||||||
single request reaching the server**. Personal audio settings are
|
single request reaching the server**. Personal audio settings are
|
||||||
browser: no `user.settings` row and no container env carries an audio
|
browser: no `user.settings` row and no container env carries an audio
|
||||||
override, yet the toast still appears ⇒ the choice lives in that browser's
|
override, yet the toast still appears ⇒ the choice lives in that browser's
|
||||||
|
|||||||
@@ -267,6 +267,12 @@ The silent re-auth path was proven end-to-end: after deleting
|
|||||||
`qa-e2e-admin`'s rows, the e2e admin spec ran against the stale storage
|
`qa-e2e-admin`'s rows, the e2e admin spec ran against the stale storage
|
||||||
state and passed with no login prompt (2 tests, 2.9 s).
|
state and passed with no login prompt (2 tests, 2.9 s).
|
||||||
|
|
||||||
|
## Login-session lifetime (how often you re-authenticate)
|
||||||
|
|
||||||
|
How long a login stays valid is the **`default-authentication-login` stage's `session_duration`** (`UserLoginStage`) — not a global setting, and not `SESSION_COOKIE_AGE` (that is the *pre-login* session). Raised to **`days=7`** on 2026-10-20 (was stage value `0` = the 24 h framework default). It is an **absolute** window, never sliding: authentik implements `SESSION_SAVE_EVERY_REQUEST` nowhere, so activity does not extend it and no supported knob makes it one.
|
||||||
|
|
||||||
|
**`session_duration` is a STRING** in authentik's idiom (`"days=7"`, `"seconds=30"`, `"hours=12"`). A Python `timedelta` stores `"7 days, 0:00:00"`, which `authentik.lib.utils.time.timedelta_from_string` rejects with `ValueError` and login then crashes computing the expiry — so set it with **`scripts/authentik-set-login-session-duration.sh`** (validates the form first). Any non-zero value applies to *every* login (the "remember me" box only adds `remember_me_offset` on top) and a positive `set_expiry` also overrides `SESSION_EXPIRE_AT_BROWSER_CLOSE`, so closing the browser no longer logs you out; **existing sessions keep their old expiry** — delete the user's `authentik_core_session`/`…proxysession` rows and re-login to force it now. This is **DB state, not repo-managed**: a blueprint re-import or rebuild reverts the stage to `0`, so re-run the script after one. The other two login stages (`default-source-*`) drive external-source/enrollment logins and stay at the framework default.
|
||||||
|
|
||||||
## Guard-chain flip recipe (adding a guarded vhost)
|
## Guard-chain flip recipe (adding a guarded vhost)
|
||||||
|
|
||||||
Move a public vhost from `reverse_proxy <app>:<port>` onto the chain:
|
Move a public vhost from `reverse_proxy <app>:<port>` onto the chain:
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ description: "Pi-hole is the LAN DNS server for the homelab."
|
|||||||
tags:
|
tags:
|
||||||
- infrastructure
|
- infrastructure
|
||||||
- pi-hole
|
- pi-hole
|
||||||
timestamp: 2026-08-28
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# Pi-hole
|
# Pi-hole
|
||||||
@@ -46,6 +46,8 @@ The file format is hosts-style (`<IP> <name> [<name> ...]`), one A record per Ca
|
|||||||
|
|
||||||
**Pitfall (2026-08-24):** the first quadlet mounted the file into `/etc/dnsmasq.d/`. FTL never read it; names only resolved from the query cache. A fresh name returned NXDOMAIN. Mount into `/etc/pihole/hosts/`, not `/etc/dnsmasq.d/`.
|
**Pitfall (2026-08-24):** the first quadlet mounted the file into `/etc/dnsmasq.d/`. FTL never read it; names only resolved from the query cache. A fresh name returned NXDOMAIN. Mount into `/etc/pihole/hosts/`, not `/etc/dnsmasq.d/`.
|
||||||
|
|
||||||
|
**Pitfall (2026-10-19): FTL can come up with the record file mounted and still serve none of it.** A plain `systemctl --user restart pihole` (16:25, inside the open-notebook deploy window) left every `franksdank.cloud` name NXDOMAIN for LAN clients while **external names kept answering** — from FTL's cache, which is exactly what makes the failure read like a client or an upstream problem. **The trigger is a container restart, not a boot**, and that matters for where the check belongs: `uptime -s` reads 2026-10-18 01:24 (two days of uptime) while `journalctl --user -u pihole.service` shows `Stopping pihole.service` at 16:25:23 and the container's `StartedAt` at 16:25:27 — so verify after **any** pihole restart, deploy-triggered or manual, not only after a reboot. The instrument is on the box: `getent hosts grafana.franksdank.cloud` (or `dig +short @192.168.0.143 <name>`) answering empty means FTL is holding no local records at all; here the file was present and readable inside the container (`-rw-rw-r-- … /etc/pihole/hosts/addn-hosts`, 44 records) and `dns.hosts` in `pihole.toml` was intact. `podman exec pihole pihole restartdns reload` had every name answering within seconds. The mechanism is **not established** — the same mounts and the same config read fine before and after the reload — so treat it as a load failure of the record sources at FTL start, not as a lost or unmounted file. What it cost: the whole Playwright gate, 18 specs across every vhost failing `ERR_NAME_NOT_RESOLVED` while the fleet itself was healthy ([[E2E Suite]]).
|
||||||
|
|
||||||
## Upstream
|
## Upstream
|
||||||
|
|
||||||
The slirp netns has no useful `resolv.conf`. The env file pins the upstreams: `DNS1=1.1.1.1`, `DNS2=1.0.0.1` (mirrors the retired `dnsmasq.conf` `server=` lines). Every query that is not a local record goes to those resolvers. The zone apex and unknown subdomains fall through to Porkbun.
|
The slirp netns has no useful `resolv.conf`. The env file pins the upstreams: `DNS1=1.1.1.1`, `DNS2=1.0.0.1` (mirrors the retired `dnsmasq.conf` `server=` lines). Every query that is not a local record goes to those resolvers. The zone apex and unknown subdomains fall through to Porkbun.
|
||||||
@@ -74,6 +76,8 @@ Both prerequisites survive the dnsmasq → Pi-hole swap. Do not remove them.
|
|||||||
| Upstream queries fail (local names still answer) | `DNS1`/`DNS2` in the env file are missing or wrong |
|
| Upstream queries fail (local names still answer) | `DNS1`/`DNS2` in the env file are missing or wrong |
|
||||||
| Port 53 `bind: permission denied` crash loop after a reboot | sysctl conflict: the value must be 53 at boot, and remember `/etc/sysctl.conf` is applied **after** all of `sysctl.d/`. Redeploy pihole — `deploy_pihole` reinstalls the drop-in, neutralizes conflicting drop-ins, and comments out a stale `/etc/sysctl.conf` line |
|
| Port 53 `bind: permission denied` crash loop after a reboot | sysctl conflict: the value must be 53 at boot, and remember `/etc/sysctl.conf` is applied **after** all of `sysctl.d/`. Redeploy pihole — `deploy_pihole` reinstalls the drop-in, neutralizes conflicting drop-ins, and comments out a stale `/etc/sysctl.conf` line |
|
||||||
| Single-label name (for example `dankclaw`) answers `127.0.1.1` | The rootless container `/etc/hosts` carries the host entry (`127.0.1.1 <host>`), and FTL serves it. Use the FQDN, or add an `/etc/hosts` entry on the client |
|
| Single-label name (for example `dankclaw`) answers `127.0.1.1` | The rootless container `/etc/hosts` carries the host entry (`127.0.1.1 <host>`), and FTL serves it. Use the FQDN, or add an `/etc/hosts` entry on the client |
|
||||||
|
| Every `franksdank.cloud` name is NXDOMAIN for LAN clients while external names still answer | FTL holds no local records: `getent hosts <name>` on Dankclaw comes back empty → `podman exec pihole pihole restartdns reload` (the 2026-10-19 pitfall under **Records**) |
|
||||||
|
| One client gets nothing while every other client is fine | FTL rate-limiting that client: `podman exec pihole grep Rate-limiting /var/log/pihole/FTL.log`. A `10.0.2.x` client address is a pasta container's DNS forwarder, not a machine |
|
||||||
|
|
||||||
## See Also
|
## See Also
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ tags:
|
|||||||
- gfx1151
|
- gfx1151
|
||||||
- halogen
|
- halogen
|
||||||
- endpoint
|
- endpoint
|
||||||
timestamp: 2026-10-18
|
timestamp: 2026-10-19
|
||||||
---
|
---
|
||||||
|
|
||||||
# frank-dragoon — halogen-flash-server 0.16.2, native v2 abliterated (Dankclaw iGPU)
|
# frank-dragoon — halogen-flash-server 0.16.2, native v2 abliterated (Dankclaw iGPU)
|
||||||
@@ -236,9 +236,8 @@ Embedding contract on that lane: `POST /v1/embeddings`, `model: "qwen3-embedding
|
|||||||
`check_embedding_ctx_length=False`.
|
`check_embedding_ctx_length=False`.
|
||||||
|
|
||||||
Four host prerequisites, each refused **by name** in the serve script and mirrored in `deploy_llm_dragoon` — a
|
Four host prerequisites, each refused **by name** in the serve script and mirrored in `deploy_llm_dragoon` — a
|
||||||
precondition that surfaces as an engine crash-loop is a gate that was never written. All four are satisfied on this
|
precondition that surfaces as an engine crash-loop is a gate that was never written. All four are satisfied and the
|
||||||
box as of 2026-10-19; the endpoint still runs with the lane **off** until that day's mount fix merges, and the first
|
lane is **armed and serving** since 2026-10-19 (`HALOGEN_NPU_MODELS=qwen3-embedding-0.6b`):
|
||||||
armed boot's evidence lands here with it:
|
|
||||||
|
|
||||||
1. `--device /dev/accel/accel0` **and** a node the container's namespace can open. `/dev/accel/accel0` is
|
1. `--device /dev/accel/accel0` **and** a node the container's namespace can open. `/dev/accel/accel0` is
|
||||||
`root:render 0660` and a rootless namespace maps exactly one uid/gid, so the engine's own first words are
|
`root:render 0660` and a rootless namespace maps exactly one uid/gid, so the engine's own first words are
|
||||||
@@ -270,11 +269,14 @@ armed boot's evidence lands here with it:
|
|||||||
unset, so the container opens no outbound connection; `scripts/download-frank-dragoon-npu.sh` pins every file by
|
unset, so the container opens no outbound connection; `scripts/download-frank-dragoon-npu.sh` pins every file by
|
||||||
size and sha256 against the catalog and the payload mount carries the tree in.
|
size and sha256 against the catalog and the payload mount carries the tree in.
|
||||||
|
|
||||||
Price: the programs and their weights sit in host RAM beside the 62.9 GiB of locked weights ("a few GB in all" for
|
Armed, the boot ledger reads (`/home/dank/logs/frank_dragoon.log`, 2026-10-19): the GPU fabric clock is held at its top level (2000 MHz) **before** the NPU starts; `/dev/accel/accel0` is `crw-rw-rw- nobody:nogroup` under driver `amdxdna` with the distro XRT and its NPU plugin; the program arrives as `15 files checked against this image's record`; then `npu_api: qwen3-embedding-0.6b (embeddings) on the NPU engine at 127.0.0.1:8740 (buckets u0…u9; inputs up to 4096 tokens)` and `up to 64 passes queued … up to 7200 s`. Callers never see `:8740` — the lane answers on the same `:8731` as the Flash model and a request picks the engine by its `model` field, so `/v1/models` lists `frank-dragoon` **and** `qwen3-embedding-0.6b`. The fleet's consumer is open-notebook's embedding default ([[Open Notebook]]); that page owns the app-side shapes (the URL guard, defaults-by-id, the `/api/embed` 500 that still writes the vector).
|
||||||
the default set; ~1.1 GiB for `embeddinggemma-2`), and **the Flash model decodes slower while the NPU works** — same
|
|
||||||
memory bus, same power budget. Under this page's RAM law the question is pool versus n-gram table, so read the boot
|
Price: the programs and their weights sit in host RAM beside the locked weights ("a few GB in all" for the default
|
||||||
ledger after the first NPU boot instead of modelling it. If the lane is opened, prefer **0.18.8 or newer**: that
|
set; ~1.1 GiB for `embeddinggemma-2`), and **the Flash model decodes slower while the NPU works** — same memory bus,
|
||||||
release budgets NPU memory before asking for it, fixing kernel errors from several NPU models under a concurrent mix
|
same power budget. Measured 2026-10-19 with one program armed and the endpoint idle, the whole container holds
|
||||||
|
**1.55 GiB** of host RSS (`podman stats`), so this lane is not what the RAM law above is negotiating against; a
|
||||||
|
multi-model set is where that question returns. If the lane is widened, prefer **0.18.8 or newer**: that release
|
||||||
|
budgets NPU memory before asking for it, fixing kernel errors from several NPU models under a concurrent mix
|
||||||
**on the stock kernel driver** — the driver this box runs.
|
**on the stock kernel driver** — the driver this box runs.
|
||||||
|
|
||||||
## Files
|
## Files
|
||||||
|
|||||||
@@ -130,7 +130,10 @@ is a locator: one row, one clause, no numbers.
|
|||||||
|---|---|
|
|---|---|
|
||||||
| [[Agent Docs and Edits]] | Two self-inflicted doc damage classes from parallel agent edits, with the audit checks and recovery order for each. |
|
| [[Agent Docs and Edits]] | Two self-inflicted doc damage classes from parallel agent edits, with the audit checks and recovery order for each. |
|
||||||
| [[Command Center Dotfiles]] | How the dotfiles repo deploys: stow-equivalent packages, full-replica rsync, the pre-overwrite audit, transport gotchas. |
|
| [[Command Center Dotfiles]] | How the dotfiles repo deploys: stow-equivalent packages, full-replica rsync, the pre-overwrite audit, transport gotchas. |
|
||||||
|
| [[Domain Docs]] | Consumer rules for GLOSSARY.md and docs/adr/, and where this vault out-ranks them on operational facts. |
|
||||||
|
| [[Issue Tracker]] | How agent skills read and write Gitea issues here: credential-safe curl, label IDs, and wayfinding without sub-issues. |
|
||||||
| [[OMP Harness]] | The omp harness as configured here: compaction chain arithmetic, per-model sampler routing, and the context-lean audits. |
|
| [[OMP Harness]] | The omp harness as configured here: compaction chain arithmetic, per-model sampler routing, and the context-lean audits. |
|
||||||
|
| [[Triage Labels]] | The five canonical triage roles mapped to this repo's Gitea label strings, plus the numeric-ID quirk. |
|
||||||
|
|
||||||
## Research and methodology
|
## Research and methodology
|
||||||
|
|
||||||
|
|||||||
@@ -30,9 +30,9 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "open-notebook",
|
"name": "open-notebook",
|
||||||
"category": "user",
|
"category": "admin",
|
||||||
"section": "AI",
|
"section": "AI",
|
||||||
"description": "Podcast generator — notes in, spoken episode out: script written by frank-zealot, voices rendered by frank-tts. Guarded twice: Authentik at the edge, then its own login.",
|
"description": "Podcast generator — notes in, spoken episode out: script written by frank-archon, voices rendered by frank-tts. Guarded twice: Authentik at the edge, then its own login. Single shared password, no per-user accounts — admin-only.",
|
||||||
"url": "http://notebook.franksdank.cloud",
|
"url": "http://notebook.franksdank.cloud",
|
||||||
"docs": "",
|
"docs": "",
|
||||||
"probe": "127.0.0.1:8502",
|
"probe": "127.0.0.1:8502",
|
||||||
|
|||||||
@@ -290,7 +290,9 @@ vhost = sys.argv[2]
|
|||||||
cards = [s for s in data if s["name"] == "open-notebook"]
|
cards = [s for s in data if s["name"] == "open-notebook"]
|
||||||
assert len(cards) == 1, f"expected exactly one open-notebook card, got {len(cards)}"
|
assert len(cards) == 1, f"expected exactly one open-notebook card, got {len(cards)}"
|
||||||
card = cards[0]
|
card = cards[0]
|
||||||
assert card["category"] == "user" and card["section"] == "AI", card
|
# category moved to "admin": single shared password, no per-user accounts, so it
|
||||||
|
# is positioned as an admin-side tool, not a user-facing app (see the vault page).
|
||||||
|
assert card["category"] == "admin" and card["section"] == "AI", card
|
||||||
assert card["probe"] == "127.0.0.1:8502", card["probe"]
|
assert card["probe"] == "127.0.0.1:8502", card["probe"]
|
||||||
assert card["method"] == "http"
|
assert card["method"] == "http"
|
||||||
# Deployed and guarded: the card is live and its link is the vhost, so the
|
# Deployed and guarded: the card is live and its link is the vhost, so the
|
||||||
|
|||||||
Reference in new issue
Block a user