bot-cmder 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- bot_cmder-0.2.0/.dockerignore +69 -0
- bot_cmder-0.2.0/.env.example +161 -0
- bot_cmder-0.2.0/.github/dependabot.yml +41 -0
- bot_cmder-0.2.0/.github/workflows/beta.yml +100 -0
- bot_cmder-0.2.0/.github/workflows/ci.yml +200 -0
- bot_cmder-0.2.0/.github/workflows/docker.yml +162 -0
- bot_cmder-0.2.0/.github/workflows/release.yml +92 -0
- bot_cmder-0.2.0/.gitignore +24 -0
- bot_cmder-0.2.0/.gitleaks.toml +143 -0
- bot_cmder-0.2.0/.pre-commit-config.yaml +40 -0
- bot_cmder-0.2.0/AGENTS.md +160 -0
- bot_cmder-0.2.0/CHANGELOG.md +154 -0
- bot_cmder-0.2.0/Dockerfile +106 -0
- bot_cmder-0.2.0/Justfile +156 -0
- bot_cmder-0.2.0/LICENSE +21 -0
- bot_cmder-0.2.0/PKG-INFO +151 -0
- bot_cmder-0.2.0/README.md +125 -0
- bot_cmder-0.2.0/bot_cmder/__init__.py +10 -0
- bot_cmder-0.2.0/bot_cmder/__main__.py +15 -0
- bot_cmder-0.2.0/bot_cmder/adapters/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/adapters/base.py +23 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/__init__.py +5 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/adapter.py +203 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/client.py +126 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/gateway.py +483 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/router.py +122 -0
- bot_cmder-0.2.0/bot_cmder/adapters/discord/schemas.py +151 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/__init__.py +5 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/adapter.py +143 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/client.py +89 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/router.py +149 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/schemas.py +59 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/signing.py +88 -0
- bot_cmder-0.2.0/bot_cmder/adapters/slack/socket.py +254 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/__init__.py +5 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/adapter.py +54 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/client.py +153 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/daemon.py +201 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/router.py +63 -0
- bot_cmder-0.2.0/bot_cmder/adapters/telegram/schemas.py +43 -0
- bot_cmder-0.2.0/bot_cmder/audit/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/audit/log.py +240 -0
- bot_cmder-0.2.0/bot_cmder/auth/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/auth/acl.py +44 -0
- bot_cmder-0.2.0/bot_cmder/auth/emergency.py +173 -0
- bot_cmder-0.2.0/bot_cmder/auth/lockout.py +202 -0
- bot_cmder-0.2.0/bot_cmder/auth/lockout_store.py +168 -0
- bot_cmder-0.2.0/bot_cmder/auth/migrations/0001_initial.sql +10 -0
- bot_cmder-0.2.0/bot_cmder/auth/migrations/0002_lockout.sql +37 -0
- bot_cmder-0.2.0/bot_cmder/auth/migrations/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/auth/pending.py +77 -0
- bot_cmder-0.2.0/bot_cmder/auth/secret_store.py +124 -0
- bot_cmder-0.2.0/bot_cmder/auth/totp.py +65 -0
- bot_cmder-0.2.0/bot_cmder/cli/__init__.py +59 -0
- bot_cmder-0.2.0/bot_cmder/cli/discord_register.py +269 -0
- bot_cmder-0.2.0/bot_cmder/cli/init_cmd.py +178 -0
- bot_cmder-0.2.0/bot_cmder/cli/keys.py +38 -0
- bot_cmder-0.2.0/bot_cmder/cli/serve.py +118 -0
- bot_cmder-0.2.0/bot_cmder/cli/slack_manifest.py +226 -0
- bot_cmder-0.2.0/bot_cmder/cli/totp.py +157 -0
- bot_cmder-0.2.0/bot_cmder/commands/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/__init__.py +104 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/health.py +70 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/help.py +28 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/kubectl.py +68 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/otp.py +498 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/runbook.py +123 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/service.py +388 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/ssh.py +129 -0
- bot_cmder-0.2.0/bot_cmder/commands/builtin/whoami.py +28 -0
- bot_cmder-0.2.0/bot_cmder/config/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/config/paths.py +106 -0
- bot_cmder-0.2.0/bot_cmder/config/schema.py +365 -0
- bot_cmder-0.2.0/bot_cmder/config/settings.py +218 -0
- bot_cmder-0.2.0/bot_cmder/connectors/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/connectors/base.py +39 -0
- bot_cmder-0.2.0/bot_cmder/connectors/local.py +73 -0
- bot_cmder-0.2.0/bot_cmder/connectors/ssh.py +203 -0
- bot_cmder-0.2.0/bot_cmder/core/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/core/context.py +37 -0
- bot_cmder-0.2.0/bot_cmder/core/dispatcher.py +209 -0
- bot_cmder-0.2.0/bot_cmder/core/errors.py +22 -0
- bot_cmder-0.2.0/bot_cmder/core/events.py +82 -0
- bot_cmder-0.2.0/bot_cmder/core/parser.py +47 -0
- bot_cmder-0.2.0/bot_cmder/core/redact.py +81 -0
- bot_cmder-0.2.0/bot_cmder/core/registry.py +213 -0
- bot_cmder-0.2.0/bot_cmder/data/__init__.py +8 -0
- bot_cmder-0.2.0/bot_cmder/data/app.yaml.example +317 -0
- bot_cmder-0.2.0/bot_cmder/main.py +440 -0
- bot_cmder-0.2.0/bot_cmder/storage/__init__.py +0 -0
- bot_cmder-0.2.0/bot_cmder/storage/migrator.py +178 -0
- bot_cmder-0.2.0/docs/audit-rotation.md +169 -0
- bot_cmder-0.2.0/docs/discord-gateway.md +182 -0
- bot_cmder-0.2.0/docs/discord-setup.md +440 -0
- bot_cmder-0.2.0/docs/docker.md +327 -0
- bot_cmder-0.2.0/docs/getting-started.md +259 -0
- bot_cmder-0.2.0/docs/git-leak-prevention.md +273 -0
- bot_cmder-0.2.0/docs/otp.md +256 -0
- bot_cmder-0.2.0/docs/release.md +248 -0
- bot_cmder-0.2.0/docs/slack-setup.md +358 -0
- bot_cmder-0.2.0/docs/slack-socket-mode.md +159 -0
- bot_cmder-0.2.0/docs/telegram-polling.md +141 -0
- bot_cmder-0.2.0/pyproject.toml +99 -0
- bot_cmder-0.2.0/scripts/hook_status.py +42 -0
- bot_cmder-0.2.0/scripts/show_env_settings.py +104 -0
- bot_cmder-0.2.0/scripts/tunnel.sh +149 -0
- bot_cmder-0.2.0/scripts/tunnel_ngrok.sh +162 -0
- bot_cmder-0.2.0/tests/__init__.py +0 -0
- bot_cmder-0.2.0/tests/adapters/__init__.py +0 -0
- bot_cmder-0.2.0/tests/adapters/discord/__init__.py +0 -0
- bot_cmder-0.2.0/tests/adapters/discord/test_adapter.py +272 -0
- bot_cmder-0.2.0/tests/adapters/discord/test_client.py +82 -0
- bot_cmder-0.2.0/tests/adapters/discord/test_gateway.py +453 -0
- bot_cmder-0.2.0/tests/adapters/discord/test_router.py +157 -0
- bot_cmder-0.2.0/tests/adapters/slack/__init__.py +0 -0
- bot_cmder-0.2.0/tests/adapters/slack/test_adapter.py +270 -0
- bot_cmder-0.2.0/tests/adapters/slack/test_client.py +64 -0
- bot_cmder-0.2.0/tests/adapters/slack/test_router.py +249 -0
- bot_cmder-0.2.0/tests/adapters/slack/test_signing.py +121 -0
- bot_cmder-0.2.0/tests/adapters/slack/test_socket.py +316 -0
- bot_cmder-0.2.0/tests/adapters/telegram/__init__.py +0 -0
- bot_cmder-0.2.0/tests/adapters/telegram/test_adapter.py +66 -0
- bot_cmder-0.2.0/tests/adapters/telegram/test_client.py +157 -0
- bot_cmder-0.2.0/tests/adapters/telegram/test_daemon.py +233 -0
- bot_cmder-0.2.0/tests/adapters/telegram/test_router.py +95 -0
- bot_cmder-0.2.0/tests/adapters/telegram/test_set_my_commands.py +45 -0
- bot_cmder-0.2.0/tests/audit/__init__.py +0 -0
- bot_cmder-0.2.0/tests/audit/test_log.py +46 -0
- bot_cmder-0.2.0/tests/audit/test_log_rotation.py +355 -0
- bot_cmder-0.2.0/tests/auth/__init__.py +0 -0
- bot_cmder-0.2.0/tests/auth/test_acl.py +70 -0
- bot_cmder-0.2.0/tests/auth/test_emergency.py +179 -0
- bot_cmder-0.2.0/tests/auth/test_lockout.py +347 -0
- bot_cmder-0.2.0/tests/auth/test_pending.py +51 -0
- bot_cmder-0.2.0/tests/auth/test_secret_store.py +60 -0
- bot_cmder-0.2.0/tests/auth/test_totp.py +66 -0
- bot_cmder-0.2.0/tests/cli/__init__.py +0 -0
- bot_cmder-0.2.0/tests/cli/test_discord_register.py +140 -0
- bot_cmder-0.2.0/tests/cli/test_dispatcher.py +64 -0
- bot_cmder-0.2.0/tests/cli/test_init.py +149 -0
- bot_cmder-0.2.0/tests/cli/test_keys.py +31 -0
- bot_cmder-0.2.0/tests/cli/test_serve.py +130 -0
- bot_cmder-0.2.0/tests/cli/test_slack_manifest.py +182 -0
- bot_cmder-0.2.0/tests/commands/__init__.py +0 -0
- bot_cmder-0.2.0/tests/commands/test_health.py +121 -0
- bot_cmder-0.2.0/tests/commands/test_kubectl.py +97 -0
- bot_cmder-0.2.0/tests/commands/test_otp.py +436 -0
- bot_cmder-0.2.0/tests/commands/test_runbook.py +164 -0
- bot_cmder-0.2.0/tests/commands/test_service.py +388 -0
- bot_cmder-0.2.0/tests/commands/test_ssh.py +140 -0
- bot_cmder-0.2.0/tests/commands/test_whoami.py +46 -0
- bot_cmder-0.2.0/tests/config/__init__.py +0 -0
- bot_cmder-0.2.0/tests/config/test_paths.py +150 -0
- bot_cmder-0.2.0/tests/config/test_schema.py +81 -0
- bot_cmder-0.2.0/tests/config/test_settings_discovery.py +119 -0
- bot_cmder-0.2.0/tests/conftest.py +60 -0
- bot_cmder-0.2.0/tests/connectors/__init__.py +0 -0
- bot_cmder-0.2.0/tests/connectors/test_local.py +33 -0
- bot_cmder-0.2.0/tests/connectors/test_ssh.py +184 -0
- bot_cmder-0.2.0/tests/core/__init__.py +0 -0
- bot_cmder-0.2.0/tests/core/test_dispatcher.py +407 -0
- bot_cmder-0.2.0/tests/core/test_parser.py +76 -0
- bot_cmder-0.2.0/tests/core/test_redact.py +124 -0
- bot_cmder-0.2.0/tests/core/test_registry.py +50 -0
- bot_cmder-0.2.0/tests/core/test_router.py +128 -0
- bot_cmder-0.2.0/tests/storage/__init__.py +0 -0
- bot_cmder-0.2.0/tests/storage/test_migrator.py +136 -0
- bot_cmder-0.2.0/tests/test_cli.py +65 -0
- bot_cmder-0.2.0/tests/test_main.py +321 -0
- bot_cmder-0.2.0/uv.lock +1651 -0
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
# Keep the build context small + deterministic — only the files that
|
|
2
|
+
# the wheel build actually needs. Everything else is either:
|
|
3
|
+
# - re-derivable (.venv, dist, __pycache__)
|
|
4
|
+
# - state that doesn't belong in an image (var, .env)
|
|
5
|
+
# - irrelevant to the runtime (tests, docs, CI configs)
|
|
6
|
+
# - agent-local (.claude)
|
|
7
|
+
|
|
8
|
+
# Version control + IDE state
|
|
9
|
+
.git
|
|
10
|
+
.gitignore
|
|
11
|
+
.gitleaks.toml
|
|
12
|
+
.gitattributes
|
|
13
|
+
.editorconfig
|
|
14
|
+
.vscode
|
|
15
|
+
.idea
|
|
16
|
+
|
|
17
|
+
# Agent worktrees, plans, ephemeral state
|
|
18
|
+
.claude
|
|
19
|
+
|
|
20
|
+
# Python build / test artifacts
|
|
21
|
+
.venv
|
|
22
|
+
.pytest_cache
|
|
23
|
+
.ruff_cache
|
|
24
|
+
.mypy_cache
|
|
25
|
+
.coverage
|
|
26
|
+
htmlcov
|
|
27
|
+
**/__pycache__
|
|
28
|
+
**/*.pyc
|
|
29
|
+
**/*.pyo
|
|
30
|
+
|
|
31
|
+
# Local build outputs (wheels are rebuilt inside the builder stage)
|
|
32
|
+
dist
|
|
33
|
+
build
|
|
34
|
+
*.egg-info
|
|
35
|
+
|
|
36
|
+
# Repo-local bot state (never ship audit logs, totp secrets, etc.)
|
|
37
|
+
var
|
|
38
|
+
runbooks
|
|
39
|
+
|
|
40
|
+
# Operator config — must be mounted at runtime, never baked in
|
|
41
|
+
.env
|
|
42
|
+
.env.example
|
|
43
|
+
.env.local
|
|
44
|
+
config/app.yaml
|
|
45
|
+
config/app.yaml.local
|
|
46
|
+
|
|
47
|
+
# Tests + dev-only files don't run inside the image
|
|
48
|
+
tests
|
|
49
|
+
.pre-commit-config.yaml
|
|
50
|
+
|
|
51
|
+
# Docs, examples, scripts that don't ship in the wheel
|
|
52
|
+
docs
|
|
53
|
+
scripts/tunnel*.sh
|
|
54
|
+
scripts/show_env_settings.py
|
|
55
|
+
scripts/hook_status.py
|
|
56
|
+
|
|
57
|
+
# CI configuration — building from source doesn't need these inside
|
|
58
|
+
# the container, GitHub Actions reads them from the repo directly
|
|
59
|
+
.github
|
|
60
|
+
|
|
61
|
+
# OS / editor cruft
|
|
62
|
+
.DS_Store
|
|
63
|
+
Thumbs.db
|
|
64
|
+
*.swp
|
|
65
|
+
*.swo
|
|
66
|
+
*~
|
|
67
|
+
|
|
68
|
+
# Justfile — only needed for source contributors, not in the image
|
|
69
|
+
Justfile
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
# Telegram bot token from @BotFather.
|
|
2
|
+
TELEGRAM_TOKEN=
|
|
3
|
+
|
|
4
|
+
# Optional: any long random string. When set, the bot rejects webhook
|
|
5
|
+
# requests whose `X-Telegram-Bot-Api-Secret-Token` header does not match.
|
|
6
|
+
# Set the same value when calling setWebhook (see Justfile).
|
|
7
|
+
TELEGRAM_WEBHOOK_SECRET=
|
|
8
|
+
|
|
9
|
+
# Phase 6a — ingestion mode.
|
|
10
|
+
# - "webhook" (default): bot exposes POST /webhooks/telegram and
|
|
11
|
+
# Telegram POSTs each update there. Requires public HTTPS URL
|
|
12
|
+
# (set TELEGRAM_HOOK_URL below or use `just tunnel-ngrok`).
|
|
13
|
+
# - "polling": bot dials OUT to api.telegram.org and long-polls for
|
|
14
|
+
# updates. NO public URL, NO tunnel needed — ideal for home labs
|
|
15
|
+
# / NAT / corp egress restrictions. The two modes are mutually
|
|
16
|
+
# exclusive in Telegram's API; the daemon auto-deletes any
|
|
17
|
+
# registered webhook at startup so flipping the mode Just Works.
|
|
18
|
+
TELEGRAM_MODE=webhook
|
|
19
|
+
|
|
20
|
+
# Polling-mode tuning (ignored unless TELEGRAM_MODE=polling).
|
|
21
|
+
# Telegram caps the long-poll wait at 50s; 25s is a sweet spot —
|
|
22
|
+
# fewer requests than short polling, but stays under most middleboxes'
|
|
23
|
+
# silent-drop window.
|
|
24
|
+
TELEGRAM_POLLING_TIMEOUT_S=25
|
|
25
|
+
# When flipping to polling, drop any updates Telegram queued while a
|
|
26
|
+
# webhook was active. Useful during dev mode flapping (don't replay
|
|
27
|
+
# yesterday's tests). Leave false in prod so a brief webhook outage
|
|
28
|
+
# doesn't lose updates.
|
|
29
|
+
TELEGRAM_POLLING_DROP_PENDING=false
|
|
30
|
+
|
|
31
|
+
# Public HTTPS URL where Telegram will deliver updates. Used by
|
|
32
|
+
# `just set-telegram-bot-webhook` only — not read by the running app.
|
|
33
|
+
# Auto-overwritten by `just tunnel` and `just tunnel-ngrok`.
|
|
34
|
+
# Ignored entirely in TELEGRAM_MODE=polling.
|
|
35
|
+
TELEGRAM_HOOK_URL=
|
|
36
|
+
|
|
37
|
+
# Reserved ngrok static domain for `just tunnel-ngrok`. Get a free
|
|
38
|
+
# one at https://dashboard.ngrok.com/domains. Stable across ngrok
|
|
39
|
+
# restarts, so DNS never has propagation lag (unlike trycloudflare).
|
|
40
|
+
#
|
|
41
|
+
# IMPORTANT: just the bare hostname — no `https://` prefix, no path
|
|
42
|
+
# suffix. The tunnel script adds those itself; pasting the full URL
|
|
43
|
+
# corrupts TELEGRAM_HOOK_URL and ngrok rejects with ERR_NGROK_9038.
|
|
44
|
+
# ✓ NGROK_DOMAIN=your-reserved-domain.ngrok-free.dev
|
|
45
|
+
# ✗ NGROK_DOMAIN=https://your-reserved-domain.ngrok-free.dev/webhooks/telegram
|
|
46
|
+
NGROK_DOMAIN=
|
|
47
|
+
|
|
48
|
+
# Path to the app YAML config (users, ACL, healthcheck targets, ...).
|
|
49
|
+
APP_CONFIG_PATH=./config/app.yaml
|
|
50
|
+
|
|
51
|
+
# Optional override for the audit log path. Falls back to the path set
|
|
52
|
+
# in app.yaml under `audit.path`.
|
|
53
|
+
# AUDIT_PATH=./var/audit.jsonl
|
|
54
|
+
|
|
55
|
+
# Optional uvicorn bind overrides. Defaults: 127.0.0.1:47823 with no
|
|
56
|
+
# autoreload. Port 47823 is intentionally uncommon to avoid clashing
|
|
57
|
+
# with other dev services on 8000 / 8080.
|
|
58
|
+
# BIND_HOST=127.0.0.1
|
|
59
|
+
# BIND_PORT=47823
|
|
60
|
+
# RELOAD=1
|
|
61
|
+
|
|
62
|
+
# Phase 2 — Fernet symmetric key used to encrypt TOTP secrets at rest
|
|
63
|
+
# in the SQLite store. REQUIRED to enable the OTP gate; without it
|
|
64
|
+
# privileged commands (/kubectl, /runbook run, anything with
|
|
65
|
+
# Risk.PRIVILEGED) cannot be authorized and refuse to run.
|
|
66
|
+
#
|
|
67
|
+
# Generate one with:
|
|
68
|
+
# python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
|
|
69
|
+
#
|
|
70
|
+
# WARNING: rotating this key invalidates every existing TOTP enrollment.
|
|
71
|
+
# All users will need to re-run `python -m bot_cmder.cli enroll-totp`.
|
|
72
|
+
BOT_CMDER_MASTER_KEY=
|
|
73
|
+
|
|
74
|
+
# Phase 4 — Discord adapter. All three are required for the adapter
|
|
75
|
+
# to mount /webhooks/discord; without them the bot still runs but
|
|
76
|
+
# Discord is silently disabled (logged at WARNING during startup).
|
|
77
|
+
#
|
|
78
|
+
# DISCORD_PUBLIC_KEY: Ed25519 verify key (hex). Discord application
|
|
79
|
+
# "General Information" → "Public Key". Used to verify every
|
|
80
|
+
# incoming interaction; Discord refuses to onboard endpoints that
|
|
81
|
+
# don't enforce signing, so this is non-negotiable.
|
|
82
|
+
DISCORD_PUBLIC_KEY=
|
|
83
|
+
# DISCORD_BOT_TOKEN: bot token from "Bot" page. Used to PATCH
|
|
84
|
+
# deferred replies via Discord's webhook API and to PUT the slash
|
|
85
|
+
# command schema (scripts/register_discord_commands.py).
|
|
86
|
+
DISCORD_BOT_TOKEN=
|
|
87
|
+
# DISCORD_APPLICATION_ID: numeric application ID (in the URL of the
|
|
88
|
+
# application page). Goes into the follow-up @original PATCH path.
|
|
89
|
+
DISCORD_APPLICATION_ID=
|
|
90
|
+
|
|
91
|
+
# DISCORD_GUILD_ID: optional. Read ONLY by scripts/register_discord_commands.py
|
|
92
|
+
# — the running bot never touches this value. When set, `just register-discord`
|
|
93
|
+
# pushes the slash command schema to that one server (instant propagation,
|
|
94
|
+
# recommended for dev). When unset, registration goes global (~1h propagation,
|
|
95
|
+
# visible in every server the bot is in + DMs, recommended for prod).
|
|
96
|
+
# Override per-call with `just register-discord --guild=<id>` or force a
|
|
97
|
+
# global push with `just register-discord --global`.
|
|
98
|
+
#
|
|
99
|
+
# Get the ID: Discord client → Settings → Advanced → Developer Mode (on),
|
|
100
|
+
# then right-click your server icon → Copy Server ID.
|
|
101
|
+
DISCORD_GUILD_ID=
|
|
102
|
+
|
|
103
|
+
# Phase 6c — ingestion mode for the Discord adapter.
|
|
104
|
+
# - "interactions" (default): bot exposes POST /webhooks/discord and
|
|
105
|
+
# Discord POSTs slash commands there. Requires public HTTPS URL +
|
|
106
|
+
# DISCORD_PUBLIC_KEY for Ed25519 verification. This is Phase 4
|
|
107
|
+
# behavior, preserved as the default.
|
|
108
|
+
# - "gateway": bot opens a WebSocket OUT to Discord and chat events
|
|
109
|
+
# arrive over that. NO public URL needed; ideal for home labs / NAT.
|
|
110
|
+
# **Slash commands DON'T arrive over the Gateway** (Discord
|
|
111
|
+
# platform limitation) — UX shifts to `@bot cmd args` in guild
|
|
112
|
+
# channels or plain `cmd args` in DMs. Needs the MESSAGE_CONTENT
|
|
113
|
+
# privileged intent enabled in the dev portal (Bot → Privileged
|
|
114
|
+
# Gateway Intents → Message Content Intent).
|
|
115
|
+
# Anything other than "interactions" / "gateway" fails fast at startup.
|
|
116
|
+
DISCORD_MODE=interactions
|
|
117
|
+
|
|
118
|
+
# Phase 5/6b — Slack adapter. Two ingestion modes (mutually exclusive
|
|
119
|
+
# in Slack's app config — when Socket Mode is toggled on, the Events
|
|
120
|
+
# API URL field greys out):
|
|
121
|
+
# - "events" (default): /webhooks/slack receives Slack POSTs.
|
|
122
|
+
# Needs SLACK_SIGNING_SECRET. Requires public HTTPS URL.
|
|
123
|
+
# - "socket": bot opens WebSocket OUT to Slack. Needs SLACK_APP_TOKEN.
|
|
124
|
+
# NO public URL needed; ideal for home labs / NAT.
|
|
125
|
+
# Without the relevant secret(s) the bot still runs but Slack is
|
|
126
|
+
# silently disabled (logged at WARNING during startup).
|
|
127
|
+
SLACK_MODE=events
|
|
128
|
+
|
|
129
|
+
# SLACK_SIGNING_SECRET (events mode only): 32-char hex string from
|
|
130
|
+
# your Slack app's "Basic Information" → "App Credentials" →
|
|
131
|
+
# "Signing Secret". Used to verify every incoming slash command via
|
|
132
|
+
# HMAC-SHA256(`v0:<ts>:<body>`). Slack rejects endpoints that accept
|
|
133
|
+
# unsigned payloads.
|
|
134
|
+
SLACK_SIGNING_SECRET=
|
|
135
|
+
|
|
136
|
+
# SLACK_APP_TOKEN (socket mode only): app-level token from "Basic
|
|
137
|
+
# Information" → "App-Level Tokens" → Generate (scope:
|
|
138
|
+
# `connections:write`). Distinct from SLACK_BOT_TOKEN — this one
|
|
139
|
+
# authorizes opening the Socket Mode WebSocket connection.
|
|
140
|
+
SLACK_APP_TOKEN=
|
|
141
|
+
|
|
142
|
+
# SLACK_BOT_TOKEN: bot user OAuth token from "OAuth & Permissions"
|
|
143
|
+
# page (xoxb-...). Optional for both modes today; reserved for
|
|
144
|
+
# future chat.postMessage-based replies (Block Kit etc.).
|
|
145
|
+
SLACK_BOT_TOKEN=
|
|
146
|
+
|
|
147
|
+
# Read ONLY by scripts/register_slack_commands.py (the running bot
|
|
148
|
+
# never touches it — Slack tells the bot where the request came from
|
|
149
|
+
# per-call). Public HTTPS URL Slack should POST slash commands to.
|
|
150
|
+
# Bare hostname OK — the script appends /webhooks/slack:
|
|
151
|
+
# ✓ SLACK_REQUEST_URL=my-tunnel.ngrok-free.dev
|
|
152
|
+
# ✓ SLACK_REQUEST_URL=https://my-tunnel.ngrok-free.dev
|
|
153
|
+
# ✓ SLACK_REQUEST_URL=https://my-tunnel.ngrok-free.dev/webhooks/slack
|
|
154
|
+
# When unset, falls back to NGROK_DOMAIN. When that's also unset,
|
|
155
|
+
# `just register-slack` errors out so a placeholder URL never lands
|
|
156
|
+
# in the Slack manifest.
|
|
157
|
+
SLACK_REQUEST_URL=
|
|
158
|
+
|
|
159
|
+
# Reply visibility (ephemeral / in_channel / by_risk + per-command
|
|
160
|
+
# overrides) is configured in `config/app.yaml` under `slack:`, NOT
|
|
161
|
+
# here — it's tunable behavior, not a secret.
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Keeps deps and Action versions fresh by opening one PR per upgrade.
|
|
2
|
+
# Each PR runs through CI like any other change, so you only have to
|
|
3
|
+
# look at green/red instead of remembering when you last ran
|
|
4
|
+
# `uv sync --upgrade`.
|
|
5
|
+
#
|
|
6
|
+
# Cadence rationale:
|
|
7
|
+
# - "weekly" not "daily": daily floods the inbox; weekly batches
|
|
8
|
+
# enough churn to be worth reviewing without going stale.
|
|
9
|
+
# - Caps at 5 open PRs per ecosystem so a bad upstream week
|
|
10
|
+
# (e.g. 20 actions get patches at once) doesn't bury everything.
|
|
11
|
+
|
|
12
|
+
version: 2
|
|
13
|
+
|
|
14
|
+
updates:
|
|
15
|
+
# GitHub Actions used in .github/workflows/*.yml — pins like
|
|
16
|
+
# actions/checkout@v4 will get bumped to v5 etc.
|
|
17
|
+
- package-ecosystem: "github-actions"
|
|
18
|
+
directory: "/"
|
|
19
|
+
schedule:
|
|
20
|
+
interval: "weekly"
|
|
21
|
+
open-pull-requests-limit: 5
|
|
22
|
+
commit-message:
|
|
23
|
+
prefix: "ci"
|
|
24
|
+
include: "scope"
|
|
25
|
+
|
|
26
|
+
# Python deps tracked via pyproject.toml + uv.lock. Dependabot's
|
|
27
|
+
# `pip` ecosystem reads pyproject and updates uv.lock alongside it
|
|
28
|
+
# (it has uv support since late 2024).
|
|
29
|
+
- package-ecosystem: "pip"
|
|
30
|
+
directory: "/"
|
|
31
|
+
schedule:
|
|
32
|
+
interval: "weekly"
|
|
33
|
+
open-pull-requests-limit: 5
|
|
34
|
+
# Group all dev-only updates into ONE PR per week — they rarely
|
|
35
|
+
# need individual scrutiny (it's just lint/test tooling).
|
|
36
|
+
groups:
|
|
37
|
+
dev-tools:
|
|
38
|
+
dependency-type: "development"
|
|
39
|
+
commit-message:
|
|
40
|
+
prefix: "deps"
|
|
41
|
+
include: "scope"
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# Test PyPI publish — issue #20 PR-B.
|
|
2
|
+
#
|
|
3
|
+
# Triggered by pushing to the `beta` branch. Mirrors the
|
|
4
|
+
# `zondatw/remote-cmder` workflow pattern (branch-based triggers
|
|
5
|
+
# instead of tag-based) so the publish flow feels familiar across
|
|
6
|
+
# Zonda's repos:
|
|
7
|
+
#
|
|
8
|
+
# git checkout beta
|
|
9
|
+
# git merge main # forward whatever was just verified
|
|
10
|
+
# git push # ← this is the trigger
|
|
11
|
+
#
|
|
12
|
+
# After the workflow goes green, eyeball https://test.pypi.org/project/bot-cmder/
|
|
13
|
+
# in a browser. If the page renders the README correctly, license
|
|
14
|
+
# shows MIT, and `pip install --index-url https://test.pypi.org/simple/
|
|
15
|
+
# --extra-index-url https://pypi.org/simple/ bot-cmder` works in a
|
|
16
|
+
# fresh venv → safe to forward to the `release` branch.
|
|
17
|
+
#
|
|
18
|
+
# Auth: PyPI Trusted Publishing (OIDC). No long-lived `TWINE_PASSWORD`
|
|
19
|
+
# in repo secrets — GitHub mints a short-lived token at publish time
|
|
20
|
+
# and PyPI validates it against the pending publisher configured on
|
|
21
|
+
# the test.pypi.org side. See docs/release.md for the one-time setup.
|
|
22
|
+
#
|
|
23
|
+
# Why a separate `beta` branch and not a `--repository test.pypi.org`
|
|
24
|
+
# flag on a workflow_dispatch: matches the repo-mate pattern, and a
|
|
25
|
+
# dedicated branch's HEAD makes "what's currently on test.pypi.org?"
|
|
26
|
+
# answerable with `git log beta`.
|
|
27
|
+
|
|
28
|
+
name: beta
|
|
29
|
+
|
|
30
|
+
on:
|
|
31
|
+
push:
|
|
32
|
+
branches: [beta]
|
|
33
|
+
|
|
34
|
+
# Don't cancel an in-flight publish if a second push lands on beta —
|
|
35
|
+
# that would leave test.pypi.org in an inconsistent state. Each push
|
|
36
|
+
# waits for the previous to finish.
|
|
37
|
+
concurrency:
|
|
38
|
+
group: beta-publish
|
|
39
|
+
cancel-in-progress: false
|
|
40
|
+
|
|
41
|
+
jobs:
|
|
42
|
+
build:
|
|
43
|
+
name: build wheel + sdist
|
|
44
|
+
runs-on: ubuntu-latest
|
|
45
|
+
steps:
|
|
46
|
+
- uses: actions/checkout@v6
|
|
47
|
+
|
|
48
|
+
- uses: astral-sh/setup-uv@v7
|
|
49
|
+
with:
|
|
50
|
+
enable-cache: true
|
|
51
|
+
|
|
52
|
+
- name: Install Python
|
|
53
|
+
run: uv python install 3.12
|
|
54
|
+
|
|
55
|
+
- name: Build distributions
|
|
56
|
+
run: uv build
|
|
57
|
+
|
|
58
|
+
- name: Verify wheel ships expected data files
|
|
59
|
+
run: |
|
|
60
|
+
set -euo pipefail
|
|
61
|
+
WHEEL=$(ls dist/*.whl)
|
|
62
|
+
echo "Inspecting $WHEEL"
|
|
63
|
+
unzip -l "$WHEEL" | grep -q 'bot_cmder/data/app.yaml.example' || (
|
|
64
|
+
echo "ERROR: bot_cmder/data/app.yaml.example missing from wheel"
|
|
65
|
+
exit 1
|
|
66
|
+
)
|
|
67
|
+
unzip -l "$WHEEL" | grep -q 'bot_cmder/auth/migrations/0001_initial.sql' || (
|
|
68
|
+
echo "ERROR: bot_cmder/auth/migrations/0001_initial.sql missing from wheel"
|
|
69
|
+
exit 1
|
|
70
|
+
)
|
|
71
|
+
echo "OK — wheel contents verified"
|
|
72
|
+
|
|
73
|
+
- uses: actions/upload-artifact@v7
|
|
74
|
+
with:
|
|
75
|
+
name: dist
|
|
76
|
+
path: dist/
|
|
77
|
+
retention-days: 7
|
|
78
|
+
|
|
79
|
+
publish-test:
|
|
80
|
+
name: publish to test.pypi.org
|
|
81
|
+
needs: build
|
|
82
|
+
runs-on: ubuntu-latest
|
|
83
|
+
environment:
|
|
84
|
+
name: release-test
|
|
85
|
+
url: https://test.pypi.org/project/bot-cmder/
|
|
86
|
+
permissions:
|
|
87
|
+
# Required for PyPI Trusted Publishing — GitHub mints a short-
|
|
88
|
+
# lived OIDC token that test.pypi.org exchanges for an upload
|
|
89
|
+
# credential. No long-lived API token in repo secrets.
|
|
90
|
+
id-token: write
|
|
91
|
+
steps:
|
|
92
|
+
- uses: actions/download-artifact@v8
|
|
93
|
+
with:
|
|
94
|
+
name: dist
|
|
95
|
+
path: dist/
|
|
96
|
+
|
|
97
|
+
- name: Publish to Test PyPI
|
|
98
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
99
|
+
with:
|
|
100
|
+
repository-url: https://test.pypi.org/legacy/
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
# GitHub Actions CI — runs the full test suite + lint on every push to
|
|
2
|
+
# main and on every PR. Mirrors what `just test` and `pre-commit run
|
|
3
|
+
# --all-files` do locally so a green CI badge actually means something.
|
|
4
|
+
#
|
|
5
|
+
# Why two jobs (test + lint) instead of one:
|
|
6
|
+
# - Different inputs change them independently — a doc typo touches
|
|
7
|
+
# lint only, a handler change touches test only. Splitting lets
|
|
8
|
+
# you see at a glance which side broke.
|
|
9
|
+
# - They can run in parallel (faster red-line on broken PRs).
|
|
10
|
+
#
|
|
11
|
+
# Why matrix Python 3.10 and 3.12 (skipping 3.11):
|
|
12
|
+
# - 3.10 is the floor declared in pyproject.toml `requires-python`
|
|
13
|
+
# and `tool.ruff.target-version`. If we don't test it, "supports
|
|
14
|
+
# 3.10" is a lie.
|
|
15
|
+
# - 3.12 is the latest widely-deployed stable. Catches accidental
|
|
16
|
+
# use of removed-in-3.12 APIs (e.g. distutils, asyncio.coroutine).
|
|
17
|
+
# - Skipping 3.11 halves matrix runtime; if either neighbor passes,
|
|
18
|
+
# 3.11 almost always does too.
|
|
19
|
+
|
|
20
|
+
name: CI
|
|
21
|
+
|
|
22
|
+
on:
|
|
23
|
+
push:
|
|
24
|
+
branches: [main]
|
|
25
|
+
pull_request:
|
|
26
|
+
|
|
27
|
+
# Cancel a previous in-flight run for the same ref when a new commit
|
|
28
|
+
# lands on a PR — saves CI minutes and gets the latest result faster.
|
|
29
|
+
# Pushes to main are NOT cancelled (the `|| github.run_id` makes each
|
|
30
|
+
# main push its own group).
|
|
31
|
+
concurrency:
|
|
32
|
+
group: ci-${{ github.workflow }}-${{ github.ref }}-${{ github.event_name == 'push' && github.run_id || '' }}
|
|
33
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
34
|
+
|
|
35
|
+
jobs:
|
|
36
|
+
test:
|
|
37
|
+
name: pytest (py${{ matrix.python-version }})
|
|
38
|
+
runs-on: ubuntu-latest
|
|
39
|
+
strategy:
|
|
40
|
+
fail-fast: false
|
|
41
|
+
matrix:
|
|
42
|
+
python-version: ["3.10", "3.12"]
|
|
43
|
+
steps:
|
|
44
|
+
- name: Check out repo
|
|
45
|
+
uses: actions/checkout@v6
|
|
46
|
+
|
|
47
|
+
- name: Install uv (with cache)
|
|
48
|
+
uses: astral-sh/setup-uv@v7
|
|
49
|
+
with:
|
|
50
|
+
enable-cache: true
|
|
51
|
+
cache-dependency-glob: "uv.lock"
|
|
52
|
+
|
|
53
|
+
- name: Pin Python ${{ matrix.python-version }}
|
|
54
|
+
run: uv python install ${{ matrix.python-version }}
|
|
55
|
+
|
|
56
|
+
- name: Sync dependencies (locked)
|
|
57
|
+
# --frozen: refuse to update uv.lock; CI must use the exact
|
|
58
|
+
# versions a developer committed, otherwise "works on my
|
|
59
|
+
# machine" creeps back in via silent upgrades.
|
|
60
|
+
run: uv sync --frozen --python ${{ matrix.python-version }}
|
|
61
|
+
|
|
62
|
+
- name: Run pytest
|
|
63
|
+
run: uv run --python ${{ matrix.python-version }} pytest
|
|
64
|
+
|
|
65
|
+
lint:
|
|
66
|
+
name: pre-commit
|
|
67
|
+
runs-on: ubuntu-latest
|
|
68
|
+
steps:
|
|
69
|
+
- name: Check out repo
|
|
70
|
+
uses: actions/checkout@v6
|
|
71
|
+
|
|
72
|
+
- name: Set up Python 3.12
|
|
73
|
+
# Single Python is enough — black / ruff / codespell are all
|
|
74
|
+
# version-agnostic for our codebase.
|
|
75
|
+
uses: actions/setup-python@v6
|
|
76
|
+
with:
|
|
77
|
+
python-version: "3.12"
|
|
78
|
+
|
|
79
|
+
- name: Run pre-commit hooks
|
|
80
|
+
# The official action handles caching of hook envs (which
|
|
81
|
+
# otherwise re-clone black + ruff repos on every run).
|
|
82
|
+
uses: pre-commit/action@v3.0.1
|
|
83
|
+
|
|
84
|
+
# Issue #20 — wheel-build smoke test runs the same build steps the
|
|
85
|
+
# release workflows (.github/workflows/{beta,release}.yml) do, but
|
|
86
|
+
# on every PR. Catches packaging regressions at PR time instead of
|
|
87
|
+
# at release time. Examples of what would have shipped broken
|
|
88
|
+
# without this gate:
|
|
89
|
+
# - someone deletes bot_cmder/data/__init__.py → app.yaml.example
|
|
90
|
+
# stops shipping in the wheel → `bot-cmder init` 500s on every
|
|
91
|
+
# install
|
|
92
|
+
# - a refactor breaks the [project.scripts] entry → `bot-cmder`
|
|
93
|
+
# console script absent after `pip install`
|
|
94
|
+
# - import error in cli/__init__.py → `bot-cmder --help` raises
|
|
95
|
+
# before printing usage
|
|
96
|
+
# Each of those is silent in the test suite (which uses the editable
|
|
97
|
+
# install) but breaks the wheel.
|
|
98
|
+
wheel-build:
|
|
99
|
+
name: wheel build smoke test
|
|
100
|
+
runs-on: ubuntu-latest
|
|
101
|
+
steps:
|
|
102
|
+
- name: Check out repo
|
|
103
|
+
uses: actions/checkout@v6
|
|
104
|
+
|
|
105
|
+
- name: Install uv (with cache)
|
|
106
|
+
uses: astral-sh/setup-uv@v7
|
|
107
|
+
with:
|
|
108
|
+
enable-cache: true
|
|
109
|
+
cache-dependency-glob: "uv.lock"
|
|
110
|
+
|
|
111
|
+
- name: Pin Python 3.12
|
|
112
|
+
run: uv python install 3.12
|
|
113
|
+
|
|
114
|
+
- name: Build wheel + sdist
|
|
115
|
+
run: uv build
|
|
116
|
+
|
|
117
|
+
- name: Verify wheel ships expected data files
|
|
118
|
+
# Same checks the release workflows do — keep in sync. The
|
|
119
|
+
# set is small enough to inline; if it grows, factor into a
|
|
120
|
+
# shared bash script under scripts/.
|
|
121
|
+
run: |
|
|
122
|
+
set -euo pipefail
|
|
123
|
+
WHEEL=$(ls dist/*.whl)
|
|
124
|
+
echo "Inspecting $WHEEL"
|
|
125
|
+
unzip -l "$WHEEL" | grep -q 'bot_cmder/data/app.yaml.example' || (
|
|
126
|
+
echo "ERROR: bot_cmder/data/app.yaml.example missing from wheel"
|
|
127
|
+
exit 1
|
|
128
|
+
)
|
|
129
|
+
unzip -l "$WHEEL" | grep -q 'bot_cmder/auth/migrations/0001_initial.sql' || (
|
|
130
|
+
echo "ERROR: bot_cmder/auth/migrations/0001_initial.sql missing from wheel"
|
|
131
|
+
exit 1
|
|
132
|
+
)
|
|
133
|
+
echo "OK — wheel data files present"
|
|
134
|
+
|
|
135
|
+
- name: Smoke install + run --version
|
|
136
|
+
# Fresh venv install of the wheel exercises:
|
|
137
|
+
# - [project.scripts] entry point lands `bot-cmder` on PATH
|
|
138
|
+
# - lazy imports in cli/__init__.py + subcommands resolve
|
|
139
|
+
# - bot_cmder.__version__ is accessible at runtime
|
|
140
|
+
# If `bot-cmder --version` exits non-zero or prints empty, fail.
|
|
141
|
+
run: |
|
|
142
|
+
set -euo pipefail
|
|
143
|
+
python3 -m venv /tmp/smoke
|
|
144
|
+
/tmp/smoke/bin/pip install --quiet dist/*.whl
|
|
145
|
+
ver=$(/tmp/smoke/bin/bot-cmder --version)
|
|
146
|
+
echo "Got: $ver"
|
|
147
|
+
echo "$ver" | grep -qE '^bot-cmder [0-9]+\.[0-9]+\.[0-9]+' || (
|
|
148
|
+
echo "ERROR: --version output didn't match 'bot-cmder X.Y.Z'"
|
|
149
|
+
exit 1
|
|
150
|
+
)
|
|
151
|
+
# Also exercise the no-args case to catch a regression in
|
|
152
|
+
# argparse setup (e.g. missing required=True on subparsers
|
|
153
|
+
# would silently no-op instead of printing usage + exiting 2).
|
|
154
|
+
/tmp/smoke/bin/bot-cmder >/dev/null 2>&1 && (
|
|
155
|
+
echo "ERROR: bot-cmder with no args should exit non-zero"
|
|
156
|
+
exit 1
|
|
157
|
+
) || echo "OK — no-args path exits non-zero as expected"
|
|
158
|
+
|
|
159
|
+
# Phase 7 — secret-scan is its own job (not a step in lint) because
|
|
160
|
+
# it's a security gate, not a style gate. When it fails on a PR the
|
|
161
|
+
# status check name "secret-scan" is unambiguous about what broke,
|
|
162
|
+
# and the red badge is visually distinct from "pre-commit failed
|
|
163
|
+
# because black wanted to reformat". Runs in parallel with lint
|
|
164
|
+
# and test so it doesn't add to total CI wall-clock time.
|
|
165
|
+
secret-scan:
|
|
166
|
+
name: secret-scan
|
|
167
|
+
runs-on: ubuntu-latest
|
|
168
|
+
# gitleaks-action calls /repos/.../pulls/N/commits to enumerate
|
|
169
|
+
# the PR's commits. The default GITHUB_TOKEN doesn't grant that
|
|
170
|
+
# scope (`Resource not accessible by integration` 403 otherwise).
|
|
171
|
+
# Read-only `pull-requests: read` is the minimum that satisfies
|
|
172
|
+
# the API call without giving the action write access to anything.
|
|
173
|
+
permissions:
|
|
174
|
+
contents: read
|
|
175
|
+
pull-requests: read
|
|
176
|
+
steps:
|
|
177
|
+
- name: Check out repo
|
|
178
|
+
uses: actions/checkout@v6
|
|
179
|
+
with:
|
|
180
|
+
# gitleaks needs the FULL history to scan every commit in
|
|
181
|
+
# the PR (not just the merge tip). Default checkout is a
|
|
182
|
+
# shallow clone with depth=1; override to fetch everything.
|
|
183
|
+
fetch-depth: 0
|
|
184
|
+
|
|
185
|
+
- name: Run gitleaks
|
|
186
|
+
# Pinned action version + the same gitleaks binary version
|
|
187
|
+
# we run locally (.pre-commit-config.yaml). Config is
|
|
188
|
+
# auto-discovered from .gitleaks.toml at the repo root.
|
|
189
|
+
uses: gitleaks/gitleaks-action@v2
|
|
190
|
+
env:
|
|
191
|
+
# Required by gitleaks-action@v2 for scanning pull
|
|
192
|
+
# requests (announced as a breaking change in their
|
|
193
|
+
# README). The auto-provisioned per-run token is
|
|
194
|
+
# sufficient — no PAT needed.
|
|
195
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
196
|
+
# Quiet the marketing pop-up about gitleaks-pro that the
|
|
197
|
+
# action would otherwise print on every run for OSS repos.
|
|
198
|
+
GITLEAKS_NOTIFY_USER_LIST: ""
|
|
199
|
+
GITLEAKS_ENABLE_UPLOAD_ARTIFACT: "false"
|
|
200
|
+
GITLEAKS_ENABLE_SUMMARY: "true"
|