alter-runtime 0.3.2__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.
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/.gitignore +12 -1
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/PKG-INFO +32 -64
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/README.md +29 -61
- alter_runtime-0.4.8/alter_runtime/__init__.py +10 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/adapters/__init__.py +9 -3
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/adapters/claude_jsonl_watcher.py +30 -20
- alter_runtime-0.4.8/alter_runtime/adapters/contract.py +103 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/adapters/git_watcher.py +9 -9
- alter_runtime-0.4.8/alter_runtime/adapters/weave_watcher.py +546 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/adapters/worktree_watcher.py +20 -20
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/cap_cache.py +37 -14
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/cli.py +36 -25
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/clients/token_usage_client.py +14 -19
- alter_runtime-0.4.8/alter_runtime/config.py +1154 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/consent.py +10 -11
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/daemon.py +172 -119
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/floor_loop.py +30 -31
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/floor_preflight.py +63 -83
- alter_runtime-0.4.8/alter_runtime/http_auth.py +39 -0
- alter_runtime-0.4.8/alter_runtime/invocation_signing.py +620 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/notifiers/__init__.py +0 -4
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/notifiers/desktop.py +3 -3
- alter_runtime-0.4.8/alter_runtime/sdk/__init__.py +9 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/sdk/client.py +23 -33
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/service_install.py +5 -62
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/services/__init__.py +2 -15
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/services/launchd/com.alter.runtime.plist.in +8 -9
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/services/systemd/alter-runtime.service.in +21 -8
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/sockets/__init__.py +3 -3
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/sockets/dbus.py +2 -2
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/sockets/unix.py +20 -20
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/__init__.py +12 -5
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_cron_emitter.py +11 -11
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_do_publisher.py +240 -83
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_gc.py +2 -2
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_writer.py +7 -10
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/adapters_writer.py +12 -12
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/agent_frames.py +53 -28
- alter_runtime-0.4.8/alter_runtime/subscribers/attunement_refresher.py +382 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/bus.py +8 -9
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/cache_writer.py +27 -30
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/ceremony_echo.py +20 -24
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/do_sse.py +74 -50
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/ebpf.py +6 -6
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/inbox_writer.py +31 -33
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/mcp_fallback.py +102 -27
- alter_runtime-0.4.8/alter_runtime/subscribers/presence_feed_writer.py +342 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/presence_writer.py +4 -4
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/session_presence.py +45 -40
- alter_runtime-0.4.8/alter_runtime/subscribers/session_refresher.py +390 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/sse.py +7 -9
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/subscribers/weave_intent_writer.py +34 -34
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/update_loop.py +51 -62
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/weave/__init__.py +3 -3
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/alter_runtime/weave/resolver.py +34 -34
- alter_runtime-0.4.8/alter_runtime/weave_log.py +437 -0
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/pyproject.toml +29 -23
- alter_runtime-0.3.2/alter_runtime/__init__.py +0 -11
- alter_runtime-0.3.2/alter_runtime/adapters/household/__init__.py +0 -29
- alter_runtime-0.3.2/alter_runtime/adapters/household/_base.py +0 -138
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/__init__.py +0 -17
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/adapter.py +0 -81
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/storage.py +0 -75
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/tests/test_adapter.py +0 -62
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/tests/test_storage.py +0 -23
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/tests/test_traits.py +0 -38
- alter_runtime-0.3.2/alter_runtime/adapters/household/compost/traits.py +0 -79
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/__init__.py +0 -30
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/adapter.py +0 -248
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/storage.py +0 -83
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +0 -216
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/tests/test_storage.py +0 -25
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/tests/test_traits.py +0 -55
- alter_runtime-0.3.2/alter_runtime/adapters/household/self_hoster/traits.py +0 -105
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/__init__.py +0 -22
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/adapter.py +0 -98
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/storage.py +0 -95
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +0 -55
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +0 -28
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +0 -45
- alter_runtime-0.3.2/alter_runtime/adapters/household/tapo_ecosystem/traits.py +0 -97
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/__init__.py +0 -25
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/adapter.py +0 -77
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/storage.py +0 -92
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +0 -48
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +0 -26
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +0 -45
- alter_runtime-0.3.2/alter_runtime/adapters/household/workshop_tools/traits.py +0 -95
- alter_runtime-0.3.2/alter_runtime/atlas/__init__.py +0 -47
- alter_runtime-0.3.2/alter_runtime/atlas/base.py +0 -102
- alter_runtime-0.3.2/alter_runtime/atlas/ledger.py +0 -196
- alter_runtime-0.3.2/alter_runtime/atlas/observations.py +0 -136
- alter_runtime-0.3.2/alter_runtime/atlas/schema.py +0 -106
- alter_runtime-0.3.2/alter_runtime/clients/__init__.py +0 -0
- alter_runtime-0.3.2/alter_runtime/config.py +0 -648
- alter_runtime-0.3.2/alter_runtime/http_auth.py +0 -173
- alter_runtime-0.3.2/alter_runtime/sdk/__init__.py +0 -12
- alter_runtime-0.3.2/alter_runtime/services/systemd/cf-access-env.conf.in +0 -29
- alter_runtime-0.3.2/docs/schemas/README.md +0 -46
- alter_runtime-0.3.2/examples/awesomewm/README.md +0 -122
- alter_runtime-0.3.2/examples/waybar/README.md +0 -66
- alter_runtime-0.3.2/pypi-truealter/README.md +0 -46
- alter_runtime-0.3.2/pypi-truealter/pyproject.toml +0 -67
- {alter_runtime-0.3.2 → alter_runtime-0.4.8}/LICENSE +0 -0
- {alter_runtime-0.3.2/alter_runtime/adapters/household/compost/tests → alter_runtime-0.4.8/alter_runtime/clients}/__init__.py +0 -0
|
@@ -22,7 +22,18 @@ htmlcov/
|
|
|
22
22
|
.vscode/
|
|
23
23
|
.idea/
|
|
24
24
|
|
|
25
|
-
#
|
|
25
|
+
# Generated daemon artefacts (local daemon state, never in the repo)
|
|
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.
|
|
4
|
-
Summary: ~Alter Identity Runtime - local
|
|
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:
|
|
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
|
-
>
|
|
72
|
-
>
|
|
73
|
-
>
|
|
74
|
-
>
|
|
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
|
|
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
|
|
@@ -110,16 +107,9 @@ filing a public issue. Non-security bugs and feature requests: email
|
|
|
110
107
|
|
|
111
108
|
## Status
|
|
112
109
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
| 2 | 2b - Subscribers, Unix socket, D-Bus, git watcher | Shipped |
|
|
117
|
-
| 2 | 2c - systemd + launchd service units | Shipped |
|
|
118
|
-
| 2 | 2d - First pixel: CC hook + scripts upgrade | Shipped |
|
|
119
|
-
| 2 | 2e - eBPF subscriber (reference impl in `alter-ebpf`) | Shipped |
|
|
120
|
-
| 3 | 3a - Cross-platform tray surfaces | Planned |
|
|
121
|
-
| 3 | 3b - Windows Service + `pam_alter` stub | Planned |
|
|
122
|
-
| 3 | 3c - PyPI release CI + signed binaries | Planned |
|
|
110
|
+
Shipped: the daemon and `AlterClient` SDK; subscribers, the Unix socket,
|
|
111
|
+
D-Bus, and the git watcher; systemd and launchd service units; the local
|
|
112
|
+
editor and shell hook surfaces; and the eBPF subscriber.
|
|
123
113
|
|
|
124
114
|
## Install
|
|
125
115
|
|
|
@@ -167,7 +157,7 @@ alter-runtime stop
|
|
|
167
157
|
at `https://mcp.truealter.com/events/~yourhandle/stream`. Events arrive with ~50ms
|
|
168
158
|
latency worldwide.
|
|
169
159
|
|
|
170
|
-
2. **Falls back gracefully** to direct polling of the
|
|
160
|
+
2. **Falls back gracefully** to direct polling of the `~alter` MCP endpoint at
|
|
171
161
|
`https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
|
|
172
162
|
seconds. Your surfaces never know which path served them.
|
|
173
163
|
|
|
@@ -176,18 +166,18 @@ alter-runtime stop
|
|
|
176
166
|
`~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
|
|
177
167
|
- **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
|
|
178
168
|
GNOME/KDE/Waybar modules
|
|
179
|
-
- **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by
|
|
180
|
-
and shell scripts
|
|
169
|
+
- **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by local
|
|
170
|
+
editor hooks and shell scripts
|
|
181
171
|
|
|
182
|
-
4. **Collects ambient signals** via adapters
|
|
172
|
+
4. **Collects ambient signals** via adapters:
|
|
183
173
|
- Git commits, branch switches, pushes (via `watchdog` on `.git/refs/heads/`)
|
|
184
|
-
-
|
|
174
|
+
- Editor session hook events (forwarded from local shell hooks)
|
|
185
175
|
- Shell command invocations (opt-in)
|
|
186
|
-
- eBPF kernel attestations (shipped
|
|
176
|
+
- eBPF kernel attestations (shipped)
|
|
187
177
|
|
|
188
178
|
5. **Maintains a local cache** of your last-known-good field state so the first-paint
|
|
189
|
-
tilde warmth renders in <1 second, even offline.
|
|
190
|
-
|
|
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.
|
|
191
181
|
|
|
192
182
|
## Layout
|
|
193
183
|
|
|
@@ -198,21 +188,21 @@ alter-runtime/
|
|
|
198
188
|
├── LICENSE
|
|
199
189
|
├── alter_runtime/
|
|
200
190
|
│ ├── __init__.py
|
|
201
|
-
│ ├── config.py # XDG loader, reads
|
|
191
|
+
│ ├── config.py # XDG loader, reads the CLI session.json
|
|
202
192
|
│ ├── daemon.py # asyncio supervisor
|
|
203
193
|
│ ├── cli.py # argparse entrypoint: init|start|stop|status|query|ingest|daemon
|
|
204
194
|
│ ├── sdk/
|
|
205
195
|
│ │ ├── __init__.py
|
|
206
196
|
│ │ └── client.py # AlterClient (async identity MCP client)
|
|
207
|
-
│ ├── subscribers/
|
|
208
|
-
│ │ ├── do_sse.py # primary - subscribes to handle
|
|
197
|
+
│ ├── subscribers/
|
|
198
|
+
│ │ ├── do_sse.py # primary - subscribes to the per-handle DO SSE stream
|
|
209
199
|
│ │ └── mcp_fallback.py # fallback - polls api.truealter.com/api/v1/mcp
|
|
210
|
-
│ ├── sockets/
|
|
200
|
+
│ ├── sockets/
|
|
211
201
|
│ │ ├── unix.py # /run/user/$UID/alter.sock
|
|
212
202
|
│ │ └── dbus.py # org.alter.Identity1
|
|
213
|
-
│ ├── adapters/
|
|
203
|
+
│ ├── adapters/
|
|
214
204
|
│ │ └── git_watcher.py # watchdog on .git/refs/heads/
|
|
215
|
-
│ └── services/
|
|
205
|
+
│ └── services/
|
|
216
206
|
│ ├── systemd/alter-runtime.service
|
|
217
207
|
│ ├── launchd/com.alter.runtime.plist
|
|
218
208
|
│ └── windows/AlterRuntimeService.py
|
|
@@ -249,41 +239,19 @@ asyncio.run(main())
|
|
|
249
239
|
|
|
250
240
|
## Packaging
|
|
251
241
|
|
|
252
|
-
|
|
253
|
-
`.github/workflows/ci.yml`).
|
|
254
|
-
- **AUR** - draft PKGBUILD at [`aur/PKGBUILD`](aur/PKGBUILD). Submission is
|
|
255
|
-
blocked on the PyPI publish resolving the sdist URL. Regenerate `.SRCINFO`
|
|
256
|
-
with `makepkg --printsrcinfo > .SRCINFO` before AUR upload.
|
|
257
|
-
- **`truealter` PyPI shim** - thin console-script package at
|
|
258
|
-
[`pypi-truealter/`](pypi-truealter/) that execs the npm-installed
|
|
259
|
-
`alter` binary. Distinct from `alter-runtime`: `pip install truealter`
|
|
260
|
-
gets pip-centric users a fourth CLI install channel alongside npm,
|
|
261
|
-
Homebrew (planned), and the AUR (planned). Tag format
|
|
262
|
-
`truealter-v<version>`; see
|
|
263
|
-
[`.github/workflows/truealter-publish.yml`](.github/workflows/truealter-publish.yml).
|
|
264
|
-
|
|
265
|
-
## Contributing
|
|
266
|
-
|
|
267
|
-
After cloning, wire the identity-trailer hook so commits land with `Acted-By:`, `Drafted-With:`, and `Co-Authored-By:` trailers automatically:
|
|
268
|
-
|
|
269
|
-
```
|
|
270
|
-
bash scripts/setup-githooks.sh
|
|
271
|
-
```
|
|
272
|
-
|
|
273
|
-
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:
|
|
274
|
-
|
|
275
|
-
```
|
|
276
|
-
git config core.hooksPath .githooks
|
|
277
|
-
```
|
|
242
|
+
`alter-runtime` is distributed through several install channels:
|
|
278
243
|
|
|
279
|
-
|
|
244
|
+
- **PyPI** - `pip install alter-runtime`
|
|
245
|
+
- **Arch Linux (AUR)** - `alter-runtime`
|
|
246
|
+
- **Homebrew** - `brew install alter-runtime`
|
|
280
247
|
|
|
281
|
-
|
|
248
|
+
Pin an exact version in downstream tooling during the alpha and read the
|
|
249
|
+
CHANGELOG before upgrading.
|
|
282
250
|
|
|
283
251
|
## Related projects
|
|
284
252
|
|
|
285
253
|
- [`@truealter/sdk`](https://www.npmjs.com/package/@truealter/sdk) - TypeScript
|
|
286
254
|
SDK client for the public MCP server.
|
|
287
|
-
-
|
|
255
|
+
- `~alter` MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
|
|
288
256
|
and `https://api.truealter.com/api/v1/mcp` (HTTP fallback). Both are documented
|
|
289
257
|
at [truealter.com](https://truealter.com).
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
# alter-runtime - ~Alter Identity Runtime
|
|
2
2
|
|
|
3
|
-
>
|
|
4
|
-
>
|
|
5
|
-
>
|
|
6
|
-
>
|
|
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
|
|
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
|
|
@@ -42,16 +39,9 @@ filing a public issue. Non-security bugs and feature requests: email
|
|
|
42
39
|
|
|
43
40
|
## Status
|
|
44
41
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
| 2 | 2b - Subscribers, Unix socket, D-Bus, git watcher | Shipped |
|
|
49
|
-
| 2 | 2c - systemd + launchd service units | Shipped |
|
|
50
|
-
| 2 | 2d - First pixel: CC hook + scripts upgrade | Shipped |
|
|
51
|
-
| 2 | 2e - eBPF subscriber (reference impl in `alter-ebpf`) | Shipped |
|
|
52
|
-
| 3 | 3a - Cross-platform tray surfaces | Planned |
|
|
53
|
-
| 3 | 3b - Windows Service + `pam_alter` stub | Planned |
|
|
54
|
-
| 3 | 3c - PyPI release CI + signed binaries | Planned |
|
|
42
|
+
Shipped: the daemon and `AlterClient` SDK; subscribers, the Unix socket,
|
|
43
|
+
D-Bus, and the git watcher; systemd and launchd service units; the local
|
|
44
|
+
editor and shell hook surfaces; and the eBPF subscriber.
|
|
55
45
|
|
|
56
46
|
## Install
|
|
57
47
|
|
|
@@ -99,7 +89,7 @@ alter-runtime stop
|
|
|
99
89
|
at `https://mcp.truealter.com/events/~yourhandle/stream`. Events arrive with ~50ms
|
|
100
90
|
latency worldwide.
|
|
101
91
|
|
|
102
|
-
2. **Falls back gracefully** to direct polling of the
|
|
92
|
+
2. **Falls back gracefully** to direct polling of the `~alter` MCP endpoint at
|
|
103
93
|
`https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
|
|
104
94
|
seconds. Your surfaces never know which path served them.
|
|
105
95
|
|
|
@@ -108,18 +98,18 @@ alter-runtime stop
|
|
|
108
98
|
`~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
|
|
109
99
|
- **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
|
|
110
100
|
GNOME/KDE/Waybar modules
|
|
111
|
-
- **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by
|
|
112
|
-
and shell scripts
|
|
101
|
+
- **HTTP/SSE** loopback at `http://127.0.0.1:<port>/events` - used by local
|
|
102
|
+
editor hooks and shell scripts
|
|
113
103
|
|
|
114
|
-
4. **Collects ambient signals** via adapters
|
|
104
|
+
4. **Collects ambient signals** via adapters:
|
|
115
105
|
- Git commits, branch switches, pushes (via `watchdog` on `.git/refs/heads/`)
|
|
116
|
-
-
|
|
106
|
+
- Editor session hook events (forwarded from local shell hooks)
|
|
117
107
|
- Shell command invocations (opt-in)
|
|
118
|
-
- eBPF kernel attestations (shipped
|
|
108
|
+
- eBPF kernel attestations (shipped)
|
|
119
109
|
|
|
120
110
|
5. **Maintains a local cache** of your last-known-good field state so the first-paint
|
|
121
|
-
tilde warmth renders in <1 second, even offline.
|
|
122
|
-
|
|
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.
|
|
123
113
|
|
|
124
114
|
## Layout
|
|
125
115
|
|
|
@@ -130,21 +120,21 @@ alter-runtime/
|
|
|
130
120
|
├── LICENSE
|
|
131
121
|
├── alter_runtime/
|
|
132
122
|
│ ├── __init__.py
|
|
133
|
-
│ ├── config.py # XDG loader, reads
|
|
123
|
+
│ ├── config.py # XDG loader, reads the CLI session.json
|
|
134
124
|
│ ├── daemon.py # asyncio supervisor
|
|
135
125
|
│ ├── cli.py # argparse entrypoint: init|start|stop|status|query|ingest|daemon
|
|
136
126
|
│ ├── sdk/
|
|
137
127
|
│ │ ├── __init__.py
|
|
138
128
|
│ │ └── client.py # AlterClient (async identity MCP client)
|
|
139
|
-
│ ├── subscribers/
|
|
140
|
-
│ │ ├── do_sse.py # primary - subscribes to handle
|
|
129
|
+
│ ├── subscribers/
|
|
130
|
+
│ │ ├── do_sse.py # primary - subscribes to the per-handle DO SSE stream
|
|
141
131
|
│ │ └── mcp_fallback.py # fallback - polls api.truealter.com/api/v1/mcp
|
|
142
|
-
│ ├── sockets/
|
|
132
|
+
│ ├── sockets/
|
|
143
133
|
│ │ ├── unix.py # /run/user/$UID/alter.sock
|
|
144
134
|
│ │ └── dbus.py # org.alter.Identity1
|
|
145
|
-
│ ├── adapters/
|
|
135
|
+
│ ├── adapters/
|
|
146
136
|
│ │ └── git_watcher.py # watchdog on .git/refs/heads/
|
|
147
|
-
│ └── services/
|
|
137
|
+
│ └── services/
|
|
148
138
|
│ ├── systemd/alter-runtime.service
|
|
149
139
|
│ ├── launchd/com.alter.runtime.plist
|
|
150
140
|
│ └── windows/AlterRuntimeService.py
|
|
@@ -181,41 +171,19 @@ asyncio.run(main())
|
|
|
181
171
|
|
|
182
172
|
## Packaging
|
|
183
173
|
|
|
184
|
-
|
|
185
|
-
`.github/workflows/ci.yml`).
|
|
186
|
-
- **AUR** - draft PKGBUILD at [`aur/PKGBUILD`](aur/PKGBUILD). Submission is
|
|
187
|
-
blocked on the PyPI publish resolving the sdist URL. Regenerate `.SRCINFO`
|
|
188
|
-
with `makepkg --printsrcinfo > .SRCINFO` before AUR upload.
|
|
189
|
-
- **`truealter` PyPI shim** - thin console-script package at
|
|
190
|
-
[`pypi-truealter/`](pypi-truealter/) that execs the npm-installed
|
|
191
|
-
`alter` binary. Distinct from `alter-runtime`: `pip install truealter`
|
|
192
|
-
gets pip-centric users a fourth CLI install channel alongside npm,
|
|
193
|
-
Homebrew (planned), and the AUR (planned). Tag format
|
|
194
|
-
`truealter-v<version>`; see
|
|
195
|
-
[`.github/workflows/truealter-publish.yml`](.github/workflows/truealter-publish.yml).
|
|
196
|
-
|
|
197
|
-
## Contributing
|
|
198
|
-
|
|
199
|
-
After cloning, wire the identity-trailer hook so commits land with `Acted-By:`, `Drafted-With:`, and `Co-Authored-By:` trailers automatically:
|
|
200
|
-
|
|
201
|
-
```
|
|
202
|
-
bash scripts/setup-githooks.sh
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
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:
|
|
206
|
-
|
|
207
|
-
```
|
|
208
|
-
git config core.hooksPath .githooks
|
|
209
|
-
```
|
|
174
|
+
`alter-runtime` is distributed through several install channels:
|
|
210
175
|
|
|
211
|
-
|
|
176
|
+
- **PyPI** - `pip install alter-runtime`
|
|
177
|
+
- **Arch Linux (AUR)** - `alter-runtime`
|
|
178
|
+
- **Homebrew** - `brew install alter-runtime`
|
|
212
179
|
|
|
213
|
-
|
|
180
|
+
Pin an exact version in downstream tooling during the alpha and read the
|
|
181
|
+
CHANGELOG before upgrading.
|
|
214
182
|
|
|
215
183
|
## Related projects
|
|
216
184
|
|
|
217
185
|
- [`@truealter/sdk`](https://www.npmjs.com/package/@truealter/sdk) - TypeScript
|
|
218
186
|
SDK client for the public MCP server.
|
|
219
|
-
-
|
|
187
|
+
- `~alter` MCP endpoint - `https://mcp.truealter.com` (SSE per-`~handle` stream)
|
|
220
188
|
and `https://api.truealter.com/api/v1/mcp` (HTTP fallback). Both are documented
|
|
221
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,
|
|
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
|
-
|
|
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
|
|
@@ -14,6 +14,12 @@ runtime component.
|
|
|
14
14
|
|
|
15
15
|
from alter_runtime.adapters.claude_jsonl_watcher import ClaudeJsonlWatcher
|
|
16
16
|
from alter_runtime.adapters.git_watcher import GitWatcher
|
|
17
|
+
from alter_runtime.adapters.weave_watcher import WeaveWatcher
|
|
17
18
|
from alter_runtime.adapters.worktree_watcher import WorktreeWatcher
|
|
18
19
|
|
|
19
|
-
__all__ = [
|
|
20
|
+
__all__ = [
|
|
21
|
+
"ClaudeJsonlWatcher",
|
|
22
|
+
"GitWatcher",
|
|
23
|
+
"WeaveWatcher",
|
|
24
|
+
"WorktreeWatcher",
|
|
25
|
+
]
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""ClaudeJsonlWatcher - ambient token-usage adapter for Claude Code transcripts.
|
|
2
2
|
|
|
3
|
-
Watches
|
|
3
|
+
Watches the per-project ``*.jsonl`` transcript files under Claude Code's
|
|
4
|
+
home projects directory with ``watchdog``.
|
|
4
5
|
On modification, reads new lines from the persisted byte offset, parses each
|
|
5
6
|
via the privacy-hard-stop ``parse_assistant_line`` function, and POSTs
|
|
6
7
|
batches to the ALTER backend token-usage audit endpoint.
|
|
@@ -11,10 +12,9 @@ Privacy guarantee
|
|
|
11
12
|
``parse_assistant_line`` is the ONLY path from JSONL bytes to the wire.
|
|
12
13
|
It is a strict **whitelist** parser: it constructs the output dict key-by-key
|
|
13
14
|
from explicitly named fields and never spreads ``record``, ``message``, or
|
|
14
|
-
``usage``.
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
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.
|
|
18
18
|
|
|
19
19
|
Allowed output keys (exact set, no extras):
|
|
20
20
|
- ``session_id`` - from top-level ``sessionId``
|
|
@@ -39,8 +39,8 @@ On file rotation (on-disk size < persisted offset), the offset is reset to 0.
|
|
|
39
39
|
Backfill
|
|
40
40
|
--------
|
|
41
41
|
|
|
42
|
-
On first run, every existing ``*.jsonl`` under
|
|
43
|
-
walked and assistant events within the last 30 days are emitted in batches of
|
|
42
|
+
On first run, every existing ``*.jsonl`` under the Claude Code projects
|
|
43
|
+
directory is walked and assistant events within the last 30 days are emitted in batches of
|
|
44
44
|
at most 500. A 5-second sleep is inserted between batches to avoid flooding
|
|
45
45
|
the backend.
|
|
46
46
|
|
|
@@ -57,18 +57,17 @@ Configuration
|
|
|
57
57
|
The adapter is opt-in: register it only when ``config.enable_claude_jsonl_watcher``
|
|
58
58
|
is ``True`` (default ``False``).
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
Data handling
|
|
61
|
+
-------------
|
|
62
62
|
|
|
63
|
-
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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.
|
|
67
68
|
- HTTP auth: JWT sourced from ``load_session()`` at adapter start, then
|
|
68
69
|
refreshed per-POST from ``ALTER_RUNTIME_SESSION_JWT`` env var as a
|
|
69
|
-
fallback.
|
|
70
|
-
Wave 2 follow-up - see ``TOKEN_USAGE_AUTH_GAP`` docstring on
|
|
71
|
-
``TokenUsageClient``.
|
|
70
|
+
fallback. See ``TokenUsageClient`` for details.
|
|
72
71
|
"""
|
|
73
72
|
|
|
74
73
|
from __future__ import annotations
|
|
@@ -227,7 +226,7 @@ class ClaudeJsonlWatcher(Component):
|
|
|
227
226
|
Shared :class:`EventBus` (not published to - present for Component
|
|
228
227
|
symmetry and future local-signal emission).
|
|
229
228
|
projects_dir:
|
|
230
|
-
Override
|
|
229
|
+
Override the Claude Code projects directory for testing.
|
|
231
230
|
"""
|
|
232
231
|
|
|
233
232
|
name = "claude_jsonl_watcher"
|
|
@@ -237,9 +236,12 @@ class ClaudeJsonlWatcher(Component):
|
|
|
237
236
|
config: DaemonConfig,
|
|
238
237
|
bus: EventBus,
|
|
239
238
|
projects_dir: Path | None = None,
|
|
239
|
+
*,
|
|
240
|
+
session_ref: object | None = None,
|
|
240
241
|
) -> None:
|
|
241
242
|
self._config = config
|
|
242
243
|
self._bus = bus
|
|
244
|
+
self._session_ref = session_ref # Optional SessionRef for live JWT read-through
|
|
243
245
|
self._projects_dir = (projects_dir or CLAUDE_PROJECTS_DIR).expanduser().resolve()
|
|
244
246
|
self._stop_event = asyncio.Event()
|
|
245
247
|
self._loop: asyncio.AbstractEventLoop | None = None
|
|
@@ -265,11 +267,19 @@ class ClaudeJsonlWatcher(Component):
|
|
|
265
267
|
jwt = session.jwt
|
|
266
268
|
|
|
267
269
|
api_base = os.environ.get("ALTER_RUNTIME_API_BASE", "https://api.truealter.com")
|
|
270
|
+
_ref = self._session_ref
|
|
268
271
|
|
|
269
272
|
def _jwt_provider() -> str | None:
|
|
270
|
-
# Prefer env-var override
|
|
273
|
+
# Prefer env-var override; then live SessionRef (proactive rotation);
|
|
271
274
|
# fall back to the session JWT loaded at startup.
|
|
272
|
-
|
|
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
|
|
273
283
|
|
|
274
284
|
self._http_client = TokenUsageClient(base_url=api_base, jwt_provider=_jwt_provider)
|
|
275
285
|
|
|
@@ -502,7 +512,7 @@ def _read_events_from_file(
|
|
|
502
512
|
def _slug_from_path(jsonl_path: Path, projects_dir: Path) -> str:
|
|
503
513
|
"""Derive the project slug from the JSONL file path.
|
|
504
514
|
|
|
505
|
-
|
|
515
|
+
``<projects-dir>/<slug>/<session>.jsonl`` → ``<slug>``
|
|
506
516
|
"""
|
|
507
517
|
try:
|
|
508
518
|
relative = jsonl_path.relative_to(projects_dir)
|
|
@@ -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)
|