agentirc-cli 9.3.0__tar.gz → 9.4.1__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.3.0 → agentirc_cli-9.4.1}/CHANGELOG.md +71 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/CLAUDE.md +14 -9
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/PKG-INFO +4 -3
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/cli.py +178 -71
- agentirc_cli-9.4.1/agentirc/config.py +126 -0
- agentirc_cli-9.4.1/docs/api-stability.md +232 -0
- agentirc_cli-9.4.1/docs/cli.md +267 -0
- agentirc_cli-9.4.1/docs/deployment.md +233 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/docs/superpowers/specs/2026-04-30-bootstrap-design.md +4 -2
- agentirc_cli-9.4.1/docs/superpowers/specs/2026-05-01-task14-audit.md +65 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/pyproject.toml +22 -21
- agentirc_cli-9.4.1/tests/test_config_loader.py +251 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/uv.lock +3 -1
- agentirc_cli-9.3.0/agentirc/config.py +0 -49
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/SKILL.md +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/portability-lint.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/pr-batch.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/pr-comments.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/pr-reply.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/pr-sonar.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/pr-status.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills/pr-review/scripts/workflow.sh +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.claude/skills.local.yaml.example +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.github/workflows/publish.yml +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.github/workflows/tests.yml +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/.gitignore +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/LICENSE +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/README.md +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/__main__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/aio.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/bots/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/bots/bot_manager.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/bots/http_listener.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/cli_shared/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/cli_shared/constants.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/cli_shared/mesh.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/constants.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/pidfile.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/protocol/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/protocol/message.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/protocol/replies.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/telemetry/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/telemetry/audit.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/telemetry/context.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/telemetry/metrics.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/telemetry/tracing.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/_internal/virtual_client.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/channel.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/client.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/events.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/history_store.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/ircd.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/protocol.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/remote_client.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/room_store.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/rooms_util.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/server_link.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skill.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skills/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skills/history.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skills/icon.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skills/rooms.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/skills/threads.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/agentirc/thread_store.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/docs/steward/onboarding.md +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/_helpers.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/conftest.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/__init__.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/_fakes.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/_metrics_helpers.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_audit_emit.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_audit_lifecycle.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_audit_module.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_audit_parse_error.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_config.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_dispatch_span.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_emit_event_span.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_metrics_init.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_metrics_s2s.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_outbound_inject.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_parse_error.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_s2s_relay_span.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_server_init.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_server_link_inject.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/telemetry/test_tracing.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_channel.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_cli.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_connection.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_discovery.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_basic.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_catalog.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_federation.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_history.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_lifecycle.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_events_reserved_nick.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_federation.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_history.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_link_reconnect.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_mentions.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_messaging.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_modes.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_room_persistence.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_rooms_federation.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_rooms_integration.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_server_icon_skill.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_skills.py +0 -0
- {agentirc_cli-9.3.0 → agentirc_cli-9.4.1}/tests/test_threads.py +0 -0
|
@@ -4,6 +4,77 @@ 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.4.1] - 2026-05-01
|
|
8
|
+
|
|
9
|
+
### Documentation
|
|
10
|
+
|
|
11
|
+
- Marked the bootstrap closed in `docs/superpowers/specs/2026-04-30-bootstrap-design.md` and `CLAUDE.md`. Tasks 16–18 (tag `v9.4.0`, verify PyPI publish, report-back to culture) are done; the spec status note now reads as a closed timeline ("Released ✅"), and `CLAUDE.md`'s "Current state" reads "bootstrap complete (9.4.0 released)" with a non-blocking follow-ups list linking to issues #7–#12 (Track A wire-format fixes, steward backport, callsite sweep, A2 test migration).
|
|
12
|
+
- Fixed 14 stale `OriNachum/*` GitHub URLs in `pyproject.toml` and the bootstrap spec to canonical `agentculture/*` paths. Investigation showed these weren't merely stylistic — `https://github.com/OriNachum/culture` returns 404 and `https://github.com/OriNachum/agentirc` 301-redirects to the wrong path. URL fixes only; sha256s are content-hashed and unaffected. `cite check` still passes.
|
|
13
|
+
|
|
14
|
+
### Notes
|
|
15
|
+
|
|
16
|
+
- Functionally identical to `9.4.0`. Published as a fresh PyPI release because `publish.yml` triggers on push-to-`main` and PyPI rejects re-publishing the same version with different sha256.
|
|
17
|
+
|
|
18
|
+
## [9.4.0] - 2026-05-01
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
|
|
22
|
+
- `ServerConfig.from_yaml(path)` — public classmethod on
|
|
23
|
+
`agentirc.config.ServerConfig` that loads `~/.culture/server.yaml`
|
|
24
|
+
(or any YAML file). Recognises `server`, `telemetry`, `links`,
|
|
25
|
+
`webhook_port`, `data_dir`, `system_bots` keys; silently ignores
|
|
26
|
+
culture-only top-level keys (`supervisor`, `agents`, `buffer_size`,
|
|
27
|
+
`poll_interval`, `sleep_start`, `sleep_end`, `webhooks`) so the same
|
|
28
|
+
file can drive both `culture server` and `agentirc` daemons.
|
|
29
|
+
- CLI handlers (`serve`, `start`, `restart`) now overlay CLI flags on
|
|
30
|
+
top of YAML config. Precedence: explicit CLI flag (sentinel-`None`
|
|
31
|
+
detection) > YAML key > built-in default. Closes the bootstrap
|
|
32
|
+
acceptance criterion *"`agentirc start --config <path>` behaves
|
|
33
|
+
indistinguishably from `culture server start`"*.
|
|
34
|
+
- New runtime dependency: `pyyaml>=6.0`.
|
|
35
|
+
- `docs/api-stability.md` — semver contract for the three public
|
|
36
|
+
modules (`agentirc.config`, `agentirc.cli`, `agentirc.protocol`),
|
|
37
|
+
full member list per module, internal-may-refactor list, wire-format
|
|
38
|
+
quirks paragraph, versioning history.
|
|
39
|
+
- `docs/cli.md` — verb table (8 verbs), per-verb flag reference, exit
|
|
40
|
+
codes, stderr formatting, YAML/CLI precedence, and the agentirc-vs-
|
|
41
|
+
culture differences table.
|
|
42
|
+
- `docs/deployment.md` — on-disk footprint, systemd `Type=simple`
|
|
43
|
+
example unit, container deployment, standalone deployment,
|
|
44
|
+
multi-host federation, log rotation (logrotate `copytruncate` rule),
|
|
45
|
+
coexistence with culture, backup recommendations.
|
|
46
|
+
|
|
47
|
+
### Changed
|
|
48
|
+
|
|
49
|
+
- `agentirc/cli.py:_add_start_flags` — argparse defaults for `--name`,
|
|
50
|
+
`--host`, `--port`, `--link`, `--webhook-port`, `--data-dir` are now
|
|
51
|
+
sentinel `None` rather than concrete values. The user-visible
|
|
52
|
+
defaults (`0.0.0.0`, `6667`, `7680`, `~/.culture/data`) are still
|
|
53
|
+
documented in help strings and live on the `ServerConfig` dataclass;
|
|
54
|
+
the change lets `_resolve_config` distinguish "user-supplied" from
|
|
55
|
+
"argparse-filled" when overlaying CLI on YAML.
|
|
56
|
+
|
|
57
|
+
### Removed
|
|
58
|
+
|
|
59
|
+
- `agentirc/cli.py:_maybe_warn_unused_config` — the
|
|
60
|
+
*"`--config` was supplied, but YAML config loading is not yet wired
|
|
61
|
+
(PR-B4)"* warning that PR #4 review (Qodo + Copilot) requested. YAML
|
|
62
|
+
loading is now wired; the warning was the placeholder for this PR.
|
|
63
|
+
|
|
64
|
+
### Notes
|
|
65
|
+
|
|
66
|
+
- Bootstrap is functionally + docs complete as of 9.4.0. Remaining
|
|
67
|
+
work per the spec status note: acceptance-criteria spot-check (Task
|
|
68
|
+
14) and release ceremony (Tasks 16–18 — tag, publish, report-back to
|
|
69
|
+
culture). The cross-repo wire-format fixes (Track A) and steward
|
|
70
|
+
backport of `pr-sonar.sh` are the only outstanding follow-ups.
|
|
71
|
+
- 13 new tests in `tests/test_config_loader.py` exercise
|
|
72
|
+
`ServerConfig.from_yaml` (missing/empty/malformed files, unknown-key
|
|
73
|
+
tolerance, telemetry/links sections) and the `_resolve_config` merge
|
|
74
|
+
(CLI overrides YAML, YAML used when CLI absent, defaults on both
|
|
75
|
+
absent, links wholesale-replace). Total suite: 328 tests, ~28s under
|
|
76
|
+
`pytest -n auto`.
|
|
77
|
+
|
|
7
78
|
## [9.3.0] - 2026-05-01
|
|
8
79
|
|
|
9
80
|
### Added
|
|
@@ -2,21 +2,26 @@
|
|
|
2
2
|
|
|
3
3
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
|
4
4
|
|
|
5
|
-
## Current state: bootstrap
|
|
5
|
+
## Current state: bootstrap complete (9.4.0 released)
|
|
6
6
|
|
|
7
|
-
This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/
|
|
7
|
+
This repo is the agentirc server-core extraction out of the sibling project [`culture`](https://github.com/agentculture/culture). As of 9.4.0 (tagged at `5590256` and live on PyPI):
|
|
8
8
|
|
|
9
9
|
- **Server-core** (`agentirc/{ircd,server_link,channel,events,skill,remote_client,…}.py`, `agentirc/skills/{rooms,threads,history,icon}.py`) — vendored from `culture@df50942` via the `cite-don't-copy` pattern (see `[tool.citation]` in `pyproject.toml`).
|
|
10
|
-
- **Client transport** (`agentirc/client.py`) — vendored from `culture/agentirc/client.py` in PR-B2.
|
|
11
|
-
- **Public CLI** (`agentirc/cli.py`) — real verb dispatch extracted from `culture/cli/server.py`. Verbs: `serve` (foreground, no PID; for systemd `Type=simple` and containers), `start`/`stop`/`status` (lifecycle), `restart`, `link` (peer-spec validator), `logs` (cat / tail of `~/.culture/logs/server-<name>.log`), `version`.
|
|
10
|
+
- **Client transport** (`agentirc/client.py`) — vendored from `culture/agentirc/client.py` in PR-B2.
|
|
11
|
+
- **Public CLI** (`agentirc/cli.py`) — real verb dispatch extracted from `culture/cli/server.py`. Verbs: `serve` (foreground, no PID; for systemd `Type=simple` and containers), `start`/`stop`/`status` (lifecycle), `restart`, `link` (peer-spec validator), `logs` (cat / tail of `~/.culture/logs/server-<name>.log`), `version`. Since 9.4.0, `serve`/`start`/`restart` overlay CLI flags on `--config` YAML (precedence: CLI > YAML > built-in default).
|
|
12
|
+
- **Public config** (`agentirc/config.py`) — `ServerConfig`, `LinkConfig`, `TelemetryConfig` dataclasses plus the `ServerConfig.from_yaml(path)` classmethod (added 9.4.0). Recognises `server`/`telemetry`/`links`/`webhook_port`/`data_dir`/`system_bots` keys; silently ignores culture-only keys (`supervisor`, `agents`, `buffer_size`, etc.) so the same `~/.culture/server.yaml` can drive both daemons.
|
|
12
13
|
- **Public protocol** (`agentirc/protocol.py`) — verb name constants, numerics, IRCv3 tag names. Wire-format quirks (`ROOMETAEND`, `ROOMETASET` typos, `ERR_NOSUCHCHANNEL` semantic misuse, `STHREAD` verb collapse) preserved verbatim — they need coordinated cross-repo bumps to fix.
|
|
13
|
-
- **Test suite** (PR-B3, 9.3.0) — 36 tests vendored from `culture@df50942` (~6.5kloc)
|
|
14
|
-
- **Internal support** (`agentirc/_internal/`) — `aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `pidfile
|
|
14
|
+
- **Test suite** (PR-B3, 9.3.0; +13 in 9.4.0) — 36 tests vendored from `culture@df50942` (~6.5kloc) plus 13 new agentirc-native tests in `tests/test_config_loader.py`. 328 tests run under `pytest -n auto` in ~28s on default workers. Three telemetry tests (`test_bot_event_dispatch_span`, `test_bot_run_span`, `test_metrics_bots`) and `test_welcome_bot` stay in culture because they depend on the real `BotManager`.
|
|
15
|
+
- **Internal support** (`agentirc/_internal/`) — `aio`, `constants`, `protocol/`, `telemetry/`, `virtual_client`, `pidfile`, `cli_shared/`, `bots/` stubs.
|
|
16
|
+
- **Bootstrap docs** (PR-B4, 9.4.0) — `docs/api-stability.md` (3 public modules + semver contract), `docs/cli.md` (verb table, flag reference, exit codes, YAML/CLI precedence, agentirc-vs-culture diff table), `docs/deployment.md` (on-disk footprint, systemd `Type=simple` example, container deployment, multi-host federation, log rotation, coexistence with culture, backup).
|
|
15
17
|
|
|
16
|
-
End-to-end verified: `agentirc start --port <p>` boots a real IRCd, TCP NICK/USER handshake returns `001 RPL_WELCOME`, `agentirc stop` shuts cleanly. `agentirc serve`
|
|
18
|
+
End-to-end verified: `agentirc start --port <p>` boots a real IRCd, TCP NICK/USER handshake returns `001 RPL_WELCOME`, `agentirc stop` shuts cleanly. `agentirc serve --config server.yaml --port 9999` correctly overlays CLI flag on YAML. `pip install agentirc-cli==9.4.0` from real PyPI in a clean venv produces both `agentirc` and `agentirc-cli` binaries; both reach the same `agentirc.cli:main` entry point. Acceptance audit recorded at [`docs/superpowers/specs/2026-05-01-task14-audit.md`](docs/superpowers/specs/2026-05-01-task14-audit.md). Culture-side cutover unblocked via [agentculture/culture#308](https://github.com/agentculture/culture/issues/308).
|
|
17
19
|
|
|
18
|
-
|
|
19
|
-
- **
|
|
20
|
+
**Outstanding follow-ups (non-blocking; the bootstrap itself is closed):**
|
|
21
|
+
- **Cross-repo wire-format fixes (Track A)** — [#7](https://github.com/agentculture/agentirc/issues/7) (`ROOMETAEND`/`ROOMETASET` typos), [#8](https://github.com/agentculture/agentirc/issues/8) (`ERR_NOSUCHCHANNEL` overload), [#9](https://github.com/agentculture/agentirc/issues/9) (`STHREAD` verb collapse). Each requires culture-side change first then agentirc bump.
|
|
22
|
+
- **Steward backport** — [#10](https://github.com/agentculture/agentirc/issues/10). Port the 9.3.0 `pr-sonar.sh` + `workflow.sh sonar` wiring upstream so other workspace projects pick it up via re-vendoring.
|
|
23
|
+
- **Optional callsite sweep** — [#11](https://github.com/agentculture/agentirc/issues/11). Replace inline IRC verb / numeric-reply string literals in `ircd.py`/`server_link.py`/`skills/*.py` with `agentirc.protocol.<NAME>` imports. Pure refactor.
|
|
24
|
+
- **A2 bot-fixtured tests (low-priority)** — [#12](https://github.com/agentculture/agentirc/issues/12). Currently in culture; could be migrated to agentirc via subprocess-fixture rewrite if culture's coverage drifts.
|
|
20
25
|
|
|
21
26
|
Read the bootstrap spec at `docs/superpowers/specs/2026-04-30-bootstrap-design.md` for the full plan; it is the operative source of truth and is intentionally self-contained. The culture-side counterpart spec is at `../culture/docs/superpowers/specs/2026-04-30-agentirc-extraction-design.md` — not normally needed, but explains *why* if a decision looks arbitrary.
|
|
22
27
|
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: agentirc-cli
|
|
3
|
-
Version: 9.
|
|
3
|
+
Version: 9.4.1
|
|
4
4
|
Summary: Agent-friendly IRCd: server core for AI agent meshes
|
|
5
|
-
Project-URL: Homepage, https://github.com/
|
|
6
|
-
Project-URL: Issues, https://github.com/
|
|
5
|
+
Project-URL: Homepage, https://github.com/agentculture/agentirc
|
|
6
|
+
Project-URL: Issues, https://github.com/agentculture/agentirc/issues
|
|
7
7
|
Author: Ori Nachum
|
|
8
8
|
License: MIT License
|
|
9
9
|
|
|
@@ -40,6 +40,7 @@ Requires-Python: >=3.11
|
|
|
40
40
|
Requires-Dist: opentelemetry-api>=1.22
|
|
41
41
|
Requires-Dist: opentelemetry-exporter-otlp-proto-grpc>=1.22
|
|
42
42
|
Requires-Dist: opentelemetry-sdk>=1.22
|
|
43
|
+
Requires-Dist: pyyaml>=6.0
|
|
43
44
|
Provides-Extra: dev
|
|
44
45
|
Requires-Dist: bandit; extra == 'dev'
|
|
45
46
|
Requires-Dist: black; extra == 'dev'
|
|
@@ -54,6 +54,8 @@ import time
|
|
|
54
54
|
from pathlib import Path
|
|
55
55
|
from typing import Sequence
|
|
56
56
|
|
|
57
|
+
import yaml
|
|
58
|
+
|
|
57
59
|
from agentirc import __version__
|
|
58
60
|
from agentirc._internal.cli_shared.constants import (
|
|
59
61
|
DEFAULT_CONFIG,
|
|
@@ -92,23 +94,116 @@ def _safe_log_name(name: str) -> str:
|
|
|
92
94
|
return _safe_name(name)
|
|
93
95
|
|
|
94
96
|
|
|
95
|
-
def
|
|
96
|
-
"""
|
|
97
|
+
def _load_raw_yaml(cfg_path: str) -> dict:
|
|
98
|
+
"""Read ``cfg_path`` as YAML and return the top-level mapping.
|
|
99
|
+
|
|
100
|
+
Missing files return ``{}``; malformed YAML raises ``yaml.YAMLError``
|
|
101
|
+
from the underlying loader. Empty files return ``{}``. A YAML
|
|
102
|
+
document whose root is a list/scalar (valid YAML but wrong shape)
|
|
103
|
+
raises ``yaml.YAMLError`` with a clear message rather than letting
|
|
104
|
+
a downstream ``AttributeError`` leak through.
|
|
105
|
+
"""
|
|
106
|
+
p = Path(cfg_path).expanduser()
|
|
107
|
+
if not p.exists():
|
|
108
|
+
return {}
|
|
109
|
+
with p.open() as f:
|
|
110
|
+
raw = yaml.safe_load(f)
|
|
111
|
+
if raw is None:
|
|
112
|
+
return {}
|
|
113
|
+
if not isinstance(raw, dict):
|
|
114
|
+
raise yaml.YAMLError(
|
|
115
|
+
f"agentirc config {cfg_path!r}: root must be a mapping, "
|
|
116
|
+
f"got {type(raw).__name__}"
|
|
117
|
+
)
|
|
118
|
+
return raw
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _build_telemetry(yaml_telemetry: dict) -> "TelemetryConfig": # noqa: F821
|
|
122
|
+
"""Build a TelemetryConfig from a YAML ``telemetry:`` block.
|
|
97
123
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
124
|
+
Drops keys not declared on the dataclass — culture extending
|
|
125
|
+
TelemetryConfig in the future shouldn't crash agentirc on read.
|
|
126
|
+
"""
|
|
127
|
+
from agentirc.config import TelemetryConfig
|
|
128
|
+
|
|
129
|
+
if not yaml_telemetry:
|
|
130
|
+
return TelemetryConfig()
|
|
131
|
+
known = {f.name for f in TelemetryConfig.__dataclass_fields__.values()}
|
|
132
|
+
tcfg = {k: v for k, v in yaml_telemetry.items() if k in known}
|
|
133
|
+
return TelemetryConfig(**tcfg)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _resolve_links(cli_links, yaml_links: list) -> list:
|
|
137
|
+
"""Pick CLI links if any, else build from YAML, else empty.
|
|
138
|
+
|
|
139
|
+
CLI links replace YAML wholesale — there is no merge.
|
|
140
|
+
"""
|
|
141
|
+
from agentirc.config import LinkConfig
|
|
142
|
+
|
|
143
|
+
if cli_links:
|
|
144
|
+
return list(cli_links)
|
|
145
|
+
if yaml_links:
|
|
146
|
+
return [LinkConfig(**entry) for entry in yaml_links]
|
|
147
|
+
return []
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def _resolve_config(args: argparse.Namespace) -> "ServerConfig": # noqa: F821 (forward ref)
|
|
151
|
+
"""Build a ServerConfig from ``--config`` YAML, overlaid with CLI flags.
|
|
152
|
+
|
|
153
|
+
Precedence (highest first): explicit CLI flag (``not None``) >
|
|
154
|
+
matching YAML key > built-in default. Loads raw YAML so we can
|
|
155
|
+
distinguish "key absent from YAML" from "key present but matching
|
|
156
|
+
the dataclass default" — needed to let downstream
|
|
157
|
+
``_resolve_server_name`` handle the "no name anywhere" case via
|
|
158
|
+
the default-server file rather than the ``ServerConfig`` dataclass
|
|
159
|
+
default ``"culture"``.
|
|
160
|
+
|
|
161
|
+
Mutates ``args`` in place so ``args.host`` / ``args.port`` /
|
|
162
|
+
``args.name`` / ``args.webhook_port`` / ``args.data_dir`` /
|
|
163
|
+
``args.link`` reflect the resolved values for the daemonize / log
|
|
164
|
+
/ status code that reads them directly. ``args.name`` is left
|
|
165
|
+
``None`` if neither CLI nor YAML supplied a name; the handler
|
|
166
|
+
then calls ``_resolve_server_name`` to pick up the default-server
|
|
167
|
+
file or the ``agentirc`` fallback.
|
|
103
168
|
"""
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
169
|
+
from agentirc.config import ServerConfig
|
|
170
|
+
|
|
171
|
+
raw = _load_raw_yaml(getattr(args, "config", None) or DEFAULT_CONFIG)
|
|
172
|
+
yaml_server = raw.get("server") or {}
|
|
173
|
+
|
|
174
|
+
def _pick(cli_value, yaml_value, default):
|
|
175
|
+
if cli_value is not None:
|
|
176
|
+
return cli_value
|
|
177
|
+
return yaml_value if yaml_value is not None else default
|
|
178
|
+
|
|
179
|
+
name = _pick(args.name, yaml_server.get("name"), None)
|
|
180
|
+
host = _pick(args.host, yaml_server.get("host"), "0.0.0.0")
|
|
181
|
+
port = _pick(args.port, yaml_server.get("port"), 6667)
|
|
182
|
+
webhook_port = _pick(args.webhook_port, raw.get("webhook_port"), 7680)
|
|
183
|
+
data_dir = os.path.expanduser(
|
|
184
|
+
_pick(
|
|
185
|
+
args.data_dir, raw.get("data_dir"), os.path.expanduser("~/.culture/data")
|
|
111
186
|
)
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
cfg = ServerConfig(
|
|
190
|
+
name=name or "agentirc",
|
|
191
|
+
host=host,
|
|
192
|
+
port=port,
|
|
193
|
+
webhook_port=webhook_port,
|
|
194
|
+
data_dir=data_dir,
|
|
195
|
+
links=_resolve_links(getattr(args, "link", None), raw.get("links") or []),
|
|
196
|
+
system_bots=raw.get("system_bots") or {},
|
|
197
|
+
telemetry=_build_telemetry(raw.get("telemetry") or {}),
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
args.name = name # may be None — handler resolves via default-server file
|
|
201
|
+
args.host = cfg.host
|
|
202
|
+
args.port = cfg.port
|
|
203
|
+
args.webhook_port = cfg.webhook_port
|
|
204
|
+
args.data_dir = cfg.data_dir
|
|
205
|
+
args.link = cfg.links
|
|
206
|
+
return cfg
|
|
112
207
|
|
|
113
208
|
|
|
114
209
|
# ---------------------------------------------------------------------------
|
|
@@ -117,21 +212,34 @@ def _maybe_warn_unused_config(args: argparse.Namespace) -> None:
|
|
|
117
212
|
|
|
118
213
|
|
|
119
214
|
def _add_start_flags(parser: argparse.ArgumentParser) -> None:
|
|
120
|
-
"""Attach the lifecycle flag set used by ``serve``/``start``/``restart``.
|
|
215
|
+
"""Attach the lifecycle flag set used by ``serve``/``start``/``restart``.
|
|
216
|
+
|
|
217
|
+
All flags default to ``None`` (the *sentinel* default) rather than
|
|
218
|
+
their concrete values. ``_resolve_config`` distinguishes "user
|
|
219
|
+
supplied this flag" from "argparse filled in the default" and
|
|
220
|
+
overlays CLI values on top of YAML; sentinel defaults are required
|
|
221
|
+
for that distinction. The user-visible defaults — ``0.0.0.0``,
|
|
222
|
+
``6667``, ``7680``, ``~/.culture/data`` — are documented in the
|
|
223
|
+
help strings and live on the ``ServerConfig`` dataclass.
|
|
224
|
+
"""
|
|
121
225
|
parser.add_argument("--name", default=None, help=_SERVER_NAME_HELP)
|
|
122
|
-
parser.add_argument(
|
|
123
|
-
|
|
226
|
+
parser.add_argument(
|
|
227
|
+
"--host", default=None, help="Listen address (default: 0.0.0.0)"
|
|
228
|
+
)
|
|
229
|
+
parser.add_argument(
|
|
230
|
+
"--port", type=int, default=None, help="Listen port (default: 6667)"
|
|
231
|
+
)
|
|
124
232
|
parser.add_argument(
|
|
125
233
|
"--link",
|
|
126
234
|
type=parse_link,
|
|
127
235
|
action="append",
|
|
128
|
-
default=
|
|
236
|
+
default=None,
|
|
129
237
|
help="Link to peer: name:host:port:password[:trust]",
|
|
130
238
|
)
|
|
131
239
|
parser.add_argument(
|
|
132
240
|
"--webhook-port",
|
|
133
241
|
type=int,
|
|
134
|
-
default=
|
|
242
|
+
default=None,
|
|
135
243
|
help=(
|
|
136
244
|
"HTTP port for bot webhooks (default: 7680; "
|
|
137
245
|
"inert in agentirc until a bot harness is wired in)"
|
|
@@ -139,7 +247,7 @@ def _add_start_flags(parser: argparse.ArgumentParser) -> None:
|
|
|
139
247
|
)
|
|
140
248
|
parser.add_argument(
|
|
141
249
|
"--data-dir",
|
|
142
|
-
default=
|
|
250
|
+
default=None,
|
|
143
251
|
help="Data directory for persistent storage (default: ~/.culture/data)",
|
|
144
252
|
)
|
|
145
253
|
parser.add_argument("--config", default=DEFAULT_CONFIG, help=_CONFIG_HELP)
|
|
@@ -309,29 +417,21 @@ def _force_kill(pid: int, name: str) -> None:
|
|
|
309
417
|
# ---------------------------------------------------------------------------
|
|
310
418
|
|
|
311
419
|
|
|
312
|
-
async def _run_server(
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
"""Run the IRC server (called in the daemon child process)."""
|
|
321
|
-
from agentirc.config import ServerConfig
|
|
420
|
+
async def _run_server(config: "ServerConfig") -> None: # noqa: F821 (forward ref)
|
|
421
|
+
"""Run the IRC server (called in the daemon child process).
|
|
422
|
+
|
|
423
|
+
Takes a fully-merged ``ServerConfig`` so YAML-supplied telemetry,
|
|
424
|
+
system_bots, and any future dataclass fields reach the IRCd.
|
|
425
|
+
Caller (``_run_foreground`` / ``_daemonize_server``) is responsible
|
|
426
|
+
for resolving CLI/YAML precedence via ``_resolve_config``.
|
|
427
|
+
"""
|
|
322
428
|
from agentirc.ircd import IRCd
|
|
323
429
|
|
|
324
|
-
config = ServerConfig(
|
|
325
|
-
name=name,
|
|
326
|
-
host=host,
|
|
327
|
-
port=port,
|
|
328
|
-
webhook_port=webhook_port,
|
|
329
|
-
links=links or [],
|
|
330
|
-
data_dir=data_dir,
|
|
331
|
-
)
|
|
332
430
|
ircd = IRCd(config)
|
|
333
431
|
await ircd.start()
|
|
334
|
-
logger.info(
|
|
432
|
+
logger.info(
|
|
433
|
+
"Server '%s' listening on %s:%d", config.name, config.host, config.port
|
|
434
|
+
)
|
|
335
435
|
|
|
336
436
|
for lc in config.links:
|
|
337
437
|
try:
|
|
@@ -356,41 +456,41 @@ async def _run_server(
|
|
|
356
456
|
signal.signal(sig, lambda *_: stop_event.set())
|
|
357
457
|
|
|
358
458
|
await stop_event.wait()
|
|
359
|
-
logger.info("Server '%s' shutting down", name)
|
|
459
|
+
logger.info("Server '%s' shutting down", config.name)
|
|
360
460
|
await ircd.stop()
|
|
361
461
|
|
|
362
462
|
|
|
363
|
-
def _run_foreground(
|
|
463
|
+
def _run_foreground(pid_name: str, cfg: "ServerConfig") -> None: # noqa: F821
|
|
364
464
|
"""Run the server in the foreground (blocking).
|
|
365
465
|
|
|
366
466
|
A PID file is written when *pid_name* is non-empty (``start
|
|
367
467
|
--foreground``). The agentirc-only ``serve`` verb passes ``""`` to
|
|
368
468
|
skip PID writes — useful for systemd ``Type=simple`` and containers
|
|
369
|
-
that own process supervision.
|
|
469
|
+
that own process supervision. All runtime values come from *cfg*;
|
|
470
|
+
``args`` is no longer needed since the verb-handler resolves the
|
|
471
|
+
YAML+CLI merge before calling here.
|
|
370
472
|
"""
|
|
371
473
|
if pid_name:
|
|
372
474
|
write_pid(pid_name, os.getpid())
|
|
373
|
-
if
|
|
475
|
+
if cfg.port:
|
|
374
476
|
# Mirror daemonize: write the port file so `agentirc status`
|
|
375
477
|
# can report `(PID N, port P)` for foreground-managed runs.
|
|
376
|
-
write_port(pid_name,
|
|
478
|
+
write_port(pid_name, cfg.port)
|
|
377
479
|
os.makedirs(LOG_DIR, exist_ok=True)
|
|
378
|
-
print(f"Server '{
|
|
379
|
-
print(f" Listening on {
|
|
380
|
-
print(f" Webhook port: {
|
|
480
|
+
print(f"Server '{cfg.name}' starting in foreground (PID {os.getpid()})")
|
|
481
|
+
print(f" Listening on {cfg.host}:{cfg.port}")
|
|
482
|
+
print(f" Webhook port: {cfg.webhook_port}")
|
|
381
483
|
if pid_name:
|
|
382
|
-
_maybe_set_default_server(
|
|
484
|
+
_maybe_set_default_server(cfg.name)
|
|
383
485
|
try:
|
|
384
|
-
asyncio.run(
|
|
385
|
-
_run_server(args.name, args.host, args.port, links, args.webhook_port, args.data_dir)
|
|
386
|
-
)
|
|
486
|
+
asyncio.run(_run_server(cfg))
|
|
387
487
|
finally:
|
|
388
488
|
if pid_name:
|
|
389
489
|
remove_pid(pid_name)
|
|
390
490
|
remove_port(pid_name)
|
|
391
491
|
|
|
392
492
|
|
|
393
|
-
def _daemonize_server(args: argparse.Namespace, pid_name: str,
|
|
493
|
+
def _daemonize_server(args: argparse.Namespace, pid_name: str, cfg: "ServerConfig") -> None: # noqa: F821
|
|
394
494
|
"""Fork and set up the daemon child process for the server."""
|
|
395
495
|
if sys.platform == "win32":
|
|
396
496
|
print("Daemon mode not supported on Windows. Use --foreground.", file=sys.stderr)
|
|
@@ -404,7 +504,7 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
|
|
|
404
504
|
os.setsid()
|
|
405
505
|
|
|
406
506
|
os.makedirs(LOG_DIR, exist_ok=True)
|
|
407
|
-
log_path = os.path.join(LOG_DIR, f"server-{_safe_log_name(
|
|
507
|
+
log_path = os.path.join(LOG_DIR, f"server-{_safe_log_name(cfg.name)}.log")
|
|
408
508
|
log_fd = os.open(log_path, os.O_WRONLY | os.O_CREAT | os.O_APPEND, 0o644)
|
|
409
509
|
os.dup2(log_fd, 1)
|
|
410
510
|
os.dup2(log_fd, 2)
|
|
@@ -426,8 +526,8 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
|
|
|
426
526
|
)
|
|
427
527
|
|
|
428
528
|
write_pid(pid_name, os.getpid())
|
|
429
|
-
if
|
|
430
|
-
write_port(pid_name,
|
|
529
|
+
if cfg.port:
|
|
530
|
+
write_port(pid_name, cfg.port)
|
|
431
531
|
|
|
432
532
|
# PR #4 review (Qodo): the previous version called os._exit(0)
|
|
433
533
|
# unconditionally in the finally block, masking crashes inside
|
|
@@ -436,16 +536,14 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
|
|
|
436
536
|
# and propagate it.
|
|
437
537
|
rc = 0
|
|
438
538
|
try:
|
|
439
|
-
asyncio.run(
|
|
440
|
-
_run_server(args.name, args.host, args.port, links, args.webhook_port, args.data_dir)
|
|
441
|
-
)
|
|
539
|
+
asyncio.run(_run_server(cfg))
|
|
442
540
|
except Exception:
|
|
443
541
|
# Catch ordinary failures so the daemon child can record them and
|
|
444
542
|
# exit non-zero. SystemExit / KeyboardInterrupt / GeneratorExit
|
|
445
543
|
# deliberately propagate (the asyncio signal handlers translate
|
|
446
544
|
# SIGINT/SIGTERM into a clean stop_event.set, so we never reach
|
|
447
545
|
# this except via an actual fault).
|
|
448
|
-
logger.exception("Daemon for server '%s' crashed",
|
|
546
|
+
logger.exception("Daemon for server '%s' crashed", cfg.name)
|
|
449
547
|
rc = 1
|
|
450
548
|
finally:
|
|
451
549
|
remove_pid(pid_name)
|
|
@@ -460,25 +558,26 @@ def _daemonize_server(args: argparse.Namespace, pid_name: str, links: list) -> N
|
|
|
460
558
|
|
|
461
559
|
def _server_serve(args: argparse.Namespace) -> None:
|
|
462
560
|
"""``agentirc serve`` — run the IRCd in the foreground without writing a PID file."""
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
561
|
+
cfg = _resolve_config(args)
|
|
562
|
+
if args.name is None:
|
|
563
|
+
args.name = _resolve_server_name(args)
|
|
564
|
+
cfg.name = args.name
|
|
565
|
+
_run_foreground(pid_name="", cfg=cfg)
|
|
467
566
|
|
|
468
567
|
|
|
469
568
|
def _server_start(args: argparse.Namespace) -> None:
|
|
470
569
|
"""``agentirc start`` — daemonize (or run foreground if ``--foreground``)."""
|
|
471
|
-
|
|
472
|
-
|
|
570
|
+
cfg = _resolve_config(args)
|
|
571
|
+
if args.name is None:
|
|
572
|
+
args.name = _resolve_server_name(args)
|
|
573
|
+
cfg.name = args.name
|
|
473
574
|
pid_name = f"server-{args.name}"
|
|
474
575
|
_check_already_running(pid_name, args.name)
|
|
475
576
|
|
|
476
|
-
links = list(getattr(args, "link", []) or [])
|
|
477
|
-
|
|
478
577
|
if getattr(args, "foreground", False):
|
|
479
|
-
_run_foreground(
|
|
578
|
+
_run_foreground(pid_name, cfg)
|
|
480
579
|
else:
|
|
481
|
-
_daemonize_server(args, pid_name,
|
|
580
|
+
_daemonize_server(args, pid_name, cfg)
|
|
482
581
|
|
|
483
582
|
|
|
484
583
|
def _server_stop(args: argparse.Namespace) -> int:
|
|
@@ -522,8 +621,16 @@ def _server_stop(args: argparse.Namespace) -> int:
|
|
|
522
621
|
|
|
523
622
|
|
|
524
623
|
def _server_restart(args: argparse.Namespace) -> int:
|
|
525
|
-
"""``agentirc restart`` — stop (best-effort) then start with the same args.
|
|
526
|
-
|
|
624
|
+
"""``agentirc restart`` — stop (best-effort) then start with the same args.
|
|
625
|
+
|
|
626
|
+
Resolves the YAML+CLI merge once up-front so ``server.name`` from
|
|
627
|
+
``--config`` is honoured for both the stop half and the subsequent
|
|
628
|
+
``_server_start`` call. Without the pre-resolution, the default-
|
|
629
|
+
server file would shadow ``server.name``.
|
|
630
|
+
"""
|
|
631
|
+
_resolve_config(args)
|
|
632
|
+
if args.name is None:
|
|
633
|
+
args.name = _resolve_server_name(args)
|
|
527
634
|
pid_name = f"server-{args.name}"
|
|
528
635
|
pid = read_pid(pid_name)
|
|
529
636
|
if pid and is_process_alive(pid):
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass, field
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Any
|
|
6
|
+
|
|
7
|
+
import yaml
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@dataclass
|
|
11
|
+
class LinkConfig:
|
|
12
|
+
"""Configuration for a server-to-server link."""
|
|
13
|
+
|
|
14
|
+
name: str
|
|
15
|
+
host: str
|
|
16
|
+
port: int
|
|
17
|
+
password: str
|
|
18
|
+
trust: str = "full" # "full" or "restricted"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass
|
|
22
|
+
class TelemetryConfig:
|
|
23
|
+
"""OpenTelemetry settings. Mirrors server.yaml `telemetry:` block."""
|
|
24
|
+
|
|
25
|
+
enabled: bool = False
|
|
26
|
+
service_name: str = "culture.agentirc"
|
|
27
|
+
otlp_endpoint: str = "http://localhost:4317"
|
|
28
|
+
otlp_protocol: str = "grpc" # grpc | http/protobuf (only grpc supported initially)
|
|
29
|
+
otlp_timeout_ms: int = 5000
|
|
30
|
+
otlp_compression: str = "gzip" # gzip | none
|
|
31
|
+
traces_enabled: bool = True
|
|
32
|
+
traces_sampler: str = "parentbased_always_on"
|
|
33
|
+
metrics_enabled: bool = True
|
|
34
|
+
metrics_export_interval_ms: int = 10000
|
|
35
|
+
# Audit JSONL sink (Plan 4). Independent of `enabled` — audit fires
|
|
36
|
+
# even when telemetry is off so admins always have the trail.
|
|
37
|
+
audit_enabled: bool = True
|
|
38
|
+
audit_dir: str = "~/.culture/audit"
|
|
39
|
+
audit_max_file_bytes: int = 256 * 1024 * 1024 # 256 MiB
|
|
40
|
+
audit_rotate_utc_midnight: bool = True
|
|
41
|
+
audit_queue_depth: int = 10000
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
@dataclass
|
|
45
|
+
class ServerConfig:
|
|
46
|
+
"""Configuration for a culture server instance."""
|
|
47
|
+
|
|
48
|
+
name: str = "culture"
|
|
49
|
+
host: str = "0.0.0.0"
|
|
50
|
+
port: int = 6667
|
|
51
|
+
webhook_port: int = 7680
|
|
52
|
+
data_dir: str = ""
|
|
53
|
+
links: list[LinkConfig] = field(default_factory=list)
|
|
54
|
+
system_bots: dict = field(default_factory=dict)
|
|
55
|
+
telemetry: TelemetryConfig = field(default_factory=TelemetryConfig)
|
|
56
|
+
|
|
57
|
+
@classmethod
|
|
58
|
+
def from_yaml(cls, path: str | Path) -> "ServerConfig":
|
|
59
|
+
"""Load a ServerConfig from a YAML file.
|
|
60
|
+
|
|
61
|
+
Recognises top-level ``server`` (host/port/name), ``telemetry``,
|
|
62
|
+
``links``, ``webhook_port``, ``data_dir``, and ``system_bots``.
|
|
63
|
+
Unknown top-level keys (``supervisor``, ``agents``, ``buffer_size``,
|
|
64
|
+
``poll_interval``, ``sleep_start``, ``sleep_end``) are silently
|
|
65
|
+
ignored — those belong to culture's broader process supervisor,
|
|
66
|
+
and agentirc must coexist with culture using the same
|
|
67
|
+
``~/.culture/server.yaml`` file. Unknown keys *inside* the
|
|
68
|
+
``server:`` block are also tolerated for the same reason
|
|
69
|
+
(culture's ``ServerConnConfig`` carries ``archived``,
|
|
70
|
+
``archived_at``, ``archived_reason`` that agentirc has no use
|
|
71
|
+
for).
|
|
72
|
+
|
|
73
|
+
A missing path returns the dataclass defaults rather than
|
|
74
|
+
raising — callers (CLI handlers) treat the file as optional.
|
|
75
|
+
Malformed YAML raises ``yaml.YAMLError`` from the underlying
|
|
76
|
+
loader; we deliberately do not catch it so users see the parse
|
|
77
|
+
error.
|
|
78
|
+
"""
|
|
79
|
+
p = Path(path).expanduser()
|
|
80
|
+
if not p.exists():
|
|
81
|
+
return cls()
|
|
82
|
+
raw = _load_root_mapping(p)
|
|
83
|
+
return cls(**_yaml_kwargs(raw))
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _load_root_mapping(p: Path) -> dict[str, Any]:
|
|
87
|
+
"""Load YAML at *p* and require the root to be a mapping."""
|
|
88
|
+
with p.open() as f:
|
|
89
|
+
loaded = yaml.safe_load(f)
|
|
90
|
+
if loaded is None:
|
|
91
|
+
return {}
|
|
92
|
+
if not isinstance(loaded, dict):
|
|
93
|
+
raise yaml.YAMLError(
|
|
94
|
+
f"agentirc config {str(p)!r}: root must be a mapping, "
|
|
95
|
+
f"got {type(loaded).__name__}"
|
|
96
|
+
)
|
|
97
|
+
return loaded
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def _yaml_kwargs(raw: dict[str, Any]) -> dict[str, Any]:
|
|
101
|
+
"""Project a raw YAML mapping onto ServerConfig constructor kwargs."""
|
|
102
|
+
server_section = raw.get("server") or {}
|
|
103
|
+
kwargs: dict[str, Any] = {}
|
|
104
|
+
for key in ("name", "host", "port"):
|
|
105
|
+
if key in server_section:
|
|
106
|
+
kwargs[key] = server_section[key]
|
|
107
|
+
for key in ("webhook_port", "data_dir"):
|
|
108
|
+
if key in raw:
|
|
109
|
+
kwargs[key] = raw[key]
|
|
110
|
+
links_section = raw.get("links") or []
|
|
111
|
+
if links_section:
|
|
112
|
+
kwargs["links"] = [LinkConfig(**entry) for entry in links_section]
|
|
113
|
+
telemetry_section = raw.get("telemetry") or {}
|
|
114
|
+
if telemetry_section:
|
|
115
|
+
kwargs["telemetry"] = _build_telemetry(telemetry_section)
|
|
116
|
+
system_bots = raw.get("system_bots") or {}
|
|
117
|
+
if system_bots:
|
|
118
|
+
kwargs["system_bots"] = system_bots
|
|
119
|
+
return kwargs
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _build_telemetry(yaml_telemetry: dict) -> TelemetryConfig:
|
|
123
|
+
"""Build a TelemetryConfig, dropping keys not on the dataclass."""
|
|
124
|
+
known = {f.name for f in TelemetryConfig.__dataclass_fields__.values()}
|
|
125
|
+
tcfg = {k: v for k, v in yaml_telemetry.items() if k in known}
|
|
126
|
+
return TelemetryConfig(**tcfg)
|