teabot 1.0.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.
Files changed (216) hide show
  1. teabot-1.0.0/.github/pull_request_template.md +17 -0
  2. teabot-1.0.0/.github/workflows/ci.yml +38 -0
  3. teabot-1.0.0/.github/workflows/release.yml +76 -0
  4. teabot-1.0.0/.gitignore +36 -0
  5. teabot-1.0.0/.python-version +1 -0
  6. teabot-1.0.0/CHANGELOG.md +15 -0
  7. teabot-1.0.0/CODE_OF_CONDUCT.md +83 -0
  8. teabot-1.0.0/CONTRIBUTING.md +49 -0
  9. teabot-1.0.0/LICENSE +21 -0
  10. teabot-1.0.0/PKG-INFO +243 -0
  11. teabot-1.0.0/README.md +221 -0
  12. teabot-1.0.0/SECURITY.md +20 -0
  13. teabot-1.0.0/docs/README.md +10 -0
  14. teabot-1.0.0/docs/explainer/.gitignore +4 -0
  15. teabot-1.0.0/docs/explainer/README.md +33 -0
  16. teabot-1.0.0/docs/explainer/artifact_page.py +32 -0
  17. teabot-1.0.0/docs/explainer/deck.py +25 -0
  18. teabot-1.0.0/docs/explainer/player.html +170 -0
  19. teabot-1.0.0/docs/explainer/pyproject.toml +24 -0
  20. teabot-1.0.0/docs/explainer/src/teabot_explainer/__init__.py +1 -0
  21. teabot-1.0.0/docs/explainer/src/teabot_explainer/common.py +97 -0
  22. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_architecture.py +406 -0
  23. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_close.py +94 -0
  24. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_hosting.py +373 -0
  25. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_intro.py +73 -0
  26. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_lifecycle.py +369 -0
  27. teabot-1.0.0/docs/explainer/src/teabot_explainer/s_plugins.py +364 -0
  28. teabot-1.0.0/docs/explainer/src/teabot_explainer/theme.py +42 -0
  29. teabot-1.0.0/docs/explainer/src/teabot_explainer/widgets.py +283 -0
  30. teabot-1.0.0/docs/explainer/tests/player_check.py +114 -0
  31. teabot-1.0.0/docs/explainer/uv.lock +540 -0
  32. teabot-1.0.0/docs/glossary.md +28 -0
  33. teabot-1.0.0/docs/implementation.md +187 -0
  34. teabot-1.0.0/docs/plan.md +490 -0
  35. teabot-1.0.0/docs/plugin-authoring.md +147 -0
  36. teabot-1.0.0/docs/self-hosting.md +128 -0
  37. teabot-1.0.0/docs/spikes.md +153 -0
  38. teabot-1.0.0/docs/worklog.md +79 -0
  39. teabot-1.0.0/plugins/README.md +7 -0
  40. teabot-1.0.0/plugins/announcements/LICENSE +21 -0
  41. teabot-1.0.0/plugins/announcements/README.md +9 -0
  42. teabot-1.0.0/plugins/announcements/pyproject.toml +21 -0
  43. teabot-1.0.0/plugins/announcements/src/teabot_announcements/__init__.py +138 -0
  44. teabot-1.0.0/plugins/announcements/src/teabot_announcements/py.typed +0 -0
  45. teabot-1.0.0/plugins/announcements/tests/test_post.py +97 -0
  46. teabot-1.0.0/plugins/announcements/tests/test_schedule.py +108 -0
  47. teabot-1.0.0/plugins/devtools/README.md +8 -0
  48. teabot-1.0.0/plugins/devtools/pyproject.toml +19 -0
  49. teabot-1.0.0/plugins/devtools/src/teabot_devtools/__init__.py +119 -0
  50. teabot-1.0.0/plugins/devtools/src/teabot_devtools/migrations/0001_notes.sql +5 -0
  51. teabot-1.0.0/plugins/devtools/src/teabot_devtools/py.typed +0 -0
  52. teabot-1.0.0/plugins/devtools/tests/test_devtools.py +75 -0
  53. teabot-1.0.0/plugins/devtools_twin/pyproject.toml +19 -0
  54. teabot-1.0.0/plugins/devtools_twin/src/teabot_devtools_twin/__init__.py +17 -0
  55. teabot-1.0.0/plugins/devtools_twin/src/teabot_devtools_twin/py.typed +0 -0
  56. teabot-1.0.0/pyproject.toml +152 -0
  57. teabot-1.0.0/scripts/README.md +5 -0
  58. teabot-1.0.0/scripts/check.sh +13 -0
  59. teabot-1.0.0/scripts/quality.sh +26 -0
  60. teabot-1.0.0/scripts/release/check.py +10 -0
  61. teabot-1.0.0/scripts/release/notes.py +7 -0
  62. teabot-1.0.0/scripts/release/pin.py +7 -0
  63. teabot-1.0.0/scripts/release/release_lib.py +115 -0
  64. teabot-1.0.0/scripts/release/template.py +8 -0
  65. teabot-1.0.0/src/teabot/README.md +18 -0
  66. teabot-1.0.0/src/teabot/__init__.py +5 -0
  67. teabot-1.0.0/src/teabot/__main__.py +5 -0
  68. teabot-1.0.0/src/teabot/adapters/README.md +11 -0
  69. teabot-1.0.0/src/teabot/adapters/__init__.py +1 -0
  70. teabot-1.0.0/src/teabot/adapters/interactions.py +212 -0
  71. teabot-1.0.0/src/teabot/adapters/mcp.py +180 -0
  72. teabot-1.0.0/src/teabot/adapters/schema.py +92 -0
  73. teabot-1.0.0/src/teabot/adapters/service_api.py +78 -0
  74. teabot-1.0.0/src/teabot/app.py +239 -0
  75. teabot-1.0.0/src/teabot/assembly.py +66 -0
  76. teabot-1.0.0/src/teabot/auth/README.md +9 -0
  77. teabot-1.0.0/src/teabot/auth/__init__.py +1 -0
  78. teabot-1.0.0/src/teabot/auth/middleware.py +68 -0
  79. teabot-1.0.0/src/teabot/auth/tokens.py +162 -0
  80. teabot-1.0.0/src/teabot/builtin/README.md +9 -0
  81. teabot-1.0.0/src/teabot/builtin/__init__.py +1 -0
  82. teabot-1.0.0/src/teabot/builtin/admin.py +293 -0
  83. teabot-1.0.0/src/teabot/builtin/config.py +11 -0
  84. teabot-1.0.0/src/teabot/builtin/lookups.py +177 -0
  85. teabot-1.0.0/src/teabot/builtin/tokens.py +173 -0
  86. teabot-1.0.0/src/teabot/cli/README.md +7 -0
  87. teabot-1.0.0/src/teabot/cli/__init__.py +34 -0
  88. teabot-1.0.0/src/teabot/cli/init.py +100 -0
  89. teabot-1.0.0/src/teabot/cli/new_plugin.py +64 -0
  90. teabot-1.0.0/src/teabot/cli/plugins.py +106 -0
  91. teabot-1.0.0/src/teabot/cli/run.py +19 -0
  92. teabot-1.0.0/src/teabot/cli/scaffold/README.md.tmpl +7 -0
  93. teabot-1.0.0/src/teabot/cli/scaffold/migration.sql.tmpl +6 -0
  94. teabot-1.0.0/src/teabot/cli/scaffold/plugin.py.tmpl +54 -0
  95. teabot-1.0.0/src/teabot/cli/scaffold/pyproject.toml.tmpl +26 -0
  96. teabot-1.0.0/src/teabot/cli/scaffold/test_plugin.py.tmpl +21 -0
  97. teabot-1.0.0/src/teabot/core/README.md +13 -0
  98. teabot-1.0.0/src/teabot/core/__init__.py +1 -0
  99. teabot-1.0.0/src/teabot/core/audit.py +193 -0
  100. teabot-1.0.0/src/teabot/core/command_sync.py +103 -0
  101. teabot-1.0.0/src/teabot/core/config.py +40 -0
  102. teabot-1.0.0/src/teabot/core/lifecycle.py +222 -0
  103. teabot-1.0.0/src/teabot/core/loader.py +207 -0
  104. teabot-1.0.0/src/teabot/core/maintenance.py +55 -0
  105. teabot-1.0.0/src/teabot/core/plans.py +190 -0
  106. teabot-1.0.0/src/teabot/core/plugin_jobs.py +42 -0
  107. teabot-1.0.0/src/teabot/core/processes.py +123 -0
  108. teabot-1.0.0/src/teabot/core/queue.py +203 -0
  109. teabot-1.0.0/src/teabot/core/registry.py +344 -0
  110. teabot-1.0.0/src/teabot/core/worker.py +112 -0
  111. teabot-1.0.0/src/teabot/db/README.md +9 -0
  112. teabot-1.0.0/src/teabot/db/__init__.py +1 -0
  113. teabot-1.0.0/src/teabot/db/bootstrap.py +74 -0
  114. teabot-1.0.0/src/teabot/db/lockkeys.py +17 -0
  115. teabot-1.0.0/src/teabot/db/migrate.py +223 -0
  116. teabot-1.0.0/src/teabot/db/migrations/0001_core.sql +54 -0
  117. teabot-1.0.0/src/teabot/db/migrations/0002_audit.sql +79 -0
  118. teabot-1.0.0/src/teabot/db/migrations/0003_drafts.sql +12 -0
  119. teabot-1.0.0/src/teabot/db/migrations/0004_command_sync.sql +9 -0
  120. teabot-1.0.0/src/teabot/db/migrations/0005_tokens.sql +18 -0
  121. teabot-1.0.0/src/teabot/db/naming.py +42 -0
  122. teabot-1.0.0/src/teabot/db/pool.py +269 -0
  123. teabot-1.0.0/src/teabot/db/types.py +9 -0
  124. teabot-1.0.0/src/teabot/envfile.py +28 -0
  125. teabot-1.0.0/src/teabot/health.py +52 -0
  126. teabot-1.0.0/src/teabot/logs.py +60 -0
  127. teabot-1.0.0/src/teabot/py.typed +0 -0
  128. teabot-1.0.0/src/teabot/runtime.py +140 -0
  129. teabot-1.0.0/src/teabot/sdk/README.md +14 -0
  130. teabot-1.0.0/src/teabot/sdk/__init__.py +70 -0
  131. teabot-1.0.0/src/teabot/sdk/actions.py +132 -0
  132. teabot-1.0.0/src/teabot/sdk/db.py +61 -0
  133. teabot-1.0.0/src/teabot/sdk/deprecation.py +56 -0
  134. teabot-1.0.0/src/teabot/sdk/fields.py +70 -0
  135. teabot-1.0.0/src/teabot/sdk/ids.py +25 -0
  136. teabot-1.0.0/src/teabot/sdk/inputs.py +96 -0
  137. teabot-1.0.0/src/teabot/sdk/plans.py +33 -0
  138. teabot-1.0.0/src/teabot/sdk/plugin.py +97 -0
  139. teabot-1.0.0/src/teabot/settings.py +61 -0
  140. teabot-1.0.0/src/teabot/testing/README.md +9 -0
  141. teabot-1.0.0/src/teabot/testing/__init__.py +43 -0
  142. teabot-1.0.0/src/teabot/testing/cleanup.py +68 -0
  143. teabot-1.0.0/src/teabot/testing/fakes.py +178 -0
  144. teabot-1.0.0/src/teabot/testing/fixtures.py +120 -0
  145. teabot-1.0.0/src/teabot/testing/harness.py +88 -0
  146. teabot-1.0.0/src/teabot/web.py +49 -0
  147. teabot-1.0.0/template/.dockerignore +5 -0
  148. teabot-1.0.0/template/.gitignore +5 -0
  149. teabot-1.0.0/template/Dockerfile +17 -0
  150. teabot-1.0.0/template/LICENSE +21 -0
  151. teabot-1.0.0/template/README.md +76 -0
  152. teabot-1.0.0/template/compose.yaml +45 -0
  153. teabot-1.0.0/template/deploy/bare-metal/deploy.sh +23 -0
  154. teabot-1.0.0/template/deploy/bare-metal/lib.sh +46 -0
  155. teabot-1.0.0/template/deploy/bare-metal/pgbackrest/pgbackrest-backup@.service +11 -0
  156. teabot-1.0.0/template/deploy/bare-metal/pgbackrest/pgbackrest-diff.timer +12 -0
  157. teabot-1.0.0/template/deploy/bare-metal/pgbackrest/pgbackrest-full.timer +12 -0
  158. teabot-1.0.0/template/deploy/bare-metal/pgbackrest/pgbackrest.conf +22 -0
  159. teabot-1.0.0/template/deploy/bare-metal/postgresql/teabot-archive.conf +6 -0
  160. teabot-1.0.0/template/deploy/bare-metal/postgresql/teabot.conf +16 -0
  161. teabot-1.0.0/template/deploy/bare-metal/restore-drill.sh +63 -0
  162. teabot-1.0.0/template/deploy/bare-metal/set-discord-token.sh +15 -0
  163. teabot-1.0.0/template/deploy/bare-metal/setup-backups.sh +70 -0
  164. teabot-1.0.0/template/deploy/bare-metal/setup-host.sh +166 -0
  165. teabot-1.0.0/template/deploy/bare-metal/teabot.caddy +15 -0
  166. teabot-1.0.0/template/deploy/bare-metal/teabot.service +44 -0
  167. teabot-1.0.0/template/plugins/README.md +5 -0
  168. teabot-1.0.0/template/pyproject.toml +20 -0
  169. teabot-1.0.0/template/teabot.env.example +34 -0
  170. teabot-1.0.0/tests/README.md +8 -0
  171. teabot-1.0.0/tests/__init__.py +0 -0
  172. teabot-1.0.0/tests/adapters/test_interactions.py +371 -0
  173. teabot-1.0.0/tests/adapters/test_mcp.py +368 -0
  174. teabot-1.0.0/tests/adapters/test_schema.py +150 -0
  175. teabot-1.0.0/tests/adapters/test_service_api.py +242 -0
  176. teabot-1.0.0/tests/auth/test_middleware.py +79 -0
  177. teabot-1.0.0/tests/auth/test_tokens.py +138 -0
  178. teabot-1.0.0/tests/builtin/test_admin.py +224 -0
  179. teabot-1.0.0/tests/builtin/test_lookups.py +152 -0
  180. teabot-1.0.0/tests/builtin/test_token_commands.py +136 -0
  181. teabot-1.0.0/tests/conftest.py +30 -0
  182. teabot-1.0.0/tests/core/sweep_migrations/0001_items.sql +1 -0
  183. teabot-1.0.0/tests/core/test_audit.py +197 -0
  184. teabot-1.0.0/tests/core/test_command_sync.py +197 -0
  185. teabot-1.0.0/tests/core/test_config.py +71 -0
  186. teabot-1.0.0/tests/core/test_guilds.py +201 -0
  187. teabot-1.0.0/tests/core/test_lifecycle.py +198 -0
  188. teabot-1.0.0/tests/core/test_loader.py +241 -0
  189. teabot-1.0.0/tests/core/test_maintenance.py +83 -0
  190. teabot-1.0.0/tests/core/test_plans.py +211 -0
  191. teabot-1.0.0/tests/core/test_plugin_jobs.py +114 -0
  192. teabot-1.0.0/tests/core/test_processes.py +117 -0
  193. teabot-1.0.0/tests/core/test_queue.py +265 -0
  194. teabot-1.0.0/tests/core/test_registry.py +284 -0
  195. teabot-1.0.0/tests/db/test_bootstrap.py +74 -0
  196. teabot-1.0.0/tests/db/test_init_cli.py +96 -0
  197. teabot-1.0.0/tests/db/test_migrate.py +216 -0
  198. teabot-1.0.0/tests/db/test_pool.py +177 -0
  199. teabot-1.0.0/tests/release/test_release_lib.py +116 -0
  200. teabot-1.0.0/tests/sdk/test_contracts.py +24 -0
  201. teabot-1.0.0/tests/sdk/test_deprecation.py +50 -0
  202. teabot-1.0.0/tests/sdk/test_sdk_docs.py +42 -0
  203. teabot-1.0.0/tests/sdk/test_sdk_types.py +133 -0
  204. teabot-1.0.0/tests/test_app.py +150 -0
  205. teabot-1.0.0/tests/test_envfile.py +24 -0
  206. teabot-1.0.0/tests/test_licence.py +17 -0
  207. teabot-1.0.0/tests/test_new_plugin_cli.py +89 -0
  208. teabot-1.0.0/tests/test_pg_cleanup.py +55 -0
  209. teabot-1.0.0/tests/test_plugins_cli.py +37 -0
  210. teabot-1.0.0/tests/test_purge_cli.py +91 -0
  211. teabot-1.0.0/tests/test_runtime.py +185 -0
  212. teabot-1.0.0/tests/test_settings.py +101 -0
  213. teabot-1.0.0/tests/test_skeleton.py +26 -0
  214. teabot-1.0.0/tests/test_web.py +60 -0
  215. teabot-1.0.0/tests/testing/test_harness.py +103 -0
  216. teabot-1.0.0/uv.lock +2177 -0
@@ -0,0 +1,17 @@
1
+ ## What and why
2
+
3
+ <!-- What changes, and why. Name the decision (D-number) or work package it serves. -->
4
+
5
+ ## How it was checked
6
+
7
+ - [ ] `scripts/check.sh` passes
8
+ - [ ] `scripts/quality.sh` passes: CRAP under 10, no duplicates at 0.8, no surviving mutants in changed files
9
+
10
+ ## For a plugin
11
+
12
+ - [ ] Imports only `teabot.sdk`
13
+ - [ ] The README's "External connections" section lists every host contacted (D12)
14
+ - [ ] Every table has a `bigint guild_id` column, and no released migration was edited (D9)
15
+ - [ ] No Discord or HTTP awaits inside a transaction block, and no blocking I/O (D30, D33)
16
+ - [ ] No dynamic SQL built from member input
17
+ - [ ] Jobs are safe to run twice (D31)
@@ -0,0 +1,38 @@
1
+ name: CI
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ workflow_dispatch:
7
+
8
+ jobs:
9
+ checks:
10
+ runs-on: ubuntu-latest
11
+ strategy:
12
+ matrix:
13
+ python: ["3.12", "3.13"]
14
+ services:
15
+ postgres:
16
+ image: postgres:16
17
+ env:
18
+ POSTGRES_USER: teabot_owner
19
+ POSTGRES_PASSWORD: teabot
20
+ POSTGRES_DB: teabot
21
+ ports:
22
+ - 55432:5432
23
+ options: >-
24
+ --health-cmd "pg_isready -U teabot_owner -d teabot"
25
+ --health-interval 2s
26
+ --health-timeout 3s
27
+ --health-retries 30
28
+ steps:
29
+ - uses: actions/checkout@v7.0.1
30
+ - uses: astral-sh/setup-uv@v10.3.3
31
+ with:
32
+ python-version: ${{ matrix.python }}
33
+ - run: uv sync --locked
34
+ - run: uv run ruff check
35
+ - run: uv run ruff format --check
36
+ - run: uv run pyright
37
+ - run: uv run lint-imports
38
+ - run: uv run pytest
@@ -0,0 +1,76 @@
1
+ name: Release
2
+
3
+ # A tag v<major>.<minor>.<patch> releases teabot and teabot-announcements to PyPI through trusted publishing,
4
+ # then attaches the instance template, locked against them, to a GitHub release (WP7.5, D42, D48).
5
+ on:
6
+ push:
7
+ tags: ["v*"]
8
+
9
+ permissions:
10
+ contents: read
11
+
12
+ jobs:
13
+ build:
14
+ runs-on: ubuntu-latest
15
+ steps:
16
+ - uses: actions/checkout@v7.0.1
17
+ - uses: astral-sh/setup-uv@v10.3.3
18
+ with:
19
+ python-version: "3.12"
20
+ - name: Check the versions, the changelog and the template against the tag
21
+ run: uv run --no-project python scripts/release/check.py "$GITHUB_REF_NAME"
22
+ - name: Pin the plugin to the core it is released with
23
+ run: uv run --no-project python scripts/release/pin.py "$GITHUB_REF_NAME"
24
+ - name: Build both packages
25
+ run: |
26
+ uv build --package teabot -o dist
27
+ uv build --package teabot-announcements -o dist
28
+ - uses: actions/upload-artifact@v7.0.2
29
+ with:
30
+ name: dist
31
+ path: dist/
32
+
33
+ publish:
34
+ needs: build
35
+ runs-on: ubuntu-latest
36
+ # PyPI's trusted publisher for each package names this workflow and this environment.
37
+ environment: pypi
38
+ permissions:
39
+ id-token: write
40
+ steps:
41
+ - uses: actions/download-artifact@v8.0.2
42
+ with:
43
+ name: dist
44
+ path: dist/
45
+ - uses: astral-sh/setup-uv@v10.3.3
46
+ # One step per package, the core first: PyPI lets a pending publisher's settings serve one new project at
47
+ # a time. --check-url skips files already uploaded, so a re-run carries on where the last one stopped.
48
+ - name: Publish teabot
49
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/ dist/teabot-*
50
+ - name: Publish teabot-announcements
51
+ run: uv publish --trusted-publishing always --check-url https://pypi.org/simple/ dist/teabot_announcements-*
52
+
53
+ github-release:
54
+ needs: publish
55
+ runs-on: ubuntu-latest
56
+ permissions:
57
+ contents: write
58
+ steps:
59
+ - uses: actions/checkout@v7.0.1
60
+ - uses: astral-sh/setup-uv@v10.3.3
61
+ with:
62
+ python-version: "3.12"
63
+ - uses: actions/download-artifact@v8.0.2
64
+ with:
65
+ name: dist
66
+ path: dist/
67
+ - name: Build the template, locked against the packages just published
68
+ run: uv run --no-project python scripts/release/template.py "$GITHUB_REF_NAME" teabot-template.tar.gz
69
+ - name: Write the release notes from the changelog
70
+ run: uv run --no-project python scripts/release/notes.py "$GITHUB_REF_NAME" > notes.md
71
+ - name: Create the GitHub release
72
+ env:
73
+ GH_TOKEN: ${{ github.token }}
74
+ run: >-
75
+ gh release create "$GITHUB_REF_NAME" teabot-template.tar.gz dist/*
76
+ --title "teabot ${GITHUB_REF_NAME#v}" --notes-file notes.md
@@ -0,0 +1,36 @@
1
+ # scratch space for experiments and test output
2
+ tmp/
3
+
4
+ # local Claude Code worktrees
5
+ .claude/worktrees/
6
+
7
+ # Python
8
+ __pycache__/
9
+ *.py[cod]
10
+ .venv/
11
+ *.egg-info/
12
+ build/
13
+ dist/
14
+
15
+ # tooling caches
16
+ .pytest_cache/
17
+ .ruff_cache/
18
+ .pyright/
19
+ .coverage
20
+ htmlcov/
21
+
22
+ # local data and secrets
23
+ *.db
24
+ *.db-wal
25
+ *.db-shm
26
+ *.env
27
+ !*.env.example
28
+
29
+ # editors and OS
30
+ .idea/
31
+ .vscode/
32
+ .DS_Store
33
+
34
+ # quality-gate tool output (crapper, dryer, mutator)
35
+ .metrics/
36
+ target/
@@ -0,0 +1 @@
1
+ 3.13
@@ -0,0 +1,15 @@
1
+ # Changelog
2
+
3
+ Every release of `teabot`, `teabot-announcements` and the instance template, newest first. The versions follow semantic versioning, which `teabot.sdk` keeps from 1.0 (D45). A **For operators** section lists any manual step an upgrade needs (D48).
4
+
5
+ ## 1.0.0 - 2026-11-14
6
+
7
+ The first release.
8
+
9
+ - A plugin framework for Discord bots. Each action a plugin offers is a slash command, an MCP tool and a service API route at once, behind one call path that checks the member's Discord permissions on every call and records an audit trail.
10
+ - `teabot.sdk` 1.0: actions, jobs, per-server config, each plugin's own tables in its own schema and role, and plan then apply for destructive actions.
11
+ - Tokens for LLM clients and event backends, tied to a member and narrowed by risk and by action.
12
+ - Servers that add and remove the bot, with their data kept for a grace period and then purged.
13
+ - `teabot-announcements`: post an announcement now, or schedule it.
14
+ - `teabot init`, `teabot run`, `teabot plugins list | purge` and `teabot new-plugin`.
15
+ - The instance template, with a quick setup on Docker Compose and a careful one on a bare-metal host with continuous backups.
@@ -0,0 +1,83 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone, regardless of age, body size, visible or invisible disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, caste, color, religion, or sexual identity and orientation.
6
+
7
+ We pledge to act and interact in ways that contribute to an open, welcoming, diverse, inclusive, and healthy community.
8
+
9
+ ## Our Standards
10
+
11
+ Examples of behavior that contributes to a positive environment for our community include:
12
+
13
+ * Demonstrating empathy and kindness toward other people
14
+ * Being respectful of differing opinions, viewpoints, and experiences
15
+ * Giving and gracefully accepting constructive feedback
16
+ * Accepting responsibility and apologizing to those affected by our mistakes, and learning from the experience
17
+ * Focusing on what is best not just for us as individuals, but for the overall community
18
+
19
+ Examples of unacceptable behavior include:
20
+
21
+ * The use of sexualized language or imagery, and sexual attention or advances of any kind
22
+ * Trolling, insulting or derogatory comments, and personal or political attacks
23
+ * Public or private harassment
24
+ * Publishing others' private information, such as a physical or email address, without their explicit permission
25
+ * Other conduct which could reasonably be considered inappropriate in a professional setting
26
+
27
+ ## Enforcement Responsibilities
28
+
29
+ Community leaders are responsible for clarifying and enforcing our standards of acceptable behavior and will take appropriate and fair corrective action in response to any behavior that they deem inappropriate, threatening, offensive, or harmful.
30
+
31
+ Community leaders have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, and will communicate reasons for moderation decisions when appropriate.
32
+
33
+ ## Scope
34
+
35
+ This Code of Conduct applies within all community spaces, and also applies when an individual is officially representing the community in public spaces. Examples of representing our community include using an official e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event.
36
+
37
+ ## Enforcement
38
+
39
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be reported to the community leaders responsible for enforcement at the repository owner, [@hballington12](https://github.com/hballington12), through the contact details on their GitHub profile. All complaints will be reviewed and investigated promptly and fairly.
40
+
41
+ All community leaders are obligated to respect the privacy and security of the reporter of any incident.
42
+
43
+ ## Enforcement Guidelines
44
+
45
+ Community leaders will follow these Community Impact Guidelines in determining the consequences for any action they deem in violation of this Code of Conduct:
46
+
47
+ ### 1. Correction
48
+
49
+ **Community Impact**: Use of inappropriate language or other behavior deemed unprofessional or unwelcome in the community.
50
+
51
+ **Consequence**: A private, written warning from community leaders, providing clarity around the nature of the violation and an explanation of why the behavior was inappropriate. A public apology may be requested.
52
+
53
+ ### 2. Warning
54
+
55
+ **Community Impact**: A violation through a single incident or series of actions.
56
+
57
+ **Consequence**: A warning with consequences for continued behavior. No interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, for a specified period of time. This includes avoiding interactions in community spaces as well as external channels like social media. Violating these terms may lead to a temporary or permanent ban.
58
+
59
+ ### 3. Temporary Ban
60
+
61
+ **Community Impact**: A serious violation of community standards, including sustained inappropriate behavior.
62
+
63
+ **Consequence**: A temporary ban from any sort of interaction or public communication with the community for a specified period of time. No public or private interaction with the people involved, including unsolicited interaction with those enforcing the Code of Conduct, is allowed during this period. Violating these terms may lead to a permanent ban.
64
+
65
+ ### 4. Permanent Ban
66
+
67
+ **Community Impact**: Demonstrating a pattern of violation of community standards, including sustained inappropriate behavior, harassment of an individual, or aggression toward or disparagement of classes of individuals.
68
+
69
+ **Consequence**: A permanent ban from any sort of public interaction within the community.
70
+
71
+ ## Attribution
72
+
73
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 2.1, available at [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1].
74
+
75
+ Community Impact Guidelines were inspired by [Mozilla's code of conduct enforcement ladder][Mozilla CoC].
76
+
77
+ For answers to common questions about this code of conduct, see the FAQ at [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at [https://www.contributor-covenant.org/translations][translations].
78
+
79
+ [homepage]: https://www.contributor-covenant.org
80
+ [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html
81
+ [Mozilla CoC]: https://github.com/mozilla/diversity
82
+ [FAQ]: https://www.contributor-covenant.org/faq
83
+ [translations]: https://www.contributor-covenant.org/translations
@@ -0,0 +1,49 @@
1
+ # Contributing to teabot
2
+
3
+ ## Before you start
4
+
5
+ - The design is in the [README](README.md), as numbered decisions such as (D21). A change that goes against a decision starts as an issue proposing to change it.
6
+ - Report a bug as an issue with the steps that show it. Report a security problem as [SECURITY.md](SECURITY.md) describes.
7
+ - A plugin for your own instance needs nobody's approval (D44); [docs/plugin-authoring.md](docs/plugin-authoring.md) covers writing one. First-party plugins in this repository follow its review checklist.
8
+
9
+ ## Setting up
10
+
11
+ ```
12
+ git clone https://github.com/hballington12/teabot.git
13
+ cd teabot
14
+ uv sync
15
+ docker compose -f template/compose.yaml up -d --wait postgres
16
+ ```
17
+
18
+ - uv runs everything: every command is `uv run ...`.
19
+ - The tests use that Postgres on `127.0.0.1:55432`. Each test gets its own database and roles, and drops them after.
20
+
21
+ ## Checks
22
+
23
+ - `scripts/check.sh` runs what CI runs: ruff's formatting and lint, pyright in strict mode, the import-linter contracts and pytest. Run it before every push.
24
+ - `scripts/quality.sh` runs the stricter gates: CRAP under 10, no near-duplicate code, and no surviving mutants in the files you changed. It needs crapper, dryer and mutator on your PATH.
25
+
26
+ ## Making a change
27
+
28
+ - One change per pull request, with tests that fail without it.
29
+ - Keep functions short and their nesting shallow, and modularise one that passes 150 lines. Each directory's README is a bullet list of what is in it.
30
+ - A commit message has a short title, then says why, what changed, and how it was proven.
31
+ - `teabot.sdk` follows semantic versioning (D45). An addition is a minor release. A breaking change needs a major release, and the old form first keeps working with a warning from `teabot.sdk.deprecation` for at least one minor release.
32
+ - A released migration never changes. Add a new numbered file instead.
33
+ - Add a line to [CHANGELOG.md](CHANGELOG.md) under **Unreleased**, with a **For operators** note when upgrading needs a manual step.
34
+
35
+ ## Releasing
36
+
37
+ For the maintainers. `teabot` and `teabot-announcements` are released together under one version.
38
+
39
+ 1. In one commit on `main`, set that version in `pyproject.toml` and `plugins/announcements/pyproject.toml`, and rename the changelog's **Unreleased** heading to `## <version> - <date>`.
40
+ 2. Tag it and push the tag: `git tag v<version>` and `git push origin v<version>`.
41
+ 3. The release workflow checks the versions, the changelog and the template against the tag, publishes both packages to PyPI, and attaches the locked template to a GitHub release with the changelog section as its notes.
42
+ 4. Set the versions to the next `.dev0` and add a new **Unreleased** heading.
43
+
44
+ A new major version also moves the template's ranges, such as `teabot>=2.0,<3`.
45
+
46
+ ## Licence and conduct
47
+
48
+ - Contributions are under the [MIT licence](LICENSE), as the rest of the project is.
49
+ - Everyone taking part follows the [code of conduct](CODE_OF_CONDUCT.md).
teabot-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 the teabot contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
teabot-1.0.0/PKG-INFO ADDED
@@ -0,0 +1,243 @@
1
+ Metadata-Version: 2.5
2
+ Name: teabot
3
+ Version: 1.0.0
4
+ Summary: A plugin-based Discord bot framework, built to self-host and to scale
5
+ License-Expression: MIT
6
+ License-File: LICENSE
7
+ Requires-Python: >=3.12
8
+ Requires-Dist: asyncpg>=0.30
9
+ Requires-Dist: discord-py>=2.6
10
+ Requires-Dist: mcp<3,>=2.3
11
+ Requires-Dist: pydantic-settings>=2.7
12
+ Requires-Dist: pydantic>=2.11
13
+ Requires-Dist: starlette>=0.47
14
+ Requires-Dist: typer>=0.29
15
+ Requires-Dist: uvicorn>=0.35
16
+ Provides-Extra: testing
17
+ Requires-Dist: psycopg[binary]>=3.3; extra == 'testing'
18
+ Requires-Dist: pytest-asyncio>=1.5; extra == 'testing'
19
+ Requires-Dist: pytest-postgresql>=10.0; extra == 'testing'
20
+ Requires-Dist: pytest>=9.2; extra == 'testing'
21
+ Description-Content-Type: text/markdown
22
+
23
+ # teabot
24
+
25
+ An open-source framework for a general Discord bot built from plugins, in the spirit of Carl-bot. Anyone can run their own teabot for their own servers, write their own plugins for it, and grow it from one process to many. A hosted teabot, for people who just want a bot that works, is planned on the same code.
26
+
27
+ The Tea clan runs the first instance, on its droplet, and is the source of the first requirements. Staff use it through Discord slash commands, and LLM clients such as Claude Code use it through an authenticated MCP server.
28
+
29
+ To run your own teabot, follow [`docs/self-hosting.md`](docs/self-hosting.md); to write plugins for it, [`docs/plugin-authoring.md`](docs/plugin-authoring.md). The concrete build plan lives in [`docs/plan.md`](docs/plan.md), and the order of work in [`docs/implementation.md`](docs/implementation.md).
30
+
31
+ ## Goals
32
+
33
+ - An MIT-licensed framework that anyone can self-host, with first-party plugins, a stable plugin SDK and a template for new instances (D41 to D48).
34
+ - One long-running Python process to begin with, with Postgres beside it and a reverse proxy in front. The same code scales out to several processes and hosts when one is no longer enough (D40).
35
+ - A small core that holds the Discord connection, the database, plugin config, tokens and permissions, a durable work queue and an audit trail.
36
+ - Plugins provide every feature. Whoever runs an instance decides which plugins it runs, including ones they wrote themselves. A plugin hub may list plugins later.
37
+ - Three ways in:
38
+ - Discord slash commands for members (interacting with what staff set up) and for staff (control).
39
+ - An MCP server over HTTPS, so an LLM can do admin work for a staff member in a server that person is authorised for.
40
+ - A service API for other programs, such as an event backend that pings a team when something happens.
41
+ - The bot holds near-Administrator power in every server it joins. Safety comes from the plugin protocol, the permission checks and the audit trail.
42
+ - Over time, plugins replace a server's dependence on other bots (Carl-bot, MEE6 and similar).
43
+
44
+ ## Pain points it addresses
45
+
46
+ - **Bulk work hits rate limits.** Changing roles on hundreds of members takes hundreds of separate requests. An LLM making one tool call per member is slow, costly and hard to review.
47
+ - **Event setup and teardown repeat.** Team roles, team channels, permission overwrites, feed channels and captain roles are made for every event and archived afterwards. Past event bots rebuilt the same roster and role-gate code each time.
48
+ - **Discord has hard caps.** 250 roles and 500 channels per server, and 50 channels per category. Long-lived event channels hit these unless they are archived.
49
+ - **Moderation needs a trail.** Joins, leaves, kicks, bans and deleted messages need recording. With an LLM acting for staff, the trail must also say which human asked for each action.
50
+ - **Several bots overlap.** Each has its own config, dashboard and permission model, and staff have to know which bot does what.
51
+ - **Identity across game and Discord is messy.** Mains, alts, RSN changes, and clan membership that only partly overlaps with Discord membership. That plugin is out of scope for now, and the core must not make it harder later.
52
+ - **Timezones vary.** Timers, giveaways and event starts are stored in UTC and shown as Discord timestamps.
53
+ - **Multi-server needs isolation.** An action in one server never touches another. A staff member in server A has no rights in server B. A server's admins can switch off any plugin.
54
+ - **Plugins come and go.** A plugin's tables, config and jobs must survive being disabled, survive an upgrade that changes their shape, and be removable completely when the plugin is gone for good.
55
+ - **Growth must not force a rewrite.** Bots that start small and succeed end up rebuilding their storage and sharding. teabot makes the choices that scale before the first plugin is written.
56
+ - **Hosted bots are closed.** A server that outgrows a hosted bot's features has nowhere to go. With teabot, the same bot can be self-hosted and extended.
57
+
58
+ ## Decisions
59
+
60
+ The design targets a framework that many people run and that grows to many servers and large member counts, starting from the Tea clan's instance. The **operator** in these decisions is whoever runs an instance: the Tea clan for its own, any self-hoster for theirs, and the teabot maintainers for the hosted service. Work that only a later need calls for is listed under deferred work in [`docs/plan.md`](docs/plan.md#deferred-work), with the trigger that brings it in.
61
+
62
+ ### Architecture
63
+
64
+ 1. **One action registry with several front doors.** A plugin registers actions, each with a typed input model, a required permission and a risk level. Slash commands, MCP tools and the service API are thin adapters over that registry. Every call goes through the same permission check and audit log, including the core's own tools.
65
+ 2. **Everything is scoped to one server.** Every action runs with a context for a single server, and the plugin API hands out nothing that reaches across servers. Every plugin table carries a `guild_id` column, which the migration runner checks (D9), and so does every core table that holds a server's data. Bookkeeping tables such as `core.migrations` hold none. Plugin config is stored per server.
66
+ 3. **Installing a plugin and enabling it are separate steps.** The operator installs plugins, which is the trusted, code-reviewed step. Each server's admins enable and configure the installed plugins they want. A plugin's commands appear only in servers where it is enabled. The full lifecycle is D7.
67
+ 4. **Plugins are Python packages found through entry points and pinned in the instance's lockfile.**
68
+ - The loader loads every installed package that declares a `teabot.plugins` entry point. Nothing checks a signature or asks the teabot maintainers, so an operator can run plugins they wrote themselves (D44).
69
+ - A plugin can be a package inside the instance's own repo, a git dependency pinned as `git+<repo>@<commit>`, or a release from PyPI. The instance's `uv.lock` records the hashes of all of them.
70
+ - An update bumps the pin, and the operator reviews the diff between the old and new versions. This is the RuneLite Plugin Hub model, where a plugin is a repository and a pinned commit, and the lockfile plays the part of the hub's signed manifest. A future plugin hub is a catalogue of such pins.
71
+ 5. **Plugins run in the bot's process with no sandbox.** The operator trusts the plugins they install, so the operator's code review is the security boundary, as it is on the RuneLite Plugin Hub. The database roles of D9 catch mistakes, and they do not replace review. Because nothing is sandboxed, the hosted service runs only plugins the teabot maintainers have reviewed (D47).
72
+ 6. **The protocol holds what current plugins use, and grows by addition.**
73
+ - From the start a plugin can declare:
74
+ - actions, which become slash commands, MCP tools and service routes, on all three front doors unless the action narrows its surfaces
75
+ - modals for multi-line `LongText` input (D29)
76
+ - scheduled jobs
77
+ - a config model (D10) and its own tables with migrations (D9)
78
+ - the permissions its actions require
79
+ - Added when a plugin needs them: Discord event hooks for joins, leaves, messages and reactions (planned for SDK 1.1), buttons and select menus, dependencies on other plugins, autocomplete, context-menu commands, voice hooks, MCP resources, HTTP routes, a per-member store, a hook for deleting a member's data (D11) and a machine-readable list of external hosts (D12).
80
+ - Each addition is a manifest field with a default or an optional protocol detected with `hasattr`, so existing plugins keep working without changes. The protocol is published as `teabot.sdk` (D45).
81
+
82
+ ### Plugins and data
83
+
84
+ 7. **A plugin has six lifecycle steps, and its data is kept until it is purged.**
85
+ - The operator installs, upgrades, uninstalls, blocks and purges. Server admins enable and disable.
86
+ - Disabling, uninstalling and blocking keep the plugin's schema, config and jobs. A reinstall carries on where it left off.
87
+ - A plugin a process has not loaded, whether blocked, uninstalled or failed, has one status with a reason. It keeps its data and cannot be enabled. Each process reports its statuses through `/healthz` and `/plugins list`, and holds a shared advisory lock for each plugin it loaded (D40).
88
+ - Purging is the only step that deletes data, and it is an operator CLI command (D34), because it cannot be undone and affects every server. It runs while the bot is up, and refuses unless it can take that plugin's lock exclusively, which fails while any process has the plugin loaded.
89
+ - What each step does is tabled in [`docs/plan.md`](docs/plan.md#plugin-lifecycle).
90
+ 8. **Plugin data lives in config and in the plugin's own tables.**
91
+ - **Config** is what a server's admins set: channels, durations, role choices. It is one Pydantic model per plugin and server, stored as JSONB (D10).
92
+ - **Own tables** hold everything the plugin writes itself: giveaway entries, posted message IDs, moderation records. Data that needs queries such as leaderboards needs tables anyway, and a single row of plugin state is a one-row table.
93
+ - A per-member store in the style of Red-DiscordBot's `Config` API can come back by addition if several plugins end up writing the same small per-member tables.
94
+ 9. **Each plugin owns a Postgres schema and role, and changes it with forward-only numbered SQL migrations.**
95
+ - Plugin `x` gets the schema `plugin_x` and the role `teabot_plugin_x`. The role can create and use objects in its own schema and nothing else. It has no rights on the `core` schema or on other plugins' schemas, so a stray query or foreign key into another plugin fails at once.
96
+ - Plugin code reaches Postgres through its own pool, logged in as `teabot_plugins`, a role with no rights of its own that is a member of every plugin role. `EXECUTE` on `set_config` is revoked from `PUBLIC`, and the `ctx.db` helpers send every statement as a prepared statement, which runs exactly one statement per call. An SQL injection bug in a plugin therefore cannot change the role, and cannot reach `core`, where token hashes live. Reaching another plugin would need that plugin to run dynamic SQL built from untrusted input in a plpgsql function, which the authoring guide forbids and review checks. This matters most for third-party plugins (D44). Spike S1 in [`docs/spikes.md`](docs/spikes.md) has the evidence.
97
+ - Each plugin ships `migrations/0001_<name>.sql`, `0002_<name>.sql` and so on. `core.migrations` records the applied version per plugin, and each plugin's history is independent of every other.
98
+ - A plugin's migrations run as its role, with `search_path` set to its schema. Each file runs as one script inside its own transaction, which Postgres supports for every DDL statement used here, so a failed migration leaves nothing half applied. The script is sent whole, so functions and triggers with dollar quoting work.
99
+ - Before the transaction commits, the runner checks that every table in the plugin's schema has a `bigint` column named `guild_id`. A table without one fails the migration. This is what lets the core delete a server's data with no plugin code (D49).
100
+ - The core's tables live in the schema `core` and use the same runner.
101
+ - Migrations only go forward. Keeping the previous release of the plugin working (add first, remove in a later release) is recommended now, and becomes required when shard clusters arrive, because only then do two releases run against one schema (D15). A self-hoster who skips releases gets every file in order.
102
+ - Purge is `DROP SCHEMA plugin_x CASCADE` and `DROP ROLE teabot_plugin_x`, so it needs no plugin code.
103
+ - This replaces Alembic, whose shared revision graph breaks once an uninstalled plugin's scripts are gone.
104
+ 10. **Config models evolve through Pydantic alone.**
105
+ - New fields get defaults.
106
+ - Renamed fields keep reading old data through `validation_alias=AliasChoices("new", "old")`.
107
+ - Removed fields are ignored with `extra="ignore"`.
108
+ - A reshape that none of these cover goes in a `model_validator(mode="before")`.
109
+ - Config is saved back in the new shape the next time it is written. Plugin tests keep stored samples from earlier versions and check that they still validate.
110
+ 11. **Deleting a member's data arrives with the first plugin that keeps it.** No plugin keeps per-member data yet. When the first one does, `teabot users delete` and a `delete_user_data` plugin hook, as in Red-DiscordBot, land together. Discord's developer policy expects bots to honour deletion requests, and the hosted service (D47) needs this from the start.
111
+ 12. **External connections are declared for review.** Every plugin's README has an "External connections" section listing each host it contacts, and the PR template asks for it. The reviewer checks the list against the code. The hosted service moves the list into the manifest, adds RuneLite-style consent at enable time and adds an HTTP client that refuses undeclared hosts.
112
+ 13. **The operator can block a plugin everywhere.** Listing it in `TEABOT_DISABLED_PLUGINS` in `teabot.env` and restarting stops it in every server, keeps its data and refuses enabling until it is removed from the list. This plays the part of RuneLite delisting a plugin.
113
+ 14. **A broken plugin never takes down the bot.**
114
+ - A plugin that fails to import, fails `setup` or fails a migration is not loaded, with the reason `failed`. Everything else starts. `/healthz`, `/plugins list` and the operator CLI show the failure.
115
+ - A plugin released outside the teabot monorepo declares its compatible SDK in its `pyproject.toml`, for example `teabot>=1.2,<2`, and uv refuses to lock an incompatible combination, so a mismatch shows up before any deploy. First-party plugins are released together with the core and pin it exactly.
116
+ 15. **Installs and migrations happen at restart.** Reloading Python packages inside a running process is fragile, and installs are rare and already reviewed. A starting process takes a Postgres advisory lock and runs pending migrations, so processes starting together migrate once. A one-process instance stops, migrates and starts. Rolling restarts arrive with shard clusters, together with the migration rule of D9 that makes them safe. Per-server enable and disable take effect at once.
117
+
118
+ ### Tokens and permissions
119
+
120
+ 16. **Every token acts as the member who created it.**
121
+ - A token looks like `teabot_<token id>_<secret>`, after GitHub's token format. The id finds the row directly, and the prefix lets secret scanners spot leaked tokens.
122
+ - The secret is long and random, so it is stored as a SHA-256 hash. Password hashes such as bcrypt are built for low-entropy secrets and are not needed.
123
+ - A member creates tokens with `/tokens create` in a server. A token is shown once, expires, and can be revoked. Rotating is revoking and creating.
124
+ - Each call checks the creator's live permissions (D18), so a token never holds more than its creator, and every audit row names a human.
125
+ - An event backend uses a token made by a staff member and narrowed to the actions it needs (D20). If that member loses their staff role, the token stops working under the same rule as D18, and a new one is issued.
126
+ - Claude Code connects with `--header "Authorization: Bearer teabot_..."`. OAuth 2.1 can come later as a second way to issue the same token row, once claude.ai or the desktop app need access.
127
+ 17. **Tokens carry a maximum risk.** Risks are ordered (`read` below `write` below `destructive`), and each token holds the highest risk it may use. New tokens default to `write`, so `destructive` is always an explicit choice. A monitoring token can be read-only whatever its creator's roles allow, and the check is one comparison.
128
+ 18. **Permission is checked live on every call.** Each call checks the member's current roles in the server, from the bot's member cache with a REST fetch when the member is not cached (D25). Removing a staff role ends access at once, for slash commands and tokens alike.
129
+ 19. **The bot's own check is authoritative.** Each action declares a default requirement in Discord permissions (for example Manage Roles for role actions), and the core checks it against the member's current permissions. Slash commands also set `default_member_permissions`, which only controls who sees them in the Discord UI. Per-server overrides that remap an action to chosen roles come later, likely when event captains need to post in team channels. The call path keeps a place for them.
130
+ 20. **Each token is bound to one server and can be narrowed to chosen actions.**
131
+ - The token decides the server, so no tool or route takes a guild id. An LLM cannot target the wrong server, and text injected from one server cannot steer actions in another.
132
+ - A staff member who works in two servers holds two tokens.
133
+ - A token may carry an allow-list of action names. A Battleships backend's token could allow only `battleships.post_feed`, and that action reads its feed channel from the plugin's config, so the backend posts there and nowhere else.
134
+
135
+ ### Safety
136
+
137
+ 21. **Bulk and destructive actions use plan then apply, and the plan is its audit row.**
138
+ - Called without a `confirm` value, a destructive action returns a summary of the diff (add 12, remove 3, skip 4 above the bot's role, with a page of entries) and a confirm value. Deletes, bans and mass removals always take two steps.
139
+ - The plan's audit row stores the action, the input, a hash of the canonical diff and the confirm value, which is random. Nothing is kept in memory, so any process can apply any plan.
140
+ - To apply, the client calls the same action again with `confirm`. It is an ordinary call, so permission is checked again. The core finds the plan row and refuses unless the actor, guild, action and input match, the plan is younger than 15 minutes, and a freshly computed diff has the stored hash. A changed target refuses the apply, and the response carries the fresh plan. Terraform refuses stale saved plans for the same reason.
141
+ - The apply's audit row points at the plan's row, so the trail links each change to the plan a human saw.
142
+ - An apply runs as a job on the work queue (D31) and reports progress. discord.py already waits out per-route and global rate limits, and the queue runs at most one apply per server at a time.
143
+ - The LLM is responsible for judging whether a plan is correct. The MCP server's instructions and every destructive tool description always tell the LLM to show the plan to its user and get their agreement before applying it.
144
+ 22. **Risk levels map onto MCP tool annotations.** `readOnlyHint` and `destructiveHint` come from each action's risk, so clients can run their own confirmation prompts.
145
+ 23. **Member-written text is marked as untrusted.** Message content, nicknames and other text written by members is wrapped in an explicit untrusted marker in every MCP tool output. Together with plan then apply (D21), maximum risk (D17) and single-server tokens (D20), this limits prompt injection through server content.
146
+ 24. **Every action is logged, and changes are posted.**
147
+ - A `WRITE` or `DESTRUCTIVE` action gets its audit row before it runs, with the outcome `started`, the member who asked, the token if any, the front door, the action and its input. A crash halfway through a side effect therefore leaves a trace. After the action, one transaction records the outcome and queues a `core.post_audit` job, which posts the row to the server's log channel. A Discord outage delays posts and never loses them.
148
+ - A `READ` action has no side effects, so it gets a single row after the call and is never posted. An LLM session's lookups then cost one write each and do not flood the log channel.
149
+ - An action's own database writes happen in its own short transactions (D30), so they are not atomic with the audit row. The `started` row covers that gap.
150
+ - `READ` rows go to `core.audit_reads`, partitioned by day and kept for 30 days by default. Old partitions are dropped whole, so the high-volume table never needs batch deletes. Every other row goes to `core.audit_log`, a plain table kept for a year by default and trimmed by a batched delete, which its low volume keeps cheap.
151
+ 25. **Reads come from the cache, and the cache stays small.**
152
+ - Guilds, channels and roles are always cached. Members are cached when they appear in events or are fetched, and the bot never downloads full member lists at startup. A server with 100,000 members then costs memory only for the members who are active.
153
+ - A permission check for an uncached member fetches it over REST (D18), and then caches it.
154
+ - Member search uses Discord's own search through the gateway, so it works without a full member list.
155
+ - MCP tools return compact summaries with paging instead of dumping members.
156
+ 26. **The bot's role sits below the owner and senior staff roles.** The setup docs say so. Discord's role hierarchy then stops the bot editing those roles, even with Administrator.
157
+
158
+ ### Stack
159
+
160
+ 27. **`discord.py`** is the Discord library, used through `AutoShardedClient` from the start. It runs as one shard below 2,500 servers and adds shards by itself above that. discord.py's own extensions system and command tree are not the plugin protocol. The core handles interactions itself (D32).
161
+ 28. **The official `mcp` Python SDK over streamable HTTP**, through its low-level server, on Starlette and uvicorn in the same asyncio loop as the Discord client.
162
+ - The low-level `on_list_tools` and `on_call_tool` handlers take JSON schemas straight from the Pydantic models, and the tool list is filtered per token. FastMCP has no easy way to filter the list per caller.
163
+ - A Starlette middleware checks the bearer token on `/mcp` and the service API.
164
+ - The SDK's DNS-rebinding protection is configured with the public hostname, because the reverse proxy forwards it.
165
+ 29. **Pydantic models define action inputs, action outputs and config, and the core builds Discord's command payloads from them.**
166
+ - One schema builder turns an input model into both the MCP tool schema and Discord's application command JSON, so the two cannot drift apart. The core registers the JSON with Discord directly. discord.py's `CommandTree` is not used, so no synthetic signatures are needed.
167
+ - Input fields are limited to a fixed set of types: `str`, `LongText`, `int`, `bool`, `Literal` and the `*Ref` types, each optional or not. Raw interaction data carries options as plain values and IDs, which is exactly what the `*Ref` types hold.
168
+ - `LongText` holds multi-line text, which slash options cannot. A slash command with a `LongText` field opens a modal for it, and the action runs once the modal is submitted.
169
+ - Outputs go back as JSON over MCP and the service API. The slash adapter renders them as an embed of the model's fields by default, and an action may supply its own renderer.
170
+ 30. **Postgres through `asyncpg`, with raw SQL and short explicit transactions.**
171
+ - Migrations already define the schema in SQL, so an ORM would describe it a second time.
172
+ - Each process holds two pools: one logged in as `teabot_app` for the core, and one as `teabot_plugins` for plugin code (D9). A plugin connection switches to the plugin's role when it is acquired and runs `RESET ROLE` when it is released, because asyncpg's own reset leaves the role in place. A single read needs no transaction.
173
+ - Writes go through `async with ctx.db.transaction() as tx:` blocks, and everything inside a block goes through `tx`, including `tx.schedule(...)` to queue a job in the same transaction. Pyright checks the handle, and nothing is routed implicitly.
174
+ - Calling `ctx.db` inside an open block raises an error, because it would use another connection and miss the block's own writes. A context variable marks open blocks for this check only.
175
+ - A transaction block never awaits Discord or HTTP, so no action holds a connection and its row locks across a slow call. Dev mode logs any block held longer than 100 ms.
176
+ 31. **One durable work queue in Postgres.** Every job is a handler and a payload with a run time, a state, retries and progress. Plugin timers and scheduled pings use it, and so do the core's own handlers for plan applies (D21), audit posts (D24), command syncs (D32) and server data removal (D49).
177
+ - Workers claim jobs with `SELECT ... FOR UPDATE SKIP LOCKED`, so any number of workers in any number of processes share the queue without double runs.
178
+ - `NOTIFY` wakes workers when a job is due now. Each process listens on its lock connection (D40), so the wake-ups need no connection of their own, and a poll every five seconds catches any notification lost while that connection reconnects.
179
+ - A process that runs only some of the shards claims only jobs whose `(guild_id >> 22) % shard_count` falls on its shards, which keeps a job beside the cache it needs. The shard is computed in the claim query and never stored, so resharding cannot leave stale values. A process that runs every shard skips the filter.
180
+ - Jobs are picked back up after a restart.
181
+ 32. **One interaction router handles every command, button, select menu and modal.**
182
+ - Slash commands are routed by command and subcommand name, and components by custom IDs of the form `teabot:<plugin id>:<handler>:<payload>`. Because no state is held in memory, buttons keep working after a restart and in any process.
183
+ - Buttons and select menus are used in place of reactions for role picks and giveaway entry. They are tied to IDs the bot controls and are easier to audit. Simple polls can use Discord's native polls.
184
+ - A plan's Confirm button is an ordinary component whose payload is the plan's confirm value, so slash commands, MCP and the service API share one confirm path (D21).
185
+ - Each plugin's command group is registered per server, only where the plugin is enabled. The core keeps a hash of each server's command set and syncs only servers whose hash changed, as queue jobs paced within Discord's limits. The hash is saved after a sync succeeds, so a failed sync is retried. A plugin upgrade that touches thousands of servers then spreads over minutes and never blocks startup.
186
+ 33. **Plugins never block the event loop.** One loop serves the Discord gateway and the MCP server, so blocking I/O in a plugin stalls both. The authoring guide requires async I/O, and dev mode turns on asyncio's slow-callback detection so offenders show in the logs.
187
+ 34. **A uv project with one entry point.** `uv run teabot run` starts a bot process. The other subcommands are the operator CLI for work that belongs outside Discord: setting up an instance (D43), listing plugins and their state, purging a plugin and scaffolding a new one (D45). The CLI works on the database alone, and reads the advisory locks in `pg_locks` to see what is loaded (D40). Tests live in `tests/` and run with pytest against a real Postgres. Ruff for linting and formatting, and pyright in strict mode, because types are part of the safety net.
188
+
189
+ ### Hosting and operations
190
+
191
+ 35. **teabot runs as independent instances.** Each instance has its own Discord application, token, database and config, and its own operator. The Tea clan's instance, any self-hoster's instance and the hosted service (D47) are all instances of the same framework. Separate applications keep rate limits, Discord verification and intent reviews of one instance away from every other.
192
+ 36. **HTTPS at the edge and plain HTTP only between the reverse proxy and the bot.** The proxy terminates TLS and forwards `/mcp` and `/api` to a `127.0.0.1` port. Every client uses the public HTTPS URL, so there is one path to secure and log. `/healthz` is not forwarded, because it lists the installed plugins. Deploy scripts call it on localhost.
193
+ 37. **The Tea clan's instance follows the droplet conventions** (`events` skill, `references/hosting.md` in `hballington12/tea-skills`). It is the bare-metal example in the instance template (D43):
194
+ - code in `/opt/teabot`, set up with `uv sync --frozen --no-dev`
195
+ - secrets in `/etc/teabot/teabot.env`
196
+ - Postgres from the distribution packages, listening on a Unix socket only, with memory settings sized for a shared droplet
197
+ - Caddy as the reverse proxy
198
+ - a hardened systemd unit
199
+ - a deploy script that restarts the bot, which runs migrations, and ends with a health check
200
+ - pgBackRest archiving WAL continuously to DigitalOcean Spaces, with weekly full and daily differential backups and its own retention, so a lost droplet costs seconds of data
201
+ 38. **Development uses a separate dev bot application and a test server**, so experiments never touch a live server. The whole stack runs locally against Postgres from the template's Compose file, and Claude Code connects to the local MCP URL with a dev server token.
202
+ 39. **Plan for verification early.** Turn on the Server Members intent, which joins, leaves and member updates need. Turn on Message Content only if a plugin needs message text. A bot must be verified by Discord before it can join more than 100 servers, and verified bots need approval for privileged intents. Large self-hosted instances meet this too, and the setup docs say so.
203
+
204
+ ### Scale
205
+
206
+ 40. **Process memory holds only caches and work in flight. Everything that must be right across processes lives in Postgres.**
207
+ - Plans, confirm buttons, jobs, plugin config, tokens and command-set hashes are all rows. No correctness depends on which process handles a request.
208
+ - Ownership is held as Postgres session advisory locks on one dedicated connection per process. A process takes an exclusive lock for each shard it runs and a shared lock for each plugin it loads.
209
+ - A second process on the same bot token fails to take its shard locks and refuses to start. Two processes on one token would otherwise both handle every event. Purge takes a plugin's lock exclusively, which fails while any process has the plugin loaded (D7).
210
+ - A crash or a dropped connection releases the locks at once, so no staleness rule is needed. A process whose lock connection drops exits, and its supervisor restarts it.
211
+ - Each instance has its own database (D35), so the locks only see processes of the same instance. With separate databases nothing can detect two processes on one token, so D38's separate dev application is the guard there, and the setup docs state it.
212
+ - Each process also writes an informational row with its host, shards and version, for `teabot plugins list` and later for routing between clusters. Nothing correct depends on it.
213
+ - The first deployment is one process with all shards. When one process is no longer enough, shards split into clusters of several processes, each owning the servers that Discord's formula `(guild_id >> 22) % shard_count` gives it. Plugins need no changes for that step. Its shape is under deferred work.
214
+
215
+ ### Open source and distribution
216
+
217
+ 41. **teabot uses the MIT licence.** The core, the SDK, the first-party plugins and the instance template all share it. It is short and permissive, which suits the goal of free self-hosting, and it was settled on 2026-11-14, before any outside contribution, because changing it later needs every contributor's agreement. Third-party plugins choose their own licence.
218
+ 42. **The framework and the instances are separate projects.**
219
+ - This repository is the framework. It holds the core, the SDK, the first-party plugins, the instance template and the docs, and it publishes `teabot` and each `teabot-<plugin>` to PyPI.
220
+ - An instance is a small project made from the template: a `pyproject.toml` that lists the plugins it runs, a `uv.lock`, an env file and deploy files. The Tea clan's instance is one such repo, and the hosted service is another.
221
+ - Choosing plugins, pinning them and reviewing their updates happen in the instance, which is where D4 belongs.
222
+ 43. **The instance template supports both a quick and a careful setup.** It ships a Docker Compose file with the bot and Postgres, for anyone who wants a working bot in minutes, and the Tea droplet's bare-metal files as a second example (D37). `teabot init` creates the database roles, generates the secrets in the env file and prints the Discord application settings to use. The README walks through creating the Discord application, the intents to turn on, and the rule of one teabot per server.
223
+ 44. **Anyone can write plugins for their own instance without approval from the teabot maintainers.** A plugin is a package with a `teabot.plugins` entry point, kept inside the instance's repo, in its own repo or on PyPI. The operator who installs it is the one who reviews it. The maintainers' review applies only to the hosted service's catalogue (D47) and to listings in a future plugin hub.
224
+ 45. **Plugins build against `teabot.sdk`, which follows semantic versioning.**
225
+ - `teabot.sdk` holds everything a plugin uses: the protocol types, `ActionContext`, the `*Ref` and `LongText` types and the registrar. Everything else in `teabot` is internal and may change in any release.
226
+ - `teabot.testing` provides guild and member fakes and a Postgres fixture that runs a plugin's migrations, so plugin authors test the way the core does.
227
+ - `teabot new-plugin <id>` scaffolds a package with its entry point, manifest, first migration, README with an "External connections" section, and tests.
228
+ - A breaking change to `teabot.sdk` needs a major version, and the old form keeps working with a deprecation warning for at least one minor release first.
229
+ 46. **Plugin ids are unique within an instance.**
230
+ - An id matches `[a-z][a-z0-9_]{1,31}`, which fits Discord's command names and the Postgres schema and role names of D9.
231
+ - The ids `core`, `admin`, `teabot` and `plugin` are reserved, as are `plugins`, `audit` and `tokens`, the admin plugin's command groups.
232
+ - When two installed packages declare the same id, the loader loads neither and names both packages in the error. A local `giveaways` plugin and a later first-party one therefore never share a schema or a command group by accident.
233
+ - A future plugin hub reserves ids across its listings.
234
+ 47. **The hosted service is an instance run by the teabot maintainers.** Server admins sign up by adding its bot, and they enable plugins from a catalogue of plugins the maintainers have reviewed. Custom plugins need a self-hosted instance, because plugins are not sandboxed (D5). Before it opens, the hosted service needs the deferred work for strangers sharing one instance: per-server quotas and fair queue claims, OAuth (D16), member data deletion (D11), egress consent and enforcement (D12), and a privacy policy and terms of service for Discord verification.
235
+ 48. **The repository carries the files an open-source project needs.** `LICENSE`, `CONTRIBUTING.md`, `SECURITY.md` with a private way to report a vulnerability, through GitHub's private reporting, a code of conduct, a changelog kept per release, and release notes that call out any manual step for operators.
236
+ 49. **When the bot leaves a server, that server's data is deleted after a grace period.**
237
+ - On the guild-remove event, the core records the time and queues a `purge_guild` job 30 days later. If the bot is added back before then, the job is cancelled and everything is as it was.
238
+ - The job deletes the server's rows from every core table and from every plugin table, using the `guild_id` column that D9 requires. No plugin code is needed.
239
+ - The grace period is a setting, so an operator can shorten it. Discord's developer policy expects bots to delete data they no longer need.
240
+
241
+ ### First milestone
242
+
243
+ 50. **The first plugin is an announcements proof of concept.** It posts an embed to a channel, and it exercises a slash command with a `LongText` body, an MCP tool, a service-API call, the permission check and the audit log.