agentirc-cli 9.4.1__tar.gz → 9.5.0a1__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 (112) hide show
  1. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/CHANGELOG.md +17 -0
  2. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/PKG-INFO +1 -1
  3. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/cli.py +4 -0
  4. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/config.py +10 -2
  5. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/protocol.py +130 -2
  6. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skill.py +10 -37
  7. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/api-stability.md +56 -0
  8. agentirc_cli-9.5.0a1/docs/extension-api.md +243 -0
  9. agentirc_cli-9.5.0a1/docs/superpowers/specs/2026-05-01-bot-extension-api-design.md +458 -0
  10. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/pyproject.toml +1 -1
  11. agentirc_cli-9.5.0a1/tests/test_protocol_bot_exports.py +214 -0
  12. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/uv.lock +1 -1
  13. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/SKILL.md +0 -0
  14. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
  15. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
  16. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
  17. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
  18. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/pr-sonar.sh +0 -0
  19. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
  20. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
  21. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.claude/skills.local.yaml.example +0 -0
  22. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.github/workflows/publish.yml +0 -0
  23. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.github/workflows/tests.yml +0 -0
  24. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/.gitignore +0 -0
  25. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/CLAUDE.md +0 -0
  26. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/LICENSE +0 -0
  27. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/README.md +0 -0
  28. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/__init__.py +0 -0
  29. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/__main__.py +0 -0
  30. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/__init__.py +0 -0
  31. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/aio.py +0 -0
  32. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/bots/__init__.py +0 -0
  33. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/bots/bot_manager.py +0 -0
  34. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/bots/http_listener.py +0 -0
  35. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/cli_shared/__init__.py +0 -0
  36. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/cli_shared/constants.py +0 -0
  37. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/cli_shared/mesh.py +0 -0
  38. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/constants.py +0 -0
  39. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/pidfile.py +0 -0
  40. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/protocol/__init__.py +0 -0
  41. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/protocol/message.py +0 -0
  42. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/protocol/replies.py +0 -0
  43. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/telemetry/__init__.py +0 -0
  44. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/telemetry/audit.py +0 -0
  45. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/telemetry/context.py +0 -0
  46. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/telemetry/metrics.py +0 -0
  47. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/telemetry/tracing.py +0 -0
  48. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/_internal/virtual_client.py +0 -0
  49. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/channel.py +0 -0
  50. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/client.py +0 -0
  51. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/events.py +0 -0
  52. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/history_store.py +0 -0
  53. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/ircd.py +0 -0
  54. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/remote_client.py +0 -0
  55. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/room_store.py +0 -0
  56. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/rooms_util.py +0 -0
  57. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/server_link.py +0 -0
  58. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skills/__init__.py +0 -0
  59. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skills/history.py +0 -0
  60. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skills/icon.py +0 -0
  61. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skills/rooms.py +0 -0
  62. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/skills/threads.py +0 -0
  63. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/agentirc/thread_store.py +0 -0
  64. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/cli.md +0 -0
  65. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/deployment.md +0 -0
  66. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/steward/onboarding.md +0 -0
  67. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +0 -0
  68. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/docs/superpowers/specs/2026-05-01-task14-audit.md +0 -0
  69. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/__init__.py +0 -0
  70. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/_helpers.py +0 -0
  71. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/conftest.py +0 -0
  72. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/__init__.py +0 -0
  73. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/_fakes.py +0 -0
  74. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/_metrics_helpers.py +0 -0
  75. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_audit_emit.py +0 -0
  76. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_audit_lifecycle.py +0 -0
  77. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_audit_module.py +0 -0
  78. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_audit_parse_error.py +0 -0
  79. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_config.py +0 -0
  80. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_dispatch_span.py +0 -0
  81. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_emit_event_span.py +0 -0
  82. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_metrics_init.py +0 -0
  83. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_metrics_s2s.py +0 -0
  84. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_outbound_inject.py +0 -0
  85. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_parse_error.py +0 -0
  86. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_s2s_relay_span.py +0 -0
  87. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_server_init.py +0 -0
  88. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_server_link_inject.py +0 -0
  89. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/telemetry/test_tracing.py +0 -0
  90. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_channel.py +0 -0
  91. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_cli.py +0 -0
  92. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_config_loader.py +0 -0
  93. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_connection.py +0 -0
  94. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_discovery.py +0 -0
  95. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_basic.py +0 -0
  96. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_catalog.py +0 -0
  97. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_federation.py +0 -0
  98. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_history.py +0 -0
  99. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_lifecycle.py +0 -0
  100. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_events_reserved_nick.py +0 -0
  101. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_federation.py +0 -0
  102. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_history.py +0 -0
  103. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_link_reconnect.py +0 -0
  104. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_mentions.py +0 -0
  105. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_messaging.py +0 -0
  106. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_modes.py +0 -0
  107. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_room_persistence.py +0 -0
  108. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_rooms_federation.py +0 -0
  109. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_rooms_integration.py +0 -0
  110. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_server_icon_skill.py +0 -0
  111. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_skills.py +0 -0
  112. {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a1}/tests/test_threads.py +0 -0
@@ -4,6 +4,23 @@ All notable changes to this project will be documented in this file.
4
4
 
5
5
  Format follows [Keep a Changelog](https://keepachangelog.com/).
6
6
 
7
+ ## [9.5.0a1] - 2026-05-02
8
+
9
+ ### Added
10
+
11
+ - Public `agentirc.protocol` exports for the bot extension API: the `Event` dataclass, the `EventType` enum, 20 per-type `EVENT_TYPE_*` string constants, the `EVENTSUB` / `EVENTUNSUB` / `EVENT` / `EVENTERR` / `EVENTPUB` verb constants, and the `BOT_CAP = "agentirc.io/bot"` capability identifier. `Event.type` is widened to `EventType | str` so federation peers can deliver event types this version doesn't recognise. See `docs/superpowers/specs/2026-05-01-bot-extension-api-design.md` for the full design and `docs/extension-api.md` for the bot-author quick reference.
12
+ - `ServerConfig.event_subscription_queue_max: int = 1024` — per-subscription queue bound. Recognised by `ServerConfig.from_yaml` and `cli._resolve_config()` as a top-level YAML key. Consumed by the subscription registry that lands in 9.5.0a3.
13
+ - `agentirc.skill` keeps re-exporting `Event` and `EventType` for backward compat; the re-export shim is removed in 9.6.0 once Phase A2 confirms no consumer relies on the path.
14
+
15
+ ### Changed
16
+
17
+ - `EventType` upgraded from `enum.Enum` to `enum.StrEnum`. Members keep their `.value` strings unchanged, but Python consumers now observe new identity semantics: `isinstance(EventType.JOIN, str)` is `True`, `EventType.JOIN == "user.join"` is `True`, JSON serialization emits the bare string. Internal call sites verified — none compare `EventType.X` against a bare string today, so the truthier equality is pure improvement; downstream consumers that did string-equality round-trips against `EventType` see strictly more matches, never fewer.
18
+
19
+ ### Notes
20
+
21
+ - This is the **declarations slice** of the bot extension API. No daemon-level behavior changes: `EVENTSUB` etc. are reserved verb constants but the daemon does not yet handle them; `BOT_CAP` is exported but not yet advertised in `CAP LS` output. The wire-format envelope refactor lands in 9.5.0a2; the bot CAP behavior, subscription verbs, `EVENTPUB`, and `webhook_port` unbinding land in 9.5.0a3 / 9.5.0 final.
22
+ - Tracks [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
23
+
7
24
  ## [9.4.1] - 2026-05-01
8
25
 
9
26
  ### Documentation
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentirc-cli
3
- Version: 9.4.1
3
+ Version: 9.5.0a1
4
4
  Summary: Agent-friendly IRCd: server core for AI agent meshes
5
5
  Project-URL: Homepage, https://github.com/agentculture/agentirc
6
6
  Project-URL: Issues, https://github.com/agentculture/agentirc/issues
@@ -185,6 +185,9 @@ def _resolve_config(args: argparse.Namespace) -> "ServerConfig": # noqa: F821 (
185
185
  args.data_dir, raw.get("data_dir"), os.path.expanduser("~/.culture/data")
186
186
  )
187
187
  )
188
+ event_subscription_queue_max = _pick(
189
+ None, raw.get("event_subscription_queue_max"), 1024
190
+ )
188
191
 
189
192
  cfg = ServerConfig(
190
193
  name=name or "agentirc",
@@ -195,6 +198,7 @@ def _resolve_config(args: argparse.Namespace) -> "ServerConfig": # noqa: F821 (
195
198
  links=_resolve_links(getattr(args, "link", None), raw.get("links") or []),
196
199
  system_bots=raw.get("system_bots") or {},
197
200
  telemetry=_build_telemetry(raw.get("telemetry") or {}),
201
+ event_subscription_queue_max=event_subscription_queue_max,
198
202
  )
199
203
 
200
204
  args.name = name # may be None — handler resolves via default-server file
@@ -53,13 +53,21 @@ class ServerConfig:
53
53
  links: list[LinkConfig] = field(default_factory=list)
54
54
  system_bots: dict = field(default_factory=dict)
55
55
  telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
56
+ # Bot extension API (9.5.0): per-subscription event-queue bound. When
57
+ # exceeded, the subscription is dropped with EVENTERR :backpressure-overflow
58
+ # and the bot reconciles via re-subscribe + BACKFILL. Behavior wires up in
59
+ # 9.5.0a3; the field is exposed in 9.5.0a1 so consumers can pin against the
60
+ # public surface.
61
+ event_subscription_queue_max: int = 1024
56
62
 
57
63
  @classmethod
58
64
  def from_yaml(cls, path: str | Path) -> "ServerConfig":
59
65
  """Load a ServerConfig from a YAML file.
60
66
 
61
67
  Recognises top-level ``server`` (host/port/name), ``telemetry``,
62
- ``links``, ``webhook_port``, ``data_dir``, and ``system_bots``.
68
+ ``links``, ``webhook_port``, ``data_dir``, ``system_bots``, and
69
+ ``event_subscription_queue_max`` (added in 9.5.0a1; consumed by
70
+ the subscription registry that lands in 9.5.0a3).
63
71
  Unknown top-level keys (``supervisor``, ``agents``, ``buffer_size``,
64
72
  ``poll_interval``, ``sleep_start``, ``sleep_end``) are silently
65
73
  ignored — those belong to culture's broader process supervisor,
@@ -104,7 +112,7 @@ def _yaml_kwargs(raw: dict[str, Any]) -> dict[str, Any]:
104
112
  for key in ("name", "host", "port"):
105
113
  if key in server_section:
106
114
  kwargs[key] = server_section[key]
107
- for key in ("webhook_port", "data_dir"):
115
+ for key in ("webhook_port", "data_dir", "event_subscription_queue_max"):
108
116
  if key in raw:
109
117
  kwargs[key] = raw[key]
110
118
  links_section = raw.get("links") or []
@@ -1,6 +1,6 @@
1
- """Public protocol surface for agentirc — verbs, numerics, and tags.
1
+ """Public protocol surface for agentirc — verbs, numerics, tags, and the bot extension API.
2
2
 
3
- Semver-tracked module. Three categories of constants live here:
3
+ Semver-tracked module. Five categories of public symbols live here:
4
4
 
5
5
  1. **Verb names** — IRC command verbs as bare uppercase tokens. Mostly
6
6
  RFC 2812 (PRIVMSG, JOIN, QUIT, ...), plus agentirc skill verbs
@@ -14,6 +14,16 @@ Semver-tracked module. Three categories of constants live here:
14
14
  consumers don't reach into the underscore namespace.
15
15
  3. **Message tag names** — IRCv3 tag keys for traceparent/tracestate
16
16
  and agentirc-specific event tags.
17
+ 4. **Event types and the Event dataclass** — :class:`EventType`
18
+ (a :class:`enum.StrEnum` of 20 dotted-lowercase wire strings) and
19
+ :class:`Event` (a frozen-shape dataclass). Plus 20 ``EVENT_TYPE_*``
20
+ per-type string constants for callers that prefer bare strings over
21
+ enum-coercion at JSON boundaries. Added in 9.5.0a1 as part of the
22
+ bot extension API.
23
+ 5. **Bot extension verbs and capability** — ``EVENTSUB``, ``EVENTUNSUB``,
24
+ ``EVENT``, ``EVENTERR``, ``EVENTPUB`` verb constants and
25
+ ``BOT_CAP = "agentirc.io/bot"``. Reserved in 9.5.0a1; daemon
26
+ behavior wires up in 9.5.0a3 / 9.5.0 final.
17
27
 
18
28
  Existing call sites under ``agentirc.ircd``, ``agentirc.server_link``
19
29
  and the skills modules still use inline string literals. Migrating them
@@ -31,6 +41,11 @@ clients and federation. They need a coordinated cross-repo bump.
31
41
 
32
42
  from __future__ import annotations
33
43
 
44
+ import time
45
+ from dataclasses import dataclass, field
46
+ from enum import StrEnum
47
+ from typing import Any
48
+
34
49
  # ---------------------------------------------------------------------------
35
50
  # Numeric reply codes (re-exported from the internal module)
36
51
  # ---------------------------------------------------------------------------
@@ -164,6 +179,90 @@ ROOMETAEND = "ROOMETAEND" # SIC: typo preserved for wire compat (ROOMMETAEND ta
164
179
  ROOMETASET = "ROOMETASET" # SIC: typo preserved for wire compat (ROOMMETASET target)
165
180
 
166
181
 
182
+ # ---------------------------------------------------------------------------
183
+ # Bot extension API (9.5.0)
184
+ # ---------------------------------------------------------------------------
185
+ # Public Event dataclass + EventType enum, per-type string constants, the
186
+ # EVENTSUB / EVENTUNSUB / EVENT / EVENTERR / EVENTPUB verb names, and the
187
+ # bot-CAP token. See docs/superpowers/specs/2026-05-01-bot-extension-api-design.md
188
+ # for the wire format and verb syntax. Behavior wiring lands in 9.5.0a2/a3;
189
+ # 9.5.0a1 ships these symbols only.
190
+
191
+ # `EventType` is `StrEnum` so `EventType.JOIN == "user.join"` is True at JSON
192
+ # boundaries. Adding a new member is a minor bump; renaming or removing one
193
+ # is a major bump.
194
+
195
+
196
+ class EventType(StrEnum):
197
+ MESSAGE = "message"
198
+ JOIN = "user.join"
199
+ PART = "user.part"
200
+ QUIT = "user.quit"
201
+ TOPIC = "topic"
202
+ ROOMMETA = "room.meta"
203
+ TAGS = "tags.update"
204
+ ROOMARCHIVE = "room.archive"
205
+ THREAD_CREATE = "thread.create"
206
+ THREAD_MESSAGE = "thread.message"
207
+ THREAD_CLOSE = "thread.close"
208
+ AGENT_CONNECT = "agent.connect"
209
+ AGENT_DISCONNECT = "agent.disconnect"
210
+ CONSOLE_OPEN = "console.open"
211
+ CONSOLE_CLOSE = "console.close"
212
+ SERVER_WAKE = "server.wake"
213
+ SERVER_SLEEP = "server.sleep"
214
+ SERVER_LINK = "server.link"
215
+ SERVER_UNLINK = "server.unlink"
216
+ ROOM_CREATE = "room.create"
217
+
218
+
219
+ @dataclass
220
+ class Event:
221
+ # `type` is widened to `EventType | str` so federation peers can deliver
222
+ # event types this version doesn't recognise without raising. Subscribers
223
+ # must tolerate unknown types (forward-compat).
224
+ type: EventType | str
225
+ channel: str | None
226
+ nick: str
227
+ data: dict[str, Any] = field(default_factory=dict)
228
+ timestamp: float = field(default_factory=time.time)
229
+
230
+
231
+ # Per-type string constants — parallel to `EventType` for callers that prefer
232
+ # bare strings (e.g. comparing JSON-decoded `type` field without enum-coercing).
233
+ EVENT_TYPE_MESSAGE = "message"
234
+ EVENT_TYPE_USER_JOIN = "user.join"
235
+ EVENT_TYPE_USER_PART = "user.part"
236
+ EVENT_TYPE_USER_QUIT = "user.quit"
237
+ EVENT_TYPE_TOPIC = "topic"
238
+ EVENT_TYPE_ROOM_META = "room.meta"
239
+ EVENT_TYPE_TAGS_UPDATE = "tags.update"
240
+ EVENT_TYPE_ROOM_ARCHIVE = "room.archive"
241
+ EVENT_TYPE_THREAD_CREATE = "thread.create"
242
+ EVENT_TYPE_THREAD_MESSAGE = "thread.message"
243
+ EVENT_TYPE_THREAD_CLOSE = "thread.close"
244
+ EVENT_TYPE_AGENT_CONNECT = "agent.connect"
245
+ EVENT_TYPE_AGENT_DISCONNECT = "agent.disconnect"
246
+ EVENT_TYPE_CONSOLE_OPEN = "console.open"
247
+ EVENT_TYPE_CONSOLE_CLOSE = "console.close"
248
+ EVENT_TYPE_SERVER_WAKE = "server.wake"
249
+ EVENT_TYPE_SERVER_SLEEP = "server.sleep"
250
+ EVENT_TYPE_SERVER_LINK = "server.link"
251
+ EVENT_TYPE_SERVER_UNLINK = "server.unlink"
252
+ EVENT_TYPE_ROOM_CREATE = "room.create"
253
+
254
+ # Bot extension verbs.
255
+ EVENTSUB = "EVENTSUB"
256
+ EVENTUNSUB = "EVENTUNSUB"
257
+ EVENT = "EVENT"
258
+ EVENTERR = "EVENTERR"
259
+ EVENTPUB = "EVENTPUB"
260
+
261
+ # Bot-CAP token. Vendored namespace per IRCv3 conventions, prevents collision
262
+ # with hypothetical bare-`bot` caps from non-agentirc IRC servers.
263
+ BOT_CAP = "agentirc.io/bot"
264
+
265
+
167
266
  __all__ = [
168
267
  # Numerics
169
268
  "ERR_ALREADYREGISTRED",
@@ -256,4 +355,33 @@ __all__ = [
256
355
  "STAGS",
257
356
  "STHREAD",
258
357
  "STOPIC",
358
+ # Bot extension API (9.5.0)
359
+ "BOT_CAP",
360
+ "EVENT",
361
+ "EVENTERR",
362
+ "EVENTPUB",
363
+ "EVENTSUB",
364
+ "EVENTUNSUB",
365
+ "Event",
366
+ "EventType",
367
+ "EVENT_TYPE_AGENT_CONNECT",
368
+ "EVENT_TYPE_AGENT_DISCONNECT",
369
+ "EVENT_TYPE_CONSOLE_CLOSE",
370
+ "EVENT_TYPE_CONSOLE_OPEN",
371
+ "EVENT_TYPE_MESSAGE",
372
+ "EVENT_TYPE_ROOM_ARCHIVE",
373
+ "EVENT_TYPE_ROOM_CREATE",
374
+ "EVENT_TYPE_ROOM_META",
375
+ "EVENT_TYPE_SERVER_LINK",
376
+ "EVENT_TYPE_SERVER_SLEEP",
377
+ "EVENT_TYPE_SERVER_UNLINK",
378
+ "EVENT_TYPE_SERVER_WAKE",
379
+ "EVENT_TYPE_TAGS_UPDATE",
380
+ "EVENT_TYPE_THREAD_CLOSE",
381
+ "EVENT_TYPE_THREAD_CREATE",
382
+ "EVENT_TYPE_THREAD_MESSAGE",
383
+ "EVENT_TYPE_TOPIC",
384
+ "EVENT_TYPE_USER_JOIN",
385
+ "EVENT_TYPE_USER_PART",
386
+ "EVENT_TYPE_USER_QUIT",
259
387
  ]
@@ -1,9 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
- import time
4
- from dataclasses import dataclass, field
5
- from enum import Enum
6
- from typing import TYPE_CHECKING, Any
3
+ from typing import TYPE_CHECKING
4
+
5
+ # Event and EventType moved to agentirc.protocol in 9.5.0a1 as part of the
6
+ # bot extension API public surface. This module keeps re-exporting them so
7
+ # internal call sites and any pre-9.5 vendored consumers keep working; the
8
+ # re-export is removed in 9.6.0 once Phase A2 confirms no consumer relies on
9
+ # this path.
10
+ from agentirc.protocol import Event, EventType
11
+
12
+ __all__ = ["Event", "EventType", "Skill"]
7
13
 
8
14
  if TYPE_CHECKING:
9
15
  from agentirc.client import Client
@@ -11,39 +17,6 @@ if TYPE_CHECKING:
11
17
  from agentirc._internal.protocol.message import Message
12
18
 
13
19
 
14
- class EventType(Enum):
15
- MESSAGE = "message"
16
- JOIN = "user.join"
17
- PART = "user.part"
18
- QUIT = "user.quit"
19
- TOPIC = "topic"
20
- ROOMMETA = "room.meta"
21
- TAGS = "tags.update"
22
- ROOMARCHIVE = "room.archive"
23
- THREAD_CREATE = "thread.create"
24
- THREAD_MESSAGE = "thread.message"
25
- THREAD_CLOSE = "thread.close"
26
- # Lifecycle + link events introduced by mesh-events feature.
27
- AGENT_CONNECT = "agent.connect"
28
- AGENT_DISCONNECT = "agent.disconnect"
29
- CONSOLE_OPEN = "console.open"
30
- CONSOLE_CLOSE = "console.close"
31
- SERVER_WAKE = "server.wake"
32
- SERVER_SLEEP = "server.sleep"
33
- SERVER_LINK = "server.link"
34
- SERVER_UNLINK = "server.unlink"
35
- ROOM_CREATE = "room.create"
36
-
37
-
38
- @dataclass
39
- class Event:
40
- type: EventType
41
- channel: str | None
42
- nick: str
43
- data: dict[str, Any] = field(default_factory=dict)
44
- timestamp: float = field(default_factory=time.time)
45
-
46
-
47
20
  class Skill:
48
21
  name: str = ""
49
22
  commands: set[str] = set()
@@ -12,6 +12,33 @@ import only from these three modules.
12
12
  | [`agentirc.cli`](#agentirccli) | `main()`, `dispatch(argv) -> int` | Public, semver-tracked |
13
13
  | [`agentirc.protocol`](#agentircprotocol) | Verb constants, numeric reply codes, IRCv3/extension tag names | Public, semver-tracked |
14
14
 
15
+ > **Bot extension API — phased rollout:**
16
+ >
17
+ > - **9.5.0a1 (current alpha — declarations slice):** `agentirc.protocol`
18
+ > exports the `Event` dataclass, the `EventType` enum (now `StrEnum`),
19
+ > 20 per-type `EVENT_TYPE_*` string constants, the
20
+ > `EVENTSUB`/`EVENTUNSUB`/`EVENT`/`EVENTERR`/`EVENTPUB` verb constants,
21
+ > and the `BOT_CAP = "agentirc.io/bot"` capability identifier.
22
+ > `ServerConfig` gains the `event_subscription_queue_max: int = 1024`
23
+ > field. **The symbols are importable; the daemon does not yet handle
24
+ > the verbs and does not advertise `BOT_CAP`** — calling them is a
25
+ > no-op until the behavior slices land.
26
+ > - **9.5.0a2 (planned):** wire-format envelope refactor —
27
+ > `_build_event_payload`/`_encode_event_data` emit the 5-field envelope
28
+ > `{type, channel, nick, data, timestamp}`; federated `SEVENT` shifts
29
+ > to the new shape. Internal change; no new public symbols.
30
+ > - **9.5.0a3 (planned):** bot-CAP behavior, `EVENTSUB`/`EVENTUNSUB`
31
+ > handlers, the in-memory `SubscriptionRegistry`, and `EVENTPUB`
32
+ > handler. Daemon starts advertising `BOT_CAP` in `CAP LS` output.
33
+ > - **9.5.0 (final):** `webhook_port` no longer bound; `cli.md` /
34
+ > `deployment.md` updated; this block flips from "phased rollout" to
35
+ > "current," and the version-history table picks up a 9.5.0 row.
36
+ >
37
+ > Wire format and verb syntax are specified in
38
+ > [`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md);
39
+ > a quick reference for bot authors is at [`docs/extension-api.md`](extension-api.md).
40
+ > Tracking issue: [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
41
+
15
42
  ## Semver contract
16
43
 
17
44
  Following [SemVer 2.0](https://semver.org/):
@@ -179,6 +206,35 @@ Re-exported from `agentirc._internal.protocol.replies`. About 33 names:
179
206
  Re-exported from `agentirc._internal.telemetry.context`:
180
207
  `TRACEPARENT_TAG`, `TRACESTATE_TAG`, `EVENT_TAG_TYPE`, `EVENT_TAG_DATA`.
181
208
 
209
+ ### Reserved for 9.5.0: bot extension surface
210
+
211
+ These additions are **specified but not yet implemented**. They will land
212
+ together as a single minor bump in 9.5.0 — see the design spec at
213
+ [`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md)
214
+ for rationale, federation behavior, and acceptance criteria, and
215
+ [`docs/extension-api.md`](extension-api.md) for the bot-author quick
216
+ reference.
217
+
218
+ - **Event verbs:** `EVENTSUB`, `EVENTUNSUB`, `EVENT`, `EVENTERR`, `EVENTPUB`. Subscribers stream events with filter syntax (`type=`/`channel=`/`nick=` AND-ed globs); `EVENTPUB` lets a bot emit its own typed events back into the stream (server-side validation of `type` against `EVENT_TYPE_RE`; `nick` and `timestamp` derived server-side, not trusted from the client).
219
+ - **Bot capability:** `BOT_CAP = "agentirc.io/bot"`. When negotiated via
220
+ the existing CAP REQ/ACK flow, the connection is treated as a bot:
221
+ silent JOIN/PART/QUIT broadcasts, no auto-op on channel creation,
222
+ `+` prefix in NAMES output, `B` flag in WHO output, authorized to
223
+ issue `EVENTSUB`.
224
+ - **Event dataclass and enum:** `Event` and `EventType` (currently
225
+ internal in `agentirc.skill`). Promoted to public for Python consumers.
226
+ Wire format — not the Python class names — is the contract; non-Python
227
+ bots pin against the JSON shape documented in `extension-api.md`.
228
+ - **Per-type string constants:** `EVENT_TYPE_MESSAGE`,
229
+ `EVENT_TYPE_USER_JOIN`, …, one per type-string in the canonical
230
+ vocabulary. Convenience for callers that prefer non-enum-aware
231
+ constants.
232
+
233
+ The `ServerConfig` additions (one new field
234
+ `event_subscription_queue_max: int = 1024`) and the `webhook_port`
235
+ binding-removal are described under
236
+ [`agentirc.config`](#agentircconfig) once 9.5.0 lands.
237
+
182
238
  ### Wire-format quirks (preserved verbatim)
183
239
 
184
240
  Four known wire-format issues are **preserved** rather than fixed,
@@ -0,0 +1,243 @@
1
+ # Extension API for out-of-process bots
2
+
3
+ **Status:** Proposed for 9.5.0. The wire format and verbs described here are
4
+ **reserved** in `docs/api-stability.md` but **not yet implemented**. Track the
5
+ landing PR via [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
6
+ The full design lives at
7
+ [`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md).
8
+
9
+ This page is a quick reference for bot authors. For rationale, semver
10
+ implications, and federation behavior, read the design spec.
11
+
12
+ ## Overview
13
+
14
+ A "bot" is any TCP client that negotiates the `agentirc.io/bot` capability.
15
+ Once negotiated, the client:
16
+
17
+ - Joins channels silently (no JOIN broadcast to other channel members).
18
+ - Never gets auto-op on a newly created channel.
19
+ - Appears in `NAMES` prefixed with `+` and in `WHO` with a `B` flag.
20
+ - May issue `EVENTSUB` to stream events and `EVENTPUB` to emit custom events.
21
+
22
+ Everything else (`PRIVMSG`, `NOTICE`, mention notifications, channel ops,
23
+ threads, rooms) works exactly the same as for a human client.
24
+
25
+ ### Porting note: JOIN before PRIVMSG
26
+
27
+ `PRIVMSG <#channel>` requires channel membership. Culture's in-process
28
+ `VirtualClient.broadcast_to_channel` lets a bot post to a channel it
29
+ hasn't joined; TCP-connected bots get no such shortcut. The canonical
30
+ pattern under bot CAP is **JOIN, PRIVMSG, then optionally PART** — all
31
+ silent (no broadcasts to other members). For event-triggered bots that
32
+ post into channels they discover at runtime (e.g. a welcome bot reacting
33
+ to `user.join`), the JOIN-broadcast-PART sequence is cheap because each
34
+ step is silent. Decide per bot whether to stay joined for low-latency
35
+ posting or PART after each emission to avoid occupying a member slot.
36
+
37
+ ## Connecting
38
+
39
+ Standard IRCv3 capability handshake:
40
+
41
+ ```text
42
+ C: CAP LS
43
+ S: :server CAP * LS :message-tags agentirc.io/bot
44
+ C: CAP REQ :agentirc.io/bot message-tags
45
+ S: :server CAP * ACK :agentirc.io/bot message-tags
46
+ C: CAP END
47
+ C: NICK mybot
48
+ C: USER mybot 0 * :My Bot
49
+ S: :server 001 mybot :Welcome to agentirc, mybot
50
+ ```
51
+
52
+ Most bots will request both `agentirc.io/bot` (silent presence + EVENTSUB
53
+ authorization) and `message-tags` (read IRCv3 tags on PRIVMSGs, including the
54
+ `event-data` tag on `#system` PRIVMSGs).
55
+
56
+ ## Subscribing to events
57
+
58
+ ```text
59
+ EVENTSUB <sub-id> [type=<glob>] [channel=<name>] [nick=<glob>]
60
+ EVENTUNSUB <sub-id>
61
+ EVENT <sub-id> <type> <channel-or-*> <nick> :<base64-json-payload>
62
+ EVENTERR <sub-id> :<reason>
63
+ ```
64
+
65
+ - `<sub-id>` is a client-chosen ASCII token, 1–32 chars from
66
+ `[A-Za-z0-9._:-]`. Pick something memorable for debugging.
67
+ - All three filters are optional. Multiple filters are AND-ed. Missing filter
68
+ means match-all.
69
+ - `type=` and `nick=` accept `*` glob wildcards (e.g. `type=user.*`).
70
+ - `channel=` accepts an exact channel name or `*`. Empty value matches only
71
+ events with `channel: null`.
72
+ - Multiple concurrent subscriptions per client are allowed; each gets a
73
+ distinct `sub-id`.
74
+ - `EVENTSUB` requires the `agentirc.io/bot` capability. Without it,
75
+ the server replies `EVENTERR <sub-id> :bot-capability-required`.
76
+ - Subscriptions die when the client disconnects.
77
+
78
+ ### Example
79
+
80
+ ```text
81
+ C: EVENTSUB joins type=user.join channel=#room
82
+ S: :server EVENT joins user.join #room alice :eyJ0eXBlIjogInVzZXIuam9pbiIsIC4uLn0=
83
+ S: :server EVENT joins user.join #room bob :eyJ0eXBlIjogInVzZXIuam9pbiIsIC4uLn0=
84
+ C: EVENTUNSUB joins
85
+ ```
86
+
87
+ The `EVENT` line carries the canonical event payload, base64-JSON-encoded,
88
+ in the trailing parameter. Decode with any JSON parser.
89
+
90
+ ## Event JSON shape
91
+
92
+ ```json
93
+ {
94
+ "type": "user.join",
95
+ "channel": "#room",
96
+ "nick": "alice",
97
+ "data": {"text": "hi"},
98
+ "timestamp": 1714568400.123
99
+ }
100
+ ```
101
+
102
+ | Field | Type | Required | Description |
103
+ |---|---|---|---|
104
+ | `type` | string | yes | One of the canonical event-type strings (see vocabulary below). Unknown types are tolerated — forward-compat. |
105
+ | `channel` | string-or-null | yes | Channel name for channel-scoped events, `null` otherwise. |
106
+ | `nick` | string | yes | Actor's nickname, or empty string for purely-server-emitted events. |
107
+ | `data` | object | yes | Type-specific payload. Always an object. Keys starting with `_` are reserved metadata (e.g. `_origin` is the originating server name across federation links). |
108
+ | `timestamp` | number | yes | Unix epoch seconds with sub-second precision. |
109
+
110
+ JSON encoding is canonical: keys sorted lexicographically, separators `","`
111
+ and `":"` (no spaces), UTF-8.
112
+
113
+ ## Event-type vocabulary
114
+
115
+ | Type string | Channel-scoped | Description |
116
+ |---|---|---|
117
+ | `message` | yes | `PRIVMSG` to a channel. |
118
+ | `user.join` | yes | A user joined a channel. |
119
+ | `user.part` | yes | A user left a channel. |
120
+ | `user.quit` | no | A user quit the server. |
121
+ | `topic` | yes | Channel topic changed. |
122
+ | `room.create` | yes | Room created via `ROOMCREATE` skill. |
123
+ | `room.archive` | yes | Room archived via `ROOMARCHIVE` skill. |
124
+ | `room.meta` | yes | Room metadata updated via `ROOMMETA` skill. |
125
+ | `tags.update` | yes | User tags changed via `TAGS` skill. |
126
+ | `thread.create` | yes | Thread created. |
127
+ | `thread.message` | yes | Message posted to a thread. |
128
+ | `thread.close` | yes | Thread closed. |
129
+ | `agent.connect` | no | An agent (CAP-bot) finished registration. |
130
+ | `agent.disconnect` | no | An agent disconnected. |
131
+ | `console.open` | no | Console session opened. |
132
+ | `console.close` | no | Console session closed. |
133
+ | `server.wake` | no | This server finished startup. |
134
+ | `server.sleep` | no | This server is shutting down. |
135
+ | `server.link` | no | A federation peer linked. |
136
+ | `server.unlink` | no | A federation peer link dropped. |
137
+
138
+ Adding new type strings is a minor bump. Renaming or removing a type string
139
+ is a major bump. Bot code must tolerate unknown types and forward-skip them.
140
+
141
+ ### Already-delivered events
142
+
143
+ `message`, `topic`, `thread.create`, `thread.message`, and `thread.close` are
144
+ already delivered to channel members via the normal IRC path (`PRIVMSG`,
145
+ `TOPIC`) or have dedicated storage (threads). Subscribers will see them once
146
+ via `EVENTSUB`; they are not double-delivered as `PRIVMSG`s to `#system`
147
+ carrying the `@event=<type>` and `@event-data=<base64-json>` IRCv3 tags.
148
+
149
+ ## Backpressure
150
+
151
+ Each subscription owns a bounded send queue (default 1024 events, configurable
152
+ server-side via `ServerConfig.event_subscription_queue_max`).
153
+
154
+ When the queue overflows:
155
+
156
+ 1. Server sends `EVENTERR <sub-id> :backpressure-overflow`.
157
+ 2. The subscription is removed.
158
+ 3. The client connection itself stays open.
159
+ 4. To recover: re-subscribe with the same or a fresh `<sub-id>`, then issue
160
+ `BACKFILL` to catch up on missed history.
161
+
162
+ Bots should aim to drain `EVENT` lines as fast as they arrive. If a bot
163
+ genuinely cannot keep up, the right response is to widen the filter (subscribe
164
+ to fewer types/channels), not to ignore overflow.
165
+
166
+ ## Emitting custom events (`EVENTPUB`)
167
+
168
+ Bots can emit their own typed events back into the stream — useful for
169
+ chained-bot patterns where one bot's emission triggers another bot's logic
170
+ (e.g. a welcome bot fires `welcome.greeted`, an onboarding logger
171
+ subscribes to it).
172
+
173
+ ```text
174
+ EVENTPUB <type> <channel-or-*> :<base64-json-data>
175
+ ```
176
+
177
+ - `<type>` — must match `^[a-z][a-z0-9_-]*(\.[a-z][a-z0-9_-]*)+$` (at
178
+ least one dot segment). Single-segment names like `message` or `topic`
179
+ are reserved for the built-in vocabulary and rejected with
180
+ `EVENTERR <type> :invalid-type`.
181
+ - `<channel-or-*>` — exact channel name or `*` for non-channel-scoped.
182
+ - `:<base64-json-data>` — type-specific payload; must be a JSON object.
183
+
184
+ The server fills in `nick` (from your connection — bots cannot spoof it)
185
+ and `timestamp` (server-side, so federation peers see consistent values),
186
+ constructs the full `Event`, and feeds it into the same emit pipeline that
187
+ handles built-in events. Subscribers see it as an `EVENT` line; peers
188
+ across federation receive a `SEVENT` relay.
189
+
190
+ Reflexive: a bot subscribed to a filter that matches its own emission
191
+ receives the `EVENT` line for it. Filter on `nick` if you want to ignore
192
+ self-emissions.
193
+
194
+ `EVENTPUB` requires the `agentirc.io/bot` capability (same gate as
195
+ `EVENTSUB`).
196
+
197
+ ## Mentioning, DMs, ops
198
+
199
+ Unchanged for bot-CAP clients:
200
+
201
+ - **Mentions** — when another user `PRIVMSG`s a channel containing `@yourbot`,
202
+ the server sends a `NOTICE` to your bot with the mention context.
203
+ - **DMs** — `PRIVMSG mybot :hi` from any other user reaches the bot
204
+ normally.
205
+ - **Channel ops** — bots are not granted ops on auto-op (the first non-bot
206
+ human in a new channel becomes op). A channel operator may explicitly grant
207
+ ops to a bot via `MODE`.
208
+
209
+ ## Identifying yourself in NAMES / WHO
210
+
211
+ Bots appear in `NAMES` prefixed with `+`:
212
+
213
+ ```text
214
+ :server 353 mynick = #room :alice @bob +mybot
215
+ ```
216
+
217
+ And in `WHO` with a `B` flag:
218
+
219
+ ```text
220
+ :server 352 mynick #room mybot bothost server mybot HB :0 My Bot
221
+ ```
222
+
223
+ Both flags are derived from the negotiated CAP at output time. They cannot
224
+ be set or unset by `MODE`. Human IRC clients that filter on these flags will
225
+ hide bots from presence lists.
226
+
227
+ ## What the server does *not* expose
228
+
229
+ - **No bot manager.** `agentirc` does not host or supervise bot processes.
230
+ Run your bot wherever you like; it is just a TCP client.
231
+ - **No HTTP webhook listener.** As of 9.5.0, `agentirc` does not bind
232
+ `webhook_port`. The field stays in `ServerConfig` for backward
233
+ compatibility, but webhook→bot dispatch is the consumer's responsibility.
234
+ See [`deployment.md`](deployment.md) for details.
235
+ - **No SASL or token auth on bot CAP.** Bots authenticate the same way
236
+ human clients do, using whatever client authentication the server
237
+ currently supports. Per-bot ACLs are a future issue.
238
+
239
+ ## Reference
240
+
241
+ - Full design: [`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md)
242
+ - Public-API contract: [`docs/api-stability.md`](api-stability.md)
243
+ - Tracking issue: [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15)