Compare commits
9
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
130e9d739f | ||
|
|
854890a155 | ||
|
|
d9f3d1be1e | ||
|
|
afa3a4521c | ||
|
|
9cc8ca1c88 | ||
|
|
9c05c40aa0 | ||
|
|
63219787af | ||
|
|
e914ecbca6 | ||
|
|
1eebf3b129 |
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;
|
||||
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
|
||||
|
||||
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
|
||||
# is a single stdlib-only file in a locally built image). Consumers — omp's
|
||||
# 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
|
||||
# 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 —
|
||||
# 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.
|
||||
DEFAULT_VOICE=narrator1
|
||||
DEFAULT_VOICE=wilwheaton
|
||||
|
||||
# CLI-backend knobs, inert while TTS_BACKEND=omni-http. Empty means "not
|
||||
# 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.
|
||||
- [[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.
|
||||
- [[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
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ tags:
|
||||
- netavark
|
||||
- rootless
|
||||
- dankclaw
|
||||
timestamp: 2026-09-02
|
||||
timestamp: 2026-10-19
|
||||
---
|
||||
|
||||
# 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 |
|
||||
| `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
|
||||
endpoints is `frank-tts` (container DNS, same network) and `frank-archon` (LAN
|
||||
The consequence that changed designs: **a rootless container's menu of local model
|
||||
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
|
||||
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
|
||||
@@ -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
|
||||
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
|
||||
|
||||
- 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`
|
||||
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
|
||||
`{"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
|
||||
(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;
|
||||
@@ -42,8 +42,8 @@ render needs the app installed, not this endpoint.
|
||||
|
||||
## Naming a voice
|
||||
|
||||
A voice **is** a directory name. `voice=narrator1` resolves to
|
||||
`/voices/narrator1/reference.wav`, and that file — not a manifest, not a config
|
||||
A voice **is** a directory name. `voice=wilwheaton` resolves to
|
||||
`/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
|
||||
shim accepts either. A call that sends **neither** — the harness's normal shape,
|
||||
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
|
||||
`400 unknown voice 'x'; available voices: …`, and that list is the honest
|
||||
census. The podcast speakers are `narrator1`, `narrator2`, `narrator3` —
|
||||
[[Open Notebook]] is configured with exactly those three, so renaming them
|
||||
breaks episode rendering, not just speech. A personal voice gets the person's
|
||||
name, lowercase.
|
||||
census. As of 2026-10-20 the live packs are `dr.kern`, `garrickhagon`,
|
||||
`jeffhays`, `vaati`, `wilwheaton`. [[Open Notebook]]'s `dank-duo` profile speaks
|
||||
`wilwheaton` (Frank) and `vaati` (Comrade), and `DEFAULT_VOICE` is `wilwheaton`.
|
||||
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
|
||||
|
||||
@@ -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` |
|
||||
| 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/` |
|
||||
| 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.
|
||||
|
||||
**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
|
||||
|
||||
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:
|
||||
@@ -68,7 +70,7 @@ Credentials live in the database, encrypted with `OPEN_NOTEBOOK_ENCRYPTION_KEY`,
|
||||
| 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 |
|
||||
| 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: narrator1`, Comrade → `narrator2`, `voice_model` = the TTS row |
|
||||
| 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." |
|
||||
|
||||
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.
|
||||
@@ -106,13 +108,13 @@ A **speaker profile** carries one to four speakers (`docs/3-USER-GUIDE/creating-
|
||||
|
||||
| Speaker | Voice ID to type |
|
||||
|---------|------------------|
|
||||
| Speaker 1 | `narrator1` |
|
||||
| Speaker 2 | `narrator2` |
|
||||
| Speaker 3 | `narrator3` |
|
||||
| Speaker 1 | `wilwheaton` |
|
||||
| Speaker 2 | `vaati` |
|
||||
| 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:
|
||||
|
||||
- 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 buttons that fail against frank-tts, and why that is cosmetic
|
||||
|
||||
@@ -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
|
||||
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)
|
||||
|
||||
Move a public vhost from `reverse_proxy <app>:<port>` onto the chain:
|
||||
|
||||
@@ -46,7 +46,7 @@ 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-10-19): FTL can come up with the record file mounted and still serve none of it.** After a Dankclaw restart every `franksdank.cloud` name went 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 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 boot-time load failure of the record sources, 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]]).
|
||||
**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
|
||||
|
||||
|
||||
@@ -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. |
|
||||
| [[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. |
|
||||
| [[Triage Labels]] | The five canonical triage roles mapped to this repo's Gitea label strings, plus the numeric-ID quirk. |
|
||||
|
||||
## Research and methodology
|
||||
|
||||
|
||||
@@ -30,9 +30,9 @@
|
||||
},
|
||||
{
|
||||
"name": "open-notebook",
|
||||
"category": "user",
|
||||
"category": "admin",
|
||||
"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",
|
||||
"docs": "",
|
||||
"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"]
|
||||
assert len(cards) == 1, f"expected exactly one open-notebook card, got {len(cards)}"
|
||||
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["method"] == "http"
|
||||
# Deployed and guarded: the card is live and its link is the vhost, so the
|
||||
|
||||
Reference in new issue
Block a user