From 3db0d7462016cb70eb72390cd0d5bf9f0f7b10bf Mon Sep 17 00:00:00 2001 From: "Abiba (pi)" Date: Mon, 13 Jul 2026 00:29:48 +0000 Subject: [PATCH 1/7] security: remove hardcoded abiba-bot credentials from CI workflow 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 aeb79c628659d72b9cb129f4ae382e29a9829a5a) --- .gitea/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index c4bfaad..483cfbe 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: validate: runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 - run: python3 --version - run: node --version - run: echo "Runner works!" - - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . - name: Python syntax check run: | @@ -45,8 +45,8 @@ print('✅ config.yaml.example valid') runs-on: ubuntu-latest needs: [validate] steps: + - uses: actions/checkout@v4 - run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)" - - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . - name: Deploy to Tanko (canary) run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped" -- 2.54.0 From 2c4136daf88941bdfdf9fd4b263cbe8d269e09b5 Mon Sep 17 00:00:00 2001 From: "Abiba (pi)" Date: Wed, 8 Jul 2026 17:57:14 +0000 Subject: [PATCH 2/7] fix(zulip): account for TRUNCATION_NOTICE overhead in _truncate 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 19c52a9425aa66c891b1ff35408c732a43f1dbf6) --- plugins/platforms/zulip/adapter.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugins/platforms/zulip/adapter.py b/plugins/platforms/zulip/adapter.py index a9c6877..1e25176 100644 --- a/plugins/platforms/zulip/adapter.py +++ b/plugins/platforms/zulip/adapter.py @@ -67,6 +67,7 @@ DEFAULT_STREAM = "agent-hub" DEFAULT_ALL_BOTS_USER_ID = 1 DEFAULT_POLL_INTERVAL = 3.0 MAX_ZULIP_MESSAGE = 10000 +TRUNCATION_NOTICE = "\n\n[...truncated at Zulip limit]" ECHO_TAG_PREFIX = "hermes-zulip-" RECONNECT_BACKOFF = [2, 5, 10, 30, 60] DEDUP_WINDOW = 300 # 5 minutes @@ -120,7 +121,7 @@ def _truncate(text: str, limit: int = MAX_ZULIP_MESSAGE) -> str: """Truncate to Zulip's message limit with notice.""" if len(text) <= limit: return text - return text[:limit] + "\n\n[...truncated at Zulip limit]" + return text[:limit - len(TRUNCATION_NOTICE)] + TRUNCATION_NOTICE def _parse_zulip_timestamp(ts: Any) -> datetime: -- 2.54.0 From f0f82d904b2b67a1418863af5bd699f094560436 Mon Sep 17 00:00:00 2001 From: Mumuni Date: Fri, 25 Sep 2026 15:28:58 +0000 Subject: [PATCH 3/7] security: remove hardcoded abiba-bot credentials from deploy workflow MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitea/workflows/deploy.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index 9014fe0..8f6a425 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -21,7 +21,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . + uses: actions/checkout@v4 - name: Python syntax run: | python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null @@ -37,7 +37,7 @@ jobs: environment: canary steps: - name: Checkout - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . + uses: actions/checkout@v4 - name: Deploy to Tanko env: TAG: ${{ github.ref_name }} @@ -66,7 +66,7 @@ jobs: environment: production steps: - name: Checkout - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . + uses: actions/checkout@v4 - name: Deploy to all Hermes agents env: TAG: ${{ github.ref_name }} @@ -97,7 +97,7 @@ jobs: environment: agent-zero steps: - name: Checkout - run: git clone --depth 1 http://abiba-bot:***REMOVED***@192.168.68.17:3000/SyslogSolution/zulip-platform-plugins.git . + uses: actions/checkout@v4 - name: Deploy to kagentz env: TAG: ${{ github.ref_name }} -- 2.54.0 From fb752be8c9aac7b0eeef99430945bcf8a854ebc7 Mon Sep 17 00:00:00 2001 From: Mumuni Date: Fri, 25 Sep 2026 15:31:28 +0000 Subject: [PATCH 4/7] =?UTF-8?q?fix(zulip):=20@all-bots=20fallback=20defaul?= =?UTF-8?q?t=20was=20user=5Fid=3D1=20=E2=80=94=20never=20valid=20in=20Sysl?= =?UTF-8?q?ogGH=20realm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- plugins/platforms/zulip/adapter.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/platforms/zulip/adapter.py b/plugins/platforms/zulip/adapter.py index 1e25176..f78074f 100644 --- a/plugins/platforms/zulip/adapter.py +++ b/plugins/platforms/zulip/adapter.py @@ -64,7 +64,11 @@ logger = logging.getLogger(__name__) # Constants DEFAULT_STREAM = "agent-hub" -DEFAULT_ALL_BOTS_USER_ID = 1 +# SyslogGH realm: the @all-bots user has user_id=20 (verified via +# /api/v1/users and CONTRACT_VERIFICATION_2026-06-29). Dynamic resolution +# on connect overrides this; this value is the fallback when that fails — +# user_id=1 was never valid in this realm and silently dropped @all-bots. +DEFAULT_ALL_BOTS_USER_ID = 20 DEFAULT_POLL_INTERVAL = 3.0 MAX_ZULIP_MESSAGE = 10000 TRUNCATION_NOTICE = "\n\n[...truncated at Zulip limit]" -- 2.54.0 From 9edc258245dca0a016eb9f6d1d450d494cd14af0 Mon Sep 17 00:00:00 2001 From: Mumuni Date: Fri, 25 Sep 2026 15:50:16 +0000 Subject: [PATCH 5/7] =?UTF-8?q?fix(ci):=20make=20the=20validate=20job=20ac?= =?UTF-8?q?tually=20able=20to=20run=20=E2=80=94=20it=20never=20had?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .gitea/workflows/ci.yml | 33 +++---------- CI_STATUS.md | 27 ++++++++++- ci_check.py | 105 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+), 26 deletions(-) create mode 100644 ci_check.py diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 483cfbe..41a33ee 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -21,35 +21,18 @@ jobs: - name: Python syntax check run: | - python3 -m py_compile plugins/platforms/zulip/adapter.py 2>/dev/null && echo "✅ adapter.py OK" || echo "⚠️ adapter.py check skipped" - python3 -m py_compile agent-zero-zulip/src/agent_zero_zulip/adapter.py 2>/dev/null && echo "✅ a2a adapter OK" || echo "⚠️ a2a check skipped" + python3 -m py_compile plugins/platforms/zulip/adapter.py && echo "✅ adapter.py OK" + python3 -m py_compile agent-zero-zulip/src/agent_zero_zulip/adapter.py && echo "✅ a2a adapter OK" + + - name: Workflow YAML parse check + run: python3 ci_check.py workflows - name: Config validation - run: | - python3 -c " -import yaml -cfg = yaml.safe_load(open('config.yaml.example')) -assert 'zulip' in cfg, 'Missing zulip config' -print('✅ config.yaml.example valid') -" 2>/dev/null || echo "⚠️ Config check skipped (no pyyaml)" + run: python3 ci_check.py config - name: No secrets check - run: | - ! grep -r "api_key.*[A-Za-z0-9]\{20,\}" --include="*.py" --include="*.ts" --include="*.yaml" . 2>/dev/null || echo "⚠️ Possible API key found!" + run: python3 ci_check.py secrets - name: Deploy script syntax - run: bash -n scripts/deploy.sh && echo "✅ deploy.sh syntax OK" || echo "⚠️ deploy.sh syntax issue" + run: bash -n scripts/deploy.sh && echo "✅ deploy.sh syntax OK" - deploy: - if: startsWith(github.ref, 'refs/tags/v') - runs-on: ubuntu-latest - needs: [validate] - steps: - - uses: actions/checkout@v4 - - run: echo "🚀 Deploy tag $(echo $GITHUB_REF_NAME)" - - - name: Deploy to Tanko (canary) - run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 jerome@192.168.68.122 "cd /root && bash -s" < scripts/deploy.sh --ct=tanko --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Tanko deploy skipped" - - - name: Deploy to Mumuni - run: ssh -o StrictHostKeyChecking=no -o ConnectTimeout=10 root@192.168.68.123 "cd /root && bash -s" < scripts/deploy.sh --ct=mumuni --mode=native "$GITHUB_REF_NAME" 2>&1 || echo "⚠️ Mumuni deploy skipped" diff --git a/CI_STATUS.md b/CI_STATUS.md index 3d709d0..1926152 100644 --- a/CI_STATUS.md +++ b/CI_STATUS.md @@ -1,2 +1,27 @@ # CI Pipeline Status -Status: Active + +**Last verified: 2026-09-25** — see PR "main security + truncate + CI repair". + +## Reality check (before that PR) +`ci.yml` was **invalid YAML** — the inline `run: |` blocks for the config and +secrets checks dedented out of their block scalar, so Gitea Actions could never +parse the workflow. CI never ran on any PR, and this file's "Active" status was +fiction. The old "No secrets check" also swallowed hits with `|| echo` (always +green) and never scanned `.yml` files — which is where six embedded +credentials were living. + +## Current pipeline +| Trigger | Job | Checks | +|---|---|---| +| PR → main, push main, tag `v*` | `validate` | adapter + a2a `py_compile`; workflow YAML parse; `config.yaml.example`; secret scan; `bash -n scripts/deploy.sh` | +| tag `v*` | `deploy` | Tanko canary + Mumuni via `scripts/deploy.sh` (native mode) | + +All validation logic lives in **`ci_check.py`** — run `python3 ci_check.py all` +locally before pushing; it fails the check on real hits instead of echoing +warnings. + +## Known caveats +- The removed credentials still exist in git history (pre-July commits) — + rotating `abiba-bot`'s password is a **server-side** task, out of repo scope. +- Runner availability (`zulip-runner` on CT 116) is not verifiable from agents + — the first green run after merge is the proof. diff --git a/ci_check.py b/ci_check.py new file mode 100644 index 0000000..2b6ee2a --- /dev/null +++ b/ci_check.py @@ -0,0 +1,105 @@ +#!/usr/bin/env python3 +"""CI validation checks for zulip-platform-plugins. + +The inline validation steps this script replaced were never valid YAML in the +first place (the `run: |` blocks dedented out of their block scalar), so the +whole `validate` job silently never ran. Keeping the logic in a real file +means it is testable locally — run `python3 ci_check.py all` before pushing. + +Usage: python3 ci_check.py {workflows|config|secrets|all} +""" +import re +import sys +from pathlib import Path + +ROOT = Path(__file__).resolve().parent +CODE_GLOBS = ("*.py", "*.ts", "*.yaml", "*.yml", "*.cjs", "*.sh") +# scheme://user:pass@host — embedded HTTP Basic credentials +CRED_RE = re.compile(r"://[^/ \"']+:[^/ \"'@]+@") +API_KEY_RE = re.compile(r"""api_key["']?\s*[:=]\s*["']?[A-Za-z0-9]{20,}""") +# Allowlist: placeholder patterns, not real secrets +PLACEHOLDER = ("${", "{{", "user:pass", "USER:TOKEN", "user:token", "example") + + +def iter_code_files(): + for path in sorted(ROOT.rglob("*")): + if not path.is_file(): + continue + if any(part in {".git", "__pycache__", "node_modules"} for part in path.parts): + continue + if path.name == Path(__file__).name: # this file documents the patterns + continue + if any(path.match(glob) for glob in CODE_GLOBS): + yield path + + +def check_workflows() -> bool: + try: + import yaml + except ModuleNotFoundError: + print("⚠️ pyyaml not installed — workflow parse check skipped") + return True + ok = True + for f in sorted((ROOT / ".gitea" / "workflows").glob("*.yml")): + try: + doc = yaml.safe_load(f.read_text()) + assert isinstance(doc, dict) and "jobs" in doc, "missing 'jobs' mapping" + print(f"✅ {f.relative_to(ROOT)} valid — jobs: {list(doc['jobs'])}") + except Exception as e: + print(f"❌ {f.relative_to(ROOT)}: {e}") + ok = False + return ok + + +def check_config() -> bool: + try: + import yaml + except ModuleNotFoundError: + print("⚠️ pyyaml not installed — config check skipped") + return True + try: + cfg = yaml.safe_load((ROOT / "config.yaml.example").read_text()) + assert isinstance(cfg, dict) and "zulip" in cfg, "missing 'zulip' section" + print("✅ config.yaml.example valid") + return True + except Exception as e: + print(f"❌ config.yaml.example: {e}") + return False + + +def check_secrets() -> bool: + hits = [] + for path in iter_code_files(): + try: + text = path.read_text(errors="ignore") + except OSError: + continue + for lineno, line in enumerate(text.splitlines(), 1): + for rx in (CRED_RE, API_KEY_RE): + if rx.search(line) and not any(p in line for p in PLACEHOLDER): + hits.append(f"{path.relative_to(ROOT)}:{lineno}: {line.strip()[:100]}") + break + if hits: + print("❌ Embedded credentials detected:") + for h in hits: + print(f" {h}") + return False + print("✅ No embedded credentials in tracked code/workflow files") + return True + + +CHECKS = {"workflows": check_workflows, "config": check_config, "secrets": check_secrets} + + +def main() -> int: + args = sys.argv[1:] or ["all"] + names = list(CHECKS) if "all" in args else args + failed = [n for n in names if n not in CHECKS or not CHECKS[n]()] + if failed: + print(f"❌ CI checks failed: {', '.join(failed)}") + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) -- 2.54.0 From f477a6a252a2f3675099b8068f0b45f1a1e87e68 Mon Sep 17 00:00:00 2001 From: Mumuni Date: Fri, 25 Sep 2026 19:20:23 +0000 Subject: [PATCH 6/7] fix(ci): install python3+PyYAML in the job when the runner image lacks them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .gitea/workflows/ci.yml | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index 41a33ee..e7ed8f3 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -15,9 +15,19 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - run: python3 --version + - name: Ensure python3 available (runner image may lack it) + run: | + if command -v python3 >/dev/null 2>&1; then + python3 --version + else + . /etc/os-release + echo "python3 missing — installing on $ID $VERSION_CODENAME" + apt-get update -qq + apt-get install -y -qq python3 python3-yaml + python3 --version + python3 -c "import yaml" && echo "PyYAML OK" + fi - run: node --version - - run: echo "Runner works!" - name: Python syntax check run: | -- 2.54.0 From 52c41ca8fbe637c60eabefaf1d459fea84a5fb53 Mon Sep 17 00:00:00 2001 From: Mumuni Date: Fri, 25 Sep 2026 19:39:36 +0000 Subject: [PATCH 7/7] =?UTF-8?q?fix(ci):=20install=20python3=20via=20apk=20?= =?UTF-8?q?=E2=80=94=20act=20job=20container=20is=20alpine,=20not=20debian?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .gitea/workflows/ci.yml | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index e7ed8f3..6acb383 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -15,17 +15,25 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Ensure python3 available (runner image may lack it) + - name: Ensure python3 + PyYAML (act container is alpine-based, no python preinstalled) run: | + . /etc/os-release + echo "job OS: $ID" if command -v python3 >/dev/null 2>&1; then python3 --version - else - . /etc/os-release - echo "python3 missing — installing on $ID $VERSION_CODENAME" + elif command -v apk >/dev/null 2>&1; then + echo "installing via apk" + apk add --no-cache python3 py3-yaml + python3 --version + python3 -c "import yaml" || { echo "yaml import failed after py3-yaml; trying pip"; python3 -m pip install --break-system-packages pyyaml; } + elif command -v apt-get >/dev/null 2>&1; then + echo "installing via apt" apt-get update -qq apt-get install -y -qq python3 python3-yaml python3 --version - python3 -c "import yaml" && echo "PyYAML OK" + else + echo "no python3 and no apk/apt in job image — cannot run CI checks" + exit 1 fi - run: node --version -- 2.54.0