feat(cli): register decnet canary subcommand + tests
decnet canary launches the HTTP + DNS callback receiver via decnet.canary.worker.run. Mirrors the shape of decnet webhook (typer command with --daemon flag, asyncio.run in the foreground). Deliberately NOT added to MASTER_ONLY_COMMANDS — every host that hosts deckies runs its own canary worker, and the bus events stay local to that host (per-host webhook fanout handles SIEM egress).
This commit is contained in:
@@ -22,6 +22,7 @@ from . import (
|
|||||||
agent,
|
agent,
|
||||||
api,
|
api,
|
||||||
bus,
|
bus,
|
||||||
|
canary,
|
||||||
db,
|
db,
|
||||||
deploy,
|
deploy,
|
||||||
emailgen,
|
emailgen,
|
||||||
@@ -58,7 +59,7 @@ for _mod in (
|
|||||||
swarm,
|
swarm,
|
||||||
deploy, lifecycle, workers, inventory,
|
deploy, lifecycle, workers, inventory,
|
||||||
web, profiler, orchestrator, emailgen, reconciler, sniffer, db,
|
web, profiler, orchestrator, emailgen, reconciler, sniffer, db,
|
||||||
topology, bus, geoip, init, webhook,
|
topology, bus, geoip, init, webhook, canary,
|
||||||
):
|
):
|
||||||
_mod.register(app)
|
_mod.register(app)
|
||||||
|
|
||||||
|
|||||||
42
decnet/cli/canary.py
Normal file
42
decnet/cli/canary.py
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
"""``decnet canary`` — HTTP + DNS callback receiver for canary tokens.
|
||||||
|
|
||||||
|
Worker process. Mirrors the shape of :mod:`decnet.cli.webhook`: a
|
||||||
|
``@app.command(name="canary")`` Typer entry point that delegates to
|
||||||
|
:func:`decnet.canary.worker.run`.
|
||||||
|
|
||||||
|
Not master-only — any host that hosts deckies can run its own
|
||||||
|
canary worker (the bus events stay local; the webhook worker on
|
||||||
|
each host fans them out to SIEMs independently per the design
|
||||||
|
in ``development/let-s-move-to-the-enumerated-pike.md``).
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import typer
|
||||||
|
|
||||||
|
from . import utils as _utils
|
||||||
|
from .utils import console, log
|
||||||
|
|
||||||
|
|
||||||
|
def register(app: typer.Typer) -> None:
|
||||||
|
@app.command(name="canary")
|
||||||
|
def canary_cmd(
|
||||||
|
daemon: bool = typer.Option(
|
||||||
|
False, "--daemon", "-d", help="Detach to background as a daemon process",
|
||||||
|
),
|
||||||
|
) -> None:
|
||||||
|
"""Run the canary HTTP + DNS callback receiver."""
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from decnet.canary.worker import run
|
||||||
|
|
||||||
|
if daemon:
|
||||||
|
log.info("canary daemonizing")
|
||||||
|
_utils._daemonize()
|
||||||
|
|
||||||
|
log.info("canary starting")
|
||||||
|
console.print("[bold cyan]Canary callback receiver starting[/]")
|
||||||
|
|
||||||
|
try:
|
||||||
|
asyncio.run(run())
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
console.print("\n[yellow]Canary worker stopped.[/]")
|
||||||
24
tests/canary/test_cli.py
Normal file
24
tests/canary/test_cli.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
"""Smoke coverage for the ``decnet canary`` CLI subcommand.
|
||||||
|
|
||||||
|
We don't run the worker (it would block on HTTP/DNS sockets) — we
|
||||||
|
just confirm the command is registered and not master-gated, so an
|
||||||
|
agent host can run ``decnet canary`` without the gate hiding it.
|
||||||
|
"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typer.testing import CliRunner
|
||||||
|
|
||||||
|
from decnet.cli import app
|
||||||
|
from decnet.cli.gating import MASTER_ONLY_COMMANDS
|
||||||
|
|
||||||
|
|
||||||
|
def test_canary_command_registered() -> None:
|
||||||
|
runner = CliRunner()
|
||||||
|
result = runner.invoke(app, ["canary", "--help"])
|
||||||
|
assert result.exit_code == 0
|
||||||
|
assert "Run the canary HTTP + DNS callback receiver" in result.output
|
||||||
|
|
||||||
|
|
||||||
|
def test_canary_is_not_master_only() -> None:
|
||||||
|
# Agents must be able to run their own canary worker.
|
||||||
|
assert "canary" not in MASTER_ONLY_COMMANDS
|
||||||
Reference in New Issue
Block a user