tabc 0.2.1__tar.gz → 0.2.3__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.1 → tabc-0.2.3}/CHANGELOG.md +93 -0
  2. {tabc-0.2.1 → tabc-0.2.3}/GUIDE.md +9 -2
  3. {tabc-0.2.1 → tabc-0.2.3}/MCP.md +2 -1
  4. {tabc-0.2.1/tabc.egg-info → tabc-0.2.3}/PKG-INFO +47 -7
  5. {tabc-0.2.1 → tabc-0.2.3}/PYPI.md +44 -1
  6. {tabc-0.2.1 → tabc-0.2.3}/pyproject.toml +1 -1
  7. {tabc-0.2.1 → tabc-0.2.3/tabc.egg-info}/PKG-INFO +47 -7
  8. {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/SOURCES.txt +1 -0
  9. {tabc-0.2.1 → tabc-0.2.3}/tabus/bus.py +1 -1
  10. {tabc-0.2.1 → tabc-0.2.3}/tabus/cli.py +72 -3
  11. {tabc-0.2.1 → tabc-0.2.3}/tabus/daemon.py +21 -0
  12. {tabc-0.2.1 → tabc-0.2.3}/tabus/route_capture.py +3 -3
  13. tabc-0.2.3/tabus/tac_lookup.py +42 -0
  14. {tabc-0.2.1 → tabc-0.2.3}/tabus/terminal_frontend.py +27 -7
  15. {tabc-0.2.1 → tabc-0.2.3}/LICENSE +0 -0
  16. {tabc-0.2.1 → tabc-0.2.3}/MANIFEST.in +0 -0
  17. {tabc-0.2.1 → tabc-0.2.3}/setup.cfg +0 -0
  18. {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/dependency_links.txt +0 -0
  19. {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/entry_points.txt +0 -0
  20. {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/requires.txt +0 -0
  21. {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/top_level.txt +0 -0
  22. {tabc-0.2.1 → tabc-0.2.3}/tabus/__init__.py +0 -0
  23. {tabc-0.2.1 → tabc-0.2.3}/tabus/admin.py +0 -0
  24. {tabc-0.2.1 → tabc-0.2.3}/tabus/codex_queue.py +0 -0
  25. {tabc-0.2.1 → tabc-0.2.3}/tabus/compat_bcn.py +0 -0
  26. {tabc-0.2.1 → tabc-0.2.3}/tabus/compat_busd.py +0 -0
  27. {tabc-0.2.1 → tabc-0.2.3}/tabus/doorbell.py +0 -0
  28. {tabc-0.2.1 → tabc-0.2.3}/tabus/doorbell_poll.py +0 -0
  29. {tabc-0.2.1 → tabc-0.2.3}/tabus/lang.py +0 -0
  30. {tabc-0.2.1 → tabc-0.2.3}/tabus/locales/en.json +0 -0
  31. {tabc-0.2.1 → tabc-0.2.3}/tabus/mcp_server.py +0 -0
  32. {tabc-0.2.1 → tabc-0.2.3}/tabus/nodekey.py +0 -0
  33. {tabc-0.2.1 → tabc-0.2.3}/tabus/paths.py +0 -0
  34. {tabc-0.2.1 → tabc-0.2.3}/tabus/process_argv.py +0 -0
  35. {tabc-0.2.1 → tabc-0.2.3}/tabus/send_refusal.py +0 -0
  36. {tabc-0.2.1 → tabc-0.2.3}/tabus/sent.py +0 -0
  37. {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_identity.py +0 -0
  38. {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_migration.py +0 -0
  39. {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_refusal.py +0 -0
  40. {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_search.py +0 -0
  41. {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_store.py +0 -0
@@ -1,5 +1,98 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.2.3 — 2026-10-17
4
+
5
+ ### Added
6
+
7
+ - The CLI accepts the exact name of an open tac, compared after case folding,
8
+ wherever it takes a tac:
9
+ `tac show`, `add`, `rm`, `rename` (the target, not the new name), `close`,
10
+ `link` (child and parent), `search --tac`, and `send --tac`. It prints
11
+ `tac <name> = <uuid>` on stderr and runs with that UUID.
12
+ - A closed tac, a missing name, or names that only resemble the input run
13
+ nothing. The CLI lists the candidates with UUID and `open` or `closed` date,
14
+ prints `code TAC_NAME_UNRESOLVED`, and exits 2. A partial name is never
15
+ chosen.
16
+ - A read-only, signed server endpoint, `GET /tac_lookup?name=`, returns the
17
+ exact match and up to 20 similar names within the same scope as `/tacs`.
18
+
19
+ ### Unchanged
20
+
21
+ - `tac create` does not look up its name.
22
+ - A canonical lowercase UUID is used as given, without a lookup. Any other
23
+ string is looked up as a name first. Only when no tac has that exact name is
24
+ another spelling of a UUID (upper case, braces, 32 hex digits, `urn:uuid:`)
25
+ sent as that identifier.
26
+ - Every other server endpoint and the MCP adapter take only the UUID. An older
27
+ `tabd` without the lookup endpoint receives the typed value unchanged,
28
+ including another UUID spelling, and answers `TAC_ID_INVALID` as before.
29
+ - The ledger schema, registration, the notifier, and the MCP adapter are
30
+ unchanged.
31
+
32
+ ### Upgrade notes (from 0.2.2)
33
+
34
+ - Nodes, signing keys, messages, delivery states, TACs, routes, and Codex
35
+ queue receipts are kept. No conversion and no re-registration are needed.
36
+ - Stop `tabus.doorbell`, install 0.2.3, restart `tabd`, then start one
37
+ `tabus.doorbell` from the same installation:
38
+
39
+ ```bash
40
+ python -m pip install --upgrade "tabc==0.2.3"
41
+ # Or, for desktop MCP support:
42
+ python -m pip install --upgrade "tabc[mcp]==0.2.3"
43
+ ```
44
+
45
+ - Name lookup needs the restarted `tabd`. Before the restart it answers 404 for
46
+ `/tac_lookup`, the CLI sends the typed name, and the server answers
47
+ `TAC_ID_INVALID`. UUIDs work either way.
48
+ - A running `tabc-mcp` process does not need a restart for this change.
49
+ - After restart, `tabc --version` and `tabd --version` report 0.2.3.
50
+
51
+ ## 0.2.2 — 2026-10-17
52
+
53
+ ### Changed
54
+
55
+ - `register` treats `--kind` case-insensitively and stores it in lowercase.
56
+ `--kind OpenCode` registers `opencode`.
57
+ - Terminal route capture at a detached session boundary no longer requires a
58
+ recognized Codex or Claude CLI. The immediate session-boundary parent
59
+ qualifies when it belongs to the same OS user, leads the target terminal's
60
+ foreground process group, and reads standard input from that terminal.
61
+ Process state, executable path, and standard input are checked again before
62
+ the route is accepted. Product names and install directories are not an
63
+ admission list.
64
+ - Codex queue selection under `--kind codex --auto-enter on` and its fallback
65
+ rules are unchanged. Because the kind is case-folded, `--kind Codex` now
66
+ selects the same queue route.
67
+
68
+ ### Notes
69
+
70
+ - The check verifies terminal ownership, not whether the program is an agent.
71
+ A foreground program that reads the terminal can qualify even if it is not
72
+ an agent. Automatic Enter remains a separate opt-in.
73
+ - No dedicated adapter for another agent product is added. The MCP adapter
74
+ still does not wake an agent.
75
+
76
+ ### Upgrade notes (from 0.2.1)
77
+
78
+ - The ledger schema is unchanged. Nodes, signing keys, messages, delivery
79
+ states, TACs, routes, and Codex queue receipts are kept.
80
+ - Stop `tabus.doorbell`, install 0.2.2, restart `tabd`, then start one
81
+ `tabus.doorbell` from the same installation:
82
+
83
+ ```bash
84
+ python -m pip install --upgrade "tabc==0.2.2"
85
+ # Or, for desktop MCP support:
86
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
87
+ ```
88
+
89
+ - Stored routes are not migrated. A failed capture does not replace an
90
+ existing route. Register again from the intended terminal, then confirm one
91
+ notification arrives.
92
+ - If a desktop client runs `tabc-mcp`, restart that MCP process from the client
93
+ after the upgrade so it loads the new installation.
94
+ - After restart, `tabc --version` and `tabd --version` report 0.2.2.
95
+
3
96
  ## 0.2.1 — 2026-10-15
4
97
 
5
98
  ### Added
@@ -269,12 +269,19 @@ tabd returns a stable `code`, a plain `message`, structured `details`, and a
269
269
  | `REQUEST_TIMEOUT` | the declared request body did not arrive within 30 seconds |
270
270
  | `REQUEST_INCOMPLETE` | the connection ended before the declared body length arrived |
271
271
  | `UNREAD_BLOCKED` | read-before-send is blocking this DM or TAC send |
272
- | `TAC_ID_INVALID` | a TAC operation requires a canonical lowercase UUID |
272
+ | `TAC_ID_INVALID` | a TAC operation requires a canonical lowercase UUID; the server does not resolve names |
273
273
  | `TAC_NOT_FOUND` | no TAC matches the supplied identifier |
274
274
  | `TAC_NAME_TAKEN` | the folded name or a reserved legacy identifier is already used |
275
275
  | `TAC_NAME_INVALID` | the TAC name violates the name rules |
276
276
  | `TAC_NOT_CONVERTED` | a partially converted ledger still contains string TAC IDs |
277
277
 
278
+ From 0.2.3, the CLI adds one code of its own before the requested TAC operation is sent.
279
+ `TAC_NAME_UNRESOLVED` means the TAC name was not exactly one open tac: it is
280
+ closed, missing, or only similar names exist. Nothing was done. The CLI lists the
281
+ candidates with UUID and state on stderr and exits 2. Use a listed UUID, or check
282
+ the name with `tabc tac ls`. The lookup accepts names of 1 to 200 characters. A
283
+ longer value fails the lookup, runs nothing, and exits 1; use the UUID instead.
284
+
278
285
  `retry=never` means change the request. `retry=after_condition` means satisfy the
279
286
  reported condition first. `retry=as_is` means the same request may be tried again,
280
287
  but a write that lost its response must first be checked by message ID. A timeout,
@@ -295,7 +302,7 @@ The same visible text can therefore use different byte counts in NFC and NFD.
295
302
  | dm | a message addressed to named recipients (`send --to`) |
296
303
  | tac | a named topic. Members receive everything posted to it |
297
304
  | tac name | a unique display and search name. It can change |
298
- | tac id | a canonical UUID minted by the server. It addresses the tac and does not change when the name changes |
305
+ | tac id | a canonical UUID minted by the server. It addresses the tac and does not change when the name changes. From 0.2.3 the CLI also resolves an exact open tac name to it |
299
306
  | program | a send-only node. It claims no terminal and is refused as a recipient |
300
307
 
301
308
  ---
@@ -114,7 +114,8 @@ titles without that wait, including messages claimed by an earlier call. Use
114
114
 
115
115
  Use the CLI to create TACs and manage membership. Use the `tac_id` returned by
116
116
  `tabc_tacs`, not its display name. A TAC name can be renamed; its canonical UUID
117
- does not change. TAC history inspection does not change delivery state. Before
117
+ does not change. The CLI resolves an exact open tac name from 0.2.3; the MCP
118
+ adapter does not. TAC history inspection does not change delivery state. Before
118
119
  sending, open and acknowledge the relevant pending deliveries using the inbox
119
120
  tools. tabd enforces the same read-before-send rules as it does for the CLI.
120
121
 
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.1
2
2
  Name: tabc
3
- Version: 0.2.1
3
+ Version: 0.2.3
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
 
@@ -108,6 +105,46 @@ from the `pull --mode full` detail block, or a unique prefix of it.
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.2
109
+
110
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
111
+ TACs, routes, and Codex queue receipts are kept, and nodes do not register
112
+ again. Stop `tabus.doorbell`, install, restart `tabd`, then start one
113
+ `tabus.doorbell` from the same installation:
114
+
115
+ ```bash
116
+ python -m pip install --upgrade "tabc==0.2.3"
117
+ # Or, when the local desktop MCP adapter is required:
118
+ python -m pip install --upgrade "tabc[mcp]==0.2.3"
119
+ ```
120
+
121
+ The CLI now accepts the exact name of an open tac and lists candidates instead
122
+ of guessing. Name lookup needs the restarted `tabd`; before that, a typed name
123
+ is refused with `TAC_ID_INVALID` and UUIDs still work. The MCP adapter is
124
+ unchanged. See the
125
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
126
+
127
+ ## Updating from 0.2.1
128
+
129
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
130
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
131
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
132
+
133
+ ```bash
134
+ python -m pip install --upgrade "tabc==0.2.2"
135
+ # Or, when the local desktop MCP adapter is required:
136
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
137
+ ```
138
+
139
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
140
+ route as `--kind codex`. A detached command no longer needs a recognized agent
141
+ product: its immediate session-boundary parent qualifies when it owns the
142
+ terminal foreground and reads standard input from it. The check proves terminal
143
+ ownership, not that the program is an agent. Register again from the intended
144
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
145
+ unchanged. See the
146
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
147
+
111
148
  ## Updating from 0.2.0
112
149
 
113
150
  The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
@@ -143,7 +180,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
143
180
  Existing string TAC IDs are not converted during installation. Follow the
144
181
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
145
182
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
146
- Then follow "Updating from 0.2.0".
183
+ Then follow "Updating from 0.2.0", "Updating from 0.2.1" and "Updating from 0.2.2".
147
184
 
148
185
  ## Commands
149
186
 
@@ -157,6 +194,9 @@ Then follow "Updating from 0.2.0".
157
194
  Register each recipient before sending. tac creators must also join their topic.
158
195
  Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
159
196
  sending, reading, renaming and closing; use the unique name for display and search.
197
+ From 0.2.3, the CLI also accepts the exact name of an open tac and prints the UUID
198
+ it used. A closed, missing or only similar name runs nothing and lists candidates
199
+ with `TAC_NAME_UNRESOLVED`. The MCP adapter still takes only the UUID.
160
200
  Run `tabc --help` for available commands. The local server is `tabd`; management
161
201
  commands are provided separately by `tabm`.
162
202
  The source repository carries the operating guide and the security notes.
@@ -63,6 +63,46 @@ from the `pull --mode full` detail block, or a unique prefix of it.
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.2
67
+
68
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
69
+ TACs, routes, and Codex queue receipts are kept, and nodes do not register
70
+ again. Stop `tabus.doorbell`, install, restart `tabd`, then start one
71
+ `tabus.doorbell` from the same installation:
72
+
73
+ ```bash
74
+ python -m pip install --upgrade "tabc==0.2.3"
75
+ # Or, when the local desktop MCP adapter is required:
76
+ python -m pip install --upgrade "tabc[mcp]==0.2.3"
77
+ ```
78
+
79
+ The CLI now accepts the exact name of an open tac and lists candidates instead
80
+ of guessing. Name lookup needs the restarted `tabd`; before that, a typed name
81
+ is refused with `TAC_ID_INVALID` and UUIDs still work. The MCP adapter is
82
+ unchanged. See the
83
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
84
+
85
+ ## Updating from 0.2.1
86
+
87
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
88
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
89
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
90
+
91
+ ```bash
92
+ python -m pip install --upgrade "tabc==0.2.2"
93
+ # Or, when the local desktop MCP adapter is required:
94
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
95
+ ```
96
+
97
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
98
+ route as `--kind codex`. A detached command no longer needs a recognized agent
99
+ product: its immediate session-boundary parent qualifies when it owns the
100
+ terminal foreground and reads standard input from it. The check proves terminal
101
+ ownership, not that the program is an agent. Register again from the intended
102
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
103
+ unchanged. See the
104
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
105
+
66
106
  ## Updating from 0.2.0
67
107
 
68
108
  The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
@@ -98,7 +138,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
98
138
  Existing string TAC IDs are not converted during installation. Follow the
99
139
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
100
140
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
101
- Then follow "Updating from 0.2.0".
141
+ Then follow "Updating from 0.2.0", "Updating from 0.2.1" and "Updating from 0.2.2".
102
142
 
103
143
  ## Commands
104
144
 
@@ -112,6 +152,9 @@ Then follow "Updating from 0.2.0".
112
152
  Register each recipient before sending. tac creators must also join their topic.
113
153
  Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
114
154
  sending, reading, renaming and closing; use the unique name for display and search.
155
+ From 0.2.3, the CLI also accepts the exact name of an open tac and prints the UUID
156
+ it used. A closed, missing or only similar name runs nothing and lists candidates
157
+ with `TAC_NAME_UNRESOLVED`. The MCP adapter still takes only the UUID.
115
158
  Run `tabc --help` for available commands. The local server is `tabd`; management
116
159
  commands are provided separately by `tabm`.
117
160
  The source repository carries the operating guide and the security notes.
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "tabc"
7
- version = "0.2.1"
7
+ version = "0.2.3"
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.1
3
+ Version: 0.2.3
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
 
@@ -108,6 +105,46 @@ from the `pull --mode full` detail block, or a unique prefix of it.
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.2
109
+
110
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
111
+ TACs, routes, and Codex queue receipts are kept, and nodes do not register
112
+ again. Stop `tabus.doorbell`, install, restart `tabd`, then start one
113
+ `tabus.doorbell` from the same installation:
114
+
115
+ ```bash
116
+ python -m pip install --upgrade "tabc==0.2.3"
117
+ # Or, when the local desktop MCP adapter is required:
118
+ python -m pip install --upgrade "tabc[mcp]==0.2.3"
119
+ ```
120
+
121
+ The CLI now accepts the exact name of an open tac and lists candidates instead
122
+ of guessing. Name lookup needs the restarted `tabd`; before that, a typed name
123
+ is refused with `TAC_ID_INVALID` and UUIDs still work. The MCP adapter is
124
+ unchanged. See the
125
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
126
+
127
+ ## Updating from 0.2.1
128
+
129
+ The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
130
+ TACs, routes, and Codex queue receipts are kept. Stop `tabus.doorbell`, install,
131
+ restart `tabd`, then start one `tabus.doorbell` from the same installation:
132
+
133
+ ```bash
134
+ python -m pip install --upgrade "tabc==0.2.2"
135
+ # Or, when the local desktop MCP adapter is required:
136
+ python -m pip install --upgrade "tabc[mcp]==0.2.2"
137
+ ```
138
+
139
+ `--kind` is now case-insensitive; `--kind Codex` selects the same Codex queue
140
+ route as `--kind codex`. A detached command no longer needs a recognized agent
141
+ product: its immediate session-boundary parent qualifies when it owns the
142
+ terminal foreground and reads standard input from it. The check proves terminal
143
+ ownership, not that the program is an agent. Register again from the intended
144
+ terminal and confirm one notification arrives. Codex queue rules are otherwise
145
+ unchanged. See the
146
+ [changelog](https://github.com/jultabc/tabc/blob/main/CHANGELOG.md).
147
+
111
148
  ## Updating from 0.2.0
112
149
 
113
150
  The ledger schema is unchanged. Nodes, signing keys, messages, delivery states,
@@ -143,7 +180,7 @@ python -m pip install --upgrade "tabc[mcp]==0.2.0"
143
180
  Existing string TAC IDs are not converted during installation. Follow the
144
181
  repository README's 0.2.0 ledger conversion procedure when converting TAC IDs
145
182
  to UUIDs. Without conversion, restart `tabd` first, then `tabus.doorbell`.
146
- Then follow "Updating from 0.2.0".
183
+ Then follow "Updating from 0.2.0", "Updating from 0.2.1" and "Updating from 0.2.2".
147
184
 
148
185
  ## Commands
149
186
 
@@ -157,6 +194,9 @@ Then follow "Updating from 0.2.0".
157
194
  Register each recipient before sending. tac creators must also join their topic.
158
195
  Since 0.2.0, the server mints a UUID for each new tac. Use that UUID for membership,
159
196
  sending, reading, renaming and closing; use the unique name for display and search.
197
+ From 0.2.3, the CLI also accepts the exact name of an open tac and prints the UUID
198
+ it used. A closed, missing or only similar name runs nothing and lists candidates
199
+ with `TAC_NAME_UNRESOLVED`. The MCP adapter still takes only the UUID.
160
200
  Run `tabc --help` for available commands. The local server is `tabd`; management
161
201
  commands are provided separately by `tabm`.
162
202
  The source repository carries the operating guide and the security notes.
@@ -30,6 +30,7 @@ tabus/route_capture.py
30
30
  tabus/send_refusal.py
31
31
  tabus/sent.py
32
32
  tabus/tac_identity.py
33
+ tabus/tac_lookup.py
33
34
  tabus/tac_migration.py
34
35
  tabus/tac_refusal.py
35
36
  tabus/tac_search.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.1"
77
+ __version__ = "0.2.3"
78
78
  PROTOCOL_VERSION = "tabus.v1"
79
79
 
80
80
  # TTLs. Past these, a derived status counts as old.
@@ -94,6 +94,7 @@ MAPPING = {
94
94
  "tac_link": "/tac_link",
95
95
  "tac_rename": "/tac_rename",
96
96
  "tac_check": "/tac_check",
97
+ "tac_lookup": "/tac_lookup",
97
98
  # name uses only the register endpoint; setting the session title is local
98
99
  }
99
100
 
@@ -251,6 +252,7 @@ def fn_config(a):
251
252
 
252
253
  def fn_register(a):
253
254
  node = _require_acting_node(a.node)
255
+ a.kind = a.kind.casefold()
254
256
  payload = {"node": node, "kind": a.kind}
255
257
  take_route = bool(getattr(a, "take_route", False))
256
258
  setting = getattr(a, "auto_enter", None)
@@ -439,6 +441,8 @@ def fn_send(a):
439
441
  if a.body_file:
440
442
  body = open(a.body_file, encoding="utf-8").read()
441
443
  group = getattr(a, "tac", None)
444
+ if group:
445
+ group = _designate_tac(group, a.sender)
442
446
  bcast = getattr(a, "broadcast", False)
443
447
  requested = _recipients(a.to)
444
448
  # 🔴 Recipients come from --to, --tac, or --broadcast. With none of the three
@@ -805,6 +809,64 @@ def fn_restore(a):
805
809
  sys.exit(1)
806
810
 
807
811
 
812
+ def _tac_state(row):
813
+ closed = row.get("closed_at")
814
+ return f"closed {closed[:10]}" if closed else "open"
815
+
816
+
817
+ def _designate_tac(given, node):
818
+ """A tac argument → the identifier to send. A name is looked up here, in the client.
819
+
820
+ 🔴 The server still takes only the identifier. A name is turned into one before the
821
+ request, and the identifier used is printed on stderr, so a later rename cannot
822
+ make the record ambiguous. Only an exact name of an open tac is used. A closed tac,
823
+ or no exact match, prints the candidates with identifier and open/closed and
824
+ exits 2 without doing anything. A partial name is never chosen for the caller.
825
+ """
826
+ from .tac_identity import is_uuid_id
827
+
828
+ if not isinstance(given, str) or not given.strip() or is_uuid_id(given):
829
+ return given
830
+ try:
831
+ # Another UUID spelling (uppercase, braces, 32 hex digits) of an identifier.
832
+ spelled = str(uuid.UUID(given.strip()))
833
+ except ValueError:
834
+ spelled = None
835
+ from urllib.parse import urlencode
836
+
837
+ # 🔴 The name is asked first, even for a string uuid.UUID can read: a tac may be
838
+ # named "0123456789abcdef0123456789abcdef", and that name must not turn into
839
+ # some other tac's identifier (jack 4c84df2c). Only with no tac of that exact
840
+ # name is the string read as an identifier spelling.
841
+ code, r = call("GET", MAPPING["tac_lookup"] + "?" + urlencode({"name": given}), node=node)
842
+ if code == 404:
843
+ # 🔴 A server before this lookup existed: send the string as typed, and the
844
+ # server answers TAC_ID_INVALID as it always did.
845
+ return given
846
+ if code != 200 or r.get("error"):
847
+ print(f"failed: tac lookup: {r.get('error') or r}", file=sys.stderr)
848
+ sys.exit(1)
849
+ exact, similar = r.get("exact"), r.get("similar") or []
850
+ if exact is None and spelled is not None:
851
+ return spelled
852
+ if exact and not exact.get("closed_at"):
853
+ print(f"tac {exact['name']} = {exact['tac_id']}", file=sys.stderr)
854
+ return exact["tac_id"]
855
+ rows = ([exact] if exact else []) + similar
856
+ if exact:
857
+ print(f"tac {exact['name']} is closed; nothing was done. Use its identifier to read it:",
858
+ file=sys.stderr)
859
+ elif rows:
860
+ print(f"no tac is named {given!r}; nothing was done. Similar names:", file=sys.stderr)
861
+ else:
862
+ print(f"no tac is named {given!r}; nothing was done (list them with: tabc tac ls)",
863
+ file=sys.stderr)
864
+ for row in rows:
865
+ print(f" {row['name']:<24} {row['tac_id']} {_tac_state(row)}", file=sys.stderr)
866
+ print(" code TAC_NAME_UNRESOLVED", file=sys.stderr)
867
+ sys.exit(2)
868
+
869
+
808
870
  def fn_tac(a):
809
871
  """tac — a named topic for group delivery.
810
872
 
@@ -817,6 +879,12 @@ def fn_tac(a):
817
879
  from another, which covers direction, forks, and merges.
818
880
  """
819
881
  action = a.action
882
+ # 🔴 Every subcommand that takes an existing tac accepts a name here and sends the
883
+ # identifier. create takes a new name and is not looked up.
884
+ if action in ("show", "add", "rm", "rename", "close", "link") or (action == "search" and a.tac):
885
+ a.tac = _designate_tac(a.tac, a.node)
886
+ if action == "link":
887
+ a.member = _designate_tac(a.member, a.node)
820
888
  if action == "search":
821
889
  from urllib.parse import urlencode
822
890
 
@@ -1134,7 +1202,7 @@ COMMANDS = {
1134
1202
  ("--tac",),
1135
1203
  dict(
1136
1204
  default=None,
1137
- help="send to a tac, fanning out to every member (instead of --to)",
1205
+ help="send to a tac, by identifier or exact name, fanning out to every member (instead of --to)",
1138
1206
  ),
1139
1207
  ),
1140
1208
  (
@@ -1234,8 +1302,9 @@ COMMANDS = {
1234
1302
  "close <tac> --summary | link <child> <parent>",
1235
1303
  ),
1236
1304
  ),
1237
- (("tac",), dict(nargs="?", help="the tac, by its identifier (not needed for ls); "
1238
- "find it with ls or search; for link this is the child")),
1305
+ (("tac",), dict(nargs="?", help="the tac, by its identifier or its exact name (not needed for ls); "
1306
+ "a name is looked up and the identifier used is printed; "
1307
+ "for create this is the new name; for link this is the child")),
1239
1308
  (("member",), dict(nargs="?", help="node for add and rm; for link this is the parent; "
1240
1309
  "for rename this is the new name")),
1241
1310
  (("--description",), dict(default=None, help="what the tac is for, for create")),
@@ -498,6 +498,27 @@ class BusHandler(BaseHTTPRequestHandler):
498
498
  con.close()
499
499
  return self._json(200, {"hits": hits})
500
500
 
501
+ if path == "/tac_lookup":
502
+ # 🔴 Read-only: which tac carries this name now. Same scope as /tacs. The
503
+ # client sends the identifier this returns; every other endpoint still
504
+ # takes only the identifier.
505
+ if not self._authorized():
506
+ return self._json(401, {"error": "token"})
507
+ from urllib.parse import parse_qs
508
+ from .tac_lookup import lookup
509
+
510
+ params = parse_qs(query, keep_blank_values=True)
511
+ if any(len(values) != 1 for values in params.values()):
512
+ return self._json(400, {"error": "duplicate query parameters"})
513
+ con = self._connect()
514
+ try:
515
+ exact, similar = lookup(con, params.get("name", [None])[0])
516
+ except (ValueError, TypeError) as exc:
517
+ return self._json(400, {"error": str(exc)})
518
+ finally:
519
+ con.close()
520
+ return self._json(200, {"exact": exact, "similar": similar})
521
+
501
522
  if path == "/tac_messages":
502
523
  # 🔴 tac contents, read-only, and member-only: a tac is a conversation you
503
524
  # read by joining it, so only its members see the body. A non-member is
@@ -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
@@ -0,0 +1,42 @@
1
+ """Find a tac by name, read-only. The client turns a typed name into an identifier here.
2
+
3
+ 🔴 Designation stays by identifier. This endpoint does not make a name a designation:
4
+ every other endpoint still refuses a name with TAC_ID_INVALID. It answers one question,
5
+ "which tac carries this name now", so the client can print the identifier it is about
6
+ to use and then send that identifier. A rename moves the name; the identifier the
7
+ client printed is what the request used.
8
+
9
+ Names are unique on the folded key (ux_tacs_name_key), closed tacs included, so an
10
+ exact match is at most one row. `similar` lists the rows whose folded name contains
11
+ the folded query, for a caller who typed part of a name. Neither changes state.
12
+ """
13
+
14
+ from .tac_identity import name_key
15
+
16
+ SIMILAR_LIMIT = 20
17
+
18
+
19
+ def _row(r):
20
+ return {"tac_id": r["tac_id"], "name": r["name"], "label": r["label"],
21
+ "closed_at": r["closed_at"]}
22
+
23
+
24
+ def lookup(con, name, limit=SIMILAR_LIMIT):
25
+ """(exact row or None, similar rows). Raises ValueError on a bad query."""
26
+ if not isinstance(name, str) or not name.strip() or len(name) > 200:
27
+ raise ValueError("name must contain 1..200 characters")
28
+ columns = {row[1] for row in con.execute("PRAGMA table_info(tacs)")}
29
+ # 🔴 A ledger before the identity migration has no name column. There is nothing
30
+ # to look up there: the identifier is the string a caller types.
31
+ if not {"name", "name_key"} <= columns:
32
+ return None, []
33
+ key = name_key(name.strip())
34
+ rows = con.execute(
35
+ "SELECT tac_id, name, label, closed_at, name_key FROM tacs ORDER BY name"
36
+ ).fetchall()
37
+ exact = next((r for r in rows if r["name_key"] == key), None)
38
+ # 🔴 Compared on name_key in Python, as tac_store.search_name does: sqlite lower()
39
+ # folds ASCII only.
40
+ similar = [r for r in rows if r["name_key"] and key in r["name_key"]
41
+ and (exact is None or r["tac_id"] != exact["tac_id"])][:limit]
42
+ return (_row(exact) if exact else None), [_row(r) for r in similar]
@@ -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,
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