Reconciles main with fixes that existed only on the stale master branch, and repairs the CI pipeline which has been unparseable (never running) since introduction.
security — removes hardcoded abiba-bot HTTP-basic credentials embedded in ci.yml (2x, via cherry-pick of aeb79c6 from master) and finishes the job in deploy.yml (4x more, same pattern: runner already checks out the repo, manual clone was redundant). After this PR: zero embedded credentials in tracked workflow files. (History still contains them — password rotation is a separate server-side task, called out in CI_STATUS.md.)
fix(zulip): _truncate overflow — cherry-pick of 19c52a9 from master: truncated messages were 10000 + len(notice) = 10,029 chars, rejected by the Zulip API on every long send. Now reserves notice space. Proven by test below.
fix(zulip): @all-bots fallback 1 → 20 — realm user_id 1 never existed; when dynamic resolution fails, @all-bots mentions were silently dropped. 20 verified live on this node (Resolved @all-bots user_id=20 from all-bots@chat.sysloggh.net, 2026-09-25) and matches the June contract verification which fixed the Pi config to 20 for the same reason.
fix(ci): the validate job can now actually run — ci.yml has been invalid YAML the whole time (inline run: | blocks dedent out of their block scalar; Gitea never parsed it, so CI never ran despite CI_STATUS.md claiming "Active"). Validation logic moved to ci_check.py (runnable locally), secrets check now fails instead of || echo-swallowing hits (and covers .yml/.cjs/.sh, which is where the credentials actually lived), workflow-YAML parse gate added, py_compile steps no longer swallow errors. Also removes ci.yml's duplicate deploy job — deploy.yml is the sole pipeline; the ci.yml copy would have deployed Mumuni on rc tags, breaking the canary policy (and never ran anyway).
Type
fix — bug fix (PATCH version bump)
refactor — CI repair (no adapter behavior change beyond fixes above)
Config schema validated against config.yaml.example (ci_check.py config ✅)
Plugin starts and connects to Zulip on affected CT(s) — live adapter unchanged by this PR (byte-identical to main until deploy tag; main's connect already resolves @all-bots dynamically)
Health endpoint returns 200 — unchanged code path
@mention detection works — tested: fallback value now equals the dynamically-resolved real value (20), so behavior is identical when resolution succeeds, correct when it fails
No API keys or secrets in code — proven by new scanner: negative-tested against old main, it catches all 6 leaks; on this branch: 0 hits
ADRs referenced if decision changed — ADR-006/ADR-012 honored (@all-bots semantics unchanged; only the fallback constant corrected)
Testing performed (all real, on kagentz 2026-09-25)
$ python3 ci_check.py all
✅ .gitea/workflows/ci.yml valid — jobs: ['validate']
✅ .gitea/workflows/deploy.yml valid — jobs: ['validate','deploy-tanko','deploy-hermes','deploy-agent-zero']
✅ config.yaml.example valid
✅ No embedded credentials in tracked code/workflow files
$ python3 -m py_compile plugins/platforms/zulip/adapter.py agent-zero-zulip/.../adapter.py → OK
$ bash -n scripts/deploy.sh → OK
# _truncate: old vs new
old: len=10031 → EXCEEDS 10000 limit (API would reject)
new: len=10000 → within limit, ends with notice
# negative test (git worktree of OLD main + new scanner)
❌ Embedded credentials detected: ci.yml:20, ci.yml:49, deploy.yml:24/40/69/100 (exit 1)
❌ ci.yml: could not find expected ':' (line 30) — proves scanner+parse-gate catch the exact regressions
Notes for reviewer
This PR brings forward aeb79c6 + 19c52a9 from master (cherry-picked -x, provenance recorded). Remaining master-only commits touch the Pi extension / Abiba's lane — deliberately NOT merged here; she should review/sync her own extension from this repo after merge. Recommend retiring the master branch after this PR to end the dual-branch drift (branch master is 8 ahead / 11 behind — dangerous ambiguity for agents cloning "the repo").
## Description
Reconciles `main` with fixes that existed only on the stale `master` branch, and repairs the CI pipeline which has been unparseable (never running) since introduction.
1. **security** — removes hardcoded `abiba-bot` HTTP-basic credentials embedded in `ci.yml` (2x, via cherry-pick of `aeb79c6` from master) and finishes the job in `deploy.yml` (4x more, same pattern: runner already checks out the repo, manual clone was redundant). After this PR: zero embedded credentials in tracked workflow files. (History still contains them — password rotation is a separate server-side task, called out in CI_STATUS.md.)
2. **fix(zulip): `_truncate` overflow** — cherry-pick of `19c52a9` from master: truncated messages were `10000 + len(notice)` = 10,029 chars, rejected by the Zulip API on every long send. Now reserves notice space. Proven by test below.
3. **fix(zulip): @all-bots fallback `1` → `20`** — realm user_id 1 never existed; when dynamic resolution fails, @all-bots mentions were silently dropped. `20` verified live on this node (`Resolved @all-bots user_id=20 from all-bots@chat.sysloggh.net`, 2026-09-25) and matches the June contract verification which fixed the Pi config to 20 for the same reason.
4. **fix(ci): the validate job can now actually run** — `ci.yml` has been invalid YAML the whole time (inline `run: |` blocks dedent out of their block scalar; Gitea never parsed it, so CI never ran despite `CI_STATUS.md` claiming "Active"). Validation logic moved to `ci_check.py` (runnable locally), secrets check now **fails** instead of `|| echo`-swallowing hits (and covers `.yml`/`.cjs`/`.sh`, which is where the credentials actually lived), workflow-YAML parse gate added, py_compile steps no longer swallow errors. Also removes `ci.yml`'s duplicate `deploy` job — `deploy.yml` is the sole pipeline; the ci.yml copy would have deployed Mumuni on rc tags, breaking the canary policy (and never ran anyway).
## Type
- [x] fix — bug fix (PATCH version bump)
- [x] refactor — CI repair (no adapter behavior change beyond fixes above)
## Platform(s) Affected
- [x] Hermes Python (Tanko, Mumuni, Koonimo, Koby) — adapter truncate + @all-bots fallback
- [x] pi TypeScript (Abiba) — credentials removed were hers; no Pi code touched
- [x] Shared infrastructure (CI workflows, ci_check.py, docs)
## Pre-Merge Checklist
- [x] Config schema validated against `config.yaml.example` (ci_check.py config ✅)
- [x] Plugin starts and connects to Zulip on affected CT(s) — live adapter unchanged by this PR (byte-identical to main until deploy tag; main's connect already resolves @all-bots dynamically)
- [x] Health endpoint returns 200 — unchanged code path
- [x] @mention detection works — tested: fallback value now equals the dynamically-resolved real value (20), so behavior is identical when resolution succeeds, correct when it fails
- [x] Error handling: timeout produces graceful message — untouched
- [x] No API keys or secrets in code — proven by new scanner: **negative-tested against old main, it catches all 6 leaks; on this branch: 0 hits**
- [x] ADRs referenced if decision changed — ADR-006/ADR-012 honored (@all-bots semantics unchanged; only the fallback constant corrected)
## Testing performed (all real, on kagentz 2026-09-25)
```
$ python3 ci_check.py all
✅ .gitea/workflows/ci.yml valid — jobs: ['validate']
✅ .gitea/workflows/deploy.yml valid — jobs: ['validate','deploy-tanko','deploy-hermes','deploy-agent-zero']
✅ config.yaml.example valid
✅ No embedded credentials in tracked code/workflow files
$ python3 -m py_compile plugins/platforms/zulip/adapter.py agent-zero-zulip/.../adapter.py → OK
$ bash -n scripts/deploy.sh → OK
# _truncate: old vs new
old: len=10031 → EXCEEDS 10000 limit (API would reject)
new: len=10000 → within limit, ends with notice
# negative test (git worktree of OLD main + new scanner)
❌ Embedded credentials detected: ci.yml:20, ci.yml:49, deploy.yml:24/40/69/100 (exit 1)
❌ ci.yml: could not find expected ':' (line 30) — proves scanner+parse-gate catch the exact regressions
```
## Notes for reviewer
- This PR brings forward `aeb79c6` + `19c52a9` from `master` (cherry-picked `-x`, provenance recorded). Remaining master-only commits touch the **Pi extension / Abiba's lane** — deliberately NOT merged here; she should review/sync her own extension from this repo after merge. Recommend retiring the `master` branch after this PR to end the dual-branch drift (branch `master` is 8 ahead / 11 behind — dangerous ambiguity for agents cloning "the repo").
Replaced manual 'git clone' with password in URL with actions/checkout@v4.
Runner already auto-checkouts the repo - manual clone was redundant.
Also fixed YAML syntax issues in Config validation and No secrets check steps.
Credentials were exposed in git history since initial commit.
(cherry picked from commit aeb79c6286)
The truncation notice '[...truncated at Zulip limit]' was appended AFTER
slicing at MAX_ZULIP_MESSAGE (10000), causing the final message to exceed
Zulip's API limit. This fix subtracts the notice length from the slice so
the total stays within bounds.
(cherry picked from commit 19c52a9425)
Completes aeb79c6 (main): four more clone steps in deploy.yml still
embedded abiba-bot HTTP Basic credentials in plaintext. Runner already
auto-checkouts the repo, so the manual clone was redundant — replaced
with actions/checkout@v4, same pattern as ci.yml.
No secrets remain in tracked workflow files after this change.
Dynamic resolution (ADR-006) overrides this on connect, but when the
/api/v1/users call fails the adapter fell back to 1, silently dropping
every @all-bots mention. The realm's all-bots user is 20 (verified
2026-09-25: 'Resolved @all-bots user_id=20 from all-bots@chat.sysloggh.net';
CONTRACT_VERIFICATION_2026-06-29 fixed the Pi config to 20 for the same
reason). Align the Hermes-side fallback with the verified realm value.
ci.yml has been invalid YAML since introduction: the 'Config validation'
and old inline checks dedented out of their run:| block scalar, so Gitea
could never parse the workflow — CI never ran on any PR despite
CI_STATUS.md claiming 'Active'. The old 'No secrets check' also always
passed (|| echo swallows the grep hit) and never scanned *.yml — where
six embedded credentials were living.
- validation logic moved to ci_check.py (testable locally: python3 ci_check.py all)
- secrets check now FAILS on embedded http-basic URLs and long api_keys,
across .py/.ts/.yaml/.yml/.cjs/.sh, with placeholder allowlist
- added workflow-YAML parse gate so this class of breakage can't recur
- py_compile steps no longer swallow errors with '|| echo skipped'
- removed ci.yml's duplicate deploy job: deploy.yml is the sole deploy
pipeline (rc tags → Tanko canary only; stable → all agents). The ci.yml
copy would have deployed Mumuni on rc tags too, breaking canary policy,
and never ran anyway.
- CI_STATUS.md rewritten with the real state + caveats (history still
contains the old creds — rotation is a server-side task)
Runner probe on PR #35 (run 461, log job 1979): checkout@v4 succeeds (network
fine) but the act container has node:20 + git 2.52 and NO python3 — 'python3
--version' exited 127, which was the failing check. Every validation step is
python3-based, so make step 2 self-provisioning via apt when missing.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Run 467 log (job 1985): 'python3 missing — installing on alpine' then
'apt-get: command not found' → exit 127. The act_runner container is
node:20-alpine-ish; the provisioning step now detects apk first, apt
second, and fails loudly with a diagnostic if neither exists.
CI is green✅ (run 468, commit 52c41ca — first real CI run this repo has ever had).
Two follow-up commits beyond the 5 in the description:
f477a6a — provision python3 in the validate job (runner image lacked it → exit 127, the originally reported failure)
52c41ca — provision via apk: the act container is alpine, not debian (run 467: apt-get: command not found)
Validate job log confirms all checks executed: adapter.py OK · a2a adapter OK · both workflows parse · config valid · 0 embedded credentials · deploy.sh syntax OK.
Probe PR #36 (diagnostic) closed and branch deleted.
**CI is green** ✅ (run 468, commit 52c41ca — first real CI run this repo has ever had).
Two follow-up commits beyond the 5 in the description:
- `f477a6a` — provision python3 in the validate job (runner image lacked it → exit 127, the originally reported failure)
- `52c41ca` — provision via **apk**: the act container is alpine, not debian (run 467: `apt-get: command not found`)
Validate job log confirms all checks executed: adapter.py OK · a2a adapter OK · both workflows parse · config valid · **0 embedded credentials** · deploy.sh syntax OK.
Probe PR #36 (diagnostic) closed and branch deleted.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Description
Reconciles
mainwith fixes that existed only on the stalemasterbranch, and repairs the CI pipeline which has been unparseable (never running) since introduction.abiba-botHTTP-basic credentials embedded inci.yml(2x, via cherry-pick ofaeb79c6from master) and finishes the job indeploy.yml(4x more, same pattern: runner already checks out the repo, manual clone was redundant). After this PR: zero embedded credentials in tracked workflow files. (History still contains them — password rotation is a separate server-side task, called out in CI_STATUS.md.)_truncateoverflow — cherry-pick of19c52a9from master: truncated messages were10000 + len(notice)= 10,029 chars, rejected by the Zulip API on every long send. Now reserves notice space. Proven by test below.1→20— realm user_id 1 never existed; when dynamic resolution fails, @all-bots mentions were silently dropped.20verified live on this node (Resolved @all-bots user_id=20 from all-bots@chat.sysloggh.net, 2026-09-25) and matches the June contract verification which fixed the Pi config to 20 for the same reason.ci.ymlhas been invalid YAML the whole time (inlinerun: |blocks dedent out of their block scalar; Gitea never parsed it, so CI never ran despiteCI_STATUS.mdclaiming "Active"). Validation logic moved toci_check.py(runnable locally), secrets check now fails instead of|| echo-swallowing hits (and covers.yml/.cjs/.sh, which is where the credentials actually lived), workflow-YAML parse gate added, py_compile steps no longer swallow errors. Also removesci.yml's duplicatedeployjob —deploy.ymlis the sole pipeline; the ci.yml copy would have deployed Mumuni on rc tags, breaking the canary policy (and never ran anyway).Type
Platform(s) Affected
Pre-Merge Checklist
config.yaml.example(ci_check.py config ✅)Testing performed (all real, on kagentz 2026-09-25)
Notes for reviewer
aeb79c6+19c52a9frommaster(cherry-picked-x, provenance recorded). Remaining master-only commits touch the Pi extension / Abiba's lane — deliberately NOT merged here; she should review/sync her own extension from this repo after merge. Recommend retiring themasterbranch after this PR to end the dual-branch drift (branchmasteris 8 ahead / 11 behind — dangerous ambiguity for agents cloning "the repo").CI is green ✅ (run 468, commit
52c41ca— first real CI run this repo has ever had).Two follow-up commits beyond the 5 in the description:
f477a6a— provision python3 in the validate job (runner image lacked it → exit 127, the originally reported failure)52c41ca— provision via apk: the act container is alpine, not debian (run 467:apt-get: command not found)Validate job log confirms all checks executed: adapter.py OK · a2a adapter OK · both workflows parse · config valid · 0 embedded credentials · deploy.sh syntax OK.
Probe PR #36 (diagnostic) closed and branch deleted.