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.
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/.gitignore +11 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/PKG-INFO +25 -51
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/README.md +22 -48
- alter_runtime-0.4.8/alter_runtime/__init__.py +10 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/__init__.py +8 -3
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/claude_jsonl_watcher.py +23 -12
- alter_runtime-0.4.8/alter_runtime/adapters/contract.py +103 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/git_watcher.py +9 -9
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/weave_watcher.py +57 -32
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/adapters/worktree_watcher.py +5 -5
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/cap_cache.py +34 -10
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/cli.py +12 -12
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/config.py +246 -104
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/consent.py +5 -5
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/daemon.py +99 -60
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/floor_loop.py +12 -13
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/floor_preflight.py +12 -15
- 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.3 → alter_runtime-0.4.8}/alter_runtime/notifiers/__init__.py +0 -4
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/notifiers/desktop.py +1 -1
- alter_runtime-0.4.8/alter_runtime/sdk/__init__.py +9 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sdk/client.py +15 -22
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/service_install.py +1 -57
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/__init__.py +0 -12
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/launchd/com.alter.runtime.plist.in +3 -4
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/services/systemd/alter-runtime.service.in +5 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/__init__.py +1 -1
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/dbus.py +2 -2
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/sockets/unix.py +13 -13
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/__init__.py +6 -3
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_cron_emitter.py +5 -5
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_do_publisher.py +55 -60
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_gc.py +1 -1
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/active_sessions_writer.py +7 -10
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/adapters_writer.py +12 -12
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/agent_frames.py +8 -8
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/attunement_refresher.py +44 -18
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/bus.py +5 -6
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/cache_writer.py +16 -16
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/ceremony_echo.py +11 -13
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/do_sse.py +70 -54
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/ebpf.py +2 -2
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/inbox_writer.py +28 -29
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/mcp_fallback.py +43 -16
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/presence_feed_writer.py +8 -9
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/presence_writer.py +3 -3
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/session_presence.py +31 -33
- alter_runtime-0.4.8/alter_runtime/subscribers/session_refresher.py +390 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/sse.py +7 -9
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/subscribers/weave_intent_writer.py +15 -15
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/update_loop.py +19 -20
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave/__init__.py +1 -1
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave/resolver.py +16 -16
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/weave_log.py +30 -30
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/pyproject.toml +21 -22
- alter_runtime-0.3.3/alter_runtime/__init__.py +0 -10
- alter_runtime-0.3.3/alter_runtime/adapters/household/__init__.py +0 -26
- alter_runtime-0.3.3/alter_runtime/adapters/household/_base.py +0 -136
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/__init__.py +0 -15
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/adapter.py +0 -81
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/storage.py +0 -75
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_adapter.py +0 -62
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_storage.py +0 -23
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests/test_traits.py +0 -38
- alter_runtime-0.3.3/alter_runtime/adapters/household/compost/traits.py +0 -78
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/__init__.py +0 -30
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/adapter.py +0 -248
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/storage.py +0 -83
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/__init__.py +0 -0
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_adapter.py +0 -216
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_storage.py +0 -25
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/tests/test_traits.py +0 -55
- alter_runtime-0.3.3/alter_runtime/adapters/household/self_hoster/traits.py +0 -104
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/__init__.py +0 -22
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/adapter.py +0 -98
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/storage.py +0 -95
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/__init__.py +0 -0
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_adapter.py +0 -55
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_storage.py +0 -28
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/tests/test_traits.py +0 -45
- alter_runtime-0.3.3/alter_runtime/adapters/household/tapo_ecosystem/traits.py +0 -96
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/__init__.py +0 -24
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/adapter.py +0 -77
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/storage.py +0 -92
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/__init__.py +0 -0
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_adapter.py +0 -48
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_storage.py +0 -26
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/tests/test_traits.py +0 -45
- alter_runtime-0.3.3/alter_runtime/adapters/household/workshop_tools/traits.py +0 -94
- alter_runtime-0.3.3/alter_runtime/atlas/__init__.py +0 -44
- alter_runtime-0.3.3/alter_runtime/atlas/base.py +0 -102
- alter_runtime-0.3.3/alter_runtime/atlas/ledger.py +0 -196
- alter_runtime-0.3.3/alter_runtime/atlas/observations.py +0 -136
- alter_runtime-0.3.3/alter_runtime/atlas/schema.py +0 -106
- alter_runtime-0.3.3/alter_runtime/clients/__init__.py +0 -0
- alter_runtime-0.3.3/alter_runtime/http_auth.py +0 -222
- alter_runtime-0.3.3/alter_runtime/sdk/__init__.py +0 -12
- alter_runtime-0.3.3/alter_runtime/services/systemd/cf-access-env.conf.in +0 -24
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/LICENSE +0 -0
- {alter_runtime-0.3.3/alter_runtime/adapters/household/compost/tests → alter_runtime-0.4.8/alter_runtime/clients}/__init__.py +0 -0
- {alter_runtime-0.3.3 → alter_runtime-0.4.8}/alter_runtime/clients/token_usage_client.py +0 -0
|
@@ -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
|
|
@@ -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
|
|
115
|
-
and
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
184
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
244
|
+
- **PyPI** - `pip install alter-runtime`
|
|
245
|
+
- **Arch Linux (AUR)** - `alter-runtime`
|
|
246
|
+
- **Homebrew** - `brew install alter-runtime`
|
|
274
247
|
|
|
275
|
-
|
|
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
|
-
-
|
|
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
|
-
>
|
|
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
|
|
@@ -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
|
|
47
|
-
and
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
176
|
+
- **PyPI** - `pip install alter-runtime`
|
|
177
|
+
- **Arch Linux (AUR)** - `alter-runtime`
|
|
178
|
+
- **Homebrew** - `brew install alter-runtime`
|
|
206
179
|
|
|
207
|
-
|
|
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
|
-
-
|
|
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,
|
|
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
|
|
@@ -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__ = [
|
|
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``.
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
62
|
-
|
|
60
|
+
Data handling
|
|
61
|
+
-------------
|
|
63
62
|
|
|
64
|
-
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
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
|
|
273
|
+
# Prefer env-var override; then live SessionRef (proactive rotation);
|
|
270
274
|
# fall back to the session JWT loaded at startup.
|
|
271
|
-
|
|
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
|
|
32
|
-
the egress producer's job
|
|
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 (
|
|
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 (
|
|
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
|
-
#
|
|
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
|
-
#
|
|
288
|
-
# bus to the
|
|
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
|
-
#
|
|
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
|
|
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.
|