- metablog-webhook-install.sh: pages through GITEA_URL/users/<owner>/repos,
filters to metablog-*, checks existing hooks for the target URL,
POSTs a new hook with content_type=json + secret + events=[push].
Idempotent.
- metablog-webhook-uninstall.sh: reverse, DELETE on matching hook IDs.
- Both support --dry-run.
Exit non-zero if any operation failed.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Same bug pattern as 34a4760e but on the source-helpers line.
Both empty-array restores now use "${var[@]}" without :-.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
set -- "${saved_args[@]:-}" produces a single empty positional
argument when the array is empty, which the case-statement then
sees as Unknown flag "". Use "${saved_args[@]}" (no :-) which
gives zero positional args correctly on empty.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After excluding dot-dirs, __pycache__ still appeared as an app
candidate. Add explicit !-name '__pycache__' to the find filter.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
/srv/streamlit/apps/.claude exists (Claude tracking artifacts:
HISTORY.md, TODO.md, WIP.md) and was being picked up as an app
candidate. Filter out dot-prefixed directory names so only real
Streamlit app directories are considered.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gitea CLI in 1.22 lacks 'user keys add', so enrolment goes:
generate-access-token (--config app.ini) -> POST /api/v1/user/keys via
LXC loopback -> delete token via sqlite3 (token list/delete API requires
basic auth in 1.22, not token auth).
Key finding: Gitea builtin SSH server (START_SSH_SERVER=true) rejects
the conventional "git@" username — the connecting username must match
the OS user Gitea runs as ("gitea" here). GITEA_SSH_USER var controls
this, defaulting to "gitea".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
URL uses gitea@ (Gitea built-in SSH server validates against the
OS user 'gitea', not 'git'). Mirrors scripts/lib/metablog-ingest-site.sh
from sub-project B (PR #97) with the streamlit- prefix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
URL uses gitea@ (Gitea built-in SSH server validates against the
OS user it runs as, which is 'gitea' in this LXC, not 'git').
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
gitea CLI in 1.22 lacks 'user keys add', so enrolment goes:
generate-access-token (--config app.ini) -> POST /api/v1/user/keys via
LXC loopback -> delete token via sqlite3 (token list/delete API requires
basic auth in 1.22, not token auth).
Key finding: Gitea builtin SSH server (START_SSH_SERVER=true) rejects
the conventional "git@" username — the connecting username must match
the OS user Gitea runs as ("gitea" here). GITEA_SSH_USER var controls
this, defaulting to "gitea".
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The section-header rule flushed missing keys but did not set
saw_push/saw_branch to 1, so the END block then appended a second
[repository] block at EOF. Set the flags inline with the flush.
Also cleaned the live MOCHAbin app.ini which had been doubly
patched by the original buggy version.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- app.ini at /var/lib/gitea/custom/conf/app.ini (not /etc/gitea/)
- awk-based INI patcher (python3 not installed in gitea LXC)
- ENABLE_PUSH_CREATE_USER=true, DEFAULT_BRANCH=main applied
- Gitea restarted and confirmed active
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two bug fixes to the CMSD-1.0 license-header tool:
1. detect_existing() previously matched the SPDX token anywhere in the
first 10 lines, including inside docstrings and prose comments.
Tightened the matcher to require comment markers (#, //, *, <!--)
and whitespace before the SPDX token.
Regression tests:
- test_detect_existing_no_false_match_in_docstring
- test_detect_existing_no_false_match_inline_comment_prose
2. _read_enrollment() now returns ["**"] when the allowlist file is
absent, per spec §5.2 "missing file → repo-wide enforcement".
Previously it returned [] (nothing enforced), making Phase C closure
impossible without an additional file.
Empty allowlist file still returns [] (Phase A initial), so
test_main_empty_allowlist_passes_check is unaffected.
New tests:
- test_read_enrollment_missing_file_means_repo_wide
- test_main_check_missing_allowlist_enforces_repo_wide
Suite: 49 → 53 passing. --check still exits 0 repo-wide.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
A merge from feature/83 reverted the fix from commit 22014865, which had
already replaced `haproxyctl regen` (non-existent sub-command, printed
help text and exited 0) with the correct `haproxyctl generate`.
This commit re-applies the same fix.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Discovered during Phase B pilot: running `--fix packages/secubox-hub`
yielded zero files because walk() computed `rel` relative to the walk
root (the subdir) while allowlist patterns are repo-relative.
Add optional `repo_root` parameter to walk(); when present, allowlist
matching uses paths relative to it instead of the walk root.
Backwards-compatible (defaults to None, preserving existing tests).
main() passes the discovered repo_root through.
Regression test added: test_walk_subdir_with_repo_root_arg.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After Session 156 fix, secubox-haproxy.service was safely restarted but
cpf.gk2.secubox.in (and other streamlit instances) immediately fell back
to "Wrong Domain" because sync-mitmproxy-routes.sh was clobbering their
container routes.
Two compounding bugs:
1. DEAD_CONTAINER_IPS included 10.100.0.50 — but that's the streamlit
LXC's actual IP (RUNNING). fix_dead_container_routes() rewrote every
route pointing to 10.100.0.50 (cpf:8523, etc.) to 10.100.0.1:9080.
2. The update conditional reapplied routes when the existing port didn't
match the computed port. The computed port logic only knows metablog
vs webui — it has no streamlit awareness, so it always tried to force
streamlit domains to port 9080 (webui default).
Fixes:
- Drop 10.100.0.50 from DEAD_CONTAINER_IPS. The streamlit LXC at that
address is alive; routes pointing to it are valid.
- Change "update if existing missing OR port mismatch" to "update only
if existing is missing." The script now only ADDS routes for ACL/
metablog domains; it never overwrites existing routes set by other
sources (sync-all-routes.sh, manual edits, etc.). fix_dead_container_
routes() still handles truly dead IPs separately.
Verified: cpf.gk2.secubox.in → HTTP 200 "Streamlit" persists across
sync-mitmproxy-routes.service runs. arm/admin still 200 with correct
content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Triggered by cpf.gk2.secubox.in returning "Wrong Domain". Root cause: the
secubox-haproxy generators (both bash haproxyctl and Python FastAPI) emit
config rules that point to dead/wrong backends, leading to HTTPS-wide
metablog/streamlit outages when a regen runs.
Generator fixes:
- packages/secubox-haproxy/api/main.py: use_backend waf_inspector
→ use_backend mitmproxy_inspector (2 occurrences). waf_inspector
pointed to 127.0.0.1:8890 which is not listening; mitmproxy_inspector
is the actual healthy WAF backend at 10.100.0.60:8080.
- packages/secubox-haproxy/sbin/haproxyctl: default_backend fallback
→ default_backend mitmproxy_inspector (2 occurrences). The fallback
backend deny_status 503's unknown hosts; switching to mitmproxy_inspector
lets mitmproxy dispatch all hosts via its routes JSON (245 routes vs
93 declared in haproxy.toml).
New safe regen wrapper:
- scripts/secubox-haproxy-regen-safe: snapshot → regen via haproxyctl →
validate via haproxy -c -f → atomic swap → reload. On validation
failure, current cfg is preserved and the broken candidate is saved
for forensics. Prevents another silent breakage like Session 156.
Board-side state (not in repo): /etc/haproxy/haproxy.cfg patched in
place to mirror these fixes, secubox-haproxy.service stopped until
user confirms safe to re-enable. All 245 routes verified live (cpf,
arm, lldh, admin, pub, werdl, 3d, 42, zkp all HTTP 200 with correct
content).
See .claude/HISTORY.md Session 156 for full symptom chain and the
6-layer root cause analysis.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Addresses code review:
- Important #4: finish now fetches issue title via `gh issue view --json title`
and uses it as PR title (was using the branch name). Falls back to branch
name if the fetch fails or returns empty.
- Minor #8: explicit `return 4` on `git push` failure for consistency with
the documented exit-code contract.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Also patches gh-mock to sanitize dashes in branch-derived env var names,
and fixes --head two-arg parsing in the pr view mock handler.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Root cause: log() in sync-mitmproxy-routes.sh wrote to stdout, polluting
$(fix_dead_container_routes ...) capture → corrupted JSON → jq parse
errors → set -e abort → bad routes pushed to mitmproxy container →
mitmproxy restart-loop → HAProxy backend DOWN → HTTP 503 on every
*.gk2.secubox.in metablog vhost (arm, zkp, 3d, ~160 sites).
Fixes:
- log() now writes to stderr (>&2) — preserves stdout for command-sub
- fix_dead_container_routes returns 0 (was returning $fixed count, set -e
killed the caller's assignment)
- sync-all-routes.sh routes metablog domains to port 8900 (was 9080,
which is webui.conf default_server → "Wrong Domain")
- defensive 2>/dev/null || true on jq reads
- fallback to old routes_json on jq write failure (preserves last valid
state instead of crashing)
- flock guard prevents concurrent runs on /run/sync-mitmproxy-routes.lock
Verified: systemctl start sync-mitmproxy-routes.service exits 0/SUCCESS,
244 routes valid in mitmproxy container, all four spot-checked domains
(admin.gk2, arm.gk2, zkp.gk2, 3d.gk2) return HTTP 200 with correct titles.
Note: also disabled duplicate timer secubox-route-sync.timer on the
board (executed the same script, racing on same file). systemd-level
change is on board only, not in repo.
See .claude/HISTORY.md Session 153 for full symptom chain and verification.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
apt-get update may exit non-zero on unsigned/file:// warnings while
the SecuBox repo is correctly discovered. Capture the log first
(via tee, so root-owned chroot output is writeable), then grep
separately so pipefail can't mask a successful discovery.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>