alter-runtime 0.3.3__tar.gz → 0.4.8__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.
Files changed (102) hide show
  1. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/.gitignore +11 -0
  2. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/PKG-INFO +25 -51
  3. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/README.md +22 -48
  4. alter_runtime-0.4.8/alter_runtime/__init__.py +10 -0
  5. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/__init__.py +8 -3
  6. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/claude_jsonl_watcher.py +23 -12
  7. alter_runtime-0.4.8/alter_runtime/adapters/contract.py +103 -0
  8. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/git_watcher.py +9 -9
  9. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/weave_watcher.py +57 -32
  10. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/worktree_watcher.py +5 -5
  11. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/cap_cache.py +34 -10
  12. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/cli.py +12 -12
  13. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/config.py +246 -104
  14. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/consent.py +5 -5
  15. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/daemon.py +99 -60
  16. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/floor_loop.py +12 -13
  17. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/floor_preflight.py +12 -15
  18. alter_runtime-0.4.8/alter_runtime/http_auth.py +39 -0
  19. alter_runtime-0.4.8/alter_runtime/invocation_signing.py +620 -0
  20. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/notifiers/__init__.py +0 -4
  21. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/notifiers/desktop.py +1 -1
  22. alter_runtime-0.4.8/alter_runtime/sdk/__init__.py +9 -0
  23. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sdk/client.py +15 -22
  24. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/service_install.py +1 -57
  25. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/__init__.py +0 -12
  26. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/launchd/com.alter.runtime.plist.in +3 -4
  27. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/systemd/alter-runtime.service.in +5 -0
  28. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/__init__.py +1 -1
  29. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/dbus.py +2 -2
  30. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/unix.py +13 -13
  31. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/__init__.py +6 -3
  32. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_cron_emitter.py +5 -5
  33. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_do_publisher.py +55 -60
  34. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_gc.py +1 -1
  35. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_writer.py +7 -10
  36. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/adapters_writer.py +12 -12
  37. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/agent_frames.py +8 -8
  38. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/attunement_refresher.py +44 -18
  39. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/bus.py +5 -6
  40. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/cache_writer.py +16 -16
  41. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/ceremony_echo.py +11 -13
  42. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/do_sse.py +70 -54
  43. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/ebpf.py +2 -2
  44. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/inbox_writer.py +28 -29
  45. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/mcp_fallback.py +43 -16
  46. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/presence_feed_writer.py +8 -9
  47. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/presence_writer.py +3 -3
  48. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/session_presence.py +31 -33
  49. alter_runtime-0.4.8/alter_runtime/subscribers/session_refresher.py +390 -0
  50. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/sse.py +7 -9
  51. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/weave_intent_writer.py +15 -15
  52. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/update_loop.py +19 -20
  53. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave/__init__.py +1 -1
  54. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave/resolver.py +16 -16
  55. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave_log.py +30 -30
  56. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/pyproject.toml +21 -22
  57. alter_runtime-0.3.3/alter_runtime/__init__.py +0 -10
  58. alter_runtime-0.3.3/alter_runtime/adapters/household/__init__.py +0 -26
  59. alter_runtime-0.3.3/alter_runtime/adapters/household/_base.py +0 -136
  60. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/__init__.py +0 -15
  61. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/adapter.py +0 -81
  62. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/storage.py +0 -75
  63. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_adapter.py +0 -62
  64. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_storage.py +0 -23
  65. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_traits.py +0 -38
  66. alter_runtime-0.3.3/alter_runtime/adapters/household/compost/traits.py +0 -78
  67. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/__init__.py +0 -30
  68. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/adapter.py +0 -248
  69. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/storage.py +0 -83
  70. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
  71. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +0 -216
  72. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_storage.py +0 -25
  73. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_traits.py +0 -55
  74. alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/traits.py +0 -104
  75. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/__init__.py +0 -22
  76. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/adapter.py +0 -98
  77. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/storage.py +0 -95
  78. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
  79. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +0 -55
  80. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +0 -28
  81. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +0 -45
  82. alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/traits.py +0 -96
  83. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/__init__.py +0 -24
  84. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/adapter.py +0 -77
  85. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/storage.py +0 -92
  86. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
  87. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +0 -48
  88. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +0 -26
  89. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +0 -45
  90. alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/traits.py +0 -94
  91. alter_runtime-0.3.3/alter_runtime/atlas/__init__.py +0 -44
  92. alter_runtime-0.3.3/alter_runtime/atlas/base.py +0 -102
  93. alter_runtime-0.3.3/alter_runtime/atlas/ledger.py +0 -196
  94. alter_runtime-0.3.3/alter_runtime/atlas/observations.py +0 -136
  95. alter_runtime-0.3.3/alter_runtime/atlas/schema.py +0 -106
  96. alter_runtime-0.3.3/alter_runtime/clients/__init__.py +0 -0
  97. alter_runtime-0.3.3/alter_runtime/http_auth.py +0 -222
  98. alter_runtime-0.3.3/alter_runtime/sdk/__init__.py +0 -12
  99. alter_runtime-0.3.3/alter_runtime/services/systemd/cf-access-env.conf.in +0 -24
  100. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/LICENSE +0 -0
  101. {alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests → alter_runtime-0.4.8/alter_runtime/clients}/__init__.py +0 -0
  102. {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/clients/token_usage_client.py +0 -0
@@ -26,3 +26,14 @@ htmlcov/
26
26
  *.sock
27
27
  keypair.json
28
28
  runtime.yaml
29
+
30
+ # Binary build outputs
31
+ out/
32
+ .pyinstaller-workdir/
33
+ .pyinstaller-dist/
34
+ .venv-pyinstaller/
35
+ *.spec.toc
36
+ *.toc
37
+ *.pkg
38
+ *.exe
39
+ *.app/
@@ -1,12 +1,12 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: alter-runtime
3
- Version: 0.3.3
4
- Summary: ~Alter Identity Runtime - local sovereign daemon for the continuous identity field. Subscribes to per-~handle Cloudflare Durable Objects, exposes Unix socket + D-Bus + CLI surfaces, falls back gracefully to direct MCP polling.
3
+ Version: 0.4.8
4
+ Summary: ~Alter Identity Runtime - local daemon for the continuous identity field. Subscribes to per-~handle Cloudflare Durable Objects, exposes Unix socket + D-Bus + CLI surfaces, falls back gracefully to direct MCP polling.
5
5
  Project-URL: Homepage, https://truealter.com
6
6
  Project-URL: Documentation, https://truealter.com/docs/alter-runtime
7
7
  Project-URL: Repository, https://github.com/true-alter/alter-runtime
8
8
  Project-URL: Bug Tracker, https://github.com/true-alter/alter-runtime/issues
9
- Author: Blake Morrison
9
+ Author: ~Alter
10
10
  Author-email: ALTER Meridian Pty Ltd <hello@truealter.com>
11
11
  License: Apache-2.0
12
12
  License-File: LICENSE
@@ -68,13 +68,10 @@ Description-Content-Type: text/markdown
68
68
 
69
69
  # alter-runtime - ~Alter Identity Runtime
70
70
 
71
- > **L3 of the six-layer identity distribution surface.** The local sovereign daemon that
72
- > lives alongside you, subscribes to your per-`~handle` Cloudflare Durable Object, and
73
- > renders your identity field across every surface your device touches - terminal, IDE,
74
- > status bars, desktop tray, browser extension. Falls back gracefully to direct MCP
75
- > polling when the edge is unreachable.
76
-
77
- The alter-runtime daemon architecture was established in April 2026.
71
+ > The local daemon that lives alongside you, subscribes to your per-`~handle`
72
+ > Cloudflare Durable Object, and renders your identity field across every surface
73
+ > your device touches - terminal, IDE, status bars, desktop tray, browser extension.
74
+ > Falls back gracefully to direct MCP polling when the edge is unreachable.
78
75
 
79
76
  ## Alpha release - filesystem key storage
80
77
 
@@ -100,7 +97,7 @@ documented fallback.
100
97
  compromise would imply identity compromise - pair the runtime with
101
98
  hardware-attested passkey registration via the browser claim flow (graduated
102
99
  attestation). The filesystem-stored device key then scopes only to ambient
103
- signal ingestion, not to Sovereign-tier authorisations, which require a fresh
100
+ signal ingestion, not to high-assurance authorisations, which require a fresh
104
101
  hardware passkey ceremony per session.
105
102
 
106
103
  **Reporting issues.** Security-relevant reports: please follow
@@ -111,9 +108,8 @@ filing a public issue. Non-security bugs and feature requests: email
111
108
  ## Status
112
109
 
113
110
  Shipped: the daemon and `AlterClient` SDK; subscribers, the Unix socket,
114
- D-Bus, and the git watcher; systemd and launchd service units; the CC hook
115
- and scripts integration; and the eBPF subscriber (reference implementation
116
- in the `alter-ebpf` package).
111
+ D-Bus, and the git watcher; systemd and launchd service units; the local
112
+ editor and shell hook surfaces; and the eBPF subscriber.
117
113
 
118
114
  ## Install
119
115
 
@@ -161,7 +157,7 @@ alter-runtime stop
161
157
  at `https://mcp.truealter.com/events/~yourhandle/stream`. Events arrive with ~50ms
162
158
  latency worldwide.
163
159
 
164
- 2. **Falls back gracefully** to direct polling of the ALTER MCP endpoint at
160
+ 2. **Falls back gracefully** to direct polling of the `~alter` MCP endpoint at
165
161
  `https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
166
162
  seconds. Your surfaces never know which path served them.
167
163
 
@@ -170,18 +166,18 @@ alter-runtime stop
170
166
  `~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
171
167
  - **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
172
168
  GNOME/KDE/Waybar modules
173
- - **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by the CC hook
174
- and shell scripts
169
+ - **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by local
170
+ editor hooks and shell scripts
175
171
 
176
172
  4. **Collects ambient signals** via adapters:
177
173
  - Git commits, branch switches, pushes (via `watchdog` on `.git/refs/heads/`)
178
174
  - Editor session hook events (forwarded from local shell hooks)
179
175
  - Shell command invocations (opt-in)
180
- - eBPF kernel attestations (shipped; reference impl in `alter-ebpf`)
176
+ - eBPF kernel attestations (shipped)
181
177
 
182
178
  5. **Maintains a local cache** of your last-known-good field state so the first-paint
183
- tilde warmth renders in <1 second, even offline. Per IFA's Five OS-Native Properties,
184
- this is the **monotonic continuity** guarantee.
179
+ tilde warmth renders in <1 second, even offline. The cached state always renders,
180
+ so your surfaces stay continuous across restarts and network drops.
185
181
 
186
182
  ## Layout
187
183
 
@@ -192,14 +188,14 @@ alter-runtime/
192
188
  ├── LICENSE
193
189
  ├── alter_runtime/
194
190
  │ ├── __init__.py
195
- │ ├── config.py # XDG loader, reads alter-cli session.json
191
+ │ ├── config.py # XDG loader, reads the CLI session.json
196
192
  │ ├── daemon.py # asyncio supervisor
197
193
  │ ├── cli.py # argparse entrypoint: init|start|stop|status|query|ingest|daemon
198
194
  │ ├── sdk/
199
195
  │ │ ├── __init__.py
200
196
  │ │ └── client.py # AlterClient (async identity MCP client)
201
197
  │ ├── subscribers/
202
- │ │ ├── do_sse.py # primary - subscribes to handle-alter DO SSE
198
+ │ │ ├── do_sse.py # primary - subscribes to the per-handle DO SSE stream
203
199
  │ │ └── mcp_fallback.py # fallback - polls api.truealter.com/api/v1/mcp
204
200
  │ ├── sockets/
205
201
  │ │ ├── unix.py # /run/user/$UID/alter.sock
@@ -243,41 +239,19 @@ asyncio.run(main())
243
239
 
244
240
  ## Packaging
245
241
 
246
- - **PyPI** - `pip install alter-runtime` (publish is tag-gated; see
247
- `.github/workflows/ci.yml`).
248
- - **AUR** - draft PKGBUILD at [`aur/PKGBUILD`](aur/PKGBUILD). Submission is
249
- blocked on the PyPI publish resolving the sdist URL. Regenerate `.SRCINFO`
250
- with `makepkg --printsrcinfo > .SRCINFO` before AUR upload.
251
- - **`truealter` PyPI shim** - thin console-script package at
252
- [`pypi-truealter/`](pypi-truealter/) that execs the npm-installed
253
- `alter` binary. Distinct from `alter-runtime`: `pip install truealter`
254
- gets pip-centric users a fourth CLI install channel alongside npm,
255
- Homebrew (planned), and the AUR (planned). Tag format
256
- `truealter-v<version>`; see
257
- [`.github/workflows/truealter-publish.yml`](.github/workflows/truealter-publish.yml).
258
-
259
- ## Contributing
260
-
261
- After cloning, wire the identity-trailer hook so commits land with `Acted-By:`, `Drafted-With:`, and `Co-Authored-By:` trailers automatically:
262
-
263
- ```
264
- bash scripts/setup-githooks.sh
265
- ```
266
-
267
- The helper sets the local `core.hooksPath` to `.githooks/` and marks every hook executable. It is idempotent - safe to re-run after pulls. Equivalent one-liner without the helper:
268
-
269
- ```
270
- git config core.hooksPath .githooks
271
- ```
242
+ `alter-runtime` is distributed through several install channels:
272
243
 
273
- The hook reads your ALTER session at `~/.config/alter/session.json` (run `alter login` from the [`@truealter/cli`](https://www.npmjs.com/package/@truealter/cli) once) plus `$CLAUDE_MODEL` for the Instrument tier attribution. It is silent when the session is missing or `jq` is unavailable, so a contributor without an ALTER session still gets a normal commit; the CI verifier catches missing trailers at PR time (warn-only during the Rung 1/2 migration window).
244
+ - **PyPI** - `pip install alter-runtime`
245
+ - **Arch Linux (AUR)** - `alter-runtime`
246
+ - **Homebrew** - `brew install alter-runtime`
274
247
 
275
- Python projects don't have npm's `prepare`-on-install lifecycle, so this step is manual - done once after clone and persisted in your local `.git/config`.
248
+ Pin an exact version in downstream tooling during the alpha and read the
249
+ CHANGELOG before upgrading.
276
250
 
277
251
  ## Related projects
278
252
 
279
253
  - [`@truealter/sdk`](https://www.npmjs.com/package/@truealter/sdk) - TypeScript
280
254
  SDK client for the public MCP server.
281
- - ALTER MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
255
+ - `~alter` MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
282
256
  and `https://api.truealter.com/api/v1/mcp` (HTTP fallback). Both are documented
283
257
  at [truealter.com](https://truealter.com).
@@ -1,12 +1,9 @@
1
1
  # alter-runtime - ~Alter Identity Runtime
2
2
 
3
- > **L3 of the six-layer identity distribution surface.** The local sovereign daemon that
4
- > lives alongside you, subscribes to your per-`~handle` Cloudflare Durable Object, and
5
- > renders your identity field across every surface your device touches - terminal, IDE,
6
- > status bars, desktop tray, browser extension. Falls back gracefully to direct MCP
7
- > polling when the edge is unreachable.
8
-
9
- The alter-runtime daemon architecture was established in April 2026.
3
+ > The local daemon that lives alongside you, subscribes to your per-`~handle`
4
+ > Cloudflare Durable Object, and renders your identity field across every surface
5
+ > your device touches - terminal, IDE, status bars, desktop tray, browser extension.
6
+ > Falls back gracefully to direct MCP polling when the edge is unreachable.
10
7
 
11
8
  ## Alpha release - filesystem key storage
12
9
 
@@ -32,7 +29,7 @@ documented fallback.
32
29
  compromise would imply identity compromise - pair the runtime with
33
30
  hardware-attested passkey registration via the browser claim flow (graduated
34
31
  attestation). The filesystem-stored device key then scopes only to ambient
35
- signal ingestion, not to Sovereign-tier authorisations, which require a fresh
32
+ signal ingestion, not to high-assurance authorisations, which require a fresh
36
33
  hardware passkey ceremony per session.
37
34
 
38
35
  **Reporting issues.** Security-relevant reports: please follow
@@ -43,9 +40,8 @@ filing a public issue. Non-security bugs and feature requests: email
43
40
  ## Status
44
41
 
45
42
  Shipped: the daemon and `AlterClient` SDK; subscribers, the Unix socket,
46
- D-Bus, and the git watcher; systemd and launchd service units; the CC hook
47
- and scripts integration; and the eBPF subscriber (reference implementation
48
- in the `alter-ebpf` package).
43
+ D-Bus, and the git watcher; systemd and launchd service units; the local
44
+ editor and shell hook surfaces; and the eBPF subscriber.
49
45
 
50
46
  ## Install
51
47
 
@@ -93,7 +89,7 @@ alter-runtime stop
93
89
  at `https://mcp.truealter.com/events/~yourhandle/stream`. Events arrive with ~50ms
94
90
  latency worldwide.
95
91
 
96
- 2. **Falls back gracefully** to direct polling of the ALTER MCP endpoint at
92
+ 2. **Falls back gracefully** to direct polling of the `~alter` MCP endpoint at
97
93
  `https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
98
94
  seconds. Your surfaces never know which path served them.
99
95
 
@@ -102,18 +98,18 @@ alter-runtime stop
102
98
  `~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
103
99
  - **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
104
100
  GNOME/KDE/Waybar modules
105
- - **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by the CC hook
106
- and shell scripts
101
+ - **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by local
102
+ editor hooks and shell scripts
107
103
 
108
104
  4. **Collects ambient signals** via adapters:
109
105
  - Git commits, branch switches, pushes (via `watchdog` on `.git/refs/heads/`)
110
106
  - Editor session hook events (forwarded from local shell hooks)
111
107
  - Shell command invocations (opt-in)
112
- - eBPF kernel attestations (shipped; reference impl in `alter-ebpf`)
108
+ - eBPF kernel attestations (shipped)
113
109
 
114
110
  5. **Maintains a local cache** of your last-known-good field state so the first-paint
115
- tilde warmth renders in <1 second, even offline. Per IFA's Five OS-Native Properties,
116
- this is the **monotonic continuity** guarantee.
111
+ tilde warmth renders in <1 second, even offline. The cached state always renders,
112
+ so your surfaces stay continuous across restarts and network drops.
117
113
 
118
114
  ## Layout
119
115
 
@@ -124,14 +120,14 @@ alter-runtime/
124
120
  ├── LICENSE
125
121
  ├── alter_runtime/
126
122
  │ ├── __init__.py
127
- │ ├── config.py # XDG loader, reads alter-cli session.json
123
+ │ ├── config.py # XDG loader, reads the CLI session.json
128
124
  │ ├── daemon.py # asyncio supervisor
129
125
  │ ├── cli.py # argparse entrypoint: init|start|stop|status|query|ingest|daemon
130
126
  │ ├── sdk/
131
127
  │ │ ├── __init__.py
132
128
  │ │ └── client.py # AlterClient (async identity MCP client)
133
129
  │ ├── subscribers/
134
- │ │ ├── do_sse.py # primary - subscribes to handle-alter DO SSE
130
+ │ │ ├── do_sse.py # primary - subscribes to the per-handle DO SSE stream
135
131
  │ │ └── mcp_fallback.py # fallback - polls api.truealter.com/api/v1/mcp
136
132
  │ ├── sockets/
137
133
  │ │ ├── unix.py # /run/user/$UID/alter.sock
@@ -175,41 +171,19 @@ asyncio.run(main())
175
171
 
176
172
  ## Packaging
177
173
 
178
- - **PyPI** - `pip install alter-runtime` (publish is tag-gated; see
179
- `.github/workflows/ci.yml`).
180
- - **AUR** - draft PKGBUILD at [`aur/PKGBUILD`](aur/PKGBUILD). Submission is
181
- blocked on the PyPI publish resolving the sdist URL. Regenerate `.SRCINFO`
182
- with `makepkg --printsrcinfo > .SRCINFO` before AUR upload.
183
- - **`truealter` PyPI shim** - thin console-script package at
184
- [`pypi-truealter/`](pypi-truealter/) that execs the npm-installed
185
- `alter` binary. Distinct from `alter-runtime`: `pip install truealter`
186
- gets pip-centric users a fourth CLI install channel alongside npm,
187
- Homebrew (planned), and the AUR (planned). Tag format
188
- `truealter-v<version>`; see
189
- [`.github/workflows/truealter-publish.yml`](.github/workflows/truealter-publish.yml).
190
-
191
- ## Contributing
192
-
193
- After cloning, wire the identity-trailer hook so commits land with `Acted-By:`, `Drafted-With:`, and `Co-Authored-By:` trailers automatically:
194
-
195
- ```
196
- bash scripts/setup-githooks.sh
197
- ```
198
-
199
- The helper sets the local `core.hooksPath` to `.githooks/` and marks every hook executable. It is idempotent - safe to re-run after pulls. Equivalent one-liner without the helper:
200
-
201
- ```
202
- git config core.hooksPath .githooks
203
- ```
174
+ `alter-runtime` is distributed through several install channels:
204
175
 
205
- The hook reads your ALTER session at `~/.config/alter/session.json` (run `alter login` from the [`@truealter/cli`](https://www.npmjs.com/package/@truealter/cli) once) plus `$CLAUDE_MODEL` for the Instrument tier attribution. It is silent when the session is missing or `jq` is unavailable, so a contributor without an ALTER session still gets a normal commit; the CI verifier catches missing trailers at PR time (warn-only during the Rung 1/2 migration window).
176
+ - **PyPI** - `pip install alter-runtime`
177
+ - **Arch Linux (AUR)** - `alter-runtime`
178
+ - **Homebrew** - `brew install alter-runtime`
206
179
 
207
- Python projects don't have npm's `prepare`-on-install lifecycle, so this step is manual - done once after clone and persisted in your local `.git/config`.
180
+ Pin an exact version in downstream tooling during the alpha and read the
181
+ CHANGELOG before upgrading.
208
182
 
209
183
  ## Related projects
210
184
 
211
185
  - [`@truealter/sdk`](https://www.npmjs.com/package/@truealter/sdk) - TypeScript
212
186
  SDK client for the public MCP server.
213
- - ALTER MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
187
+ - `~alter` MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
214
188
  and `https://api.truealter.com/api/v1/mcp` (HTTP fallback). Both are documented
215
189
  at [truealter.com](https://truealter.com).
@@ -0,0 +1,10 @@
1
+ """~Alter Identity Runtime.
2
+
3
+ The local daemon that renders your identity field across the surfaces your
4
+ device touches. See the package README for an overview.
5
+ """
6
+
7
+ from alter_runtime.sdk.client import AlterClient
8
+
9
+ __version__ = "0.4.8"
10
+ __all__ = ["AlterClient", "__version__"]
@@ -1,9 +1,9 @@
1
1
  """Local adapters - ambient-signal publishers.
2
2
 
3
3
  Adapters are the runtime's *inputs from the device itself*: they observe
4
- something local (git commits, CC hook invocations, shell activity) and
4
+ something local (git commits, editor-hook invocations, shell activity) and
5
5
  publish a signal onto the ``local.signal`` topic. A separate egress producer
6
- (W2.2d) is responsible for POSTing those signals back to the per-handle DO
6
+ is responsible for POSTing those signals back to the per-handle Durable Object
7
7
  ``/ingest`` endpoint so they become part of the continuous identity field.
8
8
 
9
9
  Adapters are deliberately kept simple: one file per signal source, no shared
@@ -17,4 +17,9 @@ from alter_runtime.adapters.git_watcher import GitWatcher
17
17
  from alter_runtime.adapters.weave_watcher import WeaveWatcher
18
18
  from alter_runtime.adapters.worktree_watcher import WorktreeWatcher
19
19
 
20
- __all__ = ["ClaudeJsonlWatcher", "GitWatcher", "WeaveWatcher", "WorktreeWatcher"]
20
+ __all__ = [
21
+ "ClaudeJsonlWatcher",
22
+ "GitWatcher",
23
+ "WeaveWatcher",
24
+ "WorktreeWatcher",
25
+ ]
@@ -12,10 +12,9 @@ Privacy guarantee
12
12
  ``parse_assistant_line`` is the ONLY path from JSONL bytes to the wire.
13
13
  It is a strict **whitelist** parser: it constructs the output dict key-by-key
14
14
  from explicitly named fields and never spreads ``record``, ``message``, or
15
- ``usage``. The privacy regression test (``tests/test_claude_jsonl_watcher_no_content_leak.py``)
16
- seeds every non-whitelisted field with a CANARY sentinel and asserts that no
17
- sentinel reaches the serialised output. Any change to the whitelist fails
18
- that test.
15
+ ``usage``. A privacy regression test seeds every non-whitelisted field with a
16
+ CANARY sentinel and asserts that no sentinel reaches the serialised output.
17
+ Any change to the whitelist fails that test.
19
18
 
20
19
  Allowed output keys (exact set, no extras):
21
20
  - ``session_id`` - from top-level ``sessionId``
@@ -58,13 +57,14 @@ Configuration
58
57
  The adapter is opt-in: register it only when ``config.enable_claude_jsonl_watcher``
59
58
  is ``True`` (default ``False``).
60
59
 
61
- Decisions
62
- ---------
60
+ Data handling
61
+ -------------
63
62
 
64
- - Identity-as-Inference: token usage is a passive aggregate with
65
- k=1 (single principal machine). The inferred signal (burn-rate by model)
66
- is operational telemetry, NOT psychometric inference - no clause of the
67
- IaI 5-point test is triggered. Return: operator visibility.
63
+ - The adapter collects only aggregate token-usage counts per model
64
+ (input, output, and cache token totals) plus the message and session
65
+ identifiers listed above. It never reads or transmits prompt text,
66
+ response text, tool calls, or any message content. The data is local
67
+ operational telemetry for the owning user's own visibility.
68
68
  - HTTP auth: JWT sourced from ``load_session()`` at adapter start, then
69
69
  refreshed per-POST from ``ALTER_RUNTIME_SESSION_JWT`` env var as a
70
70
  fallback. See ``TokenUsageClient`` for details.
@@ -236,9 +236,12 @@ class ClaudeJsonlWatcher(Component):
236
236
  config: DaemonConfig,
237
237
  bus: EventBus,
238
238
  projects_dir: Path | None = None,
239
+ *,
240
+ session_ref: object | None = None,
239
241
  ) -> None:
240
242
  self._config = config
241
243
  self._bus = bus
244
+ self._session_ref = session_ref # Optional SessionRef for live JWT read-through
242
245
  self._projects_dir = (projects_dir or CLAUDE_PROJECTS_DIR).expanduser().resolve()
243
246
  self._stop_event = asyncio.Event()
244
247
  self._loop: asyncio.AbstractEventLoop | None = None
@@ -264,11 +267,19 @@ class ClaudeJsonlWatcher(Component):
264
267
  jwt = session.jwt
265
268
 
266
269
  api_base = os.environ.get("ALTER_RUNTIME_API_BASE", "https://api.truealter.com")
270
+ _ref = self._session_ref
267
271
 
268
272
  def _jwt_provider() -> str | None:
269
- # Prefer env-var override (for future ensureFreshSession wiring);
273
+ # Prefer env-var override; then live SessionRef (proactive rotation);
270
274
  # fall back to the session JWT loaded at startup.
271
- return os.environ.get("ALTER_RUNTIME_SESSION_JWT") or jwt
275
+ env_jwt = os.environ.get("ALTER_RUNTIME_SESSION_JWT")
276
+ if env_jwt:
277
+ return env_jwt
278
+ if _ref is not None:
279
+ live = getattr(_ref, "current", None)
280
+ if live is not None:
281
+ return getattr(live, "jwt", None)
282
+ return jwt
272
283
 
273
284
  self._http_client = TokenUsageClient(base_url=api_base, jwt_provider=_jwt_provider)
274
285
 
@@ -0,0 +1,103 @@
1
+ """Adapter plugin contract, the public extension seam for runtime adapters.
2
+
3
+ Built-in adapters are registered directly in :mod:`alter_runtime.daemon`.
4
+ Out-of-tree adapters (for example gated or separately-installed adapters
5
+ kept in their own package) register under the ``alter_runtime.adapters``
6
+ entry-point group and are discovered at start-up. This keeps the daemon
7
+ free of any import of an adapter that lives outside the public tree: an
8
+ adapter the host has not installed is simply absent, and the public package
9
+ carries no reference to it.
10
+
11
+ A plugin entry-point resolves to a *factory*: a callable that accepts a
12
+ single :class:`AdapterContext` and returns one component, an iterable of
13
+ components, or ``None`` to decline registration (for example when the
14
+ adapter is disabled by config). A returned component is any object with an
15
+ async ``run`` method, matching :class:`alter_runtime.daemon.Component`.
16
+ """
17
+
18
+ from __future__ import annotations
19
+
20
+ import logging
21
+ from collections.abc import Callable, Iterable, Iterator
22
+ from dataclasses import dataclass
23
+ from importlib.metadata import entry_points
24
+ from typing import TYPE_CHECKING, Any
25
+
26
+ if TYPE_CHECKING:
27
+ from alter_runtime.daemon import Component
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+ #: Entry-point group external adapter packages register under.
32
+ ADAPTER_ENTRY_POINT_GROUP = "alter_runtime.adapters"
33
+
34
+
35
+ @dataclass(frozen=True)
36
+ class AdapterContext:
37
+ """Stable construction context handed to every adapter factory.
38
+
39
+ Passing a context object rather than positional arguments keeps the
40
+ factory signature stable as the runtime gains new wiring: new fields are
41
+ added here without changing existing plugins.
42
+ """
43
+
44
+ config: Any
45
+ event_bus: Any
46
+ session: Any = None
47
+
48
+
49
+ #: A factory takes an AdapterContext and returns component(s) or None.
50
+ AdapterFactory = Callable[["AdapterContext"], "Component | Iterable[Component] | None"]
51
+
52
+
53
+ def discover_adapter_components(context: AdapterContext) -> Iterator[Component]:
54
+ """Yield components contributed by installed adapter plugins.
55
+
56
+ Each entry-point under :data:`ADAPTER_ENTRY_POINT_GROUP` is loaded and
57
+ called with ``context``. Failures (import error, factory exception, a
58
+ non-component return) are logged and skipped so a single bad plugin can
59
+ never prevent the daemon from starting. When no plugin package is
60
+ installed the generator is empty and the daemon runs exactly as a clean
61
+ public build does.
62
+ """
63
+ try:
64
+ discovered = entry_points(group=ADAPTER_ENTRY_POINT_GROUP)
65
+ except Exception: # pragma: no cover - importlib metadata edge cases
66
+ logger.exception("failed to enumerate adapter entry-points")
67
+ return
68
+
69
+ for entry_point in discovered:
70
+ try:
71
+ factory = entry_point.load()
72
+ result = factory(context)
73
+ except Exception:
74
+ logger.exception("adapter plugin %r failed to load; skipping", entry_point.name)
75
+ continue
76
+ if result is None:
77
+ continue
78
+ candidates = result if _is_iterable(result) else [result]
79
+ for component in candidates:
80
+ if _looks_like_component(component):
81
+ logger.info("registered adapter plugin component: %s", entry_point.name)
82
+ yield component
83
+ else:
84
+ logger.warning(
85
+ "adapter plugin %r returned a non-component (%s); skipping",
86
+ entry_point.name,
87
+ type(component).__name__,
88
+ )
89
+
90
+
91
+ def _is_iterable(obj: object) -> bool:
92
+ if isinstance(obj, (str, bytes)):
93
+ return False
94
+ try:
95
+ iter(obj) # type: ignore[call-overload]
96
+ except TypeError:
97
+ return False
98
+ return True
99
+
100
+
101
+ def _looks_like_component(obj: object) -> bool:
102
+ run = getattr(obj, "run", None)
103
+ return callable(run)
@@ -28,8 +28,8 @@ Signals
28
28
  }
29
29
 
30
30
  Both are published on the ``local.signal`` topic for the eventual egress
31
- producer. The runtime itself does *not* post them back to the DO - that is
32
- the egress producer's job in W2.2d.
31
+ producer. The runtime itself does *not* post them back to the server - that
32
+ is the egress producer's job: it consumes these signals and forwards them.
33
33
 
34
34
  Design notes
35
35
  ------------
@@ -72,12 +72,12 @@ logger = logging.getLogger("alter_runtime.adapters.git_watcher")
72
72
 
73
73
  EGRESS_TOPIC: str = "local.signal"
74
74
 
75
- #: Branch-name shape gate (Pentest 2026-04-26, MEDIUM). Git itself imposes
75
+ #: Branch-name shape gate (hardening). Git itself imposes
76
76
  #: tighter rules (see ``check-ref-format(1)``), but a watchdog-driven file
77
77
  #: rename can land arbitrary bytes here if an attacker plants a malicious
78
78
  #: file under ``.git/refs/heads/``. We accept the conservative subset that
79
79
  #: covers every legitimate branch name we expect on disk and reject the
80
- #: rest before they reach the bus and any downstream consumers (DO ingest,
80
+ #: rest before they reach the bus and any downstream consumers (server ingest,
81
81
  #: status-bar widgets, etc.).
82
82
 
83
83
  _BRANCH_NAME_RE = _re.compile(r"^[A-Za-z0-9_./-]+$")
@@ -221,7 +221,7 @@ class GitWatcher(Component):
221
221
  repos: list[_WatchedRepo] = []
222
222
  for path in paths:
223
223
  git_dir = path / ".git"
224
- # Pentest 2026-04-26 (MEDIUM): refuse to register a watch when
224
+ # Hardening: refuse to register a watch when
225
225
  # ``.git`` is a symlink. Watchdog follows symlinks transparently,
226
226
  # which would let an attacker drop a symlinked .git into a CWD
227
227
  # the daemon scans and trick the watcher into observing - and
@@ -284,8 +284,8 @@ class GitWatcher(Component):
284
284
  if path == head_file or path.name == "HEAD":
285
285
  new_branch = _read_head_branch(head_file) if head_file.exists() else None
286
286
  if new_branch and new_branch != repo.head_branch:
287
- # Pentest 2026-04-26 (MEDIUM): branch names traverse the
288
- # bus to the DO ingest path. Reject anything that doesn't
287
+ # Hardening: branch names traverse the
288
+ # bus to the server ingest path. Reject anything that doesn't
289
289
  # match the conservative shape gate before publishing -
290
290
  # don't update head_branch on reject so the next valid
291
291
  # change still fires.
@@ -317,7 +317,7 @@ class GitWatcher(Component):
317
317
  if not path.exists() or not path.is_file():
318
318
  return
319
319
  branch = _branch_name_from_ref(refs_dir, path)
320
- # Pentest 2026-04-26 (MEDIUM): sanitise the branch name before it
320
+ # Hardening: sanitise the branch name before it
321
321
  # reaches the bus. _branch_name_from_ref derives from the on-disk
322
322
  # ref filename which a same-UID attacker can plant arbitrarily.
323
323
  if not _is_safe_branch_name(branch):
@@ -394,7 +394,7 @@ class _GitRefHandler:
394
394
  # FileOpenedEvent + FileClosedNoWriteEvent for every open-and-read
395
395
  # of a watched file. ``.git/HEAD`` and the active branch ref are
396
396
  # opened thousands of times per second by ``git status`` callers
397
- # (IDE git plugins, statusline scripts, parallel CC sessions), and
397
+ # (IDE git plugins, statusline scripts, parallel editor/tooling sessions), and
398
398
  # without this filter every read scheduled an ``asyncio.create_task``
399
399
  # via ``_on_ref_change`` - flooding the loop with no-op handles
400
400
  # that grew RSS by ~10MB/s and tripped OOM in <5min on a busy repo.