tabc 0.2.0__tar.gz → 0.2.2__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 (41) hide show
  1. tabc-0.2.2/CHANGELOG.md +173 -0
  2. {tabc-0.2.0 → tabc-0.2.2}/GUIDE.md +45 -6
  3. {tabc-0.2.0 → tabc-0.2.2}/MCP.md +6 -0
  4. {tabc-0.2.0/tabc.egg-info → tabc-0.2.2}/PKG-INFO +55 -9
  5. {tabc-0.2.0 → tabc-0.2.2}/PYPI.md +52 -3
  6. {tabc-0.2.0 → tabc-0.2.2}/pyproject.toml +1 -1
  7. {tabc-0.2.0 → tabc-0.2.2/tabc.egg-info}/PKG-INFO +55 -9
  8. {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/SOURCES.txt +1 -0
  9. {tabc-0.2.0 → tabc-0.2.2}/tabus/bus.py +1 -1
  10. {tabc-0.2.0 → tabc-0.2.2}/tabus/cli.py +27 -1
  11. tabc-0.2.2/tabus/codex_queue.py +194 -0
  12. {tabc-0.2.0 → tabc-0.2.2}/tabus/doorbell.py +7 -2
  13. {tabc-0.2.0 → tabc-0.2.2}/tabus/doorbell_poll.py +1 -1
  14. {tabc-0.2.0 → tabc-0.2.2}/tabus/route_capture.py +3 -3
  15. {tabc-0.2.0 → tabc-0.2.2}/tabus/terminal_frontend.py +27 -7
  16. tabc-0.2.0/CHANGELOG.md +0 -80
  17. {tabc-0.2.0 → tabc-0.2.2}/LICENSE +0 -0
  18. {tabc-0.2.0 → tabc-0.2.2}/MANIFEST.in +0 -0
  19. {tabc-0.2.0 → tabc-0.2.2}/setup.cfg +0 -0
  20. {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/dependency_links.txt +0 -0
  21. {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/entry_points.txt +0 -0
  22. {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/requires.txt +0 -0
  23. {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/top_level.txt +0 -0
  24. {tabc-0.2.0 → tabc-0.2.2}/tabus/__init__.py +0 -0
  25. {tabc-0.2.0 → tabc-0.2.2}/tabus/admin.py +0 -0
  26. {tabc-0.2.0 → tabc-0.2.2}/tabus/compat_bcn.py +0 -0
  27. {tabc-0.2.0 → tabc-0.2.2}/tabus/compat_busd.py +0 -0
  28. {tabc-0.2.0 → tabc-0.2.2}/tabus/daemon.py +0 -0
  29. {tabc-0.2.0 → tabc-0.2.2}/tabus/lang.py +0 -0
  30. {tabc-0.2.0 → tabc-0.2.2}/tabus/locales/en.json +0 -0
  31. {tabc-0.2.0 → tabc-0.2.2}/tabus/mcp_server.py +0 -0
  32. {tabc-0.2.0 → tabc-0.2.2}/tabus/nodekey.py +0 -0
  33. {tabc-0.2.0 → tabc-0.2.2}/tabus/paths.py +0 -0
  34. {tabc-0.2.0 → tabc-0.2.2}/tabus/process_argv.py +0 -0
  35. {tabc-0.2.0 → tabc-0.2.2}/tabus/send_refusal.py +0 -0
  36. {tabc-0.2.0 → tabc-0.2.2}/tabus/sent.py +0 -0
  37. {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_identity.py +0 -0
  38. {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_migration.py +0 -0
  39. {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_refusal.py +0 -0
  40. {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_search.py +0 -0
  41. {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_store.py +0 -0
@@ -0,0 +1,173 @@
1
+ # Changelog
2
+
3
+ ## 0.2.2 — 2026-10-17
4
+
5
+ ### Changed
6
+
7
+ - `register` treats `--kind` case-insensitively and stores it in lowercase.
8
+ `--kind OpenCode` registers `opencode`.
9
+ - Terminal route capture at a detached session boundary no longer requires a
10
+ recognized Codex or Claude CLI. The immediate session-boundary parent
11
+ qualifies when it belongs to the same OS user, leads the target terminal's
12
+ foreground process group, and reads standard input from that terminal.
13
+ Process state, executable path, and standard input are checked again before
14
+ the route is accepted. Product names and install directories are not an
15
+ admission list.
16
+ - Codex queue selection under `--kind codex --auto-enter on` and its fallback
17
+ rules are unchanged. Because the kind is case-folded, `--kind Codex` now
18
+ selects the same queue route.
19
+
20
+ ### Notes
21
+
22
+ - The check verifies terminal ownership, not whether the program is an agent.
23
+ A foreground program that reads the terminal can qualify even if it is not
24
+ an agent. Automatic Enter remains a separate opt-in.
25
+ - No dedicated adapter for another agent product is added. The MCP adapter
26
+ still does not wake an agent.
27
+
28
+ ### Upgrade notes (from 0.2.1)
29
+
30
+ - The ledger schema is unchanged. Nodes, signing keys, messages, delivery
31
+ states, TACs, routes, and Codex queue receipts are kept.
32
+ - Stop `tabus.doorbell`, install 0.2.2, restart `tabd`, then start one
33
+ `tabus.doorbell` from the same installation:
34
+
35
+ ```bash
36
+ python -m pip install --upgrade "tabc==0.2.2"
37
+ # Or, for desktop MCP support:
38
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
39
+ ```
40
+
41
+ - Stored routes are not migrated. A failed capture does not replace an
42
+ existing route. Register again from the intended terminal, then confirm one
43
+ notification arrives.
44
+ - If a desktop client runs `tabc-mcp`, restart that MCP process from the client
45
+ after the upgrade so it loads the new installation.
46
+ - After restart, `tabc --version` and `tabd --version` report 0.2.2.
47
+
48
+ ## 0.2.1 — 2026-10-15
49
+
50
+ ### Added
51
+
52
+ - A Codex queue notification route. Under the native Codex managed-daemon
53
+ app-server, the notifier submits notification metadata to the registered
54
+ Codex thread with `codex queue` instead of typing into a terminal.
55
+ - `register --codex-queue` requires that route with no terminal fallback. It
56
+ needs `--kind codex` and `--auto-enter on`.
57
+ - A local receipt database, `codex_queue_receipts.db`, that limits submission
58
+ to one attempt per thread UUID and notification ID. An uncertain result is
59
+ recorded as `UNKNOWN` and is not submitted again automatically.
60
+
61
+ ### Changed
62
+
63
+ - `register` requires `--kind`. The previous default, `generic`, is removed.
64
+ - `register --kind codex --auto-enter on` selects the Codex queue route when
65
+ the current Codex thread verifies, then a terminal route verified by the same
66
+ command. With neither, registration is refused and existing routes are kept.
67
+ - When registration selects the Codex queue route, `--take-route` is refused.
68
+
69
+ ### Upgrade notes
70
+
71
+ - The ledger schema is unchanged. Nodes, signing keys, messages, delivery
72
+ states, TACs, and routes are kept. No conversion is needed.
73
+ - Stop `tabus.doorbell`, install 0.2.1, restart `tabd`, then start one
74
+ `tabus.doorbell` from the same installation:
75
+
76
+ ```bash
77
+ python -m pip install --upgrade "tabc==0.2.1"
78
+ # Or, for desktop MCP support:
79
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
80
+ ```
81
+
82
+ - Confirm the running notifier is 0.2.1 before registering Codex queue routes.
83
+ Run one notifier. An older notifier records `UNSUPPORTED` for these routes.
84
+ - Scripts that call `register` without `--kind` must add it.
85
+ - To use the Codex queue route, register again from each intended Codex
86
+ conversation after the notifier is replaced.
87
+ - The notifier creates `codex_queue_receipts.db` in the tabc home directory on
88
+ its first Codex queue submission. It is separate from message read state.
89
+ - The Codex queue route is a notifier path. It is not part of the MCP adapter,
90
+ which still does not wake an agent.
91
+ - If a desktop client runs `tabc-mcp`, restart that MCP process from the client
92
+ after the upgrade so it loads the new installation.
93
+ - Checked with codex-cli 0.162.1 on macOS only.
94
+ - After restart, `tabc --version` and `tabd --version` report 0.2.1.
95
+
96
+ ## 0.2.0 — 2026-09-23
97
+
98
+ ### Added
99
+
100
+ - An optional local stdio MCP adapter for desktop agents.
101
+ - Structured refusal codes and details for send and TAC errors.
102
+ - UTF-8 byte limits: 1,024 bytes for a subject, 65,536 bytes for a body, and
103
+ 524,288 bytes for an HTTP request. Oversized input is rejected before storage.
104
+ - Server-minted canonical UUIDs for TAC identity. TAC names remain unique,
105
+ searchable, and renameable without changing the UUID.
106
+ - TAC `rename`, `check`, and member-scoped literal `search` commands.
107
+ - A copy-first migration tool for existing string TAC identifiers.
108
+ - Unicode-consistent literal TAC search across names, descriptions, summaries,
109
+ subjects, and bodies, including NFC/NFD-equivalent text.
110
+
111
+ ### Changed
112
+
113
+ - MCP responses preserve structured error codes and details from tabd.
114
+ - TAC commands address converted topics by UUID. Names are used for creation,
115
+ display, and search.
116
+ - `tac show --node` advances only the returned deliveries to `INJECTED`.
117
+ It does not mark them `READ`, and older unreturned messages may still block a send.
118
+ - The pending count printed after a send is scoped to that sender and recipient.
119
+ `tabc who` remains the total unread view.
120
+ - Terminal route capture now validates the native foreground Codex or Claude CLI
121
+ at a detached session boundary. Unknown launch modes and mismatched terminals
122
+ fail closed. New or moved routes keep automatic Enter off unless explicitly
123
+ enabled with `register --auto-enter on`.
124
+
125
+ ### Protocol details
126
+
127
+ - A fresh 0.2.0 ledger mints canonical lowercase, hyphenated UUIDs for TACs.
128
+ Existing ledgers retain string TAC IDs until explicit conversion.
129
+ - A TAC name is trimmed at its edges and compared as
130
+ `NFC(casefold(NFC(name)))`. Control, format, surrogate, private-use, unassigned,
131
+ and non-ordinary whitespace characters are refused. Unicode category results
132
+ follow the Python version running tabd.
133
+ - A partially converted ledger that has identity columns but still contains
134
+ string TAC IDs refuses TAC creation and rename with `TAC_NOT_CONVERTED`.
135
+ - Missing canonical TAC IDs return `TAC_NOT_FOUND`. Read-only name lookup can
136
+ report `exists: false`; names are not accepted where a TAC UUID designates the
137
+ target.
138
+ - Structured server refusal codes and retry rules are listed in
139
+ [GUIDE.md](GUIDE.md#refusal-codes). MCP preserves those fields without truncation.
140
+
141
+ ### Upgrade note
142
+
143
+ An upgrade from 0.1.6 preserves nodes, messages, delivery states, TAC membership,
144
+ names, and labels. Package installation and server startup do not convert TAC IDs.
145
+ Pre-0.2.0 ledgers keep their string TAC identifiers until explicitly converted.
146
+ Stop `tabus.doorbell` and `tabd`. Confirm that `<ledger.db>-wal` is absent or
147
+ empty (0 bytes). A remaining empty WAL or SHM file is safe. If the WAL is not
148
+ empty, start `tabd` and stop it cleanly, then check again. Back up the main
149
+ database only after the WAL is absent or empty. Install the required package,
150
+ then run a read-only preflight and write a separate converted database:
151
+
152
+ ```bash
153
+ python -m pip install --upgrade "tabc==0.2.0"
154
+ # Or, for desktop MCP support:
155
+ python -m pip install --upgrade "tabc[mcp]==0.2.0"
156
+
157
+ python -m tabus.tac_migration <ledger.db>
158
+ python -m tabus.tac_migration <ledger.db> --output <converted.db>
159
+ ```
160
+
161
+ Replace only the main database file after the row counts and converted database
162
+ are verified. Do not replace it while its WAL file is non-empty. A remaining
163
+ empty WAL or SHM file is safe. Restart `tabd` first, then restart
164
+ `tabus.doorbell`. Do not run old and new daemons against the same active ledger
165
+ during the swap.
166
+ After conversion, commands that use a TAC name instead of its UUID are refused
167
+ with `TAC_ID_INVALID`. Scripts must use the UUID printed by `tabc tac ls`.
168
+ After restart, use `tabc --version` and `tabd --version` to confirm that both
169
+ commands resolve to 0.2.0.
170
+
171
+ When replacing an installed notifier, stop the old `tabus.doorbell` first, restart
172
+ `tabd`, then start the new notifier. See the registration notes in
173
+ [README.md](README.md#node-identity-and-signing-keys).
@@ -78,6 +78,33 @@ Read the two together:
78
78
  A row existing is not the same as a row pointing at you. Check the string, not
79
79
  the count.
80
80
 
81
+ ### Codex queue routes
82
+
83
+ This applies to 0.2.1 and later, which add the Codex queue adapter. For a
84
+ Codex queue route, `adapter` is `codex-queue` and `target` is a Codex thread
85
+ UUID, not a terminal. Compare it with the conversation you are in:
86
+
87
+ ```bash
88
+ echo "$CODEX_THREAD_ID"
89
+ sqlite3 "file:$HOME/.tabc/tabus.db?mode=ro" \
90
+ "SELECT adapter, target, auto_enter FROM tab_routes
91
+ WHERE node_id='<your-node>' AND revoked_at IS NULL"
92
+ ```
93
+
94
+ | Outcome | What it means |
95
+ |---|---|
96
+ | `QUEUED` | Codex accepted the notification input for that thread. It is not `READ`, and the text can appear after the current reply ends |
97
+ | `UNKNOWN` | The submission result is uncertain. The same notification is not submitted again automatically. Check the inbox with `dm`. If every notification ends here, check that the notifier's own `PATH` has a native `codex` and that its Codex environment reaches the same local daemon |
98
+ | `DISABLED` | Automatic submission is off for the route, so nothing was submitted. Register again with `--auto-enter on` |
99
+ | `UNSUPPORTED` | The running notifier predates the queue adapter. Replace it with the matching release, then register again |
100
+
101
+ If the target is another conversation's UUID, register again from your own
102
+ conversation. Do not copy a UUID into the route.
103
+
104
+ After replacing the notifier and registering, confirm one real round trip: a
105
+ message arrives in the conversation and a reply comes back. A `QUEUED` record
106
+ does not show that.
107
+
81
108
  ---
82
109
 
83
110
  ## What a success record does not prove
@@ -189,9 +216,12 @@ ACCEPTED → CLAIMED → INJECTED → READ → PROCESSED
189
216
  A state cannot be skipped. `ACCEPTED` will not go straight to `READ`: pass
190
217
  through `INJECTED` with `open` or `pull` first, then `ack`.
191
218
 
192
- `--id` takes the **full** UUID. The id printed on a title line is truncated with
193
- an ellipsis, and copying that form gives `no such delivery`. Use
194
- `pull --mode full` and take the id from the detail block.
219
+ CLI `open` and `ack` take the full UUID or a unique prefix, such as the
220
+ shortened id that `dm` prints. A prefix that matches more than one message is
221
+ refused as ambiguous. `sent --id` and every MCP `message_id` take the full UUID.
222
+
223
+ A message that `pull` already claimed does not come back on a second `pull`.
224
+ It stays listed in `dm`; open it from there.
195
225
 
196
226
  ---
197
227
 
@@ -275,11 +305,20 @@ The same visible text can therefore use different byte counts in NFC and NFD.
275
305
  - Agents sharing one operating-system account can read each other's key files.
276
306
  This is not a security boundary between them.
277
307
  - Message bodies are not encrypted at rest.
278
- - Automatic Enter types into an execution-capable input field. It is off for new
279
- routes, and turning it on is a decision about that terminal, not a convenience.
280
- - Delivery is at-least-once, not exactly-once. A recipient absorbs duplicates.
308
+ - On a terminal route, automatic Enter types into an execution-capable input
309
+ field. It is off for new routes, and turning it on is a decision about that
310
+ terminal, not a convenience. A Codex queue route does not type into a
311
+ terminal; it submits input to the conversation.
312
+ - Message delivery is at-least-once, not exactly-once. A recipient absorbs
313
+ duplicates. Codex queue notifications are separate: the local receipt
314
+ database allows one submission attempt per thread UUID and notification ID,
315
+ and an `UNKNOWN` result is not submitted again automatically.
281
316
  - An alarm depends on a route. A node without one receives by `pull`, and no
282
317
  amount of sending will make it ring.
318
+ - A Codex queue route submits notification input to a Codex thread. It was
319
+ checked with codex-cli 0.162.1 on macOS only. The notification text is a
320
+ command-line argument that local process tools can see. It carries sender
321
+ names, unread counts and TAC names or UUIDs, never a body.
283
322
 
284
323
  `SECURITY.md` states the five clauses and how far each is currently kept.
285
324
 
@@ -30,6 +30,7 @@ normal node for each agent:
30
30
  Do not use `--program`: it creates a send-only node that cannot receive messages.
31
31
  Registration from iTerm2 or tmux can capture that terminal's notification route.
32
32
  Inspect the registration output; a terminal route does not notify the desktop app.
33
+ The command above omits `--auto-enter`, so it does not select a Codex queue route.
33
34
  Do not share one node between independent agent conversations: they would share
34
35
  the inbox and delivery states.
35
36
 
@@ -121,6 +122,11 @@ There are no node deletion, daemon administration, shell execution or automatic
121
122
  polling tools. Arrival does not trigger an agent turn. The client must call tools
122
123
  on user request or through separately configured scheduling.
123
124
 
125
+ The Codex queue adapter, added in 0.2.1, is a separate notification
126
+ path run by the notifier. It is not an MCP tool and does not change this
127
+ section: the MCP adapter still does not wake an agent. See
128
+ [Codex queue notifications](README.md#how-do-i-get-notifications-in-a-codex-thread).
129
+
124
130
  ## Errors and retries
125
131
 
126
132
  No request is automatically retried. A timeout, connection loss, server error or
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: tabc
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A shared inbox for AI agents and programs on one machine
5
5
  License: MIT License
6
6
 
@@ -37,11 +37,8 @@ Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Communications
38
38
  Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
- License-File: LICENSE
41
- Requires-Dist: cryptography>=41
42
40
  Provides-Extra: mcp
43
- Requires-Dist: mcp<2,>=1.27.1; python_version >= "3.10" and extra == "mcp"
44
- Dynamic: license-file
41
+ License-File: LICENSE
45
42
 
46
43
  # tabc
47
44
 
@@ -102,12 +99,51 @@ tabc sent --node alice
102
99
  ```
103
100
 
104
101
  The last line is what matters. It shows the message reached the other side,
105
- rather than showing that sending did not fail. `--id` takes the full UUID from
106
- the `pull --mode full` detail block, not the truncated one on a title line.
102
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
103
+ from the `pull --mode full` detail block, or a unique prefix of it.
107
104
 
108
105
  Terminal notification is a separate process and does not start with the
109
106
  install; see the repository's guide before turning it on.
110
107
 
108
+ ## Updating from 0.2.1
109
+
110
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
111
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
112
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
113
+
114
+ ```bash
115
+ python -m pip install --upgrade "tabc==0.2.2"
116
+ # Or, when the local desktop MCP adapter is required:
117
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
118
+ ```
119
+
120
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
121
+ route as `--kind codex`. A detached command no longer needs a recognized agent
122
+ product: its immediate session-boundary parent qualifies when it owns the
123
+ terminal foreground and reads standard input from it. The check proves terminal
124
+ ownership, not that the program is an agent. Register again from the intended
125
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
126
+ unchanged. See the
127
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
128
+
129
+ ## Updating from 0.2.0
130
+
131
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
132
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
133
+ start one `tabus.doorbell` from the same installation:
134
+
135
+ ```bash
136
+ python -m pip install --upgrade "tabc==0.2.1"
137
+ # Or, when the local desktop MCP adapter is required:
138
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
139
+ ```
140
+
141
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
142
+ register again from each conversation after the notifier is replaced. The
143
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
144
+ first Codex queue submission. See the
145
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
146
+
111
147
  ## Updating from 0.1.6
112
148
 
113
149
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -125,6 +161,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
125
161
  Existing string TAC IDs are not converted during installation. Follow the
126
162
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
127
163
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
164
+ Then follow "Updating from 0.2.0" and "Updating from 0.2.1".
128
165
 
129
166
  ## Commands
130
167
 
@@ -136,7 +173,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
136
173
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
137
174
 
138
175
  Register each recipient before sending. tac creators must also join their topic.
139
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
176
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
140
177
  sending, reading, renaming and closing; use the unique name for display and search.
141
178
  Run `tabc --help` for available commands. The local server is `tabd`; management
142
179
  commands are provided separately by `tabm`.
@@ -163,6 +200,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
163
200
  for new routes. Omitting the setting during registration preserves the same active
164
201
  route; `--auto-enter off` disables it explicitly.
165
202
 
203
+ `register` requires `--kind`. A Codex agent running under the native Codex
204
+ managed-daemon app-server can receive notifications in its conversation instead
205
+ of a terminal. Replace the notifier with this release first, then run
206
+ `tabc register --node <node> --kind codex --auto-enter on` from that
207
+ conversation. `--codex-queue` requires that route with no terminal fallback.
208
+ `QUEUED` means Codex accepted the notification input, not that it was read.
209
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
210
+ README for conditions and limits.
211
+
166
212
  Do not connect notifications to an unreviewed command prompt.
167
213
  Automatic Enter does not guarantee a reply.
168
214
 
@@ -57,12 +57,51 @@ tabc sent --node alice
57
57
  ```
58
58
 
59
59
  The last line is what matters. It shows the message reached the other side,
60
- rather than showing that sending did not fail. `--id` takes the full UUID from
61
- the `pull --mode full` detail block, not the truncated one on a title line.
60
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
61
+ from the `pull --mode full` detail block, or a unique prefix of it.
62
62
 
63
63
  Terminal notification is a separate process and does not start with the
64
64
  install; see the repository's guide before turning it on.
65
65
 
66
+ ## Updating from 0.2.1
67
+
68
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
69
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
70
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
71
+
72
+ ```bash
73
+ python -m pip install --upgrade "tabc==0.2.2"
74
+ # Or, when the local desktop MCP adapter is required:
75
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
76
+ ```
77
+
78
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
79
+ route as `--kind codex`. A detached command no longer needs a recognized agent
80
+ product: its immediate session-boundary parent qualifies when it owns the
81
+ terminal foreground and reads standard input from it. The check proves terminal
82
+ ownership, not that the program is an agent. Register again from the intended
83
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
84
+ unchanged. See the
85
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
86
+
87
+ ## Updating from 0.2.0
88
+
89
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
90
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
91
+ start one `tabus.doorbell` from the same installation:
92
+
93
+ ```bash
94
+ python -m pip install --upgrade "tabc==0.2.1"
95
+ # Or, when the local desktop MCP adapter is required:
96
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
97
+ ```
98
+
99
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
100
+ register again from each conversation after the notifier is replaced. The
101
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
102
+ first Codex queue submission. See the
103
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
104
+
66
105
  ## Updating from 0.1.6
67
106
 
68
107
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -80,6 +119,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
80
119
  Existing string TAC IDs are not converted during installation. Follow the
81
120
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
82
121
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
122
+ Then follow "Updating from 0.2.0" and "Updating from 0.2.1".
83
123
 
84
124
  ## Commands
85
125
 
@@ -91,7 +131,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
91
131
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
92
132
 
93
133
  Register each recipient before sending. tac creators must also join their topic.
94
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
134
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
95
135
  sending, reading, renaming and closing; use the unique name for display and search.
96
136
  Run `tabc --help` for available commands. The local server is `tabd`; management
97
137
  commands are provided separately by `tabm`.
@@ -118,6 +158,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
118
158
  for new routes. Omitting the setting during registration preserves the same active
119
159
  route; `--auto-enter off` disables it explicitly.
120
160
 
161
+ `register` requires `--kind`. A Codex agent running under the native Codex
162
+ managed-daemon app-server can receive notifications in its conversation instead
163
+ of a terminal. Replace the notifier with this release first, then run
164
+ `tabc register --node <node> --kind codex --auto-enter on` from that
165
+ conversation. `--codex-queue` requires that route with no terminal fallback.
166
+ `QUEUED` means Codex accepted the notification input, not that it was read.
167
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
168
+ README for conditions and limits.
169
+
121
170
  Do not connect notifications to an unreviewed command prompt.
122
171
  Automatic Enter does not guarantee a reply.
123
172
 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabc"
7
- version = "0.2.0"
7
+ version = "0.2.2"
8
8
  description = "A shared inbox for AI agents and programs on one machine"
9
9
  readme = "PYPI.md"
10
10
  requires-python = ">=3.9"
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: tabc
3
- Version: 0.2.0
3
+ Version: 0.2.2
4
4
  Summary: A shared inbox for AI agents and programs on one machine
5
5
  License: MIT License
6
6
 
@@ -37,11 +37,8 @@ Classifier: Programming Language :: Python :: 3
37
37
  Classifier: Topic :: Communications
38
38
  Requires-Python: >=3.9
39
39
  Description-Content-Type: text/markdown
40
- License-File: LICENSE
41
- Requires-Dist: cryptography>=41
42
40
  Provides-Extra: mcp
43
- Requires-Dist: mcp<2,>=1.27.1; python_version >= "3.10" and extra == "mcp"
44
- Dynamic: license-file
41
+ License-File: LICENSE
45
42
 
46
43
  # tabc
47
44
 
@@ -102,12 +99,51 @@ tabc sent --node alice
102
99
  ```
103
100
 
104
101
  The last line is what matters. It shows the message reached the other side,
105
- rather than showing that sending did not fail. `--id` takes the full UUID from
106
- the `pull --mode full` detail block, not the truncated one on a title line.
102
+ rather than showing that sending did not fail. `ack --id` takes the full UUID
103
+ from the `pull --mode full` detail block, or a unique prefix of it.
107
104
 
108
105
  Terminal notification is a separate process and does not start with the
109
106
  install; see the repository's guide before turning it on.
110
107
 
108
+ ## Updating from 0.2.1
109
+
110
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
111
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
112
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
113
+
114
+ ```bash
115
+ python -m pip install --upgrade "tabc==0.2.2"
116
+ # Or, when the local desktop MCP adapter is required:
117
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
118
+ ```
119
+
120
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
121
+ route as `--kind codex`. A detached command no longer needs a recognized agent
122
+ product: its immediate session-boundary parent qualifies when it owns the
123
+ terminal foreground and reads standard input from it. The check proves terminal
124
+ ownership, not that the program is an agent. Register again from the intended
125
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
126
+ unchanged. See the
127
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
128
+
129
+ ## Updating from 0.2.0
130
+
131
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
132
+ TACs, and routes are kept. Stop `tabus.doorbell`, install, restart `tabd`, then
133
+ start one `tabus.doorbell` from the same installation:
134
+
135
+ ```bash
136
+ python -m pip install --upgrade "tabc==0.2.1"
137
+ # Or, when the local desktop MCP adapter is required:
138
+ python -m pip install --upgrade "tabc[mcp]==0.2.1"
139
+ ```
140
+
141
+ `register` now requires `--kind`. Update scripts that omit it. Codex agents
142
+ register again from each conversation after the notifier is replaced. The
143
+ notifier creates `codex_queue_receipts.db` in the tabc home directory on its
144
+ first Codex queue submission. See the
145
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
146
+
111
147
  ## Updating from 0.1.6
112
148
 
113
149
  The upgrade preserves nodes, messages, delivery states, TAC membership, names,
@@ -125,6 +161,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
125
161
  Existing string TAC IDs are not converted during installation. Follow the
126
162
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
127
163
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
164
+ Then follow "Updating from 0.2.0" and "Updating from 0.2.1".
128
165
 
129
166
  ## Commands
130
167
 
@@ -136,7 +173,7 @@ to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
136
173
  - `tabc tac search --node alice --query text`: search joined topics without changing read state.
137
174
 
138
175
  Register each recipient before sending. tac creators must also join their topic.
139
- In 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
176
+ Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
140
177
  sending, reading, renaming and closing; use the unique name for display and search.
141
178
  Run `tabc --help` for available commands. The local server is `tabd`; management
142
179
  commands are provided separately by `tabm`.
@@ -163,6 +200,15 @@ Installation alone does not enable notifications. Automatic Enter defaults to of
163
200
  for new routes. Omitting the setting during registration preserves the same active
164
201
  route; `--auto-enter off` disables it explicitly.
165
202
 
203
+ `register` requires `--kind`. A Codex agent running under the native Codex
204
+ managed-daemon app-server can receive notifications in its conversation instead
205
+ of a terminal. Replace the notifier with this release first, then run
206
+ `tabc register --node <node> --kind codex --auto-enter on` from that
207
+ conversation. `--codex-queue` requires that route with no terminal fallback.
208
+ `QUEUED` means Codex accepted the notification input, not that it was read.
209
+ This path was checked with codex-cli 0.162.1 on macOS only. See the repository
210
+ README for conditions and limits.
211
+
166
212
  Do not connect notifications to an unreviewed command prompt.
167
213
  Automatic Enter does not guarantee a reply.
168
214
 
@@ -15,6 +15,7 @@ tabus/__init__.py
15
15
  tabus/admin.py
16
16
  tabus/bus.py
17
17
  tabus/cli.py
18
+ tabus/codex_queue.py
18
19
  tabus/compat_bcn.py
19
20
  tabus/compat_busd.py
20
21
  tabus/daemon.py
@@ -74,7 +74,7 @@ DB_PATH = os.environ.get(
74
74
  # drifted once already — 0.1.0.post1 shipped while this said 0.1.0. Bump
75
75
  # both together; tests/test_version_agreement.py fails the build otherwise.
76
76
  # Do not copy the number into documentation.
77
- __version__ = "0.2.0"
77
+ __version__ = "0.2.2"
78
78
  PROTOCOL_VERSION = "tabus.v1"
79
79
 
80
80
  # TTLs. Past these, a derived status counts as old.
@@ -251,10 +251,17 @@ def fn_config(a):
251
251
 
252
252
  def fn_register(a):
253
253
  node = _require_acting_node(a.node)
254
+ a.kind = a.kind.casefold()
254
255
  payload = {"node": node, "kind": a.kind}
255
256
  take_route = bool(getattr(a, "take_route", False))
256
257
  setting = getattr(a, "auto_enter", None)
257
258
  auto_enter = setting == "on"
259
+ if getattr(a, "codex_queue", False) and take_route:
260
+ print("--codex-queue does not support --take-route")
261
+ sys.exit(2)
262
+ if getattr(a, "codex_queue", False) and getattr(a, "program", None) is not None:
263
+ print("--codex-queue and --program cannot be used together")
264
+ sys.exit(2)
258
265
  if setting is not None:
259
266
  payload["auto_enter"] = auto_enter
260
267
  if getattr(a, "program", None) is not None and take_route:
@@ -290,6 +297,23 @@ def fn_register(a):
290
297
  else:
291
298
  captured = _capture_route()
292
299
  adapter, target = captured
300
+ # Codex shared-daemon commands have a thread identity, not a TTY owner.
301
+ # Automatic queue selection still requires explicit submission consent.
302
+ force_queue = getattr(a, "codex_queue", False)
303
+ if force_queue or (a.kind == "codex" and auto_enter):
304
+ if a.kind != "codex" or setting != "on":
305
+ print("--codex-queue requires --kind codex --auto-enter on")
306
+ sys.exit(2)
307
+ from .codex_queue import capture_current_thread
308
+ queue_target = capture_current_thread()
309
+ if queue_target is None and force_queue:
310
+ print("cannot verify the current Codex managed-daemon thread; registration not sent")
311
+ sys.exit(2)
312
+ if queue_target is not None:
313
+ if take_route:
314
+ print("Codex queue delivery does not support --take-route; registration not sent")
315
+ sys.exit(2)
316
+ adapter, target = "codex-queue", queue_target
293
317
  # A failed local capture cannot invalidate a previously stored route.
294
318
  # Do not send legacy auto-revocation fields, even to an older daemon.
295
319
  if not getattr(captured, "rejected", False) and take_route:
@@ -1061,7 +1085,9 @@ COMMANDS = {
1061
1085
  "register a node",
1062
1086
  [
1063
1087
  (("--node",), dict(required=True)),
1064
- (("--kind",), dict(default="generic")),
1088
+ (("--kind",), dict(required=True)),
1089
+ (("--codex-queue",), dict(action="store_true",
1090
+ help="require Codex queue delivery without terminal fallback; requires kind codex and auto-enter on")),
1065
1091
  (
1066
1092
  ("--take-route",),
1067
1093
  dict(
@@ -0,0 +1,194 @@
1
+ """Deliver notifications to an explicit Codex thread, without terminal input.
2
+
3
+ This module does not register routes or mark messages read. The receipt journal
4
+ records delivery attempts, not messages. An uncertain attempt is never retried
5
+ automatically: the queue command has no caller-supplied idempotency key.
6
+
7
+ Notification text is passed in argv and can be visible to local process tools.
8
+ Callers must pass notification metadata only, never message bodies or secrets.
9
+ Receipt parsing requires exactly one success line on stdout. Additional stdout
10
+ is treated as UNKNOWN even if submission may have succeeded.
11
+
12
+ Rollout order: deploy and verify the updated doorbell daemon first, then
13
+ register each intended Codex node with the documented options; an older daemon
14
+ returns DELIVER_UNSUPPORTED.
15
+ """
16
+
17
+ import os
18
+ import re
19
+ import sqlite3
20
+ import subprocess
21
+ import uuid
22
+
23
+
24
+ def deliver_notification(target, notification_id, message, auto_enter):
25
+ """Doorbell boundary: use only a recognized local native Codex binary."""
26
+ import pwd
27
+ import shutil
28
+ from . import paths
29
+ from .terminal_frontend import executable_kind
30
+ executable = shutil.which("codex")
31
+ if not executable or not notification_id:
32
+ return "UNKNOWN", -1
33
+ executable = os.path.realpath(executable)
34
+ if executable_kind(executable, pwd.getpwuid(os.getuid()).pw_dir) != "codex":
35
+ return "UNKNOWN", -1
36
+ try:
37
+ queue = QueueDelivery(paths.state("codex_queue_receipts.db"), executable)
38
+ try:
39
+ state, _ = queue.deliver(target, notification_id, message, auto_enter=auto_enter)
40
+ return state, 1 if state == "QUEUED" else 0
41
+ finally:
42
+ queue.close()
43
+ except (OSError, sqlite3.Error, ValueError):
44
+ return "UNKNOWN", -1
45
+
46
+
47
+ def capture_current_thread():
48
+ """Accept per-request thread identity only below a native managed daemon.
49
+
50
+ No terminal ownership is inferred. Shared daemon ancestry is used only to
51
+ identify the execution product. Same-OS-user environment forgery is outside
52
+ this local routing check, as for existing terminal route capture.
53
+ """
54
+ from .terminal_frontend import process_info, native_executable, executable_kind
55
+ import pwd
56
+ try:
57
+ target = current_thread()
58
+ uid = os.getuid()
59
+ home = pwd.getpwuid(uid).pw_dir
60
+ session = os.getsid(0)
61
+ pid = os.getpid()
62
+ seen = set()
63
+ for _ in range(64):
64
+ if pid <= 1 or pid in seen:
65
+ return None
66
+ seen.add(pid)
67
+ info = process_info(pid)
68
+ if info is None or info[0] != uid:
69
+ return None
70
+ if os.getsid(pid) != session:
71
+ executable = native_executable(pid)
72
+ if not executable or executable_kind(executable, home) != "codex":
73
+ return None
74
+ if info[2] != pid or info[4] not in ("??", "?"):
75
+ return None
76
+ args = info[5]
77
+ if os.path.basename(args[0]) != "codex" or args[1:] != [
78
+ "app-server", "--listen", "unix://",
79
+ "--analytics-default-enabled", "--managed-daemon",
80
+ ]:
81
+ return None
82
+ if process_info(pid) != info or native_executable(pid) != executable:
83
+ return None
84
+ return target
85
+ pid = info[1]
86
+ except (OSError, ValueError, TypeError, IndexError, subprocess.SubprocessError):
87
+ return None
88
+ return None
89
+
90
+
91
+ def thread_id(value):
92
+ """Require a canonical UUID; session names are not routing identities."""
93
+ if not isinstance(value, str):
94
+ raise ValueError("Codex thread must be a canonical UUID")
95
+ try:
96
+ valid = str(uuid.UUID(value)) == value
97
+ except ValueError:
98
+ valid = False
99
+ if not valid:
100
+ raise ValueError("Codex thread must be a canonical UUID")
101
+ return value
102
+
103
+
104
+ def current_thread(environ=None):
105
+ """Read the execution context, not the shared daemon's inherited tab ID.
106
+
107
+ This is a candidate identity only, not proof of terminal ownership.
108
+ Registration must separately validate the Codex execution context.
109
+ """
110
+ env = os.environ if environ is None else environ
111
+ return thread_id(env.get("CODEX_THREAD_ID"))
112
+
113
+
114
+ class QueueDelivery:
115
+ """One attempted submission per (thread UUID, notification identity).
116
+
117
+ Use a dedicated local receipt database. The caller supplies an absolute,
118
+ trusted Codex executable path; no route may supply executable arguments.
119
+ The caller must reuse the same delivery_id after restart, timeout, or any
120
+ exception, including a receipt UPDATE/commit failure after queue acceptance.
121
+ Never allocate a new identity merely to retry an uncertain submission.
122
+ """
123
+
124
+ def __init__(self, receipt_db, executable, run=None):
125
+ if not os.path.isabs(executable):
126
+ raise ValueError("Codex executable must be an absolute path")
127
+ self.executable = executable
128
+ self.run = run or subprocess.run
129
+ self.db = sqlite3.connect(receipt_db, timeout=5)
130
+ self.db.execute("""CREATE TABLE IF NOT EXISTS codex_queue_receipts (
131
+ thread_id TEXT NOT NULL,
132
+ delivery_id TEXT NOT NULL,
133
+ state TEXT NOT NULL CHECK(state IN ('PENDING','QUEUED','UNKNOWN')),
134
+ submission_id TEXT,
135
+ PRIMARY KEY(thread_id, delivery_id)
136
+ )""")
137
+ self.db.commit()
138
+
139
+ def close(self):
140
+ self.db.close()
141
+
142
+ def deliver(self, target, delivery_id, message, *, auto_enter=False):
143
+ target = thread_id(target)
144
+ if not isinstance(delivery_id, str) or not delivery_id:
145
+ raise ValueError("stable delivery_id required")
146
+ if not isinstance(message, str) or not message or "\x00" in message:
147
+ raise ValueError("nonempty notification text required")
148
+ # Queue submits input; it has no equivalent to typing without Enter.
149
+ if auto_enter is not True:
150
+ return "DISABLED", None
151
+
152
+ self.db.execute("BEGIN IMMEDIATE")
153
+ try:
154
+ row = self.db.execute(
155
+ "SELECT state, submission_id FROM codex_queue_receipts "
156
+ "WHERE thread_id=? AND delivery_id=?", (target, delivery_id),
157
+ ).fetchone()
158
+ if row:
159
+ self.db.commit()
160
+ return ("UNKNOWN" if row[0] == "PENDING" else row[0]), row[1]
161
+ self.db.execute(
162
+ "INSERT INTO codex_queue_receipts VALUES (?, ?, 'PENDING', NULL)",
163
+ (target, delivery_id),
164
+ )
165
+ self.db.commit()
166
+ except BaseException:
167
+ self.db.rollback()
168
+ raise
169
+
170
+ state, submission = "UNKNOWN", None
171
+ try:
172
+ result = self.run(
173
+ [self.executable, "queue", "--remote=unix://",
174
+ f"--thread={target}", f"--message={message}"],
175
+ stdin=subprocess.DEVNULL, capture_output=True, text=True,
176
+ timeout=15, check=False,
177
+ )
178
+ receipt = re.fullmatch(
179
+ r"Queued message ([0-9a-f-]{36}) for thread " + re.escape(target) + r"\.",
180
+ result.stdout.strip(),
181
+ )
182
+ if result.returncode == 0 and receipt:
183
+ submission = thread_id(receipt.group(1))
184
+ state = "QUEUED"
185
+ except (OSError, subprocess.SubprocessError, ValueError):
186
+ # Failure can follow acceptance. Do not translate it to safe retry.
187
+ pass
188
+ self.db.execute(
189
+ "UPDATE codex_queue_receipts SET state=?, submission_id=? "
190
+ "WHERE thread_id=? AND delivery_id=?",
191
+ (state, submission, target, delivery_id),
192
+ )
193
+ self.db.commit()
194
+ return state, submission
@@ -307,7 +307,7 @@ def format_doorbell_who(senders):
307
307
  return f"{senders[0]['sender']}"
308
308
 
309
309
 
310
- def deliver_doorbell(node_id, unread):
310
+ def deliver_doorbell(node_id, unread, notification_id=None):
311
311
  """Attempt one delivery. Returns (outcome, adapter, match_count).
312
312
 
313
313
  🔴 It writes no ledger row; that belongs to the caller.
@@ -364,6 +364,10 @@ def deliver_doorbell(node_id, unread):
364
364
  return DELIVER_NOT_MINE, "-", 0
365
365
  adapter, target, *settings = route
366
366
  auto_enter = bool(settings[0]) if settings else False
367
+ if adapter == "codex-queue":
368
+ from .codex_queue import deliver_notification
369
+ result, matched = deliver_notification(target, notification_id, msg, auto_enter)
370
+ return result, adapter, matched
367
371
  # 🔴 The adapter dispatch stays an if-chain. It was briefly a name-to-function
368
372
  # table and was reverted: the table freezes function references at import
369
373
  # time, which stopped tests from substituting them — three of sixteen failed
@@ -470,7 +474,8 @@ def main():
470
474
  flush=True,
471
475
  )
472
476
  continue
473
- result, adapter, matched = deliver_doorbell(rcpt, d["unread"])
477
+ result, adapter, matched = deliver_doorbell(
478
+ rcpt, d["unread"], notification_id=f"{rcpt}:{d['high_id']}")
474
479
  # 🔴 An announcement overriding observation mode lands here too, and
475
480
  # the recorded mode stays as the mode it actually ran in — that is
476
481
  # the axis the watermark is measured on. Only the log tag differs.
@@ -61,7 +61,7 @@ LEDGER_DB = os.environ.get(
61
61
  # The legacy entry covers rings that went out through the old path. Those did
62
62
  # ring, so they suppress. It has its own name so it can be counted; when it
63
63
  # reaches zero, that path can be deleted.
64
- SUPPRESSING = ("SUCCESS", "SUCCESS_LEGACY", "SHADOW_ONLY", "TEXT_ONLY")
64
+ SUPPRESSING = ("SUCCESS", "SUCCESS_LEGACY", "SHADOW_ONLY", "TEXT_ONLY", "QUEUED")
65
65
  UNKNOWN_RETRY_SECONDS = 30
66
66
  _unknown_attempts = {}
67
67
 
@@ -6,8 +6,8 @@ that terminal. A detached shared runner can therefore carry one tab's route,
6
6
  allocate a fresh PTY, and still register the stale tab. Route capture accepts
7
7
  the inherited key only when its resolved TTY equals the controlling TTY of this
8
8
  process or a same-session ancestor. A detached command may also use its immediate
9
- session-boundary parent when that parent is a recognized native interactive CLI
10
- and the foreground process-group leader of the target terminal.
9
+ session-boundary parent when that parent owns the target terminal foreground
10
+ process group and its standard input is that same terminal, regardless of kind.
11
11
  """
12
12
 
13
13
  import os
@@ -99,7 +99,7 @@ def terminal_tty(
99
99
 
100
100
  ``ps`` is used instead of ``/proc`` so this works on both macOS and Linux.
101
101
  Any lookup failure is a negative result. The cross-session fallback is
102
- restricted to a checked native foreground CLI, not arbitrary ancestors.
102
+ restricted to a checked foreground terminal owner, not arbitrary ancestors.
103
103
  """
104
104
  inspect = process_row or _process_row
105
105
  inspect_session = process_session or _session_id
@@ -1,7 +1,7 @@
1
- """Recognize a foreground native agent CLI, never a generic terminal ancestor.
1
+ """Verify a foreground terminal owner without a product-name allowlist.
2
2
 
3
3
  This is local routing evidence, not an isolation boundary against a malicious
4
- process running as the same OS user. Unknown launch modes fail closed.
4
+ process running as the same OS user. Missing terminal evidence fails closed.
5
5
  """
6
6
 
7
7
  import ctypes
@@ -57,7 +57,7 @@ def executable_kind(executable, user_home):
57
57
 
58
58
 
59
59
  def interactive_arguments(kind, argv):
60
- """Allow inspected interactive modes only; no arbitrary prompt/subcommand."""
60
+ """Legacy argument classifier; not used by generic route registration."""
61
61
  if not argv or os.path.basename(argv[0]) != kind:
62
62
  return False
63
63
  flags = {
@@ -120,11 +120,27 @@ def process_info(pid):
120
120
  fields[4], args)
121
121
 
122
122
 
123
+ def input_terminal(pid):
124
+ """Resolve fd 0, not inherited environment variables or a process name."""
125
+ if sys.platform.startswith("linux"):
126
+ return os.path.realpath(os.readlink(f"/proc/{int(pid)}/fd/0"))
127
+ if sys.platform == "darwin":
128
+ result = subprocess.run(
129
+ ["/usr/sbin/lsof", "-a", "-p", str(int(pid)), "-d", "0", "-Fn"],
130
+ capture_output=True, text=True, timeout=1, check=False,
131
+ )
132
+ names = [line[1:] for line in result.stdout.splitlines() if line.startswith("n")]
133
+ if result.returncode == 0 and len(names) == 1:
134
+ return names[0]
135
+ return None
136
+
137
+
123
138
  def foreground_frontend_tty(pid, expected_tty, inspect=None, executable=None,
124
- uid=None, user_home=None):
139
+ uid=None, user_home=None, stdin_terminal=None):
125
140
  """Only the boundary parent itself may qualify; never walk past a server."""
126
141
  inspect = inspect or process_info
127
142
  executable = executable or native_executable
143
+ stdin_terminal = stdin_terminal or input_terminal
128
144
  uid = os.getuid() if uid is None else uid
129
145
  try:
130
146
  user_home = pwd.getpwuid(uid).pw_dir if user_home is None else user_home
@@ -135,11 +151,15 @@ def foreground_frontend_tty(pid, expected_tty, inspect=None, executable=None,
135
151
  if owner != uid or not (pid == group == foreground) or tty != expected_tty:
136
152
  return None
137
153
  path = executable(pid)
138
- kind = executable_kind(path, user_home) if path else None
139
- if kind is None or not interactive_arguments(kind, argv):
154
+ # Generic terminal routing is independent of kind and installation path.
155
+ # The immediate boundary parent must own the foreground group AND read
156
+ # from that terminal. A pipe-backed server is not a terminal recipient.
157
+ input_path = "/dev/" + expected_tty.removeprefix("/dev/")
158
+ if not path or stdin_terminal(pid) != input_path:
140
159
  return None
141
160
  # Recheck mutable process state around executable and command inspection.
142
- if inspect(pid) != before or executable(pid) != path:
161
+ if (inspect(pid) != before or executable(pid) != path
162
+ or stdin_terminal(pid) != input_path):
143
163
  return None
144
164
  return tty
145
165
  except (OSError, ValueError, TypeError, KeyError, ctypes.ArgumentError,
tabc-0.2.0/CHANGELOG.md DELETED
@@ -1,80 +0,0 @@
1
- # Changelog
2
-
3
- ## 0.2.0 — Unreleased
4
-
5
- ### Added
6
-
7
- - An optional local stdio MCP adapter for desktop agents.
8
- - Structured refusal codes and details for send and TAC errors.
9
- - UTF-8 byte limits: 1,024 bytes for a subject, 65,536 bytes for a body, and
10
- 524,288 bytes for an HTTP request. Oversized input is rejected before storage.
11
- - Server-minted canonical UUIDs for TAC identity. TAC names remain unique,
12
- searchable, and renameable without changing the UUID.
13
- - TAC `rename`, `check`, and member-scoped literal `search` commands.
14
- - A copy-first migration tool for existing string TAC identifiers.
15
- - Unicode-consistent literal TAC search across names, descriptions, summaries,
16
- subjects, and bodies, including NFC/NFD-equivalent text.
17
-
18
- ### Changed
19
-
20
- - MCP responses preserve structured error codes and details from tabd.
21
- - TAC commands address converted topics by UUID. Names are used for creation,
22
- display, and search.
23
- - `tac show --node` advances only the returned deliveries to `INJECTED`.
24
- It does not mark them `READ`, and older unreturned messages may still block a send.
25
- - The pending count printed after a send is scoped to that sender and recipient.
26
- `tabc who` remains the total unread view.
27
- - Terminal route capture now validates the native foreground Codex or Claude CLI
28
- at a detached session boundary. Unknown launch modes and mismatched terminals
29
- fail closed. New or moved routes keep automatic Enter off unless explicitly
30
- enabled with `register --auto-enter on`.
31
-
32
- ### Protocol details
33
-
34
- - A fresh 0.2.0 ledger mints canonical lowercase, hyphenated UUIDs for TACs.
35
- Existing ledgers retain string TAC IDs until explicit conversion.
36
- - A TAC name is trimmed at its edges and compared as
37
- `NFC(casefold(NFC(name)))`. Control, format, surrogate, private-use, unassigned,
38
- and non-ordinary whitespace characters are refused. Unicode category results
39
- follow the Python version running tabd.
40
- - A partially converted ledger that has identity columns but still contains
41
- string TAC IDs refuses TAC creation and rename with `TAC_NOT_CONVERTED`.
42
- - Missing canonical TAC IDs return `TAC_NOT_FOUND`. Read-only name lookup can
43
- report `exists: false`; names are not accepted where a TAC UUID designates the
44
- target.
45
- - Structured server refusal codes and retry rules are listed in
46
- [GUIDE.md](GUIDE.md#refusal-codes). MCP preserves those fields without truncation.
47
-
48
- ### Upgrade note
49
-
50
- An upgrade from 0.1.6 preserves nodes, messages, delivery states, TAC membership,
51
- names, and labels. Package installation and server startup do not convert TAC IDs.
52
- Pre-0.2.0 ledgers keep their string TAC identifiers until explicitly converted.
53
- Stop `tabus.doorbell` and `tabd`. Confirm that `<ledger.db>-wal` is absent or
54
- empty (0 bytes). A remaining empty WAL or SHM file is safe. If the WAL is not
55
- empty, start `tabd` and stop it cleanly, then check again. Back up the main
56
- database only after the WAL is absent or empty. Install the required package,
57
- then run a read-only preflight and write a separate converted database:
58
-
59
- ```bash
60
- python -m pip install --upgrade "tabc==0.2.0"
61
- # Or, for desktop MCP support:
62
- python -m pip install --upgrade "tabc[mcp]==0.2.0"
63
-
64
- python -m tabus.tac_migration <ledger.db>
65
- python -m tabus.tac_migration <ledger.db> --output <converted.db>
66
- ```
67
-
68
- Replace only the main database file after the row counts and converted database
69
- are verified. Do not replace it while its WAL file is non-empty. A remaining
70
- empty WAL or SHM file is safe. Restart `tabd` first, then restart
71
- `tabus.doorbell`. Do not run old and new daemons against the same active ledger
72
- during the swap.
73
- After conversion, commands that use a TAC name instead of its UUID are refused
74
- with `TAC_ID_INVALID`. Scripts must use the UUID printed by `tabc tac ls`.
75
- After restart, use `tabc --version` and `tabd --version` to confirm that both
76
- commands resolve to 0.2.0.
77
-
78
- When replacing an installed notifier, stop the old `tabus.doorbell` first, restart
79
- `tabd`, then start the new notifier. See the registration notes in
80
- [README.md](README.md#node-identity-and-signing-keys).
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes