cswap-pin 0.1.0__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.
- cswap_pin-0.1.0/.gitignore +7 -0
- cswap_pin-0.1.0/PKG-INFO +115 -0
- cswap_pin-0.1.0/README.md +92 -0
- cswap_pin-0.1.0/pyproject.toml +42 -0
- cswap_pin-0.1.0/src/cswap_pin/__init__.py +35 -0
- cswap_pin-0.1.0/src/cswap_pin/_host.py +68 -0
- cswap_pin-0.1.0/src/cswap_pin/proxy.py +2910 -0
- cswap_pin-0.1.0/tests/test_packaging.py +121 -0
- cswap_pin-0.1.0/tests/test_proxy.py +2045 -0
- cswap_pin-0.1.0/tests/test_proxy_server.py +1838 -0
cswap_pin-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cswap-pin
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Keep Claude Code's Remote Control and Artifacts on one account while inference follows cswap's account swap
|
|
5
|
+
Project-URL: Homepage, https://github.com/codeslake/cswap-pin
|
|
6
|
+
Project-URL: Repository, https://github.com/codeslake/cswap-pin
|
|
7
|
+
Project-URL: Issues, https://github.com/codeslake/cswap-pin/issues
|
|
8
|
+
Project-URL: Upstream discussion, https://github.com/realiti4/claude-swap/issues/198
|
|
9
|
+
Author: Junyong Lee
|
|
10
|
+
License: MIT
|
|
11
|
+
Keywords: claude,claude-code,claude-swap,cswap,proxy,remote-control
|
|
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: Topic :: Utilities
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: cryptography>=41.0
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# cswap-pin
|
|
25
|
+
|
|
26
|
+
Keep Claude Code's **Remote Control** and **Artifacts** on one account while
|
|
27
|
+
inference keeps following [`cswap`](https://github.com/realiti4/claude-swap)'s
|
|
28
|
+
account swap.
|
|
29
|
+
|
|
30
|
+
## The problem
|
|
31
|
+
|
|
32
|
+
cswap swaps the on-disk credential, so *everything* follows the swap —
|
|
33
|
+
including two things that are not inference and that you usually want to stay
|
|
34
|
+
put:
|
|
35
|
+
|
|
36
|
+
- **Remote Control** — a session's owner is fixed at creation by whichever
|
|
37
|
+
bearer created it. Swap accounts and the phone/web loses the session; stale
|
|
38
|
+
"ghost" sessions pile up on the old account.
|
|
39
|
+
- **Artifacts** — owned by the publishing bearer. After a swap a republish
|
|
40
|
+
403s and the artifact "disappears" from the account you are logged into.
|
|
41
|
+
|
|
42
|
+
Claude Code resolves all of these through one credential accessor and has no
|
|
43
|
+
per-operation token selector, so splitting auth *per operation inside one
|
|
44
|
+
session* means intercepting the requests.
|
|
45
|
+
|
|
46
|
+
## How it works
|
|
47
|
+
|
|
48
|
+
A local MITM forward proxy that swaps the `Authorization` bearer on exactly
|
|
49
|
+
the routes whose server-side ownership is decided by it, and passes everything
|
|
50
|
+
else — `/v1/messages` above all — through untouched.
|
|
51
|
+
|
|
52
|
+
```
|
|
53
|
+
claude session
|
|
54
|
+
HTTPS_PROXY ─► cswap pin proxy ──► (whatever HTTPS_PROXY was already set) ──► api.anthropic.com
|
|
55
|
+
swaps bearer on: /v1/code/sessions*, /v1/sessions/*,
|
|
56
|
+
/api/frame/*, /v1/ultrareview/*
|
|
57
|
+
passes through: /v1/messages, /api/oauth/usage, everything else
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Inference keeps billing whichever account cswap has swapped onto. Only the
|
|
61
|
+
claude.ai-side assets are pinned.
|
|
62
|
+
|
|
63
|
+
## Install
|
|
64
|
+
|
|
65
|
+
```bash
|
|
66
|
+
uv tool install 'claude-swap[pin]' # or: pipx install 'claude-swap[pin]'
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
The pin is an optional extra of claude-swap, not a standalone tool: it reads
|
|
70
|
+
cswap's account store and rewrites the config cswap already manages. Installing
|
|
71
|
+
`cswap-pin` on its own does nothing useful.
|
|
72
|
+
|
|
73
|
+
## Use
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
cswap pin 2 # RC / artifacts / ultrareview → account 2
|
|
77
|
+
cswap pin # show the current pin
|
|
78
|
+
cswap pin --clear # remove it
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
The pinned account is re-read per request, so `cswap pin <other>` takes effect
|
|
82
|
+
under a live daemon — no session restart. The one thing a re-pin cannot move is
|
|
83
|
+
a Remote Control session that is **already open**: the server fixed its owner
|
|
84
|
+
when it was created, so reconnecting inside it is what mints a new one under
|
|
85
|
+
the new pin.
|
|
86
|
+
|
|
87
|
+
## Requirements
|
|
88
|
+
|
|
89
|
+
- Python 3.10+
|
|
90
|
+
- [`claude-swap`](https://github.com/realiti4/claude-swap) — a peer, not a
|
|
91
|
+
dependency: this package is loaded *by* it (see `src/cswap_pin/_host.py` for
|
|
92
|
+
the exact surface it borrows)
|
|
93
|
+
- `cryptography` (installed automatically) for the MITM CA
|
|
94
|
+
|
|
95
|
+
## Why a separate package
|
|
96
|
+
|
|
97
|
+
Upstream did not want a MITM proxy shipped inside claude-swap itself and asked
|
|
98
|
+
for a companion distribution exposed through an optional extra. See
|
|
99
|
+
[realiti4/claude-swap#198](https://github.com/realiti4/claude-swap/issues/198).
|
|
100
|
+
|
|
101
|
+
## Trust
|
|
102
|
+
|
|
103
|
+
The proxy generates its own CA to re-sign `api.anthropic.com` and names it in
|
|
104
|
+
`NODE_EXTRA_CA_CERTS`. Node accepts exactly one file there, so an existing CA
|
|
105
|
+
(a corporate MITM, another local proxy) is **merged**, never replaced —
|
|
106
|
+
otherwise the session silently loses trust in every host the other proxy
|
|
107
|
+
re-signs.
|
|
108
|
+
|
|
109
|
+
The proxy also requires a per-daemon credential on `CONNECT`: it listens on
|
|
110
|
+
loopback, which carries no identity, and without one any local process could
|
|
111
|
+
have a junk bearer replaced with the pinned account's real token.
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# cswap-pin
|
|
2
|
+
|
|
3
|
+
Keep Claude Code's **Remote Control** and **Artifacts** on one account while
|
|
4
|
+
inference keeps following [`cswap`](https://github.com/realiti4/claude-swap)'s
|
|
5
|
+
account swap.
|
|
6
|
+
|
|
7
|
+
## The problem
|
|
8
|
+
|
|
9
|
+
cswap swaps the on-disk credential, so *everything* follows the swap —
|
|
10
|
+
including two things that are not inference and that you usually want to stay
|
|
11
|
+
put:
|
|
12
|
+
|
|
13
|
+
- **Remote Control** — a session's owner is fixed at creation by whichever
|
|
14
|
+
bearer created it. Swap accounts and the phone/web loses the session; stale
|
|
15
|
+
"ghost" sessions pile up on the old account.
|
|
16
|
+
- **Artifacts** — owned by the publishing bearer. After a swap a republish
|
|
17
|
+
403s and the artifact "disappears" from the account you are logged into.
|
|
18
|
+
|
|
19
|
+
Claude Code resolves all of these through one credential accessor and has no
|
|
20
|
+
per-operation token selector, so splitting auth *per operation inside one
|
|
21
|
+
session* means intercepting the requests.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
A local MITM forward proxy that swaps the `Authorization` bearer on exactly
|
|
26
|
+
the routes whose server-side ownership is decided by it, and passes everything
|
|
27
|
+
else — `/v1/messages` above all — through untouched.
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
claude session
|
|
31
|
+
HTTPS_PROXY ─► cswap pin proxy ──► (whatever HTTPS_PROXY was already set) ──► api.anthropic.com
|
|
32
|
+
swaps bearer on: /v1/code/sessions*, /v1/sessions/*,
|
|
33
|
+
/api/frame/*, /v1/ultrareview/*
|
|
34
|
+
passes through: /v1/messages, /api/oauth/usage, everything else
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Inference keeps billing whichever account cswap has swapped onto. Only the
|
|
38
|
+
claude.ai-side assets are pinned.
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
uv tool install 'claude-swap[pin]' # or: pipx install 'claude-swap[pin]'
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The pin is an optional extra of claude-swap, not a standalone tool: it reads
|
|
47
|
+
cswap's account store and rewrites the config cswap already manages. Installing
|
|
48
|
+
`cswap-pin` on its own does nothing useful.
|
|
49
|
+
|
|
50
|
+
## Use
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
cswap pin 2 # RC / artifacts / ultrareview → account 2
|
|
54
|
+
cswap pin # show the current pin
|
|
55
|
+
cswap pin --clear # remove it
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The pinned account is re-read per request, so `cswap pin <other>` takes effect
|
|
59
|
+
under a live daemon — no session restart. The one thing a re-pin cannot move is
|
|
60
|
+
a Remote Control session that is **already open**: the server fixed its owner
|
|
61
|
+
when it was created, so reconnecting inside it is what mints a new one under
|
|
62
|
+
the new pin.
|
|
63
|
+
|
|
64
|
+
## Requirements
|
|
65
|
+
|
|
66
|
+
- Python 3.10+
|
|
67
|
+
- [`claude-swap`](https://github.com/realiti4/claude-swap) — a peer, not a
|
|
68
|
+
dependency: this package is loaded *by* it (see `src/cswap_pin/_host.py` for
|
|
69
|
+
the exact surface it borrows)
|
|
70
|
+
- `cryptography` (installed automatically) for the MITM CA
|
|
71
|
+
|
|
72
|
+
## Why a separate package
|
|
73
|
+
|
|
74
|
+
Upstream did not want a MITM proxy shipped inside claude-swap itself and asked
|
|
75
|
+
for a companion distribution exposed through an optional extra. See
|
|
76
|
+
[realiti4/claude-swap#198](https://github.com/realiti4/claude-swap/issues/198).
|
|
77
|
+
|
|
78
|
+
## Trust
|
|
79
|
+
|
|
80
|
+
The proxy generates its own CA to re-sign `api.anthropic.com` and names it in
|
|
81
|
+
`NODE_EXTRA_CA_CERTS`. Node accepts exactly one file there, so an existing CA
|
|
82
|
+
(a corporate MITM, another local proxy) is **merged**, never replaced —
|
|
83
|
+
otherwise the session silently loses trust in every host the other proxy
|
|
84
|
+
re-signs.
|
|
85
|
+
|
|
86
|
+
The proxy also requires a per-daemon credential on `CONNECT`: it listens on
|
|
87
|
+
loopback, which carries no identity, and without one any local process could
|
|
88
|
+
have a junk bearer replaced with the pinned account's real token.
|
|
89
|
+
|
|
90
|
+
## License
|
|
91
|
+
|
|
92
|
+
MIT
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "cswap-pin"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
description = "Keep Claude Code's Remote Control and Artifacts on one account while inference follows cswap's account swap"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.10"
|
|
7
|
+
license = { text = "MIT" }
|
|
8
|
+
authors = [{ name = "Junyong Lee" }]
|
|
9
|
+
keywords = ["claude", "claude-code", "claude-swap", "cswap", "proxy", "remote-control"]
|
|
10
|
+
classifiers = [
|
|
11
|
+
"Development Status :: 4 - Beta",
|
|
12
|
+
"Environment :: Console",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"License :: OSI Approved :: MIT License",
|
|
15
|
+
"Operating System :: MacOS",
|
|
16
|
+
"Operating System :: POSIX :: Linux",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Topic :: Utilities",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
# claude-swap is a PEER, not a dependency. cswap-pin is loaded BY claude-swap
|
|
22
|
+
# (via `claude-swap[pin]`), so depending on it here would make the pair
|
|
23
|
+
# circular and let `pip install cswap-pin` drag in the whole switcher for
|
|
24
|
+
# someone who already has it. The import surface it needs is small and
|
|
25
|
+
# declared in _host.py.
|
|
26
|
+
dependencies = ["cryptography>=41.0"]
|
|
27
|
+
|
|
28
|
+
[project.urls]
|
|
29
|
+
Homepage = "https://github.com/codeslake/cswap-pin"
|
|
30
|
+
Repository = "https://github.com/codeslake/cswap-pin"
|
|
31
|
+
Issues = "https://github.com/codeslake/cswap-pin/issues"
|
|
32
|
+
"Upstream discussion" = "https://github.com/realiti4/claude-swap/issues/198"
|
|
33
|
+
|
|
34
|
+
[build-system]
|
|
35
|
+
requires = ["hatchling"]
|
|
36
|
+
build-backend = "hatchling.build"
|
|
37
|
+
|
|
38
|
+
[tool.hatch.build.targets.wheel]
|
|
39
|
+
packages = ["src/cswap_pin"]
|
|
40
|
+
|
|
41
|
+
[tool.pytest.ini_options]
|
|
42
|
+
testpaths = ["tests"]
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
"""Keep Claude Code's Remote Control and Artifacts on one account.
|
|
2
|
+
|
|
3
|
+
cswap swaps the on-disk credential, so everything follows the swap — including
|
|
4
|
+
two things that are not inference and usually should not move: Remote Control
|
|
5
|
+
(a session's owner is fixed at creation by the bearer that created it) and
|
|
6
|
+
Artifacts (owned by the publishing bearer). This package keeps those on one
|
|
7
|
+
account while inference keeps following ``cswap switch`` / ``cswap auto``.
|
|
8
|
+
|
|
9
|
+
Companion to claude-swap. Installed as ``claude-swap[pin]``; see
|
|
10
|
+
https://github.com/realiti4/claude-swap/issues/198 for why it lives apart.
|
|
11
|
+
|
|
12
|
+
**Nothing is imported from :mod:`cswap_pin.proxy` here.** That module reaches
|
|
13
|
+
into claude-swap at import time, so re-exporting its symbols made
|
|
14
|
+
``import cswap_pin`` itself raise ``HostMissing`` when the host was absent —
|
|
15
|
+
from proxy.py line 43, naming neither package, and before any caller could
|
|
16
|
+
catch it. Importing a package must never require the thing the package is
|
|
17
|
+
meant to report on. Reach for ``cswap_pin.proxy`` directly; use
|
|
18
|
+
:func:`host_available` first if you want to check rather than catch.
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
__version__ = "0.1.0"
|
|
24
|
+
|
|
25
|
+
__all__ = ["__version__", "host_available"]
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def host_available() -> bool:
|
|
29
|
+
"""Whether claude-swap is importable, i.e. whether the pin can do anything.
|
|
30
|
+
|
|
31
|
+
Safe to call with the host missing — that is the case it exists for.
|
|
32
|
+
"""
|
|
33
|
+
from cswap_pin._host import available
|
|
34
|
+
|
|
35
|
+
return available()
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"""Everything cswap-pin borrows from claude-swap, in one place.
|
|
2
|
+
|
|
3
|
+
The pin runs INSIDE claude-swap: it is loaded by `claude-swap[pin]`, reads the
|
|
4
|
+
account store cswap owns, and rewrites the config file cswap already rewrites.
|
|
5
|
+
So it needs part of cswap's internals — but "needs part of" is exactly the
|
|
6
|
+
coupling that made splitting the two hard in the first place, and an import
|
|
7
|
+
scattered across 2800 lines is a dependency nobody can see the shape of.
|
|
8
|
+
|
|
9
|
+
This module IS that shape. Seven symbols, six modules, listed once:
|
|
10
|
+
|
|
11
|
+
paths.get_global_config_path where the env block lives
|
|
12
|
+
paths.get_claude_config_home the config dir root
|
|
13
|
+
settings read/write the pin record
|
|
14
|
+
exceptions.AccountNotFoundError pin resolution failures
|
|
15
|
+
exceptions.ConfigError ...
|
|
16
|
+
switcher.ClaudeAccountSwitcher the account store
|
|
17
|
+
oauth token extraction and refresh
|
|
18
|
+
claude_locks.claude_config_lock serialize config rewrites
|
|
19
|
+
|
|
20
|
+
Two rules follow from putting it here:
|
|
21
|
+
|
|
22
|
+
1. **Nothing else in cswap_pin imports claude_swap directly.** A new import
|
|
23
|
+
elsewhere grows the contract silently; a new name in this file is a
|
|
24
|
+
deliberate change to it, visible in one diff.
|
|
25
|
+
2. **claude-swap is a PEER, not a dependency.** It is not in our
|
|
26
|
+
``dependencies``: cswap-pin is loaded by claude-swap, so declaring it here
|
|
27
|
+
would make the pair circular and pull the whole switcher into anyone who
|
|
28
|
+
installs cswap-pin alone. The import therefore has to be able to fail, and
|
|
29
|
+
``require()`` is where that failure gets a readable message instead of a
|
|
30
|
+
traceback from line 2000 of the proxy.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
from __future__ import annotations
|
|
34
|
+
|
|
35
|
+
from types import ModuleType
|
|
36
|
+
|
|
37
|
+
_HINT = (
|
|
38
|
+
"cswap-pin runs inside claude-swap and cannot find it. "
|
|
39
|
+
"Install the pair together: pip install 'claude-swap[pin]'"
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class HostMissing(RuntimeError):
|
|
44
|
+
"""claude-swap is not importable, so the pin has nothing to pin."""
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def require(name: str) -> ModuleType:
|
|
48
|
+
"""Import a claude_swap submodule, or fail with something readable.
|
|
49
|
+
|
|
50
|
+
``name`` is the part after ``claude_swap.`` — ``require("paths")`` gets
|
|
51
|
+
``claude_swap.paths``.
|
|
52
|
+
"""
|
|
53
|
+
import importlib
|
|
54
|
+
|
|
55
|
+
try:
|
|
56
|
+
return importlib.import_module(f"claude_swap.{name}")
|
|
57
|
+
except ImportError as exc: # noqa: TRY003 — the message IS the point
|
|
58
|
+
raise HostMissing(_HINT) from exc
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def available() -> bool:
|
|
62
|
+
"""Whether the host is importable at all. For callers that want to check
|
|
63
|
+
rather than catch."""
|
|
64
|
+
try:
|
|
65
|
+
require("paths")
|
|
66
|
+
except HostMissing:
|
|
67
|
+
return False
|
|
68
|
+
return True
|