agent-letter-bridge 0.3.0__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.
- agent_letter_bridge-0.3.0/.githooks/commit-msg +4 -0
- agent_letter_bridge-0.3.0/.githooks/pre-commit +4 -0
- agent_letter_bridge-0.3.0/.github/workflows/ci.yml +59 -0
- agent_letter_bridge-0.3.0/.gitignore +5 -0
- agent_letter_bridge-0.3.0/CHANGELOG.md +300 -0
- agent_letter_bridge-0.3.0/CONTRIBUTING.md +83 -0
- agent_letter_bridge-0.3.0/INSTALL.md +496 -0
- agent_letter_bridge-0.3.0/LICENSE +21 -0
- agent_letter_bridge-0.3.0/PKG-INFO +284 -0
- agent_letter_bridge-0.3.0/README.md +252 -0
- agent_letter_bridge-0.3.0/SECURITY.md +38 -0
- agent_letter_bridge-0.3.0/docs/COMPARE.md +176 -0
- agent_letter_bridge-0.3.0/docs/adapter-contract.md +39 -0
- agent_letter_bridge-0.3.0/docs/agent-install.md +481 -0
- agent_letter_bridge-0.3.0/docs/agent-setup.md +152 -0
- agent_letter_bridge-0.3.0/docs/architecture.md +87 -0
- agent_letter_bridge-0.3.0/docs/invariants.md +166 -0
- agent_letter_bridge-0.3.0/docs/operations.md +412 -0
- agent_letter_bridge-0.3.0/docs/retention.md +134 -0
- agent_letter_bridge-0.3.0/docs/threat-model.md +81 -0
- agent_letter_bridge-0.3.0/examples/README.md +4 -0
- agent_letter_bridge-0.3.0/examples/launchd.plist +119 -0
- agent_letter_bridge-0.3.0/examples/systemd.user.service +41 -0
- agent_letter_bridge-0.3.0/pyproject.toml +34 -0
- agent_letter_bridge-0.3.0/scripts/check_conformance_vendor.py +59 -0
- agent_letter_bridge-0.3.0/scripts/deps_check.py +63 -0
- agent_letter_bridge-0.3.0/scripts/editorial_check.py +125 -0
- agent_letter_bridge-0.3.0/scripts/mutation_check.py +1165 -0
- agent_letter_bridge-0.3.0/scripts/privacy_scan.py +126 -0
- agent_letter_bridge-0.3.0/src/alb/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/__main__.py +11 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/README.md +7 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/cmux/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/cmux/transport.py +74 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/telegram/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/telegram/api.py +422 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/tmux/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/adapters/tmux/transport.py +57 -0
- agent_letter_bridge-0.3.0/src/alb/allowlist/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/allowlist/gate.py +57 -0
- agent_letter_bridge-0.3.0/src/alb/bridge/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/bridge/run.py +383 -0
- agent_letter_bridge-0.3.0/src/alb/bridge/singleton.py +161 -0
- agent_letter_bridge-0.3.0/src/alb/canary/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/canary/probe.py +82 -0
- agent_letter_bridge-0.3.0/src/alb/cli.py +870 -0
- agent_letter_bridge-0.3.0/src/alb/doctor/README.md +22 -0
- agent_letter_bridge-0.3.0/src/alb/doctor/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/doctor/checks.py +333 -0
- agent_letter_bridge-0.3.0/src/alb/grant/__init__.py +1 -0
- agent_letter_bridge-0.3.0/src/alb/grant/store.py +383 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/__init__.py +1 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/budget.py +130 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/compose.py +49 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/destinations.py +52 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/durable.py +80 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/ids.py +85 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/reservation.py +71 -0
- agent_letter_bridge-0.3.0/src/alb/initiate/send.py +275 -0
- agent_letter_bridge-0.3.0/src/alb/letter/README.md +7 -0
- agent_letter_bridge-0.3.0/src/alb/letter/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/letter/store.py +303 -0
- agent_letter_bridge-0.3.0/src/alb/media/__init__.py +1 -0
- agent_letter_bridge-0.3.0/src/alb/media/attach.py +64 -0
- agent_letter_bridge-0.3.0/src/alb/media/inspect.py +127 -0
- agent_letter_bridge-0.3.0/src/alb/media/store.py +317 -0
- agent_letter_bridge-0.3.0/src/alb/msgindex.py +53 -0
- agent_letter_bridge-0.3.0/src/alb/notifier/README.md +5 -0
- agent_letter_bridge-0.3.0/src/alb/notifier/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/notifier/ring.py +43 -0
- agent_letter_bridge-0.3.0/src/alb/outbound/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/outbound/store.py +411 -0
- agent_letter_bridge-0.3.0/src/alb/poller/README.md +8 -0
- agent_letter_bridge-0.3.0/src/alb/poller/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/poller/loop.py +319 -0
- agent_letter_bridge-0.3.0/src/alb/retrieval.py +121 -0
- agent_letter_bridge-0.3.0/src/alb/send/README.md +7 -0
- agent_letter_bridge-0.3.0/src/alb/send/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/send/reply.py +407 -0
- agent_letter_bridge-0.3.0/src/alb/setup/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/setup/discover.py +110 -0
- agent_letter_bridge-0.3.0/src/alb/setup/wizard.py +866 -0
- agent_letter_bridge-0.3.0/src/alb/watchdog/README.md +6 -0
- agent_letter_bridge-0.3.0/src/alb/watchdog/__init__.py +0 -0
- agent_letter_bridge-0.3.0/src/alb/watchdog/health.py +158 -0
- agent_letter_bridge-0.3.0/tests/fake_platform.py +49 -0
- agent_letter_bridge-0.3.0/tests/test_allowlist.py +67 -0
- agent_letter_bridge-0.3.0/tests/test_bridge.py +304 -0
- agent_letter_bridge-0.3.0/tests/test_canary.py +81 -0
- agent_letter_bridge-0.3.0/tests/test_check_command.py +219 -0
- agent_letter_bridge-0.3.0/tests/test_cmux_adapter.py +51 -0
- agent_letter_bridge-0.3.0/tests/test_conformance_vendor.py +77 -0
- agent_letter_bridge-0.3.0/tests/test_cycle_report.py +124 -0
- agent_letter_bridge-0.3.0/tests/test_deps_gate.py +81 -0
- agent_letter_bridge-0.3.0/tests/test_doctor.py +63 -0
- agent_letter_bridge-0.3.0/tests/test_doctor_probe.py +375 -0
- agent_letter_bridge-0.3.0/tests/test_editorial_check.py +162 -0
- agent_letter_bridge-0.3.0/tests/test_envelope.py +103 -0
- agent_letter_bridge-0.3.0/tests/test_grant.py +690 -0
- agent_letter_bridge-0.3.0/tests/test_heartbeat_states.py +69 -0
- agent_letter_bridge-0.3.0/tests/test_initiate.py +467 -0
- agent_letter_bridge-0.3.0/tests/test_letter.py +254 -0
- agent_letter_bridge-0.3.0/tests/test_logging.py +72 -0
- agent_letter_bridge-0.3.0/tests/test_mail_root.py +380 -0
- agent_letter_bridge-0.3.0/tests/test_media.py +629 -0
- agent_letter_bridge-0.3.0/tests/test_mutation_gate.py +73 -0
- agent_letter_bridge-0.3.0/tests/test_notifier.py +66 -0
- agent_letter_bridge-0.3.0/tests/test_notifier_bounds.py +140 -0
- agent_letter_bridge-0.3.0/tests/test_offset_state_machine.py +139 -0
- agent_letter_bridge-0.3.0/tests/test_outbound.py +521 -0
- agent_letter_bridge-0.3.0/tests/test_permissions.py +64 -0
- agent_letter_bridge-0.3.0/tests/test_poll_backoff.py +252 -0
- agent_letter_bridge-0.3.0/tests/test_poller.py +311 -0
- agent_letter_bridge-0.3.0/tests/test_privacy_scan.py +100 -0
- agent_letter_bridge-0.3.0/tests/test_resident_launch.py +240 -0
- agent_letter_bridge-0.3.0/tests/test_resume_cli.py +83 -0
- agent_letter_bridge-0.3.0/tests/test_retrieval.py +222 -0
- agent_letter_bridge-0.3.0/tests/test_send.py +711 -0
- agent_letter_bridge-0.3.0/tests/test_setup.py +1420 -0
- agent_letter_bridge-0.3.0/tests/test_setup_discover.py +152 -0
- agent_letter_bridge-0.3.0/tests/test_singleton.py +50 -0
- agent_letter_bridge-0.3.0/tests/test_stop.py +402 -0
- agent_letter_bridge-0.3.0/tests/test_telegram_adapter.py +391 -0
- agent_letter_bridge-0.3.0/tests/test_tmux_adapter.py +59 -0
- agent_letter_bridge-0.3.0/tests/test_w2_identity_threading.py +208 -0
- agent_letter_bridge-0.3.0/tests/test_wake_check.py +184 -0
- agent_letter_bridge-0.3.0/tests/test_watchdog.py +67 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-source.json +8 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/README.md +22 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/SHA256SUMS +6 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/accepted/basic.md +13 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/accepted/unicode-body-key.md +15 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/cases.tsv +6 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/rejected/one-fence.md +9 -0
- agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/rejected/trailing-space-fence.md +10 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
pull_request:
|
|
6
|
+
|
|
7
|
+
jobs:
|
|
8
|
+
check:
|
|
9
|
+
runs-on: ubuntu-latest
|
|
10
|
+
steps:
|
|
11
|
+
- uses: actions/checkout@v4
|
|
12
|
+
|
|
13
|
+
- uses: actions/setup-python@v5
|
|
14
|
+
with:
|
|
15
|
+
python-version: '3.11'
|
|
16
|
+
|
|
17
|
+
# Runs on EVERY commit. A private repo's whole history becomes public if it
|
|
18
|
+
# is ever flipped, so this is enforcement, not a pre-release checkpoint.
|
|
19
|
+
- name: Privacy scan
|
|
20
|
+
run: python3 scripts/privacy_scan.py
|
|
21
|
+
# Binds TREE CONTENT on every commit path, web edits included. Commit
|
|
22
|
+
# MESSAGES are checked by the commit-msg hook only - a hook-less path
|
|
23
|
+
# can land a message the policy forbids, and reviewers read for that.
|
|
24
|
+
- name: Editorial tripwire
|
|
25
|
+
run: python3 scripts/editorial_check.py --worktree
|
|
26
|
+
|
|
27
|
+
# Asserts the PROPERTY (no runtime dependencies) rather than the
|
|
28
|
+
# absence of packaging. A build backend is install-time and never
|
|
29
|
+
# imported by the running daemon; a runtime dependency is. The gate
|
|
30
|
+
# explains that distinction so nobody bans pyproject.toml again.
|
|
31
|
+
- name: Dependency gate
|
|
32
|
+
run: python3 scripts/deps_check.py
|
|
33
|
+
|
|
34
|
+
# W5 vendors reviewed Letterbox conformance bytes. CI verifies the local
|
|
35
|
+
# manifest and inventory only; ordinary CI never fetches fixture data.
|
|
36
|
+
- name: Conformance fixture snapshot
|
|
37
|
+
run: python3 scripts/check_conformance_vendor.py
|
|
38
|
+
|
|
39
|
+
# A wheel that imports cleanly is proved by INSTALLING it, not by the
|
|
40
|
+
# suite - which runs from the source tree so mutations stay visible.
|
|
41
|
+
- name: Install smoke test
|
|
42
|
+
run: |
|
|
43
|
+
python3 -m venv /tmp/albvenv
|
|
44
|
+
/tmp/albvenv/bin/pip install --quiet .
|
|
45
|
+
/tmp/albvenv/bin/alb --help > /dev/null
|
|
46
|
+
/tmp/albvenv/bin/python -m alb --help > /dev/null
|
|
47
|
+
echo "wheel installs and both entry points run"
|
|
48
|
+
|
|
49
|
+
- name: Tests
|
|
50
|
+
run: python3 -m unittest discover -s tests -v
|
|
51
|
+
env:
|
|
52
|
+
PYTHONPATH: src
|
|
53
|
+
|
|
54
|
+
# A test that still passes with an invariant disabled is coverage without
|
|
55
|
+
# proof. This breaks each invariant in turn and asserts the suite goes red.
|
|
56
|
+
- name: Mutation gate
|
|
57
|
+
run: python3 scripts/mutation_check.py
|
|
58
|
+
env:
|
|
59
|
+
PYTHONPATH: src
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project are documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and
|
|
6
|
+
this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.3.0] — 2026-09-10
|
|
11
|
+
|
|
12
|
+
Two capabilities that were deliberately absent in the reply-only bridge, each
|
|
13
|
+
added without loosening the guarantee that bounded it.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
**Initiating send, gated by an explicit, revocable grant.** Until now the
|
|
18
|
+
bridge could only answer a letter it already held — there was no way for an
|
|
19
|
+
agent to start a conversation to a phone, by design. It can now, but only to a
|
|
20
|
+
destination the operator has explicitly authorised with a *grant*
|
|
21
|
+
(`alb --grant-create --as <owner> --platform telegram --chat-id <id>`), and a
|
|
22
|
+
grant can be listed (`--grant-list`) and revoked (`--grant-disable`) at any
|
|
23
|
+
time. With no grant, an initiating send fails closed. This narrows the old
|
|
24
|
+
flat "no first-send command exists" guarantee to one that is still a guarantee:
|
|
25
|
+
the bridge cannot originate contact anywhere the operator has not named, and a
|
|
26
|
+
destination can be cut off without touching the code.
|
|
27
|
+
|
|
28
|
+
**Initiating sends are budgeted, not open-ended.** Even with a grant an agent
|
|
29
|
+
cannot flood a phone. Each destination admits only a small, refundable daily
|
|
30
|
+
and hourly allowance with a cap on messages in flight; a send refused for
|
|
31
|
+
budget is refused before anything leaves the machine. A send is idempotent on a
|
|
32
|
+
caller-supplied intent id (`alb --send --id <intent>`), so a crash or a retry
|
|
33
|
+
settles as exactly one message — or, when the outcome is genuinely unknown, as
|
|
34
|
+
an explicitly ambiguous one that consumes its budget rather than risking a
|
|
35
|
+
silent double-send.
|
|
36
|
+
|
|
37
|
+
**Images, in and out.** An outbound reply or send may carry a photo from an
|
|
38
|
+
allowlisted directory (`<root>/state/attach-roots.json` is a JSON object
|
|
39
|
+
`{"roots": ["/absolute/dir", ...]}`; default deny), checked for real file type
|
|
40
|
+
and sane dimensions before it leaves. An inbound photo becomes part of the letter as an opaque media
|
|
41
|
+
reference plus its metadata — never a raw filesystem path, chat id, or original
|
|
42
|
+
filename — so the archive gains the fact of the image without leaking where it
|
|
43
|
+
lives. Photos stored before 0.3.0 have no `.ready` marker: after upgrade, stop
|
|
44
|
+
the bridge and run `alb --grandfather-media --root <root>` once per seat.
|
|
45
|
+
An unreadable existing media root is a failed grandfather (exit 1), not a
|
|
46
|
+
successful empty run. The command takes the same root lock as the bridge:
|
|
47
|
+
if a bridge is running it refuses (naming that pid) and writes nothing.
|
|
48
|
+
|
|
49
|
+
## [0.2.5] — 2026-09-07
|
|
50
|
+
|
|
51
|
+
Both of these were found by the first installation done for real, on a seat
|
|
52
|
+
that had never had one, after the whole install path had already been audited
|
|
53
|
+
against fixtures.
|
|
54
|
+
|
|
55
|
+
**`init` could start a bridge from a different installation than the one that
|
|
56
|
+
ran it.** The autostart command was the bare word `alb`, handed to a new
|
|
57
|
+
shell, which resolves it from `PATH`. An operator who deliberately installed
|
|
58
|
+
into a dedicated environment, and ran `init` from it, got a resident running
|
|
59
|
+
whichever copy `PATH` found first — a different installation, possibly a
|
|
60
|
+
different version, and on a machine with several bridges, one shared with
|
|
61
|
+
another of them. The command now names the running installation absolutely:
|
|
62
|
+
the console script beside the interpreter, quoted as one argv so a path
|
|
63
|
+
containing a space survives the shell — a root with a space was rejected by
|
|
64
|
+
argument parsing, and an interpreter with one failed to execute at all, both
|
|
65
|
+
ordinary where a home directory carries somebody's name. What is printed and
|
|
66
|
+
what the pane runs are still the same bytes.
|
|
67
|
+
|
|
68
|
+
The bridge is started as `<interpreter> -I -m alb`. The isolation flag is the
|
|
69
|
+
guarantee, not the absolute path: without it the launched process still honours
|
|
70
|
+
`PYTHONPATH`, the user site directory and its own working directory, any of
|
|
71
|
+
which can supply a different `alb`. The console script was friendlier to read
|
|
72
|
+
and could not carry this — it is a shebang into an interpreter that will honour
|
|
73
|
+
all three.
|
|
74
|
+
|
|
75
|
+
The check is one of **identity, not availability**, and it is made under the
|
|
76
|
+
same isolation the launch uses. Asking whether an
|
|
77
|
+
interpreter can import `alb` establishes that some copy is reachable, not that
|
|
78
|
+
it is the one running setup — an environment holding an older copy answers yes,
|
|
79
|
+
and the command then starts that older copy. The interpreter is asked where its
|
|
80
|
+
`alb` actually comes from, and the answer is compared with the code running
|
|
81
|
+
`init`. Version strings are not compared, because two builds can agree about
|
|
82
|
+
their name and differ. Proving identity under conditions the launch does not
|
|
83
|
+
apply proves nothing about the launch: an approval obtained with `PYTHONPATH`
|
|
84
|
+
stripped, for a command that honours it, describes a context that never
|
|
85
|
+
happens.
|
|
86
|
+
|
|
87
|
+
Where no command can be named that a fresh shell would resolve to this
|
|
88
|
+
installation, `init` declines to start one and says why. Offering
|
|
89
|
+
`<interpreter> -m alb` instead does not work for a source checkout: that
|
|
90
|
+
process can import the package only because of the path it was started with,
|
|
91
|
+
and a new shell inherits none of it. An autostart that reliably fails is worse
|
|
92
|
+
than no offer.
|
|
93
|
+
|
|
94
|
+
**The start warning reads the allowlist that was saved**, not the answers
|
|
95
|
+
given during setup. On a re-run those differ, and both directions were wrong:
|
|
96
|
+
an existing deny-all file is kept — correctly, nothing is clobbered — but if
|
|
97
|
+
setup had read a chat id it did not save, it reported delivery as possible and
|
|
98
|
+
offered to start; while an existing, populated allowlist was warned about as
|
|
99
|
+
deny-all. The gate on disk after keep-or-write is the one that gets read, and a
|
|
100
|
+
file that cannot be parsed counts as denying.
|
|
101
|
+
|
|
102
|
+
The question is now asked of the allowlist itself rather than answered beside
|
|
103
|
+
it. Setup used its own truthiness test, which read `{"chats": "42"}` and
|
|
104
|
+
`{"chats": 42}` as permissive where the gate requires a non-empty list — so the
|
|
105
|
+
gate denied everyone while setup offered to start. The gate was never weakened;
|
|
106
|
+
there were two definitions of an allowlist, and now there is one.
|
|
107
|
+
|
|
108
|
+
**Starting a bridge with a deny-all allowlist is now a deliberate answer
|
|
109
|
+
rather than the default.** The gate is unchanged and still denies everyone
|
|
110
|
+
until a chat id is added — that part worked exactly as intended, which is what
|
|
111
|
+
made it confusing: a bridge reported as running, messages sent, nothing
|
|
112
|
+
delivered, and no error anywhere to explain it. The offer now states plainly
|
|
113
|
+
that nothing will arrive, and defaults to no. An operator who means it can
|
|
114
|
+
still say yes.
|
|
115
|
+
|
|
116
|
+
## [0.2.4] — 2026-09-06
|
|
117
|
+
|
|
118
|
+
Two things a person installing this would have been misled by.
|
|
119
|
+
|
|
120
|
+
**The setup wizard told a correct install it was not an install.** It gated
|
|
121
|
+
on a terminal pane being chosen, but an integrated install does not ring
|
|
122
|
+
through a pane — it rings through the letterbox's own doorbell, and the
|
|
123
|
+
runtime asks for a pane only when there isn't one. So a correct install was
|
|
124
|
+
told "a poller with nothing to ping is not an install" and refused a start,
|
|
125
|
+
while working deployments ran exactly that configuration with a working
|
|
126
|
+
doorbell. The wizard now recognises the helper as the ring it is, and stops
|
|
127
|
+
asking integrated installs to pin a pane id that nothing ever reads.
|
|
128
|
+
|
|
129
|
+
**The single-consumer probe now compares bots rather than process names.**
|
|
130
|
+
The platform allows one consumer per token, so what matters is which bot a
|
|
131
|
+
process holds. The probe matched on the executable's name alone, and so
|
|
132
|
+
reported other relays that hold different bots and cannot compete. It now
|
|
133
|
+
reads the identifying half of each candidate's token — never the secret half,
|
|
134
|
+
which is dropped before any value is returned — and reports a process unless
|
|
135
|
+
its bot is positively known to be a different one. An unreadable config is
|
|
136
|
+
reported rather than cleared: being unable to prove a conflict is not the
|
|
137
|
+
same as proving there is none.
|
|
138
|
+
|
|
139
|
+
**A local network fault was reported as a Telegram outage.** Every transient
|
|
140
|
+
that was not a rate limit was recorded as `upstream_5xx`, so a dropped wifi
|
|
141
|
+
connection sent operators looking for a platform incident. `network` was
|
|
142
|
+
already in the vocabulary and nothing emitted it. It does now, matched on the
|
|
143
|
+
message prefix so a gateway error that merely contains the word cannot claim
|
|
144
|
+
the code.
|
|
145
|
+
|
|
146
|
+
> 0.2.2 and 0.2.3 were never published. Upgrading from 0.2.1 brings all three
|
|
147
|
+
> sets of changes at once, which is why the notes below cover more than one
|
|
148
|
+
> version.
|
|
149
|
+
|
|
150
|
+
## [0.2.3] — unreleased
|
|
151
|
+
|
|
152
|
+
The relay stops needing a person to notice it died - it needs an agent that wakes and asks.
|
|
153
|
+
|
|
154
|
+
An agent asks `alb --check` when it wakes: exit 0 nothing to do, 2 the
|
|
155
|
+
relay has been silent past a policy threshold, which is grounds to restart
|
|
156
|
+
it rather than proof it is dead, and here is how; 3 something a restart
|
|
157
|
+
will not fix. The allowance follows the state rather than one number for
|
|
158
|
+
everything, because a bridge waiting out a rate limit is quiet BECAUSE it
|
|
159
|
+
is behaving, and a bridge that has just started has not finished a poll
|
|
160
|
+
yet. Both used to read as dead.
|
|
161
|
+
|
|
162
|
+
`alb --stop` asks the running bridge to stand down and signals nothing.
|
|
163
|
+
A request names the exact run it was meant for, so one written for a
|
|
164
|
+
bridge that then crashes cannot stop the next bridge to start.
|
|
165
|
+
|
|
166
|
+
Failures reach a log file as well as the terminal, timestamped, so "is
|
|
167
|
+
it down" and "since when" have different answers. A broken log can never
|
|
168
|
+
stop the bridge.
|
|
169
|
+
|
|
170
|
+
The install refuses to finish without a bell. Pane discovery asks every
|
|
171
|
+
multiplexer present and the notifier follows the pane the operator picks,
|
|
172
|
+
so an id we never showed cannot be accepted and silently pointed at a
|
|
173
|
+
multiplexer that is not there.
|
|
174
|
+
|
|
175
|
+
## [0.2.2] — unreleased, canary only
|
|
176
|
+
|
|
177
|
+
**Not a release.** The version exists so a running bridge can say which code
|
|
178
|
+
it is, because a canary you cannot identify proves nothing.
|
|
179
|
+
|
|
180
|
+
Recoverable platform conditions stop being terminal. 429 and 5xx on fetch and
|
|
181
|
+
confirm are waited out rather than died on, honouring Telegram's documented
|
|
182
|
+
`parameters.retry_after` as a floor; 401 and 403 stay fatal, and 409 still
|
|
183
|
+
yields. A throttled send becomes `Throttled` rather than a permanent refusal,
|
|
184
|
+
keeps its claim, survives a restart as a deferred state rather than being
|
|
185
|
+
dead-lettered as ambiguous, and can be finished by retyping the same reply.
|
|
186
|
+
|
|
187
|
+
Three further defects were found and fixed in the same work, each after the
|
|
188
|
+
previous fix's gates were green: receipt ordering was lexical and broke past
|
|
189
|
+
nine events; resume was check-then-act, so two resumers could both send; and
|
|
190
|
+
the lock added to fix that built its path from unvalidated caller text.
|
|
191
|
+
|
|
192
|
+
## [0.2.1] — 2026-09-04
|
|
193
|
+
|
|
194
|
+
Package metadata correction. 0.2.0 shipped with `version = "0.1.1"` in
|
|
195
|
+
`pyproject.toml`, so `alb --version` reported the previous release on a tree
|
|
196
|
+
tagged v0.2.0 — a stranger could not tell which world they had cloned. The tag
|
|
197
|
+
stands as published;
|
|
198
|
+
this release makes the metadata tell the truth.
|
|
199
|
+
|
|
200
|
+
Also in this release: the README no longer describes the ring as something that
|
|
201
|
+
"only makes it faster" — letter first, then ring, and without a bell mail lands
|
|
202
|
+
in a dead drop and nobody is told. The Design section no longer dates its
|
|
203
|
+
isolation claim to v0.1.
|
|
204
|
+
|
|
205
|
+
## [0.2.0] — 2026-09-04
|
|
206
|
+
|
|
207
|
+
Durable outbound, correspondent identity and threading, and read-only
|
|
208
|
+
retrieval. First release published to a public repository.
|
|
209
|
+
|
|
210
|
+
### The letter is the claim
|
|
211
|
+
|
|
212
|
+
An outbound reply is written as a letter — created `O_EXCL` — **before** the
|
|
213
|
+
platform is touched. That create *is* the claim on the work, so a crash between
|
|
214
|
+
send and record cannot produce a silent double-send: the second attempt finds
|
|
215
|
+
the letter already there. Delivery events are immutable numbered files rather
|
|
216
|
+
than appended lines, so a crash mid-write cannot tear the record. Startup
|
|
217
|
+
reconciliation finds anything left in flight with no terminal event,
|
|
218
|
+
dead-letters it, and says so.
|
|
219
|
+
|
|
220
|
+
### Identity and threading
|
|
221
|
+
|
|
222
|
+
Correspondents are provenance, not participants: `from` and `to` remain
|
|
223
|
+
routable agent ids. Replies resolve on the exact `(platform, origin,
|
|
224
|
+
message_id)` triple in both directions — no prefix matching. One thread per
|
|
225
|
+
correspondent, stamped inside publish.
|
|
226
|
+
|
|
227
|
+
### Retrieval
|
|
228
|
+
|
|
229
|
+
`--list`, `--show`, `--search`, `--thread`, `--export`. Read-only, standard
|
|
230
|
+
library, no config and no token: reading your own records never needs a
|
|
231
|
+
credential.
|
|
232
|
+
|
|
233
|
+
### Coverage
|
|
234
|
+
|
|
235
|
+
The invariants are pinned by mutation testing. Automated coverage still uses
|
|
236
|
+
fakes — the suite proves the invariants, live runs prove the transports, and
|
|
237
|
+
those are different claims. The inbound path has real live mileage; the paths
|
|
238
|
+
added here are covered and reviewed but newer.
|
|
239
|
+
|
|
240
|
+
## [0.1.1] — 2026-09-02
|
|
241
|
+
|
|
242
|
+
First complete release (0.1.0 tagged earlier the same day; 0.1.1 lands the
|
|
243
|
+
findings of a full-repo consistency review —
|
|
244
|
+
init writes the pasted pane id before the resident offer reads the config,
|
|
245
|
+
claims scoped to what the envelope actually records, roles-not-processes
|
|
246
|
+
stated honestly, integrated `--mail-root` documented with its true shape, and
|
|
247
|
+
one voice across package metadata, CLI help and docs). Private until the
|
|
248
|
+
repository owner flips it; the version marks "finished", not "published".
|
|
249
|
+
|
|
250
|
+
### Highlights since the pre-release notes below
|
|
251
|
+
- `alb --init`: interactive setup that owns every boilerplate step — 0700
|
|
252
|
+
state directory, mode-600 config, DENY-ALL allowlist — asks only what no
|
|
253
|
+
program can derive, never overwrites, never invents an allowlist entry,
|
|
254
|
+
never touches the network unless explicitly asked, and ends by starting
|
|
255
|
+
the bridge in its own cmux pane (or printing the exact command when it
|
|
256
|
+
cannot).
|
|
257
|
+
- Integrated mode (`--mail-root`): letters delivered into a mailbox the
|
|
258
|
+
bridge does not own, private state strictly separated, ring through the
|
|
259
|
+
mailbox's own doorbell helper with the outcome parsed rather than assumed.
|
|
260
|
+
A missing mailbox is refused, never invented.
|
|
261
|
+
- Cycle report: `fetched N · published N · denied N (allowlist)` — the deny
|
|
262
|
+
visible to the operator while the sender still hears silence; duplicates
|
|
263
|
+
counted apart from denials. Counts, never identities.
|
|
264
|
+
- Refusals grown from live installs: placeholder `ALB_SURFACE` values
|
|
265
|
+
refused by name; unknown config keys refused; `--version` answers without
|
|
266
|
+
a state directory.
|
|
267
|
+
- Two install routes: `INSTALL.md` for people, `docs/agent-install.md` as a
|
|
268
|
+
brief for a CLI agent installing on someone's behalf — the latter hardened
|
|
269
|
+
against the ways an install actually goes wrong.
|
|
270
|
+
- `docs/COMPARE.md`: the field, checked against each tool's current code and
|
|
271
|
+
docs, our own limitations stated first, every citation dated.
|
|
272
|
+
- Verified live: a full phone → letter → doorbell → read → reply loop, and an
|
|
273
|
+
install performed end-to-end by a CLI agent working from the docs.
|
|
274
|
+
|
|
275
|
+
### Proven by
|
|
276
|
+
Invariants pinned by mutation testing — each proved by disabling it and
|
|
277
|
+
watching the suite go red — privacy and dependency gates in pre-commit and
|
|
278
|
+
CI, and the live runs above.
|
|
279
|
+
|
|
280
|
+
### Added
|
|
281
|
+
- Durable-letter store: atomic publish, two-fence parsing, exact-id resolution,
|
|
282
|
+
path-shaped identifiers refused, delivered-ids ledger with a durable
|
|
283
|
+
update-id lookup behind it.
|
|
284
|
+
- Fail-closed allowlist, deny-all by default, enforced at both inbound and send.
|
|
285
|
+
- Untrusted poller: letter to disk, then acknowledge. Denied senders produce
|
|
286
|
+
silence and are still consumed.
|
|
287
|
+
- Notifier: fixed content-free line to one explicitly identified surface.
|
|
288
|
+
- Bounded outbound: replies only to a stored letter, claim before send,
|
|
289
|
+
ambiguous outcomes dead-letter and are never retried.
|
|
290
|
+
- Watchdog reporting, and `alb --status` as the single should-I-worry surface.
|
|
291
|
+
- `alb --doctor`: local single-consumer probe, daemon-context checks, and an
|
|
292
|
+
explicit statement of what it cannot prove.
|
|
293
|
+
- `alb --canary`: proves the send path through the real send helper.
|
|
294
|
+
- Telegram and cmux adapters. `examples/` unit files for launchd and systemd.
|
|
295
|
+
- Mutation gate: every invariant is disabled in turn and the suite must go red.
|
|
296
|
+
- Privacy enforcement from the first commit, in hooks and CI.
|
|
297
|
+
|
|
298
|
+
### Notes
|
|
299
|
+
- The ring requires a multiplexer. There is no notifier that works without one.
|
|
300
|
+
- Verified live against a sacrificial bot. (Superseded above.)
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Contributing
|
|
2
|
+
|
|
3
|
+
## Non-negotiables
|
|
4
|
+
|
|
5
|
+
Read `docs/invariants.md` first. The invariants are the product; a change that
|
|
6
|
+
weakens one is not a change we can take, however tidy it is.
|
|
7
|
+
|
|
8
|
+
**Zero third-party runtime dependencies in the core.** A PR that adds one to the
|
|
9
|
+
poller, send helper, watchdog or doctor will be declined. This is a security
|
|
10
|
+
property, not a style preference — the audit has to stay finishable.
|
|
11
|
+
|
|
12
|
+
**Every invariant needs a mutation-proved test.** A test that passes when the
|
|
13
|
+
invariant is disabled is not a test of that invariant.
|
|
14
|
+
|
|
15
|
+
**Fixtures are synthetic.** Never copy a real message, chat id, username or path
|
|
16
|
+
into a fixture. This is a privacy control, not a testing preference: git history
|
|
17
|
+
makes a leak permanent.
|
|
18
|
+
|
|
19
|
+
**Honest labelling.** A partially-met criterion is labelled PARTIAL. Do not pad
|
|
20
|
+
it to look complete.
|
|
21
|
+
|
|
22
|
+
## Repository editorial checks
|
|
23
|
+
|
|
24
|
+
These are this repository's publication policy for all public prose — docs,
|
|
25
|
+
README, CHANGELOG, and commit messages. `scripts/editorial_check.py` trips on
|
|
26
|
+
known banned phrasings at commit and in CI; it is a **known-phrase tripwire,
|
|
27
|
+
not a meaning checker**, so passing it does not certify compliance — reviewers
|
|
28
|
+
still read for meaning. If it refuses your commit, reword per the rule below;
|
|
29
|
+
the check names which one.
|
|
30
|
+
|
|
31
|
+
**The doorbell is a required part of a working deployment.** Never write it as
|
|
32
|
+
something a deployment can reasonably skip. Durability may be stated — a
|
|
33
|
+
landed letter survives a failed bell — but not in a sentence that presents
|
|
34
|
+
ringing as a choice. Violates the policy (a reviewer catches it even where the
|
|
35
|
+
script does not): "you can disable the ring and rely on the inbox". Passes:
|
|
36
|
+
"mail still lands if the bell fails; a bridge left in that state is not
|
|
37
|
+
installed".
|
|
38
|
+
|
|
39
|
+
**Comparisons name their subject.** A claim about another tool cites
|
|
40
|
+
[`docs/COMPARE.md`](docs/COMPARE.md) and names the tool; sentences asserting
|
|
41
|
+
something about all tools of a category are refused.
|
|
42
|
+
|
|
43
|
+
**Public text carries product facts, defects and method.** No numeric test or
|
|
44
|
+
invariant totals in prose or commit messages — state that behaviour is
|
|
45
|
+
covered, don't count it. The script scans all tracked Markdown as it stands
|
|
46
|
+
and your new commit message; historic commit messages are not rescanned. In
|
|
47
|
+
CI only the tree is scanned — messages are checked by the commit-msg hook, so
|
|
48
|
+
a hook-less commit path relies on review for its message.
|
|
49
|
+
|
|
50
|
+
## Set up the hooks — they are not automatic
|
|
51
|
+
|
|
52
|
+
Git hooks are per-clone local config. A fresh clone inherits nothing, so run this
|
|
53
|
+
once after cloning:
|
|
54
|
+
|
|
55
|
+
```sh
|
|
56
|
+
git config core.hooksPath .githooks
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
That wires the `pre-commit` privacy scan and the `commit-msg` trailer check. CI is
|
|
60
|
+
the real gate and runs regardless; the hooks just fail faster.
|
|
61
|
+
|
|
62
|
+
## What the privacy scan does and does not cover
|
|
63
|
+
|
|
64
|
+
The scan uses **structural** patterns — absolute home paths, volume paths,
|
|
65
|
+
service-manager domains, token-shaped strings, assistant trailers, machine
|
|
66
|
+
identifiers. It deliberately contains no list of private strings, because such a
|
|
67
|
+
list in the repo would be the leak it exists to prevent.
|
|
68
|
+
|
|
69
|
+
**It therefore cannot catch semantically private names** (an internal tool or host
|
|
70
|
+
name that looks like an ordinary word). Supply those out of band via
|
|
71
|
+
`ALB_EXTRA_PATTERNS`, a path to a newline-separated regex file that is never
|
|
72
|
+
committed. **CI does not set it**, so that class is covered by review and the
|
|
73
|
+
pre-release history audit — not by the automated gate. Do not assume CI covers it.
|
|
74
|
+
|
|
75
|
+
## Claims
|
|
76
|
+
|
|
77
|
+
Every claim in the docs must be traceable to the mechanism that makes it true.
|
|
78
|
+
If you cannot point at the mechanism, narrow the claim. Absolutes that sound
|
|
79
|
+
strong and are disprovable discredit the true claims beside them.
|
|
80
|
+
|
|
81
|
+
## Style
|
|
82
|
+
|
|
83
|
+
Python 3.11+, standard library only. Match the surrounding code.
|