agentirc-cli 9.4.1__tar.gz → 9.5.0a2__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.
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/CHANGELOG.md +47 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/PKG-INFO +1 -1
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/telemetry/audit.py +7 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/cli.py +4 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/config.py +10 -2
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/ircd.py +58 -18
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/protocol.py +132 -2
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/server_link.py +59 -9
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skill.py +10 -37
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/api-stability.md +58 -0
- agentirc_cli-9.5.0a2/docs/extension-api.md +243 -0
- agentirc_cli-9.5.0a2/docs/superpowers/specs/2026-05-01-bot-extension-api-design.md +458 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/pyproject.toml +1 -1
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_basic.py +9 -2
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_lifecycle.py +16 -9
- agentirc_cli-9.5.0a2/tests/test_protocol_bot_exports.py +214 -0
- agentirc_cli-9.5.0a2/tests/test_wire_format_envelope.py +325 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/uv.lock +1 -1
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/pr-sonar.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.claude/skills.local.yaml.example +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/.gitignore +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/CLAUDE.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/LICENSE +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/README.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/__main__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/aio.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/bots/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/bots/bot_manager.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/bots/http_listener.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/cli_shared/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/cli_shared/constants.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/cli_shared/mesh.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/constants.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/pidfile.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/protocol/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/protocol/message.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/protocol/replies.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/telemetry/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/telemetry/context.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/telemetry/metrics.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/telemetry/tracing.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/_internal/virtual_client.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/channel.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/client.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/events.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/history_store.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/remote_client.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/room_store.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/rooms_util.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skills/history.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skills/icon.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/skills/threads.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/agentirc/thread_store.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/cli.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/deployment.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/steward/onboarding.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/docs/superpowers/specs/2026-05-01-task14-audit.md +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/_helpers.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/conftest.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/__init__.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/_fakes.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/_metrics_helpers.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_audit_emit.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_audit_lifecycle.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_audit_module.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_audit_parse_error.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_config.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_dispatch_span.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_emit_event_span.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_metrics_init.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_metrics_s2s.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_outbound_inject.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_parse_error.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_s2s_relay_span.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_server_init.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_server_link_inject.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/telemetry/test_tracing.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_channel.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_cli.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_config_loader.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_connection.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_discovery.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_federation.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_history.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_federation.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_history.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_mentions.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_messaging.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_modes.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_skills.py +0 -0
- {agentirc_cli-9.4.1 → agentirc_cli-9.5.0a2}/tests/test_threads.py +0 -0
|
@@ -4,6 +4,53 @@ 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.0a2] - 2026-05-02
|
|
8
|
+
|
|
9
|
+
### Changed
|
|
10
|
+
|
|
11
|
+
- **Wire format: SEVENT payload and IRCv3 `event-data` tag now carry the 5-field envelope** `{type, channel, nick, data, timestamp}`. Both the federation seam (`SEVENT` between linked servers) and the human-visible `#system` PRIVMSG `event-data` tag emit the new shape via `IRCd._build_event_envelope` + `IRCd._encode_event_data`. Type-specific keys (e.g. `text`, `room_id`, `purpose`) now nest under `data`; `nick` and `channel` remain at the top level. See `docs/superpowers/specs/2026-05-01-bot-extension-api-design.md` § Decision A for the rationale.
|
|
12
|
+
- `IRCd._build_event_payload` is replaced by `IRCd._build_event_envelope` (internal — no public API impact). Old name removed; the only two callsites (`_surface_event_privmsg` and `ServerLink.relay_event`'s SEVENT fallback) updated in the same commit.
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- `ServerLink._is_envelope(decoded)` — sniff helper that recognises the new envelope shape vs. the legacy data-only dict at decode time. `_handle_sevent` uses it for asymmetric tolerance (see Notes).
|
|
17
|
+
- `agentirc.protocol.SEVENT` — verb-name constant for the federation event-relay verb. Used by the new test suite and the outbound relay path. Other agentirc-side string-literal callsites are tracked separately in [#11](https://github.com/agentculture/agentirc/issues/11).
|
|
18
|
+
- `tests/test_wire_format_envelope.py` — 12 tests including a golden-file byte-equality lock-in for the canonical JSON encoding (any future change to the encoder that breaks this is a wire-format break and requires a major bump), plus regression guards for the trust-bypass and underscore-injection fixes below.
|
|
19
|
+
|
|
20
|
+
### Security
|
|
21
|
+
|
|
22
|
+
- **`ServerLink._handle_sevent` now treats the SEVENT verb-arg channel as authoritative.** The previous draft of this PR (pre-review) used the envelope's `channel` field on the receive side, which would have allowed a malformed/malicious peer to bypass `_check_incoming_trust` by sending `target="*"` (no trust check) while putting a restricted channel name in the envelope. The verb-arg channel is what the trust gate already protects, so the receiver now ignores any envelope channel claim and uses `verb_channel` for both the trust check and the resulting `Event.channel`.
|
|
23
|
+
- **Peer-supplied `_`-prefixed keys are stripped from incoming SEVENT data before emit.** `_render` and similar server-internal hints would otherwise let a peer dictate human-readable surfacing on this server. The receiver strips every key starting with `_` from the decoded data before adding its own `_origin` marker.
|
|
24
|
+
|
|
25
|
+
### Fixed
|
|
26
|
+
|
|
27
|
+
- **`IRCd._encode_event_data` fallback now produces a valid 5-field envelope instead of `b"{}"`.** A serialization failure used to emit a bare empty dict, which fails the receiver's envelope sniff and is misclassified as legacy data-only — worse than emitting an event with no type-specific payload. The fallback now emits `{type, channel, nick, data: {}, timestamp: time.time()}` so consumers can still parse the shape.
|
|
28
|
+
|
|
29
|
+
### Notes
|
|
30
|
+
|
|
31
|
+
- **Federation interop story.** A 9.5 daemon's `_handle_sevent` sniffs the decoded payload: if it has a top-level `type` (string) and `data` (dict), treat as 9.5+ envelope; otherwise treat as ≤9.4 legacy data-only and reconstruct an `Event` from the SEVENT verb args + the bare data dict. This gives 9.5 receivers asymmetric tolerance — they read traffic from both upgraded and pre-9.5 peers — so federations can roll forward one peer at a time. The reverse direction (9.5 emit → 9.4 receive) does **not** work; 9.4 daemons have no sniff and will fail to find expected `data` fields. Operators must either upgrade all peers in lockstep, or roll one at a time and accept that 9.5-emitted events drop on still-9.4 peers until those peers also upgrade. This matches the migration cost the spec calls out.
|
|
32
|
+
- **Audit JSONL is unchanged.** `agentirc/_internal/telemetry/audit.py:build_audit_record` continues to record the legacy data-only payload (data dict with `nick`/`channel` merged in via `setdefault`, `_`-prefixed keys stripped). Ops dashboards and downstream log processors that read the audit JSONL keep their existing field shape; the 5-field envelope is reserved for the public network surface (SEVENT payload + IRCv3 `event-data` tag).
|
|
33
|
+
- **Audit timestamp on legacy peers.** When `_handle_sevent` decodes a legacy payload from a ≤9.4 peer, no originating timestamp is available; the receiver assigns `time.time()` to the reconstructed `Event.timestamp`. 9.5+ peers preserve the originating timestamp.
|
|
34
|
+
- This is the **wire-format slice** of the bot extension API. The next slice (9.5.0a3) wires the bot CAP behavior (`agentirc.io/bot`), the `EVENTSUB`/`EVENTUNSUB`/`EVENTPUB` handlers, and the `SubscriptionRegistry`. 9.5.0 final adds `webhook_port` unbinding and flips `docs/api-stability.md` to "current".
|
|
35
|
+
- Tracks [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
|
|
36
|
+
|
|
37
|
+
## [9.5.0a1] - 2026-05-02
|
|
38
|
+
|
|
39
|
+
### Added
|
|
40
|
+
|
|
41
|
+
- 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.
|
|
42
|
+
- `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.
|
|
43
|
+
- `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.
|
|
44
|
+
|
|
45
|
+
### Changed
|
|
46
|
+
|
|
47
|
+
- `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.
|
|
48
|
+
|
|
49
|
+
### Notes
|
|
50
|
+
|
|
51
|
+
- 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.
|
|
52
|
+
- Tracks [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
|
|
53
|
+
|
|
7
54
|
## [9.4.1] - 2026-05-01
|
|
8
55
|
|
|
9
56
|
### Documentation
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.5.0a2
|
|
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
|
|
@@ -313,6 +313,13 @@ def build_audit_record(
|
|
|
313
313
|
|
|
314
314
|
See culture/protocol/extensions/audit.md for the field set.
|
|
315
315
|
"""
|
|
316
|
+
# Audit JSONL records the legacy data-only payload (data dict with
|
|
317
|
+
# nick/channel merged in via setdefault, _-prefixed keys stripped).
|
|
318
|
+
# This intentionally diverges from the 9.5+ public wire envelope built
|
|
319
|
+
# by IRCd._build_event_envelope: ops dashboards and downstream log
|
|
320
|
+
# processors that read the audit JSONL keep their existing field shape;
|
|
321
|
+
# the 5-field envelope is reserved for the public network surface
|
|
322
|
+
# (SEVENT payload + IRCv3 event-data tag).
|
|
316
323
|
payload = {k: v for k, v in (event.data or {}).items() if not k.startswith("_")}
|
|
317
324
|
if event.nick:
|
|
318
325
|
payload.setdefault("nick", event.nick)
|
|
@@ -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``,
|
|
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 []
|
|
@@ -275,31 +275,63 @@ class IRCd:
|
|
|
275
275
|
_NO_SURFACE_TYPES = NO_SURFACE_EVENT_TYPES
|
|
276
276
|
|
|
277
277
|
@staticmethod
|
|
278
|
-
def
|
|
279
|
-
"""Build the public event
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
278
|
+
def _build_event_envelope(event: Event) -> dict:
|
|
279
|
+
"""Build the public 5-field event envelope.
|
|
280
|
+
|
|
281
|
+
Shape (semver-stable as of 9.5.0a2)::
|
|
282
|
+
|
|
283
|
+
{
|
|
284
|
+
"type": str, # wire-form event type, e.g. "user.join"
|
|
285
|
+
"channel": str | None, # channel name or None
|
|
286
|
+
"nick": str, # actor's nickname (empty for purely-server events)
|
|
287
|
+
"data": dict, # type-specific payload, _-prefixed keys stripped
|
|
288
|
+
"timestamp": float, # Unix epoch seconds with sub-second precision
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
See ``docs/superpowers/specs/2026-05-01-bot-extension-api-design.md``
|
|
292
|
+
§ Decision A for the rationale and federation-interop story.
|
|
293
|
+
"""
|
|
294
|
+
type_wire = event.type.value if hasattr(event.type, "value") else str(event.type)
|
|
295
|
+
return {
|
|
296
|
+
"type": type_wire,
|
|
297
|
+
"channel": event.channel,
|
|
298
|
+
"nick": event.nick,
|
|
299
|
+
"data": {k: v for k, v in event.data.items() if not k.startswith("_")},
|
|
300
|
+
"timestamp": event.timestamp,
|
|
301
|
+
}
|
|
288
302
|
|
|
289
303
|
@staticmethod
|
|
290
|
-
def _encode_event_data(
|
|
291
|
-
"""Base64-encode the
|
|
304
|
+
def _encode_event_data(envelope: dict, type_wire: str) -> str:
|
|
305
|
+
"""Base64-encode the envelope as canonical JSON.
|
|
306
|
+
|
|
307
|
+
On serialization failure the fallback is a *minimal but well-formed*
|
|
308
|
+
envelope ``{type, channel, nick, data, timestamp}`` with empty
|
|
309
|
+
``data`` and current wall-clock ``timestamp`` — never a bare ``{}``.
|
|
310
|
+
Subscribers and federation peers depend on the 5-field shape; an
|
|
311
|
+
empty-dict fallback would fail the receiver's envelope sniff and
|
|
312
|
+
be misclassified as legacy data-only, which is worse than emitting
|
|
313
|
+
an event with no type-specific payload.
|
|
314
|
+
"""
|
|
292
315
|
try:
|
|
293
316
|
return base64.b64encode(
|
|
294
|
-
json.dumps(
|
|
317
|
+
json.dumps(envelope, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
295
318
|
).decode("ascii")
|
|
296
319
|
except (TypeError, ValueError) as exc:
|
|
297
320
|
logger.warning(
|
|
298
|
-
"Event %s
|
|
321
|
+
"Event %s envelope not JSON-serializable, surfacing empty envelope: %s",
|
|
299
322
|
type_wire,
|
|
300
323
|
exc,
|
|
301
324
|
)
|
|
302
|
-
|
|
325
|
+
empty_envelope = {
|
|
326
|
+
"type": type_wire,
|
|
327
|
+
"channel": envelope.get("channel") if isinstance(envelope, dict) else None,
|
|
328
|
+
"nick": envelope.get("nick", "") if isinstance(envelope, dict) else "",
|
|
329
|
+
"data": {},
|
|
330
|
+
"timestamp": time.time(),
|
|
331
|
+
}
|
|
332
|
+
return base64.b64encode(
|
|
333
|
+
json.dumps(empty_envelope, separators=(",", ":"), sort_keys=True).encode("utf-8")
|
|
334
|
+
).decode("ascii")
|
|
303
335
|
|
|
304
336
|
async def _deliver_to_members(self, channel, msg: Message, type_wire: str) -> None:
|
|
305
337
|
"""Send the surfaced PRIVMSG to channel members (skipping VirtualClients)."""
|
|
@@ -352,9 +384,17 @@ class IRCd:
|
|
|
352
384
|
origin_server = event.data.get("_origin") or self.config.name
|
|
353
385
|
system_nick = f"{SYSTEM_USER_PREFIX}{origin_server}"
|
|
354
386
|
|
|
355
|
-
|
|
356
|
-
encoded = self._encode_event_data(
|
|
357
|
-
|
|
387
|
+
envelope = self._build_event_envelope(event)
|
|
388
|
+
encoded = self._encode_event_data(envelope, type_wire)
|
|
389
|
+
# Render templates expect the type-specific data dict (with nick/channel
|
|
390
|
+
# merged in for backward compat with the pre-9.5 render-function shape),
|
|
391
|
+
# not the envelope. Reconstruct the legacy data shape for render only.
|
|
392
|
+
render_data = dict(envelope["data"])
|
|
393
|
+
if event.nick:
|
|
394
|
+
render_data.setdefault("nick", event.nick)
|
|
395
|
+
if event.channel:
|
|
396
|
+
render_data.setdefault("channel", event.channel)
|
|
397
|
+
body = event.data.get("_render") or render_event(type_wire, render_data, event.channel)
|
|
358
398
|
|
|
359
399
|
msg = Message(
|
|
360
400
|
tags={EVENT_TAG_TYPE: type_wire, EVENT_TAG_DATA: encoded},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
"""Public protocol surface for agentirc — verbs, numerics, and
|
|
1
|
+
"""Public protocol surface for agentirc — verbs, numerics, tags, and the bot extension API.
|
|
2
2
|
|
|
3
|
-
Semver-tracked module.
|
|
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
|
# ---------------------------------------------------------------------------
|
|
@@ -132,6 +147,7 @@ SROOMMETA = "SROOMMETA"
|
|
|
132
147
|
SROOMARCHIVE = "SROOMARCHIVE"
|
|
133
148
|
STAGS = "STAGS"
|
|
134
149
|
STHREAD = "STHREAD"
|
|
150
|
+
SEVENT = "SEVENT"
|
|
135
151
|
BACKFILL = "BACKFILL"
|
|
136
152
|
BACKFILLEND = "BACKFILLEND"
|
|
137
153
|
|
|
@@ -164,6 +180,90 @@ ROOMETAEND = "ROOMETAEND" # SIC: typo preserved for wire compat (ROOMMETAEND ta
|
|
|
164
180
|
ROOMETASET = "ROOMETASET" # SIC: typo preserved for wire compat (ROOMMETASET target)
|
|
165
181
|
|
|
166
182
|
|
|
183
|
+
# ---------------------------------------------------------------------------
|
|
184
|
+
# Bot extension API (9.5.0)
|
|
185
|
+
# ---------------------------------------------------------------------------
|
|
186
|
+
# Public Event dataclass + EventType enum, per-type string constants, the
|
|
187
|
+
# EVENTSUB / EVENTUNSUB / EVENT / EVENTERR / EVENTPUB verb names, and the
|
|
188
|
+
# bot-CAP token. See docs/superpowers/specs/2026-05-01-bot-extension-api-design.md
|
|
189
|
+
# for the wire format and verb syntax. Behavior wiring lands in 9.5.0a2/a3;
|
|
190
|
+
# 9.5.0a1 ships these symbols only.
|
|
191
|
+
|
|
192
|
+
# `EventType` is `StrEnum` so `EventType.JOIN == "user.join"` is True at JSON
|
|
193
|
+
# boundaries. Adding a new member is a minor bump; renaming or removing one
|
|
194
|
+
# is a major bump.
|
|
195
|
+
|
|
196
|
+
|
|
197
|
+
class EventType(StrEnum):
|
|
198
|
+
MESSAGE = "message"
|
|
199
|
+
JOIN = "user.join"
|
|
200
|
+
PART = "user.part"
|
|
201
|
+
QUIT = "user.quit"
|
|
202
|
+
TOPIC = "topic"
|
|
203
|
+
ROOMMETA = "room.meta"
|
|
204
|
+
TAGS = "tags.update"
|
|
205
|
+
ROOMARCHIVE = "room.archive"
|
|
206
|
+
THREAD_CREATE = "thread.create"
|
|
207
|
+
THREAD_MESSAGE = "thread.message"
|
|
208
|
+
THREAD_CLOSE = "thread.close"
|
|
209
|
+
AGENT_CONNECT = "agent.connect"
|
|
210
|
+
AGENT_DISCONNECT = "agent.disconnect"
|
|
211
|
+
CONSOLE_OPEN = "console.open"
|
|
212
|
+
CONSOLE_CLOSE = "console.close"
|
|
213
|
+
SERVER_WAKE = "server.wake"
|
|
214
|
+
SERVER_SLEEP = "server.sleep"
|
|
215
|
+
SERVER_LINK = "server.link"
|
|
216
|
+
SERVER_UNLINK = "server.unlink"
|
|
217
|
+
ROOM_CREATE = "room.create"
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
@dataclass
|
|
221
|
+
class Event:
|
|
222
|
+
# `type` is widened to `EventType | str` so federation peers can deliver
|
|
223
|
+
# event types this version doesn't recognise without raising. Subscribers
|
|
224
|
+
# must tolerate unknown types (forward-compat).
|
|
225
|
+
type: EventType | str
|
|
226
|
+
channel: str | None
|
|
227
|
+
nick: str
|
|
228
|
+
data: dict[str, Any] = field(default_factory=dict)
|
|
229
|
+
timestamp: float = field(default_factory=time.time)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# Per-type string constants — parallel to `EventType` for callers that prefer
|
|
233
|
+
# bare strings (e.g. comparing JSON-decoded `type` field without enum-coercing).
|
|
234
|
+
EVENT_TYPE_MESSAGE = "message"
|
|
235
|
+
EVENT_TYPE_USER_JOIN = "user.join"
|
|
236
|
+
EVENT_TYPE_USER_PART = "user.part"
|
|
237
|
+
EVENT_TYPE_USER_QUIT = "user.quit"
|
|
238
|
+
EVENT_TYPE_TOPIC = "topic"
|
|
239
|
+
EVENT_TYPE_ROOM_META = "room.meta"
|
|
240
|
+
EVENT_TYPE_TAGS_UPDATE = "tags.update"
|
|
241
|
+
EVENT_TYPE_ROOM_ARCHIVE = "room.archive"
|
|
242
|
+
EVENT_TYPE_THREAD_CREATE = "thread.create"
|
|
243
|
+
EVENT_TYPE_THREAD_MESSAGE = "thread.message"
|
|
244
|
+
EVENT_TYPE_THREAD_CLOSE = "thread.close"
|
|
245
|
+
EVENT_TYPE_AGENT_CONNECT = "agent.connect"
|
|
246
|
+
EVENT_TYPE_AGENT_DISCONNECT = "agent.disconnect"
|
|
247
|
+
EVENT_TYPE_CONSOLE_OPEN = "console.open"
|
|
248
|
+
EVENT_TYPE_CONSOLE_CLOSE = "console.close"
|
|
249
|
+
EVENT_TYPE_SERVER_WAKE = "server.wake"
|
|
250
|
+
EVENT_TYPE_SERVER_SLEEP = "server.sleep"
|
|
251
|
+
EVENT_TYPE_SERVER_LINK = "server.link"
|
|
252
|
+
EVENT_TYPE_SERVER_UNLINK = "server.unlink"
|
|
253
|
+
EVENT_TYPE_ROOM_CREATE = "room.create"
|
|
254
|
+
|
|
255
|
+
# Bot extension verbs.
|
|
256
|
+
EVENTSUB = "EVENTSUB"
|
|
257
|
+
EVENTUNSUB = "EVENTUNSUB"
|
|
258
|
+
EVENT = "EVENT"
|
|
259
|
+
EVENTERR = "EVENTERR"
|
|
260
|
+
EVENTPUB = "EVENTPUB"
|
|
261
|
+
|
|
262
|
+
# Bot-CAP token. Vendored namespace per IRCv3 conventions, prevents collision
|
|
263
|
+
# with hypothetical bare-`bot` caps from non-agentirc IRC servers.
|
|
264
|
+
BOT_CAP = "agentirc.io/bot"
|
|
265
|
+
|
|
266
|
+
|
|
167
267
|
__all__ = [
|
|
168
268
|
# Numerics
|
|
169
269
|
"ERR_ALREADYREGISTRED",
|
|
@@ -245,6 +345,7 @@ __all__ = [
|
|
|
245
345
|
"BACKFILL",
|
|
246
346
|
"BACKFILLEND",
|
|
247
347
|
"SERVER",
|
|
348
|
+
"SEVENT",
|
|
248
349
|
"SJOIN",
|
|
249
350
|
"SMSG",
|
|
250
351
|
"SNICK",
|
|
@@ -256,4 +357,33 @@ __all__ = [
|
|
|
256
357
|
"STAGS",
|
|
257
358
|
"STHREAD",
|
|
258
359
|
"STOPIC",
|
|
360
|
+
# Bot extension API (9.5.0)
|
|
361
|
+
"BOT_CAP",
|
|
362
|
+
"EVENT",
|
|
363
|
+
"EVENTERR",
|
|
364
|
+
"EVENTPUB",
|
|
365
|
+
"EVENTSUB",
|
|
366
|
+
"EVENTUNSUB",
|
|
367
|
+
"Event",
|
|
368
|
+
"EventType",
|
|
369
|
+
"EVENT_TYPE_AGENT_CONNECT",
|
|
370
|
+
"EVENT_TYPE_AGENT_DISCONNECT",
|
|
371
|
+
"EVENT_TYPE_CONSOLE_CLOSE",
|
|
372
|
+
"EVENT_TYPE_CONSOLE_OPEN",
|
|
373
|
+
"EVENT_TYPE_MESSAGE",
|
|
374
|
+
"EVENT_TYPE_ROOM_ARCHIVE",
|
|
375
|
+
"EVENT_TYPE_ROOM_CREATE",
|
|
376
|
+
"EVENT_TYPE_ROOM_META",
|
|
377
|
+
"EVENT_TYPE_SERVER_LINK",
|
|
378
|
+
"EVENT_TYPE_SERVER_SLEEP",
|
|
379
|
+
"EVENT_TYPE_SERVER_UNLINK",
|
|
380
|
+
"EVENT_TYPE_SERVER_WAKE",
|
|
381
|
+
"EVENT_TYPE_TAGS_UPDATE",
|
|
382
|
+
"EVENT_TYPE_THREAD_CLOSE",
|
|
383
|
+
"EVENT_TYPE_THREAD_CREATE",
|
|
384
|
+
"EVENT_TYPE_THREAD_MESSAGE",
|
|
385
|
+
"EVENT_TYPE_TOPIC",
|
|
386
|
+
"EVENT_TYPE_USER_JOIN",
|
|
387
|
+
"EVENT_TYPE_USER_PART",
|
|
388
|
+
"EVENT_TYPE_USER_QUIT",
|
|
259
389
|
]
|
|
@@ -11,6 +11,7 @@ from typing import TYPE_CHECKING
|
|
|
11
11
|
from opentelemetry import trace as otel_trace
|
|
12
12
|
from opentelemetry.context import Context as _OtelContext
|
|
13
13
|
|
|
14
|
+
from agentirc import protocol
|
|
14
15
|
from agentirc.remote_client import RemoteClient
|
|
15
16
|
from agentirc.skill import Event, EventType
|
|
16
17
|
from agentirc._internal.aio import maybe_await
|
|
@@ -887,34 +888,83 @@ class ServerLink:
|
|
|
887
888
|
except ValueError:
|
|
888
889
|
return type_str
|
|
889
890
|
|
|
891
|
+
@staticmethod
|
|
892
|
+
def _is_envelope(decoded: dict) -> bool:
|
|
893
|
+
"""Sniff whether a decoded SEVENT payload is a 9.5+ 5-field envelope.
|
|
894
|
+
|
|
895
|
+
9.5+ peers emit ``{type, channel, nick, data, timestamp}`` with a
|
|
896
|
+
nested ``data`` dict. 9.4 and earlier peers emit the type-specific
|
|
897
|
+
``data`` dict directly (no outer envelope, no top-level ``type``).
|
|
898
|
+
Both are valid SEVENT payloads on the wire; this sniff lets a 9.5
|
|
899
|
+
receiver tolerate either shape so federations can roll forward one
|
|
900
|
+
peer at a time.
|
|
901
|
+
"""
|
|
902
|
+
return (
|
|
903
|
+
isinstance(decoded.get("type"), str)
|
|
904
|
+
and isinstance(decoded.get("data"), dict)
|
|
905
|
+
)
|
|
906
|
+
|
|
890
907
|
async def _handle_sevent(self, msg: Message) -> None:
|
|
891
908
|
"""Ingest a generic federated event from a peer.
|
|
892
909
|
|
|
893
|
-
Wire format: SEVENT <origin-server> <seq> <type> <channel_or_*> :<b64-json
|
|
910
|
+
Wire format: SEVENT <origin-server> <seq> <type> <channel_or_*> :<b64-json>
|
|
911
|
+
|
|
912
|
+
The base64-JSON payload is the 5-field envelope from 9.5+ peers, or
|
|
913
|
+
a bare type-specific data dict from ≤9.4 peers (asymmetric tolerance
|
|
914
|
+
via :meth:`_is_envelope`). Either way we reconstruct an :class:`Event`
|
|
915
|
+
with all five fields populated, falling back to ``time.time()`` for
|
|
916
|
+
``timestamp`` only when the legacy peer didn't provide one.
|
|
894
917
|
"""
|
|
895
918
|
if len(msg.params) < 5:
|
|
896
919
|
return
|
|
897
920
|
origin, _seq, type_str, target, b64 = msg.params[:5]
|
|
898
|
-
|
|
921
|
+
verb_channel = None if target == "*" else target
|
|
899
922
|
|
|
900
923
|
if origin != self.peer_name:
|
|
901
924
|
logger.warning("SEVENT origin %s != peer %s", origin, self.peer_name)
|
|
902
925
|
|
|
903
|
-
if
|
|
926
|
+
if verb_channel is not None and not self._check_incoming_trust(verb_channel):
|
|
904
927
|
return
|
|
905
928
|
|
|
906
|
-
|
|
907
|
-
if
|
|
929
|
+
decoded = self._decode_sevent_payload(b64, self.peer_name)
|
|
930
|
+
if decoded is None:
|
|
908
931
|
return
|
|
909
932
|
|
|
933
|
+
if self._is_envelope(decoded):
|
|
934
|
+
# 9.5+ envelope. Verb-arg type wins on mismatch (defensive — the
|
|
935
|
+
# SEVENT line is the canonical type carrier).
|
|
936
|
+
raw_data = decoded["data"]
|
|
937
|
+
nick = decoded.get("nick") or f"{SYSTEM_USER_PREFIX}{origin}"
|
|
938
|
+
timestamp = decoded.get("timestamp")
|
|
939
|
+
if not isinstance(timestamp, (int, float)):
|
|
940
|
+
timestamp = time.time()
|
|
941
|
+
else:
|
|
942
|
+
# ≤9.4 legacy: decoded dict IS the data payload.
|
|
943
|
+
raw_data = decoded
|
|
944
|
+
nick = raw_data.get("nick", f"{SYSTEM_USER_PREFIX}{origin}")
|
|
945
|
+
timestamp = time.time()
|
|
946
|
+
|
|
947
|
+
# Verb-arg channel is authoritative for routing and trust: the
|
|
948
|
+
# _check_incoming_trust() above gated on it. Ignore any channel claim
|
|
949
|
+
# in the envelope — a malformed/malicious peer must not be able to
|
|
950
|
+
# bypass trust by sending target="*" while putting a restricted
|
|
951
|
+
# channel name in the envelope.
|
|
952
|
+
channel = verb_channel
|
|
953
|
+
|
|
954
|
+
# Strip incoming `_`-prefixed keys from the peer-supplied data before
|
|
955
|
+
# we add our own `_origin` marker. `_render` and similar server-side
|
|
956
|
+
# hints must not be peer-controllable; allowing them would let a
|
|
957
|
+
# peer dictate the human-readable surfacing on this server.
|
|
958
|
+
data = {k: v for k, v in raw_data.items() if not k.startswith("_")}
|
|
910
959
|
data["_origin"] = origin
|
|
911
960
|
type_enum = self._parse_event_type(type_str)
|
|
912
961
|
|
|
913
962
|
ev = Event(
|
|
914
963
|
type=type_enum,
|
|
915
964
|
channel=channel,
|
|
916
|
-
nick=
|
|
965
|
+
nick=nick,
|
|
917
966
|
data=data,
|
|
967
|
+
timestamp=timestamp,
|
|
918
968
|
)
|
|
919
969
|
await self.server.emit_event(ev)
|
|
920
970
|
|
|
@@ -1014,14 +1064,14 @@ class ServerLink:
|
|
|
1014
1064
|
else:
|
|
1015
1065
|
# If no typed relay exists, fall back to generic SEVENT.
|
|
1016
1066
|
# v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
|
|
1017
|
-
|
|
1018
|
-
encoded = self.server._encode_event_data(
|
|
1067
|
+
envelope = self.server._build_event_envelope(event)
|
|
1068
|
+
encoded = self.server._encode_event_data(envelope, event_type_str)
|
|
1019
1069
|
target = event.channel or "*"
|
|
1020
1070
|
# Egress trust check: channel-scoped events respect should_relay; global events always relay
|
|
1021
1071
|
if event.channel is None or self.should_relay(event.channel):
|
|
1022
1072
|
seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
|
|
1023
1073
|
await self.send_raw(
|
|
1024
|
-
f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
|
|
1074
|
+
f":{origin} {protocol.SEVENT} {origin} {seq} {event_type_str} {target} :{encoded}"
|
|
1025
1075
|
)
|
|
1026
1076
|
relayed = True
|
|
1027
1077
|
|
|
@@ -1,9 +1,15 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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,35 @@ 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 (shipped):** `agentirc.protocol` exports the `Event`
|
|
18
|
+
> dataclass, the `EventType` enum (now `StrEnum`), 20 per-type
|
|
19
|
+
> `EVENT_TYPE_*` string constants, the `EVENTSUB`/`EVENTUNSUB`/
|
|
20
|
+
> `EVENT`/`EVENTERR`/`EVENTPUB` verb constants, and the
|
|
21
|
+
> `BOT_CAP = "agentirc.io/bot"` capability identifier.
|
|
22
|
+
> `ServerConfig` gains the `event_subscription_queue_max: int = 1024`
|
|
23
|
+
> field. **Declarations slice — symbols are importable but inert.**
|
|
24
|
+
> - **9.5.0a2 (current alpha — wire-format slice):** `IRCd._build_event_payload`
|
|
25
|
+
> replaced by `IRCd._build_event_envelope`. Federated `SEVENT` payload
|
|
26
|
+
> and the IRCv3 `event-data` tag on `#system` PRIVMSGs now carry the
|
|
27
|
+
> canonical 5-field envelope `{type, channel, nick, data, timestamp}`.
|
|
28
|
+
> `ServerLink._handle_sevent` sniffs the shape so 9.5 receivers
|
|
29
|
+
> tolerate both envelope (9.5+ peers) and legacy data-only (≤9.4
|
|
30
|
+
> peers); 9.5→9.4 emit breaks until peers upgrade. Internal change;
|
|
31
|
+
> no new public symbols. See CHANGELOG `[9.5.0a2]` § Notes.
|
|
32
|
+
> - **9.5.0a3 (planned):** bot-CAP behavior, `EVENTSUB`/`EVENTUNSUB`
|
|
33
|
+
> handlers, the in-memory `SubscriptionRegistry`, and `EVENTPUB`
|
|
34
|
+
> handler. Daemon starts advertising `BOT_CAP` in `CAP LS` output.
|
|
35
|
+
> - **9.5.0 (final):** `webhook_port` no longer bound; `cli.md` /
|
|
36
|
+
> `deployment.md` updated; this block flips from "phased rollout" to
|
|
37
|
+
> "current," and the version-history table picks up a 9.5.0 row.
|
|
38
|
+
>
|
|
39
|
+
> Wire format and verb syntax are specified in
|
|
40
|
+
> [`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md);
|
|
41
|
+
> a quick reference for bot authors is at [`docs/extension-api.md`](extension-api.md).
|
|
42
|
+
> Tracking issue: [agentculture/agentirc#15](https://github.com/agentculture/agentirc/issues/15).
|
|
43
|
+
|
|
15
44
|
## Semver contract
|
|
16
45
|
|
|
17
46
|
Following [SemVer 2.0](https://semver.org/):
|
|
@@ -179,6 +208,35 @@ Re-exported from `agentirc._internal.protocol.replies`. About 33 names:
|
|
|
179
208
|
Re-exported from `agentirc._internal.telemetry.context`:
|
|
180
209
|
`TRACEPARENT_TAG`, `TRACESTATE_TAG`, `EVENT_TAG_TYPE`, `EVENT_TAG_DATA`.
|
|
181
210
|
|
|
211
|
+
### Reserved for 9.5.0: bot extension surface
|
|
212
|
+
|
|
213
|
+
These additions are **specified but not yet implemented**. They will land
|
|
214
|
+
together as a single minor bump in 9.5.0 — see the design spec at
|
|
215
|
+
[`docs/superpowers/specs/2026-05-01-bot-extension-api-design.md`](superpowers/specs/2026-05-01-bot-extension-api-design.md)
|
|
216
|
+
for rationale, federation behavior, and acceptance criteria, and
|
|
217
|
+
[`docs/extension-api.md`](extension-api.md) for the bot-author quick
|
|
218
|
+
reference.
|
|
219
|
+
|
|
220
|
+
- **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).
|
|
221
|
+
- **Bot capability:** `BOT_CAP = "agentirc.io/bot"`. When negotiated via
|
|
222
|
+
the existing CAP REQ/ACK flow, the connection is treated as a bot:
|
|
223
|
+
silent JOIN/PART/QUIT broadcasts, no auto-op on channel creation,
|
|
224
|
+
`+` prefix in NAMES output, `B` flag in WHO output, authorized to
|
|
225
|
+
issue `EVENTSUB`.
|
|
226
|
+
- **Event dataclass and enum:** `Event` and `EventType` (currently
|
|
227
|
+
internal in `agentirc.skill`). Promoted to public for Python consumers.
|
|
228
|
+
Wire format — not the Python class names — is the contract; non-Python
|
|
229
|
+
bots pin against the JSON shape documented in `extension-api.md`.
|
|
230
|
+
- **Per-type string constants:** `EVENT_TYPE_MESSAGE`,
|
|
231
|
+
`EVENT_TYPE_USER_JOIN`, …, one per type-string in the canonical
|
|
232
|
+
vocabulary. Convenience for callers that prefer non-enum-aware
|
|
233
|
+
constants.
|
|
234
|
+
|
|
235
|
+
The `ServerConfig` additions (one new field
|
|
236
|
+
`event_subscription_queue_max: int = 1024`) and the `webhook_port`
|
|
237
|
+
binding-removal are described under
|
|
238
|
+
[`agentirc.config`](#agentircconfig) once 9.5.0 lands.
|
|
239
|
+
|
|
182
240
|
### Wire-format quirks (preserved verbatim)
|
|
183
241
|
|
|
184
242
|
Four known wire-format issues are **preserved** rather than fixed,
|