docs: add stelloauth implementation plan
This commit is contained in:
@@ -0,0 +1,831 @@
|
|||||||
|
# Home Assistant Stelloauth Add-on 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:** Build a source-only Home Assistant custom add-on repository that runs pinned Stelloauth and CloakBrowser in one Supervisor-managed container and gives Stellantis Vehicles the exact internal worker URL.
|
||||||
|
|
||||||
|
**Architecture:** A small Python process manager starts a loopback-only CloakBrowser CDP service, proves it is ready, then starts a locally built and security-patched Stelloauth listener. Home Assistant Core reaches only Stelloauth through Supervisor DNS; the repository never redistributes CloakBrowser's proprietary binary and never publishes a derived image.
|
||||||
|
|
||||||
|
**Tech Stack:** Home Assistant add-on metadata, Docker BuildKit, Go 1.27.1, Stelloauth v0.6.0, CloakBrowser 0.5.10, Python 3.12 standard library, pytest, PyYAML, Gitea Actions.
|
||||||
|
|
||||||
|
**Spec:** docs/superpowers/specs/2026-09-24-home-assistant-stelloauth-addon-design.md
|
||||||
|
|
||||||
|
## Global Constraints
|
||||||
|
|
||||||
|
- Pin Stelloauth to tag v0.6.0 and commit 367d4f8c02a3b072c59142c49dffc129edc8548b.
|
||||||
|
- Pin CloakBrowser to 0.5.10, commit f04c23da285b3b3d3cf10c8f9d282e7adc1d52ce, and OCI index digest sha256:2ed5b2d047cbdde22cde7ef1a796526c716aadaa5bccbe1db5ade49282b64a76.
|
||||||
|
- Pin the builder to golang:1.27.1-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195.
|
||||||
|
- Support amd64 and aarch64; the inspected HAOS target is amd64.
|
||||||
|
- Use one non-privileged add-on with init: true; request no host network, Docker API, devices, maps, ingress, or full access.
|
||||||
|
- Keep host port 8080/tcp disabled by default and bind CDP only to 127.0.0.1:9222.
|
||||||
|
- Document the exact internal worker URL as http://0031621f-stelloauth:8080/worker.
|
||||||
|
- Keep CLOAK_MAX_SESSIONS=1; expose only queue_timeout, rate_limit_count, and rate_limit_duration.
|
||||||
|
- Never persist or log passwords, email addresses, OAuth codes, cookies, access tokens, refresh tokens, redirect URLs, or browser URLs.
|
||||||
|
- Accept worker URLs only for HTTPS on the default port and exact /am/oauth2/authorize path at the five approved identity hosts.
|
||||||
|
- Limit /worker bodies to 64 KiB and rate-limit OAuth by direct peer address.
|
||||||
|
- Do not push, tag, release, publish an image, install on HAOS, or change production Home Assistant settings.
|
||||||
|
|
||||||
|
## Review Focus
|
||||||
|
|
||||||
|
- Invalid JSON, missing or unknown option keys, booleans used as integers, zero counts, and malformed durations must stop startup with one fixed non-secret error; Task 3 tests them.
|
||||||
|
- URL userinfo, explicit ports, deceptive suffixes, escaped paths, fragments, schemes, and case normalization must obey the exact allowlist; Task 2 tests them.
|
||||||
|
- A live CloakBrowser root page is insufficient; readiness must require a loopback webSocketDebuggerUrl and close its named readiness profile before Stelloauth starts; Task 3 tests it.
|
||||||
|
- Every unexpected child exit, including exit 0, must stop and reap the sibling and make the manager fail; Task 3 tests both directions.
|
||||||
|
- SIGTERM or SIGINT during readiness or shutdown must stop both process groups within ten seconds, escalate to SIGKILL, and reap all children; Task 3 tests it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
| --- | --- |
|
||||||
|
| .gitignore | Ignore Python caches, pytest state, and local evidence. |
|
||||||
|
| requirements-dev.txt and pytest.ini | Pin and configure the Python test harness. |
|
||||||
|
| repository.yaml | Describe the repository to Home Assistant. |
|
||||||
|
| stelloauth/config.yaml | Define Supervisor metadata, options, disabled host port, and watchdog. |
|
||||||
|
| stelloauth/Dockerfile | Verify pins, apply patches, test/build Stelloauth, and assemble the local runtime. |
|
||||||
|
| stelloauth/rootfs/usr/local/bin/addon-supervisor | Validate options and supervise both process groups. |
|
||||||
|
| stelloauth/patches/stelloauth-security.patch | Harden URL validation, body size, rate-limit identity, and logs. |
|
||||||
|
| stelloauth/patches/cloakserve-loopback.patch | Bind CloakBrowser to container loopback. |
|
||||||
|
| stelloauth/translations/{da,en}.yaml | Describe the three options in Supervisor. |
|
||||||
|
| README.md, stelloauth/README.md, stelloauth/DOCS.md | Repository and user installation documentation. |
|
||||||
|
| LICENSE and stelloauth/CHANGELOG.md | Repository license and version history. |
|
||||||
|
| tests/test_addon_metadata.py | Validate metadata, pins, DNS derivation, and documentation. |
|
||||||
|
| tests/test_security_patches.py | Apply/test patches against exact upstream commits. |
|
||||||
|
| tests/test_supervisor.py | Unit-test parsing, readiness, lifecycle, signals, and logs. |
|
||||||
|
| tests/test_runtime.sh | Build and exercise the real amd64 container. |
|
||||||
|
| .gitea/workflows/ci.yml | Repeat supported checks without publishing or deploying. |
|
||||||
|
|
||||||
|
### Task 1: Supervisor metadata and repository contract
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: .gitignore
|
||||||
|
- Create: requirements-dev.txt
|
||||||
|
- Create: pytest.ini
|
||||||
|
- Create: repository.yaml
|
||||||
|
- Create: stelloauth/config.yaml
|
||||||
|
- Create: stelloauth/translations/da.yaml
|
||||||
|
- Create: stelloauth/translations/en.yaml
|
||||||
|
- Create: tests/test_addon_metadata.py
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: the approved repository URL, version, architecture, and options.
|
||||||
|
- Produces: the options contract consumed by load_options() in Task 3 and the port/watchdog contract consumed by Supervisor.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the test harness and failing metadata tests**
|
||||||
|
|
||||||
|
Create requirements-dev.txt:
|
||||||
|
|
||||||
|
pytest==8.4.2
|
||||||
|
PyYAML==6.0.2
|
||||||
|
|
||||||
|
Create pytest.ini:
|
||||||
|
|
||||||
|
[pytest]
|
||||||
|
markers =
|
||||||
|
upstream: fetches and validates pinned upstream source
|
||||||
|
docker: builds or runs the add-on image
|
||||||
|
|
||||||
|
Create tests/test_addon_metadata.py with:
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import hashlib
|
||||||
|
from pathlib import Path
|
||||||
|
import yaml
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parents[1]
|
||||||
|
REPOSITORY_URL = "https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon.git"
|
||||||
|
|
||||||
|
def load_yaml(path: str) -> dict:
|
||||||
|
with (ROOT / path).open(encoding="utf-8") as handle:
|
||||||
|
value = yaml.safe_load(handle)
|
||||||
|
assert isinstance(value, dict)
|
||||||
|
return value
|
||||||
|
|
||||||
|
def test_repository_metadata() -> None:
|
||||||
|
metadata = load_yaml("repository.yaml")
|
||||||
|
assert metadata["url"] == REPOSITORY_URL
|
||||||
|
assert metadata["name"] == "Stelloauth for Home Assistant"
|
||||||
|
assert metadata["maintainer"] == "Dennis / Radix ApS"
|
||||||
|
|
||||||
|
def test_addon_contract() -> None:
|
||||||
|
config = load_yaml("stelloauth/config.yaml")
|
||||||
|
assert config["slug"] == "stelloauth"
|
||||||
|
assert config["version"] == "0.1.0"
|
||||||
|
assert config["arch"] == ["amd64", "aarch64"]
|
||||||
|
assert config["startup"] == "application"
|
||||||
|
assert config["boot"] == "auto"
|
||||||
|
assert config["init"] is True
|
||||||
|
assert config["watchdog"] == "http://[HOST]:[PORT:8080]/"
|
||||||
|
assert config["ports"] == {"8080/tcp": None}
|
||||||
|
for key in ("ingress", "host_network", "privileged", "full_access", "docker_api", "devices", "map"):
|
||||||
|
assert key not in config
|
||||||
|
|
||||||
|
def test_defaults_and_schema_are_aligned() -> None:
|
||||||
|
config = load_yaml("stelloauth/config.yaml")
|
||||||
|
assert config["options"] == {
|
||||||
|
"queue_timeout": "60s",
|
||||||
|
"rate_limit_count": 5,
|
||||||
|
"rate_limit_duration": "1h",
|
||||||
|
}
|
||||||
|
assert set(config["schema"]) == set(config["options"])
|
||||||
|
assert config["schema"]["rate_limit_count"] == "int(1,20)"
|
||||||
|
|
||||||
|
def test_repository_hostname_derivation() -> None:
|
||||||
|
repository_id = hashlib.sha1(REPOSITORY_URL.lower().encode()).hexdigest()[:8]
|
||||||
|
assert repository_id == "0031621f"
|
||||||
|
assert f"{repository_id}-stelloauth" == "0031621f-stelloauth"
|
||||||
|
|
||||||
|
def test_translations_cover_every_option() -> None:
|
||||||
|
keys = set(load_yaml("stelloauth/config.yaml")["options"])
|
||||||
|
for language in ("da", "en"):
|
||||||
|
translation = load_yaml(f"stelloauth/translations/{language}.yaml")
|
||||||
|
assert set(translation["configuration"]) == keys
|
||||||
|
for entry in translation["configuration"].values():
|
||||||
|
assert set(entry) == {"name", "description"}
|
||||||
|
assert all(isinstance(value, str) and value.strip() for value in entry.values())
|
||||||
|
|
||||||
|
- [ ] **Step 2: Prove the tests are red**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
python3 -m venv .venv
|
||||||
|
.venv/bin/pip install -r requirements-dev.txt
|
||||||
|
.venv/bin/pytest tests/test_addon_metadata.py -q
|
||||||
|
|
||||||
|
Expected: FAIL with FileNotFoundError for repository.yaml.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the minimal metadata**
|
||||||
|
|
||||||
|
Create repository.yaml:
|
||||||
|
|
||||||
|
name: Stelloauth for Home Assistant
|
||||||
|
url: https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon.git
|
||||||
|
maintainer: Dennis / Radix ApS
|
||||||
|
|
||||||
|
Create stelloauth/config.yaml:
|
||||||
|
|
||||||
|
name: Stelloauth
|
||||||
|
version: "0.1.0"
|
||||||
|
slug: stelloauth
|
||||||
|
description: Local OAuth worker for Stellantis Vehicles using CloakBrowser
|
||||||
|
url: https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon
|
||||||
|
arch:
|
||||||
|
- amd64
|
||||||
|
- aarch64
|
||||||
|
startup: application
|
||||||
|
boot: auto
|
||||||
|
init: true
|
||||||
|
watchdog: http://[HOST]:[PORT:8080]/
|
||||||
|
ports:
|
||||||
|
8080/tcp: null
|
||||||
|
ports_description:
|
||||||
|
8080/tcp: Temporary LAN access for troubleshooting only
|
||||||
|
options:
|
||||||
|
queue_timeout: 60s
|
||||||
|
rate_limit_count: 5
|
||||||
|
rate_limit_duration: 1h
|
||||||
|
schema:
|
||||||
|
queue_timeout: match(^[1-9][0-9]*(ms|s|m|h)$)
|
||||||
|
rate_limit_count: int(1,20)
|
||||||
|
rate_limit_duration: match(^[1-9][0-9]*(ms|s|m|h)$)
|
||||||
|
|
||||||
|
Create both translation files with configuration entries for all three keys. Danish labels are Køventetid, Loginforsøg, and Rate limit-periode; English labels are Queue timeout, Login attempts, and Rate limit period. Each gets one sentence describing its behavior.
|
||||||
|
|
||||||
|
Create .gitignore:
|
||||||
|
|
||||||
|
.venv/
|
||||||
|
.pytest_cache/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.coverage
|
||||||
|
artifacts/
|
||||||
|
|
||||||
|
- [ ] **Step 4: Prove metadata is green and review the exact diff**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_addon_metadata.py -q
|
||||||
|
git diff --check
|
||||||
|
git diff -- repository.yaml stelloauth/config.yaml stelloauth/translations tests/test_addon_metadata.py
|
||||||
|
|
||||||
|
Expected: all tests PASS; no privileged capability exists and 8080/tcp is null.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
git add .gitignore requirements-dev.txt pytest.ini repository.yaml stelloauth/config.yaml stelloauth/translations tests/test_addon_metadata.py
|
||||||
|
git commit -m "feat: add Home Assistant addon metadata"
|
||||||
|
|
||||||
|
### Task 2: Reproducible upstream security patches
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: stelloauth/patches/stelloauth-security.patch
|
||||||
|
- Create: stelloauth/patches/cloakserve-loopback.patch
|
||||||
|
- Create: tests/test_security_patches.py
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: exact upstream commits from Global Constraints.
|
||||||
|
- Produces: patches that Task 4 applies during the image build.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write failing repository-level patch tests**
|
||||||
|
|
||||||
|
Create tests/test_security_patches.py:
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
import subprocess
|
||||||
|
from pathlib import Path
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
ROOT = Path(__file__).parents[1]
|
||||||
|
STELLOAUTH_COMMIT = "367d4f8c02a3b072c59142c49dffc129edc8548b"
|
||||||
|
CLOAK_COMMIT = "f04c23da285b3b3d3cf10c8f9d282e7adc1d52ce"
|
||||||
|
|
||||||
|
def run(*args: str, cwd: Path):
|
||||||
|
return subprocess.run(args, cwd=cwd, text=True, capture_output=True, check=True)
|
||||||
|
|
||||||
|
def fetch_exact(tmp_path: Path, name: str, url: str, commit: str) -> Path:
|
||||||
|
target = tmp_path / name
|
||||||
|
run("git", "init", str(target), cwd=tmp_path)
|
||||||
|
run("git", "remote", "add", "origin", url, cwd=target)
|
||||||
|
run("git", "fetch", "--depth=1", "origin", commit, cwd=target)
|
||||||
|
run("git", "checkout", "--detach", "FETCH_HEAD", cwd=target)
|
||||||
|
assert run("git", "rev-parse", "HEAD", cwd=target).stdout.strip() == commit
|
||||||
|
return target
|
||||||
|
|
||||||
|
@pytest.mark.upstream
|
||||||
|
def test_stelloauth_patch_applies_and_tests_pass(tmp_path: Path) -> None:
|
||||||
|
source = fetch_exact(tmp_path, "stelloauth", "https://github.com/tamcore/stelloauth.git", STELLOAUTH_COMMIT)
|
||||||
|
patch = ROOT / "stelloauth/patches/stelloauth-security.patch"
|
||||||
|
run("git", "apply", "--check", str(patch), cwd=source)
|
||||||
|
run("git", "apply", str(patch), cwd=source)
|
||||||
|
run("go", "test", "./...", cwd=source)
|
||||||
|
|
||||||
|
@pytest.mark.upstream
|
||||||
|
def test_cloak_patch_binds_only_loopback(tmp_path: Path) -> None:
|
||||||
|
source = fetch_exact(tmp_path, "cloakbrowser", "https://github.com/CloakHQ/CloakBrowser.git", CLOAK_COMMIT)
|
||||||
|
patch = ROOT / "stelloauth/patches/cloakserve-loopback.patch"
|
||||||
|
run("git", "apply", "--check", str(patch), cwd=source)
|
||||||
|
run("git", "apply", str(patch), cwd=source)
|
||||||
|
wrapper = (source / "bin/cloakserve").read_text(encoding="utf-8")
|
||||||
|
assert 'host = "127.0.0.1"' in wrapper
|
||||||
|
assert 'host = "0.0.0.0" if in_container' not in wrapper
|
||||||
|
run("python3", "-m", "py_compile", "bin/cloakserve", cwd=source)
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_security_patches.py -q
|
||||||
|
|
||||||
|
Expected: FAIL because both patch files are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add red tests to an exact Stelloauth checkout**
|
||||||
|
|
||||||
|
Create /tmp/stelloauth-addon-patch at commit 367d4f8c02a3b072c59142c49dffc129edc8548b. In internal/app/worker_test.go add table-driven validation with these allowed inputs:
|
||||||
|
|
||||||
|
https://idpcvs.citroen.com/am/oauth2/authorize?redirect_uri=mycitroen%3A%2F%2Foauth2redirect%2Fdk
|
||||||
|
https://idpcvs.driveds.com/am/oauth2/authorize?redirect_uri=mymap%3A%2F%2Foauth2redirect%2Fdk
|
||||||
|
https://idpcvs.opel.com/am/oauth2/authorize?redirect_uri=myopel%3A%2F%2Foauth2redirect%2Fdk
|
||||||
|
https://idpcvs.peugeot.com/am/oauth2/authorize?redirect_uri=mypeugeot%3A%2F%2Foauth2redirect%2Fdk
|
||||||
|
https://idpcvs.vauxhall.co.uk/am/oauth2/authorize?redirect_uri=myvauxhall%3A%2F%2Foauth2redirect%2Fgb
|
||||||
|
https://IDPCVS.OPEL.COM/am/oauth2/authorize?redirect_uri=myopel%3A%2F%2Foauth2redirect%2Fdk
|
||||||
|
|
||||||
|
Add rejected inputs for HTTP, userinfo, explicit :443, an evil suffix, an extra path component, an escaped path, and a fragment. Add these tests:
|
||||||
|
|
||||||
|
- TestHandleWorker_RequestTooLarge: send 65 KiB, expect 413 and Request body too large.
|
||||||
|
- TestRemoteClientIPIgnoresForwardedHeaders: RemoteAddr 192.0.2.10:1234 with spoofed forwarded headers returns 192.0.2.10.
|
||||||
|
- TestRequestLogsDoNotContainCredentialsOrURLs: capture logs and reject sentinel email, password, authorize URL, redirect URL, and OAuth code.
|
||||||
|
- Update the existing TestRedirectScheme success cases to approved hosts.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
cd /tmp/stelloauth-addon-patch
|
||||||
|
go test ./internal/app -run 'TestValidateWorkerURL|TestHandleWorker_RequestTooLarge|TestRemoteClientIPIgnoresForwardedHeaders|TestRequestLogsDoNotContainCredentialsOrURLs|TestRedirectScheme' -count=1
|
||||||
|
|
||||||
|
Expected: FAIL because validation, body limiting, direct peer identity, and redaction are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Implement the minimal upstream hardening**
|
||||||
|
|
||||||
|
In internal/app/worker.go define maxWorkerRequestBytes = 64 << 10 and validateWorkerURL(rawURL string) error. Parse with net/url and require:
|
||||||
|
|
||||||
|
- parsed.Scheme == "https";
|
||||||
|
- parsed.User == nil;
|
||||||
|
- parsed.Port() == "";
|
||||||
|
- parsed.Fragment == "";
|
||||||
|
- strings.ToLower(parsed.Hostname()) is exactly one approved host;
|
||||||
|
- parsed.EscapedPath() == "/am/oauth2/authorize".
|
||||||
|
|
||||||
|
Call validation before redirectScheme. Wrap the body with http.MaxBytesReader; identify *http.MaxBytesError using errors.As and return 413 with the fixed message.
|
||||||
|
|
||||||
|
Add remoteClientIP(r) based only on r.RemoteAddr using net.SplitHostPort with a bare-IP fallback. Use it for /oauth and /worker rate-limit keys and logs. Keep the current proxy-aware helper only for /geo.
|
||||||
|
|
||||||
|
Replace sensitive messages with:
|
||||||
|
|
||||||
|
log.Printf("[%s] worker OAuth request from %s", requestID, clientIP)
|
||||||
|
log.Printf("[%s] OAuth request from %s (%s/%s)", requestID, clientIP, req.Brand, req.Country)
|
||||||
|
log.Printf("[%s] Captured OAuth redirect", requestID)
|
||||||
|
log.Printf("[%s] Stellantis error page received", requestID)
|
||||||
|
log.Printf("Current browser location checked")
|
||||||
|
|
||||||
|
Ensure returned/logged error strings do not interpolate current or redirect URLs.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Prove upstream tests pass and export the patch**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
cd /tmp/stelloauth-addon-patch
|
||||||
|
gofmt -w internal/app/worker.go internal/app/worker_test.go internal/app/server.go internal/app/server_test.go internal/app/oauth.go internal/app/oauth_test.go
|
||||||
|
go test ./... -count=1
|
||||||
|
git diff --check
|
||||||
|
mkdir -p "/Users/dennis/Gitea/HA Addon Stellantis/stelloauth/patches"
|
||||||
|
git diff --binary > "/Users/dennis/Gitea/HA Addon Stellantis/stelloauth/patches/stelloauth-security.patch"
|
||||||
|
|
||||||
|
Expected: all upstream tests PASS; the patch includes both tests and code.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Make the CloakBrowser listener patch**
|
||||||
|
|
||||||
|
Checkout CloakBrowser commit f04c23da285b3b3d3cf10c8f9d282e7adc1d52ce in /tmp/cloakbrowser-addon-patch. Replace only:
|
||||||
|
|
||||||
|
in_container = os.path.exists("/.dockerenv") or os.path.exists("/run/.containerenv")
|
||||||
|
host = "0.0.0.0" if in_container else "127.0.0.1"
|
||||||
|
|
||||||
|
with:
|
||||||
|
|
||||||
|
host = "127.0.0.1"
|
||||||
|
|
||||||
|
Run Python bytecode compilation, diff check, and export the binary diff to stelloauth/patches/cloakserve-loopback.patch.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Validate fresh patch application and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_security_patches.py -q
|
||||||
|
git diff --check
|
||||||
|
git diff -- stelloauth/patches tests/test_security_patches.py
|
||||||
|
|
||||||
|
Expected: both exact-commit patch tests PASS.
|
||||||
|
|
||||||
|
git add stelloauth/patches tests/test_security_patches.py
|
||||||
|
git commit -m "fix: harden stelloauth oauth worker"
|
||||||
|
|
||||||
|
### Task 3: Options, readiness, and process lifecycle
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: stelloauth/rootfs/usr/local/bin/addon-supervisor
|
||||||
|
- Create: tests/test_supervisor.py
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: /data/options.json with queue_timeout: str, rate_limit_count: int, rate_limit_duration: str.
|
||||||
|
- Produces: Options, load_options(path: Path) -> Options, build_environment(options: Options) -> dict[str, str], HttpResponse, probe_cloak(request: Callable[[str, str, float], HttpResponse]) -> None, wait_until_ready(name: str, probe: Callable[[], None], timeout: float, stopping: Callable[[], bool], monotonic, sleep) -> None, and ProcessManager.run() -> int.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write red option and environment tests**
|
||||||
|
|
||||||
|
Load the extensionless script with importlib.machinery.SourceFileLoader. Assert valid defaults map to:
|
||||||
|
|
||||||
|
CLOAK_CDP_URL=http://127.0.0.1:9222
|
||||||
|
CLOAK_MAX_SESSIONS=1
|
||||||
|
CLOAK_QUEUE_TIMEOUT=60s
|
||||||
|
RATE_LIMIT_COUNT=5
|
||||||
|
RATE_LIMIT_DURATION=1h
|
||||||
|
HTTP_ADDRESS=0.0.0.0
|
||||||
|
PORT=8080
|
||||||
|
METRICS_ADDRESS=127.0.0.1
|
||||||
|
METRICS_PORT=9090
|
||||||
|
|
||||||
|
Parameterize invalid cases: empty object, unknown key, missing key, 0s, duration without unit, boolean count, count 0, count 21, malformed JSON, and unreadable file. Every case raises ConfigError("Invalid add-on configuration"), and captured logs contain no input value.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_supervisor.py -k 'options or environment' -q
|
||||||
|
|
||||||
|
Expected: FAIL because the script is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Implement strict parsing and environment construction**
|
||||||
|
|
||||||
|
The script begins:
|
||||||
|
|
||||||
|
#!/usr/bin/env python3
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import dataclasses
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import signal
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import urllib.request
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Callable, NamedTuple
|
||||||
|
|
||||||
|
OPTIONS_PATH = Path("/data/options.json")
|
||||||
|
PROFILE_PATH = Path("/tmp/cloakserve")
|
||||||
|
CLOAK_ROOT = "http://127.0.0.1:9222/"
|
||||||
|
CLOAK_VERSION = "http://127.0.0.1:9222/json/version?fingerprint=addon-readiness"
|
||||||
|
CLOAK_CLOSE = "http://127.0.0.1:9222/fingerprint/addon-readiness/close"
|
||||||
|
STELLOAUTH_ROOT = "http://127.0.0.1:8080/"
|
||||||
|
DURATION = re.compile(r"^[1-9][0-9]*(?:ms|s|m|h)$")
|
||||||
|
|
||||||
|
class ConfigError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@dataclasses.dataclass(frozen=True)
|
||||||
|
class Options:
|
||||||
|
queue_timeout: str
|
||||||
|
rate_limit_count: int
|
||||||
|
rate_limit_duration: str
|
||||||
|
|
||||||
|
class HttpResponse(NamedTuple):
|
||||||
|
status: int
|
||||||
|
body: bytes
|
||||||
|
|
||||||
|
load_options must require exactly the three keys, reject bool before the integer range check, validate both durations, and translate every parse/read/type failure to the fixed ConfigError. build_environment returns a copy of os.environ plus the nine exact variables above. Configure message-only INFO logs; the main path logs only the fixed error and exits 2 on invalid configuration.
|
||||||
|
|
||||||
|
Run the focused tests; expected PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Write red readiness tests**
|
||||||
|
|
||||||
|
Test probe_cloak with an injected request callable. Require calls in this order:
|
||||||
|
|
||||||
|
GET http://127.0.0.1:9222/
|
||||||
|
GET http://127.0.0.1:9222/json/version?fingerprint=addon-readiness
|
||||||
|
POST http://127.0.0.1:9222/fingerprint/addon-readiness/close
|
||||||
|
|
||||||
|
The version body must contain a non-empty webSocketDebuggerUrl beginning ws://127.0.0.1:. Reject empty JSON, empty URL, non-JSON, non-200, and a non-loopback websocket. Test probe_stelloauth requires HTTP 200 at its root.
|
||||||
|
|
||||||
|
Test wait_until_ready with injected monotonic and sleep functions: transient exceptions back off from 0.25 seconds to at most 2 seconds; Cloak expires at 60 seconds; Stelloauth expires at 30 seconds; a stop flag aborts immediately.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_supervisor.py -k 'probe or readiness or backoff' -q
|
||||||
|
|
||||||
|
Expected: FAIL because readiness functions are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Implement bounded, proxy-free readiness**
|
||||||
|
|
||||||
|
Implement http_request(method, url, timeout) using urllib.request.build_opener(urllib.request.ProxyHandler({})). Never log bodies, returned URLs, option values, or exception strings.
|
||||||
|
|
||||||
|
Use this retry contract:
|
||||||
|
|
||||||
|
class ReadinessError(RuntimeError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def wait_until_ready(name, probe, timeout, stopping,
|
||||||
|
monotonic=time.monotonic, sleep=time.sleep):
|
||||||
|
deadline = monotonic() + timeout
|
||||||
|
delay = 0.25
|
||||||
|
while monotonic() < deadline and not stopping():
|
||||||
|
try:
|
||||||
|
probe()
|
||||||
|
return
|
||||||
|
except (OSError, ValueError, ReadinessError):
|
||||||
|
sleep(delay)
|
||||||
|
delay = min(delay * 2, 2.0)
|
||||||
|
raise ReadinessError(f"{name} did not become ready")
|
||||||
|
|
||||||
|
probe_cloak parses and validates the websocket field, then closes the readiness profile. probe_stelloauth checks root status only.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Write red lifecycle and shutdown tests**
|
||||||
|
|
||||||
|
Build FakeProcess objects with pid, poll(), wait(timeout), and recorded group signals. Inject process creation, killpg, probes, sleep, and monotonic into ProcessManager. Assert exact startup events:
|
||||||
|
|
||||||
|
clean profiles
|
||||||
|
start /usr/local/bin/cloakserve --headless=true --idle-timeout=30 --data-dir=/tmp/cloakserve
|
||||||
|
probe cloak
|
||||||
|
start /usr/local/bin/stelloauth
|
||||||
|
probe stelloauth
|
||||||
|
|
||||||
|
Cover:
|
||||||
|
|
||||||
|
- stale profile removal and recreation with mode 0700;
|
||||||
|
- Cloak readiness failure never starts Stelloauth;
|
||||||
|
- Stelloauth readiness failure stops both;
|
||||||
|
- either child returning 0 or nonzero unexpectedly stops its sibling and returns nonzero;
|
||||||
|
- SIGTERM and SIGINT stop both groups and return 0;
|
||||||
|
- an ignoring child receives SIGKILL after one shared ten-second deadline;
|
||||||
|
- every child is reaped once;
|
||||||
|
- manager logs contain only fixed lifecycle strings and no sentinel option, credential, code, cookie, or token.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_supervisor.py -k 'lifecycle or child or shutdown or signal or profiles or logs' -q
|
||||||
|
|
||||||
|
Expected: FAIL because ProcessManager is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Implement process-group management**
|
||||||
|
|
||||||
|
Use:
|
||||||
|
|
||||||
|
CLOAK_COMMAND = [
|
||||||
|
"/usr/local/bin/cloakserve",
|
||||||
|
"--headless=true",
|
||||||
|
"--idle-timeout=30",
|
||||||
|
"--data-dir=/tmp/cloakserve",
|
||||||
|
]
|
||||||
|
STELLOAUTH_COMMAND = ["/usr/local/bin/stelloauth"]
|
||||||
|
|
||||||
|
Spawn with subprocess.Popen(command, env=environment, start_new_session=True). The manager must:
|
||||||
|
|
||||||
|
1. remove and recreate /tmp/cloakserve mode 0700;
|
||||||
|
2. start CloakBrowser, log fixed messages, and wait at most 60 seconds;
|
||||||
|
3. start Stelloauth and wait at most 30 seconds;
|
||||||
|
4. poll both children every 250 ms;
|
||||||
|
5. treat every pre-shutdown child exit as failure, terminate/reap its sibling, and return the child's nonzero code or 1 for exit 0;
|
||||||
|
6. on SIGTERM/SIGINT, signal both groups with SIGTERM, share one ten-second deadline, signal remaining groups with SIGKILL, reap all, and return 0;
|
||||||
|
7. emit only the five approved readiness messages, fixed failure categories, and fixed shutdown states.
|
||||||
|
|
||||||
|
Expose main() and call sys.exit(main()) only beneath the standard module guard. chmod the file 0755.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Prove the manager suite is green and commit**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
chmod 0755 stelloauth/rootfs/usr/local/bin/addon-supervisor
|
||||||
|
.venv/bin/pytest tests/test_supervisor.py -q
|
||||||
|
python3 -m py_compile stelloauth/rootfs/usr/local/bin/addon-supervisor
|
||||||
|
git diff --check
|
||||||
|
git diff -- stelloauth/rootfs tests/test_supervisor.py
|
||||||
|
|
||||||
|
Expected: all tests PASS and sentinel values are absent from captured logs.
|
||||||
|
|
||||||
|
git add stelloauth/rootfs/usr/local/bin/addon-supervisor tests/test_supervisor.py
|
||||||
|
git commit -m "feat: supervise stelloauth and cloakbrowser"
|
||||||
|
|
||||||
|
### Task 4: Pinned image and real runtime validation
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: stelloauth/Dockerfile
|
||||||
|
- Create: tests/test_runtime.sh
|
||||||
|
- Modify: tests/test_addon_metadata.py
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: both patches and the process manager.
|
||||||
|
- Produces: homeassistant-stelloauth-addon:test with only port 8080 exposed and addon-supervisor as its command.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red Dockerfile contract tests**
|
||||||
|
|
||||||
|
Require the Dockerfile to contain all three approved pins, go test ./..., explicit io.hass labels, EXPOSE 8080, ENTRYPOINT [], and CMD ["/usr/local/bin/addon-supervisor"]. Reject EXPOSE 9222, unpinned latest tags, and COPY of a CloakBrowser binary.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_addon_metadata.py -k dockerfile -q
|
||||||
|
|
||||||
|
Expected: FAIL because the Dockerfile is absent.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the two-stage Dockerfile**
|
||||||
|
|
||||||
|
Use this structure:
|
||||||
|
|
||||||
|
# syntax=docker/dockerfile:1.7
|
||||||
|
FROM golang:1.27.1-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195 AS stelloauth-builder
|
||||||
|
|
||||||
|
ARG TARGETARCH
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends ca-certificates git patch \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
WORKDIR /src
|
||||||
|
RUN git clone --filter=blob:none https://github.com/tamcore/stelloauth.git . \
|
||||||
|
&& git checkout --detach 367d4f8c02a3b072c59142c49dffc129edc8548b \
|
||||||
|
&& test "$(git rev-parse HEAD)" = "367d4f8c02a3b072c59142c49dffc129edc8548b"
|
||||||
|
COPY patches/stelloauth-security.patch /tmp/stelloauth-security.patch
|
||||||
|
RUN git apply --check /tmp/stelloauth-security.patch \
|
||||||
|
&& git apply /tmp/stelloauth-security.patch \
|
||||||
|
&& go test ./... \
|
||||||
|
&& CGO_ENABLED=0 GOOS=linux GOARCH="$TARGETARCH" go build -trimpath -ldflags="-s -w" -o /out/stelloauth ./cmd/stelloauth
|
||||||
|
|
||||||
|
FROM cloakhq/cloakbrowser:0.5.10@sha256:2ed5b2d047cbdde22cde7ef1a796526c716aadaa5bccbe1db5ade49282b64a76
|
||||||
|
|
||||||
|
ARG BUILD_ARCH
|
||||||
|
ARG BUILD_DATE
|
||||||
|
ARG BUILD_DESCRIPTION="Local OAuth worker for Stellantis Vehicles using CloakBrowser"
|
||||||
|
ARG BUILD_NAME="Stelloauth"
|
||||||
|
ARG BUILD_REF
|
||||||
|
ARG BUILD_REPOSITORY="https://git.radixadm.dk/dennis/homeassistant-stelloauth-addon"
|
||||||
|
ARG BUILD_VERSION="0.1.0"
|
||||||
|
LABEL io.hass.name="$BUILD_NAME" \
|
||||||
|
io.hass.description="$BUILD_DESCRIPTION" \
|
||||||
|
io.hass.arch="$BUILD_ARCH" \
|
||||||
|
io.hass.type="addon" \
|
||||||
|
io.hass.version="$BUILD_VERSION" \
|
||||||
|
org.opencontainers.image.created="$BUILD_DATE" \
|
||||||
|
org.opencontainers.image.revision="$BUILD_REF" \
|
||||||
|
org.opencontainers.image.source="$BUILD_REPOSITORY" \
|
||||||
|
org.opencontainers.image.version="$BUILD_VERSION"
|
||||||
|
|
||||||
|
USER root
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends patch \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY patches/cloakserve-loopback.patch /tmp/cloakserve-loopback.patch
|
||||||
|
RUN patch --dry-run -p2 -d /usr/local/bin < /tmp/cloakserve-loopback.patch \
|
||||||
|
&& patch -p2 -d /usr/local/bin < /tmp/cloakserve-loopback.patch \
|
||||||
|
&& rm /tmp/cloakserve-loopback.patch \
|
||||||
|
&& apt-get purge -y --auto-remove patch \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
COPY --from=stelloauth-builder /out/stelloauth /usr/local/bin/stelloauth
|
||||||
|
COPY rootfs/ /
|
||||||
|
RUN chmod 0755 /usr/local/bin/stelloauth /usr/local/bin/addon-supervisor /usr/local/bin/cloakserve \
|
||||||
|
&& mkdir -p /data /tmp/cloakserve \
|
||||||
|
&& chmod 0700 /tmp/cloakserve
|
||||||
|
|
||||||
|
EXPOSE 8080
|
||||||
|
ENTRYPOINT []
|
||||||
|
CMD ["/usr/local/bin/addon-supervisor"]
|
||||||
|
|
||||||
|
First dry-run the patch command against an isolated copy of /usr/local/bin/cloakserve. If the generated patch path needs a different strip count, regenerate the patch with labels a/bin/cloakserve and b/bin/cloakserve so -p2 resolves exactly to cloakserve; do not broaden the runtime patch.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Prove static validation and the amd64 build**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_addon_metadata.py -q
|
||||||
|
docker buildx build --check stelloauth
|
||||||
|
docker buildx build --platform linux/amd64 --build-arg BUILD_ARCH=amd64 --load -t homeassistant-stelloauth-addon:test stelloauth
|
||||||
|
docker image inspect homeassistant-stelloauth-addon:test --format '{{.Architecture}} {{json .Config.Entrypoint}} {{json .Config.Cmd}} {{index .Config.Labels "io.hass.type"}} {{index .Config.Labels "io.hass.version"}}'
|
||||||
|
|
||||||
|
Expected: build PASS; inspect prints amd64 [] ["/usr/local/bin/addon-supervisor"] addon 0.1.0.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Write the real runtime test**
|
||||||
|
|
||||||
|
Create executable tests/test_runtime.sh with set -Eeuo pipefail, unique container names, a temporary directory, and a trap that removes all created containers. It must:
|
||||||
|
|
||||||
|
1. build the amd64 image unless SKIP_BUILD=1;
|
||||||
|
2. write only the three default options to temporary options.json;
|
||||||
|
3. run with that file read-only at /data/options.json and map 127.0.0.1 to a random host port for container 8080;
|
||||||
|
4. wait at most 90 seconds for the fixed Stelloauth listening log and HTTP 200;
|
||||||
|
5. parse /proc/net/tcp inside the container and require 0100007F:240E while rejecting 00000000:240E;
|
||||||
|
6. call the real CDP version endpoint with fingerprint runtime-readiness, require webSocketDebuggerUrl, then close the profile;
|
||||||
|
7. POST an invalid worker body containing sentinel email/password/token strings and example.invalid, expecting HTTP 400 without an OAuth flow;
|
||||||
|
8. scan all container logs and fail on any sentinel;
|
||||||
|
9. write docker stats, image size, and docker top output under artifacts/;
|
||||||
|
10. stop within ten seconds, assert exit, start a fresh container, and repeat root/CDP checks;
|
||||||
|
11. assert docker port reports no mapping for 9222.
|
||||||
|
|
||||||
|
Use these exact sentinels:
|
||||||
|
|
||||||
|
sentinel-email@example.invalid
|
||||||
|
SENTINEL_PASSWORD_9a34
|
||||||
|
SENTINEL_COOKIE_7b21
|
||||||
|
SENTINEL_OAUTH_CODE_5c88
|
||||||
|
SENTINEL_ACCESS_TOKEN_1d62
|
||||||
|
SENTINEL_REFRESH_TOKEN_4e73
|
||||||
|
|
||||||
|
Use Python standard-library HTTP and JSON handling inside the script, so host curl and jq are not required.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Run, diagnose one contract at a time, and prove green**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
chmod 0755 tests/test_runtime.sh
|
||||||
|
SKIP_BUILD=1 tests/test_runtime.sh
|
||||||
|
.venv/bin/pytest -q
|
||||||
|
git diff --check
|
||||||
|
|
||||||
|
Expected: real CloakBrowser/CDP and Stelloauth checks PASS, CDP is loopback-only, restart/shutdown passes, and logs contain no sentinel. For any failure, add a focused regression assertion to the owning Python test before the minimal fix and rerun this step.
|
||||||
|
|
||||||
|
- [ ] **Step 6: Verify manifests and arm64 buildability**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
docker buildx imagetools inspect golang:1.27.1-bookworm@sha256:69a7b9788769bec032d238959b61854e9ae87f57be9029ec04e9885fabf99195
|
||||||
|
docker buildx imagetools inspect cloakhq/cloakbrowser:0.5.10@sha256:2ed5b2d047cbdde22cde7ef1a796526c716aadaa5bccbe1db5ade49282b64a76
|
||||||
|
docker buildx inspect --bootstrap
|
||||||
|
|
||||||
|
Expected: both indexes list linux/amd64 and linux/arm64. When the active builder lists linux/arm64, run:
|
||||||
|
|
||||||
|
docker buildx build --platform linux/arm64 --build-arg BUILD_ARCH=aarch64 --load -t homeassistant-stelloauth-addon:arm64-test stelloauth
|
||||||
|
docker image inspect homeassistant-stelloauth-addon:arm64-test --format '{{.Architecture}}'
|
||||||
|
|
||||||
|
Expected with emulation: PASS and arm64. If emulation is absent, record arm64 runtime as skipped while retaining mandatory manifest evidence.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Commit**
|
||||||
|
|
||||||
|
git add stelloauth/Dockerfile tests/test_runtime.sh tests/test_addon_metadata.py
|
||||||
|
git commit -m "test: build and verify addon runtime"
|
||||||
|
|
||||||
|
### Task 5: Installation documentation and Gitea CI
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Create: README.md
|
||||||
|
- Create: LICENSE
|
||||||
|
- Create: stelloauth/README.md
|
||||||
|
- Create: stelloauth/DOCS.md
|
||||||
|
- Create: stelloauth/CHANGELOG.md
|
||||||
|
- Create: .gitea/workflows/ci.yml
|
||||||
|
- Modify: tests/test_addon_metadata.py
|
||||||
|
|
||||||
|
**Interfaces:**
|
||||||
|
- Consumes: exact tested behavior and measurements from Task 4.
|
||||||
|
- Produces: normal Add-on Store steps and a non-publishing Gitea validation workflow.
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add red documentation contract tests**
|
||||||
|
|
||||||
|
Append tests that require:
|
||||||
|
|
||||||
|
- the exact repository URL;
|
||||||
|
- http://0031621f-stelloauth:8080/worker;
|
||||||
|
- temporary fallback http://192.168.1.20:8080/worker and an instruction to disable the mapping;
|
||||||
|
- Brand: Opel and Country: DK;
|
||||||
|
- versions v0.6.0 and 0.5.10;
|
||||||
|
- the phrase CloakBrowser Binary License;
|
||||||
|
- the observed approximately 4 GB free target space;
|
||||||
|
- the exact sentence Login-flow RAM: not measured without real MyOpel credentials.
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest tests/test_addon_metadata.py -k documentation -q
|
||||||
|
|
||||||
|
Expected: FAIL because documentation files are absent.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Write the user and maintenance documentation**
|
||||||
|
|
||||||
|
README.md must include:
|
||||||
|
|
||||||
|
- the one-container/two-process diagram;
|
||||||
|
- why one add-on and a source-only local build were selected;
|
||||||
|
- repository URL and amd64/aarch64 support;
|
||||||
|
- short Add-on Store instructions and a link to stelloauth/DOCS.md;
|
||||||
|
- upstream versions, commits, and OCI digests;
|
||||||
|
- MIT scope for repository-authored work and the separate CloakBrowser Binary License;
|
||||||
|
- no prebuilt image publication and no HAOS installation claim.
|
||||||
|
|
||||||
|
stelloauth/DOCS.md must give these numbered Danish steps:
|
||||||
|
|
||||||
|
1. Add the repository URL under Indstillinger → Tilføjelser → Tilføjelsesbutik → ⋮ → Repositorier.
|
||||||
|
2. Install Stelloauth, enable Start ved opstart and Watchdog, then start.
|
||||||
|
3. Wait for the five fixed readiness messages.
|
||||||
|
4. Keep host port disabled. For troubleshooting only, map 8080, test http://192.168.1.20:8080/, then disable it.
|
||||||
|
5. Enter exactly http://0031621f-stelloauth:8080/worker in Stellantis Vehicles and explain that /worker is not appended.
|
||||||
|
6. Select Brand: Opel and Country: DK and perform OAuth setup.
|
||||||
|
7. Explain all three options and fixed one-session behavior.
|
||||||
|
8. Explain /tmp/cloakserve, 30-second idle cleanup, no credential persistence, loopback CDP, and redacted logs.
|
||||||
|
9. Insert measured idle RAM and image size from artifacts, the approximately 4 GB free-space observation, and the exact unmeasured login-flow sentence.
|
||||||
|
10. Cover readiness timeout, invalid URL 400, rate limit 429, repository-hostname changes, disk pressure, temporary port checking, and Supervisor removal.
|
||||||
|
|
||||||
|
stelloauth/README.md is a short store description linking to DOCS.md. stelloauth/CHANGELOG.md starts at 0.1.0 and names pins, supervision, internal URL, and hardening. LICENSE is standard MIT text for 2026 Dennis / Radix ApS; the docs explicitly preserve CloakBrowser's separate license.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add Gitea CI without publication**
|
||||||
|
|
||||||
|
Create .gitea/workflows/ci.yml:
|
||||||
|
|
||||||
|
name: CI
|
||||||
|
"on":
|
||||||
|
push:
|
||||||
|
branches: [main]
|
||||||
|
pull_request:
|
||||||
|
branches: [main]
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
jobs:
|
||||||
|
validate:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
timeout-minutes: 45
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||||
|
- uses: actions/setup-python@42375524e23c412d93fb67b49958b491fce71c38
|
||||||
|
with:
|
||||||
|
python-version: "3.12"
|
||||||
|
cache: pip
|
||||||
|
- uses: actions/setup-go@0a12ed9d6a96ab950c8f026ed9f722fe0da7ef32
|
||||||
|
with:
|
||||||
|
go-version: "1.27.1"
|
||||||
|
- name: Install test dependencies
|
||||||
|
run: python -m pip install --requirement requirements-dev.txt
|
||||||
|
- name: Validate metadata, patches, and process manager
|
||||||
|
run: pytest -q
|
||||||
|
- name: Build amd64 image
|
||||||
|
run: docker build --build-arg BUILD_ARCH=amd64 --tag homeassistant-stelloauth-addon:test stelloauth
|
||||||
|
- name: Exercise runtime
|
||||||
|
run: SKIP_BUILD=1 tests/test_runtime.sh
|
||||||
|
|
||||||
|
The workflow contains no registry login, push, release, tag, secret reference, or deployment step.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Prove docs and full release candidate**
|
||||||
|
|
||||||
|
Run:
|
||||||
|
|
||||||
|
.venv/bin/pytest -q
|
||||||
|
docker buildx build --platform linux/amd64 --build-arg BUILD_ARCH=amd64 --load -t homeassistant-stelloauth-addon:test stelloauth
|
||||||
|
SKIP_BUILD=1 tests/test_runtime.sh
|
||||||
|
git diff --check
|
||||||
|
git status --short
|
||||||
|
|
||||||
|
Expected: all tests PASS; runtime evidence is current; only Task 5 files are uncommitted.
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
git add README.md LICENSE stelloauth/README.md stelloauth/DOCS.md stelloauth/CHANGELOG.md .gitea/workflows/ci.yml tests/test_addon_metadata.py
|
||||||
|
git commit -m "docs: add installation and maintenance guide"
|
||||||
|
|
||||||
|
## Final Verification Gate
|
||||||
|
|
||||||
|
- [ ] Compare every path with the File Map and approved spec; remove caches and scratch files.
|
||||||
|
- [ ] Run git diff --check 20551e8..HEAD and git diff --stat 20551e8..HEAD.
|
||||||
|
- [ ] Run .venv/bin/pytest -q and retain exact passed/skipped counts.
|
||||||
|
- [ ] Run SKIP_BUILD=1 tests/test_runtime.sh and retain image size, idle memory, listener, restart, and log-scan evidence.
|
||||||
|
- [ ] Inspect the image architecture, command, Home Assistant labels, and exposed-port metadata.
|
||||||
|
- [ ] Run git status --short --branch; expect a clean local main with no upstream tracking branch because the Gitea repository is still empty.
|
||||||
|
- [ ] Run git log -n 8 --oneline; verify logical commits and no merge, tag, release, or push.
|
||||||
|
- [ ] Perform a whole-branch review focused on credentials, URL normalization, CDP exposure, process-group shutdown, patch drift, and CI publication risk. Resolve each concrete finding with a focused test and commit.
|
||||||
|
- [ ] Stop before git push. Report architecture, all files, exact test evidence, measurements, limitations, installation steps, and http://0031621f-stelloauth:8080/worker.
|
||||||
Reference in New Issue
Block a user