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.
- {tabc-0.2.1 → tabc-0.2.3}/CHANGELOG.md +93 -0
- {tabc-0.2.1 → tabc-0.2.3}/GUIDE.md +9 -2
- {tabc-0.2.1 → tabc-0.2.3}/MCP.md +2 -1
- {tabc-0.2.1/tabc.egg-info → tabc-0.2.3}/PKG-INFO +47 -7
- {tabc-0.2.1 → tabc-0.2.3}/PYPI.md +44 -1
- {tabc-0.2.1 → tabc-0.2.3}/pyproject.toml +1 -1
- {tabc-0.2.1 → tabc-0.2.3/tabc.egg-info}/PKG-INFO +47 -7
- {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/SOURCES.txt +1 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/bus.py +1 -1
- {tabc-0.2.1 → tabc-0.2.3}/tabus/cli.py +72 -3
- {tabc-0.2.1 → tabc-0.2.3}/tabus/daemon.py +21 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/route_capture.py +3 -3
- tabc-0.2.3/tabus/tac_lookup.py +42 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/terminal_frontend.py +27 -7
- {tabc-0.2.1 → tabc-0.2.3}/LICENSE +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/MANIFEST.in +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/setup.cfg +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/dependency_links.txt +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/entry_points.txt +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/requires.txt +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabc.egg-info/top_level.txt +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/__init__.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/admin.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/codex_queue.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/compat_bcn.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/compat_busd.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/doorbell.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/doorbell_poll.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/lang.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/locales/en.json +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/mcp_server.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/nodekey.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/paths.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/process_argv.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/send_refusal.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/sent.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_identity.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_migration.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_refusal.py +0 -0
- {tabc-0.2.1 → tabc-0.2.3}/tabus/tac_search.py +0 -0
- {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
|
---
|
{tabc-0.2.1 → tabc-0.2.3}/MCP.md
RENAMED
|
@@ -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.
|
|
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.
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
2
|
Name: tabc
|
|
3
|
-
Version: 0.2.
|
|
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
|
-
|
|
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.
|
|
@@ -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.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
|
-
|
|
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.
|
|
@@ -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.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
|
-
"
|
|
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
|
|
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
|
|
@@ -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
|
-
"""
|
|
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,
|
|
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
|
|
File without changes
|
|
File without changes
|