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.
- tabc-0.2.2/CHANGELOG.md +173 -0
- {tabc-0.2.0 → tabc-0.2.2}/GUIDE.md +45 -6
- {tabc-0.2.0 → tabc-0.2.2}/MCP.md +6 -0
- {tabc-0.2.0/tabc.egg-info → tabc-0.2.2}/PKG-INFO +55 -9
- {tabc-0.2.0 → tabc-0.2.2}/PYPI.md +52 -3
- {tabc-0.2.0 → tabc-0.2.2}/pyproject.toml +1 -1
- {tabc-0.2.0 → tabc-0.2.2/tabc.egg-info}/PKG-INFO +55 -9
- {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/SOURCES.txt +1 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/bus.py +1 -1
- {tabc-0.2.0 → tabc-0.2.2}/tabus/cli.py +27 -1
- tabc-0.2.2/tabus/codex_queue.py +194 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/doorbell.py +7 -2
- {tabc-0.2.0 → tabc-0.2.2}/tabus/doorbell_poll.py +1 -1
- {tabc-0.2.0 → tabc-0.2.2}/tabus/route_capture.py +3 -3
- {tabc-0.2.0 → tabc-0.2.2}/tabus/terminal_frontend.py +27 -7
- tabc-0.2.0/CHANGELOG.md +0 -80
- {tabc-0.2.0 → tabc-0.2.2}/LICENSE +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/MANIFEST.in +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/setup.cfg +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/dependency_links.txt +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/entry_points.txt +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/requires.txt +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabc.egg-info/top_level.txt +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/__init__.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/admin.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/compat_bcn.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/compat_busd.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/daemon.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/lang.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/locales/en.json +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/mcp_server.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/nodekey.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/paths.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/process_argv.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/send_refusal.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/sent.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_identity.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_migration.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_refusal.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_search.py +0 -0
- {tabc-0.2.0 → tabc-0.2.2}/tabus/tac_store.py +0 -0
tabc-0.2.2/CHANGELOG.md
ADDED
|
@@ -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
|
-
|
|
193
|
-
|
|
194
|
-
`
|
|
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
|
-
-
|
|
279
|
-
routes, and turning it on is a decision about that
|
|
280
|
-
|
|
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
|
|
{tabc-0.2.0 → tabc-0.2.2}/MCP.md
RENAMED
|
@@ -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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: tabc
|
|
3
|
-
Version: 0.2.
|
|
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
|
-
|
|
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.
|
|
106
|
-
the `pull --mode full` detail block,
|
|
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
|
-
|
|
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.
|
|
61
|
-
the `pull --mode full` detail block,
|
|
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
|
-
|
|
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
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: tabc
|
|
3
|
-
Version: 0.2.
|
|
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
|
-
|
|
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.
|
|
106
|
-
the `pull --mode full` detail block,
|
|
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
|
-
|
|
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
|
|
|
@@ -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.
|
|
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(
|
|
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(
|
|
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
|
|
10
|
-
and
|
|
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
|
|
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
|
-
"""
|
|
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.
|
|
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
|
-
"""
|
|
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
|
-
|
|
139
|
-
|
|
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
|
|
File without changes
|
|
File without changes
|