Skip to content

feat(config): one config.toml → fail-closed prophet doctor + native rc emit (POSIX/Windows) - #60

Merged
mdheller merged 2 commits into
mainfrom
feat/prophet-config-doctor
Aug 4, 2026
Merged

feat(config): one config.toml → fail-closed prophet doctor + native rc emit (POSIX/Windows)#60
mdheller merged 2 commits into
mainfrom
feat/prophet-config-doctor

Conversation

@mdheller

@mdheller mdheller commented Aug 4, 2026

Copy link
Copy Markdown
Member

The rollout gap

Config was ~18 env vars scattered across 6 namespaces; .noetica is a state dir, not config; prophet-cli read no config; and the shell dotfiles diverge per build (SourceOS Nix / macOS brew / Windows PowerShell — different shells, service managers, secret stores). Nothing made "install → it works as advertised" true.

One source of truth, three native bindings

  • config.toml — the single human-edited config (endpoints, contexts, tenancy, opt-ins, secret references).
  • prophet doctor — a fail-closed check-set (same on every platform): required keys present, kube-context is not a production cluster, referenced secret files exist and are 0600 on POSIX (cred:/DPAPI on Windows), CLIs on PATH. This is what turns "works as advertised" into an enforced control.
  • prophet doctor --emit posix|powershellgenerates the native rc (.prophetrc / prophet.psm1) so the three platforms never drift by hand.
  • prophet config path|show|init.

Verified: fail-closed (missing secret / gke_prod context / missing key → exit 1 "would NOT work as advertised"); emitters produce POSIX (export/XDG/*_FILE 0600) and Windows ($env:/%APPDATA%/cred: DPAPI). +8 tests.

Next binders: SourceOS (doctor.sh calls this; sourceos-shell.nix writes ~/.prophetrc), macOS (brew caveats), and the Windows toolchain packaging (winget/choco — currently absent).

… rc emit (POSIX/Windows)

The rollout gap: config was ~18 scattered env vars, .noetica is a state dir (not config), prophet-cli read no config, and shell dotfiles diverge per build (SourceOS Nix / macOS brew / Windows PowerShell). This adds ONE platform-neutral source of truth + the shared core all three bindings sit on:

- config.py: load(config.toml) + doctor(cfg) [fail-closed check-set: required keys, kube-context is NOT a prod cluster, referenced secret files exist and are 0600 on POSIX (cred:/DPAPI on Windows), CLIs on PATH] + emit(target=posix|powershell) that GENERATES the native rc so the three platforms never drift.
- prophet doctor [--json|--quiet|--emit posix|powershell|--config PATH] and prophet config [path|show|init], wired into the argparse cli (intercepted before the delegating parser).
- config.example.toml (documented template; `prophet config init` writes it).

Verified: doctor is fail-closed (missing secret / prod-context / missing key → exit 1 'would NOT work as advertised'); emit produces .prophetrc (export/XDG/*_FILE 0600) and prophet.psm1 ($env:/%APPDATA%/cred: DPAPI). +8 tests. Next: the SourceOS binder (doctor.sh calls this; sourceos-shell.nix writes ~/.prophetrc), then macOS (brew caveats), then the Windows toolchain packaging.
Comment thread src/prophet_cli/config.py
errors = [r for r in results if r["level"] == "error"]
if as_json:
import json
print(json.dumps({"ok": not errors, "results": results}, indent=2))
Comment thread src/prophet_cli/config.py
elif not quiet:
sym = {"ok": "✓", "warn": "!", "error": "✗"}
for r in results:
print(f" {sym.get(r['level'],'?')} {r['name']}: {r['message']}", file=sys.stderr)
Comment thread src/prophet_cli/config.py
print(f"prophet doctor: {e}", file=sys.stderr)
return 1
if a.emit:
sys.stdout.write(emit(cfg, target=a.emit))
Comment thread tests/test_config.py Fixed
CodeQL flagged four high-severity alerts on this PR and it was right on all four. I had earlier
dismissed the failure as a 1s startup fault without opening it — it was a real finding.

THREE were clear-text logging of sensitive information. `doctor` built its messages with the
RESOLVED PATH of every secret and then printed them, so a `prophet doctor` run in any CI log would
permanently disclose where each signing key lives. Not the key — the location, which is attack
surface on its own, and which is exactly what this estate's own rule about scanners not echoing
their matches exists to prevent. The reporter now names the SECRET and the CONDITION and nothing
else: "present, 0600", "present but 0o640 — must be 0600", "referenced secret file is missing".
Redaction is not leniency: the wrong mode is still refused, and a test asserts both halves.

`emit` deliberately still carries the reference, because it is a GENERATOR rather than a reporter —
its stdout IS the rc file, and the rc file must contain the path for the shell to resolve at load.
It carries no secret VALUE. Said so at the write site, and pinned by a test, so the distinction does
not get "fixed" later by someone reading only the first half.

THE FOURTH was my own negative-control fixture chmod'ing a file to 0o644 — world-readable — to
prove the 0600 check refuses it. 0o640 proves exactly the same thing (anything but 0600 is refused)
without creating a world-readable file on disk to do it.

10 tests pass.
Comment thread tests/test_config.py
def test_secret_file_existence_and_0600_enforced_on_posix():
with tempfile.TemporaryDirectory() as td:
good = Path(td) / "k.key"; good.write_text("x"); os.chmod(good, 0o600)
bad = Path(td) / "b.key"; bad.write_text("x"); os.chmod(bad, 0o640) # group-readable: fails 0600, not world-readable
Comment thread tests/test_config.py
with tempfile.TemporaryDirectory() as td:
secret = Path(td) / "sovereign-root.key"
secret.write_text("x")
os.chmod(secret, 0o640) # wrong mode, so it is reported at all
@mdheller
mdheller merged commit 110cfba into main Aug 4, 2026
12 of 13 checks passed
@mdheller
mdheller deleted the feat/prophet-config-doctor branch August 4, 2026 23:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants