agent-session-bridge 0.2.0__py3-none-any.whl

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 (37) hide show
  1. agent_session_bridge-0.2.0.dist-info/METADATA +288 -0
  2. agent_session_bridge-0.2.0.dist-info/RECORD +37 -0
  3. agent_session_bridge-0.2.0.dist-info/WHEEL +5 -0
  4. agent_session_bridge-0.2.0.dist-info/entry_points.txt +3 -0
  5. agent_session_bridge-0.2.0.dist-info/licenses/LICENSE +21 -0
  6. agent_session_bridge-0.2.0.dist-info/top_level.txt +1 -0
  7. session_bridge/__init__.py +0 -0
  8. session_bridge/_ids.py +43 -0
  9. session_bridge/cli.py +473 -0
  10. session_bridge/convert.py +130 -0
  11. session_bridge/handshake.py +175 -0
  12. session_bridge/ir.py +246 -0
  13. session_bridge/place.py +98 -0
  14. session_bridge/readers/__init__.py +0 -0
  15. session_bridge/readers/_content.py +42 -0
  16. session_bridge/readers/_jsonl.py +50 -0
  17. session_bridge/readers/_pending.py +63 -0
  18. session_bridge/readers/claude_code.py +226 -0
  19. session_bridge/readers/codex.py +203 -0
  20. session_bridge/readers/hermes.py +167 -0
  21. session_bridge/skill_install.py +134 -0
  22. session_bridge/skills/session-handoff/SKILL.md +119 -0
  23. session_bridge/tui/__init__.py +0 -0
  24. session_bridge/tui/actions.py +123 -0
  25. session_bridge/tui/app.py +65 -0
  26. session_bridge/tui/discovery.py +208 -0
  27. session_bridge/tui/options.py +86 -0
  28. session_bridge/tui/register.py +475 -0
  29. session_bridge/tui/screens.py +710 -0
  30. session_bridge/tui/summary.py +41 -0
  31. session_bridge/writers/__init__.py +0 -0
  32. session_bridge/writers/_common.py +288 -0
  33. session_bridge/writers/claude_code.py +152 -0
  34. session_bridge/writers/codex.py +154 -0
  35. session_bridge/writers/codex_db.py +302 -0
  36. session_bridge/writers/hermes.py +142 -0
  37. session_bridge/writers/hermes_db.py +294 -0
@@ -0,0 +1,288 @@
1
+ Metadata-Version: 2.4
2
+ Name: agent-session-bridge
3
+ Version: 0.2.0
4
+ Summary: Local-first cross-harness agent-session portability: export a live coding-agent session from one harness and resume it in another.
5
+ Author: Prakash Chaudhary
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/connectwithprakash/session-bridge
8
+ Project-URL: Repository, https://github.com/connectwithprakash/session-bridge
9
+ Project-URL: Changelog, https://github.com/connectwithprakash/session-bridge/blob/main/CHANGELOG.md
10
+ Project-URL: Project page, https://www.connectwithprakash.com/project/session-bridge
11
+ Keywords: ai-agents,claude-code,codex,session,cli,tui
12
+ Classifier: Development Status :: 4 - Beta
13
+ Classifier: Environment :: Console
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: License :: OSI Approved :: MIT License
16
+ Classifier: Operating System :: MacOS
17
+ Classifier: Operating System :: POSIX :: Linux
18
+ Classifier: Programming Language :: Python :: 3
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Utilities
23
+ Requires-Python: >=3.11
24
+ Description-Content-Type: text/markdown
25
+ License-File: LICENSE
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=7; extra == "dev"
28
+ Provides-Extra: tui
29
+ Requires-Dist: textual>=1.0; extra == "tui"
30
+ Dynamic: license-file
31
+
32
+ # session-bridge
33
+
34
+ [![test](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml/badge.svg)](https://github.com/connectwithprakash/session-bridge/actions/workflows/test.yml)
35
+ [![PyPI](https://img.shields.io/pypi/v/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
36
+ [![Python](https://img.shields.io/pypi/pyversions/agent-session-bridge)](https://pypi.org/project/agent-session-bridge/)
37
+ [![License: MIT](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
38
+
39
+ Local-first, cross-harness **agent-session portability**. Export a coding-agent
40
+ session from one harness and resume it in another when the original hits a usage
41
+ limit or otherwise stops.
42
+
43
+ Supports three harnesses today:
44
+
45
+ | Harness | Session store |
46
+ |---|---|
47
+ | Claude Code | `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl` |
48
+ | Codex | `~/.codex/sessions/YYYY/MM/DD/rollout-*.jsonl` |
49
+ | Hermes | `~/.hermes/sessions/<ts>_<id>.jsonl` |
50
+
51
+ No cloud. Everything runs against files already on your disk.
52
+
53
+ ## Supported versions
54
+
55
+ session-bridge's real dependencies are other tools' storage formats, which can
56
+ change without notice. This table states exactly what each reader/registrar was
57
+ last verified against; registration paths validate the store schema at runtime
58
+ and fail closed (no write) when it doesn't match.
59
+
60
+ | Harness | Last verified against | Verification |
61
+ |---|---|---|
62
+ | Claude Code | 2.1.x transcripts | Round-trip + live `claude --resume` recall of a converted-only fact |
63
+ | Codex | Codex CLI 0.145.0 (`state_5.sqlite`) | Live `codex resume` recall of a converted-only sentinel |
64
+ | Hermes | `state.db` schema as of 2026-07 | Live `hermes --resume` replay of a registered session |
65
+ | Python | 3.11 – 3.13 | CI test matrix |
66
+
67
+ A newer harness version usually still works (formats drift rarely), but treat
68
+ anything beyond this table as unverified: run `session-bridge inspect` first,
69
+ and expect SQLite registration to refuse cleanly if the schema moved.
70
+
71
+ ## Why
72
+
73
+ Each harness writes an incompatible session log, and nothing bridges them.
74
+ Claude Code `/export` is lossy plain text, and OpenCode import/export is buggy
75
+ across versions. session-bridge normalizes any supported session into one
76
+ intermediate representation (IR), then renders it into another harness's shape.
77
+ It also carries the *pending state* (open tool calls, queued input) forward
78
+ through a **resume handshake**, so the receiving agent picks up deliberately
79
+ instead of guessing.
80
+
81
+ ## How it works
82
+
83
+ ```
84
+ source.jsonl ─▶ reader ─▶ IR (Session: messages, tools, pending) ─▶ writer ─▶ target.jsonl
85
+
86
+ └─▶ resume handshake (prepended system message)
87
+ ```
88
+
89
+ - **IR** (`ir.py`) is the union of what the three harnesses can express: threaded
90
+ messages with typed content blocks (text / reasoning / tool_call / tool_result),
91
+ session metadata, tool schemas, and explicit pending state.
92
+ - **Readers** (`readers/`) normalize each harness into the IR.
93
+ - **Writers** (`writers/`) render the IR into a target harness and emit a
94
+ `ConversionReport` naming every asymmetry that could not transfer losslessly.
95
+ - **Handshake** (`handshake.py`) turns detected pending state + conversion notes
96
+ into a resume preamble injected as the first message of the resumed session.
97
+
98
+ ## Install
99
+
100
+ ```bash
101
+ uv tool install 'agent-session-bridge[tui]' # recommended: CLI + TUI, isolated env
102
+ uvx agent-session-bridge --help # or: no-install run
103
+ pip install 'agent-session-bridge[tui]' # or: plain pip
104
+ ```
105
+
106
+ Installs the `session-bridge` command (docs use that name); an
107
+ `agent-session-bridge` alias is included, so either name runs the same CLI.
108
+ Drop `[tui]` for the CLI without the interactive TUI. From source:
109
+
110
+ ```bash
111
+ cd session-bridge && uv sync --extra dev --extra tui # or: python3 -m pip install -e .
112
+ ```
113
+
114
+ New here? [`TUTORIAL.md`](TUTORIAL.md) is a step-by-step walkthrough (find your
115
+ session file → inspect → convert → resume) with a real worked example. The
116
+ sections below are the quick reference.
117
+
118
+ ## Usage
119
+
120
+ Prefer a guided flow? The interactive TUI discovers sessions across all three
121
+ stores, walks you through target/options, and shows conversion notes **before**
122
+ anything is written:
123
+
124
+ ![session-bridge TUI walkthrough: pick a session, inspect, dry-run, write](docs/media/tui-demo.gif)
125
+
126
+ ```bash
127
+ python3 -m pip install -e '.[tui]' # the TUI needs the optional textual dependency
128
+ session-bridge tui
129
+ ```
130
+
131
+ Pick a session, then `c` to convert (with optional Claude Code placement) or
132
+ `g` to register it into Hermes's `state.db` or Codex's `state_5.sqlite`. Both
133
+ flows end in a plan screen — loss warnings, backup plan, and the equivalent
134
+ CLI command — before any file or database is touched; registration backs up
135
+ the store first by default, and the planning phase opens live stores
136
+ read-only.
137
+
138
+ Working with an agent instead? The repo ships an agent skill
139
+ ([`skills/session-handoff`](skills/session-handoff/SKILL.md)) that teaches
140
+ any harness's agent to hand off its current session to another harness.
141
+ Install it machine-wide into every harness found on the box:
142
+
143
+ ```bash
144
+ session-bridge install-skill # symlinks into ~/.claude, ~/.codex, ~/.hermes skills dirs
145
+ session-bridge install-skill --copy # copies instead (survives uninstall, goes stale on upgrade)
146
+ ```
147
+
148
+ Inspect a session's structure:
149
+
150
+ ```bash
151
+ session-bridge inspect --from claude-code ~/.claude/projects/<dir>/<uuid>.jsonl
152
+ ```
153
+
154
+ Convert between harnesses:
155
+
156
+ ```bash
157
+ session-bridge convert --from hermes --to claude-code SESSION.jsonl \
158
+ -o resumed.jsonl --handshake-out resume.md
159
+ ```
160
+
161
+ Conversion notes (lossy asymmetries) are printed to stderr; the handshake is
162
+ prepended to the output by default (use `--no-handshake` to disable).
163
+
164
+ If the source stopped mid-turn with a tool call that never returned, that call
165
+ has no result, and a provider rejects a tool call with no matching result on the
166
+ next turn (OpenAI Responses returns a 400; Anthropic requires a `tool_result`).
167
+ Pass `--stub-open-calls` to append a synthetic interrupted result
168
+ (`[session interrupted...]`, marked as an error) for each genuinely-open call, so
169
+ the converted transcript is valid to resume; the report still discloses that the
170
+ call was interrupted.
171
+
172
+ ## What transfers, and what doesn't
173
+
174
+ The conversation core (user/assistant text, reasoning summaries, tool calls,
175
+ tool results, and call↔result linkage) transfers between all three harnesses.
176
+ The following are **inherently lossy** and are reported per conversion (see
177
+ `docs/schema-reference.md` for the full analysis):
178
+
179
+ 1. **Thread topology:** only Claude Code has `parentUuid` branches. Converting
180
+ away flattens forks; converting in synthesizes a linear chain.
181
+ 2. **Reasoning signatures:** provider-bound, so reasoning survives as summary text.
182
+ 3. **Tool schemas:** only Hermes stores them; reconstructed from invoked names otherwise.
183
+ 4. **Base/system instructions:** only Codex stores them.
184
+ 5. **Queued user input:** only Claude Code records it, so it surfaces in the handshake.
185
+ 6. **Permission/sandbox posture:** richest in Codex, absent in Hermes.
186
+ 7. **Per-turn model switches:** Hermes stores a single session model.
187
+
188
+ ## Getting a converted session recognized by the target
189
+
190
+ How a converted transcript becomes resumable differs per harness. Both cases are
191
+ verified on real installs (a session was round-tripped Claude Code → Hermes →
192
+ Claude Code and successfully resumed in a live `claude` process, recalling a fact
193
+ that existed only in the converted transcript).
194
+
195
+ | Harness | How to place it | Resumes from file alone? |
196
+ |---|---|---|
197
+ | Claude Code (2.1.x) | Write to `~/.claude/projects/<encoded-cwd>/<uuid>.jsonl`, then `claude --resume <uuid>` **launched from the matching cwd** | **Yes** |
198
+ | Hermes | Valid filename in `~/.hermes/sessions/` is not enough | **No** (needs a SQLite session-store row) |
199
+ | Codex | `register-codex` writes a rollout and its `threads` index row in `state_5.sqlite` | **Yes** (live-recall verified with Codex CLI 0.145.0) |
200
+
201
+ **Claude Code** resolves `--resume <uuid>` directly from the transcript file. The
202
+ one catch: the encoded-cwd directory name must match the directory you launch
203
+ `claude` from (note macOS symlinks like `/tmp` → `/private/tmp`; use the real
204
+ resolved path). No separate index write is needed. Pass `--place-claude-cwd` to
205
+ `convert` and session-bridge writes the transcript to the right place and prints
206
+ the exact resume command:
207
+
208
+ ```bash
209
+ session-bridge convert --from hermes --to claude-code SESSION.jsonl \
210
+ --place-claude-cwd ~/Developer/myproject
211
+ # placed resumable session -> ~/.claude/projects/-Users-you-Developer-myproject/<uuid>.jsonl
212
+ # resume with: (cd ~/Developer/myproject && claude --resume <uuid>)
213
+ ```
214
+
215
+ If a transcript already exists at the chosen `--session-id`, placement fails
216
+ rather than silently overwriting a recovered session; pass `--force` to replace
217
+ it deliberately.
218
+
219
+ **Hermes** stores sessions in a SQLite database (`~/.hermes/state.db`), across a
220
+ `sessions` row plus one `messages` row per turn; the `.jsonl` files are exports,
221
+ not the source of truth. Use `session-bridge register` to write those rows (it
222
+ backs up the DB first):
223
+
224
+ ```bash
225
+ session-bridge register --from claude-code SESSION.jsonl \
226
+ --model moonshotai/kimi-k3 --title "resumed from claude code"
227
+ # backed up state.db -> ...
228
+ # registered session sb_... into ~/.hermes/state.db
229
+ # resume with: hermes --resume sb_...
230
+ ```
231
+
232
+ Verified end-to-end against a real store: `hermes --resume` replays the registered
233
+ history and the model recalls it. Two things matter, both handled by the command:
234
+
235
+ - a real `started_at` (set automatically) so the session isn't sorted below the
236
+ default `hermes sessions list` limit;
237
+ - `--model` must name a model Hermes has a provider for. A cross-harness source id
238
+ (e.g. an Anthropic `claude-*` id from a Claude Code session) that Hermes cannot
239
+ route makes the resumed turn fall back and lose context, so set `--model` to a
240
+ Hermes-configured model.
241
+
242
+ **Codex** reading and round-trip are validated against a real tool-using session
243
+ (driven through OpenRouter): `function_call` / `function_call_output` / `reasoning`
244
+ (both `summary` and `content[]` shapes) parse correctly and round-trip identically.
245
+ Codex also indexes sessions in SQLite (`state_5.sqlite`, a `threads` row with a
246
+ `rollout_path`). Use `register-codex` to write a converted rollout and index it;
247
+ the command validates the local schema, takes a SQLite online-backup before the
248
+ mutation, publishes the rollout without overwriting another registration, then
249
+ indexes it in SQLite. It removes its own rollout if the index transaction fails;
250
+ a process crash can still leave an unindexed rollout, which Codex ignores.
251
+
252
+ ```bash
253
+ session-bridge register-codex --from hermes SESSION.jsonl \
254
+ --cwd ~/Developer/myproject --title "resumed from Hermes"
255
+ # backed up Codex state_5.sqlite -> ...
256
+ # registered session <uuid> into ~/.codex/state_5.sqlite
257
+ # resume with: (cd ~/Developer/myproject && codex resume <uuid>)
258
+ ```
259
+
260
+ This registration path is covered against an isolated Codex-shaped SQLite store
261
+ and was live-recall verified with Codex CLI 0.145.0: after `codex exec resume`
262
+ opened an imported session, the model returned a unique sentinel that existed
263
+ only in that transcript.
264
+
265
+ ## Known limitations
266
+
267
+ - Codex registration is schema-validated and regression-tested against an isolated
268
+ `threads` store; live recall was verified with Codex CLI 0.145.0. Future Codex
269
+ schema changes still need the same authenticated acceptance check.
270
+ - Queued-input detection is conservative: it may over-report undelivered input
271
+ rather than silently drop it (the safe direction for resume). Enqueue/dequeue
272
+ matching is scoped per `sessionId`.
273
+ - Pending-state resumption produces a handshake for a human/agent to act on; it
274
+ does not itself re-execute open tool calls.
275
+ - Failed tool results: Codex and Hermes have no native error flag, so `is_error`
276
+ is preserved as a `[tool error]` text prefix (and reported) rather than a field.
277
+ - Empty-content messages: writers preserve the turn to keep message count stable,
278
+ but a fully empty turn does not round-trip back through the Codex/Hermes readers'
279
+ content guards. This is documented, and not observed in real data.
280
+
281
+ ## Development
282
+
283
+ ```bash
284
+ python3 -m pytest # IR, three readers, writers/round-trips, handshake, placement, and both SQLite registrars
285
+ ```
286
+
287
+ Real captured sessions may contain secrets; `fixtures/real/` is gitignored and
288
+ tests run only against synthetic, faithful fixtures.
@@ -0,0 +1,37 @@
1
+ agent_session_bridge-0.2.0.dist-info/licenses/LICENSE,sha256=lM-0nT3o6bxmBhZE7Dx805LAsocbK292sH2JTvenvps,1074
2
+ session_bridge/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ session_bridge/_ids.py,sha256=aFRqgzFaDA0bWhHiqQCR2unfUZQ9kFBaJtpuCvo-0SA,1627
4
+ session_bridge/cli.py,sha256=HcAI07jNlWc7ndjQ1FtuMzDXxeue2cFLS7eDUfEcaO8,19421
5
+ session_bridge/convert.py,sha256=fjA_qfcZg9hHHCY-mPufckrKJKDJWdapGVJGvTIUhH8,4684
6
+ session_bridge/handshake.py,sha256=csfTHVPit2EPcvFwSSHxCU8SEadrhtMNYRu79hoB9JE,7509
7
+ session_bridge/ir.py,sha256=Y8dqkF_3okeK_zVl1sqN0YJAa0zELrdvGqPkJCYMINU,8920
8
+ session_bridge/place.py,sha256=oY4j83BIvTA19XwfhqLiwkitCDIv7NhLWmYBIn4cqNc,3776
9
+ session_bridge/skill_install.py,sha256=txh7AyPGldfCN5yKhYF5H_sTOXEjHuCHZhtEyeeLuuE,4673
10
+ session_bridge/readers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
11
+ session_bridge/readers/_content.py,sha256=nfGECKPg8r5hWZqY5ZOkyTip5Cx3d2zX6pzlOJQUark,1553
12
+ session_bridge/readers/_jsonl.py,sha256=wT-NbyyifmgL_DSmrnTnmDZK2YukdqCSv1ayZ7BJQjM,2158
13
+ session_bridge/readers/_pending.py,sha256=Gv7Mg-Qa92XFSnfFD7adr4lXG5g0Mon_QYSJd7uVzlI,3107
14
+ session_bridge/readers/claude_code.py,sha256=ZiCXQVd8CHMHwQZWDkQFVt4cw6zGH6vmfZdZsUzGAnU,9710
15
+ session_bridge/readers/codex.py,sha256=1TWGme4koEtHQXduIG0LpDCusHwHQo5TGlvqMJ94wl4,8429
16
+ session_bridge/readers/hermes.py,sha256=kAYJnjRrmLtn7jlWUx9PtgWLGmctR5gRn2wRpY14Xpo,6109
17
+ session_bridge/skills/session-handoff/SKILL.md,sha256=ahK87vtI9X1HZD4YNc9szf2I6KAEjcw4MK16yVD8-mE,5021
18
+ session_bridge/tui/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
19
+ session_bridge/tui/actions.py,sha256=4eSItFAxZbnJhXzY6Fx89slqtCp0Ya905-pFEuxP540,4257
20
+ session_bridge/tui/app.py,sha256=xOKzw_cmsdkMXoUoyCz_ognm90vADxCkJFBhWxAEhl0,1934
21
+ session_bridge/tui/discovery.py,sha256=g4V3_a6AXd2PMO29Azf5QCPYgJYh6Vzxtme64-joXGM,7786
22
+ session_bridge/tui/options.py,sha256=HpQHS-yUh9ldSRHsIb2fcqc9ewrh1BdV1zecl5u0w0A,3036
23
+ session_bridge/tui/register.py,sha256=QnP-y0JEW4jKVpQelsIp5LAu_jCXlCWQBs9RgRKB2zw,16840
24
+ session_bridge/tui/screens.py,sha256=BSGxLYaWlHa4EGAWQXWFw5bklfkraA3GhrkKZ_9fiT8,28146
25
+ session_bridge/tui/summary.py,sha256=c_UuMaDsA660Fdtgc5HpKHYZlMeAhcwa0sKazu5aLFc,1828
26
+ session_bridge/writers/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
27
+ session_bridge/writers/_common.py,sha256=DOcwESFvzKvd2kZgl5rcYjJ_vsFFFkE5GhGjcyyp6Bo,13350
28
+ session_bridge/writers/claude_code.py,sha256=EpC0cK0pfFYme8YLcsNNLNZ9bOF8F1ALRg2JwpgSGGo,6223
29
+ session_bridge/writers/codex.py,sha256=fHObELNwkM7gPqQyU0e5eOYvyMRlkd_Ay92bfmkem-U,6372
30
+ session_bridge/writers/codex_db.py,sha256=sqNVmgzW79uOUNCM3qylNFotE-VyOfRMumnkceQjIqM,10710
31
+ session_bridge/writers/hermes.py,sha256=qh-oWS0l-m89GMS5n63OOllnFnR1_3bDOS-OeTDpTlQ,5151
32
+ session_bridge/writers/hermes_db.py,sha256=SrFMJ44QBbPSw56ovwifU9mimYfbw34RD_YQ6zNwNI8,12242
33
+ agent_session_bridge-0.2.0.dist-info/METADATA,sha256=fCAK08BLq1jwsl5HGf0tOVsl9hTH6gVZeIDAyT4tQjs,13928
34
+ agent_session_bridge-0.2.0.dist-info/WHEEL,sha256=K260EYznzXsJYBQGqmI8VTxEdiZYNvDZwW9cBh9-_MA,91
35
+ agent_session_bridge-0.2.0.dist-info/entry_points.txt,sha256=nbWMsfbzdM0kc98QBFWYG8gSt9YbL7gTdhLhnqVMpeQ,106
36
+ agent_session_bridge-0.2.0.dist-info/top_level.txt,sha256=PQPKTGEKt7KLPeW7SH9c8InG50Yup8yEaz301YQIPYA,15
37
+ agent_session_bridge-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (83.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ agent-session-bridge = session_bridge.cli:main
3
+ session-bridge = session_bridge.cli:main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Prakash Chaudhary
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1 @@
1
+ session_bridge
File without changes
session_bridge/_ids.py ADDED
@@ -0,0 +1,43 @@
1
+ """Session-id validation shared by every path that puts an id into a filesystem
2
+ path or a store key.
3
+
4
+ A session id can come from a CLI argument (``--session-id``) or an untrusted
5
+ source session. Interpolating it into a file path (``place_claude_code``) without
6
+ validation allows path traversal (``../../evil``), so all id-consuming paths must
7
+ reject anything outside a safe charset.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ import re
13
+
14
+ # UUIDs, Hermes ids (``20260718_124320_97f3660d``), and the tool's own ``sb_...``
15
+ # ids all fit this. Deliberately excludes ``/``, ``.``, and whitespace.
16
+ _SAFE_ID = re.compile(r"^[A-Za-z0-9_-]+$")
17
+
18
+ # Bound the length so a charset-valid but absurd id doesn't reach the filesystem
19
+ # and raise a raw OSError ("File name too long", errno 63) at write time. 128 is
20
+ # comfortably above every real id format (a UUID is 36 chars).
21
+ _MAX_ID_LEN = 128
22
+
23
+
24
+ class UnsafeSessionIdError(ValueError):
25
+ pass
26
+
27
+
28
+ def validate_session_id(session_id: str) -> str:
29
+ """Return ``session_id`` unchanged if safe, else raise UnsafeSessionIdError.
30
+
31
+ Rejects empty ids, ids longer than ``_MAX_ID_LEN``, and any id containing
32
+ characters outside ``[A-Za-z0-9_-]`` — in particular path separators and
33
+ ``..`` sequences.
34
+ """
35
+ if not session_id or not _SAFE_ID.match(session_id):
36
+ raise UnsafeSessionIdError(
37
+ f"unsafe session id {session_id!r}: only letters, digits, '_' and '-' allowed"
38
+ )
39
+ if len(session_id) > _MAX_ID_LEN:
40
+ raise UnsafeSessionIdError(
41
+ f"session id too long ({len(session_id)} chars; max {_MAX_ID_LEN})"
42
+ )
43
+ return session_id