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.
Files changed (136) hide show
  1. agent_letter_bridge-0.3.0/.githooks/commit-msg +4 -0
  2. agent_letter_bridge-0.3.0/.githooks/pre-commit +4 -0
  3. agent_letter_bridge-0.3.0/.github/workflows/ci.yml +59 -0
  4. agent_letter_bridge-0.3.0/.gitignore +5 -0
  5. agent_letter_bridge-0.3.0/CHANGELOG.md +300 -0
  6. agent_letter_bridge-0.3.0/CONTRIBUTING.md +83 -0
  7. agent_letter_bridge-0.3.0/INSTALL.md +496 -0
  8. agent_letter_bridge-0.3.0/LICENSE +21 -0
  9. agent_letter_bridge-0.3.0/PKG-INFO +284 -0
  10. agent_letter_bridge-0.3.0/README.md +252 -0
  11. agent_letter_bridge-0.3.0/SECURITY.md +38 -0
  12. agent_letter_bridge-0.3.0/docs/COMPARE.md +176 -0
  13. agent_letter_bridge-0.3.0/docs/adapter-contract.md +39 -0
  14. agent_letter_bridge-0.3.0/docs/agent-install.md +481 -0
  15. agent_letter_bridge-0.3.0/docs/agent-setup.md +152 -0
  16. agent_letter_bridge-0.3.0/docs/architecture.md +87 -0
  17. agent_letter_bridge-0.3.0/docs/invariants.md +166 -0
  18. agent_letter_bridge-0.3.0/docs/operations.md +412 -0
  19. agent_letter_bridge-0.3.0/docs/retention.md +134 -0
  20. agent_letter_bridge-0.3.0/docs/threat-model.md +81 -0
  21. agent_letter_bridge-0.3.0/examples/README.md +4 -0
  22. agent_letter_bridge-0.3.0/examples/launchd.plist +119 -0
  23. agent_letter_bridge-0.3.0/examples/systemd.user.service +41 -0
  24. agent_letter_bridge-0.3.0/pyproject.toml +34 -0
  25. agent_letter_bridge-0.3.0/scripts/check_conformance_vendor.py +59 -0
  26. agent_letter_bridge-0.3.0/scripts/deps_check.py +63 -0
  27. agent_letter_bridge-0.3.0/scripts/editorial_check.py +125 -0
  28. agent_letter_bridge-0.3.0/scripts/mutation_check.py +1165 -0
  29. agent_letter_bridge-0.3.0/scripts/privacy_scan.py +126 -0
  30. agent_letter_bridge-0.3.0/src/alb/__init__.py +0 -0
  31. agent_letter_bridge-0.3.0/src/alb/__main__.py +11 -0
  32. agent_letter_bridge-0.3.0/src/alb/adapters/README.md +7 -0
  33. agent_letter_bridge-0.3.0/src/alb/adapters/__init__.py +0 -0
  34. agent_letter_bridge-0.3.0/src/alb/adapters/cmux/__init__.py +0 -0
  35. agent_letter_bridge-0.3.0/src/alb/adapters/cmux/transport.py +74 -0
  36. agent_letter_bridge-0.3.0/src/alb/adapters/telegram/__init__.py +0 -0
  37. agent_letter_bridge-0.3.0/src/alb/adapters/telegram/api.py +422 -0
  38. agent_letter_bridge-0.3.0/src/alb/adapters/tmux/__init__.py +0 -0
  39. agent_letter_bridge-0.3.0/src/alb/adapters/tmux/transport.py +57 -0
  40. agent_letter_bridge-0.3.0/src/alb/allowlist/__init__.py +0 -0
  41. agent_letter_bridge-0.3.0/src/alb/allowlist/gate.py +57 -0
  42. agent_letter_bridge-0.3.0/src/alb/bridge/__init__.py +0 -0
  43. agent_letter_bridge-0.3.0/src/alb/bridge/run.py +383 -0
  44. agent_letter_bridge-0.3.0/src/alb/bridge/singleton.py +161 -0
  45. agent_letter_bridge-0.3.0/src/alb/canary/__init__.py +0 -0
  46. agent_letter_bridge-0.3.0/src/alb/canary/probe.py +82 -0
  47. agent_letter_bridge-0.3.0/src/alb/cli.py +870 -0
  48. agent_letter_bridge-0.3.0/src/alb/doctor/README.md +22 -0
  49. agent_letter_bridge-0.3.0/src/alb/doctor/__init__.py +0 -0
  50. agent_letter_bridge-0.3.0/src/alb/doctor/checks.py +333 -0
  51. agent_letter_bridge-0.3.0/src/alb/grant/__init__.py +1 -0
  52. agent_letter_bridge-0.3.0/src/alb/grant/store.py +383 -0
  53. agent_letter_bridge-0.3.0/src/alb/initiate/__init__.py +1 -0
  54. agent_letter_bridge-0.3.0/src/alb/initiate/budget.py +130 -0
  55. agent_letter_bridge-0.3.0/src/alb/initiate/compose.py +49 -0
  56. agent_letter_bridge-0.3.0/src/alb/initiate/destinations.py +52 -0
  57. agent_letter_bridge-0.3.0/src/alb/initiate/durable.py +80 -0
  58. agent_letter_bridge-0.3.0/src/alb/initiate/ids.py +85 -0
  59. agent_letter_bridge-0.3.0/src/alb/initiate/reservation.py +71 -0
  60. agent_letter_bridge-0.3.0/src/alb/initiate/send.py +275 -0
  61. agent_letter_bridge-0.3.0/src/alb/letter/README.md +7 -0
  62. agent_letter_bridge-0.3.0/src/alb/letter/__init__.py +0 -0
  63. agent_letter_bridge-0.3.0/src/alb/letter/store.py +303 -0
  64. agent_letter_bridge-0.3.0/src/alb/media/__init__.py +1 -0
  65. agent_letter_bridge-0.3.0/src/alb/media/attach.py +64 -0
  66. agent_letter_bridge-0.3.0/src/alb/media/inspect.py +127 -0
  67. agent_letter_bridge-0.3.0/src/alb/media/store.py +317 -0
  68. agent_letter_bridge-0.3.0/src/alb/msgindex.py +53 -0
  69. agent_letter_bridge-0.3.0/src/alb/notifier/README.md +5 -0
  70. agent_letter_bridge-0.3.0/src/alb/notifier/__init__.py +0 -0
  71. agent_letter_bridge-0.3.0/src/alb/notifier/ring.py +43 -0
  72. agent_letter_bridge-0.3.0/src/alb/outbound/__init__.py +0 -0
  73. agent_letter_bridge-0.3.0/src/alb/outbound/store.py +411 -0
  74. agent_letter_bridge-0.3.0/src/alb/poller/README.md +8 -0
  75. agent_letter_bridge-0.3.0/src/alb/poller/__init__.py +0 -0
  76. agent_letter_bridge-0.3.0/src/alb/poller/loop.py +319 -0
  77. agent_letter_bridge-0.3.0/src/alb/retrieval.py +121 -0
  78. agent_letter_bridge-0.3.0/src/alb/send/README.md +7 -0
  79. agent_letter_bridge-0.3.0/src/alb/send/__init__.py +0 -0
  80. agent_letter_bridge-0.3.0/src/alb/send/reply.py +407 -0
  81. agent_letter_bridge-0.3.0/src/alb/setup/__init__.py +0 -0
  82. agent_letter_bridge-0.3.0/src/alb/setup/discover.py +110 -0
  83. agent_letter_bridge-0.3.0/src/alb/setup/wizard.py +866 -0
  84. agent_letter_bridge-0.3.0/src/alb/watchdog/README.md +6 -0
  85. agent_letter_bridge-0.3.0/src/alb/watchdog/__init__.py +0 -0
  86. agent_letter_bridge-0.3.0/src/alb/watchdog/health.py +158 -0
  87. agent_letter_bridge-0.3.0/tests/fake_platform.py +49 -0
  88. agent_letter_bridge-0.3.0/tests/test_allowlist.py +67 -0
  89. agent_letter_bridge-0.3.0/tests/test_bridge.py +304 -0
  90. agent_letter_bridge-0.3.0/tests/test_canary.py +81 -0
  91. agent_letter_bridge-0.3.0/tests/test_check_command.py +219 -0
  92. agent_letter_bridge-0.3.0/tests/test_cmux_adapter.py +51 -0
  93. agent_letter_bridge-0.3.0/tests/test_conformance_vendor.py +77 -0
  94. agent_letter_bridge-0.3.0/tests/test_cycle_report.py +124 -0
  95. agent_letter_bridge-0.3.0/tests/test_deps_gate.py +81 -0
  96. agent_letter_bridge-0.3.0/tests/test_doctor.py +63 -0
  97. agent_letter_bridge-0.3.0/tests/test_doctor_probe.py +375 -0
  98. agent_letter_bridge-0.3.0/tests/test_editorial_check.py +162 -0
  99. agent_letter_bridge-0.3.0/tests/test_envelope.py +103 -0
  100. agent_letter_bridge-0.3.0/tests/test_grant.py +690 -0
  101. agent_letter_bridge-0.3.0/tests/test_heartbeat_states.py +69 -0
  102. agent_letter_bridge-0.3.0/tests/test_initiate.py +467 -0
  103. agent_letter_bridge-0.3.0/tests/test_letter.py +254 -0
  104. agent_letter_bridge-0.3.0/tests/test_logging.py +72 -0
  105. agent_letter_bridge-0.3.0/tests/test_mail_root.py +380 -0
  106. agent_letter_bridge-0.3.0/tests/test_media.py +629 -0
  107. agent_letter_bridge-0.3.0/tests/test_mutation_gate.py +73 -0
  108. agent_letter_bridge-0.3.0/tests/test_notifier.py +66 -0
  109. agent_letter_bridge-0.3.0/tests/test_notifier_bounds.py +140 -0
  110. agent_letter_bridge-0.3.0/tests/test_offset_state_machine.py +139 -0
  111. agent_letter_bridge-0.3.0/tests/test_outbound.py +521 -0
  112. agent_letter_bridge-0.3.0/tests/test_permissions.py +64 -0
  113. agent_letter_bridge-0.3.0/tests/test_poll_backoff.py +252 -0
  114. agent_letter_bridge-0.3.0/tests/test_poller.py +311 -0
  115. agent_letter_bridge-0.3.0/tests/test_privacy_scan.py +100 -0
  116. agent_letter_bridge-0.3.0/tests/test_resident_launch.py +240 -0
  117. agent_letter_bridge-0.3.0/tests/test_resume_cli.py +83 -0
  118. agent_letter_bridge-0.3.0/tests/test_retrieval.py +222 -0
  119. agent_letter_bridge-0.3.0/tests/test_send.py +711 -0
  120. agent_letter_bridge-0.3.0/tests/test_setup.py +1420 -0
  121. agent_letter_bridge-0.3.0/tests/test_setup_discover.py +152 -0
  122. agent_letter_bridge-0.3.0/tests/test_singleton.py +50 -0
  123. agent_letter_bridge-0.3.0/tests/test_stop.py +402 -0
  124. agent_letter_bridge-0.3.0/tests/test_telegram_adapter.py +391 -0
  125. agent_letter_bridge-0.3.0/tests/test_tmux_adapter.py +59 -0
  126. agent_letter_bridge-0.3.0/tests/test_w2_identity_threading.py +208 -0
  127. agent_letter_bridge-0.3.0/tests/test_wake_check.py +184 -0
  128. agent_letter_bridge-0.3.0/tests/test_watchdog.py +67 -0
  129. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-source.json +8 -0
  130. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/README.md +22 -0
  131. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/SHA256SUMS +6 -0
  132. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/accepted/basic.md +13 -0
  133. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/accepted/unicode-body-key.md +15 -0
  134. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/cases.tsv +6 -0
  135. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/rejected/one-fence.md +9 -0
  136. agent_letter_bridge-0.3.0/vendor/letterbox-conformance-v1/rejected/trailing-space-fence.md +10 -0
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Assistant trailers must never reach a commit message.
3
+ python3 scripts/privacy_scan.py "$1" || exit 1
4
+ exec python3 scripts/editorial_check.py "$1"
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Privacy discipline from commit one. See docs/ and scripts/privacy_scan.py.
3
+ python3 scripts/privacy_scan.py || exit 1
4
+ exec python3 scripts/editorial_check.py
@@ -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,5 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ *.env
5
+ state/
@@ -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.