alter-runtime 0.3.0__py3-none-any.whl
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.
- alter_runtime/__init__.py +11 -0
- alter_runtime/adapters/__init__.py +19 -0
- alter_runtime/adapters/claude_jsonl_watcher.py +545 -0
- alter_runtime/adapters/git_watcher.py +457 -0
- alter_runtime/adapters/household/__init__.py +29 -0
- alter_runtime/adapters/household/_base.py +138 -0
- alter_runtime/adapters/household/compost/__init__.py +17 -0
- alter_runtime/adapters/household/compost/adapter.py +81 -0
- alter_runtime/adapters/household/compost/storage.py +75 -0
- alter_runtime/adapters/household/compost/tests/__init__.py +0 -0
- alter_runtime/adapters/household/compost/tests/test_adapter.py +62 -0
- alter_runtime/adapters/household/compost/tests/test_storage.py +23 -0
- alter_runtime/adapters/household/compost/tests/test_traits.py +38 -0
- alter_runtime/adapters/household/compost/traits.py +79 -0
- alter_runtime/adapters/household/self_hoster/__init__.py +30 -0
- alter_runtime/adapters/household/self_hoster/adapter.py +248 -0
- alter_runtime/adapters/household/self_hoster/storage.py +83 -0
- alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
- alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +216 -0
- alter_runtime/adapters/household/self_hoster/tests/test_storage.py +25 -0
- alter_runtime/adapters/household/self_hoster/tests/test_traits.py +55 -0
- alter_runtime/adapters/household/self_hoster/traits.py +105 -0
- alter_runtime/adapters/household/tapo_ecosystem/__init__.py +22 -0
- alter_runtime/adapters/household/tapo_ecosystem/adapter.py +98 -0
- alter_runtime/adapters/household/tapo_ecosystem/storage.py +95 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +55 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +28 -0
- alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/tapo_ecosystem/traits.py +97 -0
- alter_runtime/adapters/household/workshop_tools/__init__.py +25 -0
- alter_runtime/adapters/household/workshop_tools/adapter.py +77 -0
- alter_runtime/adapters/household/workshop_tools/storage.py +92 -0
- alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +48 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +26 -0
- alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +45 -0
- alter_runtime/adapters/household/workshop_tools/traits.py +95 -0
- alter_runtime/adapters/worktree_watcher.py +378 -0
- alter_runtime/atlas/__init__.py +48 -0
- alter_runtime/atlas/base.py +102 -0
- alter_runtime/atlas/ledger.py +196 -0
- alter_runtime/atlas/observations.py +136 -0
- alter_runtime/atlas/schema.py +106 -0
- alter_runtime/cap_cache.py +392 -0
- alter_runtime/cli.py +517 -0
- alter_runtime/clients/__init__.py +0 -0
- alter_runtime/clients/token_usage_client.py +273 -0
- alter_runtime/config.py +648 -0
- alter_runtime/consent.py +425 -0
- alter_runtime/daemon.py +518 -0
- alter_runtime/floor_loop.py +335 -0
- alter_runtime/floor_preflight.py +734 -0
- alter_runtime/http_auth.py +173 -0
- alter_runtime/notifiers/__init__.py +18 -0
- alter_runtime/notifiers/desktop.py +321 -0
- alter_runtime/sdk/__init__.py +12 -0
- alter_runtime/sdk/client.py +399 -0
- alter_runtime/service_install.py +616 -0
- alter_runtime/services/__init__.py +59 -0
- alter_runtime/services/launchd/com.alter.runtime.plist.in +90 -0
- alter_runtime/services/systemd/alter-runtime.service.in +74 -0
- alter_runtime/services/systemd/cf-access-env.conf.in +29 -0
- alter_runtime/sockets/__init__.py +20 -0
- alter_runtime/sockets/dbus.py +272 -0
- alter_runtime/sockets/unix.py +702 -0
- alter_runtime/subscribers/__init__.py +58 -0
- alter_runtime/subscribers/active_sessions_cron_emitter.py +313 -0
- alter_runtime/subscribers/active_sessions_do_publisher.py +1159 -0
- alter_runtime/subscribers/active_sessions_gc.py +432 -0
- alter_runtime/subscribers/active_sessions_writer.py +446 -0
- alter_runtime/subscribers/adapters_writer.py +415 -0
- alter_runtime/subscribers/agent_frames.py +461 -0
- alter_runtime/subscribers/bus.py +188 -0
- alter_runtime/subscribers/cache_writer.py +347 -0
- alter_runtime/subscribers/ceremony_echo.py +290 -0
- alter_runtime/subscribers/do_sse.py +864 -0
- alter_runtime/subscribers/ebpf.py +506 -0
- alter_runtime/subscribers/inbox_writer.py +469 -0
- alter_runtime/subscribers/mcp_fallback.py +391 -0
- alter_runtime/subscribers/presence_writer.py +426 -0
- alter_runtime/subscribers/session_presence.py +467 -0
- alter_runtime/subscribers/sse.py +125 -0
- alter_runtime/subscribers/weave_intent_writer.py +608 -0
- alter_runtime/update_loop.py +519 -0
- alter_runtime/weave/__init__.py +21 -0
- alter_runtime/weave/resolver.py +544 -0
- alter_runtime-0.3.0.dist-info/METADATA +289 -0
- alter_runtime-0.3.0.dist-info/RECORD +92 -0
- alter_runtime-0.3.0.dist-info/WHEEL +4 -0
- alter_runtime-0.3.0.dist-info/entry_points.txt +2 -0
- alter_runtime-0.3.0.dist-info/licenses/LICENSE +190 -0
alter_runtime/daemon.py
ADDED
|
@@ -0,0 +1,518 @@
|
|
|
1
|
+
"""Asyncio supervisor for the alter-runtime daemon.
|
|
2
|
+
|
|
3
|
+
This is the Wave 1 stream 1c skeleton: it wires the supervisor loop, signal
|
|
4
|
+
handling, and the configuration loader but leaves the actual subscribers
|
|
5
|
+
(L1 DO SSE client and MCP fallback), socket servers, and adapters as
|
|
6
|
+
Wave 2 expansions. Running ``alter-runtime daemon`` will cleanly start, log,
|
|
7
|
+
and shut down without needing those components.
|
|
8
|
+
|
|
9
|
+
Architecture:
|
|
10
|
+
|
|
11
|
+
┌──────────────────────────────────────┐
|
|
12
|
+
│ alter_runtime.daemon │
|
|
13
|
+
│ Supervisor (asyncio) │
|
|
14
|
+
└──────┬───────────────┬───────────────┘
|
|
15
|
+
│ │
|
|
16
|
+
┌───────────▼──┐ ┌────────▼──────────┐
|
|
17
|
+
│ subscribers │ │ sockets / dbus │
|
|
18
|
+
│ do_sse │ │ unix / dbus │
|
|
19
|
+
│ mcp_fallback│ │ │
|
|
20
|
+
└──────────────┘ └───────────────────┘
|
|
21
|
+
│ │
|
|
22
|
+
Event bus (in-process asyncio.Queue)
|
|
23
|
+
│
|
|
24
|
+
┌───────▼────────┐
|
|
25
|
+
│ adapters │
|
|
26
|
+
│ git_watcher │
|
|
27
|
+
│ cc_hook_bridge│
|
|
28
|
+
└────────────────┘
|
|
29
|
+
|
|
30
|
+
Each component registers with the supervisor via ``Supervisor.register(...)``.
|
|
31
|
+
The supervisor runs them as long-lived tasks and restarts them on failure
|
|
32
|
+
with exponential backoff. On SIGTERM/SIGINT, the supervisor cancels all
|
|
33
|
+
tasks and awaits clean shutdown.
|
|
34
|
+
|
|
35
|
+
D-RT1 (local sovereign daemon), D-RT2 (three transports in parallel),
|
|
36
|
+
D-RT9 (graceful fallback).
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
from __future__ import annotations
|
|
40
|
+
|
|
41
|
+
import asyncio
|
|
42
|
+
import logging
|
|
43
|
+
import signal
|
|
44
|
+
import sys
|
|
45
|
+
from abc import ABC, abstractmethod
|
|
46
|
+
from collections.abc import Awaitable, Callable
|
|
47
|
+
from dataclasses import dataclass
|
|
48
|
+
from typing import TYPE_CHECKING
|
|
49
|
+
|
|
50
|
+
from alter_runtime.config import DaemonConfig, ensure_directories, load_config, load_session
|
|
51
|
+
|
|
52
|
+
if TYPE_CHECKING:
|
|
53
|
+
from alter_runtime.config import Session
|
|
54
|
+
|
|
55
|
+
__all__ = ["Component", "Supervisor", "run_daemon"]
|
|
56
|
+
|
|
57
|
+
logger = logging.getLogger("alter_runtime.daemon")
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
# ---------------------------------------------------------------------------
|
|
61
|
+
# Component ABC - everything the supervisor runs implements this
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Component(ABC):
|
|
66
|
+
"""A long-running component managed by the supervisor.
|
|
67
|
+
|
|
68
|
+
Subclasses implement :meth:`run` as an async method that blocks until
|
|
69
|
+
the component should exit. The supervisor cancels the task on shutdown
|
|
70
|
+
and awaits cancellation propagation.
|
|
71
|
+
"""
|
|
72
|
+
|
|
73
|
+
#: Human-readable name used in logs
|
|
74
|
+
name: str = "component"
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
async def run(self) -> None:
|
|
78
|
+
"""Main event loop for the component."""
|
|
79
|
+
|
|
80
|
+
async def stop(self) -> None:
|
|
81
|
+
"""Optional graceful shutdown hook.
|
|
82
|
+
|
|
83
|
+
Called before task cancellation. Components can use this to drain
|
|
84
|
+
pending work (flush queues, close sockets) before being cancelled.
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
@dataclass
|
|
89
|
+
class _ManagedTask:
|
|
90
|
+
"""Bookkeeping for a registered component."""
|
|
91
|
+
|
|
92
|
+
component: Component
|
|
93
|
+
task: asyncio.Task[None] | None = None
|
|
94
|
+
restart_count: int = 0
|
|
95
|
+
restart_at: float = 0.0
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
# ---------------------------------------------------------------------------
|
|
99
|
+
# Supervisor
|
|
100
|
+
# ---------------------------------------------------------------------------
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class Supervisor:
|
|
104
|
+
"""Supervises a set of Components with restart-on-failure semantics."""
|
|
105
|
+
|
|
106
|
+
#: Base backoff in seconds; exponential to a maximum
|
|
107
|
+
BASE_BACKOFF_SECONDS: float = 1.0
|
|
108
|
+
MAX_BACKOFF_SECONDS: float = 60.0
|
|
109
|
+
|
|
110
|
+
def __init__(self, config: DaemonConfig) -> None:
|
|
111
|
+
self.config = config
|
|
112
|
+
self._managed: list[_ManagedTask] = []
|
|
113
|
+
self._stopping = asyncio.Event()
|
|
114
|
+
self._on_stop_callbacks: list[Callable[[], Awaitable[None]]] = []
|
|
115
|
+
|
|
116
|
+
def register(self, component: Component) -> None:
|
|
117
|
+
"""Register a component to be managed.
|
|
118
|
+
|
|
119
|
+
May only be called before :meth:`run` starts. After startup, use
|
|
120
|
+
``add_component`` for dynamic registration.
|
|
121
|
+
"""
|
|
122
|
+
self._managed.append(_ManagedTask(component=component))
|
|
123
|
+
|
|
124
|
+
def on_stop(self, callback: Callable[[], Awaitable[None]]) -> None:
|
|
125
|
+
"""Register a callback to run during shutdown.
|
|
126
|
+
|
|
127
|
+
Callbacks run sequentially after component tasks are cancelled.
|
|
128
|
+
"""
|
|
129
|
+
self._on_stop_callbacks.append(callback)
|
|
130
|
+
|
|
131
|
+
async def run(self) -> None:
|
|
132
|
+
"""Start the supervisor and block until shutdown is requested."""
|
|
133
|
+
logger.info(
|
|
134
|
+
"alter-runtime daemon starting pid=%d components=%d",
|
|
135
|
+
_getpid(),
|
|
136
|
+
len(self._managed),
|
|
137
|
+
)
|
|
138
|
+
|
|
139
|
+
if not self._managed:
|
|
140
|
+
# Wave 1 skeleton: no components wired yet. We still start the
|
|
141
|
+
# supervisor so that operators can verify `alter-runtime daemon`
|
|
142
|
+
# works end-to-end - it just idles until signalled.
|
|
143
|
+
logger.warning(
|
|
144
|
+
"supervisor started with zero registered components "
|
|
145
|
+
"(Wave 1 skeleton - subscribers, sockets, adapters come in Wave 2)"
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
# Install signal handlers for clean shutdown
|
|
149
|
+
self._install_signal_handlers()
|
|
150
|
+
|
|
151
|
+
# Launch each component
|
|
152
|
+
for managed in self._managed:
|
|
153
|
+
managed.task = asyncio.create_task(
|
|
154
|
+
self._supervise(managed),
|
|
155
|
+
name=f"alter-{managed.component.name}",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
# Block until stop is requested
|
|
159
|
+
await self._stopping.wait()
|
|
160
|
+
|
|
161
|
+
logger.info("alter-runtime daemon stopping")
|
|
162
|
+
|
|
163
|
+
# Stop phase - give each component a chance to flush
|
|
164
|
+
for managed in self._managed:
|
|
165
|
+
try:
|
|
166
|
+
await asyncio.wait_for(managed.component.stop(), timeout=5.0)
|
|
167
|
+
except (TimeoutError, Exception) as exc:
|
|
168
|
+
logger.warning("component %s stop() raised: %s", managed.component.name, exc)
|
|
169
|
+
|
|
170
|
+
# Cancel tasks
|
|
171
|
+
for managed in self._managed:
|
|
172
|
+
if managed.task and not managed.task.done():
|
|
173
|
+
managed.task.cancel()
|
|
174
|
+
|
|
175
|
+
# Await cancellation
|
|
176
|
+
for managed in self._managed:
|
|
177
|
+
if managed.task:
|
|
178
|
+
try:
|
|
179
|
+
await managed.task
|
|
180
|
+
except asyncio.CancelledError:
|
|
181
|
+
pass
|
|
182
|
+
except Exception as exc:
|
|
183
|
+
logger.warning(
|
|
184
|
+
"component %s exited with error: %s",
|
|
185
|
+
managed.component.name,
|
|
186
|
+
exc,
|
|
187
|
+
)
|
|
188
|
+
|
|
189
|
+
# Run stop callbacks
|
|
190
|
+
for callback in self._on_stop_callbacks:
|
|
191
|
+
try:
|
|
192
|
+
await callback()
|
|
193
|
+
except Exception as exc:
|
|
194
|
+
logger.warning("stop callback raised: %s", exc)
|
|
195
|
+
|
|
196
|
+
logger.info("alter-runtime daemon stopped")
|
|
197
|
+
|
|
198
|
+
async def stop(self) -> None:
|
|
199
|
+
"""Request graceful shutdown."""
|
|
200
|
+
self._stopping.set()
|
|
201
|
+
|
|
202
|
+
async def _supervise(self, managed: _ManagedTask) -> None:
|
|
203
|
+
"""Run a component and restart it with backoff on failure."""
|
|
204
|
+
name = managed.component.name
|
|
205
|
+
while not self._stopping.is_set():
|
|
206
|
+
try:
|
|
207
|
+
logger.info("starting component %s", name)
|
|
208
|
+
await managed.component.run()
|
|
209
|
+
logger.info("component %s exited cleanly", name)
|
|
210
|
+
return
|
|
211
|
+
except asyncio.CancelledError:
|
|
212
|
+
raise
|
|
213
|
+
except Exception as exc:
|
|
214
|
+
managed.restart_count += 1
|
|
215
|
+
backoff = min(
|
|
216
|
+
self.BASE_BACKOFF_SECONDS * (2 ** (managed.restart_count - 1)),
|
|
217
|
+
self.MAX_BACKOFF_SECONDS,
|
|
218
|
+
)
|
|
219
|
+
logger.warning(
|
|
220
|
+
"component %s crashed (attempt %d): %s - restarting in %.1fs",
|
|
221
|
+
name,
|
|
222
|
+
managed.restart_count,
|
|
223
|
+
exc,
|
|
224
|
+
backoff,
|
|
225
|
+
)
|
|
226
|
+
try:
|
|
227
|
+
await asyncio.wait_for(
|
|
228
|
+
self._stopping.wait(),
|
|
229
|
+
timeout=backoff,
|
|
230
|
+
)
|
|
231
|
+
return # stop requested during backoff
|
|
232
|
+
except (TimeoutError, asyncio.TimeoutError):
|
|
233
|
+
continue
|
|
234
|
+
|
|
235
|
+
def _install_signal_handlers(self) -> None:
|
|
236
|
+
loop = asyncio.get_running_loop()
|
|
237
|
+
# Windows doesn't support add_signal_handler; use signal.signal as a fallback
|
|
238
|
+
if sys.platform == "win32":
|
|
239
|
+
signal.signal(signal.SIGINT, lambda *_: loop.call_soon_threadsafe(self._stopping.set))
|
|
240
|
+
return
|
|
241
|
+
for sig in (signal.SIGTERM, signal.SIGINT):
|
|
242
|
+
try:
|
|
243
|
+
loop.add_signal_handler(sig, self._stopping.set)
|
|
244
|
+
except NotImplementedError:
|
|
245
|
+
# Fallback for exotic event loops
|
|
246
|
+
signal.signal(sig, lambda *_: self._stopping.set())
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
# ---------------------------------------------------------------------------
|
|
250
|
+
# Top-level entrypoint used by the CLI `alter-runtime daemon`
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
async def run_daemon(config: DaemonConfig | None = None) -> None:
|
|
255
|
+
"""Start the supervisor and block until shutdown.
|
|
256
|
+
|
|
257
|
+
Called by ``alter_runtime.cli.cmd_daemon``. Wave 1 registers no
|
|
258
|
+
components; Wave 2 will wire subscribers, sockets, and adapters here.
|
|
259
|
+
"""
|
|
260
|
+
if config is None:
|
|
261
|
+
config = load_config()
|
|
262
|
+
|
|
263
|
+
_configure_logging(config.log_level)
|
|
264
|
+
ensure_directories()
|
|
265
|
+
|
|
266
|
+
session: Session | None = load_session()
|
|
267
|
+
if session is None:
|
|
268
|
+
logger.warning(
|
|
269
|
+
"no alter-cli session found at %s - daemon starting in degraded mode. "
|
|
270
|
+
"Run `alter login` to enable identity-aware subscribers.",
|
|
271
|
+
"~/.config/alter/session.json",
|
|
272
|
+
)
|
|
273
|
+
else:
|
|
274
|
+
logger.info(
|
|
275
|
+
"session loaded handle=%s consent_tier=L%d api=%s",
|
|
276
|
+
session.handle,
|
|
277
|
+
session.consent_tier,
|
|
278
|
+
session.api,
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
supervisor = Supervisor(config)
|
|
282
|
+
|
|
283
|
+
# Tighten the umask before any subscriber writes the inbox / state files
|
|
284
|
+
# so that O_CREAT | 0o600 always lands at the requested mode regardless
|
|
285
|
+
# of the inherited shell umask (§6.5 of Alter-to-Alter Messaging).
|
|
286
|
+
try:
|
|
287
|
+
import os as _os
|
|
288
|
+
|
|
289
|
+
_os.umask(0o077)
|
|
290
|
+
except Exception: # pragma: no cover - exotic environments
|
|
291
|
+
pass
|
|
292
|
+
|
|
293
|
+
# D-AUTOUPDATE-1 Phase 1 — daemon-side update observer.
|
|
294
|
+
#
|
|
295
|
+
# Polls the release substrate on cadence and logs what it sees. No
|
|
296
|
+
# download, no verify, no apply this phase. Runs regardless of session
|
|
297
|
+
# state because the manifest is a public read surface — the loop never
|
|
298
|
+
# touches an authenticated endpoint.
|
|
299
|
+
if config.autoupdate_enabled:
|
|
300
|
+
from alter_runtime.update_loop import UpdateLoop
|
|
301
|
+
|
|
302
|
+
supervisor.register(
|
|
303
|
+
UpdateLoop(
|
|
304
|
+
manifest_url=config.autoupdate_manifest_url,
|
|
305
|
+
poll_interval_seconds=config.autoupdate_poll_interval_seconds,
|
|
306
|
+
)
|
|
307
|
+
)
|
|
308
|
+
|
|
309
|
+
# D-MIN-VERSION-FLOOR-1 Phase 3 — daemon-side floor preflight + gate.
|
|
310
|
+
#
|
|
311
|
+
# Sister loop to UpdateLoop: polls the server-side floor on the same
|
|
312
|
+
# 24h cadence, verifies the HMAC signature, and writes a shared
|
|
313
|
+
# FloorState. The Unix-socket server reads the state on every method
|
|
314
|
+
# dispatch and refuses to serve when below floor — emitting the
|
|
315
|
+
# canonical ``client_below_floor`` envelope (byte-shape-equal to the
|
|
316
|
+
# Phase 1 server middleware reject body). The A15.3 carve-out lets
|
|
317
|
+
# ``urgency: critical`` ingest payloads through even when locked.
|
|
318
|
+
#
|
|
319
|
+
# The floor endpoint is a public read; the loop runs regardless of
|
|
320
|
+
# session state.
|
|
321
|
+
from alter_runtime.floor_loop import FloorLoop, FloorState
|
|
322
|
+
|
|
323
|
+
floor_state = FloorState()
|
|
324
|
+
supervisor.register(FloorLoop(config, floor_state))
|
|
325
|
+
|
|
326
|
+
# Construct the in-process event bus that couples subscribers, sockets,
|
|
327
|
+
# and adapters. The bus is a transient coupling layer - durable state
|
|
328
|
+
# lives in the DO (replayable via Last-Event-ID) and the backend audit DB.
|
|
329
|
+
from alter_runtime.subscribers import (
|
|
330
|
+
ActiveSessionsCronEmitter,
|
|
331
|
+
ActiveSessionsDoPublisher,
|
|
332
|
+
ActiveSessionsGc,
|
|
333
|
+
ActiveSessionsWriter,
|
|
334
|
+
AdaptersWriter,
|
|
335
|
+
AgentFrameSubscriber,
|
|
336
|
+
CacheWriter,
|
|
337
|
+
CeremonyEchoWriter,
|
|
338
|
+
DoSseSubscriber,
|
|
339
|
+
EventBus,
|
|
340
|
+
InboxWriter,
|
|
341
|
+
McpFallbackSubscriber,
|
|
342
|
+
PresenceWriter,
|
|
343
|
+
SessionPresenceWriter,
|
|
344
|
+
)
|
|
345
|
+
|
|
346
|
+
event_bus = EventBus()
|
|
347
|
+
|
|
348
|
+
# Wave 2: register the full ingress/egress stack when we have an
|
|
349
|
+
# authenticated session. Without a session there's no handle to
|
|
350
|
+
# subscribe against, so the daemon idles in degraded mode until
|
|
351
|
+
# `alter login` populates session.json.
|
|
352
|
+
if session is not None:
|
|
353
|
+
# --- Ingress (L1) -------------------------------------------------
|
|
354
|
+
# Primary: DO SSE. Fallback: direct MCP polling per D-RT9. Both
|
|
355
|
+
# publish to ``identity.frame`` / ``identity.event`` - the fan-out
|
|
356
|
+
# layer below consumes from the bus so it never learns which path
|
|
357
|
+
# served a given event.
|
|
358
|
+
supervisor.register(DoSseSubscriber(config, session, event_bus))
|
|
359
|
+
supervisor.register(McpFallbackSubscriber(config, session, event_bus))
|
|
360
|
+
|
|
361
|
+
# InboxWriter was Wave 1's projection component; in Wave 2 we
|
|
362
|
+
# drive its ``handle_raw_frame`` entrypoint from the bus rather
|
|
363
|
+
# than having it open its own socket. This preserves the existing
|
|
364
|
+
# unit tests (which call handle_event directly) while hooking it
|
|
365
|
+
# into the real network path.
|
|
366
|
+
inbox = InboxWriter(config, session)
|
|
367
|
+
supervisor.register(inbox)
|
|
368
|
+
event_bus.subscribe("identity.frame", inbox.handle_raw_frame)
|
|
369
|
+
|
|
370
|
+
# CeremonyEchoWriter sits next to InboxWriter on the same bus.
|
|
371
|
+
# It filters the same frame stream for x-alter-recognition and
|
|
372
|
+
# x-alter-ceremony content_types and persists a tiny "echo
|
|
373
|
+
# state" file with a 72 h expiry. Shell-greeting consumers
|
|
374
|
+
# (alter-cli `alter room`, future menu greeting) read that
|
|
375
|
+
# file and render the echo iff still within the window.
|
|
376
|
+
# Per proposed-D-CUST-1 surface 21 (alter-internal #140).
|
|
377
|
+
ceremony = CeremonyEchoWriter(config, session)
|
|
378
|
+
supervisor.register(ceremony)
|
|
379
|
+
event_bus.subscribe("identity.frame", ceremony.handle_raw_frame)
|
|
380
|
+
|
|
381
|
+
# --- Local fan-out surfaces (L3 transports) -----------------------
|
|
382
|
+
# Import lazily so the top of the module stays cheap and so that
|
|
383
|
+
# missing optional deps (dbus-next) only fail on Linux installs
|
|
384
|
+
# that asked for them.
|
|
385
|
+
from alter_runtime.sockets import UnixSocketServer
|
|
386
|
+
from alter_runtime.sockets.dbus import DBusService
|
|
387
|
+
|
|
388
|
+
# CacheWriter projects identity state into the shared shell cache
|
|
389
|
+
# file (``$XDG_CACHE_HOME/alter/identity.json``) that
|
|
390
|
+
# ``scripts/alter-identity.sh`` and the CC ``alter-identity.sh``
|
|
391
|
+
# hook already read from. This is the W2.2d *first pixel* glue -
|
|
392
|
+
# when the daemon is running, existing shell tools transparently
|
|
393
|
+
# start showing live identity state without any shell changes.
|
|
394
|
+
supervisor.register(CacheWriter(event_bus))
|
|
395
|
+
|
|
396
|
+
# SessionPresenceWriter polls the org-alter /queries/presence
|
|
397
|
+
# projection on a cadence and writes ~/.local/share/org-alter/state/
|
|
398
|
+
# sessions.json. The alter monorepo's bash awareness hook merges
|
|
399
|
+
# that cache with same-host /dev/shm sibling files so every CC
|
|
400
|
+
# session sees parallel sessions across hosts. Cap-minted with the
|
|
401
|
+
# alter_org.read scope; idle when ``session_presence_enabled`` is
|
|
402
|
+
# false. No bus coupling - file-mediated handoff to bash hooks.
|
|
403
|
+
supervisor.register(SessionPresenceWriter(config, session))
|
|
404
|
+
|
|
405
|
+
# Identity Presence consolidation (Wave 2) - bus-driven JSONL writers
|
|
406
|
+
# under ``~/.local/share/alter-runtime/``. Schemas locked at
|
|
407
|
+
# ``docs/schemas/{presence,active-sessions,adapters}.schema.json``.
|
|
408
|
+
supervisor.register(PresenceWriter(config, event_bus))
|
|
409
|
+
supervisor.register(ActiveSessionsWriter(config, event_bus))
|
|
410
|
+
# D-COORD-D2 Wave B - periodic GC pass over the active-sessions
|
|
411
|
+
# JSONL. Reads under LOCK_SH, emits session_heartbeat (idle) /
|
|
412
|
+
# session_ended (complete) envelopes via the shared bus so the
|
|
413
|
+
# writer's dedup + rotation + 0o600 invariants are preserved.
|
|
414
|
+
# PID-liveness probe gated on tool==cc.
|
|
415
|
+
if config.active_sessions_gc_enabled:
|
|
416
|
+
supervisor.register(ActiveSessionsGc(config, event_bus))
|
|
417
|
+
supervisor.register(AdaptersWriter(config, event_bus))
|
|
418
|
+
|
|
419
|
+
# D-COORD-D2 Wave C - DO publisher. Tails the active-sessions
|
|
420
|
+
# JSONL written by ActiveSessionsWriter (above) and POSTs each new
|
|
421
|
+
# envelope to ``{do_publish_url}/events/{handle}/sessions/ingest``.
|
|
422
|
+
# The Worker side of this contract is the parallel `/events/<handle>/
|
|
423
|
+
# sessions/ingest` route; this side just publishes, the DO is
|
|
424
|
+
# responsible for filtering `session_ended` out of live SSE.
|
|
425
|
+
supervisor.register(ActiveSessionsDoPublisher(config, session))
|
|
426
|
+
|
|
427
|
+
# D-COORD-D2 Wave D - CronCreate emitter. Subscribes to
|
|
428
|
+
# ``cron.routine.started`` / ``cron.routine.finished`` on the
|
|
429
|
+
# in-process bus and re-publishes each as a ``tool: cron``
|
|
430
|
+
# envelope on ``identity.event``. The writer above projects
|
|
431
|
+
# those envelopes into the active-sessions JSONL exactly like
|
|
432
|
+
# any other tool, so daemon-scheduled routines participate in
|
|
433
|
+
# the cross-host substrate without per-routine code.
|
|
434
|
+
supervisor.register(ActiveSessionsCronEmitter(config, event_bus, session))
|
|
435
|
+
|
|
436
|
+
# D-AGENT-CHANNEL-1 Phase 2 Wave 6 — agent_frame subscriber.
|
|
437
|
+
# Filters DO SSE for ``content_type=x-alter-agent``, projects to
|
|
438
|
+
# ``~/.cache/alter/agent-frames.jsonl``, and re-publishes per-kind
|
|
439
|
+
# bus topics (``alter.agent.frame.{kind}``). The subscriber also
|
|
440
|
+
# maintains an in-memory instrument roster which the UnixSocketServer
|
|
441
|
+
# serves via the ``agent/roster`` method without a DO round-trip.
|
|
442
|
+
agent_frames = AgentFrameSubscriber(event_bus)
|
|
443
|
+
supervisor.register(agent_frames)
|
|
444
|
+
|
|
445
|
+
supervisor.register(
|
|
446
|
+
UnixSocketServer(
|
|
447
|
+
config,
|
|
448
|
+
event_bus,
|
|
449
|
+
session,
|
|
450
|
+
agent_frames_subscriber=agent_frames,
|
|
451
|
+
floor_state=floor_state,
|
|
452
|
+
)
|
|
453
|
+
)
|
|
454
|
+
if config.enable_dbus:
|
|
455
|
+
supervisor.register(DBusService(config, event_bus, session))
|
|
456
|
+
|
|
457
|
+
# --- Ambient signal adapters --------------------------------------
|
|
458
|
+
from alter_runtime.adapters import GitWatcher
|
|
459
|
+
from alter_runtime.adapters.worktree_watcher import WorktreeWatcher
|
|
460
|
+
|
|
461
|
+
supervisor.register(GitWatcher(config, event_bus))
|
|
462
|
+
|
|
463
|
+
# D-WEAVE-VC-2 §8 item 1 (b) — working-tree fs-watch.
|
|
464
|
+
# WorktreeWatcher observes the repo working tree and publishes
|
|
465
|
+
# local.signal kind=worktree_edit on every file save (plain-editor
|
|
466
|
+
# tap — covers non-CC editors as a coarse second producer for weave).
|
|
467
|
+
supervisor.register(WorktreeWatcher(config, event_bus))
|
|
468
|
+
|
|
469
|
+
# D-WEAVE-VC-2 §8 items 1 + 2 — intent record writer.
|
|
470
|
+
# WeaveIntentWriter subscribes to local.signal worktree_edit events
|
|
471
|
+
# and tails weave-intent.jsonl for cc-intent hook records, writing
|
|
472
|
+
# enriched intent.* records with TTL + semantic_unit to
|
|
473
|
+
# ~/.local/share/alter-runtime/weave-intent.jsonl.
|
|
474
|
+
from alter_runtime.subscribers.weave_intent_writer import WeaveIntentWriter
|
|
475
|
+
|
|
476
|
+
supervisor.register(WeaveIntentWriter(config, event_bus))
|
|
477
|
+
|
|
478
|
+
# Claude Code JSONL token-usage watcher - opt-in via config flag.
|
|
479
|
+
# Tails ~/.claude/projects/**/*.jsonl and posts token-usage events to
|
|
480
|
+
# the ALTER backend audit endpoint. Disabled by default; enable via
|
|
481
|
+
# ALTER_RUNTIME_CLAUDE_JSONL_WATCHER=1 or direct config mutation.
|
|
482
|
+
if config.enable_claude_jsonl_watcher:
|
|
483
|
+
from alter_runtime.adapters.claude_jsonl_watcher import ClaudeJsonlWatcher
|
|
484
|
+
|
|
485
|
+
supervisor.register(ClaudeJsonlWatcher(config, event_bus))
|
|
486
|
+
|
|
487
|
+
# --- Kernel attestation (Wave 5b, Patent M) -----------------------
|
|
488
|
+
# The eBPF subscriber spawns the Rust ``alter-ebpf`` loader and
|
|
489
|
+
# republishes its exec stream onto ``kernel.attest.exec`` and
|
|
490
|
+
# ``local.signal``. It silently disables itself on hosts where the
|
|
491
|
+
# binary is missing or BPF LSM is unavailable, so it's safe to
|
|
492
|
+
# always register on Linux. Other platforms skip the import - the
|
|
493
|
+
# subscriber is Linux-only by construction.
|
|
494
|
+
if sys.platform.startswith("linux"):
|
|
495
|
+
from alter_runtime.subscribers import EbpfSubscriber
|
|
496
|
+
|
|
497
|
+
supervisor.register(EbpfSubscriber(config, event_bus))
|
|
498
|
+
|
|
499
|
+
await supervisor.run()
|
|
500
|
+
|
|
501
|
+
|
|
502
|
+
def _configure_logging(level: str) -> None:
|
|
503
|
+
"""Structured-ish logging for the daemon. Matches backend structlog intent
|
|
504
|
+
but keeps the dependency surface small (stdlib logging only)."""
|
|
505
|
+
logging.basicConfig(
|
|
506
|
+
level=level.upper(),
|
|
507
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
508
|
+
datefmt="%Y-%m-%dT%H:%M:%S%z",
|
|
509
|
+
)
|
|
510
|
+
|
|
511
|
+
|
|
512
|
+
def _getpid() -> int:
|
|
513
|
+
try:
|
|
514
|
+
import os
|
|
515
|
+
|
|
516
|
+
return os.getpid()
|
|
517
|
+
except Exception:
|
|
518
|
+
return -1
|