Addresses the four issues raised in the slice-3 audit before pushing. #1 (critical) — refuse to finish install when .env still contains placeholder secrets like "changeme". Without this, `furtka app install fileshare` would happily start an SMB server with a publicly-known password — the kind of default that ends up screenshotted on Hacker News. PLACEHOLDER_SECRETS lives in installer.py; new tests cover placeholder rejection, post-edit retry, and quoted values. #3 — reconciler now catches DockerError / FileNotFoundError / OSError per-app instead of letting a single broken app abort the whole boot-scan. Errors get surfaced as Action(kind="error", …) and has_errors() drives the CLI exit code so systemd still shows red, but the other apps actually got reconciled. #4 — chmod 0600 on .env after install so app secrets aren't world- readable on multi-user boxes. Done before the placeholder check so even the half-installed state is safe. #5 — load_manifest() got an optional expected_name. The scanner passes the folder name (filesystem source-of-truth contract); installer leaves it None so `furtka app install /tmp/some-fork/` works regardless of what the source folder is named. #2 — TODO comment on dperson/samba:latest. Switching to a digest needs a verified upstream release; left for the test-day pin. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
35 lines
1 KiB
Python
35 lines
1 KiB
Python
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
|
|
from furtka.manifest import Manifest, ManifestError, load_manifest
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class ScanResult:
|
|
path: Path
|
|
manifest: Manifest | None
|
|
error: str | None
|
|
|
|
@property
|
|
def ok(self) -> bool:
|
|
return self.manifest is not None
|
|
|
|
|
|
def scan(apps_root: Path) -> list[ScanResult]:
|
|
if not apps_root.exists():
|
|
return []
|
|
out: list[ScanResult] = []
|
|
for entry in sorted(apps_root.iterdir()):
|
|
if not entry.is_dir():
|
|
continue
|
|
manifest_path = entry / "manifest.json"
|
|
if not manifest_path.exists():
|
|
out.append(ScanResult(path=entry, manifest=None, error="manifest.json missing"))
|
|
continue
|
|
try:
|
|
m = load_manifest(manifest_path, expected_name=entry.name)
|
|
except ManifestError as e:
|
|
out.append(ScanResult(path=entry, manifest=None, error=str(e)))
|
|
continue
|
|
out.append(ScanResult(path=entry, manifest=m, error=None))
|
|
return out
|