coding-agent-wrapper 0.1.0__tar.gz → 0.1.1__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.
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/PKG-INFO +6 -6
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/README.md +5 -5
- coding_agent_wrapper-0.1.1/caw/auth/README.md +114 -0
- coding_agent_wrapper-0.1.1/caw/auth/__init__.py +115 -0
- coding_agent_wrapper-0.1.1/caw/auth/cli.py +95 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/auth/collector.py +47 -32
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/auth/manifest.py +1 -1
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/auth/providers.py +2 -163
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/auth/status.py +89 -82
- coding_agent_wrapper-0.1.1/caw/cli.py +104 -0
- coding_agent_wrapper-0.1.1/caw/traj_cli.py +998 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/pyproject.toml +2 -1
- coding_agent_wrapper-0.1.0/caw/auth/README.md +0 -118
- coding_agent_wrapper-0.1.0/caw/auth/__init__.py +0 -23
- coding_agent_wrapper-0.1.0/caw/auth/cli.py +0 -68
- coding_agent_wrapper-0.1.0/caw/auth/linker.py +0 -174
- coding_agent_wrapper-0.1.0/caw/cli.py +0 -50
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/.gitignore +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/LICENSE +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/__init__.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/agent.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/display.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/faststats.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/mcp.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/models.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/pricing.json +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/pricing.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/provider.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/providers/__init__.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/providers/claude_code.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/providers/codex.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/py.typed +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/storage.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/toolkit.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/viewer/__init__.py +0 -0
- {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.1}/caw/viewer/static/index.html +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: coding-agent-wrapper
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: Unified Python library and CLI for orchestrating coding agents (Claude Code, Codex, etc.) with MCP tool servers and credential management.
|
|
5
5
|
Project-URL: Homepage, https://github.com/zzjas/caw
|
|
6
6
|
Project-URL: Repository, https://github.com/zzjas/caw
|
|
@@ -30,7 +30,7 @@ Description-Content-Type: text/markdown
|
|
|
30
30
|
|
|
31
31
|
# caw
|
|
32
32
|
|
|
33
|
-
**Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex
|
|
33
|
+
**Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex) with a unified interface, MCP tool servers, and credential management for Docker containers.
|
|
34
34
|
|
|
35
35
|
## Install
|
|
36
36
|
|
|
@@ -197,13 +197,13 @@ Sessions are persisted to JSONL in `caw_data/` by default.
|
|
|
197
197
|
|
|
198
198
|
## CLI: `caw auth` — Credential Management for Docker Containers
|
|
199
199
|
|
|
200
|
-
Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code
|
|
200
|
+
Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code and Codex. Host credential files are never modified — they are bind-mounted into the container at run time.
|
|
201
201
|
|
|
202
202
|
```bash
|
|
203
|
-
caw auth setup #
|
|
204
|
-
caw auth status #
|
|
203
|
+
caw auth setup # snapshot configs, write mount manifest
|
|
204
|
+
caw auth status # token expiry, last modified, mount flags
|
|
205
205
|
docker run $(caw auth docker-flags) -v ./project:/work my-image
|
|
206
|
-
caw auth teardown #
|
|
206
|
+
caw auth teardown # rm -rf ~/.caw/auth/ (host files untouched)
|
|
207
207
|
```
|
|
208
208
|
|
|
209
209
|
See [`caw/auth/README.md`](caw/auth/README.md) for details on how it works, container setup, and supported agents.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# caw
|
|
2
2
|
|
|
3
|
-
**Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex
|
|
3
|
+
**Coding Agent Wrapper** — a Python library and CLI for orchestrating coding agents (Claude Code, Codex) with a unified interface, MCP tool servers, and credential management for Docker containers.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -167,13 +167,13 @@ Sessions are persisted to JSONL in `caw_data/` by default.
|
|
|
167
167
|
|
|
168
168
|
## CLI: `caw auth` — Credential Management for Docker Containers
|
|
169
169
|
|
|
170
|
-
Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code
|
|
170
|
+
Manages coding agent OAuth credentials so they stay in sync between your host and Docker containers. Supports Claude Code and Codex. Host credential files are never modified — they are bind-mounted into the container at run time.
|
|
171
171
|
|
|
172
172
|
```bash
|
|
173
|
-
caw auth setup #
|
|
174
|
-
caw auth status #
|
|
173
|
+
caw auth setup # snapshot configs, write mount manifest
|
|
174
|
+
caw auth status # token expiry, last modified, mount flags
|
|
175
175
|
docker run $(caw auth docker-flags) -v ./project:/work my-image
|
|
176
|
-
caw auth teardown #
|
|
176
|
+
caw auth teardown # rm -rf ~/.caw/auth/ (host files untouched)
|
|
177
177
|
```
|
|
178
178
|
|
|
179
179
|
See [`caw/auth/README.md`](caw/auth/README.md) for details on how it works, container setup, and supported agents.
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
# caw auth — Credential Management for Docker Containers
|
|
2
|
+
|
|
3
|
+
Coding agents store OAuth credentials in home directory files (e.g., `~/.claude/.credentials.json`). When running agents inside Docker containers, token refresh creates new tokens (OAuth rotation), invalidating the host's tokens.
|
|
4
|
+
|
|
5
|
+
`caw auth` solves this **without modifying host files**: it bind-mounts the host credentials directly into the container, and runs an inotify-based guard that syncs the container user's home copy with the bind-mounted host file in both directions.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
HOST: CONTAINER:
|
|
11
|
+
|
|
12
|
+
~/.claude/.credentials.json ←—docker bind—→ /tmp/caw_auth/claude/credentials.json
|
|
13
|
+
(untouched, real file) ↓ copy + inotify sync
|
|
14
|
+
/home/playground/.claude/.credentials.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
`~/.caw/auth/` only holds things CAW legitimately owns: the manifest, the container setup script, and cleaned/stripped configs. Credentials stay at their original host paths.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
caw auth setup # snapshot configs + write manifest/setup script
|
|
23
|
+
caw auth setup --agents claude codex # specific agents only
|
|
24
|
+
|
|
25
|
+
caw auth status # token expiry, last modified, mount flags
|
|
26
|
+
|
|
27
|
+
docker run $(caw auth docker-flags) -v ./project:/work my-image
|
|
28
|
+
|
|
29
|
+
caw auth teardown # rm -rf ~/.caw/auth/ (host files never touched)
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Commands
|
|
33
|
+
|
|
34
|
+
### `caw auth setup`
|
|
35
|
+
|
|
36
|
+
Reads credentials and configs from the host, validates them, writes cleaned configs and a credential snapshot into `~/.caw/auth/`, and generates `manifest.json` + `setup-container.sh`. Host credential files are **read but never modified**.
|
|
37
|
+
|
|
38
|
+
- Credential files (tokens, OAuth) — `strategy: bind`. Bind-mounted from the host into the container at run time; the container-side guard copies them to the user's home and keeps them in sync.
|
|
39
|
+
- Config files (.claude.json, config.toml) — `strategy: copy`. Cleaned/stripped for containers and shipped in the staging directory.
|
|
40
|
+
|
|
41
|
+
### `caw auth teardown`
|
|
42
|
+
|
|
43
|
+
Removes `~/.caw/auth/`. Host credential files are never involved.
|
|
44
|
+
|
|
45
|
+
### `caw auth status`
|
|
46
|
+
|
|
47
|
+
Shows a table with each managed file, where its source of truth lives (host for bind, staged for copy), last modified time, and token expiry for credential files. Credential freshness is read from the host file directly.
|
|
48
|
+
|
|
49
|
+
### `caw auth docker-flags`
|
|
50
|
+
|
|
51
|
+
Emits one directory mount for the staging area plus one file mount per credential:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
$ caw auth docker-flags
|
|
55
|
+
-v /home/user/.caw/auth:/tmp/caw_auth:rw \
|
|
56
|
+
-v /home/user/.claude/.credentials.json:/tmp/caw_auth/claude/credentials.json:rw \
|
|
57
|
+
-v /home/user/.codex/auth.json:/tmp/caw_auth/codex/auth.json:rw
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Command substitution (`$(caw auth docker-flags)`) expands these into separate `docker run` arguments.
|
|
61
|
+
|
|
62
|
+
## Container setup
|
|
63
|
+
|
|
64
|
+
The generated `setup-container.sh` runs inside the container (called from your entrypoint). It reads `manifest.json`, copies credentials and configs into the container user's home, and starts a bidirectional inotify guard for credential sync.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
# In your entrypoint.sh:
|
|
68
|
+
if [ -f /tmp/caw_auth/setup-container.sh ]; then
|
|
69
|
+
/tmp/caw_auth/setup-container.sh /tmp/caw_auth /home/playground playground
|
|
70
|
+
fi
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The guard runs as root and uses plain `cp` (no `--preserve`, no `chown` on the mount side), so writes back to the host file preserve the host user's uid/gid/mode on the real inode. Requires `jq` in the container image; `inotify-tools` is installed automatically if not present.
|
|
74
|
+
|
|
75
|
+
## Directory structure
|
|
76
|
+
|
|
77
|
+
```
|
|
78
|
+
~/.caw/auth/
|
|
79
|
+
├── manifest.json # file map + metadata (records strategy per file)
|
|
80
|
+
├── setup-container.sh # POSIX script for container setup
|
|
81
|
+
├── claude/
|
|
82
|
+
│ ├── credentials.json # snapshot (bind-mounted over at container run)
|
|
83
|
+
│ └── config.json # cleaned .claude.json (copied)
|
|
84
|
+
└── codex/
|
|
85
|
+
├── auth.json # snapshot (bind-mounted over at container run)
|
|
86
|
+
└── config.toml # cleaned config (copied)
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
The credential snapshots exist so Docker has a target path to overlay with the host file. The staged bytes are never read at container run time — the bind mount supersedes them.
|
|
90
|
+
|
|
91
|
+
## Supported agents
|
|
92
|
+
|
|
93
|
+
| Agent | Credential files | Config files |
|
|
94
|
+
|-------|-----------------|--------------|
|
|
95
|
+
| Claude Code | `.claude/.credentials.json` | `.claude.json` (stripped to essential keys) |
|
|
96
|
+
| Codex | `.codex/auth.json` | `.codex/config.toml` (local trust removed) |
|
|
97
|
+
|
|
98
|
+
## Programmatic API
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from caw.auth import setup, teardown, get_status, get_docker_flags
|
|
102
|
+
|
|
103
|
+
setup(agents=["claude"])
|
|
104
|
+
statuses = get_status()
|
|
105
|
+
flags = get_docker_flags()
|
|
106
|
+
teardown()
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
See [`examples/auth.py`](../../examples/auth.py) for a full example.
|
|
110
|
+
|
|
111
|
+
## Known limitations
|
|
112
|
+
|
|
113
|
+
- **OAuth token rotation**: a refresh returns a new refresh token, invalidating the old one. If two processes refresh simultaneously, one gets an invalid token. Don't run the same agent identity in two places at once.
|
|
114
|
+
- **Atomic rewrites**: if an agent refreshes by writing a temp file and `rename(2)`-ing it over the credential, a single-file bind mount detaches from the new inode. If this becomes a real problem for a given agent, switch that agent's bind to a directory bind (mount the parent directory instead), which survives renames.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"""caw.auth — Credential management for Docker containers."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from .collector import default_auth_dir, setup
|
|
7
|
+
from .manifest import Manifest, AgentManifest, ManifestFile
|
|
8
|
+
from .providers import PROVIDERS, AgentAuthProvider, CollectedFile
|
|
9
|
+
from .status import AuthFileStatus, get_docker_flags, get_status, status
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class TeardownWouldOrphanSymlinksError(RuntimeError):
|
|
13
|
+
"""Raised when teardown would break host symlinks left by the old caw design."""
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _find_old_design_symlinks(auth_dir: Path) -> list[tuple[Path, Path]]:
|
|
17
|
+
"""Return (host_path, target_inside_auth_dir) pairs for any host files
|
|
18
|
+
that are symlinks pointing into ``auth_dir``.
|
|
19
|
+
|
|
20
|
+
The pre-bind-mount version of caw replaced host credential files with
|
|
21
|
+
symlinks into ~/.caw/auth/. Removing ``auth_dir`` in that state would
|
|
22
|
+
leave dangling symlinks with no backup. This helper lets teardown detect
|
|
23
|
+
the situation and refuse.
|
|
24
|
+
"""
|
|
25
|
+
manifest_path = auth_dir / "manifest.json"
|
|
26
|
+
if not manifest_path.exists():
|
|
27
|
+
return []
|
|
28
|
+
manifest = Manifest.load(manifest_path)
|
|
29
|
+
host_home = Path(manifest.host_home)
|
|
30
|
+
resolved_auth = auth_dir.resolve()
|
|
31
|
+
|
|
32
|
+
dangerous: list[tuple[Path, Path]] = []
|
|
33
|
+
for agent in manifest.agents.values():
|
|
34
|
+
for mf in agent.files:
|
|
35
|
+
host = host_home / mf.host_original
|
|
36
|
+
if not host.is_symlink():
|
|
37
|
+
continue
|
|
38
|
+
try:
|
|
39
|
+
target = Path(str(host.resolve(strict=False)))
|
|
40
|
+
except OSError:
|
|
41
|
+
continue
|
|
42
|
+
try:
|
|
43
|
+
target.relative_to(resolved_auth)
|
|
44
|
+
except ValueError:
|
|
45
|
+
continue
|
|
46
|
+
dangerous.append((host, target))
|
|
47
|
+
return dangerous
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def teardown(auth_dir: str | Path | None = None, force: bool = False) -> None:
|
|
51
|
+
"""Remove the auth directory. Host credential files are never touched.
|
|
52
|
+
|
|
53
|
+
Refuses to run if any host credential file is still a symlink into
|
|
54
|
+
``auth_dir`` (legacy state from the old symlink-based design), since
|
|
55
|
+
removing the directory would leave dangling symlinks with no backup.
|
|
56
|
+
Pass ``force=True`` to override.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
auth_dir: Custom auth directory. Defaults to ~/.caw/auth/.
|
|
60
|
+
force: Delete even if host symlinks point into ``auth_dir``.
|
|
61
|
+
|
|
62
|
+
Raises:
|
|
63
|
+
TeardownWouldOrphanSymlinksError: If host symlinks point into the
|
|
64
|
+
auth directory and ``force`` is False.
|
|
65
|
+
"""
|
|
66
|
+
target = Path(auth_dir) if auth_dir else default_auth_dir()
|
|
67
|
+
if not target.exists():
|
|
68
|
+
return
|
|
69
|
+
|
|
70
|
+
if not force:
|
|
71
|
+
dangerous = _find_old_design_symlinks(target)
|
|
72
|
+
if dangerous:
|
|
73
|
+
lines = [f" {host} -> {t}" for host, t in dangerous]
|
|
74
|
+
raise TeardownWouldOrphanSymlinksError(
|
|
75
|
+
"Refusing to remove "
|
|
76
|
+
f"{target}: host credential files still symlink into it "
|
|
77
|
+
"(leftover from the old symlink-based design). Removing the "
|
|
78
|
+
"directory would leave dangling symlinks and you would need "
|
|
79
|
+
"to re-authenticate every agent.\n\n"
|
|
80
|
+
"Do one of:\n"
|
|
81
|
+
" 1. Replace each symlink with its real file first:\n"
|
|
82
|
+
" for f in <paths>; do cp --remove-destination "
|
|
83
|
+
'"$(readlink -f "$f")" "$f"; done\n'
|
|
84
|
+
" 2. Call teardown(force=True) if you accept re-auth.\n\n"
|
|
85
|
+
"Affected symlinks:\n" + "\n".join(lines)
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
shutil.rmtree(target)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def __getattr__(name: str):
|
|
92
|
+
# Re-resolve AUTH_DIR at access time so users who set CAW_AUTH_DIR after
|
|
93
|
+
# `from caw.auth import AUTH_DIR` still see the override.
|
|
94
|
+
if name == "AUTH_DIR":
|
|
95
|
+
return default_auth_dir()
|
|
96
|
+
raise AttributeError(name)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
__all__ = [
|
|
100
|
+
"AUTH_DIR",
|
|
101
|
+
"AgentAuthProvider",
|
|
102
|
+
"default_auth_dir",
|
|
103
|
+
"AgentManifest",
|
|
104
|
+
"AuthFileStatus",
|
|
105
|
+
"CollectedFile",
|
|
106
|
+
"Manifest",
|
|
107
|
+
"ManifestFile",
|
|
108
|
+
"PROVIDERS",
|
|
109
|
+
"TeardownWouldOrphanSymlinksError",
|
|
110
|
+
"get_docker_flags",
|
|
111
|
+
"get_status",
|
|
112
|
+
"setup",
|
|
113
|
+
"status",
|
|
114
|
+
"teardown",
|
|
115
|
+
]
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"""CLI subcommands for `caw auth`."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Annotated, Optional
|
|
7
|
+
|
|
8
|
+
import typer
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
app = typer.Typer(help="Manage credentials for Docker containers.")
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@app.command()
|
|
15
|
+
def setup(
|
|
16
|
+
agents: Annotated[
|
|
17
|
+
Optional[list[str]],
|
|
18
|
+
typer.Option("--agents", "-a", help="Agents to include (claude, codex, or all)"),
|
|
19
|
+
] = None,
|
|
20
|
+
source_home: Annotated[
|
|
21
|
+
str,
|
|
22
|
+
typer.Option("--source-home", help="Source home directory to read credentials from"),
|
|
23
|
+
] = str(Path.home()),
|
|
24
|
+
):
|
|
25
|
+
"""Snapshot credentials and write the container setup bundle into ~/.caw/auth/.
|
|
26
|
+
|
|
27
|
+
Host credential files are not modified; they are bind-mounted into the
|
|
28
|
+
container at run time via `caw auth docker-flags`.
|
|
29
|
+
"""
|
|
30
|
+
from .collector import setup as do_setup
|
|
31
|
+
|
|
32
|
+
do_setup(agents=agents or ["all"], source_home=source_home)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@app.command()
|
|
36
|
+
def teardown(
|
|
37
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-n", help="Show what would be done")] = False,
|
|
38
|
+
force: Annotated[
|
|
39
|
+
bool,
|
|
40
|
+
typer.Option("--force", "-f", help="Delete even if host symlinks point into the auth dir"),
|
|
41
|
+
] = False,
|
|
42
|
+
):
|
|
43
|
+
"""Remove ~/.caw/auth/. Host credential files are untouched.
|
|
44
|
+
|
|
45
|
+
Refuses to run if host credentials are still symlinks into the auth
|
|
46
|
+
directory (leftover from the old symlink-based design). Use `--force`
|
|
47
|
+
to override — but you will have to re-authenticate every agent.
|
|
48
|
+
"""
|
|
49
|
+
from . import TeardownWouldOrphanSymlinksError, teardown as do_teardown
|
|
50
|
+
from .collector import default_auth_dir
|
|
51
|
+
|
|
52
|
+
auth_dir = default_auth_dir()
|
|
53
|
+
|
|
54
|
+
if dry_run:
|
|
55
|
+
if auth_dir.exists():
|
|
56
|
+
typer.echo(f"Would remove: {auth_dir}")
|
|
57
|
+
else:
|
|
58
|
+
typer.echo(f"Nothing to remove: {auth_dir} does not exist.")
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if not auth_dir.exists():
|
|
62
|
+
typer.echo(f"Nothing to remove: {auth_dir} does not exist.")
|
|
63
|
+
return
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
do_teardown(force=force)
|
|
67
|
+
except TeardownWouldOrphanSymlinksError as e:
|
|
68
|
+
typer.echo(str(e), err=True)
|
|
69
|
+
raise typer.Exit(1)
|
|
70
|
+
typer.echo(f"Removed {auth_dir}.")
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
@app.command("status")
|
|
74
|
+
def status_cmd(
|
|
75
|
+
agents: Annotated[
|
|
76
|
+
Optional[list[str]],
|
|
77
|
+
typer.Option("--agents", "-a", help="Agents to show"),
|
|
78
|
+
] = None,
|
|
79
|
+
):
|
|
80
|
+
"""Show token expiry, last modified, and docker mount flags."""
|
|
81
|
+
from .status import status as do_status
|
|
82
|
+
|
|
83
|
+
do_status(agents=agents)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@app.command("docker-flags")
|
|
87
|
+
def docker_flags():
|
|
88
|
+
"""Output the -v flags for docker (one per bind mount, space-separated)."""
|
|
89
|
+
from .status import get_docker_flags as do_get_docker_flags
|
|
90
|
+
|
|
91
|
+
try:
|
|
92
|
+
typer.echo(do_get_docker_flags())
|
|
93
|
+
except FileNotFoundError:
|
|
94
|
+
typer.echo("Error: manifest.json not found. Run `caw auth setup` first.", err=True)
|
|
95
|
+
raise typer.Exit(1)
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
from __future__ import annotations
|
|
4
4
|
|
|
5
|
+
import os
|
|
5
6
|
import textwrap
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
@@ -12,8 +13,27 @@ from .providers import PROVIDERS, AgentAuthProvider, CollectedFile
|
|
|
12
13
|
|
|
13
14
|
console = Console()
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
16
|
+
|
|
17
|
+
def default_auth_dir() -> Path:
|
|
18
|
+
"""Return the default auth directory, honoring the ``CAW_AUTH_DIR`` env var.
|
|
19
|
+
|
|
20
|
+
Set ``CAW_AUTH_DIR`` (e.g. by an embedding tool that wants caw's state to
|
|
21
|
+
live inside its own home) to relocate the staging dir. Falls back to
|
|
22
|
+
``~/.caw/auth/``. Resolved on every call so the override can be set after
|
|
23
|
+
import.
|
|
24
|
+
"""
|
|
25
|
+
override = os.environ.get("CAW_AUTH_DIR")
|
|
26
|
+
if override:
|
|
27
|
+
return Path(override).expanduser()
|
|
28
|
+
return Path.home() / ".caw" / "auth"
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def __getattr__(name: str):
|
|
32
|
+
# Module-level dynamic AUTH_DIR so `from caw.auth.collector import AUTH_DIR`
|
|
33
|
+
# picks up the current env var at access time.
|
|
34
|
+
if name == "AUTH_DIR":
|
|
35
|
+
return default_auth_dir()
|
|
36
|
+
raise AttributeError(name)
|
|
17
37
|
|
|
18
38
|
|
|
19
39
|
def _resolve_providers(agent_names: list[str]) -> list[AgentAuthProvider]:
|
|
@@ -84,11 +104,19 @@ def _generate_setup_container_sh(manifest: Manifest) -> str:
|
|
|
84
104
|
target_dir=$(dirname "$target_path")
|
|
85
105
|
mkdir -p "$target_dir"
|
|
86
106
|
|
|
87
|
-
if [ "$strategy" = "
|
|
88
|
-
# Credential files
|
|
89
|
-
# The
|
|
90
|
-
#
|
|
91
|
-
#
|
|
107
|
+
if [ "$strategy" = "bind" ]; then
|
|
108
|
+
# Credential files are bind-mounted from the host directly into
|
|
109
|
+
# $MOUNT_POINT. The setup script runs as root so it can read the
|
|
110
|
+
# host-owned 0600 file; we copy it into the container user's home
|
|
111
|
+
# (chowned to the container user) and start an inotify guard to
|
|
112
|
+
# keep the two in sync. Writes from the guard use plain `cp`,
|
|
113
|
+
# which preserves the host inode's uid/gid/mode on the mount side.
|
|
114
|
+
if [ ! -s "$source_path" ]; then
|
|
115
|
+
echo "[setup-container] ERROR: $source_path is empty or missing."
|
|
116
|
+
echo "[setup-container] Did you forget the credential file mount for $src?"
|
|
117
|
+
echo "[setup-container] Use \`caw auth docker-flags\` to get the full flag list."
|
|
118
|
+
exit 1
|
|
119
|
+
fi
|
|
92
120
|
rm -f "$target_path"
|
|
93
121
|
cp "$source_path" "$target_path"
|
|
94
122
|
chmod "$mode" "$target_path"
|
|
@@ -204,22 +232,24 @@ def _generate_setup_container_sh(manifest: Manifest) -> str:
|
|
|
204
232
|
def setup(
|
|
205
233
|
agents: list[str] | None = None,
|
|
206
234
|
source_home: str | None = None,
|
|
207
|
-
force: bool = False,
|
|
208
235
|
dest_dir: str | Path | None = None,
|
|
209
236
|
) -> Path:
|
|
210
|
-
"""
|
|
237
|
+
"""Snapshot credentials and cleaned configs into an auth directory.
|
|
238
|
+
|
|
239
|
+
Host credential files are read but not modified. At container run time
|
|
240
|
+
they are bind-mounted into the same paths under the mount point — see
|
|
241
|
+
:func:`caw.auth.get_docker_flags`.
|
|
211
242
|
|
|
212
243
|
Args:
|
|
213
244
|
agents: List of agent names, or None / ["all"] for all agents.
|
|
214
245
|
source_home: Home directory to read credentials from.
|
|
215
|
-
force: Overwrite existing auth dir without prompting.
|
|
216
246
|
dest_dir: Custom destination directory. Defaults to ~/.caw/auth/.
|
|
217
247
|
|
|
218
248
|
Returns:
|
|
219
249
|
Path to the auth directory.
|
|
220
250
|
"""
|
|
221
251
|
src_home = Path(source_home) if source_home else Path.home()
|
|
222
|
-
auth_dir = Path(dest_dir) if dest_dir else
|
|
252
|
+
auth_dir = Path(dest_dir) if dest_dir else default_auth_dir()
|
|
223
253
|
|
|
224
254
|
console.print(f"[bold]Collecting credentials into {auth_dir}/[/bold]\n")
|
|
225
255
|
|
|
@@ -268,22 +298,8 @@ def setup(
|
|
|
268
298
|
if auth_dir.exists():
|
|
269
299
|
import shutil
|
|
270
300
|
|
|
271
|
-
# Preserve .backups directory across re-collections
|
|
272
|
-
backups = auth_dir / ".backups"
|
|
273
|
-
backup_tmp = None
|
|
274
|
-
if backups.exists():
|
|
275
|
-
backup_tmp = auth_dir.parent / ".backups_tmp"
|
|
276
|
-
if backup_tmp.exists():
|
|
277
|
-
shutil.rmtree(backup_tmp)
|
|
278
|
-
backups.rename(backup_tmp)
|
|
279
|
-
|
|
280
301
|
shutil.rmtree(auth_dir)
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
if backup_tmp and backup_tmp.exists():
|
|
284
|
-
backup_tmp.rename(auth_dir / ".backups")
|
|
285
|
-
else:
|
|
286
|
-
auth_dir.mkdir(parents=True)
|
|
302
|
+
auth_dir.mkdir(parents=True)
|
|
287
303
|
|
|
288
304
|
# Write collected files
|
|
289
305
|
for cf in all_files:
|
|
@@ -314,11 +330,10 @@ def setup(
|
|
|
314
330
|
console.print(f" [dim]{cf.manifest_file.src}[/dim] ({ftype}, {strategy})")
|
|
315
331
|
console.print(" [dim]manifest.json[/dim]")
|
|
316
332
|
console.print(" [dim]setup-container.sh[/dim]")
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
do_link(agents=agents, force=force, auth_dir=auth_dir)
|
|
333
|
+
console.print(
|
|
334
|
+
"\n[dim]Host credential files are untouched; they will be bind-mounted "
|
|
335
|
+
"into the container at run time. Use `caw auth docker-flags` to get the "
|
|
336
|
+
"full -v flag list.[/dim]"
|
|
337
|
+
)
|
|
323
338
|
|
|
324
339
|
return auth_dir
|
|
@@ -16,7 +16,7 @@ class ManifestFile:
|
|
|
16
16
|
container_target: str # relative to $HOME in container, e.g. ".claude/.credentials.json"
|
|
17
17
|
host_original: str # relative to $HOME on host, e.g. ".claude/.credentials.json"
|
|
18
18
|
type: str # "credential" or "config"
|
|
19
|
-
strategy: str # "
|
|
19
|
+
strategy: str # "bind" (bind-mounted from host, copy+guard in container) or "copy"
|
|
20
20
|
mode: str # e.g. "0600"
|
|
21
21
|
|
|
22
22
|
|