alter-runtime 0.4.8__tar.gz → 0.4.9__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.4.8 → alter_runtime-0.4.9}/LICENSE +1 -1
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/PKG-INFO +11 -13
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/README.md +9 -11
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/__init__.py +1 -1
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/adapters/__init__.py +0 -2
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/adapters/claude_jsonl_watcher.py +2 -2
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/cli.py +92 -1
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/clients/token_usage_client.py +3 -3
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/config.py +64 -19
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/consent.py +43 -12
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/daemon.py +17 -16
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/http_auth.py +1 -1
- alter_runtime-0.4.9/alter_runtime/messaging.py +147 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/sdk/client.py +1 -1
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/services/systemd/alter-runtime.service.in +3 -3
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/sockets/unix.py +60 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/__init__.py +3 -1
- alter_runtime-0.4.9/alter_runtime/subscribers/doctrine_projection.py +678 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/session_presence.py +3 -3
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/update_loop.py +112 -14
- alter_runtime-0.4.9/alter_runtime/verified_apply.py +340 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/weave/__init__.py +1 -1
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/weave/resolver.py +3 -5
- alter_runtime-0.4.9/hatch_build.py +17 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/pyproject.toml +15 -9
- alter_runtime-0.4.8/.gitignore +0 -39
- alter_runtime-0.4.8/alter_runtime/adapters/weave_watcher.py +0 -546
- alter_runtime-0.4.8/alter_runtime/weave_log.py +0 -437
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/adapters/contract.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/adapters/git_watcher.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/adapters/worktree_watcher.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/cap_cache.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/clients/__init__.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/floor_loop.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/floor_preflight.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/invocation_signing.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/notifiers/__init__.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/notifiers/desktop.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/sdk/__init__.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/service_install.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/services/__init__.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/services/launchd/com.alter.runtime.plist.in +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/sockets/__init__.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/sockets/dbus.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/active_sessions_cron_emitter.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/active_sessions_do_publisher.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/active_sessions_gc.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/active_sessions_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/adapters_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/agent_frames.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/attunement_refresher.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/bus.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/cache_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/ceremony_echo.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/do_sse.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/ebpf.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/inbox_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/mcp_fallback.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/presence_feed_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/presence_writer.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/session_refresher.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/sse.py +0 -0
- {alter_runtime-0.4.8 → alter_runtime-0.4.9}/alter_runtime/subscribers/weave_intent_writer.py +0 -0
|
@@ -175,7 +175,7 @@
|
|
|
175
175
|
|
|
176
176
|
END OF TERMS AND CONDITIONS
|
|
177
177
|
|
|
178
|
-
Copyright 2026
|
|
178
|
+
Copyright 2026 Alter Meridian Pty Ltd
|
|
179
179
|
|
|
180
180
|
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
181
|
you may not use this file except in compliance with the License.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: alter-runtime
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.9
|
|
4
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
9
|
Author: ~Alter
|
|
10
|
-
Author-email:
|
|
10
|
+
Author-email: Alter Meridian Pty Ltd <hello@truealter.com>
|
|
11
11
|
License: Apache-2.0
|
|
12
12
|
License-File: LICENSE
|
|
13
13
|
Keywords: agent,ai-agents,alter,cloudflare,durable-object,identity,llm,mcp,model-context-protocol,psychometric,sovereign-identity,verifiable-credentials,x402
|
|
@@ -75,10 +75,10 @@ Description-Content-Type: text/markdown
|
|
|
75
75
|
|
|
76
76
|
## Alpha release - filesystem key storage
|
|
77
77
|
|
|
78
|
-
|
|
78
|
+
**This is an alpha release.** The public API surface (CLI flags,
|
|
79
79
|
Unix-socket JSON schema, D-Bus interface, Python SDK signatures) may change in
|
|
80
|
-
backwards-incompatible ways before the
|
|
81
|
-
downstream tooling
|
|
80
|
+
backwards-incompatible ways before the stable release. Pin exact versions in
|
|
81
|
+
downstream tooling before upgrading.
|
|
82
82
|
|
|
83
83
|
**Private keys are stored on the filesystem, not in an OS keychain.** On first
|
|
84
84
|
`alter-runtime init`, the device Ed25519 keypair is written to
|
|
@@ -88,7 +88,7 @@ owning user and never leaves the device. This behaviour is deliberate for the
|
|
|
88
88
|
alpha: it keeps the daemon self-contained on any POSIX host and avoids a hard
|
|
89
89
|
dependency on distro-specific secret stores during early testing.
|
|
90
90
|
|
|
91
|
-
**OS-native secret storage is deferred to
|
|
91
|
+
**OS-native secret storage is deferred to a future release:** Linux `libsecret` /
|
|
92
92
|
`kwallet`, macOS Keychain, and Windows Credential Manager integration will ship
|
|
93
93
|
behind a `keystore = "native"` config knob. Filesystem storage will remain the
|
|
94
94
|
documented fallback.
|
|
@@ -100,10 +100,9 @@ attestation). The filesystem-stored device key then scopes only to ambient
|
|
|
100
100
|
signal ingestion, not to high-assurance authorisations, which require a fresh
|
|
101
101
|
hardware passkey ceremony per session.
|
|
102
102
|
|
|
103
|
-
**Reporting issues.** Security-relevant reports: please
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
`support@truealter.com`.
|
|
103
|
+
**Reporting issues.** Security-relevant reports: please email
|
|
104
|
+
`security@truealter.com` rather than filing a public issue. Non-security bugs
|
|
105
|
+
and feature requests: email `support@truealter.com`.
|
|
107
106
|
|
|
108
107
|
## Status
|
|
109
108
|
|
|
@@ -161,7 +160,7 @@ alter-runtime stop
|
|
|
161
160
|
`https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
|
|
162
161
|
seconds. Your surfaces never know which path served them.
|
|
163
162
|
|
|
164
|
-
3. **Exposes three local transports
|
|
163
|
+
3. **Exposes** three local transports:
|
|
165
164
|
- **Unix socket** at `/run/user/$UID/alter.sock` (Linux) or
|
|
166
165
|
`~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
|
|
167
166
|
- **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
|
|
@@ -245,8 +244,7 @@ asyncio.run(main())
|
|
|
245
244
|
- **Arch Linux (AUR)** - `alter-runtime`
|
|
246
245
|
- **Homebrew** - `brew install alter-runtime`
|
|
247
246
|
|
|
248
|
-
Pin an exact version in downstream tooling during the alpha
|
|
249
|
-
CHANGELOG before upgrading.
|
|
247
|
+
Pin an exact version in downstream tooling during the alpha before upgrading.
|
|
250
248
|
|
|
251
249
|
## Related projects
|
|
252
250
|
|
|
@@ -7,10 +7,10 @@
|
|
|
7
7
|
|
|
8
8
|
## Alpha release - filesystem key storage
|
|
9
9
|
|
|
10
|
-
|
|
10
|
+
**This is an alpha release.** The public API surface (CLI flags,
|
|
11
11
|
Unix-socket JSON schema, D-Bus interface, Python SDK signatures) may change in
|
|
12
|
-
backwards-incompatible ways before the
|
|
13
|
-
downstream tooling
|
|
12
|
+
backwards-incompatible ways before the stable release. Pin exact versions in
|
|
13
|
+
downstream tooling before upgrading.
|
|
14
14
|
|
|
15
15
|
**Private keys are stored on the filesystem, not in an OS keychain.** On first
|
|
16
16
|
`alter-runtime init`, the device Ed25519 keypair is written to
|
|
@@ -20,7 +20,7 @@ owning user and never leaves the device. This behaviour is deliberate for the
|
|
|
20
20
|
alpha: it keeps the daemon self-contained on any POSIX host and avoids a hard
|
|
21
21
|
dependency on distro-specific secret stores during early testing.
|
|
22
22
|
|
|
23
|
-
**OS-native secret storage is deferred to
|
|
23
|
+
**OS-native secret storage is deferred to a future release:** Linux `libsecret` /
|
|
24
24
|
`kwallet`, macOS Keychain, and Windows Credential Manager integration will ship
|
|
25
25
|
behind a `keystore = "native"` config knob. Filesystem storage will remain the
|
|
26
26
|
documented fallback.
|
|
@@ -32,10 +32,9 @@ attestation). The filesystem-stored device key then scopes only to ambient
|
|
|
32
32
|
signal ingestion, not to high-assurance authorisations, which require a fresh
|
|
33
33
|
hardware passkey ceremony per session.
|
|
34
34
|
|
|
35
|
-
**Reporting issues.** Security-relevant reports: please
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
`support@truealter.com`.
|
|
35
|
+
**Reporting issues.** Security-relevant reports: please email
|
|
36
|
+
`security@truealter.com` rather than filing a public issue. Non-security bugs
|
|
37
|
+
and feature requests: email `support@truealter.com`.
|
|
39
38
|
|
|
40
39
|
## Status
|
|
41
40
|
|
|
@@ -93,7 +92,7 @@ alter-runtime stop
|
|
|
93
92
|
`https://api.truealter.com/api/v1/mcp` when the DO is unreachable for more than 3
|
|
94
93
|
seconds. Your surfaces never know which path served them.
|
|
95
94
|
|
|
96
|
-
3. **Exposes three local transports
|
|
95
|
+
3. **Exposes** three local transports:
|
|
97
96
|
- **Unix socket** at `/run/user/$UID/alter.sock` (Linux) or
|
|
98
97
|
`~/Library/Application Support/alter/runtime.sock` (macOS) - line-delimited JSON
|
|
99
98
|
- **D-Bus** interface `org.alter.Identity1` on the session bus (Linux) - used by
|
|
@@ -177,8 +176,7 @@ asyncio.run(main())
|
|
|
177
176
|
- **Arch Linux (AUR)** - `alter-runtime`
|
|
178
177
|
- **Homebrew** - `brew install alter-runtime`
|
|
179
178
|
|
|
180
|
-
Pin an exact version in downstream tooling during the alpha
|
|
181
|
-
CHANGELOG before upgrading.
|
|
179
|
+
Pin an exact version in downstream tooling during the alpha before upgrading.
|
|
182
180
|
|
|
183
181
|
## Related projects
|
|
184
182
|
|
|
@@ -14,12 +14,10 @@ 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
|
|
18
17
|
from alter_runtime.adapters.worktree_watcher import WorktreeWatcher
|
|
19
18
|
|
|
20
19
|
__all__ = [
|
|
21
20
|
"ClaudeJsonlWatcher",
|
|
22
21
|
"GitWatcher",
|
|
23
|
-
"WeaveWatcher",
|
|
24
22
|
"WorktreeWatcher",
|
|
25
23
|
]
|
|
@@ -4,7 +4,7 @@ Watches the per-project ``*.jsonl`` transcript files under Claude Code's
|
|
|
4
4
|
home projects directory with ``watchdog``.
|
|
5
5
|
On modification, reads new lines from the persisted byte offset, parses each
|
|
6
6
|
via the privacy-hard-stop ``parse_assistant_line`` function, and POSTs
|
|
7
|
-
batches to the
|
|
7
|
+
batches to the ~alter backend token-usage audit endpoint.
|
|
8
8
|
|
|
9
9
|
Privacy guarantee
|
|
10
10
|
-----------------
|
|
@@ -123,7 +123,7 @@ def parse_assistant_line(record: dict) -> Optional[dict]:
|
|
|
123
123
|
"""Parse one JSONL record and return a token-usage dict, or None.
|
|
124
124
|
|
|
125
125
|
This is the privacy hard-stop between Claude Code's local transcripts
|
|
126
|
-
and
|
|
126
|
+
and ~alter's audit database. It is a strict whitelist: the output dict is
|
|
127
127
|
constructed key-by-key from explicitly named fields. ``record``,
|
|
128
128
|
``message``, and ``usage`` are NEVER spread. No key beyond the whitelist
|
|
129
129
|
can reach the output.
|
|
@@ -9,6 +9,7 @@ Subcommands:
|
|
|
9
9
|
daemon Run the supervisor in the foreground (for systemd Type=exec or debugging).
|
|
10
10
|
query Query the current materialised view (attunement, warmth, trust tier).
|
|
11
11
|
ingest Manually ingest a signal (for testing).
|
|
12
|
+
send Send a member-to-member message via the running daemon.
|
|
12
13
|
|
|
13
14
|
Matches the shape of the companion CLI's command handler - a simple switch-on-verb,
|
|
14
15
|
no external CLI framework dependency (argparse is stdlib).
|
|
@@ -139,6 +140,20 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
139
140
|
)
|
|
140
141
|
p_ingest.add_argument("--producer", default="cli:manual", help="Producer identifier.")
|
|
141
142
|
|
|
143
|
+
# send
|
|
144
|
+
p_send = sub.add_parser(
|
|
145
|
+
"send",
|
|
146
|
+
help="Send a member-to-member message via the running daemon.",
|
|
147
|
+
)
|
|
148
|
+
p_send.add_argument("to", help="Recipient ~handle (e.g. ~peer).")
|
|
149
|
+
p_send.add_argument("body", help="Message body, markdown, <= 8 KiB.")
|
|
150
|
+
p_send.add_argument(
|
|
151
|
+
"--content-type",
|
|
152
|
+
dest="content_type",
|
|
153
|
+
default=None,
|
|
154
|
+
help="Message content_type (default: text/markdown).",
|
|
155
|
+
)
|
|
156
|
+
|
|
142
157
|
return parser
|
|
143
158
|
|
|
144
159
|
|
|
@@ -232,7 +247,7 @@ def cmd_start(args: argparse.Namespace) -> int:
|
|
|
232
247
|
except UserDeletedArtefact as exc:
|
|
233
248
|
print(
|
|
234
249
|
f" {exc.args[0]} was previously deleted by the user.\n"
|
|
235
|
-
"
|
|
250
|
+
" ~alter will not recreate it automatically.\n"
|
|
236
251
|
" To reinstall, run: alter-runtime start --reinstall",
|
|
237
252
|
file=sys.stderr,
|
|
238
253
|
)
|
|
@@ -448,6 +463,81 @@ def cmd_ingest(args: argparse.Namespace) -> int:
|
|
|
448
463
|
return 0
|
|
449
464
|
|
|
450
465
|
|
|
466
|
+
def _daemon_token() -> str | None:
|
|
467
|
+
"""Read the daemon auth token written beside the socket, or None."""
|
|
468
|
+
token_file = unix_socket_path().parent / "alter-daemon-token"
|
|
469
|
+
try:
|
|
470
|
+
return token_file.read_text(encoding="utf-8").strip() or None
|
|
471
|
+
except OSError:
|
|
472
|
+
return None
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
async def _socket_request(request: dict[str, object]) -> dict[str, object]:
|
|
476
|
+
"""Connect to the daemon socket, authenticate, send one request, return the reply.
|
|
477
|
+
|
|
478
|
+
Mirrors the same auth handshake the local MCP bridge performs: read the
|
|
479
|
+
daemon token file, present it via ``{"method":"auth",...}``, then send the
|
|
480
|
+
request line and read one JSON reply line.
|
|
481
|
+
"""
|
|
482
|
+
socket = unix_socket_path()
|
|
483
|
+
reader, writer = await asyncio.open_unix_connection(str(socket))
|
|
484
|
+
try:
|
|
485
|
+
token = _daemon_token()
|
|
486
|
+
if token:
|
|
487
|
+
writer.write((json.dumps({"method": "auth", "token": token}) + "\n").encode("utf-8"))
|
|
488
|
+
await writer.drain()
|
|
489
|
+
auth_line = await asyncio.wait_for(reader.readline(), timeout=5.0)
|
|
490
|
+
auth_resp = json.loads(auth_line or b"{}")
|
|
491
|
+
if not auth_resp.get("ok"):
|
|
492
|
+
return {"ok": False, "error": f"auth failed: {auth_resp.get('error', 'unknown')}"}
|
|
493
|
+
writer.write((json.dumps(request) + "\n").encode("utf-8"))
|
|
494
|
+
await writer.drain()
|
|
495
|
+
# A send does a backend Durable-Object round-trip that can exceed a
|
|
496
|
+
# minute; wait above the daemon's own send timeout so the CLI does not
|
|
497
|
+
# give up before the daemon answers.
|
|
498
|
+
line = await asyncio.wait_for(reader.readline(), timeout=90.0)
|
|
499
|
+
if not line:
|
|
500
|
+
return {"ok": False, "error": "no response from daemon"}
|
|
501
|
+
reply = json.loads(line)
|
|
502
|
+
if not isinstance(reply, dict):
|
|
503
|
+
return {"ok": False, "error": "malformed reply from daemon"}
|
|
504
|
+
return reply
|
|
505
|
+
finally:
|
|
506
|
+
writer.close()
|
|
507
|
+
with contextlib.suppress(Exception):
|
|
508
|
+
await writer.wait_closed()
|
|
509
|
+
|
|
510
|
+
|
|
511
|
+
def cmd_send(args: argparse.Namespace) -> int:
|
|
512
|
+
"""Send a member-to-member message via the running daemon.
|
|
513
|
+
|
|
514
|
+
The daemon performs the authenticated send (it holds the session bearer
|
|
515
|
+
and signing key); this verb is a thin socket client. Default-closed: the
|
|
516
|
+
recipient must have granted you messaging permission first.
|
|
517
|
+
"""
|
|
518
|
+
socket = unix_socket_path()
|
|
519
|
+
if not socket.exists():
|
|
520
|
+
print(
|
|
521
|
+
"daemon socket absent - is alter-runtime running? (`alter-runtime status`)",
|
|
522
|
+
file=sys.stderr,
|
|
523
|
+
)
|
|
524
|
+
return 1
|
|
525
|
+
request: dict[str, object] = {"method": "send", "to": args.to, "body": args.body}
|
|
526
|
+
if args.content_type:
|
|
527
|
+
request["content_type"] = args.content_type
|
|
528
|
+
try:
|
|
529
|
+
reply = asyncio.run(_socket_request(request))
|
|
530
|
+
except (OSError, ConnectionError) as exc:
|
|
531
|
+
print(f"failed to reach daemon socket: {exc}", file=sys.stderr)
|
|
532
|
+
return 1
|
|
533
|
+
if reply.get("ok"):
|
|
534
|
+
mid = reply.get("message_id") or reply.get("id") or ""
|
|
535
|
+
print(f"sent to {args.to}" + (f" (message {mid})" if mid else ""))
|
|
536
|
+
return 0
|
|
537
|
+
print(f"send failed: {reply.get('error', 'unknown error')}", file=sys.stderr)
|
|
538
|
+
return 1
|
|
539
|
+
|
|
540
|
+
|
|
451
541
|
# ---------------------------------------------------------------------------
|
|
452
542
|
# Main dispatcher
|
|
453
543
|
# ---------------------------------------------------------------------------
|
|
@@ -461,6 +551,7 @@ COMMANDS: dict[str, object] = {
|
|
|
461
551
|
"daemon": cmd_daemon,
|
|
462
552
|
"query": cmd_query,
|
|
463
553
|
"ingest": cmd_ingest,
|
|
554
|
+
"send": cmd_send,
|
|
464
555
|
}
|
|
465
556
|
|
|
466
557
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""TokenUsageClient - authenticated HTTP client for the token-usage audit endpoint.
|
|
2
2
|
|
|
3
|
-
Posts batches of parsed token-usage events to the
|
|
3
|
+
Posts batches of parsed token-usage events to the ~alter backend.
|
|
4
4
|
|
|
5
5
|
Authentication
|
|
6
6
|
--------------
|
|
@@ -101,12 +101,12 @@ def _derive_host_id() -> str:
|
|
|
101
101
|
|
|
102
102
|
|
|
103
103
|
class TokenUsageClient:
|
|
104
|
-
"""Async client for the
|
|
104
|
+
"""Async client for the ~alter backend token-usage audit endpoint.
|
|
105
105
|
|
|
106
106
|
Parameters
|
|
107
107
|
----------
|
|
108
108
|
base_url:
|
|
109
|
-
Base URL of the
|
|
109
|
+
Base URL of the ~alter backend API. Reads ``ALTER_RUNTIME_API_BASE``
|
|
110
110
|
env var at construction if not explicitly provided; defaults to
|
|
111
111
|
``https://api.truealter.com``.
|
|
112
112
|
jwt_provider:
|
|
@@ -43,6 +43,7 @@ __all__ = [
|
|
|
43
43
|
"cache_dir",
|
|
44
44
|
"config_dir",
|
|
45
45
|
"data_dir",
|
|
46
|
+
"doctrine_projection_dir",
|
|
46
47
|
"enc_keyfile_path",
|
|
47
48
|
"enc_session_path",
|
|
48
49
|
"keypair_path",
|
|
@@ -111,6 +112,24 @@ def runtime_state_dir() -> Path:
|
|
|
111
112
|
return path
|
|
112
113
|
|
|
113
114
|
|
|
115
|
+
def doctrine_projection_dir() -> Path:
|
|
116
|
+
"""Return $XDG_DATA_HOME/alter/doctrine (default ~/.local/share/alter/doctrine).
|
|
117
|
+
|
|
118
|
+
The canonical home of the local read-only doctrine projection:
|
|
119
|
+
``personal.jsonl`` / ``collective.jsonl`` plus their per-scope checkpoint
|
|
120
|
+
files live here. Note the ``alter/`` namespace (NOT ``alter-runtime/``):
|
|
121
|
+
the projection is pinned under the broad ``alter`` data tree alongside the
|
|
122
|
+
CLI's other ``alter``-namespaced data, not the daemon's private
|
|
123
|
+
``alter-runtime`` subtree.
|
|
124
|
+
|
|
125
|
+
Created on first access with mode ``0o700`` so the projected doctrine (which
|
|
126
|
+
may carry personal-scope notes) cannot be world-read.
|
|
127
|
+
"""
|
|
128
|
+
path = _xdg_path("XDG_DATA_HOME", ".local/share") / "alter" / "doctrine"
|
|
129
|
+
path.mkdir(parents=True, exist_ok=True, mode=0o700)
|
|
130
|
+
return path
|
|
131
|
+
|
|
132
|
+
|
|
114
133
|
def keypair_path() -> Path:
|
|
115
134
|
"""Path to the device Ed25519 keypair (generated by `alter-runtime init`)."""
|
|
116
135
|
return config_dir() / "keypair.json"
|
|
@@ -173,6 +192,18 @@ class Session:
|
|
|
173
192
|
#: ``handle_mismatch``. ``None`` on legacy sessions (legacy fallback:
|
|
174
193
|
#: attempt to sign and let the server surface any mismatch).
|
|
175
194
|
signing_key_fingerprint: str | None = None
|
|
195
|
+
#: Long-lived member API key written by ``alter login``. This is the
|
|
196
|
+
#: credential the bridge sends as ``X-ALTER-API-Key`` for member-self tools
|
|
197
|
+
#: (doctrine, messaging, alignment, consent, attunement) - those tools
|
|
198
|
+
#: require ``member_self`` scope, which the 24h bearer JWT does NOT carry.
|
|
199
|
+
#: The doctrine-projection poller authenticates with this key; ``None`` when
|
|
200
|
+
#: the session predates the member-key cutover (the poller then soft-fails
|
|
201
|
+
#: the doctrine read and logs, never crashes).
|
|
202
|
+
member_api_key: str | None = None
|
|
203
|
+
#: The member's UUID (assessment-DB ``members.id``). Carried for parity with
|
|
204
|
+
#: the bridge; the backend resolves ownership from the validated credential,
|
|
205
|
+
#: not from this value. ``None`` on legacy sessions.
|
|
206
|
+
member_id: str | None = None
|
|
176
207
|
|
|
177
208
|
|
|
178
209
|
class SessionRef:
|
|
@@ -423,7 +454,7 @@ def _read_enc_store() -> str | None:
|
|
|
423
454
|
|
|
424
455
|
|
|
425
456
|
def _parse_consent_tier(value: Any) -> int:
|
|
426
|
-
"""Normalise ``consent_tier`` to the integer
|
|
457
|
+
"""Normalise ``consent_tier`` to the integer ~alter uses internally.
|
|
427
458
|
|
|
428
459
|
The CLI writes the consent tier as a string label
|
|
429
460
|
(``"L1"``, ``"L2"``, ``"L3"``, ``"L4"``) - see the SDK type
|
|
@@ -488,6 +519,8 @@ def _parse_session_text(raw_text: str) -> Session | None:
|
|
|
488
519
|
logged_in_at=raw.get("logged_in_at"),
|
|
489
520
|
signing_kid=raw.get("signing_kid") or None,
|
|
490
521
|
signing_key_fingerprint=raw.get("signing_key_fingerprint") or None,
|
|
522
|
+
member_api_key=raw.get("member_api_key") or None,
|
|
523
|
+
member_id=raw.get("member_id") or None,
|
|
491
524
|
)
|
|
492
525
|
except (KeyError, TypeError):
|
|
493
526
|
return None
|
|
@@ -788,16 +821,6 @@ class DaemonConfig:
|
|
|
788
821
|
#: ``ALTER_RUNTIME_CLAUDE_JSONL_WATCHER=1`` or direct config mutation.
|
|
789
822
|
enable_claude_jsonl_watcher: bool = False
|
|
790
823
|
|
|
791
|
-
# --- Working-tree write-event watcher --------------------------------
|
|
792
|
-
#: When ``True``, register :class:`WeaveWatcher` as a supervised
|
|
793
|
-
#: Component. It appends every observed working-tree file write to the
|
|
794
|
-
#: durable, append-only WriteEvent log (``alter_runtime.weave_log``).
|
|
795
|
-
#: Consent-gated: opt-in (default ``False``) because a durable per-handle
|
|
796
|
-
#: write log is more sensitive than the short-lived, expiring intent
|
|
797
|
-
#: coordination stream. Enable via ``ALTER_RUNTIME_WEAVE_WATCHER=1`` or
|
|
798
|
-
#: direct config mutation.
|
|
799
|
-
enable_weave_watcher: bool = False
|
|
800
|
-
|
|
801
824
|
# --- Hardening (frame-signature enforcement) --------------
|
|
802
825
|
#: When ``True``, every SSE frame received by :class:`DoSseSubscriber`
|
|
803
826
|
#: MUST carry a valid Ed25519 signature over the canonical (JCS-style
|
|
@@ -858,6 +881,24 @@ class DaemonConfig:
|
|
|
858
881
|
#: ``ALTER_RUNTIME_AUTOUPDATE_POLL_INTERVAL_SECONDS``.
|
|
859
882
|
autoupdate_poll_interval_seconds: int = 24 * 60 * 60
|
|
860
883
|
|
|
884
|
+
# --- Doctrine projection poller --------------------------------------
|
|
885
|
+
#: Master toggle for :class:`DoctrineProjectionPoller`. When enabled (the
|
|
886
|
+
#: default) the daemon maintains a local read-only JSONL projection of the
|
|
887
|
+
#: member's doctrine substrate per scope (``personal`` / ``collective``)
|
|
888
|
+
#: under :func:`doctrine_projection_dir`. Pull-mode (the backend emits no
|
|
889
|
+
#: event on a doctrine write): the poller calls ``alter_doctrine``'s cheap
|
|
890
|
+
#: ``summary`` verb each tick and only pulls the ``list`` delta when the
|
|
891
|
+
#: etag changes. It shares the read-side MCP wire contract (envelope +
|
|
892
|
+
#: ``mcp_fallback_endpoint`` + Bearer JWT) with the other pollers. Disable
|
|
893
|
+
#: via ``ALTER_RUNTIME_DOCTRINE_PROJECTION=0`` (e.g. air-gapped fleets that
|
|
894
|
+
#: don't want the periodic authenticated round-trip).
|
|
895
|
+
doctrine_projection_enabled: bool = True
|
|
896
|
+
#: Cadence in seconds for the doctrine summary poll. 60s is deliberately
|
|
897
|
+
#: slow: doctrine is a low-velocity living-state surface and the summary
|
|
898
|
+
#: call is cheap, so a steadily-fresh cache (not a tight loop) is the goal.
|
|
899
|
+
#: Override via ``ALTER_RUNTIME_DOCTRINE_PROJECTION_INTERVAL_SECONDS``.
|
|
900
|
+
doctrine_projection_interval_seconds: float = 60.0
|
|
901
|
+
|
|
861
902
|
@classmethod
|
|
862
903
|
def load(cls) -> DaemonConfig:
|
|
863
904
|
"""Load config from environment + YAML file + defaults.
|
|
@@ -1000,6 +1041,18 @@ class DaemonConfig:
|
|
|
1000
1041
|
except ValueError:
|
|
1001
1042
|
pass
|
|
1002
1043
|
|
|
1044
|
+
# --- Doctrine projection poller ----------------------------------
|
|
1045
|
+
_dp_flag = os.environ.get("ALTER_RUNTIME_DOCTRINE_PROJECTION", "").lower()
|
|
1046
|
+
if _dp_flag in ("0", "false", "no", "off"):
|
|
1047
|
+
config.doctrine_projection_enabled = False
|
|
1048
|
+
elif _dp_flag in ("1", "true", "yes", "on"):
|
|
1049
|
+
config.doctrine_projection_enabled = True
|
|
1050
|
+
if dp_interval := os.environ.get("ALTER_RUNTIME_DOCTRINE_PROJECTION_INTERVAL_SECONDS"):
|
|
1051
|
+
try:
|
|
1052
|
+
config.doctrine_projection_interval_seconds = float(dp_interval)
|
|
1053
|
+
except ValueError:
|
|
1054
|
+
pass # Ignore malformed override; default stands.
|
|
1055
|
+
|
|
1003
1056
|
if level := os.environ.get("ALTER_RUNTIME_LOG_LEVEL"):
|
|
1004
1057
|
config.log_level = level
|
|
1005
1058
|
if socket := os.environ.get("ALTER_RUNTIME_SOCKET"):
|
|
@@ -1009,14 +1062,6 @@ class DaemonConfig:
|
|
|
1009
1062
|
config.enable_claude_jsonl_watcher = True
|
|
1010
1063
|
elif _cjw_flag in ("0", "false", "no", "off"):
|
|
1011
1064
|
config.enable_claude_jsonl_watcher = False
|
|
1012
|
-
# Working-tree write-event watcher. Consent-gated opt-in: the watcher
|
|
1013
|
-
# only writes a local append-only log and never touches frame-signature
|
|
1014
|
-
# enforcement, so a plain env toggle is safe.
|
|
1015
|
-
_weave_flag = os.environ.get("ALTER_RUNTIME_WEAVE_WATCHER", "").lower()
|
|
1016
|
-
if _weave_flag in ("1", "true", "yes", "on"):
|
|
1017
|
-
config.enable_weave_watcher = True
|
|
1018
|
-
elif _weave_flag in ("0", "false", "no", "off"):
|
|
1019
|
-
config.enable_weave_watcher = False
|
|
1020
1065
|
# Post-pentest-2026-04-26: default is True. Env var still toggles -
|
|
1021
1066
|
# explicit "0"/"false"/"no"/"off" disables (migration-window opt-out),
|
|
1022
1067
|
# everything else (including unset) keeps the default True.
|
|
@@ -18,7 +18,7 @@ Implements the following install-transparency countermeasures:
|
|
|
18
18
|
tombstoned, the install is refused with an actionable message. The user
|
|
19
19
|
can explicitly clear the tombstone with ``--reinstall``.
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
~Alter does not use a silent-install pattern.
|
|
22
22
|
"""
|
|
23
23
|
|
|
24
24
|
from __future__ import annotations
|
|
@@ -88,7 +88,11 @@ def _sha256_file(path: Path) -> str:
|
|
|
88
88
|
# ---------------------------------------------------------------------------
|
|
89
89
|
|
|
90
90
|
|
|
91
|
-
def enumerate_artefacts(
|
|
91
|
+
def enumerate_artefacts(
|
|
92
|
+
binary_path: Path | None = None,
|
|
93
|
+
*,
|
|
94
|
+
binary_sigstore_bundle_url: str | None = None,
|
|
95
|
+
) -> list[dict[str, Any]]:
|
|
92
96
|
"""Return a list of artefacts that ``alter-runtime start`` will write.
|
|
93
97
|
|
|
94
98
|
Each entry is a dict::
|
|
@@ -102,13 +106,21 @@ def enumerate_artefacts(binary_path: Path | None = None) -> list[dict[str, Any]]
|
|
|
102
106
|
|
|
103
107
|
For v1 the set is:
|
|
104
108
|
|
|
105
|
-
* The systemd user unit *or* launchd plist (platform-appropriate).
|
|
109
|
+
* The systemd user unit *or* launchd plist (platform-appropriate). This is
|
|
110
|
+
a locally-rendered template, not a signed release artefact, so its
|
|
111
|
+
``sigstore_bundle_url`` is always ``None``.
|
|
106
112
|
* The ``alter-runtime`` binary itself (resolved via
|
|
107
113
|
:func:`~alter_runtime.service_install.resolve_runtime_binary`, with
|
|
108
114
|
a graceful fallback to ``None`` if the binary is not yet installed).
|
|
109
115
|
|
|
110
|
-
|
|
111
|
-
|
|
116
|
+
Parameters
|
|
117
|
+
----------
|
|
118
|
+
binary_sigstore_bundle_url:
|
|
119
|
+
The sigstore bundle URL for the runtime binary, read from the release
|
|
120
|
+
manifest entry for the running platform. When supplied it is stamped
|
|
121
|
+
onto the binary artefact so the consent screen and the post-install
|
|
122
|
+
manifest record the signature provenance. ``None`` (the default) means
|
|
123
|
+
no bundle is known for this install path, e.g. a local source build.
|
|
112
124
|
"""
|
|
113
125
|
from alter_runtime.service_install import (
|
|
114
126
|
current_platform,
|
|
@@ -154,7 +166,7 @@ def enumerate_artefacts(binary_path: Path | None = None) -> list[dict[str, Any]]
|
|
|
154
166
|
"path": resolved_binary,
|
|
155
167
|
"size_bytes": bin_size,
|
|
156
168
|
"sha256": _sha256_file(resolved_binary),
|
|
157
|
-
"sigstore_bundle_url":
|
|
169
|
+
"sigstore_bundle_url": binary_sigstore_bundle_url,
|
|
158
170
|
}
|
|
159
171
|
)
|
|
160
172
|
|
|
@@ -177,7 +189,12 @@ def _human_size(size_bytes: int | None) -> str:
|
|
|
177
189
|
return f"{size_bytes / (1024 * 1024):.1f} MiB"
|
|
178
190
|
|
|
179
191
|
|
|
180
|
-
def print_consent_screen(
|
|
192
|
+
def print_consent_screen(
|
|
193
|
+
artefacts: list[dict[str, Any]],
|
|
194
|
+
uninstall_cmd: str,
|
|
195
|
+
*,
|
|
196
|
+
verified: bool = False,
|
|
197
|
+
) -> None:
|
|
181
198
|
"""Render the pre-install consent screen to stdout.
|
|
182
199
|
|
|
183
200
|
Format::
|
|
@@ -189,24 +206,38 @@ def print_consent_screen(artefacts: list[dict[str, Any]], uninstall_cmd: str) ->
|
|
|
189
206
|
...
|
|
190
207
|
Sigstore-keyless verification: UNVERIFIED
|
|
191
208
|
To remove everything: alter-runtime stop
|
|
209
|
+
|
|
210
|
+
Parameters
|
|
211
|
+
----------
|
|
212
|
+
verified:
|
|
213
|
+
Whether an actual sigstore-verified verdict has been obtained for the
|
|
214
|
+
release artefacts (e.g. from ``alter runtime verify`` / the
|
|
215
|
+
verified-apply path). The headline status reads ``VERIFIED`` ONLY when
|
|
216
|
+
this is ``True``; it is ``UNVERIFIED`` otherwise. A bundle URL being
|
|
217
|
+
merely *present* on an artefact is no longer treated as proof of
|
|
218
|
+
verification: an unverified install must never claim it passed.
|
|
192
219
|
"""
|
|
193
220
|
print("ALTER Runtime - install consent")
|
|
194
221
|
print("================================")
|
|
195
222
|
print("The following artefacts will be written to your system:")
|
|
196
223
|
|
|
197
|
-
any_sigstore = False
|
|
198
224
|
for artefact in artefacts:
|
|
199
225
|
path = artefact["path"]
|
|
200
226
|
size_str = _human_size(artefact.get("size_bytes"))
|
|
201
227
|
sha = artefact.get("sha256") or ""
|
|
202
228
|
short_sha = sha[:12] if sha else "(not yet on disk)"
|
|
203
229
|
bundle_url = artefact.get("sigstore_bundle_url")
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
230
|
+
# Per-artefact label is driven by the verdict, not URL presence: a
|
|
231
|
+
# bundle URL with no verified verdict is "pending", never "verified".
|
|
232
|
+
if verified and bundle_url:
|
|
233
|
+
sig_label = "verified"
|
|
234
|
+
elif bundle_url:
|
|
235
|
+
sig_label = "pending"
|
|
236
|
+
else:
|
|
237
|
+
sig_label = "unverified"
|
|
207
238
|
print(f" {path} ({size_str}) sha256:{short_sha} sigstore: {sig_label}")
|
|
208
239
|
|
|
209
|
-
sig_status = "
|
|
240
|
+
sig_status = "VERIFIED" if verified else "UNVERIFIED"
|
|
210
241
|
print(f"Sigstore-keyless verification: {sig_status}")
|
|
211
242
|
print(f"To remove everything: {uninstall_cmd}")
|
|
212
243
|
print()
|
|
@@ -309,6 +309,7 @@ async def run_daemon(config: DaemonConfig | None = None) -> None:
|
|
|
309
309
|
AttunementRefresher,
|
|
310
310
|
CacheWriter,
|
|
311
311
|
CeremonyEchoWriter,
|
|
312
|
+
DoctrineProjectionPoller,
|
|
312
313
|
DoSseSubscriber,
|
|
313
314
|
EventBus,
|
|
314
315
|
InboxWriter,
|
|
@@ -352,6 +353,20 @@ async def run_daemon(config: DaemonConfig | None = None) -> None:
|
|
|
352
353
|
if config.attunement_refresh_enabled:
|
|
353
354
|
supervisor.register(AttunementRefresher(config, session, event_bus))
|
|
354
355
|
|
|
356
|
+
# DoctrineProjectionPoller maintains a local read-only JSONL projection
|
|
357
|
+
# of the member's doctrine substrate per scope (personal/collective)
|
|
358
|
+
# under ~/.local/share/alter/doctrine. Pull-mode: the backend emits no
|
|
359
|
+
# event on a doctrine write, so it polls the cheap ``alter_doctrine``
|
|
360
|
+
# ``summary`` verb each tick and only pulls the ``list`` delta when the
|
|
361
|
+
# etag changes. Shares the read-side MCP wire contract (envelope +
|
|
362
|
+
# endpoint + JWT) with the fallback / attunement pollers; it does NOT
|
|
363
|
+
# touch do_sse's connect/auth/mint path. The projection file is a
|
|
364
|
+
# rolling, rebuildable-from-substrate cache. Pass the session_ref holder
|
|
365
|
+
# when available so the poller reads the live JWT after a proactive
|
|
366
|
+
# rotation. Disable via ALTER_RUNTIME_DOCTRINE_PROJECTION=0.
|
|
367
|
+
if config.doctrine_projection_enabled:
|
|
368
|
+
supervisor.register(DoctrineProjectionPoller(config, session_ref or session))
|
|
369
|
+
|
|
355
370
|
# SessionRefresher proactively shells ``alter creds refresh --force``
|
|
356
371
|
# a configurable lead time before the access JWT expires. After a
|
|
357
372
|
# successful rotation it updates the shared ``session_ref`` holder so
|
|
@@ -485,8 +500,7 @@ async def run_daemon(config: DaemonConfig | None = None) -> None:
|
|
|
485
500
|
|
|
486
501
|
# WorktreeWatcher: working-tree filesystem watcher. Observes the
|
|
487
502
|
# repo working tree and publishes local.signal kind=worktree_edit
|
|
488
|
-
# on every file save, covering editors that lack a hook
|
|
489
|
-
# second producer for the write log.
|
|
503
|
+
# on every file save, covering editors that lack a hook.
|
|
490
504
|
supervisor.register(WorktreeWatcher(config, event_bus))
|
|
491
505
|
|
|
492
506
|
# WeaveIntentWriter: subscribes to local.signal worktree_edit events
|
|
@@ -499,26 +513,13 @@ async def run_daemon(config: DaemonConfig | None = None) -> None:
|
|
|
499
513
|
|
|
500
514
|
# Claude Code JSONL token-usage watcher - opt-in via config flag.
|
|
501
515
|
# Tails the Claude Code projects transcripts and posts token-usage events to
|
|
502
|
-
# the
|
|
516
|
+
# the ~alter backend audit endpoint. Disabled by default; enable via
|
|
503
517
|
# ALTER_RUNTIME_CLAUDE_JSONL_WATCHER=1 or direct config mutation.
|
|
504
518
|
if config.enable_claude_jsonl_watcher:
|
|
505
519
|
from alter_runtime.adapters.claude_jsonl_watcher import ClaudeJsonlWatcher
|
|
506
520
|
|
|
507
521
|
supervisor.register(ClaudeJsonlWatcher(config, event_bus))
|
|
508
522
|
|
|
509
|
-
# Working-tree write-log watcher: opt-in via config flag or
|
|
510
|
-
# ALTER_RUNTIME_WEAVE_WATCHER=1. Observes working-tree
|
|
511
|
-
# IN_CLOSE_WRITE events into the durable, append-only write log
|
|
512
|
-
# (~/.local/share/alter-runtime/weave-log.jsonl) that the
|
|
513
|
-
# read-back MCP surface reads back. No TTL, no rotation-to-deletion.
|
|
514
|
-
# The TTL-bounded weave-intent.jsonl above is the separate
|
|
515
|
-
# write-log plane. The write log is an append-only overlay on git,
|
|
516
|
-
# never a replacement.
|
|
517
|
-
if config.enable_weave_watcher:
|
|
518
|
-
from alter_runtime.adapters import WeaveWatcher
|
|
519
|
-
|
|
520
|
-
supervisor.register(WeaveWatcher(config, event_bus))
|
|
521
|
-
|
|
522
523
|
# --- External adapter plugins (entry-point discovery) -------------
|
|
523
524
|
# Out-of-tree adapters register under the ``alter_runtime.adapters``
|
|
524
525
|
# entry-point group and are discovered here rather than imported by
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
"""HTTP header helpers for upstream calls into the
|
|
1
|
+
"""HTTP header helpers for upstream calls into the ~alter backend.
|
|
2
2
|
|
|
3
3
|
Every authenticated request carries an ``Authorization: Bearer <jwt>``
|
|
4
4
|
header set explicitly by the calling subscriber; that member bearer is
|