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.
Files changed (92) hide show
  1. alter_runtime/__init__.py +11 -0
  2. alter_runtime/adapters/__init__.py +19 -0
  3. alter_runtime/adapters/claude_jsonl_watcher.py +545 -0
  4. alter_runtime/adapters/git_watcher.py +457 -0
  5. alter_runtime/adapters/household/__init__.py +29 -0
  6. alter_runtime/adapters/household/_base.py +138 -0
  7. alter_runtime/adapters/household/compost/__init__.py +17 -0
  8. alter_runtime/adapters/household/compost/adapter.py +81 -0
  9. alter_runtime/adapters/household/compost/storage.py +75 -0
  10. alter_runtime/adapters/household/compost/tests/__init__.py +0 -0
  11. alter_runtime/adapters/household/compost/tests/test_adapter.py +62 -0
  12. alter_runtime/adapters/household/compost/tests/test_storage.py +23 -0
  13. alter_runtime/adapters/household/compost/tests/test_traits.py +38 -0
  14. alter_runtime/adapters/household/compost/traits.py +79 -0
  15. alter_runtime/adapters/household/self_hoster/__init__.py +30 -0
  16. alter_runtime/adapters/household/self_hoster/adapter.py +248 -0
  17. alter_runtime/adapters/household/self_hoster/storage.py +83 -0
  18. alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
  19. alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +216 -0
  20. alter_runtime/adapters/household/self_hoster/tests/test_storage.py +25 -0
  21. alter_runtime/adapters/household/self_hoster/tests/test_traits.py +55 -0
  22. alter_runtime/adapters/household/self_hoster/traits.py +105 -0
  23. alter_runtime/adapters/household/tapo_ecosystem/__init__.py +22 -0
  24. alter_runtime/adapters/household/tapo_ecosystem/adapter.py +98 -0
  25. alter_runtime/adapters/household/tapo_ecosystem/storage.py +95 -0
  26. alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
  27. alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +55 -0
  28. alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +28 -0
  29. alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +45 -0
  30. alter_runtime/adapters/household/tapo_ecosystem/traits.py +97 -0
  31. alter_runtime/adapters/household/workshop_tools/__init__.py +25 -0
  32. alter_runtime/adapters/household/workshop_tools/adapter.py +77 -0
  33. alter_runtime/adapters/household/workshop_tools/storage.py +92 -0
  34. alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
  35. alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +48 -0
  36. alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +26 -0
  37. alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +45 -0
  38. alter_runtime/adapters/household/workshop_tools/traits.py +95 -0
  39. alter_runtime/adapters/worktree_watcher.py +378 -0
  40. alter_runtime/atlas/__init__.py +48 -0
  41. alter_runtime/atlas/base.py +102 -0
  42. alter_runtime/atlas/ledger.py +196 -0
  43. alter_runtime/atlas/observations.py +136 -0
  44. alter_runtime/atlas/schema.py +106 -0
  45. alter_runtime/cap_cache.py +392 -0
  46. alter_runtime/cli.py +517 -0
  47. alter_runtime/clients/__init__.py +0 -0
  48. alter_runtime/clients/token_usage_client.py +273 -0
  49. alter_runtime/config.py +648 -0
  50. alter_runtime/consent.py +425 -0
  51. alter_runtime/daemon.py +518 -0
  52. alter_runtime/floor_loop.py +335 -0
  53. alter_runtime/floor_preflight.py +734 -0
  54. alter_runtime/http_auth.py +173 -0
  55. alter_runtime/notifiers/__init__.py +18 -0
  56. alter_runtime/notifiers/desktop.py +321 -0
  57. alter_runtime/sdk/__init__.py +12 -0
  58. alter_runtime/sdk/client.py +399 -0
  59. alter_runtime/service_install.py +616 -0
  60. alter_runtime/services/__init__.py +59 -0
  61. alter_runtime/services/launchd/com.alter.runtime.plist.in +90 -0
  62. alter_runtime/services/systemd/alter-runtime.service.in +74 -0
  63. alter_runtime/services/systemd/cf-access-env.conf.in +29 -0
  64. alter_runtime/sockets/__init__.py +20 -0
  65. alter_runtime/sockets/dbus.py +272 -0
  66. alter_runtime/sockets/unix.py +702 -0
  67. alter_runtime/subscribers/__init__.py +58 -0
  68. alter_runtime/subscribers/active_sessions_cron_emitter.py +313 -0
  69. alter_runtime/subscribers/active_sessions_do_publisher.py +1159 -0
  70. alter_runtime/subscribers/active_sessions_gc.py +432 -0
  71. alter_runtime/subscribers/active_sessions_writer.py +446 -0
  72. alter_runtime/subscribers/adapters_writer.py +415 -0
  73. alter_runtime/subscribers/agent_frames.py +461 -0
  74. alter_runtime/subscribers/bus.py +188 -0
  75. alter_runtime/subscribers/cache_writer.py +347 -0
  76. alter_runtime/subscribers/ceremony_echo.py +290 -0
  77. alter_runtime/subscribers/do_sse.py +864 -0
  78. alter_runtime/subscribers/ebpf.py +506 -0
  79. alter_runtime/subscribers/inbox_writer.py +469 -0
  80. alter_runtime/subscribers/mcp_fallback.py +391 -0
  81. alter_runtime/subscribers/presence_writer.py +426 -0
  82. alter_runtime/subscribers/session_presence.py +467 -0
  83. alter_runtime/subscribers/sse.py +125 -0
  84. alter_runtime/subscribers/weave_intent_writer.py +608 -0
  85. alter_runtime/update_loop.py +519 -0
  86. alter_runtime/weave/__init__.py +21 -0
  87. alter_runtime/weave/resolver.py +544 -0
  88. alter_runtime-0.3.0.dist-info/METADATA +289 -0
  89. alter_runtime-0.3.0.dist-info/RECORD +92 -0
  90. alter_runtime-0.3.0.dist-info/WHEEL +4 -0
  91. alter_runtime-0.3.0.dist-info/entry_points.txt +2 -0
  92. alter_runtime-0.3.0.dist-info/licenses/LICENSE +190 -0
@@ -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