Compare commits

..
Author SHA1 Message Date
Mumuni 52c41ca8fb fix(ci): install python3 via apk — act job container is alpine, not debian
CI / validate (pull_request) Successful in 14s
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.
2026-09-25 19:39:36 +00:00
MumuniandClaude Opus 5 f477a6a252 fix(ci): install python3+PyYAML in the job when the runner image lacks them
CI / validate (pull_request) Failing after 5s
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>
2026-09-25 19:20:23 +00:00
Mumuni 9edc258245 fix(ci): make the validate job actually able to run — it never had
CI / validate (pull_request) Failing after 14s
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)
2026-09-25 15:50:16 +00:00
Mumuni fb752be8c9 fix(zulip): @all-bots fallback default was user_id=1 — never valid in SyslogGH realm
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.
2026-09-25 15:31:28 +00:00
Mumuni f0f82d904b security: remove hardcoded abiba-bot credentials from deploy workflow
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.
2026-09-25 15:28:58 +00:00
Abiba (pi) 2c4136daf8 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 19c52a9425)
2026-09-25 15:25:54 +00:00
Abiba (pi) 3db0d74620 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 aeb79c6286)
2026-09-25 15:25:54 +00:00
abiba-bot 9f72da5b8d Merge pull request 'feat: Zulip Gateway v3 — Production Resilience Rewrite' (#34) from feat/zulip-v3-resilience into main 2026-07-15 16:09:55 +00:00
5 changed files with 171 additions and 35 deletions
+29 -28
View File
@@ -14,42 +14,43 @@ jobs:
validate:
runs-on: ubuntu-latest
steps:
- run: python3 --version
- uses: actions/checkout@v4
- 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
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
else
echo "no python3 and no apk/apt in job image — cannot run CI checks"
exit 1
fi
- 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: |
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:
- 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"
- 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"
+4 -4
View File
@@ -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 }}
+26 -1
View File
@@ -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.
+105
View File
@@ -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())
+7 -2
View File
@@ -64,9 +64,14 @@ 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]"
ECHO_TAG_PREFIX = "hermes-zulip-"
RECONNECT_BACKOFF = [2, 5, 10, 30, 60]
DEDUP_WINDOW = 300 # 5 minutes
@@ -120,7 +125,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: