feat: finish Pi and workspace management updates

This commit is contained in:
2026-08-16 14:19:32 +02:00
parent 7651b63cea
commit 351361f72f
20 changed files with 2276 additions and 149 deletions
+2 -2
View File
@@ -6,8 +6,8 @@
"apiKey": "$ZAI_API_KEY",
"models": [
{
"id": "glm-5.2",
"name": "GLM-5.2",
"id": "glm-5.3",
"name": "GLM-5.3",
"reasoning": true,
"contextWindow": 200000,
"maxTokens": 131072
+1 -1
View File
@@ -1,7 +1,7 @@
{
"defaultProjectTrust": "always",
"enabledModels": [
"zai/glm-5.2",
"zai/glm-5.3",
"deepseek/deepseek-v4-flash",
"deepseek/deepseek-v4-pro",
"aritmolab/qwen3.6-35b-a3b"
+31 -14
View File
@@ -6,13 +6,18 @@ does not mount a Docker socket, and Pi is never updated in a running container.
## Inspection and configuration
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi status
thothctl --installation /absolute/path/thothii-installation.yaml pi doctor
thothctl --installation /absolute/path/thothii-installation.yaml pi test
thothctl --installation /absolute/path/thothii-installation.yaml pi logs
thothctl --installation /absolute/path/thothii-installation.yaml pi configure
thothctl pi status
thothctl pi doctor
thothctl pi test
thothctl pi logs
thothctl pi configure
```
When `--installation` is omitted, `thothctl` first uses `THOTHII_INSTALLATION` and otherwise
discovers one valid `thothii-installation.yaml` in the current project tree, including an immediate
`deploy/*` directory. Use `--installation /absolute/path/thothii-installation.yaml` as an explicit
override when the descriptor is outside that tree or more than one installation is available.
`status` executes the image-bundled `pi --version`. `doctor` compares that value with both the
container's `PI_VERSION` contract and the `io.thothii.pi.version` image label; a merely nonempty
version is not sufficient. `doctor` and `test` also require a healthy core, a successful Pi smoke,
@@ -25,7 +30,7 @@ models come from the backend's closed model list, and the model choices are rest
selected provider. In non-interactive use, all choices must be explicit:
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi configure \
thothctl pi configure \
--provider zai --model glm-5.2 --thinking medium
```
@@ -70,7 +75,7 @@ secret overrides must never bypass that preflight wrapper.
Configuration reload is a separate lifecycle operation from an image update:
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi restart --yes [--drain]
thothctl pi restart --yes [--drain]
```
`--yes` is required after reviewing the planned core recreation. Restart activates the durable
@@ -104,13 +109,25 @@ recovery rather than deleting recovery material.
## Updating Pi
Every update requires a pinned version, an explicit source, and confirmation:
The normal update uses the repository's pinned version and build source automatically:
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi update \
--version 0.81.0 --source build --yes
thothctl pi update
thothctl pi update --version 0.81.0
```
thothctl --installation /absolute/path/thothii-installation.yaml pi update \
With no `--version`, the command reads the single default `ARG PI_VERSION=<version>` from
`docker/core.Dockerfile` in the selected project. The normal path confirms the explicit update
command, drains active sessions without terminating them, builds the candidate, recreates only
`core`, verifies it, and promotes it transactionally.
Advanced registry updates remain available and require an immutable digest:
```text
thothctl pi update \
--version 0.81.0 --source build --yes --drain
thothctl pi update \
--version 0.81.0 --source pull \
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> --yes
```
@@ -178,7 +195,7 @@ gate can open.
For a failed update with `update-state.json`, first run:
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi rollback --yes
thothctl pi rollback --yes
```
Rollback restores the image recorded in update state, but it checks restart state before making any
@@ -189,8 +206,8 @@ reported problem, then use maintenance recovery.
Inspect and clean a stale durable gate with:
```text
thothctl --installation /absolute/path/thothii-installation.yaml pi maintenance status
thothctl --installation /absolute/path/thothii-installation.yaml pi maintenance recover --yes
thothctl pi maintenance status
thothctl pi maintenance recover --yes
```
`maintenance recover` restores the captured restart image pin and lifecycle override when needed,
+38 -24
View File
@@ -4,14 +4,21 @@ ThothII bundles Pi in the `core` image. Operators use the Pi Management page for
defaults and the host-side `thothctl` CLI for lifecycle work. A local Pi installation is not
required.
In the commands below, replace `/absolute/path/to/thothii-installation.yaml` with the protected
installation descriptor created by the [local installation guide](local.md).
Run these commands from the root of the current ThothII checkout or worktree. `thothctl` discovers
the valid installation descriptor in that project tree, so it uses the `deploy/` files belonging to
the checkout from which you run it. Do not use `~/bin`: `~` is the user home directory, not the
project root.
```sh
THTCTL=/absolute/path/to/thothctl
INSTALLATION=/absolute/path/to/thothii-installation.yaml
mkdir -p bin
go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl
THTCTL=./bin/thothctl
```
If `thothctl` is already on `PATH`, you may use `THTCTL=thothctl` instead. For an installation
stored elsewhere, set `THOTHII_INSTALLATION` or pass
`--installation <absolute-path>/thothii-installation.yaml` explicitly.
## Choose application defaults
Use the **Pi Management** page to select the supported provider, model, and reasoning default, then
@@ -21,8 +28,8 @@ diagnostics; it never accepts or displays a credential, opens a terminal, or upd
Alternatively, use the CLI from an administrator terminal:
```sh
"$THTCTL" --installation "$INSTALLATION" pi configure
"$THTCTL" --installation "$INSTALLATION" pi configure --provider zai --model glm-5.2 --thinking medium
"$THTCTL" pi configure
"$THTCTL" pi configure --provider zai --model glm-5.2 --thinking medium
```
Use GUI Save defaults or CLI `pi configure`, not both for the same change. The CLI's interactive
@@ -32,11 +39,11 @@ methods store application defaults in backend installation settings, not in the
Useful read-only checks are:
```sh
"$THTCTL" --installation "$INSTALLATION" pi status
"$THTCTL" --installation "$INSTALLATION" pi doctor
"$THTCTL" --installation "$INSTALLATION" pi test
"$THTCTL" --installation "$INSTALLATION" pi check
"$THTCTL" --installation "$INSTALLATION" pi logs
"$THTCTL" pi status
"$THTCTL" pi doctor
"$THTCTL" pi test
"$THTCTL" pi check
"$THTCTL" pi logs
```
`pi check` is an alias for `pi test`; logs are a sanitized, bounded snapshot with no follow mode.
@@ -72,7 +79,7 @@ After changing the provider catalog, enabled-model policy, or selected credentia
running application with one confirmed restart:
```sh
"$THTCTL" --installation "$INSTALLATION" pi restart --yes --drain
"$THTCTL" pi restart --yes --drain
```
`--yes` confirms that core will be recreated. Without `--drain`, restart refuses active sessions;
@@ -89,18 +96,25 @@ and `thothctl start` or raw Compose commands for this reload workflow.
## Update the bundled Pi version
`pi update` is for a new bundled Pi version; it is not a configuration reload. Finish or drain
active work, then choose an explicit source and version. A build update uses this checkout:
`pi update` is for a new bundled Pi version; it is not a configuration reload. The simple command
uses the single `ARG PI_VERSION=...` pin in `docker/core.Dockerfile`, builds that version, waits for
active sessions to finish, and recreates only `core`:
```sh
"$THTCTL" --installation "$INSTALLATION" pi update \
--version 0.81.0 --source build --yes --drain
"$THTCTL" pi update
```
To build a specific version, pass `--version`; source, confirmation, and drain are automatic for
this normal build path:
```sh
"$THTCTL" pi update --version 0.81.0
```
A registry update must use an immutable digest, never a mutable tag:
```sh
"$THTCTL" --installation "$INSTALLATION" pi update \
"$THTCTL" pi update \
--version 0.81.0 --source pull \
--image registry.example.invalid/thothii-core@sha256:<64-lowercase-hex-digits> \
--yes --drain
@@ -117,24 +131,24 @@ reported recovery state and transaction override. Do not delete `.thothctl`, sta
containers, or volumes. Inspect status and sanitized logs:
```sh
"$THTCTL" --installation "$INSTALLATION" pi maintenance status
"$THTCTL" --installation "$INSTALLATION" pi status
"$THTCTL" --installation "$INSTALLATION" pi logs
"$THTCTL" pi maintenance status
"$THTCTL" pi status
"$THTCTL" pi logs
```
For a failed update, restore its prior image:
```sh
"$THTCTL" --installation "$INSTALLATION" pi rollback --yes
"$THTCTL" pi rollback --yes
```
For a failed restart, use maintenance recovery instead of rollback. After repairing the reported
Docker, disk, or configuration problem, use the same command to complete either safe recovery path:
```sh
"$THTCTL" --installation "$INSTALLATION" pi maintenance recover --yes
"$THTCTL" --installation "$INSTALLATION" pi doctor
"$THTCTL" --installation "$INSTALLATION" pi test
"$THTCTL" pi maintenance recover --yes
"$THTCTL" pi doctor
"$THTCTL" pi test
```
`pi rollback --yes` restores the prior update image. `pi maintenance recover --yes` checks both
@@ -0,0 +1,171 @@
# Audit critico dei comandi `tht`
Data: 2026-08-15
## Scopo
Questo audit valuta tutti i comandi terminali registrati dall'attuale CLI Python `tht` prima di
unificare la CLI di ThothII sotto un solo eseguibile pubblico. La valutazione incrocia:
- il contratto del workflow Pi in `harness/.pi/skills/tht-sessione/SKILL.md`;
- le invocazioni reali del gate in `harness/.pi/extensions/tht-gate.js`;
- le invocazioni del backend in `backend/src/tht/tht-runner.ts`;
- i job operatore in `backend/src/workspaces/preprocessing-service.ts`;
- il migratore in `docker/session-migrate.sh`;
- test e documentazione esistenti.
L'inventario autorevole contiene 76 comandi Typer più il comando callback `doctor`: 77 comandi
terminali complessivi.
## Legenda
- **WF — intoccabile workflow**: chiamato da Pi, dal gate o dal contratto delle otto fasi. Va
conservato con semantica, output JSON ed exit code compatibili. Non deve necessariamente apparire
nell'help ordinario dell'utente.
- **PL — intoccabile piattaforma**: chiamato dal backend, dai job workspace o dal deployment. Anche
questo è un contratto interno, non necessariamente un comando da mostrare all'utente.
- **ADV — mantenere avanzato**: non è nel flusso automatico, ma offre una capacità amministrativa o
di recupero che sarebbe imprudente perdere. Va nascosto dall'help base.
- **ACCORPA**: la capacità serve, ma non merita un comando autonomo.
- **RIMUOVI**: il comando non ha chiamanti reali ed è duplicato, superato, pericoloso o incompleto.
L'eventuale logica riutilizzata da altri flussi resta una libreria interna.
## Risultato sintetico
| Esito | Numero | Conseguenza |
|---|---:|---|
| WF o PL, intoccabili | 55 | Conservare il contratto; nascondere i primitivi tecnici dall'help base |
| ADV o ACCORPA | 8 | Conservare la capacità riducendo la superficie UX |
| RIMUOVI | 14 | Eliminare il comando dalla nuova CLI |
| **Totale** | **77** | Una sola CLI pubblica molto più semplice, senza riscrivere il workflow vivo |
## Matrice completa
### Diagnostica, configurazione e dipendenze
| Comando | Valutazione |
|---|---|
| `config check` | **ACCORPA** in `tht doctor`: la validazione della configurazione serve, ma due preflight distinti confondono l'utente. |
| `doctor` | **ACCORPA/MANTIENI pubblico** come unico `tht doctor`, includendo controlli host, Compose, storage e configurazione runtime. |
| `db ping` | **PL**: il backend lo usa per rifiutare correttamente una nuova sessione quando il DWH non è raggiungibile o non è read-only. Interno. |
| `db fetch-ca` | **ACCORPA** in `tht setup` o nella configurazione workspace: utile per TLS, ma non giustifica un comando isolato. |
| `ollama ensure` | **PL**: preflight automatico dell'embedder usato dal backend. Interno. |
### Fasi e decision ledger
| Comando | Valutazione |
|---|---|
| `phase advance` | **WF**: il gate lo usa per avanzare solo dopo la decisione umana. Primitivo anti-bypass, quindi interno. |
| `phase meta` | **WF**: fornisce al gate la definizione data-driven delle fasi e dei tipi di decisione. Interno. |
| `phase reopen` | **WF**: è il percorso canonico per tornare a una fase precedente e invalidare deterministicamente gli artefatti successivi. |
| `phase show` | **WF**: il gate lo usa per calcolare la fase corrente. Interno. |
| `decision add` | **WF**: persistenza fondamentale delle decisioni del reviewer. Solo gate, non shell utente. |
| `decision add-batch` | **WF**: scrittura atomica delle decisioni multiple. Evita ledger parziali. |
| `decision add-join-set` | **WF**: sostituzione atomica dell'intero insieme di join. |
| `decision list` | **RIMUOVI**: nessun chiamante; `session show --json` contiene già il ledger necessario. |
| `decision retract` | **RIMUOVI** dalla CLI: nessun flusso vivo lo invoca e `phase reopen` è il percorso di correzione supportato. La semantica tombstone può restare nel dominio finché utile. |
### Sessioni
| Comando | Valutazione |
|---|---|
| `session archive` | **PL**: usato dalla gestione sessioni del backend. |
| `session check` | **WF**: gate oggettivo della fase 5; verifica decisioni e schema linking. |
| `session close` | **PL**: usato dal backend. |
| `session delete` | **PL**: usato dal backend con i relativi controlli applicativi. |
| `session documents` | **WF/PL**: ricostruisce il contesto persistito e alimenta sia Pi sia la GUI. |
| `session fail` | **PL**: usato dal backend per rappresentare il fallimento terminale. |
| `session finalize` | **WF**: chiusura deterministica della fase finale e indicizzazione della domanda risolta. |
| `session list` | **PL**: alimenta la lista sessioni della GUI. |
| `session migrate` | **PL**: eseguito dal servizio one-shot di migrazione server; resta interno dietro `tht sessions migrate`. |
| `session new` | **WF/PL**: crea la persistenza iniziale della domanda; il backend dipende dal JSON restituito. |
| `session preferences get` | **PL**: lettura delle preferenze applicative. Interno. |
| `session preferences set` | **PL**: scrittura delle preferenze applicative. Interno. |
| `session reopen` | **PL**: riapertura dello stato terminale esposta dalla gestione sessioni. |
| `session retrieval-pack` | **WF**: legge il retrieval pack già persistito per il kickoff di Pi. Distinto da `search pack`, che lo costruisce. |
| `session set-group` | **PL**: rinomina il raggruppamento dalla GUI. |
| `session set-name` | **PL**: rinomina la sessione dalla GUI. |
| `session set-question` | **WF**: persiste deterministicamente domanda riscritta e assunzioni. Solo gate. |
| `session set-schema-linking` | **WF**: valida e scrive `schema_linking.json`. Solo gate. |
| `session show` | **WF/PL**: fonte compatta dello stato persistito per resume, gate e backend. |
| `session sync-schema-linking` | **WF**: riproietta deterministicamente il ledger nello schema linking. |
| `session unarchive` | **PL**: usato dalla gestione sessioni del backend. |
### Schema e retrieval
| Comando | Valutazione |
|---|---|
| `schema check` | **PL**: validazione delle annotazioni curate nel workflow workspace. |
| `schema columns` | **WF**: il gate usa il catalogo colonne per validare e correggere il linking. |
| `schema introspect` | **WF**: fallback previsto dal contratto quando manca lo schema fisico; la modalità refresh resta manutenzione. |
| `schema render` | **WF**: produce il contesto mschema usato dal modello. |
| `schema suggest-fks` | **PL**: comando del flusso operatore per le annotazioni FK curate. |
| `search find` | **WF**: ricerca mirata di evidence, valori e formule durante le fasi. |
| `search pack` | **WF/PL**: costruisce e persiste il contesto iniziale F1; usato anche dal backend. |
### CTE, SQL e datamart
| Comando | Valutazione |
|---|---|
| `cte info` | **WF**: restituisce SQL persistito, posizione nel piano e ultimo test. |
| `cte list` | **RIMUOVI**: nessun chiamante o test; `cte plan`, `cte info` e `session documents` coprono il bisogno. |
| `cte next` | **WF**: il gate determina il prossimo CTE da revisionare. |
| `cte plan` | **WF**: persiste l'ordine completo dei CTE. |
| `cte save` | **WF**: tool deterministico di scrittura usato dal gate. |
| `cte test` | **WF**: verifica read-only dei CTE prevista esplicitamente dal contratto. |
| `sql validate` | **WF**: validazione strutturale e read-only prima dell'esecuzione. |
| `sql preview` | **WF/PL**: preview controllata usata dal modello e dalla GUI. |
| `sql set-final` | **WF**: unica scrittura canonica di `sql_final.sql` attraverso il repository di sessione. |
| `sql export` | **PL**: esportazione richiesta dalla GUI. |
| `sql explain` | **RIMUOVI**: nessun chiamante, test o requisito nel workflow corrente. Si reintroduce solo con un vero passo di analisi del piano. |
| `sql save` | **RIMUOVI**: duplica `set-final` ed `export` e permette un percorso di scrittura non usato. |
| `datamart generate` | **WF**: fase 8 del workflow. |
### Memory
| Comando | Valutazione |
|---|---|
| `memory promote` | **WF**: preview dei candidati di promozione usata dal gate. |
| `memory save-one` | **WF**: persistenza atomica della singola memory approvata. |
| `memory search` | **WF**: recupero delle memory riutilizzabili nella fase 2. |
| `memory solved-index` | **WF**: recupero manuale previsto se l'indicizzazione al finalize fallisce. |
| `memory solved-search` | **WF**: recupero di domande risolte simili nelle fasi successive. |
| `memory list` | **ADV**: mantenere per amministrare record errati, ma fuori dall'help base. |
| `memory show` | **ADV**: mantenere insieme a `list` per ispezione puntuale. |
| `memory update` | **ADV**: mantenere per correggere il merito di una memory senza alterarne la provenienza. |
| `memory delete` | **ADV**: mantenere come rimedio selettivo; richiede conferma esplicita nella nuova CLI. |
| `memory index` | **ADV**: utile come riparazione/full-resync, ma va presentato come manutenzione e non come uso normale. |
| `memory clear` | **RIMUOVI**: distruzione globale non usata; confligge con una UX sicura di backup/ripristino. |
| `memory migrate` | **RIMUOVI**: migrazione legacy una tantum senza dati di produzione da preservare. |
### Preprocessing, evidence e indici
| Comando | Valutazione |
|---|---|
| `preprocess dwh` | **PL**: pipeline canonica usata da `tht workspace preprocess dwh/run`. |
| `preprocess evidence` | **PL**: pipeline canonica usata da `tht workspace preprocess evidence/run`. |
| `vector index-schema` | **PL**: indicizzazione schema usata dal workflow workspace. |
| `evidence extract` | **RIMUOVI**: primitivo superato dalla pipeline versionata `preprocess evidence`. Conservare soltanto la logica riusata. |
| `evidence index` | **RIMUOVI**: primitivo superato dalla stessa pipeline versionata. |
| `lsh build` | **RIMUOVI** come comando: è già uno step di `preprocess dwh`; il builder resta interno. |
| `lsh query` | **RIMUOVI**: probe visuale senza chiamanti, test o documentazione operativa. La ricerca applicativa passa da `search find`. |
| `vector init` | **RIMUOVI**: il controllo di Qdrant/embedder è ormai coperto dal reconciler di collezione, da `ollama ensure` e dal nuovo `tht doctor`. |
### Formule di concetto
| Comando | Valutazione |
|---|---|
| `formula save` | **RIMUOVI** dalla CLI corrente: nessun chiamante, test o flusso di approvazione lo usa. Conservare il formato/store e la lettura tramite `search find --kind formula`. |
| `formula list` | **RIMUOVI**: stesso sottosistema incompleto. Un futuro flusso di curation dovrà progettare insieme creazione, approvazione, elenco e modifica. |
## Conseguenza per la nuova CLI unica
La semplificazione migliore non consiste nel rinominare tutti i 55 contratti vivi o nel mostrarli
all'utente. Consiste nel mantenere un unico eseguibile `tht` con due livelli di visibilità:
1. l'help ordinario mostra soltanto setup, lifecycle, backup/restore, Pi e workspace;
2. i contratti WF/PL restano invocabili dallo stesso eseguibile, ma sono interni/nascosti e usati da
backend, gate e job one-shot.
In questo modo l'utente vede una CLI piccola, mentre il workflow non subisce una riscrittura inutile
e rischiosa. Non serve un secondo eseguibile né un alias `thothctl`.
@@ -0,0 +1,148 @@
# Proposta maintain-erase-enhance per i comandi `tht`
Data: 2026-08-15
## Criterio
- **MAINTAIN**: il comando resta disponibile senza modifiche sostanziali. Come richiesto, non viene
aggiunta una motivazione.
- **ERASE**: il comando viene eliminato dalla nuova CLI; la motivazione indica la duplicazione, il
superamento o l'assenza di un utilizzo reale.
- **ENHANCE**: la capacità viene mantenuta, ma il comando viene migliorato, accorpato o reso più
sicuro. La proposta indica l'intervento.
La proposta copre tutti i 77 comandi terminali dell'attuale CLI Python.
## Sintesi
| Proposta | Numero |
|---|---:|
| MAINTAIN | 55 |
| ENHANCE | 8 |
| ERASE | 14 |
| **Totale** | **77** |
## Lista completa
### Diagnostica, configurazione e dipendenze
| Comando | Proposta |
|---|---|
| `config check` | **ENHANCE** — incorporare la validazione nel comando pubblico `tht doctor`, mantenendo una funzione interna riutilizzabile e l'output strutturato. Evita due preflight sovrapposti. |
| `doctor` | **ENHANCE** — farne l'unica diagnostica multilivello: installazione, descriptor, Compose, storage, configurazione runtime, DWH, Pi, Qdrant ed embedder. Deve offrire output umano e `--json`, senza mutare lo stato. |
| `db ping` | **MAINTAIN** |
| `db fetch-ca` | **ENHANCE** — integrarlo nel setup guidato del workspace, mostrando endpoint e fingerprint prima della conferma. Può restare disponibile come operazione TLS avanzata, ma non come passaggio manuale obbligatorio. |
| `ollama ensure` | **MAINTAIN** |
### Fasi e decision ledger
| Comando | Proposta |
|---|---|
| `phase advance` | **MAINTAIN** |
| `phase meta` | **MAINTAIN** |
| `phase reopen` | **MAINTAIN** |
| `phase show` | **MAINTAIN** |
| `decision add` | **MAINTAIN** |
| `decision add-batch` | **MAINTAIN** |
| `decision add-join-set` | **MAINTAIN** |
| `decision list` | **ERASE** — non ha chiamanti reali e duplica il ledger già restituito da `session show --json`. |
| `decision retract` | **ERASE** — non è invocato dal workflow corrente; `phase reopen` è il percorso supportato per correggere e invalidare deterministicamente le decisioni. La semantica tombstone può restare nel dominio. |
### Sessioni
| Comando | Proposta |
|---|---|
| `session archive` | **MAINTAIN** |
| `session check` | **MAINTAIN** |
| `session close` | **MAINTAIN** |
| `session delete` | **MAINTAIN** |
| `session documents` | **MAINTAIN** |
| `session fail` | **MAINTAIN** |
| `session finalize` | **MAINTAIN** |
| `session list` | **MAINTAIN** |
| `session migrate` | **MAINTAIN** |
| `session new` | **MAINTAIN** |
| `session preferences get` | **MAINTAIN** |
| `session preferences set` | **MAINTAIN** |
| `session reopen` | **MAINTAIN** |
| `session retrieval-pack` | **MAINTAIN** |
| `session set-group` | **MAINTAIN** |
| `session set-name` | **MAINTAIN** |
| `session set-question` | **MAINTAIN** |
| `session set-schema-linking` | **MAINTAIN** |
| `session show` | **MAINTAIN** |
| `session sync-schema-linking` | **MAINTAIN** |
| `session unarchive` | **MAINTAIN** |
### Schema e retrieval
| Comando | Proposta |
|---|---|
| `schema check` | **MAINTAIN** |
| `schema columns` | **MAINTAIN** |
| `schema introspect` | **MAINTAIN** |
| `schema render` | **MAINTAIN** |
| `schema suggest-fks` | **MAINTAIN** |
| `search find` | **MAINTAIN** |
| `search pack` | **MAINTAIN** |
### CTE, SQL e datamart
| Comando | Proposta |
|---|---|
| `cte info` | **MAINTAIN** |
| `cte list` | **ERASE** — non ha chiamanti o test e sovrappone informazioni già disponibili con `cte plan`, `cte info` e `session documents`. |
| `cte next` | **MAINTAIN** |
| `cte plan` | **MAINTAIN** |
| `cte save` | **MAINTAIN** |
| `cte test` | **MAINTAIN** |
| `sql validate` | **MAINTAIN** |
| `sql preview` | **MAINTAIN** |
| `sql set-final` | **MAINTAIN** |
| `sql export` | **MAINTAIN** |
| `sql explain` | **ERASE** — non è usato né testato dal workflow attuale. Va reintrodotto soltanto se l'analisi del piano diventa un passo esplicito del processo. |
| `sql save` | **ERASE** — duplica `sql set-final` e `sql export` e introduce un percorso di scrittura non utilizzato. |
| `datamart generate` | **MAINTAIN** |
### Memory
| Comando | Proposta |
|---|---|
| `memory promote` | **MAINTAIN** |
| `memory save-one` | **MAINTAIN** |
| `memory search` | **MAINTAIN** |
| `memory solved-index` | **MAINTAIN** |
| `memory solved-search` | **MAINTAIN** |
| `memory list` | **ENHANCE** — trasformarlo in una vista amministrativa paginata, con filtri, provenienza, stato e output `--json`; non mostrarlo nell'help base. |
| `memory show` | **ENHANCE** — mostrare provenienza immutabile, decisione sorgente, stato dell'indice e riferimenti necessari a una correzione consapevole. |
| `memory update` | **ENHANCE** — limitare l'aggiornamento ai campi modificabili, mostrare un diff prima della conferma e impedire modifiche alla provenienza. |
| `memory delete` | **ENHANCE** — richiedere identificatore esatto e conferma esplicita, mostrare l'impatto e verificare la rimozione coerente da registro e indice. |
| `memory index` | **ENHANCE** — riposizionarlo come comando di repair: prima rileva il drift, poi ricostruisce soltanto con conferma e verifica finale. Non deve sembrare un'operazione ordinaria. |
| `memory clear` | **ERASE** — cancellazione globale non usata e troppo facile da eseguire per errore; backup/ripristino e cancellazione selettiva sono percorsi più sicuri. |
| `memory migrate` | **ERASE** — migrazione legacy una tantum; non esistono dati di produzione da preservare e la nuova architettura può partire direttamente dal formato corrente. |
### Preprocessing, evidence e indici
| Comando | Proposta |
|---|---|
| `preprocess dwh` | **MAINTAIN** |
| `preprocess evidence` | **MAINTAIN** |
| `vector index-schema` | **MAINTAIN** |
| `evidence extract` | **ERASE** — è un primitivo superato dalla pipeline versionata `preprocess evidence`; l'eventuale logica condivisa resta interna. |
| `evidence index` | **ERASE** — è un secondo primitivo superato dalla stessa pipeline, che già gestisce materializzazione, indicizzazione, versionamento e resume. |
| `lsh build` | **ERASE** — la costruzione LSH è già uno step di `preprocess dwh`; mantenere due ingressi permette esecuzioni parziali incoerenti. |
| `lsh query` | **ERASE** — probe visuale senza chiamanti, test o documentazione operativa; il workflow usa `search find`. |
| `vector init` | **ERASE** — il controllo di Qdrant ed embedder è già coperto dal reconciler della collezione, da `ollama ensure` e dal nuovo `tht doctor`. |
### Formule di concetto
| Comando | Proposta |
|---|---|
| `formula save` | **ERASE** — non ha chiamanti, test o un flusso di approvazione completo. Il formato e lo store possono restare disponibili alla ricerca finché non viene progettata una vera curation. |
| `formula list` | **ERASE** — appartiene allo stesso sottosistema incompleto; un futuro flusso deve progettare insieme creazione, approvazione, elenco, modifica e cancellazione. |
## Impatto sulla UX
I 55 comandi `MAINTAIN` comprendono molti contratti macchina intoccabili. Mantenerli non implica
mostrarli tutti nell'help principale. La futura CLI unica può conservare gli stessi percorsi per
backend, gate e job, mostrando all'utente soltanto i gruppi operativi di primo livello.
@@ -0,0 +1,86 @@
# Simplified `thothctl` installation selection and Pi update Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Allow all existing `thothctl` commands to discover the installation descriptor automatically and make `thothctl pi update` use the checkout's pinned Pi version by default.
**Architecture:** Add a small config-level resolver that chooses one validated installation descriptor from an explicit flag, environment variable, or bounded upward search from the working directory. Keep the existing Pi lifecycle transaction intact; resolve only the requested version/source at the CLI boundary so the update engine retains its safety and recovery guarantees.
**Tech Stack:** Go 1.26, Docker Compose v2, existing `tools/thothctl` config and Pi lifecycle packages, Go tests.
**Spec:** `docs/superpowers/specs/2026-08-14-thothctl-discovery-and-pi-update-design.md`
## Global Constraints
- Preserve every existing `pi` subcommand, alias, safety check, and explicit invocation form.
- `--installation` remains an explicit override and accepts only an absolute descriptor path named `thothii-installation.yaml`.
- Automatic discovery must not recursively scan `.artifacts`, home directories, or unrelated descendants.
- The default Pi version is the single `ARG PI_VERSION=<version>` in the selected project's `docker/core.Dockerfile`.
- The default Pi update uses the existing transactional build path and must not install an arbitrary network “latest”.
- All failures remain sanitized and must not reveal secret values.
## File Map
- Create `tools/thothctl/internal/config/discovery.go` and `discovery_test.go` for bounded descriptor resolution and safe diagnostics.
- Modify `tools/thothctl/cmd/thothctl/main.go` and `main_test.go` for optional global selection, `pi update` defaults, help text, and dispatch.
- Create `tools/thothctl/internal/pi/version.go` and `version_test.go` for reading the project Pi pin.
- Modify `tools/thothctl/internal/pi/update.go` and `update_test.go` only if the default request needs a typed source/confirmation adjustment; keep lifecycle internals unchanged otherwise.
- Modify `docs/contracts/thothctl-pi.md`, `docs/install/pi-management.md`, and relevant command-contract verification scripts.
### Task 1: Add bounded installation descriptor discovery
**Files:**
- Create: `tools/thothctl/internal/config/discovery.go`
- Test: `tools/thothctl/internal/config/discovery_test.go`
**Interface:** `func Resolve(explicit string, environment func(string) string, workingDirectory string) (string, error)`.
- [x] Write failing tests for explicit-path precedence, `THOTHII_INSTALLATION`, `deploy/*/thothii-installation.yaml` discovery, parent discovery, `.artifacts` exclusion, invalid candidates, ambiguity, and no-candidate errors.
- [x] Run `cd tools/thothctl && go test ./internal/config -run 'TestResolve' -count=1`; confirm RED because `Resolve` is absent.
- [x] Implement a bounded upward walk. At each level inspect only the exact descriptor and immediate `deploy/*/thothii-installation.yaml` entries; skip `.artifacts`; require regular files; validate candidates through `config.Load`; deduplicate canonical paths; fail clearly on zero or multiple valid candidates.
- [x] Re-run the focused tests and confirm GREEN.
- [x] Refactor only after green, keeping path collection separate from candidate validation.
### Task 2: Make the global installation option optional
**Files:**
- Modify: `tools/thothctl/cmd/thothctl/main.go`
- Test: `tools/thothctl/cmd/thothctl/main_test.go`
- [x] Add failing CLI tests proving `thothctl pi status` works from a project tree, the environment variable is used, an explicit flag wins, ambiguity fails before Docker, and all existing commands retain their dispatch.
- [x] Run the focused CLI tests and confirm RED because the current parser requires `--installation`.
- [x] Parse optional `--installation`, call `config.Resolve` with `THOTHII_INSTALLATION` and the process working directory, and update help to `thothctl [--installation PATH] <command>`.
- [x] Run `cd tools/thothctl && go test ./cmd/thothctl -count=1`; confirm GREEN.
### Task 3: Default `pi update` to the repository Pi pin
**Files:**
- Create: `tools/thothctl/internal/pi/version.go`
- Test: `tools/thothctl/internal/pi/version_test.go`
- Modify: `tools/thothctl/cmd/thothctl/main.go`
- Test: `tools/thothctl/cmd/thothctl/main_test.go`
**Interface:** `func ReadPinnedVersion(projectDirectory string) (string, error)`.
- [x] Add failing tests for one valid Dockerfile pin, missing Dockerfile, duplicate default pins, malformed versions, and `pi update` without `--version`; retain explicit version and advanced pull tests.
- [x] Run `cd tools/thothctl && go test ./internal/pi ./cmd/thothctl -run 'Test(ReadPinnedVersion|ParsePiUpdate|RunPiUpdate)' -count=1`; confirm RED.
- [x] Read only `docker/core.Dockerfile`, require one default `ARG PI_VERSION=...`, validate it with the existing version grammar, and make the short request select build mode while preserving the lifecycle transaction.
- [x] Re-run focused tests and confirm GREEN.
### Task 4: Update contracts without removing commands
**Files:**
- Modify: `docs/contracts/thothctl-pi.md`
- Modify: `docs/install/pi-management.md`
- Modify: the documentation verification script that asserts the old mandatory update invocation.
- [x] Document automatic descriptor discovery, the explicit override, `thothctl pi update` as the normal path, `--version` as an explicit pin, and the advanced pull/digest form.
- [x] Keep status, doctor, test/check, configure, restart, rollback, maintenance, and logs documented.
- [x] Run the targeted documentation checks and `git diff --check`.
### Task 5: Full verification
- [x] Run `cd tools/thothctl && go test ./... -count=1`.
- [x] Run `cd tools/thothctl && go build ./cmd/thothctl`.
- [x] Run the relevant documentation contract script and inspect `git status --short`.
- [x] Verify the help text contains the optional form and all existing commands; do not mutate the live Docker installation unless separately requested.
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,44 @@
# Simplified `thothctl` installation selection and Pi update design
## Decision
Keep every existing `thothctl pi` subcommand. Make the installation descriptor optional on the command line and resolve it automatically when the operator runs from the project tree. Keep `--installation <path>` as an explicit override for non-standard locations or multiple installations.
The normal Pi update becomes:
```sh
thothctl pi update
```
When `--version` is omitted, `thothctl` reads the single default `ARG PI_VERSION=<version>` from `docker/core.Dockerfile` in the selected installation's project directory and uses that pinned version with the existing transactional build/update path. An explicit `--version <version>` remains supported. The command does not fetch an arbitrary npm “latest”; the repository pin, lockfile, image labels, and executable must remain consistent.
## Installation resolution
Resolution order is:
1. an explicit `--installation <absolute-path>/thothii-installation.yaml`;
2. `THOTHII_INSTALLATION`, when set to an absolute descriptor path;
3. automatic discovery from the current working directory and its parents.
Automatic discovery examines only the exact descriptor at each directory level and the immediate `deploy/*/thothii-installation.yaml` locations. It never recursively scans `.artifacts`, home directories, or unrelated descendants. A candidate must be a regular file and must pass `config.Load`. One valid candidate is selected. No candidates or multiple valid candidates produce an actionable error that names the expected locations and explains how to use `--installation`.
The resolver is shared by all existing top-level commands, not only `pi`, so `thothctl status`, `start`, `stop`, `doctor`, `workspace`, and the Pi commands have the same invocation rules. Existing explicit invocations remain valid.
## Safety and compatibility
- No existing Pi subcommand is removed or renamed.
- Existing advanced `pi update --source ... --image ... --yes --drain` syntax remains accepted for compatibility.
- The short update path selects build mode and preserves the existing lifecycle lock, maintenance gate, session handling, candidate verification, image selector promotion, and rollback/recovery behavior.
- The default update uses the current checkout's declared Pi pin; changing to a newer Pi release still requires updating the repository pin and lockfile in the normal source-update workflow.
- Errors and automatic-discovery diagnostics never expose secret file contents.
## User-facing examples
```sh
thothctl pi update
thothctl pi update --version 0.81.0
thothctl pi status
thothctl pi restart --yes --drain
thothctl --installation ~/operator/thothii-installation.yaml pi update
```
@@ -0,0 +1,235 @@
# Unified `tht` CLI and Product Setup Design
Date: 2026-08-15
## Objective
Turn the repository into a product that can be cloned, bootstrapped once, and then operated with a
single normal system command named `tht`. The operator must not build Go manually, create a `bin`
directory, remember an installation descriptor path, or use raw Docker commands for normal
installation, lifecycle, Pi updates, backup, or restore.
## Confirmed decisions
1. The only product command name is `tht`.
2. `thothctl` is removed completely. There is no compatibility alias, wrapper, deprecation period,
or second installed command.
3. The host operator implementation remains a native Go binary so macOS, Linux, and Windows hosts
do not require Go, Python, or a virtualenv.
4. The existing Python workflow CLI remains inside `core` under the same command name `tht`.
Backend and Pi continue to use it there. It is not installed on the host and is not presented in
operator documentation. No `tht-runtime` command or namespace is introduced.
5. The command audit is binding: 55 commands are `MAINTAIN`, 8 are `ENHANCE`, and 14 are `ERASE`.
The detailed decision matrix is in
`docs/reports/2026-08-15-tht-command-maintain-erase-enhance.md`.
6. Workflow commands called by Pi, the gate, the backend, workspace maintenance, or the session
migrator are machine contracts. Their semantics, pristine JSON output, stdin behavior, and exit
codes are not changed merely to simplify the operator help.
7. The public operator help stays small. Internal workflow commands do not appear in host help.
8. `--installation` remains available as an optional override. Normal commands discover the
installation from the current project root or worktree.
9. `backup` and `restore` are introduced as first-class product commands.
10. The final implementation is installed on this Mac and used to rebuild/restart the live stack on
port 8080.
## Command boundary
### Host operator CLI
The native host command exposes:
```text
tht setup
tht version
tht start [--build]
tht stop
tht status
tht doctor [--json]
tht logs
tht update [--check-only] [--yes] [--drain]
tht backup [--output PATH] [--include-secrets --yes] [--drain]
tht restore ARCHIVE --yes [--drain]
tht sessions migrate --yes
tht remove [--yes ID...]
tht pi ...
tht workspace ...
```
`tht pi` preserves `status`, `doctor`, `test`/`check`, `configure`, `restart`, `update`,
`rollback`, `maintenance`, and `logs`. `tht workspace` preserves every currently implemented
workspace operation, including the two vector operations missing from the current help.
### Container workflow CLI
The Python command tree remains the deterministic protocol used by Pi and the backend. The host
installation does not expose these commands as operator shortcuts. This prevents a human operator
from bypassing reviewer gates while avoiding a risky rewrite of the live workflow.
The `MAINTAIN`, `ENHANCE`, and `ERASE` decisions apply exactly as recorded in the command audit.
`MAINTAIN` commands retain their current path. `ENHANCE` commands receive the approved safety or
diagnostic improvements. `ERASE` commands disappear from Typer registration and active docs; domain
functions still used by canonical pipelines remain internal libraries.
## Bootstrap and PATH installation
A freshly cloned repository necessarily needs one bootstrap action before `tht` exists:
```bash
bash scripts/install-tht.sh
```
Windows uses:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/install-tht.ps1
```
The scripts require Docker, build the correct native binary using the repository-pinned Docker
builder, verify it, and install it atomically:
- macOS and Linux: `/usr/local/bin/tht`, requesting elevation only for the final atomic install;
- Windows: `%LOCALAPPDATA%\ThothII\bin\tht.exe`, adding that directory to the user PATH when needed.
The user never selects a platform binary, runs Go, creates a `bin` directory, or invokes a
project-relative executable. Re-running the installer upgrades the installed command idempotently.
## Project and worktree discovery
Every host command starts from the current working directory and walks parents until it finds the
ThothII root contract (`compose.yaml`, `deploy/`, and the repository marker). A Git worktree root is
treated exactly like the main checkout.
Installation resolution order is:
1. explicit `--installation PATH`;
2. `THOTHII_INSTALLATION`;
3. one valid `thothii-installation.yaml` in the current directory or immediate `deploy/*`;
4. the same search while walking parent directories.
Zero candidates produce a setup-oriented error. Multiple candidates produce a bounded list and
require `--installation`; no arbitrary recursive search is allowed.
## `tht setup`
`tht setup` is idempotent and defaults to the local profile on macOS, Windows, and workstation
Linux. `--profile server` selects server behavior. Generated installation files live under
`deploy/<installation-id>/`, where `deploy` is explicitly documented as a directory in the project
or worktree root.
The setup flow:
1. verifies root/worktree identity, Docker, Compose, supported architecture, and line endings;
2. creates or validates the installation descriptor and non-secret environment files;
3. asks plain-language questions and stores secret file paths, never secret values in the
descriptor;
4. creates protected secret-file templates only after explicit confirmation and never overwrites an
existing file;
5. renders and validates Compose configuration;
6. builds the ThothII images from the current checkout;
7. starts the stack with `docker compose up --detach --remove-orphans`;
8. waits for bounded health checks;
9. runs installation diagnostics and Pi diagnostics;
10. prints the URL and the exact descriptor selected.
`tht setup --configure-only` stops after validated configuration. `tht setup` never silently
replaces a descriptor, environment file, secret file, or generated state belonging to another
installation.
## Lifecycle and product update
- `tht start` starts the selected installation without rebuilding.
- `tht start --build` builds current-checkout images before startup.
- `tht stop` stops the installation while preserving state.
- `tht update --check-only` retains its current non-mutating validation behavior.
- `tht update` becomes the complete product update: lifecycle lock, active-session check/drain,
backup checkpoint, image build from the current checkout, controlled recreation, health checks,
diagnostics, and rollback to the recorded images when verification fails.
Product update and Pi update remain separate. `tht update` updates ThothII. `tht pi update` updates
only Pi in `core`.
## Pi management
`tht pi update [--version VERSION]` makes the version optional. Without `--version`, it queries the
latest stable version of the pinned Pi package from the authoritative package registry. A lookup
failure stops before mutation and tells the operator to retry or supply `--version`; it never
silently substitutes an older pin.
The command builds or pulls a candidate, verifies the Pi executable, `PI_VERSION`, and image label,
recreates only `core`, checks health, runs the smoke test, preserves volumes, and rolls back on
failure. Interactive terminals receive one clear confirmation; non-interactive execution requires
`--yes`. Model/provider selection remains the responsibility of `tht pi configure` and is not a
required argument to Pi update.
The project release pin in `docker/core.Dockerfile` remains the clean-build default. Installation
update state records the selected newer image/version so ordinary restart does not revert it.
## Backup and restore
`tht backup` creates a versioned manifest and checksummed archive in
`~/.thothii/backups/<installation-id>/` unless `--output` is supplied. It acquires the lifecycle
lock, refuses active work unless `--drain` is accepted, obtains a consistent stopped snapshot, and
restarts/verifies a previously running installation.
The backup includes:
- installation descriptor, non-secret environment/configuration, generated overrides, and source
revision metadata;
- installation-owned settings, Pi state, workspace registry, workspace secrets volume, sessions,
Qdrant data, and embedding-model volume;
- server bind roots returned by the installation preservation contract;
- a manifest of external secret-file paths and digests.
Secret-file contents are excluded by default. `--include-secrets --yes` includes them and marks the
archive sensitive; the file is created with owner-only permissions. A backup without secrets is
restorable only when all referenced secret files still exist and match preflight requirements.
`tht restore ARCHIVE --yes` validates schema version, checksums, installation identity, target
ownership, secret prerequisites, disk space, and stopped/quiescent state before mutation. It creates
a rollback checkpoint, restores only manifest-listed paths/volumes, starts the stack when it was
previously running, and runs health, `doctor`, `pi doctor`, and workspace inspection. Failure keeps
the target in a recoverable stopped state and prints the checkpoint path.
## Pi Management frontend
The frontend uses only Docker-based instructions and only the command `tht`. The section:
- begins fully collapsed;
- uses separate Linux, macOS, and Windows environment panels, with none open initially;
- is shorter than the current panel and has a working vertical scrollbar;
- begins with the exact concise wording `Using the host terminal`;
- explains that `deploy` is a directory in the project/worktree root beside `compose.yaml`;
- explains that `deploy/pi/models.json` and `deploy/pi/settings.json` are host files mounted
read-only into `core`, so the operator edits the host files, not files inside the container;
- explains credential-file concepts in plain language rather than presenting environment-variable
names without context;
- shows direct commands such as `tht pi configure`, `tht pi restart`, `tht pi update`,
`tht pi status`, `tht pi doctor`, and `tht pi test`;
- contains no Go build, `~/bin`, `./bin`, `./tht`, `thothctl`, or mandatory descriptor path.
The sanitized-log control must either display the bounded, sanitized `core` log response or fail
with a visible error. The live model selector must reflect the mounted Pi configuration, including
the existing GLM 5.3 change after `core` is recreated.
## Documentation
Active user, installation, architecture, CLI-contract, testing, and README documentation is
rewritten around the bootstrap-plus-setup flow and the direct `tht` command. Historical
`docs/superpowers` plans/specs remain historical records; the new spec supersedes them.
Documentation examples run from the project/worktree root, refer to the home directory as `~`, and
do not teach manual Go builds or manually constructed installation paths for normal use. Advanced
sections may document optional `--installation` and non-interactive flags.
## Safety and acceptance
- Existing user changes to `deploy/pi/models.json` and `deploy/pi/settings.json` are preserved.
- Unrelated dirty-worktree files are not overwritten or committed accidentally.
- JSON contracts remain pristine and secrets are sanitized from stdout, stderr, logs, archives, and
failure messages.
- Tests cover macOS/Linux shell installation, Windows PowerShell installation, root/worktree
discovery, setup idempotency, lifecycle rollback, Pi latest-version lookup, command audit,
backup/restore, frontend layout/copy, and active-document command examples.
- Final acceptance installs `tht` on this Mac, verifies `command -v tht`, confirms `thothctl` is
absent, updates the live stack, checks healthy services, opens port 8080, verifies Pi Management,
and confirms GLM 5.3 is selectable.
+44 -33
View File
@@ -238,25 +238,30 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Set the provider credential",
"Check the provider credential",
"Reload Pi configuration",
"Update the Pi version",
"Recover a failed update",
]);
expect(linux).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
expect(linux).toHaveTextContent("The deploy directory is beside compose.yaml");
expect(linux).toHaveTextContent("All commands below start in this project root");
expect(linux).toHaveTextContent("mkdir -p bin");
expect(linux).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
expect(linux).toHaveTextContent("deploy/pi/models.json");
expect(linux).toHaveTextContent("deploy/pi/settings.json");
expect(linux).toHaveTextContent("baseUrl is the provider API endpoint");
expect(linux).toHaveTextContent("enabledModels uses provider/model identifiers");
expect(linux).toHaveTextContent("PI_AUTH_FILE is a setting in the installation environment file");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(linux).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
expect(linux).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
expect(linux).toHaveTextContent("Pi reads the provider API key from a protected file on the host");
expect(linux).toHaveTextContent("Do not put the key in models.json or settings.json");
expect(linux).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
expect(linux).toHaveTextContent("./bin/thothctl pi update");
expect(linux).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(linux).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance status");
expect(linux).toHaveTextContent("./bin/thothctl pi logs");
expect(linux).toHaveTextContent("./bin/thothctl pi rollback --yes");
expect(linux).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
expect(linux).not.toHaveTextContent("~/bin/");
expect(linux).not.toHaveTextContent("~/.pi/agent/");
await user.click(macosTab);
@@ -266,22 +271,24 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Set the provider credential",
"Check the provider credential",
"Reload Pi configuration",
"Update the Pi version",
"Recover a failed update",
]);
expect(macos).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
expect(macos).toHaveTextContent("The deploy directory is beside compose.yaml");
expect(macos).toHaveTextContent("All commands below start in this project root");
expect(macos).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl");
expect(macos).toHaveTextContent("deploy/pi/models.json");
expect(macos).toHaveTextContent("deploy/pi/settings.json");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(macos).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi logs");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes");
expect(macos).toHaveTextContent("~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes");
expect(macos).toHaveTextContent("./bin/thothctl pi restart --yes --drain");
expect(macos).toHaveTextContent("./bin/thothctl pi update");
expect(macos).toHaveTextContent("./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain");
expect(macos).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance status");
expect(macos).toHaveTextContent("./bin/thothctl pi logs");
expect(macos).toHaveTextContent("./bin/thothctl pi rollback --yes");
expect(macos).toHaveTextContent("./bin/thothctl pi maintenance recover --yes");
await user.click(windowsTab);
const windows = screen.getByRole("tabpanel", { name: "Windows" });
@@ -290,22 +297,26 @@ test("shows a seven-step host-terminal workflow in scrollable platform tabs", as
"Open the project root",
"Edit the provider catalog",
"Enable the model",
"Set the provider credential",
"Check the provider credential",
"Reload Pi configuration",
"Update the Pi version",
"Recover a failed update",
]);
expect(windows).toHaveTextContent("The deploy directory is in the ThothII project root, beside compose.yaml");
expect(windows).toHaveTextContent("The deploy directory is beside compose.yaml");
expect(windows).toHaveTextContent("All commands below start in this project root");
expect(windows).toHaveTextContent("deploy\\pi\\models.json");
expect(windows).toHaveTextContent("deploy\\pi\\settings.json");
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain');
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain');
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
expect(windows).toHaveTextContent("<VERSION> is a placeholder. Replace it with the Pi release/version you want to install.");
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status');
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs');
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes');
expect(windows).toHaveTextContent('& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes');
expect(windows).toHaveTextContent("New-Item -ItemType Directory -Force bin");
expect(windows).toHaveTextContent("go -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl");
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi restart --yes --drain');
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update');
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain');
expect(windows).toHaveTextContent("The command installs the Pi version pinned in docker/core.Dockerfile");
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance status');
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi logs');
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi rollback --yes');
expect(windows).toHaveTextContent('.\\bin\\thothctl.exe pi maintenance recover --yes');
expect(windows).not.toHaveTextContent("~\\bin\\");
});
test("keeps the required PI_AUTH_FILE guidance in one static text node", async () => {
@@ -314,13 +325,13 @@ test("keeps the required PI_AUTH_FILE guidance in one static text node", async (
const tablist = await screen.findByRole("tablist", { name: "Pi host platform" });
await user.click(within(tablist).getByRole("tab", { name: "Linux" }));
const credentialStep = screen.getByRole("heading", { name: "Set the provider credential" }).closest("li");
const credentialStep = screen.getByRole("heading", { name: "Check the provider credential" }).closest("li");
const guidance = credentialStep?.querySelector("p");
expect(
Array.from(guidance?.childNodes ?? []).some(
(node) => node.nodeType === Node.TEXT_NODE
&& node.textContent?.includes("PI_AUTH_FILE is a setting in the installation environment file"),
&& node.textContent?.includes("Pi reads the provider API key from a protected file on the host"),
),
).toBe(true);
});
+22 -17
View File
@@ -108,6 +108,7 @@ type PiPlatformDetails = {
settingsPath: string;
terminal: string;
credentialProtection: string;
buildCommand: string;
restartCommand: string;
updateCommand: string;
pullCommand: string;
@@ -123,10 +124,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
settingsPath: "deploy/pi/settings.json",
terminal: "a terminal",
credentialProtection: "a protected host file with mode 0600",
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
restartCommand: "./bin/thothctl pi restart --yes --drain",
updateCommand: "./bin/thothctl pi update",
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
},
},
{
@@ -137,10 +139,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
settingsPath: "deploy/pi/settings.json",
terminal: "Terminal",
credentialProtection: "a protected host file with mode 0600",
restartCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi restart --yes --drain",
updateCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source build --yes --drain",
pullCommand: "~/bin/thothctl --installation ~/thothii-installation.yaml pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
recoveryCommands: "~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance status\n~/bin/thothctl --installation ~/thothii-installation.yaml pi logs\n~/bin/thothctl --installation ~/thothii-installation.yaml pi rollback --yes\n~/bin/thothctl --installation ~/thothii-installation.yaml pi maintenance recover --yes",
buildCommand: "mkdir -p bin\ngo -C tools/thothctl build -o ../../bin/thothctl ./cmd/thothctl",
restartCommand: "./bin/thothctl pi restart --yes --drain",
updateCommand: "./bin/thothctl pi update",
pullCommand: "./bin/thothctl pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
recoveryCommands: "./bin/thothctl pi maintenance status\n./bin/thothctl pi logs\n./bin/thothctl pi rollback --yes\n./bin/thothctl pi maintenance recover --yes",
},
},
{
@@ -151,10 +154,11 @@ const piPlatforms: Array<{ id: PiPlatform; label: string; details: PiPlatformDet
settingsPath: "deploy\\pi\\settings.json",
terminal: "PowerShell",
credentialProtection: "a protected host file with a user-only ACL",
restartCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi restart --yes --drain',
updateCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source build --yes --drain',
pullCommand: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain',
recoveryCommands: '& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance status\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi logs\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi rollback --yes\n& (Resolve-Path "~\\bin\\thothctl-windows-amd64.exe") --installation (Resolve-Path "~\\thothii-installation.yaml") pi maintenance recover --yes',
buildCommand: "New-Item -ItemType Directory -Force bin | Out-Null\ngo -C tools/thothctl build -o ../../bin/thothctl.exe ./cmd/thothctl",
restartCommand: ".\\bin\\thothctl.exe pi restart --yes --drain",
updateCommand: ".\\bin\\thothctl.exe pi update",
pullCommand: ".\\bin\\thothctl.exe pi update --version <VERSION> --source pull --image <IMAGE>@sha256:<DIGEST> --yes --drain",
recoveryCommands: ".\\bin\\thothctl.exe pi maintenance status\n.\\bin\\thothctl.exe pi logs\n.\\bin\\thothctl.exe pi rollback --yes\n.\\bin\\thothctl.exe pi maintenance recover --yes",
},
},
];
@@ -167,7 +171,9 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
return <ol className="grid gap-4 pl-5 marker:font-semibold marker:text-muted-foreground">
<li>
<h4 className="font-semibold text-foreground">Open the project root</h4>
<p className="mt-1 text-muted-foreground">Open {details.terminal} in the ThothII project root. The deploy directory is in the ThothII project root, beside <code>compose.yaml</code>.</p>
<p className="mt-1 text-muted-foreground">Using {details.terminal}, open the current ThothII checkout or worktree root. All commands below start in this project root. The deploy directory is beside <code>compose.yaml</code>. If this checkout has no <code>bin</code> directory, create it and build the local CLI once:</p>
<PiCodeBlock className="mt-2">{details.buildCommand}</PiCodeBlock>
<p className="mt-2 text-muted-foreground">The commands below use the executable built in this checkout, so they cannot accidentally target another worktree.</p>
</li>
<li>
<h4 className="font-semibold text-foreground">Edit the provider catalog</h4>
@@ -188,8 +194,8 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
</dl>
</li>
<li>
<h4 className="font-semibold text-foreground">Set the provider credential</h4>
<p className="mt-1 text-muted-foreground">PI_AUTH_FILE is a setting in the installation environment file. It selects {details.credentialProtection}; Docker mounts the selected host credential file read-only for Pi.</p>
<h4 className="font-semibold text-foreground">Check the provider credential</h4>
<p className="mt-1 text-muted-foreground">Pi reads the provider API key from a protected file on the host. <code>PI_AUTH_FILE</code> tells this installation which file to use; Docker mounts it read-only into the core container. Do not put the key in <code>models.json</code> or <code>settings.json</code>. Keep {details.credentialProtection}.</p>
</li>
<li>
<h4 className="font-semibold text-foreground">Reload Pi configuration</h4>
@@ -198,8 +204,7 @@ function PiInstructionSteps({ details }: { details: PiPlatformDetails }) {
</li>
<li>
<h4 className="font-semibold text-foreground">Update the Pi version</h4>
<p className="mt-1 text-muted-foreground">Use a build update only when changing the bundled Pi version.</p>
<p className="mt-1 text-muted-foreground">{"<VERSION> is a placeholder. Replace it with the Pi release/version you want to install."}</p>
<p className="mt-1 text-muted-foreground">The command installs the Pi version pinned in <code>docker/core.Dockerfile</code>. Use <code>--version &lt;VERSION&gt;</code> only when you deliberately want another version.</p>
<PiCodeBlock className="mt-2">{details.updateCommand}</PiCodeBlock>
<p className="mt-2 text-[11px] text-muted-foreground">Advanced: pull an immutable, digest-pinned image.</p>
<PiCodeBlock className="mt-1 text-[11px] text-muted-foreground">{details.pullCommand}</PiCodeBlock>
+59 -2
View File
@@ -114,7 +114,13 @@ test("level one explains the read-only Git sequence and the repository update bu
const overview = screen.getByTestId("workspace-overview");
expect(within(overview).getByText(/Git server such as GitHub, GitLab, or Gitea/i)).toBeVisible();
expect(within(overview).getByText(/configured during ThothII installation/i)).toBeVisible();
expect(within(overview).getAllByRole("listitem")[0]).toHaveTextContent(/choosing any local directory you prefer/i);
const repositoryStep = within(overview).getAllByRole("listitem")[0];
expect(repositoryStep).toHaveTextContent(/create a workspace repository/i);
expect(repositoryStep).toHaveTextContent(/one directory for each workspace/i);
expect(repositoryStep).toHaveTextContent(/thoth-workspaces\.yaml/i);
expect(repositoryStep).toHaveTextContent(/database connection/i);
expect(repositoryStep).toHaveTextContent(/Evidence sources/i);
expect(repositoryStep).toHaveTextContent(/vector-database collection/i);
expect(within(overview).getByRole("link", { name: /workspace authoring instructions on GitHub/i })).toHaveAttribute(
"href",
"https://github.com/mptyl/ThothII/blob/main/docs/install/local-workspace-registry.md#prepare-and-publish-a-workspace-source",
@@ -126,7 +132,7 @@ test("level one explains the read-only Git sequence and the repository update bu
await user.click(screen.getByRole("button", { name: "Update workspace repository" }));
expect(await screen.findByText("Workspace repository updated and validated.")).toBeVisible();
expect(screen.getByText(/create a local workspace/i)).toBeInTheDocument();
expect(screen.getByText(/create a workspace repository/i)).toBeInTheDocument();
expect(screen.queryByText(/import|export|bundle/i)).not.toBeInTheDocument();
});
@@ -141,10 +147,61 @@ test("workspace-specific commands remain isolated until a workspace is selected"
expect(screen.getByText(/reads this revision without modifying or publishing it/i)).toBeVisible();
expect(screen.getByText(/checks workspace.yaml and the required workspace directories/i)).toBeVisible();
expect(screen.getByText(/temporary decrypted credentials/i)).toBeVisible();
const databaseField = screen.getByText("Database").parentElement;
expect(databaseField).not.toBeNull();
expect(databaseField).toHaveTextContent("engine: postgres");
expect(databaseField).toHaveTextContent("database: database");
expect(databaseField).toHaveTextContent("schema: public");
expect(screen.getByRole("button", { name: "Validate workspace source" })).toBeVisible();
expect(screen.getByRole("button", { name: "Test workspace connections" })).toBeVisible();
});
test("keeps validation and connection results inside their respective action cards", async () => {
const user = userEvent.setup();
server.use(
http.post("/api/workspaces/validate", () => HttpResponse.json({ workspace, contract: {} })),
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
activatable: false,
diagnostics: [{ level: "error", code: "connector_unavailable", message: "Connector diagnostic failed." }],
})),
);
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const validationCard = screen.getByTestId("workspace-validation-card");
const connectionCard = screen.getByTestId("workspace-connection-card");
await user.click(within(validationCard).getByRole("button", { name: "Validate workspace source" }));
const validationStatus = await within(validationCard).findByRole("status");
expect(validationStatus).toHaveTextContent("Workspace source is valid.");
expect(validationStatus).toHaveClass("text-emerald-700");
expect(within(connectionCard).queryByText("Workspace source is valid.")).not.toBeInTheDocument();
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
expect(await within(connectionCard).findByRole("alert")).toHaveTextContent(
"connector_unavailable: Connector diagnostic failed.",
);
expect(within(validationCard).queryByText("connector_unavailable: Connector diagnostic failed.")).not.toBeInTheDocument();
});
test("renders binding_ok as a green connection success", async () => {
const user = userEvent.setup();
server.use(
http.post("/api/workspaces/psd-clinical/test", () => HttpResponse.json({
activatable: true,
diagnostics: [{ level: "info", code: "binding_ok", message: "Installation bindings and diagnostics succeeded." }],
})),
);
renderManager();
await user.click(await screen.findByRole("button", { name: "PSD Clinical" }));
const connectionCard = screen.getByTestId("workspace-connection-card");
await user.click(within(connectionCard).getByRole("button", { name: "Test workspace connections" }));
const connectionStatus = await within(connectionCard).findByRole("status");
expect(connectionStatus).toHaveTextContent("binding_ok: Installation bindings and diagnostics succeeded.");
expect(connectionStatus).toHaveClass("text-emerald-700");
expect(within(connectionCard).queryByRole("alert")).not.toBeInTheDocument();
});
test("secret fields are write-only, clear after blind save, and may be forgotten", async () => {
const user = userEvent.setup();
let savedBody: unknown;
+69 -13
View File
@@ -66,6 +66,10 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
const [secretValues, setSecretValues] = useState<Record<string, string>>({});
const [notice, setNotice] = useState<string>();
const [diagnostics, setDiagnostics] = useState<string[]>([]);
const [validationNotice, setValidationNotice] = useState<string>();
const [validationDiagnostics, setValidationDiagnostics] = useState<string[]>([]);
const [connectionNotice, setConnectionNotice] = useState<string>();
const [connectionDiagnostics, setConnectionDiagnostics] = useState<string[]>([]);
const [busyAction, setBusyAction] = useState<string>();
const statusQuery = useQuery({
@@ -97,6 +101,15 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
const clearMessages = () => {
setNotice(undefined);
setDiagnostics([]);
setValidationNotice(undefined);
setValidationDiagnostics([]);
setConnectionNotice(undefined);
setConnectionDiagnostics([]);
};
const clearGlobalMessages = () => {
setNotice(undefined);
setDiagnostics([]);
};
const close = () => {
@@ -139,12 +152,14 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
async function validateSource() {
if (!detailQuery.data) return;
setBusyAction("validate");
clearMessages();
clearGlobalMessages();
setValidationNotice(undefined);
setValidationDiagnostics([]);
try {
await validateWorkspace(detailQuery.data.workspace);
setNotice("Workspace source is valid.");
setValidationNotice("Workspace source is valid.");
} catch (error) {
setDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
setValidationDiagnostics([publicError(error, "workspace_invalid: Workspace validation could not be completed")]);
} finally {
setBusyAction(undefined);
}
@@ -153,17 +168,23 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
async function testConnections() {
if (!selectedId) return;
setBusyAction("test");
clearMessages();
clearGlobalMessages();
setConnectionNotice(undefined);
setConnectionDiagnostics([]);
try {
const result = await testWorkspace(selectedId);
setDiagnostics(result.diagnostics.map(({ code, message }) => `${code}: ${message}`));
if (result.diagnostics.length === 0) {
setNotice(result.activatable
? "Workspace connections are valid."
const issues = result.diagnostics.filter(({ level }) => level !== "info");
const informational = result.diagnostics.find(({ level }) => level === "info");
setConnectionDiagnostics(issues.map(({ code, message }) => `${code}: ${message}`));
if (issues.length === 0) {
setConnectionNotice(result.activatable
? informational
? `${informational.code}: ${informational.message}`
: "Workspace connections are valid."
: "Workspace connection test completed.");
}
} catch (error) {
setDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
setConnectionDiagnostics([publicError(error, "connector_unavailable: Workspace connections could not be tested")]);
} finally {
setBusyAction(undefined);
}
@@ -293,7 +314,7 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
<h2 className="font-heading text-xl font-semibold">How workspaces reach ThothII</h2>
</div>
<ol className="grid list-decimal gap-3 pl-5 text-sm leading-6 text-muted-foreground">
<li><span className="font-medium text-foreground">Create a local workspace</span> by choosing any local directory you prefer and setting up the workspace there. It must contain <code>workspace.yaml</code> and every required subdirectory, including any versioned Evidence files.</li>
<li><span className="font-medium text-foreground">Create a workspace repository</span> in any local directory you choose. Add one directory for each workspace you want ThothII to manage. At the repository root, <code>thoth-workspaces.yaml</code> lists those workspaces; each workspace directory contains its own <code>workspace.yaml</code>, which declares the database connection, the Evidence sources, and the ThothII vector-database collection used during the process.</li>
<li>Publish that source by committing and pushing it to a repository hosted by a Git server such as GitHub, GitLab, or Gitea.</li>
<li>The repository address, branch, and read-only Git credentials are configured during ThothII installation. This installation reads <span className="font-medium text-foreground">{repositoryLabel}</span> on branch <span className="font-mono text-foreground">{statusQuery.data?.branch ?? "main"}</span>.</li>
<li>ThothII fetches the configured branch into its managed read-only checkout, validates the complete candidate revision, and activates it only when validation succeeds. It never edits, commits, pushes, or publishes workspace source.</li>
@@ -337,21 +358,56 @@ export function WorkspaceManager({ open, onClose }: { open: boolean; onClose: ()
<dl className="grid gap-3 rounded-lg border border-border bg-muted/20 p-4 text-sm sm:grid-cols-2">
<div><dt className="text-xs text-muted-foreground">Source file</dt><dd className="font-mono">{selectedSummary.file}</dd></div>
<div><dt className="text-xs text-muted-foreground">Active revision</dt><dd className="truncate font-mono">{detailQuery.data.revision.commit}</dd></div>
<div><dt className="text-xs text-muted-foreground">Data warehouse</dt><dd>{detailQuery.data.workspace.dwh.engine} · {detailQuery.data.workspace.dwh.database}/{detailQuery.data.workspace.dwh.schema}</dd></div>
<div>
<dt className="text-xs text-muted-foreground">Database</dt>
<dd className="grid gap-0.5 font-mono text-xs">
<span>engine: {detailQuery.data.workspace.dwh.engine}</span>
<span>database: {detailQuery.data.workspace.dwh.database}</span>
<span>schema: {detailQuery.data.workspace.dwh.schema}</span>
</dd>
</div>
<div><dt className="text-xs text-muted-foreground">Runtime status</dt><dd>{stateLabel(runtime.configurationState)}</dd></div>
</dl>
<div className="grid gap-3 lg:grid-cols-2">
<div className="rounded-lg border border-border p-4">
<div data-testid="workspace-validation-card" className="rounded-lg border border-border p-4">
<h4 className="font-heading font-semibold">Validate workspace source</h4>
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Checks workspace.yaml and the required workspace directories against the supported workspace schema. No source file is changed.</p>
{validationNotice && (
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{validationNotice}
</p>
)}
{validationDiagnostics.length > 0 && (
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
{validationDiagnostics.map((diagnostic) => (
<p key={diagnostic} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
</p>
))}
</div>
)}
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "validate"} onClick={() => { void validateSource(); }}>
<ClipboardCheck />Validate workspace source
</Button>
</div>
<div className="rounded-lg border border-border p-4">
<div data-testid="workspace-connection-card" className="rounded-lg border border-border p-4">
<h4 className="font-heading font-semibold">Test workspace connections</h4>
<p className="mt-1 min-h-12 text-sm leading-5 text-muted-foreground">Uses temporary decrypted credentials to verify the configured data warehouse and Evidence source. Temporary files are deleted after the test.</p>
{connectionNotice && (
<p role="status" className="mt-3 rounded-md border border-emerald-500/30 bg-emerald-500/10 px-3 py-2 text-sm text-emerald-700">
<CheckCircle2 className="mr-1 inline size-4 text-emerald-600" />{connectionNotice}
</p>
)}
{connectionDiagnostics.length > 0 && (
<div role="alert" aria-live="polite" className="mt-3 grid gap-1 rounded-md border border-amber-500/30 bg-amber-500/10 px-3 py-2 text-sm">
{connectionDiagnostics.map((diagnostic) => (
<p key={diagnostic} className="flex items-start gap-2">
<AlertCircle className="mt-0.5 size-4 shrink-0 text-amber-700" />{diagnostic}
</p>
))}
</div>
)}
<Button className="mt-3" size="sm" variant="outline" disabled={busyAction === "test"} onClick={() => { void testConnections(); }}>
<FlaskConical />Test workspace connections
</Button>
+6
View File
@@ -122,6 +122,12 @@ func extractSecretValues(contents []byte) ([]string, error) {
if whole != "" {
values = append(values, whole)
}
// PEM files (including OpenSSH private keys) can contain base64 lines that look
// like dotenv assignments. Keep the complete document opaque instead of trying
// to parse it as a dotenv bundle.
if bytes.HasPrefix(trimmed, []byte("-----BEGIN ")) {
return values, nil
}
if trimmed[0] == '{' || trimmed[0] == '[' {
var document any
@@ -105,6 +105,26 @@ func TestSecretValuesFromFilesRedactsEveryDotenvBundleValue(t *testing.T) {
}
}
func TestSecretValuesFromFilesAcceptsOpenSSHPrivateKey(t *testing.T) {
t.Parallel()
secretFile := filepath.Join(physicalTempDir(t), "git-ssh-key")
contents := "-----BEGIN OPENSSH PRIVATE KEY-----\n" +
"ZmFrZS1rZXktcGF5bG9hZA==\n" +
"-----END OPENSSH PRIVATE KEY-----\n"
if err := os.WriteFile(secretFile, []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
secrets, err := SecretValuesFromFiles([]string{secretFile})
if err != nil {
t.Fatalf("SecretValuesFromFiles() error = %v, want OpenSSH key accepted", err)
}
if len(secrets) == 0 {
t.Fatal("SecretValuesFromFiles() returned no values for OpenSSH key")
}
}
func TestSanitizeRecognizesQuotedCredentialKeys(t *testing.T) {
t.Parallel()
+46
View File
@@ -0,0 +1,46 @@
package pi
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"regexp"
"strings"
)
var defaultPiArgPattern = regexp.MustCompile(`^ARG[[:space:]]+PI_VERSION[[:space:]]*=(.*)$`)
// ReadPinnedVersion reads the repository's declared Pi runtime version. It deliberately reads
// only the default ARG, not the later ARG PI_VERSION declarations used by build stages.
func ReadPinnedVersion(projectDirectory string) (string, error) {
path := filepath.Join(projectDirectory, "docker", "core.Dockerfile")
file, err := os.Open(path)
if err != nil {
return "", fmt.Errorf("read Pi version pin: %w", err)
}
defer file.Close()
var version string
count := 0
scanner := bufio.NewScanner(file)
for scanner.Scan() {
match := defaultPiArgPattern.FindStringSubmatch(strings.TrimSuffix(scanner.Text(), "\r"))
if len(match) != 2 {
continue
}
count++
version = strings.TrimSpace(match[1])
}
if err := scanner.Err(); err != nil {
return "", fmt.Errorf("read Pi version pin: %w", err)
}
if count != 1 {
return "", errors.New("docker/core.Dockerfile must contain exactly one default PI_VERSION")
}
if _, err := parseSemanticVersion(version); err != nil {
return "", errors.New("docker/core.Dockerfile contains an invalid Pi version")
}
return version, nil
}
+52
View File
@@ -0,0 +1,52 @@
package pi
import (
"os"
"path/filepath"
"strings"
"testing"
)
func TestReadPinnedVersionReadsTheSingleDefaultPiArg(t *testing.T) {
root := t.TempDir()
writeCoreDockerfile(t, root, "ARG PI_VERSION=0.81.0\nARG PI_VERSION\n")
got, err := ReadPinnedVersion(root)
if err != nil {
t.Fatal(err)
}
if got != "0.81.0" {
t.Fatalf("ReadPinnedVersion() = %q, want 0.81.0", got)
}
}
func TestReadPinnedVersionRejectsMissingDuplicateAndMalformedPins(t *testing.T) {
for _, test := range []struct {
name string
file string
want string
}{
{name: "missing", file: "# no default\n", want: "one default PI_VERSION"},
{name: "duplicate", file: "ARG PI_VERSION=0.80.3\nARG PI_VERSION=0.81.0\n", want: "one default PI_VERSION"},
{name: "malformed", file: "ARG PI_VERSION=latest\n", want: "invalid Pi version"},
} {
t.Run(test.name, func(t *testing.T) {
root := t.TempDir()
writeCoreDockerfile(t, root, test.file)
_, err := ReadPinnedVersion(root)
if err == nil || !strings.Contains(err.Error(), test.want) {
t.Fatalf("ReadPinnedVersion() error = %v, want %q", err, test.want)
}
})
}
}
func writeCoreDockerfile(t *testing.T, root, contents string) {
t.Helper()
if err := os.MkdirAll(filepath.Join(root, "docker"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(root, "docker", "core.Dockerfile"), []byte(contents), 0o600); err != nil {
t.Fatal(err)
}
}