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,90 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <!--
3
+ alter-runtime — ~Alter Identity Runtime (launchd user agent template)
4
+
5
+ Install path: ~/Library/LaunchAgents/com.alter.runtime.plist
6
+
7
+ Placeholders substituted by alter_runtime.service_install.render_launchd_plist():
8
+ @ALTER_RUNTIME_BIN@ — absolute path to the `alter-runtime` entry point
9
+ @ALTER_RUNTIME_LOG_DIR@ — directory for stdout/stderr log files
10
+ @ALTER_RUNTIME_HOME@ — value of $HOME at install time
11
+
12
+ After install, load with:
13
+ launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.alter.runtime.plist
14
+ launchctl kickstart -k gui/$(id -u)/com.alter.runtime
15
+
16
+ Unload with:
17
+ launchctl bootout gui/$(id -u)/com.alter.runtime
18
+ -->
19
+ <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
20
+ "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
21
+ <plist version="1.0">
22
+ <dict>
23
+ <key>Label</key>
24
+ <string>com.alter.runtime</string>
25
+
26
+ <key>ProgramArguments</key>
27
+ <array>
28
+ <string>@ALTER_RUNTIME_BIN@</string>
29
+ <string>daemon</string>
30
+ </array>
31
+
32
+ <!-- Start at login and keep running; restart on crash with a 5-second throttle. -->
33
+ <key>RunAtLoad</key>
34
+ <true/>
35
+ <key>KeepAlive</key>
36
+ <dict>
37
+ <key>SuccessfulExit</key>
38
+ <false/>
39
+ <key>Crashed</key>
40
+ <true/>
41
+ </dict>
42
+ <key>ThrottleInterval</key>
43
+ <integer>5</integer>
44
+
45
+ <!-- Tight process-creation mask so sockets / state files land at 0o600. -->
46
+ <key>Umask</key>
47
+ <integer>63</integer>
48
+
49
+ <!-- Logs land under ~/Library/Logs/alter-runtime/ so macOS Console.app picks them up. -->
50
+ <key>StandardOutPath</key>
51
+ <string>@ALTER_RUNTIME_LOG_DIR@/alter-runtime.log</string>
52
+ <key>StandardErrorPath</key>
53
+ <string>@ALTER_RUNTIME_LOG_DIR@/alter-runtime.err.log</string>
54
+
55
+ <!-- Environment -->
56
+ <key>EnvironmentVariables</key>
57
+ <dict>
58
+ <key>HOME</key>
59
+ <string>@ALTER_RUNTIME_HOME@</string>
60
+ </dict>
61
+
62
+ <!-- Soft process limits — mirror the systemd unit. ProcessType=Background
63
+ tells launchd to apply the OS X timer / IO / scheduling defaults for
64
+ a background daemon (less CPU + IO priority, eligible for App Nap),
65
+ and LowPriorityIO further drops disk-IO priority so the daemon never
66
+ starves a foreground IDE / build that the operator is actively using.
67
+
68
+ Pentest 2026-04-26 (MEDIUM) follow-up: ship a sandbox-exec profile
69
+ next to this template that constrains filesystem writes to
70
+ ~/Library/Application Support/alter, ~/Library/Caches/alter and
71
+ ~/Library/Logs/alter-runtime, denies all network listening sockets
72
+ except the Unix socket under Application Support, and uses
73
+ ``allow-with-prompt`` for outbound HTTPS to alter-api.fly.dev only.
74
+ Tracked at alter-runtime#sandbox-profile (TODO; out of scope for
75
+ this PR — the plist itself is the security floor pre-sandbox). -->
76
+ <key>ProcessType</key>
77
+ <string>Background</string>
78
+ <key>LowPriorityIO</key>
79
+ <true/>
80
+
81
+ <!-- Nice-to-have for GUI sessions: the LaunchAgent inherits the user's
82
+ Aqua session so the Unix socket at ~/Library/Application Support/alter
83
+ is writable without extra entitlements. -->
84
+ <key>LimitLoadToSessionType</key>
85
+ <array>
86
+ <string>Aqua</string>
87
+ <string>Background</string>
88
+ </array>
89
+ </dict>
90
+ </plist>
@@ -0,0 +1,74 @@
1
+ #
2
+ # alter-runtime — ~Alter Identity Runtime (systemd user unit template)
3
+ #
4
+ # Install path: ~/.config/systemd/user/alter-runtime.service
5
+ #
6
+ # Placeholders substituted by alter_runtime.service_install.render_systemd_unit():
7
+ # @ALTER_RUNTIME_BIN@ — absolute path to the `alter-runtime` entry point
8
+ # @ALTER_RUNTIME_HOME@ — value of $HOME at install time (for Environment=)
9
+ #
10
+ # After install, enable with:
11
+ # systemctl --user daemon-reload
12
+ # systemctl --user enable --now alter-runtime.service
13
+ #
14
+ # Logs: journalctl --user -u alter-runtime -f
15
+ #
16
+
17
+ [Unit]
18
+ Description=~Alter Identity Runtime — local sovereign daemon
19
+ Documentation=https://github.com/true-alter/Alter/blob/main/packages/alter-runtime/README.md
20
+ # We do not require network-online.target because the daemon tolerates
21
+ # offline starts — it degrades to its local cache and retries the DO
22
+ # connection with exponential backoff (D-RT9).
23
+ After=default.target
24
+ # Cap restart storms during a bad deploy. StartLimit* keys live on [Unit]
25
+ # (not [Service]) per systemd.unit(5).
26
+ StartLimitIntervalSec=60
27
+ StartLimitBurst=5
28
+
29
+ [Service]
30
+ Type=exec
31
+ ExecStart=@ALTER_RUNTIME_BIN@ daemon
32
+ Restart=on-failure
33
+ RestartSec=5s
34
+
35
+ # Environment
36
+ Environment=HOME=@ALTER_RUNTIME_HOME@
37
+ # XDG_RUNTIME_DIR is set by pam_systemd on interactive logins; for headless
38
+ # sessions we let the daemon fall back to the per-uid /tmp path via
39
+ # alter_runtime.config.unix_socket_path().
40
+
41
+ # Keep stdout/stderr on the journal so `journalctl --user -u alter-runtime`
42
+ # gives the operator immediate visibility.
43
+ StandardOutput=journal
44
+ StandardError=journal
45
+ SyslogIdentifier=alter-runtime
46
+
47
+ # Process hygiene — run as the invoking user (implicit for user units),
48
+ # with a tight umask so newly created socket / state files stay 0o600.
49
+ UMask=0077
50
+
51
+ # Resource limits — the daemon holds one httpx connection + small state
52
+ # caches. Everything else is defensive.
53
+ LimitNOFILE=1024
54
+ MemoryMax=256M
55
+ TasksMax=64
56
+
57
+ # Hardening (works across systemd ≥ 240, which every supported distro ships)
58
+ NoNewPrivileges=true
59
+ PrivateTmp=true
60
+ ProtectSystem=strict
61
+ ProtectHome=read-only
62
+ ReadWritePaths=%h/.config/alter %h/.cache/alter %h/.local/state/alter %h/.local/state/alter-runtime %h/.local/share/alter-runtime %h/.local/share/org-alter %t
63
+ ProtectKernelTunables=true
64
+ ProtectKernelModules=true
65
+ ProtectKernelLogs=true
66
+ ProtectControlGroups=true
67
+ RestrictSUIDSGID=true
68
+ RestrictNamespaces=true
69
+ LockPersonality=true
70
+ SystemCallFilter=@system-service
71
+ SystemCallErrorNumber=EPERM
72
+
73
+ [Install]
74
+ WantedBy=default.target
@@ -0,0 +1,29 @@
1
+ # alter-runtime — Cloudflare Access service-token environment drop-in.
2
+ #
3
+ # Install path: ~/.config/systemd/user/alter-runtime.service.d/cf-access-env.conf
4
+ #
5
+ # Loads the host's Cloudflare Access service-token credentials from
6
+ # ~/.config/alter/cf-access.env so the daemon's httpx clients can inject
7
+ # CF-Access-Client-Id + CF-Access-Client-Secret headers on every request
8
+ # to mcp.truealter.com. Without these headers CF Access intercepts at the
9
+ # edge with a 302 redirect to the truealteraccess.com login flow and the
10
+ # daemon's session-JWT bearer token is never seen by the origin.
11
+ #
12
+ # The leading "-" on the EnvironmentFile= line makes the load non-fatal
13
+ # when the file is absent — dev hosts, CI runners, and freshly installed
14
+ # systems without a CF Access service token configured all still start
15
+ # the daemon cleanly. The daemon's HTTP client treats absent env-vars as
16
+ # "no CF Access auth" and returns an empty header dict from
17
+ # alter_runtime.http_auth.cf_access_default_headers().
18
+ #
19
+ # Sister doctrine: D-SUBSTRATE-UNIFIED-1 §2.3 (the CF Access fork),
20
+ # feedback-cf-access-secrets-out-of-mcp-json (envfile is the single
21
+ # source of truth for CF Access service-token credentials).
22
+ #
23
+ # Option B (substrate-mTLS, canonical) in D-SUBSTRATE-UNIFIED-1 Stream 6
24
+ # supersedes this drop-in once the Worker route at
25
+ # mcp.truealter.com/substrate/events/{handle}/stream lands; until then
26
+ # this is the transitional shape.
27
+
28
+ [Service]
29
+ EnvironmentFile=-%h/.config/alter/cf-access.env
@@ -0,0 +1,20 @@
1
+ """Local-transport surfaces exposed by the alter-runtime daemon.
2
+
3
+ Per D-RT2, the daemon exposes identity state on three local transports in
4
+ parallel:
5
+
6
+ * **Unix socket** at ``$XDG_RUNTIME_DIR/alter.sock`` (Linux) / Application
7
+ Support (macOS) - used by the CC hook, shell scripts, and arbitrary
8
+ language-agnostic callers. Line-delimited JSON-RPC.
9
+ * **D-Bus** on the session bus (Linux only, optional dependency on
10
+ ``dbus-next``) - used by GNOME / KDE / Waybar modules and any desktop
11
+ tooling that speaks D-Bus natively.
12
+ * **HTTP/SSE loopback** (Wave 3) - not yet implemented.
13
+
14
+ Each transport is a :class:`alter_runtime.daemon.Component` that registers
15
+ with the supervisor and shares the single-instance :class:`EventBus`.
16
+ """
17
+
18
+ from alter_runtime.sockets.unix import UnixSocketServer
19
+
20
+ __all__ = ["UnixSocketServer"]
@@ -0,0 +1,272 @@
1
+ """DBusService - ``org.alter.Identity1`` on the session bus (Linux only).
2
+
3
+ Exposes the daemon's identity event stream on the Linux session D-Bus under
4
+ the bus name ``org.alter.Identity1`` and object path ``/org/alter/Identity``.
5
+ This is the surface that Waybar / GNOME Shell / KDE Plasmoids consume.
6
+
7
+ Interface
8
+ ---------
9
+
10
+ ::
11
+
12
+ <interface name="org.alter.Identity1">
13
+ <method name="Whoami">
14
+ <arg name="handle" type="s" direction="out"/>
15
+ <arg name="consent_tier" type="i" direction="out"/>
16
+ </method>
17
+ <method name="Ingest">
18
+ <arg name="kind" type="s" direction="in"/>
19
+ <arg name="payload_json" type="s" direction="in"/>
20
+ <arg name="accepted" type="b" direction="out"/>
21
+ </method>
22
+ <signal name="IdentityEvent">
23
+ <arg name="event_json" type="s"/>
24
+ </signal>
25
+ </interface>
26
+
27
+ ``IdentityEvent`` is emitted for every ``identity.event`` bus publish. The
28
+ ``event_json`` argument is the JSON-encoded event payload - consumers on the
29
+ desktop side typically JSON-decode it and render a minimal subset
30
+ (attunement, handle, kind) into a status-bar widget.
31
+
32
+ Optional dependency
33
+ -------------------
34
+
35
+ The service is gated on ``dbus-next`` (installed via
36
+ ``alter-runtime[dbus]``). If the import fails (module missing, or running on
37
+ a non-Linux platform) the component logs a warning and sleeps on the
38
+ shutdown event - the daemon continues to run with its other surfaces.
39
+
40
+ Tests skip this component unless ``dbus-next`` is importable *and* a session
41
+ bus is available. CI on non-Linux platforms or on headless Linux runners
42
+ without ``dbus-daemon`` will hit the fallback no-op path.
43
+ """
44
+
45
+ from __future__ import annotations
46
+
47
+ import asyncio
48
+ import json
49
+ import logging
50
+ import sys
51
+ from typing import TYPE_CHECKING, Any
52
+
53
+ from alter_runtime.config import DaemonConfig
54
+ from alter_runtime.daemon import Component
55
+ from alter_runtime.sockets.unix import INGEST_KIND_WHITELIST
56
+ from alter_runtime.subscribers.bus import EventBus
57
+
58
+ if TYPE_CHECKING:
59
+ from alter_runtime.config import Session
60
+
61
+ __all__ = ["DBusService", "INGEST_KIND_WHITELIST", "is_dbus_available"]
62
+
63
+ logger = logging.getLogger("alter_runtime.sockets.dbus")
64
+
65
+ BUS_NAME: str = "org.alter.Identity1"
66
+ OBJECT_PATH: str = "/org/alter/Identity"
67
+ INTERFACE_NAME: str = "org.alter.Identity1"
68
+
69
+ #: Egress topic - mirrors :mod:`alter_runtime.sockets.unix`.
70
+ EGRESS_TOPIC: str = "local.signal"
71
+
72
+
73
+ def is_dbus_available() -> bool:
74
+ """Return True if ``dbus-next`` can be imported on this platform.
75
+
76
+ We do the import here rather than at module top so that ``sockets/dbus.py``
77
+ is importable on every platform - the component class has to exist even
78
+ if the optional dependency isn't installed, so the daemon can log a
79
+ helpful warning and continue.
80
+ """
81
+ if not sys.platform.startswith("linux"):
82
+ return False
83
+ try:
84
+ import dbus_next # noqa: F401
85
+ except ImportError:
86
+ return False
87
+ return True
88
+
89
+
90
+ class DBusService(Component):
91
+ """Exports identity events on the Linux session D-Bus.
92
+
93
+ When the optional ``dbus-next`` dependency is unavailable the component's
94
+ :meth:`run` logs a single warning and blocks on shutdown, so callers can
95
+ register it unconditionally without breaking non-Linux installs.
96
+
97
+ Parameters
98
+ ----------
99
+ config:
100
+ Loaded :class:`DaemonConfig`.
101
+ bus:
102
+ Shared :class:`EventBus` - subscribes to ``identity.event`` to emit
103
+ ``IdentityEvent`` signals, and publishes to ``local.signal`` for
104
+ Ingest() callers.
105
+ session:
106
+ Authenticated alter-cli :class:`Session` - used for the ``Whoami``
107
+ method. Optional; Whoami returns empty strings when absent.
108
+ bus_name, object_path, interface_name:
109
+ Test hooks for overriding the D-Bus identifiers. Production code
110
+ should leave these at their defaults.
111
+ """
112
+
113
+ name = "dbus"
114
+
115
+ def __init__(
116
+ self,
117
+ config: DaemonConfig,
118
+ bus: EventBus,
119
+ session: Session | None = None,
120
+ *,
121
+ bus_name: str = BUS_NAME,
122
+ object_path: str = OBJECT_PATH,
123
+ interface_name: str = INTERFACE_NAME,
124
+ ) -> None:
125
+ self._config = config
126
+ self._bus = bus
127
+ self._session = session
128
+ self._bus_name = bus_name
129
+ self._object_path = object_path
130
+ self._interface_name = interface_name
131
+ self._stop_event = asyncio.Event()
132
+ self._message_bus: Any = None
133
+ self._interface: Any = None
134
+ self._bus_handler: Any = None
135
+
136
+ async def run(self) -> None:
137
+ """Start the D-Bus service, or fall back to a quiet no-op."""
138
+ if not is_dbus_available():
139
+ logger.warning(
140
+ "dbus unavailable (install `alter-runtime[dbus]` for the Linux desktop "
141
+ "surface) - continuing without D-Bus export"
142
+ )
143
+ await self._stop_event.wait()
144
+ return
145
+
146
+ try:
147
+ await self._start_dbus()
148
+ except Exception as exc:
149
+ logger.warning(
150
+ "dbus failed to start (%s: %s) - continuing without D-Bus export",
151
+ type(exc).__name__,
152
+ exc,
153
+ )
154
+ await self._stop_event.wait()
155
+ return
156
+
157
+ logger.info(
158
+ "dbus exported bus=%s path=%s interface=%s",
159
+ self._bus_name,
160
+ self._object_path,
161
+ self._interface_name,
162
+ )
163
+
164
+ try:
165
+ await self._stop_event.wait()
166
+ finally:
167
+ await self._teardown_dbus()
168
+
169
+ async def stop(self) -> None:
170
+ """Cooperative shutdown."""
171
+ self._stop_event.set()
172
+
173
+ # ------------------------------------------------------------------
174
+ # D-Bus lifecycle (only reached when dbus_next is importable)
175
+ # ------------------------------------------------------------------
176
+
177
+ async def _start_dbus(self) -> None:
178
+ """Connect to the session bus, publish the interface, subscribe to events."""
179
+ # Local imports guarded by is_dbus_available(). Kept inside the
180
+ # function so that test collection on non-Linux machines doesn't
181
+ # choke when the optional dep is missing.
182
+ from dbus_next.aio import MessageBus # type: ignore[import-not-found]
183
+ from dbus_next.service import ( # type: ignore[import-not-found]
184
+ ServiceInterface,
185
+ method,
186
+ signal,
187
+ )
188
+
189
+ session = self._session
190
+ egress_bus = self._bus
191
+ loop = asyncio.get_running_loop()
192
+
193
+ class _AlterIdentityInterface(ServiceInterface):
194
+ """Concrete implementation of the ``org.alter.Identity1`` interface."""
195
+
196
+ def __init__(self, interface_name: str) -> None:
197
+ super().__init__(interface_name)
198
+
199
+ @method()
200
+ def Whoami(self) -> tuple[str, int]: # type: ignore[override]
201
+ if session is None:
202
+ return ("", 0)
203
+ return (session.handle, int(session.consent_tier))
204
+
205
+ @method()
206
+ def Ingest(self, kind: s, payload_json: s) -> b: # type: ignore[override,valid-type] # noqa: F821
207
+ try:
208
+ payload = json.loads(payload_json) if payload_json else {}
209
+ except ValueError:
210
+ return False
211
+ if not isinstance(payload, dict):
212
+ return False
213
+ # Pentest 2026-04-26 (HIGH): mirror the unix-socket
214
+ # ``INGEST_KIND_WHITELIST`` gate - without it any session-
215
+ # bus peer could publish arbitrary ``kind`` values onto the
216
+ # in-process bus, including kinds reserved for trusted
217
+ # internal subscribers (``git_commit``, ``ebpf_exec`` etc.)
218
+ # which downstream consumers would treat as authentic.
219
+ if kind not in INGEST_KIND_WHITELIST:
220
+ logger.warning(
221
+ "dbus rejecting ingest kind=%r (not in whitelist)",
222
+ kind,
223
+ )
224
+ return False
225
+ # Publish from the loop thread - D-Bus calls happen on the
226
+ # same asyncio loop in dbus-next's aio backend. We deliberately
227
+ # fire-and-forget the publish task; the bus guarantees its own
228
+ # in-memory ordering and nothing here awaits the result.
229
+ loop.create_task( # noqa: RUF006
230
+ egress_bus.publish(
231
+ EGRESS_TOPIC,
232
+ {"kind": kind, "payload": payload, "source": "dbus"},
233
+ )
234
+ )
235
+ return True
236
+
237
+ @signal()
238
+ def IdentityEvent(self, event_json: s) -> s: # type: ignore[override,valid-type] # noqa: F821
239
+ return event_json
240
+
241
+ self._message_bus = await MessageBus().connect()
242
+ self._interface = _AlterIdentityInterface(self._interface_name)
243
+ self._message_bus.export(self._object_path, self._interface)
244
+ await self._message_bus.request_name(self._bus_name)
245
+
246
+ async def _on_identity_event(payload: dict[str, Any]) -> None:
247
+ if self._interface is None:
248
+ return
249
+ try:
250
+ body = json.dumps(payload, separators=(",", ":"))
251
+ except (TypeError, ValueError):
252
+ logger.debug("dbus skipping non-JSON-serialisable event")
253
+ return
254
+ try:
255
+ self._interface.IdentityEvent(body)
256
+ except Exception as exc: # pragma: no cover
257
+ logger.warning("dbus signal emit failed: %s", exc)
258
+
259
+ self._bus_handler = _on_identity_event
260
+ self._bus.subscribe("identity.event", _on_identity_event)
261
+
262
+ async def _teardown_dbus(self) -> None:
263
+ if self._bus_handler is not None:
264
+ self._bus.unsubscribe("identity.event", self._bus_handler)
265
+ self._bus_handler = None
266
+ if self._message_bus is not None:
267
+ try:
268
+ self._message_bus.disconnect()
269
+ except Exception: # pragma: no cover
270
+ pass
271
+ self._message_bus = None
272
+ self._interface = None