Konfiguration in drei Stufen: eingecheckt (Instanz), lokal (Checkout), Secrets außerhalb des Baums - mit 1Password-CLI #207

Closed
opened 2026-10-09 21:40:01 +00:00 by torben · 3 comments
Owner

Angelegt am 2026-10-09 in der Design-Sitzung zu #87/#200 (Stand 9.0.0, 7cfafd7). Entscheidungen des Betreibers vom 2026-10-10 sind eingearbeitet (§ Entscheidungen). Dieses Paket baut den Mechanismus nur für neue Einstellungen. Die bestehenden Dateien zieht #208 um, und erst dort verschwinden die Altlasten.

Abgeschlossen (2026-10-10). Publiziert als 9.1.0-beta.1, Commit f6aaadc (nach Freigabe des Mass-Update Gate, 40 Dateien). Verifiziert durch CI-Run 644 (https://gitea.nehmer.net/torben/chemenu/actions/runs/644): verify grün (2762 passed, Coverage 91,7 %; die 1Password-Action hat WIKITOOL_SECRET_CI_SMOKE geladen, der Smoke-Test lief mit 1 passed, nicht übersprungen), pwsh grün; Release-Run 645 grün. Schlussphase: Doku-Nachzug e085a8b (nur docs/, außerhalb des Version-Gates, kein Bump): Das How-to nennt jetzt auch den Pfad der Secret-Datei unter Windows und mit $XDG_CONFIG_HOME; die Erklärseite sagt, dass die Freigabe-Abfrage von 1Password je Terminal-Sitzung gilt und nicht je Wert. Verifiziert durch CI-Run 646 (https://gitea.nehmer.net/torben/chemenu/actions/runs/646), verify und pwsh grün, Smoke-Test erneut 1 passed. Die übrigen neuen und berührten Dokumente (AGENTS.md § Settings, docs/reference/configuration.md § Tiered settings, tools/CONTRACT.md mit den Records zu config set, doctor und docs verify, INSTALL.md, instructions/preflight.md, tools/README.md) habe ich gegen den Code gelesen. Sie stimmen. Die CI-Boilerplate des Betreibers: torben/gitea-mcp, Branch ci-binford, .gitea/workflows/binford-release.yaml. Abweichungen vom Entwurf, die der Bau ergeben hat, stehen jeweils am Punkt selbst und sind mit Bau: markiert.

Befund

  • Bis zu diesem Paket lag jede Einstellung von wikitool in einer eigenen, gitignorierten Datei pro Feature: .wikitool-remotes.json, .wikitool-telemetry.json, .wikitool-upload.json, .wikitool-tasks.json (dazu .wikitool-tasks.d/) und .wikitool-tools.json. Eingecheckt ist nur .wikitool-kb.json, und die beschreibt die Form des Korpus, ist also keine Einstellung.
  • Jede neue Datei kostet etwa fünf Stellen: Konstante in tools/chemenu/config.py, Absatz in .gitignore, Ignore-Kanarie in docs verify, Zeile in doctor, Doku.
  • Die Formprüfung ist schon zentral (tools/chemenu/config_shapes.py: Shape/Field/check, benutzt von upload.py, telemetry/policy.py, git_publish.py, tasks/*.py). Jeder Leser liest seine Datei aber selbst.
  • Drei Arten von Daten liegen dort gemischt:
    • was zur Instanz gehört (Schwellen der Aufgaben-Schicht);
    • was zu einem Checkout gehört (Werkzeugpfade, Remotes-Allowlist, Telemetrie-Zustimmung);
    • Secrets (api_token in superproductivity, app_password in caldav), im Klartext im Arbeitsbaum und nur durch .gitignore geschützt.

Die Altdateien bleiben bis #208 bestehen. Dieses Paket stellt nur das Ziel bereit.

Ziel

Stufe Inhalt Ablage
instance, eingecheckt Was zum Korpus gehört und in jedem Klon gleich sein soll wikitool.json im Instanz-Root
checkout, lokal Was nur auf diesem Rechner oder in diesem Klon stimmt .wikitool-local.json, gitignored
secret Tokens, Passwörter, Sessions außerhalb des Arbeitsbaums. Die beiden Dateien verweisen nur darauf

Entscheidungen des Betreibers (2026-10-10)

  1. Eingecheckte Datei wikitool.json, lokale Datei .wikitool-local.json.
  2. Secrets über eine Umgebungsvariable oder eine Datei im Nutzerverzeichnis (Entwurf 4), ohne OS-Keyring.
  3. Geschrieben wird über wikitool config set (Entwurf 7).
  4. Zusätzlich die 1Password-CLI (op) als Secret-Quelle (Entwurf 5): für Desktop-Installationen die bevorzugte Variante, in der CI wie bei anderen Action-Runnern. op kann auch Nicht-Secrets liefern (URLs, Connection-Strings), was die Konfiguration über Rechner hinweg portabel macht.
  5. Freigabe des Publish mit 40 Dateien über das Mass-Update Gate (Token 1da42d56b1be), samt der Bau-Entscheidungen unten (CI-Element, gate-Felder nur durch den Nutzer, WARN bei gesperrtem 1Password).

Entwurf (gebaut)

  1. Jedes Feld gehört genau einer Stufe, deklariert im Field von config_shapes.py (tier="instance" | "checkout" | "secret"). Ein Wert in der falschen Stufe ist ein Fehler, keine Überschreibung; derselbe Schlüssel in beiden Dateien ebenso. Die Abschnitte stehen in config_shapes.SETTINGS_SECTIONS. Nach diesem Paket ist das Tupel leer, Abschnitte bringen #208 und die Verbraucher #200/#169. Bau: Ein Abschnitt ist flach - keine verschachtelten Felder, keine Varianten -, damit die Stufe am Feld allein ablesbar ist.
  2. Harte Regel: Was ein Programm zum Ausführen benennt (names_command) oder ein Gate betrifft (gate), ist nie instance. Field.__post_init__ wirft beim Deklarieren. Auch als Verweis steht so ein Feld nie in wikitool.json. Die Regel steht für Agenten genau einmal: AGENTS.md § Settings.
  3. Verweise statt Werte. Ein secret-Feld nimmt nur einen Verweis, ein private-Feld (private Infrastruktur) darf einen nehmen, alle anderen keinen. Zwei Formen: {"ref": "<name>"} (Kleinbuchstaben, Ziffern, einzelne _) und {"ref": "op://<vault>/<item>/<field>"}. Bau: Ein private-Feld ist immer Stufe checkout (beim Deklarieren erzwungen): Klartext nur in .wikitool-local.json, als Verweis in beiden Dateien.
  4. Quelle Name: zuerst WIKITOOL_SECRET_<NAME>, sonst ~/.config/chemenu/<instanz-id>/secrets.json (Windows: %APPDATA%\chemenu\<instanz-id>\secrets.json). Die <instanz-id> (UUID) erzeugt das erste config set in wikitool.json. Bau: $XDG_CONFIG_HOME wird statt ~/.config beachtet, wenn gesetzt. "Nur für den Nutzer lesbar" wird erzwungen: außerhalb Windows wird eine Datei mit Rechten für Gruppe oder andere abgelehnt (exit 1, Hinweis chmod 600).
  5. Quelle 1Password: op read --no-newline <ref> ohne Shell, stdin /dev/null, Timeout 60 s, Wert nur im Speicher. Fehlt op → exit 1 mit Preflight-Hinweis. Element/Feld/Tresor fehlt (op-Meldung "isn't an item/a vault/a field", "does not have a field" …) → exit 1 mit dem Verweis. Jeder andere Fehler und ein Timeout gelten als gesperrt/nicht angemeldet → exit 42, ohne den Verweis. Bau: op steht als neue Zeilenart optional|op|… in tools/prerequisites.txt; beide Preflights erfassen es, wenn vorhanden (ABSENT sonst, nie ein Problem), --set op= geht. Geprüft: Der Launcher liest aus .wikitool-tools.json nur "complete": true, toolpaths nur benannte Schlüssel - ein zusätzlicher Schlüssel stört keine ältere Version. Neu: toolpaths.optional().
  6. CI. Bau, nach der Boilerplate des Betreibers: Der Weg ist die Action. 1password/load-secrets-action@v2 lädt op://CI-CD/gitea-package-token/username (das am wenigsten sensible Feld, das der Service-Account sieht) nach WIKITOOL_SECRET_CI_SMOKE; der Test test_live_secret_reference_resolves_from_ci löst {"ref": "ci_smoke"} darüber auf und läuft mit CHEMENU_LIVE_SECRET_REQUIRE=1, damit "kein Wert" rot statt übersprungen ist. Dazu curl unzip in den Systemabhängigkeiten, wie in der Boilerplate. In Run 644 und 646 bestätigt. Einschränkung, die der Bau gezeigt hat: Der Action-Weg bedient Namensverweise. Eine Konfiguration mit op://-Verweisen braucht in der CI den anderen Weg (op im Job plus OP_SERVICE_ACCOUNT_TOKEN). Der Code kann das, diese CI testet es nicht. "Dieselbe Konfiguration auf beiden Wegen" gilt also nur je Verweisform, und so steht es im How-to. Nicht blockierend: Braucht ein Verbraucher (#200, #169) op:// in der CI, kommt der zweite Weg dort dazu.
  7. wikitool config set <schlüssel> <wert|--ref …> prüft Stufe und Form. Klartext instance → wikitool.json, Klartext checkout/private → .wikitool-local.json, Verweis → wikitool.json; derselbe Schlüssel in der anderen Datei wird entfernt und gemeldet. Ein Secret-Wert wird abgelehnt (exit 1, Wert nicht ausgegeben). Bau: "Nur der Nutzer setzt" gilt für names_command und gate - wie bei der Allowlist des Publish-Remote Gate; config set endet dort mit exit 42 und nennt die Datei zum Selbst-Editieren. Vor dem Schreiben werden beide Dateien geprüft, aber ohne Pflichtfelder, sonst ließe sich der erste Wert eines Abschnitts nie schreiben (check_files(..., complete=False)).
  8. Leseschicht tools/chemenu/settings.py: liest beide Dateien, prüft Platzierung und Form, löst Verweise erst beim Zugriff auf, gibt einem Leser seinen Abschnitt. Bau: Für Kommandos gibt es _util.load_settings() und _util.setting_value() (exit 1 bzw. 42) - darüber testen die Kriterien die Exit-Codes, darüber lesen #200/#169. doctor: Zeilen settings-instance, settings-checkout, settings-secrets; ein nicht auflösbarer Verweis ist FAIL, ein gesperrtes 1Password WARN (sonst wäre doctor bei gesperrter App rot), ein unbekannter Abschnitt WARN.
  9. Erste Verbraucher: #200 (Site-Handler) und #169 (Abruf mit Session). Hier wird mit einem Test-Shape getestet.

Sicherheitsmodell (ehrlich)

Secrets außerhalb des Baums schützen vor dem Einchecken. Sie schützen auch davor, dass ein Wert in einer Datei steht, die der Agent bei normaler Arbeit liest, oder in einer Ausgabe von wikitool auftaucht. Sie schützen nicht vor einem Agenten mit Shell-Zugriff, der es darauf anlegt.

  • Ist 1Password im Terminal entsperrt, kann der Agent op read selbst aufrufen.
  • Die Secret-Datei kann er ebenfalls lesen.
  • Was der Prozess von wikitool zur Laufzeit lesen kann, kann der Agent im selben Benutzerkontext grundsätzlich auch.

Das begrenzt man, ohne es zu verhindern:

  • ein eigener 1Password-Tresor nur für die Einträge dieser Instanz;
  • die Freigabe-Abfrage der 1Password-Desktop-App. Sie gilt je Terminal-Sitzung, nicht je Wert, hält also nur eine Sitzung auf, die noch niemand freigegeben hat;
  • in der CI ein Service-Account, der nur diesen Tresor sieht.

Die Doku sagt das so (docs/explanation/configuration-tiers.md), und der Stack verspricht nichts darüber hinaus.

Betroffene Dateien (gebaut)

  • tools/chemenu/config_shapes.py (tier, private, names_command, gate, SETTINGS_SECTIONS), tools/chemenu/config.py (Dateinamen, ENV_SECRET_PREFIX, secrets_file()), tools/chemenu/toolpaths.py (optional())
  • neu: tools/chemenu/settings.py; tools/chemenu/commands/config_cmd.py mit Record, Gruppe "Configuration" in cli_contract.GROUPS; _util.load_settings/setting_value
  • tools/chemenu/commands/doctor.py, tools/chemenu/commands/docs_verify.py (Kanarien .wikitool-local.json ignoriert / wikitool.json getrackt; check_instance_settings: nur Platzierung in wikitool.json, keine Pflichtfelder, die im lokalen File stehen dürfen)
  • tools/prerequisites.txt, tools/chemenu/prerequisites.py, tools/preflight.sh, tools/preflight.ps1, instructions/preflight.md, INSTALL.md
  • .gitignore, tools/chemenu/tests/conftest.py (WIKITOOL_SECRET_* per Präfix gelöscht)
  • dist_cmd.py unverändert: wikitool.json steht nicht in ROOT_FILES, wird also nicht ausgeliefert und von dist upgrade nie berührt (Test test_plan_ships_neither_settings_file)
  • .gitea/workflows/ci.yml
  • AGENTS.md § Settings (neu) und § File naming (sieben statt sechs Erklärseiten), instructions/dev/doc-pull-through.md, instructions/dev/testing-conventions.md, tools/README.md
  • docs/explanation/configuration-tiers.md, docs/how-to/use-1password-for-settings.md (beide in der Schlussphase nachgezogen, e085a8b), docs/reference/configuration.md § Tiered settings, docs/reference/commands/configuration.md (generiert)
  • Tests: test_settings.py (neu), test_preflight.py, test_preflight_pwsh.py, test_dist_cmd.py

Akzeptanzkriterien

  • Ein Field mit Stufe instance, das einen Befehl benennt, lässt sich nicht deklarieren (Test). - test_a_field_that_names_a_program_or_a_gate_cannot_be_declared_instance
  • Ein Wert in der falschen Stufe ergibt beim Lesen exit 1 mit Datei, Schlüssel und richtiger Stufe. - test_a_value_in_the_wrong_tier_fails_naming_file_key_and_tier
  • Ein Klartext-Wert in einem secret-Feld (beide Dateien) oder in einem private-Feld in wikitool.json ergibt exit 1. Der Wert erscheint in keiner Ausgabe. - test_a_plain_secret_or_committed_private_value_fails_without_showing_it (Kommando, doctor), test_docs_verify_flags_a_plain_secret_in_the_committed_file_only
  • {"ref": "<name>"} löst über WIKITOOL_SECRET_<NAME> auf. Ohne die Variable löst es über die Secret-Datei im Nutzerverzeichnis auf, mit dem Test-Home.
  • {"ref": "op://…"} löst über das falsche op auf. Ein gesperrtes op (Testskript mit entsprechendem Exit) ergibt exit 42, ein fehlendes op exit 1 mit dem Preflight-Hinweis. Dazu: Timeout → exit 42, fehlendes Element → exit 1 mit Verweis.
  • Ohne wikitool.json und .wikitool-local.json verhält sich jedes bestehende Kommando byte-identisch zum Stand davor. Bau: Ausnahme ist doctor mit seinen drei neuen Zeilen (nächstes Kriterium). Geprüft gegen einen Worktree von HEAD: lint, search, version show, docs verify, instructions verify, types list, sources coverage byte-identisch.
  • doctor zeigt für jede Stufe vorhanden/fehlt/fehlerhaft und für Verweise nur auflösbar/nicht auflösbar.
  • config set setzt einen instance-Wert in wikitool.json und verweigert einen Secret-Wert im Klartext.
  • pytest, docs verify, instructions verify grün; CI grün, mit einem echten 1Password-Wert in der CI. Die Action löst op://CI-CD/gitea-package-token/username auf, die Einstellung verweist per Namen darauf (Entwurf 6). CI-Run 644 grün, Smoke-Test 1 passed; nach dem Doku-Nachzug erneut in Run 646. Ein op://-Verweis in den Einstellungen selbst ist in der CI nicht getestet, nur mit dem falschen op im Test (voriges Kriterium). Nicht blockierend, siehe Entwurf 6.

Versionsteil

--minor - gebaut als 9.0.1-beta.2 → 9.1.0-beta.1. Neue, optionale Dateien und ein neues Kommando, die eine ältere Version ignoriert. Ohne sie ändert sich kein Verhalten außer drei OK-Zeilen in doctor. Neue Umgebungsvariablen sind additiv, der optionale Eintrag in .wikitool-tools.json ebenfalls (geprüft, Entwurf 5). Drop-in in beide Richtungen. Der Bruch kommt erst mit #208.

Kette

Kette „Beschaffung und Konfiguration“

> Angelegt am 2026-10-09 in der Design-Sitzung zu #87/#200 (Stand `9.0.0`, `7cfafd7`). Entscheidungen des Betreibers vom 2026-10-10 sind eingearbeitet (§ Entscheidungen). Dieses Paket baut den Mechanismus **nur für neue Einstellungen**. Die bestehenden Dateien zieht #208 um, und erst dort verschwinden die Altlasten. > > **Abgeschlossen (2026-10-10).** Publiziert als `9.1.0-beta.1`, Commit `f6aaadc` (nach Freigabe des Mass-Update Gate, 40 Dateien). **Verifiziert durch CI-Run 644** (https://gitea.nehmer.net/torben/chemenu/actions/runs/644): `verify` grün (2762 passed, Coverage 91,7 %; die 1Password-Action hat `WIKITOOL_SECRET_CI_SMOKE` geladen, der Smoke-Test lief mit `1 passed`, nicht übersprungen), `pwsh` grün; Release-Run 645 grün. **Schlussphase:** Doku-Nachzug `e085a8b` (nur `docs/`, außerhalb des Version-Gates, kein Bump): Das How-to nennt jetzt auch den Pfad der Secret-Datei unter Windows und mit `$XDG_CONFIG_HOME`; die Erklärseite sagt, dass die Freigabe-Abfrage von 1Password je Terminal-Sitzung gilt und nicht je Wert. Verifiziert durch CI-Run 646 (https://gitea.nehmer.net/torben/chemenu/actions/runs/646), `verify` und `pwsh` grün, Smoke-Test erneut `1 passed`. Die übrigen neuen und berührten Dokumente (`AGENTS.md` § Settings, `docs/reference/configuration.md` § Tiered settings, `tools/CONTRACT.md` mit den Records zu `config set`, `doctor` und `docs verify`, `INSTALL.md`, `instructions/preflight.md`, `tools/README.md`) habe ich gegen den Code gelesen. Sie stimmen. Die CI-Boilerplate des Betreibers: `torben/gitea-mcp`, Branch `ci-binford`, `.gitea/workflows/binford-release.yaml`. Abweichungen vom Entwurf, die der Bau ergeben hat, stehen jeweils am Punkt selbst und sind mit **Bau:** markiert. ## Befund - Bis zu diesem Paket lag jede Einstellung von `wikitool` in einer eigenen, gitignorierten Datei pro Feature: `.wikitool-remotes.json`, `.wikitool-telemetry.json`, `.wikitool-upload.json`, `.wikitool-tasks.json` (dazu `.wikitool-tasks.d/`) und `.wikitool-tools.json`. Eingecheckt ist nur `.wikitool-kb.json`, und die beschreibt die Form des Korpus, ist also keine Einstellung. - Jede neue Datei kostet etwa fünf Stellen: Konstante in `tools/chemenu/config.py`, Absatz in `.gitignore`, Ignore-Kanarie in `docs verify`, Zeile in `doctor`, Doku. - Die Formprüfung ist schon zentral (`tools/chemenu/config_shapes.py`: `Shape`/`Field`/`check`, benutzt von `upload.py`, `telemetry/policy.py`, `git_publish.py`, `tasks/*.py`). Jeder Leser liest seine Datei aber selbst. - **Drei Arten von Daten liegen dort gemischt:** - was zur Instanz gehört (Schwellen der Aufgaben-Schicht); - was zu einem Checkout gehört (Werkzeugpfade, Remotes-Allowlist, Telemetrie-Zustimmung); - Secrets (`api_token` in `superproductivity`, `app_password` in `caldav`), im Klartext im Arbeitsbaum und nur durch `.gitignore` geschützt. Die Altdateien bleiben bis #208 bestehen. Dieses Paket stellt nur das Ziel bereit. ## Ziel | Stufe | Inhalt | Ablage | |---|---|---| | `instance`, **eingecheckt** | Was zum Korpus gehört und in jedem Klon gleich sein soll | `wikitool.json` im Instanz-Root | | `checkout`, lokal | Was nur auf diesem Rechner oder in diesem Klon stimmt | `.wikitool-local.json`, gitignored | | `secret` | Tokens, Passwörter, Sessions | außerhalb des Arbeitsbaums. Die beiden Dateien verweisen nur darauf | ## Entscheidungen des Betreibers (2026-10-10) 1. Eingecheckte Datei `wikitool.json`, lokale Datei `.wikitool-local.json`. 2. Secrets über eine Umgebungsvariable oder eine Datei im Nutzerverzeichnis (Entwurf 4), ohne OS-Keyring. 3. Geschrieben wird über `wikitool config set` (Entwurf 7). 4. **Zusätzlich die 1Password-CLI (`op`) als Secret-Quelle** (Entwurf 5): für Desktop-Installationen die bevorzugte Variante, in der CI wie bei anderen Action-Runnern. `op` kann auch Nicht-Secrets liefern (URLs, Connection-Strings), was die Konfiguration über Rechner hinweg portabel macht. 5. Freigabe des Publish mit 40 Dateien über das Mass-Update Gate (Token `1da42d56b1be`), samt der Bau-Entscheidungen unten (CI-Element, `gate`-Felder nur durch den Nutzer, `WARN` bei gesperrtem 1Password). ## Entwurf (gebaut) 1. **Jedes Feld gehört genau einer Stufe**, deklariert im `Field` von `config_shapes.py` (`tier="instance" | "checkout" | "secret"`). Ein Wert in der falschen Stufe ist ein Fehler, keine Überschreibung; derselbe Schlüssel in beiden Dateien ebenso. Die Abschnitte stehen in `config_shapes.SETTINGS_SECTIONS`. Nach diesem Paket ist das Tupel leer, Abschnitte bringen #208 und die Verbraucher #200/#169. **Bau:** Ein Abschnitt ist flach - keine verschachtelten Felder, keine Varianten -, damit die Stufe am Feld allein ablesbar ist. 2. **Harte Regel: Was ein Programm zum Ausführen benennt (`names_command`) oder ein Gate betrifft (`gate`), ist nie `instance`.** `Field.__post_init__` wirft beim Deklarieren. Auch als Verweis steht so ein Feld nie in `wikitool.json`. Die Regel steht für Agenten genau einmal: `AGENTS.md` § Settings. 3. **Verweise statt Werte.** Ein `secret`-Feld nimmt nur einen Verweis, ein `private`-Feld (private Infrastruktur) darf einen nehmen, alle anderen keinen. Zwei Formen: `{"ref": "<name>"}` (Kleinbuchstaben, Ziffern, einzelne `_`) und `{"ref": "op://<vault>/<item>/<field>"}`. **Bau:** Ein `private`-Feld ist immer Stufe `checkout` (beim Deklarieren erzwungen): Klartext nur in `.wikitool-local.json`, als Verweis in beiden Dateien. 4. **Quelle Name:** zuerst `WIKITOOL_SECRET_<NAME>`, sonst `~/.config/chemenu/<instanz-id>/secrets.json` (Windows: `%APPDATA%\chemenu\<instanz-id>\secrets.json`). Die `<instanz-id>` (UUID) erzeugt das erste `config set` in `wikitool.json`. **Bau:** `$XDG_CONFIG_HOME` wird statt `~/.config` beachtet, wenn gesetzt. "Nur für den Nutzer lesbar" wird erzwungen: außerhalb Windows wird eine Datei mit Rechten für Gruppe oder andere abgelehnt (exit 1, Hinweis `chmod 600`). 5. **Quelle 1Password:** `op read --no-newline <ref>` ohne Shell, stdin `/dev/null`, Timeout 60 s, Wert nur im Speicher. Fehlt `op` → exit 1 mit Preflight-Hinweis. Element/Feld/Tresor fehlt (op-Meldung "isn't an item/a vault/a field", "does not have a field" …) → exit 1 mit dem Verweis. Jeder andere Fehler und ein Timeout gelten als gesperrt/nicht angemeldet → exit 42, ohne den Verweis. **Bau:** `op` steht als neue Zeilenart `optional|op|…` in `tools/prerequisites.txt`; beide Preflights erfassen es, wenn vorhanden (`ABSENT` sonst, nie ein Problem), `--set op=` geht. Geprüft: Der Launcher liest aus `.wikitool-tools.json` nur `"complete": true`, `toolpaths` nur benannte Schlüssel - ein zusätzlicher Schlüssel stört keine ältere Version. Neu: `toolpaths.optional()`. 6. **CI.** **Bau, nach der Boilerplate des Betreibers:** Der Weg ist die Action. `1password/load-secrets-action@v2` lädt `op://CI-CD/gitea-package-token/username` (das am wenigsten sensible Feld, das der Service-Account sieht) nach `WIKITOOL_SECRET_CI_SMOKE`; der Test `test_live_secret_reference_resolves_from_ci` löst `{"ref": "ci_smoke"}` darüber auf und läuft mit `CHEMENU_LIVE_SECRET_REQUIRE=1`, damit "kein Wert" rot statt übersprungen ist. Dazu `curl unzip` in den Systemabhängigkeiten, wie in der Boilerplate. In Run 644 und 646 bestätigt. **Einschränkung, die der Bau gezeigt hat:** Der Action-Weg bedient Namensverweise. Eine Konfiguration mit `op://`-Verweisen braucht in der CI den anderen Weg (`op` im Job plus `OP_SERVICE_ACCOUNT_TOKEN`). Der Code kann das, diese CI testet es nicht. "Dieselbe Konfiguration auf beiden Wegen" gilt also nur je Verweisform, und so steht es im How-to. Nicht blockierend: Braucht ein Verbraucher (#200, #169) `op://` in der CI, kommt der zweite Weg dort dazu. 7. **`wikitool config set <schlüssel> <wert|--ref …>`** prüft Stufe und Form. Klartext `instance` → `wikitool.json`, Klartext `checkout`/`private` → `.wikitool-local.json`, Verweis → `wikitool.json`; derselbe Schlüssel in der anderen Datei wird entfernt und gemeldet. Ein Secret-Wert wird abgelehnt (exit 1, Wert nicht ausgegeben). **Bau:** "Nur der Nutzer setzt" gilt für `names_command` **und** `gate` - wie bei der Allowlist des Publish-Remote Gate; `config set` endet dort mit exit 42 und nennt die Datei zum Selbst-Editieren. Vor dem Schreiben werden beide Dateien geprüft, aber ohne Pflichtfelder, sonst ließe sich der erste Wert eines Abschnitts nie schreiben (`check_files(..., complete=False)`). 8. **Leseschicht `tools/chemenu/settings.py`:** liest beide Dateien, prüft Platzierung und Form, löst Verweise erst beim Zugriff auf, gibt einem Leser seinen Abschnitt. **Bau:** Für Kommandos gibt es `_util.load_settings()` und `_util.setting_value()` (exit 1 bzw. 42) - darüber testen die Kriterien die Exit-Codes, darüber lesen #200/#169. `doctor`: Zeilen `settings-instance`, `settings-checkout`, `settings-secrets`; ein nicht auflösbarer Verweis ist `FAIL`, ein gesperrtes 1Password `WARN` (sonst wäre `doctor` bei gesperrter App rot), ein unbekannter Abschnitt `WARN`. 9. **Erste Verbraucher:** #200 (Site-Handler) und #169 (Abruf mit Session). Hier wird mit einem Test-Shape getestet. ## Sicherheitsmodell (ehrlich) Secrets außerhalb des Baums schützen vor dem Einchecken. Sie schützen auch davor, dass ein Wert in einer Datei steht, die der Agent bei normaler Arbeit liest, oder in einer Ausgabe von `wikitool` auftaucht. **Sie schützen nicht vor einem Agenten mit Shell-Zugriff, der es darauf anlegt.** - Ist 1Password im Terminal entsperrt, kann der Agent `op read` selbst aufrufen. - Die Secret-Datei kann er ebenfalls lesen. - Was der Prozess von `wikitool` zur Laufzeit lesen kann, kann der Agent im selben Benutzerkontext grundsätzlich auch. Das begrenzt man, ohne es zu verhindern: - ein eigener 1Password-Tresor nur für die Einträge dieser Instanz; - die Freigabe-Abfrage der 1Password-Desktop-App. Sie gilt je Terminal-Sitzung, nicht je Wert, hält also nur eine Sitzung auf, die noch niemand freigegeben hat; - in der CI ein Service-Account, der nur diesen Tresor sieht. Die Doku sagt das so (`docs/explanation/configuration-tiers.md`), und der Stack verspricht nichts darüber hinaus. ## Betroffene Dateien (gebaut) - `tools/chemenu/config_shapes.py` (`tier`, `private`, `names_command`, `gate`, `SETTINGS_SECTIONS`), `tools/chemenu/config.py` (Dateinamen, `ENV_SECRET_PREFIX`, `secrets_file()`), `tools/chemenu/toolpaths.py` (`optional()`) - neu: `tools/chemenu/settings.py`; `tools/chemenu/commands/config_cmd.py` mit Record, Gruppe "Configuration" in `cli_contract.GROUPS`; `_util.load_settings/setting_value` - `tools/chemenu/commands/doctor.py`, `tools/chemenu/commands/docs_verify.py` (Kanarien `.wikitool-local.json` ignoriert / `wikitool.json` getrackt; `check_instance_settings`: nur Platzierung in `wikitool.json`, keine Pflichtfelder, die im lokalen File stehen dürfen) - `tools/prerequisites.txt`, `tools/chemenu/prerequisites.py`, `tools/preflight.sh`, `tools/preflight.ps1`, `instructions/preflight.md`, `INSTALL.md` - `.gitignore`, `tools/chemenu/tests/conftest.py` (`WIKITOOL_SECRET_*` per Präfix gelöscht) - `dist_cmd.py` unverändert: `wikitool.json` steht nicht in `ROOT_FILES`, wird also nicht ausgeliefert und von `dist upgrade` nie berührt (Test `test_plan_ships_neither_settings_file`) - `.gitea/workflows/ci.yml` - `AGENTS.md` § Settings (neu) und § File naming (sieben statt sechs Erklärseiten), `instructions/dev/doc-pull-through.md`, `instructions/dev/testing-conventions.md`, `tools/README.md` - `docs/explanation/configuration-tiers.md`, `docs/how-to/use-1password-for-settings.md` (beide in der Schlussphase nachgezogen, `e085a8b`), `docs/reference/configuration.md` § Tiered settings, `docs/reference/commands/configuration.md` (generiert) - Tests: `test_settings.py` (neu), `test_preflight.py`, `test_preflight_pwsh.py`, `test_dist_cmd.py` ## Akzeptanzkriterien - [x] Ein `Field` mit Stufe `instance`, das einen Befehl benennt, lässt sich nicht deklarieren (Test). - `test_a_field_that_names_a_program_or_a_gate_cannot_be_declared_instance` - [x] Ein Wert in der falschen Stufe ergibt beim Lesen exit 1 mit Datei, Schlüssel und richtiger Stufe. - `test_a_value_in_the_wrong_tier_fails_naming_file_key_and_tier` - [x] Ein Klartext-Wert in einem `secret`-Feld (beide Dateien) oder in einem `private`-Feld in `wikitool.json` ergibt exit 1. Der Wert erscheint in keiner Ausgabe. - `test_a_plain_secret_or_committed_private_value_fails_without_showing_it` (Kommando, `doctor`), `test_docs_verify_flags_a_plain_secret_in_the_committed_file_only` - [x] `{"ref": "<name>"}` löst über `WIKITOOL_SECRET_<NAME>` auf. Ohne die Variable löst es über die Secret-Datei im Nutzerverzeichnis auf, mit dem Test-Home. - [x] `{"ref": "op://…"}` löst über das falsche `op` auf. Ein gesperrtes `op` (Testskript mit entsprechendem Exit) ergibt exit 42, ein fehlendes `op` exit 1 mit dem Preflight-Hinweis. Dazu: Timeout → exit 42, fehlendes Element → exit 1 mit Verweis. - [x] Ohne `wikitool.json` und `.wikitool-local.json` verhält sich jedes bestehende Kommando byte-identisch zum Stand davor. **Bau:** Ausnahme ist `doctor` mit seinen drei neuen Zeilen (nächstes Kriterium). Geprüft gegen einen Worktree von `HEAD`: `lint`, `search`, `version show`, `docs verify`, `instructions verify`, `types list`, `sources coverage` byte-identisch. - [x] `doctor` zeigt für jede Stufe vorhanden/fehlt/fehlerhaft und für Verweise nur auflösbar/nicht auflösbar. - [x] `config set` setzt einen `instance`-Wert in `wikitool.json` und verweigert einen Secret-Wert im Klartext. - [x] `pytest`, `docs verify`, `instructions verify` grün; CI grün, mit einem echten 1Password-Wert in der CI. Die Action löst `op://CI-CD/gitea-package-token/username` auf, die Einstellung verweist per Namen darauf (Entwurf 6). CI-Run 644 grün, Smoke-Test `1 passed`; nach dem Doku-Nachzug erneut in Run 646. Ein `op://`-Verweis *in den Einstellungen selbst* ist in der CI nicht getestet, nur mit dem falschen `op` im Test (voriges Kriterium). Nicht blockierend, siehe Entwurf 6. ## Versionsteil **`--minor`** - gebaut als `9.0.1-beta.2` → `9.1.0-beta.1`. Neue, optionale Dateien und ein neues Kommando, die eine ältere Version ignoriert. Ohne sie ändert sich kein Verhalten außer drei `OK`-Zeilen in `doctor`. Neue Umgebungsvariablen sind additiv, der optionale Eintrag in `.wikitool-tools.json` ebenfalls (geprüft, Entwurf 5). Drop-in in beide Richtungen. Der Bruch kommt erst mit #208. ## Kette Kette „Beschaffung und Konfiguration“ - Vorgänger: #206 - Nachfolger: #208
torben added the prio/plannedsize/Larea/distributionkind/decision labels 2026-10-09 21:40:01 +00:00
torben changed title from Konfiguration in drei Stufen: eingecheckt (Instanz), lokal (Checkout), Secrets außerhalb des Baums to Konfiguration in drei Stufen: eingecheckt (Instanz), lokal (Checkout), Secrets außerhalb des Baums - mit 1Password-CLI 2026-10-10 06:15:36 +00:00
torben added kind/build and removed kind/decision labels 2026-10-10 06:15:36 +00:00
Author
Owner

Changelog: Die vier offenen Fragen sind entschieden (2026-10-10): Dateinamen, Secrets über Umgebungsvariable oder Nutzerverzeichnis, config set. Neu ist die 1Password-CLI als Secret-Quelle mit op://-Verweisen, auch für private Nicht-Secrets wie URLs. Damit dürfen Verweise auf private Werte in die eingecheckte Datei. Neu ist auch ein ehrlicher Abschnitt zum Sicherheitsmodell: Ein Agent mit Shell erreicht, was das Werkzeug erreicht. Die CI-Anbindung wartet auf die Boilerplate des Betreibers zum Start des Baus. kind/decision → kind/build. Die Kettenzeile listet nicht mehr alle Glieder (Konvention aus #205).

**Changelog:** Die vier offenen Fragen sind entschieden (2026-10-10): Dateinamen, Secrets über Umgebungsvariable oder Nutzerverzeichnis, `config set`. Neu ist die 1Password-CLI als Secret-Quelle mit `op://`-Verweisen, auch für private Nicht-Secrets wie URLs. Damit dürfen Verweise auf private Werte in die eingecheckte Datei. Neu ist auch ein ehrlicher Abschnitt zum Sicherheitsmodell: Ein Agent mit Shell erreicht, was das Werkzeug erreicht. Die CI-Anbindung wartet auf die Boilerplate des Betreibers zum Start des Baus. `kind/decision` → `kind/build`. Die Kettenzeile listet nicht mehr alle Glieder (Konvention aus #205).
Author
Owner

Changelog: Gebaut und publiziert als 9.1.0-beta.1 (f6aaadc), CI-Run 644 grün, inklusive des echten 1Password-Smoke-Tests. Die CI folgt der Boilerplate des Betreibers (gitea-mcp@ci-binford, Weg über die Action). Neu im Body an den jeweiligen Punkten (Bau:):

  • Der Action-Weg bedient nur Namensverweise. op:// in der CI braucht op plus Token und ist nicht getestet (nicht blockierend).
  • Auch gate-Felder setzt nur der Nutzer (exit 42).
  • private ist immer checkout.
  • config set prüft ohne Pflichtfelder.
  • doctor meldet WARN bei gesperrtem 1Password.
  • op steht als optional|-Zeile im Manifest.
  • Die Secret-Datei muss 0600 sein.
  • doctor ist die benannte Ausnahme vom Byte-identisch-Kriterium.

Alle Kriterien sind abgehakt. Offen bleibt nur die Schlussphase.

**Changelog:** Gebaut und publiziert als `9.1.0-beta.1` (`f6aaadc`), CI-Run 644 grün, inklusive des echten 1Password-Smoke-Tests. Die CI folgt der Boilerplate des Betreibers (`gitea-mcp@ci-binford`, Weg über die Action). Neu im Body an den jeweiligen Punkten (**Bau:**): - Der Action-Weg bedient nur Namensverweise. `op://` in der CI braucht `op` plus Token und ist nicht getestet (nicht blockierend). - Auch `gate`-Felder setzt nur der Nutzer (exit 42). - `private` ist immer `checkout`. - `config set` prüft ohne Pflichtfelder. - `doctor` meldet `WARN` bei gesperrtem 1Password. - `op` steht als `optional|`-Zeile im Manifest. - Die Secret-Datei muss `0600` sein. - `doctor` ist die benannte Ausnahme vom Byte-identisch-Kriterium. Alle Kriterien sind abgehakt. Offen bleibt nur die Schlussphase.
Author
Owner

Changelog: Abgeschlossen. Einstellungen gibt es jetzt in drei Stufen: wikitool.json (eingecheckt), .wikitool-local.json (lokal) und Secrets nur als Verweis, aufgelöst über WIKITOOL_SECRET_<NAME>, eine Secret-Datei im Nutzerverzeichnis oder die 1Password-CLI. Geschrieben wird mit dem neuen Kommando config set, ausgeliefert in 9.1.0-beta.1 (f6aaadc, CI-Run 644). In der Schlussphase wurden zwei Stellen der neuen Doku nachgezogen (e085a8b, CI-Run 646): der Pfad der Secret-Datei unter Windows und mit XDG im How-to, und dass die Freigabe-Abfrage von 1Password je Terminal-Sitzung gilt, nicht je Wert. Weiter mit #208.

**Changelog:** Abgeschlossen. Einstellungen gibt es jetzt in drei Stufen: `wikitool.json` (eingecheckt), `.wikitool-local.json` (lokal) und Secrets nur als Verweis, aufgelöst über `WIKITOOL_SECRET_<NAME>`, eine Secret-Datei im Nutzerverzeichnis oder die 1Password-CLI. Geschrieben wird mit dem neuen Kommando `config set`, ausgeliefert in `9.1.0-beta.1` (`f6aaadc`, CI-Run 644). In der Schlussphase wurden zwei Stellen der neuen Doku nachgezogen (`e085a8b`, CI-Run 646): der Pfad der Secret-Datei unter Windows und mit XDG im How-to, und dass die Freigabe-Abfrage von 1Password je Terminal-Sitzung gilt, nicht je Wert. Weiter mit #208.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: torben/chemenu#207