coding-agent-wrapper 0.1.0__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/PKG-INFO +6 -6
  2. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/README.md +5 -5
  3. coding_agent_wrapper-0.1.2/caw/auth/README.md +114 -0
  4. coding_agent_wrapper-0.1.2/caw/auth/__init__.py +115 -0
  5. coding_agent_wrapper-0.1.2/caw/auth/cli.py +95 -0
  6. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/auth/collector.py +47 -32
  7. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/auth/manifest.py +1 -1
  8. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/auth/providers.py +2 -163
  9. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/auth/status.py +89 -82
  10. coding_agent_wrapper-0.1.2/caw/cli.py +104 -0
  11. coding_agent_wrapper-0.1.2/caw/traj_cli.py +998 -0
  12. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/viewer/static/index.html +2 -2
  13. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/pyproject.toml +2 -1
  14. coding_agent_wrapper-0.1.0/caw/auth/README.md +0 -118
  15. coding_agent_wrapper-0.1.0/caw/auth/__init__.py +0 -23
  16. coding_agent_wrapper-0.1.0/caw/auth/cli.py +0 -68
  17. coding_agent_wrapper-0.1.0/caw/auth/linker.py +0 -174
  18. coding_agent_wrapper-0.1.0/caw/cli.py +0 -50
  19. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/.gitignore +0 -0
  20. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/LICENSE +0 -0
  21. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/__init__.py +0 -0
  22. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/agent.py +0 -0
  23. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/display.py +0 -0
  24. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/faststats.py +0 -0
  25. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/mcp.py +0 -0
  26. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/models.py +0 -0
  27. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/pricing.json +0 -0
  28. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/pricing.py +0 -0
  29. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/provider.py +0 -0
  30. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/providers/__init__.py +0 -0
  31. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/providers/claude_code.py +0 -0
  32. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/providers/codex.py +0 -0
  33. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/py.typed +0 -0
  34. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/storage.py +0 -0
  35. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/toolkit.py +0 -0
  36. {coding_agent_wrapper-0.1.0 → coding_agent_wrapper-0.1.2}/caw/viewer/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: coding-agent-wrapper
3
- Version: 0.1.0
3
+ Version: 0.1.2
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, Gemini CLI, Cursor) with a unified interface, MCP tool servers, and credential management for Docker containers.
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, Codex, Gemini CLI, and Cursor.
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 # collect + symlink credentials
204
- caw auth status # check symlink state and token expiry
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 # restore original files
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, Gemini CLI, Cursor) with a unified interface, MCP tool servers, and credential management for Docker containers.
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, Codex, Gemini CLI, and Cursor.
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 # collect + symlink credentials
174
- caw auth status # check symlink state and token expiry
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 # restore original files
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
- # Canonical auth directory
16
- AUTH_DIR = Path.home() / ".caw" / "auth"
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" = "symlink" ]; then
88
- # Credential files: copy (not symlink) so container user can read 0600 files.
89
- # The bind-mounted source is owned by the host UID and is 0600, but the
90
- # setup script runs as root so it can read it. We copy into the container
91
- # user's home and record the pair for the inotify guard.
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
- """Collect credentials from host into an auth directory and link them.
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 AUTH_DIR
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
- auth_dir.mkdir(parents=True)
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
- # Link credential files
319
- console.print()
320
- from .linker import link as do_link
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 # "symlink" or "copy"
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