Commit Graph

2578 Commits

Author SHA1 Message Date
5f654e2c2f docs(tor-plan): Task 7 webui must warn about IPv6 leak on routed VPN clients (Task 3 limitation) 2026-07-09 08:44:01 +02:00
12ce912164 feat(tor): auto-discover all on-disk .onion services + .onion-DNS status
- _discover_hidden_services() globs TOR_DATA for every */hostname (not just
  config-listed ones), cross-referencing secubox config for local_port/
  virtual_port/enabled where a matching entry exists. Fail-safe: missing
  TOR_DATA, broken config, or unreadable hostname files degrade to empty
  entries, never raise.
- GET /hidden_services now surfaces every on-disk hidden service via
  _discover_hidden_services(), including orphaned/hand-created ones.
- GET /onion_dns reports {dnsport_up, forward_zone_installed, resolves}:
  dnsport_up via a local-only /proc/net/{tcp,udp}[6] scan (no network I/O,
  never hangs), forward_zone_installed via checking the unbound drop-in,
  resolves via a tightly bounded (1.5s) UDP canary query only attempted when
  both prerequisites hold.
2026-07-09 08:39:49 +02:00
691a5cbad1 docs(tor-plan): Task 9 must-fix — package secubox-exposure-tor-reconcile sbin+unit (Task 5 persist-on-boot) 2026-07-09 08:35:43 +02:00
217628ce71 feat(exposure): emancipate webui .onion (standalone federate-optional) + persist-on-boot reconcile 2026-07-09 08:32:26 +02:00
6b1c76253f fix(toolbox): reject IPv6 VPN selectors (backend is v4-only) — no false-success 2026-07-09 08:26:11 +02:00
cf548517b7 docs(tor-plan): Task 8 also adds reapply-when-armed (edits apply without disarm/arm cycle) 2026-07-09 08:24:58 +02:00
c848b7c65a feat(toolbox): exit-country + Tor-VPN-client + obfs4-bridge API (validated, audited, reconcile-triggered) 2026-07-09 08:17:45 +02:00
6e163614e5 fix(toolbox): Tor-VPN DNS-leak ordering + fail-closed kill-switch + mac-branch guard 2026-07-09 08:07:43 +02:00
805e3e94c5 feat(toolbox): per-client Tor-VPN prerouting chain + tor_vpn_src set population 2026-07-09 07:57:22 +02:00
4ccfe4a331 docs(tor-plan): add Task 8 obfs4 bridges; fold bridge API/webui into Tasks 4/7; bump live-e2e to Task 9 2026-07-09 07:54:51 +02:00
e983520604 docs(tor-spec): add feature 6 — obfs4 bridges (Niveau-1 anti-censorship, entry-side) 2026-07-09 07:53:53 +02:00
5bdf2cd5ef feat(toolbox): global Tor exit-country drop-in (ExitNodes/StrictNodes from state file) 2026-07-09 07:44:17 +02:00
e9f1c0706d docs(tor-plan): Tor UI lives in /toolbox/#tor tab; name toolbox api module (secubox_toolbox/api.py) 2026-07-09 07:41:50 +02:00
132e0a24ab fix(toolbox): update failclosed test for DNSPort 9053 2026-07-09 07:41:22 +02:00
f686d8811b feat(toolbox): move Tor egress DNSPort 5353→9053 (avahi conflict) + Unbound .onion forward-zone 2026-07-09 07:37:14 +02:00
f2ed362015 docs: Tor enhancement Phase-1 implementation plan (8 tasks, SDD) 2026-07-09 07:34:27 +02:00
1d298b622b docs: Tor enhancement design (country-exit, Tor-VPN per-client, emancipate .onion, persist, auto .onion DNS) 2026-07-09 07:28:46 +02:00
75722e2a59 Merge feature/openclaw-lxc-scanner: dedicated-LXC OSINT+nmap scanner (live on gk2)
Some checks are pending
License Headers / check (push) Waiting to run
8-task SDD build + opus final review (READY TO MERGE). LXC-sandboxed scan
toolchain (nmap/dig/whois/curl @ 10.100.0.41), aggregator-safe async-job API
(all def handlers, detached workers — SPOF-proof 6ms), target policy + audit,
XSS-hardened dashboard. Security fixes: leading-dash flag-injection x2,
ports policy/audit bypass, scan-id race, dh_installsystemd auto-enable.
2026-07-09 07:19:56 +02:00
db9841071a docs(openclaw): mark module implemented (LXC scanner live on gk2)
Task 8 verified the full 1.0.1 deb builds/installs cleanly on gk2 over
the already-provisioned openclaw LXC, the aggregator sudoers grant
works, and a real scan does not block the shared aggregator loop
(concurrent cookies/status returned in 6ms during an active scan).
Dashboard renders wired markup at /openclaw/. Board recovered fully
after the single required aggregator restart.
2026-07-09 07:06:56 +02:00
bf22964ee1 feat(openclaw): wire + XSS-harden the scanner dashboard 2026-07-09 06:56:56 +02:00
2fe92754ea fix(openclaw): dh_installsystemd --no-enable --no-start (aggregator serves in-process) 2026-07-09 06:41:43 +02:00
cc61eb9c42 feat(openclaw): packaging — sudoers+visudo, install openclawctl+toml, deps debootstrap/lxc/jq 2026-07-09 06:35:11 +02:00
f4468484f1 fix(openclaw): gate+audit ports quick-lookup; per-request scan-id (no shared-file race) 2026-07-09 06:30:47 +02:00
ad5fc2c638 feat(openclaw): async-job scan endpoints + target policy/audit + sync lookups; drop async machinery 2026-07-09 06:23:39 +02:00
3dee901d92 fix(openclaw): remove disconnected legacy POST /config + unused imports 2026-07-09 06:21:10 +02:00
5e81a25e12 feat(openclaw): API core — ctl routing, def+cached status, target classification 2026-07-09 06:13:53 +02:00
6b0151f788 fix(openclaw): close email-arm flag-injection (re-validate post-@ domain) 2026-07-09 06:03:36 +02:00
87a345a530 fix(openclaw): reject leading-dash targets (flag-injection) + failure-record schema 2026-07-09 05:56:57 +02:00
fe10cf5dcd docs(openclaw): tighten target regex in plan (reject leading-dash flag-injection) 2026-07-09 05:55:37 +02:00
d57b5decf8 feat(openclaw): openclawctl scan — run tools in-container, write host-side result JSON 2026-07-09 05:47:52 +02:00
1220e59147 feat(openclaw): openclawctl install (debootstrap sandbox + nmap/dig/whois/curl toolchain) 2026-07-09 05:42:46 +02:00
81bc034895 feat(openclaw): openclawctl control-plane core (guards, status --json, start/stop, selftest) 2026-07-09 05:20:44 +02:00
32a2907201 docs: openclaw LXC scanner implementation plan (8 tasks, SDD) 2026-07-09 05:16:55 +02:00
eb15e8d2ac docs: openclaw spec — correct LXC path (/data/lxc) + free IP (.41, .30 taken by matrix) 2026-07-09 05:11:56 +02:00
ad3b6f52d4 release(threat-analyst): remove Endpoint status panel 2026-07-09 05:10:26 +02:00
a670cda9e6 docs: openclaw dedicated-LXC OSINT+scanner design 2026-07-09 05:08:47 +02:00
87b2ab9dd8 feat(threat-analyst): remove Endpoint status diagnostic panel from webui 2026-07-09 04:58:28 +02:00
d041f12a05 docs: c3box kernel migration design (6.12.85 + one-shot boot guard)
Some checks are pending
License Headers / check (push) Waiting to run
2026-07-08 13:15:31 +02:00
372dc677c3 docs: plan addendum — real /storage via nextcloudctl (fold b715 #429) (ref #429) 2026-07-08 07:22:10 +02:00
5096b5f015 docs: implementation plan — Nextcloud dashboard real data + manageable users (ref #429) 2026-07-08 07:20:17 +02:00
c1885c9646 docs: spec — Nextcloud dashboard real data + manageable users (ref #429) 2026-07-08 07:13:39 +02:00
CyberMind
4f11e03285
feat(sentinel): plumb JA4 into the pipeline — makes non_browser_ja live (ref #826) (#831)
Some checks failed
License Headers / check (push) Has been cancelled
* feat(sbxmitm): plumb SNI-independent client-stack JA4 into the Sentinel FlowMeta — makes non_browser_ja live (ref #826)

* docs(sbxmitm): clarify ja4stack is ad-hoc (not spec JA4), self-consistent with capture (ref #826)

* release: toolbox-ng 0.1.34 — JA4 plumbing (ref #826)

* fix(sbxmitm): exclude GREASE from ja4stack (RFC 8701) so the fingerprint is stable per client (ref #826)

* release: toolbox-ng 0.1.35 — GREASE-stable ja4stack (ref #826)

* feat(sentinel): browser-ja4.txt as an /etc conffile (operator-maintained, upgrade-safe) + capture-based population docs (ref #826)

* release: toolbox-ng 0.1.36 — browser-ja4 /etc conffile (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:50:55 +02:00
CyberMind
fe4b12bf64
fix(sentinel): correct MVT/Pegasus feed URL + env-gated JA4 capture infra (ref #826) (#830)
Some checks are pending
License Headers / check (push) Waiting to run
* feat(sentinel): env-gated JA4 capture (bootstrap browser-ja4 from live traffic) + fix MVT feed URL (Amnesty repo) (ref #826)

* release: toolbox-ng 0.1.33 — MVT feed URL fix + JA4 capture (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:28:39 +02:00
CyberMind
72c57ccfac
feat(sentinel): report-only surfacing of non-spyware feed IOCs (botnet_c2/malware) — ref #826 (#829)
* feat(sentinel): report-only IOC reporter — surface non-spyware feed detections (botnet_c2/malware) as observed, never blocked (ref #826)

* release: toolbox-ng 0.1.32 — report-only feed surfacing (ref #826)

* docs(sentinel): iocreport MatchIP uses Host (destination), not ClientIP (source) — false-positive vector + IP test (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 12:14:37 +02:00
CyberMind
f19d566793
fix(sentinel): C2 auto-learn false positive — require a strong signal (ref #826) (#828)
Some checks are pending
License Headers / check (push) Waiting to run
* release: toolbox-ng 0.1.30 + toolbox 2.8.2 — Sentinel C2 auto-learning (ref #826)

* fix(sentinel): C2 false-positive — require strong signal (dga/non-browser), gate non_browser_ja on configured browser set (ref #826)

* release: toolbox-ng 0.1.31 — C2 false-positive fix (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 08:13:48 +02:00
0008c89e58 docs: track Sentinel engine + activation + 3 surfaces + C2 auto-learning (#821 #823-#828) 2026-07-07 08:09:59 +02:00
1b42406a33 release: toolbox-ng 0.1.30 + toolbox 2.8.2 — Sentinel C2 auto-learning (ref #826) 2026-07-07 07:46:51 +02:00
CyberMind
62e8631f2b
feat(sentinel): C2/botnet auto-learning — high-precision, report-only (Closes #826) (#827)
* docs: spec — Sentinel C2/botnet auto-learning (high-precision, report-only) (ref #823)

* docs: implementation plan — Sentinel C2/botnet auto-learning (ref #823)

* feat(sentinel): C2 autolearn false-positive allowlist gate (ref #826)

* fix(sentinel): serialize c2allow Add + robust atomicWriteFile (relative-path + temp cleanup) (ref #826)

* feat(sentinel): C2 autolearn corroborating signals (rare/non-browser-JA/DGA) (ref #826)

* feat(sentinel): C2 autolearn candidate lifecycle + persistence (ref #826)

* fix(sentinel): c2cand deep-copy snapshots (persist/snapshot/record race) + evict order + device cap (ref #826)

* feat(sentinel): C2 autolearn orchestrator — learned set + report-only verdicts (ref #826)

* feat(sentinel): wire C2 learner into daemon + /c2 endpoints + seeded allowlist (ref #826)

* fix(sentinel): make /c2/allow writable in packaged deploy (RW path + ownership) + sanitize Add (ref #826)

POST /c2/allow could never write in production: sbx-sentinel.service runs
User=secubox-toolbox under ProtectSystem=strict + ReadOnlyPaths=/etc/secubox,
and the seeded c2-allow.txt ships root:root. Fix both halves: nest
ReadWritePaths=/etc/secubox/sentinel under the existing ReadOnlyPaths=/etc/secubox
in the unit, and chown/chmod 0750 the sentinel/ subdir + file (never the shared
/etc/secubox parent) in postinst's configure branch, fail-safe.

Also: fix the /c2/allow doc comment (form/query only, not JSON — r.FormValue
never parses a body), and sanitize C2Allow.Add against newline injection now
that host is network-reachable.

* feat(toolbox): WebUI C2 appris sub-view + Ignorer allowlist proxy (ref #826)

* fix(toolbox): C2 view XSS (data-host not onclick) + normalize candidate signal maps (ref #826)

* fix(sentinel): bound c2 beaconing/learned maps + drop promoted from candidates + reject bare-TLD allow (ref #826)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
2026-07-07 07:46:19 +02:00
CyberMind
916cb91b72
feat(toolbox): activate Sentinel + surface compromise detections (WebUI tab · kbin report · PDF) — ref #823 (#825)
Some checks are pending
License Headers / check (push) Waiting to run
* feat(toolbox): arcade-HUD transparency banner redesign + responsive close-X fix

Redesign both MITM transparency-banner renderers into a C3BOX arcade-HUD
"scoreboard" skin and fix the phone bug where the close X dropped off-screen.

Structural responsive fix (both renderers): three flex clusters — LEFT rank
pinned (flex:0 0 auto), MIDDLE evidence flex:1 1 auto;min-width:0;overflow-x:auto
(scrolls instead of pushing), RIGHT report+close pinned (flex:0 0 auto) so the
close X is always visible; box-sizing:border-box added.

bundle.py _BANNER_CORE (primary R3 renderer): still built entirely via the mk()
DOM API (no innerHTML — Trusted-Types/strict-CSP invariant kept). Tier-accent
palette (r0 gold..r4 pink) drives border/glow/rank chip/level switch; evidence +
Tor/Ad-Guard toggles + live sbx-trk/sbx-ck counts become neon pills; one scoped
prefers-reduced-motion <style> for sheen/hover. All ids, toggles, csp proof,
un-hideable styles, SPA re-assert preserved.

inject_banner.py _banner_html_dynamic (secondary addon renderer): same look +
three-cluster fix; keeps grade shield + geo pills, _LEVEL_THEME, _GUARD, NCR
ascii-encodability, gondwana-mitm-banner id, JS-path close moved to right cluster.

Tests updated to the new structure; invariants still asserted (no innerHTML,
ids, toggles, ascii-encodability, dismiss control in right cluster).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(nginx): revalidate luci-static assets (Cache-Control no-cache) so webui updates aren't cached stale

* docs: spec — activate Sentinel + surface on WebUI tab, kbin report, PDF (ref #823)

* docs: implementation plan — Sentinel activation + 3 ToolBoX surfaces (ref #823)

* feat(sentinel): add mac filter to /verdicts status endpoint (ref #823)

* feat(toolbox): sentinel_link — fail-safe daemon fetch + compromise assess (ref #823)

* fix(toolbox): harden sentinel_link int coercions to never raise (ref #823)

* feat(toolbox): /admin/sentinel/{stats,verdicts} fleet proxy routes (ref #823)

* fix(toolbox): guard /admin/sentinel/stats against non-numeric daemon values (ref #823)

* feat(toolbox): WebUI Sentinelle fleet tab (evaluation + detections) (ref #823)

* feat(toolbox): fold per-device Sentinel detections into build_report_data (ref #823)

* feat(toolbox): kbin report per-device Compromission tab (ref #823)

* feat(toolbox): wire per-device Sentinel data into live HTML report render (ref #823)

* fix(toolbox): report-live tab must render when report var is absent (ref #823)

* feat(toolbox): PDF + text-fallback Sentinel compromise section (ref #823)

* docs: GPT demo prompt for kbin Sentinel augmentation (ref #823)

* fix(toolbox): sentinel never-raise on non-dict daemon rows + guard ts render (ref #823)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 17:39:42 +02:00
CyberMind
afd8f6eb83
feat(sentinel): sbxmitm Sentinel — inline+async exploit/malware/spyware detection engine (#823) (#824)
Some checks are pending
License Headers / check (push) Waiting to run
* docs(sentinel): design — inline IOC gate + async Go analyzer + spyware packs (ref #823)

* docs(sentinel): implementation plan — 12 tasks (ref #823)

* feat(sentinel): IOC model + IOCSet matchers (ref #823)

* feat(sentinel): pack loader + live-overlay merge + hot-reload (ref #823)

Add internal/sentinel/pack.go: Pack (JSON base/overlay content bundle),
LoadBasePack, MergePacks (base + N overlays, override by Type+Value via
IOCSet's per-type maps), and Loader (NewLoader/Set/MaybeReload/YaraRules).

Loader mirrors cmd/sbxmitm/policy.go's LoadPolicy/maybeReload mtime-based
hot-reload pattern, adapted for a directory of a DYNAMIC *.json file set
(glob-based) rather than policy.go's fixed Target list. Fail-safe: a
corrupt overlay pack is skipped+logged, never wipes the base or panics;
MaybeReload is a cheap stat-only no-op when nothing on disk changed;
Set()/YaraRules() read via atomic.Pointer so a reload swap never torn-reads
a concurrent hot-path lookup.

Adds JSON tags to Task 1's IOC struct (type/value/class/severity/source/
action) so packs round-trip through encoding/json.

* fix(sentinel): pack loader — fail-closed on base-wipe + throttle MaybeReload + url_regex override + yara dedup (ref #823)

Review found 1 Critical + 2 Important + 1 Minor in Task 2 (pack loader):

- Critical: a vanished/degraded base pack directory (deleted dir, mount
  blip, mid-dpkg-upgrade window) could silently empty the live detection
  set on the next MaybeReload. loaderState now tracks baseCount; a reload
  that would drop base IOCs from >0 to 0 is refused (prior state kept,
  warning logged) instead of swapped in.
- Important: MaybeReload() is called per-flow by Task 3's inline gate: add
  a 5s default throttle (mirrors cmd/sbxmitm/policy.go's reloadThrottle)
  so a hot-path call is a pure no-op until the window elapses.
- Important: url_regex IOCs previously only appended, so a live overlay
  could never override a base pack's URL rule (base always matched
  first). Add urlIndex (pattern -> slice index) so re-adding the same
  pattern replaces the entry in place, keeping deterministic match order.
- Minor: YaraRules() now dedups identical rule bodies across base+overlay
  packs instead of emitting duplicates.

go test ./internal/sentinel/: 21/21 PASS. go vet/gofmt/go build clean.

* feat(sentinel): inline gate matcher (fail-open, highest-severity) (ref #823)

* feat(sentinel): bbolt verdict store (pure-Go, WAL-free, TTL prune) (ref #823)

* feat(sentinel): action scorer — heuristic/low-confidence forced report-only (ref #823)

* feat(sentinel): bounded fire-and-forget mirror channel (drop-with-count) (ref #823)

* fix(sentinel): mirror Emit always copies body — no aliasing of caller's hot-path buffer (ref #823)

* feat(sbxmitm): inline Sentinel gate — neutralize high-confidence, mirror rest, fail-open (ref #823)

Wire internal/sentinel's inline IOC gate into the shared mitmPipeline response
path (covers both the transparent R3 and CONNECT accept paths without drift):
build a FlowMeta from Host/URL/ClientIP/MacHash after up.Do, call the gate, and
on a HIGH-CONFIDENCE known-infra hit neutralize inline — block/sinkhole serve a
Sentinel block page instead of the upstream response, strip serves the response
with an emptied body. Everything else (miss, heuristic, low-confidence, report)
is mirrored bounded fire-and-forget to the async analyzer and proceeds unchanged.

Fail-open everywhere: SENTINEL_ENABLED unset/false, a pack-load failure, a nil
hook, or a gate panic all degrade to a transparent passthrough — a Sentinel
setup problem can never break browsing. With the engine off, behavior is
byte-identical to today. Env: SENTINEL_ENABLED / SENTINEL_PACK_DIR /
SENTINEL_OVERLAY_DIR / SENTINEL_MIRROR_SOCK.

Hot path (BenchmarkSentinelInspectMiss, -benchmem): 101.0 ns/op, 0 B/op,
0 allocs/op — one throttled gate Match + a non-blocking mirror Emit; no YARA or
heavy analysis inline.

go test ./cmd/sbxmitm/... -race → ok (all pre-existing + 7 new green).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(sentinel): sbx-sentinel async daemon scaffold + store wiring (ref #823)

* feat(sentinel): YARA engine (cgo, build-tagged) + no-cgo stub (ref #823)

Add internal/sentinel/yara.go (cgo && yara) wrapping
github.com/hillu/go-yara/v4 for compiled-rule body scanning, and
yara_stub.go (!yara) as the no-op default: NewYaraEngine always
succeeds, Scan/Analyze always return nil. Both expose identical
signatures (YaraEngine, NewYaraEngine, Scan, Analyze, Close) so
sbx-sentinel/sbxmitm can wire a YaraEngine unconditionally.

yara_test.go (untagged) proves the default build works with YARA
disabled. yara_real_test.go (yara tag) compiles a trivial EICAR rule
and exercises the real libyara-backed engine — split into its own
file since Go build constraints are file-scoped and can't mix a
tagged and untagged test in one file.

go-yara is now an indirect go.mod requirement and vendored, but is
only reachable via the yara build tag: default `go build ./...` and
`CGO_ENABLED=0 go build ./...` both stay clean, no libyara/cgo
required. `-tags yara` needs libyara-dev + a C toolchain (documented
in yara.go's header).

* fix(sentinel): YARA verdict confidence 90 (>= threshold) so a confirmed malware match actually strips, not report-only (ref #823)

* feat(sentinel): behavioral engine — beaconing + zero-click heuristics (report-only) (ref #823)

* fix(sentinel): tighten one-time-link heuristic — exclude file-extension + base64 shapes (cut zero-click FPs) (ref #823)

* feat(sentinel): spyware indicator engine + commercial-spyware base pack (MVT/CitizenLab) (ref #823)

* fix(sentinel): base-pack declared-block IOCs severity>=85 so they actually block + general guard test (ref #823)

Three shipped base-pack IOCs declared action:"block" but severity:80,
below scorer.go's HighConfidenceThreshold=85 — FinalizeAction silently
downgrades any Confidence below that threshold to report-only, so these
entries never actually blocked despite being declared as known-bad:

- packs/base/malware.json: ip 198.51.100.23 (class malware)
- packs/base/malware.json: cert_sha1 ...01234567 (class trojan)
- packs/base/botnet.json: ja3 deadbeef... (class botnet_c2)

Bumped all three to severity 90 so they survive FinalizeAction and
actually block, matching every other declared-block entry in the base
packs.

Added TestBasePackDeclaredBlockIsHighSeverity, a general guard over
every base pack file/entry: any IOC with an auto-neutralize action
(block/strip/sinkhole) must carry Severity >= HighConfidenceThreshold.
This generalizes the narrower TestBasePackKnownInfraIsHighSeverity
(spyware classes only) so a future pack entry in any class can't ship
a "block" that silently never blocks. Confirmed the new test fails red
against the pre-fix JSON (all 3 offenders reported) and passes green
after the severity bump.

* feat(sentinel): proposal/solution reporter — RenderReport(Verdict) (ref #823)

* feat(sentinel): wire real analyzers into sbx-sentinel + read-only status HTTP surface (ref #823)

* feat(sentinel): live threat-feed → overlay pack fetcher (fail-safe, atomic) (ref #823)

* feat(sentinel): live feed overlay + reporter + daemon analyzer wiring + packaging (ref #823)

* fix(sentinel): feeds fetcher — add curl --fail so HTTP 4xx/5xx is treated as a fetch failure (ref #823)

* fix(sbxmitm): drop mis-wired ClientIP from inline Sentinel FlowMeta (source IP must not match destination-C2 IP IOCs) (ref #823)

---------

Co-authored-by: CyberMind-FR <gandalf@Gk2.net>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-06 15:53:21 +02:00