agentirc-cli 8.3.0__tar.gz → 8.5.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.
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/CHANGELOG.md +30 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/PKG-INFO +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/client.py +76 -2
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/config.py +9 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/ircd.py +50 -3
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/server_link.py +75 -14
- agentirc_cli-8.5.0/culture/protocol/extensions/audit.md +109 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/tracing.md +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/__init__.py +8 -0
- agentirc_cli-8.5.0/culture/telemetry/audit.py +380 -0
- agentirc_cli-8.5.0/culture/telemetry/metrics.py +237 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/tracing.py +4 -1
- agentirc_cli-8.5.0/docs/agentirc/audit.md +154 -0
- agentirc_cli-8.5.0/docs/agentirc/telemetry.md +166 -0
- agentirc_cli-8.5.0/docs/superpowers/plans/2026-04-26-otel-metrics.md +303 -0
- agentirc_cli-8.5.0/docs/superpowers/plans/2026-04-27-otel-audit.md +247 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/pyproject.toml +1 -1
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/conftest.py +47 -2
- agentirc_cli-8.5.0/tests/telemetry/_metrics_helpers.py +66 -0
- agentirc_cli-8.5.0/tests/telemetry/test_audit_emit.py +145 -0
- agentirc_cli-8.5.0/tests/telemetry/test_audit_federation.py +132 -0
- agentirc_cli-8.5.0/tests/telemetry/test_audit_lifecycle.py +69 -0
- agentirc_cli-8.5.0/tests/telemetry/test_audit_module.py +306 -0
- agentirc_cli-8.5.0/tests/telemetry/test_audit_parse_error.py +177 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_clients.py +79 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_events.py +81 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_init.py +58 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_irc.py +99 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_s2s.py +160 -0
- agentirc_cli-8.5.0/tests/telemetry/test_metrics_trace_inbound.py +127 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/uv.lock +1 -1
- agentirc_cli-8.3.0/docs/agentirc/telemetry.md +0 -101
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/agents/doc-test-alignment.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/run-tests/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.claude/skills/run-tests/scripts/test.sh +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.flake8 +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/docs-check.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/security-checks.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.gitignore +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.markdownlint-cli2.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pr_agent.toml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pre-commit-config.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/.pylintrc +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/CLAUDE.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/Gemfile +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/Gemfile.lock +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/LICENSE +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/SECURITY.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_config.base.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_config.culture.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_data/sites.yml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_includes/head_custom.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/color_schemes/anthropic.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/color_schemes/dark-terminal.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/_sass/custom/custom.scss +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/IMG_3183.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/apple-touch-icon.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon-16x16.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon-32x32.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/favicon.ico +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/og-agentirc.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/assets/images/og-culture.png +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/CLAUDE.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-architecture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-features.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc-skill.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/docs/agentirc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/events.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/history_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/remote_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/room_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/rooms_util.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skill.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/icon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/skills/threads.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/agentirc/thread_store.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/aio.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/bot_manager.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/filter_dsl.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/http_listener.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/bot.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/system/welcome/handler.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/template_engine.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/bots/virtual_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/_passthrough.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/afi.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/agent.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/devex.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/introspect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/mesh.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/constants.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/display.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/formatting.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/mesh.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/shared/process.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/cli/skills.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/acp/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/__main__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/claude/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/codex/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/clients/copilot/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/app.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/status.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/chat.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/info_panel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/console/widgets/sidebar.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/constants.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/credentials.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/formatting.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/learn_prompt.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/mesh_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/observer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/collector.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/model.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/renderer_text.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/renderer_web.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/overview/web/style.css +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/pidfile.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/history.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/icons.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/rooms.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/tags.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/extensions/threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/message.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/protocol-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/protocol/replies.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/culture/telemetry/context.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/architecture-overview.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/bots.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/otelcol-template.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/agentirc/why-agentirc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/agent-lifecycle.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/choose-a-harness.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/features.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/mental-model.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/operate.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/patterns.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/quickstart.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/reflective-development.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/vision-patterns-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/vision.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/culture/what-is-culture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/agent-harness-spec.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/layers.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/subsites.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/architecture/threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/afi.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/commands.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/devex.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/cli/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/console.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/acp.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/claude.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/codex.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/copilot.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/harnesses/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/architecture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/config.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/deployment.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/reference/server/security.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/resources/github-copilot-sdk-instructions.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/resources/positioning.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/harnesses.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/humans-and-agents.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/persistence.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/concepts/rooms.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/demos/magic-demo.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/first-session.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/join-as-human.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/local-setup.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/guides/multi-machine.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/01-pair-programming.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/02-code-review-ensemble.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/03-cross-server-delegation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/04-knowledge-propagation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/05-the-observer.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/06-cross-server-ops.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/07-supervisor-intervention.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/08-apps-as-agents.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/09-research-swarm.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases/10-agent-lifecycle.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/shared/use-cases-index.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-19-layer1-core-irc.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-21-layer5-agent-harness.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-30-overview.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-03-30-rooms-management.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-02-conversation-threads.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-02-ops-tooling.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-04-culture-rename.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-05-docs-speak-culture.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-06-console-chat.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-09-decentralized-agent-config.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-12-console-enhancements.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-15-mesh-events.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-18-culture-dev-positioning.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-22-agex-integration.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-24-otel-foundation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/plans/2026-04-25-otel-federation.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-19-agentirc-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-21-layer5-agent-harness-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-30-overview-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-03-30-rooms-management-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-02-conversation-threads-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-02-ops-tooling-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-03-bots-webhooks-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-04-culture-rename-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-05-docs-speak-culture-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-05-lifecycle-reframe-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-06-cli-reorganization-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-06-console-chat-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-07-entity-archiving-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-07-reflective-development-reframe-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-08-reflective-development-deepening-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-09-decentralized-agent-config-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-12-console-enhancements-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-15-mesh-events-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-17-sites-repositioning-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-18-culture-dev-positioning-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-22-agex-integration-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/docs/superpowers/specs/2026-04-24-otel-observability-design.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/favicon.ico +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/README.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/culture.yaml +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/skill/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/skill/irc_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/packages/agent-harness/webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/.claude-plugin/plugin.json +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/skills/culture/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/claude-code/skills/irc/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/plugins/codex/skills/culture-irc/SKILL.md +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/robots.txt +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap-agentirc.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap-main.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sitemap.html +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/sonar-project.properties +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/__init__.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/_fakes.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_config_load.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_context.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_dispatch_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_emit_event_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_federation_propagation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_outbound_inject.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_parse_error.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_privmsg_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_dispatch_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_relay_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_s2s_session_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_server_init.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_server_link_inject.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_session_span.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/telemetry/test_tracing.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_acp_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_agent_runner.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_archive.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_config_fires_event_toplevel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bot_manager.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_bots_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_channel.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_channel_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_afi.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_devex.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_introspect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_cli_passthrough.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_codex_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_connection.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_chat_markdown.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_commands.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_connection.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_fixes_224_227.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_icons.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_console_status.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_copilot_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_credentials.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_culture_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_daemon_ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_discovery.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_display.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_basic.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_bot_chain.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_bot_trigger.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_cap_fallback.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_lifecycle.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_filter_dsl.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_history.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_http_listener.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_integration_layer5.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_ipc.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_irc_transport.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_irc_transport_tags.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_learn_prompt.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_manifest_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_alias.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_target_cleanup.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mention_warning.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mentions.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mesh_config.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_mesh_readiness.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_message_tags.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_messaging.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_migrate_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_modes.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_collector.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_model.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_renderer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_overview_web.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_persistence_timeout.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_pidfile.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_poll_loop.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_register_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_setup_update_cli.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skill_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skill_docs.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_skills.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_socket_server.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_supervisor.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_template_engine.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_thread_buffer.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_threads.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_virtual_client.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_wait_for_port.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_webhook.py +0 -0
- {agentirc_cli-8.3.0 → agentirc_cli-8.5.0}/tests/test_welcome_bot.py +0 -0
|
@@ -4,6 +4,36 @@ 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
|
+
## [8.5.0] - 2026-04-25
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
|
|
11
|
+
- `culture/telemetry/audit.py` — `AuditSink` with bounded `asyncio.Queue` + dedicated writer task + daily/size rotation + `0600`/`0700` perms.
|
|
12
|
+
- Public `culture.telemetry.AuditSink`, `init_audit`, `build_audit_record`, `utc_iso_timestamp`.
|
|
13
|
+
- `TelemetryConfig.audit_enabled` (default `True`), `audit_dir`, `audit_max_file_bytes`, `audit_rotate_utc_midnight`, `audit_queue_depth` — independent of `telemetry.enabled` (audit fires even with OTEL off).
|
|
14
|
+
- `culture/protocol/extensions/audit.md` — JSONL record schema as a stable contract.
|
|
15
|
+
- `docs/agentirc/audit.md` — operator guide.
|
|
16
|
+
- Audit metrics extend the Plan-3 `MetricsRegistry`: `culture.audit.writes` (Counter, labels `outcome=ok|error`) and `culture.audit.queue_depth` (UpDownCounter).
|
|
17
|
+
- `IRCd.__init__` creates the sink; `IRCd.start()` awaits `sink.start()`; `IRCd.stop()` awaits `sink.shutdown()` so SERVER_WAKE / SERVER_SLEEP both land in the JSONL.
|
|
18
|
+
- `IRCd.emit_event` submits one record per event after the `irc.event.emit` span; `trace_id` / `span_id` captured inside the span for cross-pillar joins.
|
|
19
|
+
- `Client._process_buffer` submits `PARSE_ERROR` records for malformed inbound lines.
|
|
20
|
+
- Federation audit: federated `message` events arrive on the receiver with `origin=federated`, `peer=<peer_name>`. Federated lifecycle events (JOIN/PART/QUIT) are deferred — see #296.
|
|
21
|
+
|
|
22
|
+
## [8.4.0] - 2026-04-25
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
|
|
26
|
+
- `culture/telemetry/metrics.py`: `init_metrics(config)` + `MetricsRegistry` dataclass for all 15 server-side instruments — mirrors `tracing.py`'s idempotency + no-op pattern.
|
|
27
|
+
- Public `culture.telemetry.MetricsRegistry` and `culture.telemetry.init_metrics`.
|
|
28
|
+
- `TelemetryConfig.metrics_enabled` (default `True`) and `metrics_export_interval_ms` (default 10000).
|
|
29
|
+
- Message-flow metrics: `culture.irc.bytes_sent`, `culture.irc.bytes_received`, `culture.irc.message.size`, `culture.privmsg.delivered`.
|
|
30
|
+
- Events metrics: `culture.events.emitted`, `culture.events.render.duration`.
|
|
31
|
+
- Federation metrics: `culture.s2s.messages` (inbound), `culture.s2s.relay_latency`, `culture.s2s.links_active`, `culture.s2s.link_events`.
|
|
32
|
+
- Client metrics: `culture.clients.connected`, `culture.client.session.duration`, `culture.client.command.duration`.
|
|
33
|
+
- `culture.trace.inbound` counter — closes Plan 2's deferral.
|
|
34
|
+
- `tests/conftest.py` `metrics_reader` fixture parallel to `tracing_exporter`.
|
|
35
|
+
- `tests/telemetry/_metrics_helpers.py` — `get_counter_value`, `get_histogram_count`, `get_up_down_value`.
|
|
36
|
+
|
|
7
37
|
## [8.3.0] - 2026-04-25
|
|
8
38
|
|
|
9
39
|
### Added
|
|
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|
|
4
4
|
import asyncio
|
|
5
5
|
import logging
|
|
6
6
|
import re
|
|
7
|
+
import time
|
|
7
8
|
from typing import TYPE_CHECKING
|
|
8
9
|
|
|
9
10
|
from opentelemetry import trace as _otel_trace
|
|
@@ -16,6 +17,7 @@ from culture.aio import maybe_await
|
|
|
16
17
|
from culture.constants import SYSTEM_USER_PREFIX
|
|
17
18
|
from culture.protocol import replies
|
|
18
19
|
from culture.protocol.message import Message
|
|
20
|
+
from culture.telemetry.audit import utc_iso_timestamp as _utc_iso_timestamp
|
|
19
21
|
from culture.telemetry.context import TRACEPARENT_TAG as _TP_TAG_NAME
|
|
20
22
|
from culture.telemetry.context import (
|
|
21
23
|
context_from_traceparent,
|
|
@@ -77,8 +79,12 @@ class Client:
|
|
|
77
79
|
if tp is not None:
|
|
78
80
|
_inject_traceparent(message, traceparent=tp, tracestate=None)
|
|
79
81
|
try:
|
|
80
|
-
|
|
82
|
+
wire = message.format().encode("utf-8")
|
|
83
|
+
self.writer.write(wire)
|
|
81
84
|
await self.writer.drain()
|
|
85
|
+
# Record bytes after a successful drain so we don't count
|
|
86
|
+
# writes that immediately faulted.
|
|
87
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
|
|
82
88
|
except OSError:
|
|
83
89
|
pass # Client disconnected; cleanup happens in ircd._handle_connection
|
|
84
90
|
|
|
@@ -96,8 +102,10 @@ class Client:
|
|
|
96
102
|
# block; prefix a fresh @tag.
|
|
97
103
|
line = f"@{_TP_TAG_NAME}={tp} {line}"
|
|
98
104
|
try:
|
|
99
|
-
|
|
105
|
+
wire = f"{line}\r\n".encode("utf-8")
|
|
106
|
+
self.writer.write(wire)
|
|
100
107
|
await self.writer.drain()
|
|
108
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2c"})
|
|
101
109
|
except OSError:
|
|
102
110
|
pass # Client disconnected; cleanup happens in ircd._handle_connection
|
|
103
111
|
|
|
@@ -141,14 +149,68 @@ class Client:
|
|
|
141
149
|
"error": type(exc).__name__,
|
|
142
150
|
},
|
|
143
151
|
)
|
|
152
|
+
self._submit_parse_error_audit(line, exc)
|
|
144
153
|
continue
|
|
154
|
+
# Record received bytes + message size for every successfully-parsed
|
|
155
|
+
# line. +2 accounts for the \r\n that was stripped during line-split.
|
|
156
|
+
line_bytes = len(line.encode("utf-8")) + 2
|
|
157
|
+
self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "c2s"})
|
|
158
|
+
self.server.metrics.irc_message_size.record(
|
|
159
|
+
line_bytes, {"verb": msg.command, "direction": "c2s"}
|
|
160
|
+
)
|
|
145
161
|
if msg.command:
|
|
146
162
|
await self._dispatch(msg)
|
|
147
163
|
return buffer
|
|
148
164
|
|
|
165
|
+
def _submit_parse_error_audit(self, line: str, exc: BaseException) -> None:
|
|
166
|
+
"""Build and submit a PARSE_ERROR audit record for a malformed inbound line.
|
|
167
|
+
|
|
168
|
+
The record cannot go through build_audit_record (which expects an Event);
|
|
169
|
+
PARSE_ERROR is a synthetic event_type with no Event object behind it.
|
|
170
|
+
"""
|
|
171
|
+
# Capture trace/span ids from the active span (the
|
|
172
|
+
# `irc.client.process_buffer` we're inside of).
|
|
173
|
+
span = _otel_trace.get_current_span()
|
|
174
|
+
ctx = span.get_span_context()
|
|
175
|
+
trace_id_hex = format(ctx.trace_id, "032x") if ctx.is_valid else ""
|
|
176
|
+
span_id_hex = format(ctx.span_id, "016x") if ctx.is_valid else ""
|
|
177
|
+
|
|
178
|
+
peer_info = self.writer.get_extra_info("peername")
|
|
179
|
+
remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
|
|
180
|
+
|
|
181
|
+
tags: dict[str, str] = {}
|
|
182
|
+
tp = current_traceparent()
|
|
183
|
+
if tp:
|
|
184
|
+
tags["culture.dev/traceparent"] = tp
|
|
185
|
+
|
|
186
|
+
record = {
|
|
187
|
+
"ts": _utc_iso_timestamp(time.time()),
|
|
188
|
+
"server": self.server.config.name,
|
|
189
|
+
"event_type": "PARSE_ERROR",
|
|
190
|
+
"origin": "local",
|
|
191
|
+
"peer": "",
|
|
192
|
+
"trace_id": trace_id_hex,
|
|
193
|
+
"span_id": span_id_hex,
|
|
194
|
+
"actor": {
|
|
195
|
+
"nick": self.nick or "",
|
|
196
|
+
"kind": "human",
|
|
197
|
+
"remote_addr": remote_addr,
|
|
198
|
+
},
|
|
199
|
+
"target": {"kind": "", "name": ""},
|
|
200
|
+
"payload": {
|
|
201
|
+
"line_preview": line[:64],
|
|
202
|
+
"error": type(exc).__name__,
|
|
203
|
+
},
|
|
204
|
+
"tags": tags,
|
|
205
|
+
}
|
|
206
|
+
self.server.audit.submit(record)
|
|
207
|
+
|
|
149
208
|
async def handle(self, initial_msg: str | None = None) -> None:
|
|
150
209
|
peer_info = self.writer.get_extra_info("peername")
|
|
151
210
|
remote_addr = f"{peer_info[0]}:{peer_info[1]}" if peer_info else ""
|
|
211
|
+
kind = "human" # Plan 5/6 will refine to bot/harness
|
|
212
|
+
self.server.metrics.clients_connected.add(1, {"kind": kind})
|
|
213
|
+
session_started = time.perf_counter()
|
|
152
214
|
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
153
215
|
"irc.client.session",
|
|
154
216
|
attributes={"irc.client.remote_addr": remote_addr},
|
|
@@ -172,9 +234,15 @@ class Client:
|
|
|
172
234
|
buffer = await self._process_buffer(buffer)
|
|
173
235
|
except (ConnectionError, asyncio.IncompleteReadError):
|
|
174
236
|
pass
|
|
237
|
+
finally:
|
|
238
|
+
self.server.metrics.clients_connected.add(-1, {"kind": kind})
|
|
239
|
+
self.server.metrics.client_session_duration.record(
|
|
240
|
+
time.perf_counter() - session_started, {"kind": kind}
|
|
241
|
+
)
|
|
175
242
|
|
|
176
243
|
async def _dispatch(self, msg: Message) -> None:
|
|
177
244
|
extract = extract_traceparent_from_tags(msg, peer=None)
|
|
245
|
+
self.server.metrics.trace_inbound.add(1, {"result": extract.status, "peer": ""})
|
|
178
246
|
if extract.status == "valid":
|
|
179
247
|
parent_ctx: _OtelContext | None = context_from_traceparent(extract.traceparent)
|
|
180
248
|
else:
|
|
@@ -190,6 +258,7 @@ class Client:
|
|
|
190
258
|
attrs["culture.trace.dropped_reason"] = extract.status
|
|
191
259
|
|
|
192
260
|
# Per-call get_tracer: test fixture swaps provider between tests.
|
|
261
|
+
cmd_started = time.perf_counter()
|
|
193
262
|
with _otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
194
263
|
f"irc.command.{verb}",
|
|
195
264
|
context=parent_ctx,
|
|
@@ -211,6 +280,9 @@ class Client:
|
|
|
211
280
|
await self.send_numeric(
|
|
212
281
|
replies.ERR_UNKNOWNCOMMAND, msg.command, "Unknown command"
|
|
213
282
|
)
|
|
283
|
+
self.server.metrics.client_command_duration.record(
|
|
284
|
+
(time.perf_counter() - cmd_started) * 1000.0, {"verb": verb}
|
|
285
|
+
)
|
|
214
286
|
|
|
215
287
|
async def _handle_ping(self, msg: Message) -> None:
|
|
216
288
|
token = msg.params[0] if msg.params else ""
|
|
@@ -724,6 +796,7 @@ class Client:
|
|
|
724
796
|
for member in list(channel.members):
|
|
725
797
|
if member is not self:
|
|
726
798
|
await member.send(relay)
|
|
799
|
+
self.server.metrics.privmsg_delivered.add(1, {"kind": "channel", "channel": target})
|
|
727
800
|
event_data = {"text": text}
|
|
728
801
|
if is_notice:
|
|
729
802
|
event_data["notice"] = True
|
|
@@ -758,6 +831,7 @@ class Client:
|
|
|
758
831
|
)
|
|
759
832
|
else:
|
|
760
833
|
await recipient.send(relay)
|
|
834
|
+
self.server.metrics.privmsg_delivered.add(1, {"kind": "dm"})
|
|
761
835
|
event_data = {"text": text, "target": target}
|
|
762
836
|
if is_notice:
|
|
763
837
|
event_data["notice"] = True
|
|
@@ -24,6 +24,15 @@ class TelemetryConfig:
|
|
|
24
24
|
otlp_compression: str = "gzip" # gzip | none
|
|
25
25
|
traces_enabled: bool = True
|
|
26
26
|
traces_sampler: str = "parentbased_always_on"
|
|
27
|
+
metrics_enabled: bool = True
|
|
28
|
+
metrics_export_interval_ms: int = 10000
|
|
29
|
+
# Audit JSONL sink (Plan 4). Independent of `enabled` — audit fires
|
|
30
|
+
# even when telemetry is off so admins always have the trail.
|
|
31
|
+
audit_enabled: bool = True
|
|
32
|
+
audit_dir: str = "~/.culture/audit"
|
|
33
|
+
audit_max_file_bytes: int = 256 * 1024 * 1024 # 256 MiB
|
|
34
|
+
audit_rotate_utc_midnight: bool = True
|
|
35
|
+
audit_queue_depth: int = 10000
|
|
27
36
|
|
|
28
37
|
|
|
29
38
|
@dataclass
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import time
|
|
8
9
|
from collections import deque
|
|
9
10
|
from typing import TYPE_CHECKING
|
|
10
11
|
|
|
@@ -22,9 +23,14 @@ from culture.constants import (
|
|
|
22
23
|
SYSTEM_USER_PREFIX,
|
|
23
24
|
)
|
|
24
25
|
from culture.protocol.message import Message
|
|
26
|
+
from culture.telemetry import build_audit_record, current_traceparent
|
|
25
27
|
|
|
26
28
|
logger = logging.getLogger(__name__)
|
|
27
29
|
|
|
30
|
+
# Span/metric attribute keys defined once so a future rename has one edit point.
|
|
31
|
+
_ATTR_EVENT_TYPE = "event.type"
|
|
32
|
+
|
|
33
|
+
|
|
28
34
|
if TYPE_CHECKING:
|
|
29
35
|
from culture.agentirc.client import Client
|
|
30
36
|
from culture.agentirc.remote_client import RemoteClient
|
|
@@ -35,10 +41,12 @@ class IRCd:
|
|
|
35
41
|
"""The culture IRC server."""
|
|
36
42
|
|
|
37
43
|
def __init__(self, config: ServerConfig):
|
|
38
|
-
from culture.telemetry import init_telemetry
|
|
44
|
+
from culture.telemetry import init_audit, init_metrics, init_telemetry
|
|
39
45
|
|
|
40
46
|
self.config = config
|
|
41
47
|
self.tracer = init_telemetry(config)
|
|
48
|
+
self.metrics = init_metrics(config)
|
|
49
|
+
self.audit = init_audit(config, self.metrics)
|
|
42
50
|
self.clients: dict[str, Client | VirtualClient] = {} # nick -> Client
|
|
43
51
|
self.channels: dict[str, Channel] = {} # name -> Channel
|
|
44
52
|
self.skills: list[Skill] = []
|
|
@@ -69,6 +77,8 @@ class IRCd:
|
|
|
69
77
|
logger.info("Bootstrapping system identity...")
|
|
70
78
|
self._bootstrap_system_identity()
|
|
71
79
|
|
|
80
|
+
await self.audit.start()
|
|
81
|
+
|
|
72
82
|
await self.emit_event(
|
|
73
83
|
Event(
|
|
74
84
|
type=EventType.SERVER_WAKE,
|
|
@@ -178,7 +188,7 @@ class IRCd:
|
|
|
178
188
|
# server_link.py).
|
|
179
189
|
event_type_str = event.type.value if hasattr(event.type, "value") else str(event.type)
|
|
180
190
|
attrs: dict[str, str] = {
|
|
181
|
-
|
|
191
|
+
_ATTR_EVENT_TYPE: event_type_str,
|
|
182
192
|
"event.origin": "federated" if origin_tag else "local",
|
|
183
193
|
}
|
|
184
194
|
if event.channel:
|
|
@@ -212,20 +222,56 @@ class IRCd:
|
|
|
212
222
|
async def emit_event(self, event: Event) -> None:
|
|
213
223
|
origin_tag = event.data.get("_origin")
|
|
214
224
|
attrs = self._build_event_span_attrs(event, origin_tag)
|
|
225
|
+
event_type_str = attrs[_ATTR_EVENT_TYPE]
|
|
226
|
+
origin_str = "federated" if origin_tag else "local"
|
|
227
|
+
|
|
228
|
+
self.metrics.events_emitted.add(1, {_ATTR_EVENT_TYPE: event_type_str, "origin": origin_str})
|
|
229
|
+
render_started = time.perf_counter()
|
|
230
|
+
|
|
231
|
+
trace_id_hex = ""
|
|
232
|
+
span_id_hex = ""
|
|
233
|
+
tp: str | None = None
|
|
234
|
+
|
|
215
235
|
# Per-call get_tracer: the `tracing_exporter` test fixture swaps the
|
|
216
236
|
# global provider between tests; a cached Tracer would bind to the
|
|
217
237
|
# first test's provider and stop delivering to later ones.
|
|
218
238
|
with _otel_trace.get_tracer("culture.agentirc").start_as_current_span(
|
|
219
239
|
"irc.event.emit", attributes=attrs
|
|
220
|
-
):
|
|
240
|
+
) as span:
|
|
221
241
|
seq = self.next_seq()
|
|
222
242
|
self._event_log.append((seq, event))
|
|
243
|
+
ctx = span.get_span_context()
|
|
244
|
+
if ctx.is_valid:
|
|
245
|
+
trace_id_hex = format(ctx.trace_id, "032x")
|
|
246
|
+
span_id_hex = format(ctx.span_id, "016x")
|
|
247
|
+
# Capture traceparent inside the span so trace_flags reflect
|
|
248
|
+
# the actual sampling decision for this span, not a hardcoded -01.
|
|
249
|
+
tp = current_traceparent()
|
|
223
250
|
await self._run_skill_hooks(event)
|
|
224
251
|
if not origin_tag:
|
|
225
252
|
await self._relay_to_peers(event)
|
|
226
253
|
await self._dispatch_to_bots(event)
|
|
227
254
|
await self._surface_event_privmsg(event)
|
|
228
255
|
|
|
256
|
+
render_ms = (time.perf_counter() - render_started) * 1000.0
|
|
257
|
+
self.metrics.events_render_duration.record(render_ms, {_ATTR_EVENT_TYPE: event_type_str})
|
|
258
|
+
|
|
259
|
+
# Audit submit happens after the span exits so it doesn't sit inside
|
|
260
|
+
# the irc.event.emit span (would skew render duration). The trace_id/
|
|
261
|
+
# span_id captured inside the span point back at it for cross-pillar
|
|
262
|
+
# joins.
|
|
263
|
+
tags: dict[str, str] = {"culture.dev/traceparent": tp} if tp else {}
|
|
264
|
+
self.audit.submit(
|
|
265
|
+
build_audit_record(
|
|
266
|
+
server_name=self.config.name,
|
|
267
|
+
event=event,
|
|
268
|
+
origin_tag=origin_tag,
|
|
269
|
+
trace_id=trace_id_hex,
|
|
270
|
+
span_id=span_id_hex,
|
|
271
|
+
extra_tags=tags,
|
|
272
|
+
)
|
|
273
|
+
)
|
|
274
|
+
|
|
229
275
|
_NO_SURFACE_TYPES = NO_SURFACE_EVENT_TYPES
|
|
230
276
|
|
|
231
277
|
@staticmethod
|
|
@@ -381,6 +427,7 @@ class IRCd:
|
|
|
381
427
|
if self._server:
|
|
382
428
|
self._server.close()
|
|
383
429
|
await self._server.wait_closed()
|
|
430
|
+
await self.audit.shutdown()
|
|
384
431
|
finally:
|
|
385
432
|
self._stopped.set()
|
|
386
433
|
|
|
@@ -5,6 +5,7 @@ import asyncio
|
|
|
5
5
|
import base64
|
|
6
6
|
import json
|
|
7
7
|
import logging
|
|
8
|
+
import time
|
|
8
9
|
from typing import TYPE_CHECKING
|
|
9
10
|
|
|
10
11
|
from opentelemetry import trace as otel_trace
|
|
@@ -137,8 +138,11 @@ class ServerLink:
|
|
|
137
138
|
except Exception: # noqa: BLE001 - telemetry must never break the link
|
|
138
139
|
logger.debug("traceparent injection failed; sending untagged", exc_info=True)
|
|
139
140
|
try:
|
|
140
|
-
|
|
141
|
+
wire = f"{line}\r\n".encode("utf-8")
|
|
142
|
+
self.writer.write(wire)
|
|
141
143
|
await self.writer.drain()
|
|
144
|
+
if self.server is not None:
|
|
145
|
+
self.server.metrics.irc_bytes_sent.add(len(wire), {"direction": "s2s"})
|
|
142
146
|
except OSError:
|
|
143
147
|
pass
|
|
144
148
|
|
|
@@ -155,6 +159,13 @@ class ServerLink:
|
|
|
155
159
|
line, buffer = buffer.split("\n", 1)
|
|
156
160
|
if line.strip():
|
|
157
161
|
msg = Message.parse(line)
|
|
162
|
+
if self.server is not None:
|
|
163
|
+
# +2 accounts for the \r\n that was stripped during line-split.
|
|
164
|
+
line_bytes = len(line.encode("utf-8")) + 2
|
|
165
|
+
self.server.metrics.irc_bytes_received.add(line_bytes, {"direction": "s2s"})
|
|
166
|
+
self.server.metrics.irc_message_size.record(
|
|
167
|
+
line_bytes, {"verb": msg.command, "direction": "s2s"}
|
|
168
|
+
)
|
|
158
169
|
if msg.command:
|
|
159
170
|
await self._dispatch(msg)
|
|
160
171
|
return buffer
|
|
@@ -187,6 +198,14 @@ class ServerLink:
|
|
|
187
198
|
except (ConnectionError, asyncio.IncompleteReadError):
|
|
188
199
|
pass
|
|
189
200
|
finally:
|
|
201
|
+
if self._authenticated:
|
|
202
|
+
direction = "outbound" if self.initiator else "inbound"
|
|
203
|
+
self.server.metrics.s2s_links_active.add(
|
|
204
|
+
-1, {"peer": self.peer_name or "", "direction": direction}
|
|
205
|
+
)
|
|
206
|
+
self.server.metrics.s2s_link_events.add(
|
|
207
|
+
1, {"peer": self.peer_name or "", "event": "disconnect"}
|
|
208
|
+
)
|
|
190
209
|
await self.server._remove_link(self, squit=self._squit_received)
|
|
191
210
|
self.writer.close()
|
|
192
211
|
try:
|
|
@@ -203,11 +222,20 @@ class ServerLink:
|
|
|
203
222
|
handler = getattr(self, f"_handle_{msg.command.lower()}", None)
|
|
204
223
|
|
|
205
224
|
extracted = extract_traceparent_from_tags(msg, peer=self.peer_name)
|
|
225
|
+
self.server.metrics.trace_inbound.add(
|
|
226
|
+
1, {"result": extracted.status, "peer": self.peer_name or ""}
|
|
227
|
+
)
|
|
206
228
|
if extracted.status == "valid":
|
|
207
229
|
parent_ctx: _OtelContext | None = context_from_traceparent(extracted.traceparent)
|
|
208
230
|
else:
|
|
209
231
|
parent_ctx = _OtelContext() # force root: detach from session span
|
|
210
232
|
|
|
233
|
+
if self.server is not None:
|
|
234
|
+
self.server.metrics.s2s_messages.add(
|
|
235
|
+
1,
|
|
236
|
+
{"verb": verb, "direction": "inbound", "peer": self.peer_name or ""},
|
|
237
|
+
)
|
|
238
|
+
|
|
211
239
|
attrs = {
|
|
212
240
|
"irc.command": verb,
|
|
213
241
|
"culture.trace.origin": "remote",
|
|
@@ -266,17 +294,26 @@ class ServerLink:
|
|
|
266
294
|
"""
|
|
267
295
|
if self._peer_pass != self.password:
|
|
268
296
|
logger.warning("Bad password from peer %s", self.peer_name)
|
|
297
|
+
self.server.metrics.s2s_link_events.add(
|
|
298
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
299
|
+
)
|
|
269
300
|
await self.send_raw("ERROR :Bad password")
|
|
270
301
|
raise ConnectionError("Bad S2S password")
|
|
271
302
|
|
|
272
303
|
# Check for duplicate server name
|
|
273
304
|
if self.peer_name in self.server.links:
|
|
274
305
|
logger.warning("Duplicate server name %s", self.peer_name)
|
|
306
|
+
self.server.metrics.s2s_link_events.add(
|
|
307
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
308
|
+
)
|
|
275
309
|
await self.send_raw(f"ERROR :Server name {self.peer_name} already linked")
|
|
276
310
|
raise ConnectionError("Duplicate server name")
|
|
277
311
|
|
|
278
312
|
if self.peer_name == self.server.config.name:
|
|
279
313
|
logger.warning("Peer has same name as us: %s", self.peer_name)
|
|
314
|
+
self.server.metrics.s2s_link_events.add(
|
|
315
|
+
1, {"peer": self.peer_name or "", "event": "auth_fail"}
|
|
316
|
+
)
|
|
280
317
|
await self.send_raw("ERROR :Cannot link to self")
|
|
281
318
|
raise ConnectionError("Cannot link to self")
|
|
282
319
|
|
|
@@ -291,6 +328,11 @@ class ServerLink:
|
|
|
291
328
|
await self._validate_peer_credentials()
|
|
292
329
|
|
|
293
330
|
self._authenticated = True
|
|
331
|
+
direction = "outbound" if self.initiator else "inbound"
|
|
332
|
+
self.server.metrics.s2s_links_active.add(
|
|
333
|
+
1, {"peer": self.peer_name, "direction": direction}
|
|
334
|
+
)
|
|
335
|
+
self.server.metrics.s2s_link_events.add(1, {"peer": self.peer_name, "event": "connect"})
|
|
294
336
|
if self._session_span is not None:
|
|
295
337
|
self._session_span.set_attribute("s2s.peer", self.peer_name)
|
|
296
338
|
self.server.links[self.peer_name] = self
|
|
@@ -900,6 +942,10 @@ class ServerLink:
|
|
|
900
942
|
except ValueError:
|
|
901
943
|
return
|
|
902
944
|
|
|
945
|
+
self.server.metrics.s2s_link_events.add(
|
|
946
|
+
1, {"peer": self.peer_name or "", "event": "backfill_start"}
|
|
947
|
+
)
|
|
948
|
+
|
|
903
949
|
# Use the higher of: what peer claims, or what we know they acked
|
|
904
950
|
# (during real-time relay, peer saw everything up to our _seq at link drop)
|
|
905
951
|
acked = self.server._peer_acked_seq.get(self.peer_name, 0)
|
|
@@ -910,6 +956,9 @@ class ServerLink:
|
|
|
910
956
|
await self._replay_event(seq, event)
|
|
911
957
|
|
|
912
958
|
await self.send_raw(f":{self.server.config.name} BACKFILLEND {self.server._seq}")
|
|
959
|
+
self.server.metrics.s2s_link_events.add(
|
|
960
|
+
1, {"peer": self.peer_name or "", "event": "backfill_complete"}
|
|
961
|
+
)
|
|
913
962
|
|
|
914
963
|
def _handle_backfillend(self, msg: Message) -> None:
|
|
915
964
|
"""Peer finished backfilling."""
|
|
@@ -946,6 +995,8 @@ class ServerLink:
|
|
|
946
995
|
"event.type": event_type_str,
|
|
947
996
|
"s2s.peer": self.peer_name or "",
|
|
948
997
|
}
|
|
998
|
+
relay_started = time.perf_counter()
|
|
999
|
+
relayed = False
|
|
949
1000
|
# Single span name (no verb suffix): the wire verb is decided downstream
|
|
950
1001
|
# by _RELAY_DISPATCH or the SEVENT fallback, after this span opens.
|
|
951
1002
|
with otel_trace.get_tracer(_TRACER_NAME).start_as_current_span(
|
|
@@ -955,19 +1006,29 @@ class ServerLink:
|
|
|
955
1006
|
handler = self._RELAY_DISPATCH.get(event.type)
|
|
956
1007
|
if handler:
|
|
957
1008
|
await maybe_await(handler(self, event, origin))
|
|
958
|
-
return
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
1009
|
+
# v1: typed _relay_* handlers may early-return on internal trust checks
|
|
1010
|
+
# without signaling. We optimistically count typed dispatch as relayed.
|
|
1011
|
+
# Future: refactor _relay_* to return bool so latency only fires on
|
|
1012
|
+
# genuine sends.
|
|
1013
|
+
relayed = True
|
|
1014
|
+
else:
|
|
1015
|
+
# If no typed relay exists, fall back to generic SEVENT.
|
|
1016
|
+
# v1 assumes all peers support SEVENT; cap negotiation is deferred — see plan task 12.
|
|
1017
|
+
payload = self.server._build_event_payload(event)
|
|
1018
|
+
encoded = self.server._encode_event_data(payload, event_type_str)
|
|
1019
|
+
target = event.channel or "*"
|
|
1020
|
+
# Egress trust check: channel-scoped events respect should_relay; global events always relay
|
|
1021
|
+
if event.channel is None or self.should_relay(event.channel):
|
|
1022
|
+
seq = self.server._seq # current local seq; peer stores but doesn't re-sequence
|
|
1023
|
+
await self.send_raw(
|
|
1024
|
+
f":{origin} SEVENT {origin} {seq} {event_type_str} {target} :{encoded}"
|
|
1025
|
+
)
|
|
1026
|
+
relayed = True
|
|
1027
|
+
|
|
1028
|
+
if relayed:
|
|
1029
|
+
self.server.metrics.s2s_relay_latency.record(
|
|
1030
|
+
(time.perf_counter() - relay_started) * 1000.0,
|
|
1031
|
+
{"event.type": event_type_str, "peer": self.peer_name or ""},
|
|
971
1032
|
)
|
|
972
1033
|
|
|
973
1034
|
async def _relay_message(self, event: Event, origin: str) -> None:
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# Extension: Audit JSONL Sink
|
|
2
|
+
|
|
3
|
+
The audit log is a durable, file-based JSON-Lines (`.jsonl`) trail of every event the server
|
|
4
|
+
emits. It is **separate from OTEL traces / metrics / logs** — audit lands directly on local disk
|
|
5
|
+
and never depends on a running collector. Admin-only "who said what to whom, when, via what path."
|
|
6
|
+
|
|
7
|
+
## File Layout
|
|
8
|
+
|
|
9
|
+
- **Path:** `<audit_dir>/<server_name>-<YYYY-MM-DD>.jsonl` where `<audit_dir>` defaults to
|
|
10
|
+
`~/.culture/audit/` (configurable via `telemetry.audit_dir`). The date is **UTC**.
|
|
11
|
+
- **File mode:** `0600` (owner read/write only).
|
|
12
|
+
- **Directory mode:** `0700` (owner only). Created on demand if missing; existing dir mode is
|
|
13
|
+
left as-is.
|
|
14
|
+
- **Rotation suffix:** when the daily file hits the size cap, the next file gets `.1`, then
|
|
15
|
+
`.2`, … same date. New day starts a fresh file with no suffix.
|
|
16
|
+
|
|
17
|
+
Example for server `spark` on 2026-04-27 with two size-cap rotations:
|
|
18
|
+
|
|
19
|
+
```text
|
|
20
|
+
~/.culture/audit/spark-2026-04-27.jsonl # first 256 MiB
|
|
21
|
+
~/.culture/audit/spark-2026-04-27.1.jsonl # next 256 MiB
|
|
22
|
+
~/.culture/audit/spark-2026-04-27.2.jsonl # current
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Record Schema
|
|
26
|
+
|
|
27
|
+
Each line in the file is a single JSON object. Lines never wrap. Keys are lowercase with `_`
|
|
28
|
+
separators. Order is canonicalized at write time (stable across writes). Future schema additions
|
|
29
|
+
are additive only — consumers must tolerate unknown keys.
|
|
30
|
+
|
|
31
|
+
Keys within each record are alphabetically sorted (the writer uses
|
|
32
|
+
`json.dumps(..., sort_keys=True)`). Consumers SHOULD NOT rely on
|
|
33
|
+
insertion order; future producers may emit keys in any order.
|
|
34
|
+
|
|
35
|
+
| Field | Type | Required | Description |
|
|
36
|
+
|-------|------|----------|-------------|
|
|
37
|
+
| `ts` | string | yes | ISO 8601 UTC timestamp with microsecond precision and trailing `Z` (e.g. `2026-04-27T14:32:05.123456Z`). |
|
|
38
|
+
| `server` | string | yes | Server name from `ServerConfig.name`. |
|
|
39
|
+
| `event_type` | string | yes | `EventType.value` (e.g. `message`, `user.join`, `room.create`) or the special string `PARSE_ERROR` for malformed inbound lines. |
|
|
40
|
+
| `origin` | string | yes | `local` if the event originated on this server; `federated` if it arrived via a peer link. |
|
|
41
|
+
| `peer` | string | yes | Peer server name when `origin=federated`; empty string `""` otherwise. |
|
|
42
|
+
| `trace_id` | string | yes | OTEL trace-id (32 hex chars) of the active span at submit time, or `""` if no span. |
|
|
43
|
+
| `span_id` | string | yes | OTEL span-id (16 hex chars) of the active span, or `""`. |
|
|
44
|
+
| `actor` | object | yes | `{nick, kind, remote_addr}` describing who/what produced the event. |
|
|
45
|
+
| `actor.nick` | string | yes | The nick from the event (`event.nick`), or `""`. |
|
|
46
|
+
| `actor.kind` | string | yes | One of `human`, `bot`, `harness`. v1 always emits `human` — Plans 5/6 refine. |
|
|
47
|
+
| `actor.remote_addr` | string | yes | `"<ip>:<port>"` if known (PARSE_ERROR via Client; empty for server-internal sites). |
|
|
48
|
+
| `target` | object | yes | `{kind, name}` describing what the event affected. |
|
|
49
|
+
| `target.kind` | string | yes | `channel` (event.channel set), `nick` (DM target), or `""` for global events. |
|
|
50
|
+
| `target.name` | string | yes | The channel or nick; `""` for global. |
|
|
51
|
+
| `payload` | object | yes | `event.data` with all underscore-prefix keys (`_origin`, etc.) stripped. May include `nick` / `channel` defaulted from `event.nick`/`event.channel`. |
|
|
52
|
+
| `tags` | object | yes | IRCv3-style tag bag. v1 emits at most `culture.dev/traceparent` derived from the active span; empty `{}` if no span. |
|
|
53
|
+
|
|
54
|
+
## Rotation
|
|
55
|
+
|
|
56
|
+
Rotation fires when **either** condition is met, checked at the top of every record write:
|
|
57
|
+
|
|
58
|
+
1. The current UTC date differs from `current_date` (daily roll, controlled by
|
|
59
|
+
`telemetry.audit_rotate_utc_midnight`).
|
|
60
|
+
2. The current file size + the about-to-be-written record size exceeds
|
|
61
|
+
`telemetry.audit_max_file_bytes` (default 256 MiB).
|
|
62
|
+
|
|
63
|
+
The new file is opened with `O_WRONLY | O_APPEND | O_CREAT` mode `0600`. Writes use a single
|
|
64
|
+
`os.write` per record so partial-line interleaving is impossible.
|
|
65
|
+
|
|
66
|
+
A record larger than `audit_max_file_bytes` is still written — the cap is a soft
|
|
67
|
+
ceiling for accumulated bytes, not a hard reject. The oversized record lands in
|
|
68
|
+
its own freshly-rotated file, and the next record triggers another rotation.
|
|
69
|
+
|
|
70
|
+
## Durability
|
|
71
|
+
|
|
72
|
+
Records flow through a bounded `asyncio.Queue` (depth `telemetry.audit_queue_depth`, default
|
|
73
|
+
10000). A dedicated writer task drains the queue and writes each record. On queue overflow, the
|
|
74
|
+
record is **dropped** and `culture.audit.writes{outcome=error}` increments. A stderr warning is
|
|
75
|
+
logged.
|
|
76
|
+
|
|
77
|
+
This is a deliberate trade-off: dropping records is preferable to blocking `IRCd.emit_event`. A
|
|
78
|
+
real-world audit gap is rare and recoverable; a blocked event loop is catastrophic.
|
|
79
|
+
|
|
80
|
+
No `fsync` per record — writes hit the page cache and rely on the OS to flush. A hard crash can
|
|
81
|
+
lose the in-flight record.
|
|
82
|
+
|
|
83
|
+
## Retention
|
|
84
|
+
|
|
85
|
+
Files are not auto-pruned in v1. Operators prune manually:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
find ~/.culture/audit -name 'spark-*.jsonl*' -mtime +30 -delete
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
A future `audit-prune` CLI is TODO.
|
|
92
|
+
|
|
93
|
+
## Compat
|
|
94
|
+
|
|
95
|
+
The schema is a stable contract:
|
|
96
|
+
|
|
97
|
+
- New fields can be added in future versions; old consumers must tolerate unknown keys.
|
|
98
|
+
- Existing keys keep their type and semantics across versions.
|
|
99
|
+
- If a future version needs a breaking change, a top-level `schema_version` integer will be
|
|
100
|
+
added and bumped — until that exists, treat the schema as version 1.
|
|
101
|
+
|
|
102
|
+
## Example
|
|
103
|
+
|
|
104
|
+
PRIVMSG from `alpha-alice` (a federated client on the `alpha` peer) to channel `#general` on
|
|
105
|
+
the local server `spark`:
|
|
106
|
+
|
|
107
|
+
```json
|
|
108
|
+
{"ts":"2026-04-27T14:32:05.123456Z","server":"spark","event_type":"message","origin":"federated","peer":"alpha","trace_id":"4bf92f3577b34da6a3ce929d0e0e4736","span_id":"00f067aa0ba902b7","actor":{"nick":"alpha-alice","kind":"human","remote_addr":""},"target":{"kind":"channel","name":"#general"},"payload":{"text":"hi","nick":"alpha-alice","channel":"#general"},"tags":{"culture.dev/traceparent":"00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01"}}
|
|
109
|
+
```
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
|
|
12
12
|
1. Tag absent → start a new root span. Attribute: `culture.trace.origin=local`.
|
|
13
13
|
2. Tag present and valid → start a child span linked to the extracted context. Attributes: `culture.trace.origin=remote`, `culture.federation.peer=<peer>`.
|
|
14
|
-
3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}
|
|
14
|
+
3. Tag present but malformed or over the length cap → drop the tag, start a new root span. Attributes: `culture.trace.origin=remote`, `culture.trace.dropped_reason=malformed|too_long`, `culture.federation.peer=<peer>`. Log a rate-limited warning. Increment `culture.trace.inbound{result=malformed|too_long, peer=<peer>}` (shipped in culture 8.4.0; on every inbound regardless of result, so `result=valid` and `result=missing` are also recorded).
|
|
15
15
|
|
|
16
16
|
**Length caps** (hard-coded, not configurable):
|
|
17
17
|
|