coding-agent-wrapper 0.1.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.
- caw/__init__.py +88 -0
- caw/agent.py +578 -0
- caw/auth/README.md +118 -0
- caw/auth/__init__.py +23 -0
- caw/auth/cli.py +68 -0
- caw/auth/collector.py +324 -0
- caw/auth/linker.py +174 -0
- caw/auth/manifest.py +77 -0
- caw/auth/providers.py +433 -0
- caw/auth/status.py +241 -0
- caw/cli.py +50 -0
- caw/display.py +223 -0
- caw/faststats.py +298 -0
- caw/mcp.py +602 -0
- caw/models.py +385 -0
- caw/pricing.json +15 -0
- caw/pricing.py +33 -0
- caw/provider.py +135 -0
- caw/providers/__init__.py +0 -0
- caw/providers/claude_code.py +648 -0
- caw/providers/codex.py +564 -0
- caw/py.typed +0 -0
- caw/storage.py +184 -0
- caw/toolkit.py +198 -0
- caw/viewer/__init__.py +149 -0
- caw/viewer/static/index.html +847 -0
- coding_agent_wrapper-0.1.0.dist-info/METADATA +213 -0
- coding_agent_wrapper-0.1.0.dist-info/RECORD +31 -0
- coding_agent_wrapper-0.1.0.dist-info/WHEEL +4 -0
- coding_agent_wrapper-0.1.0.dist-info/entry_points.txt +2 -0
- coding_agent_wrapper-0.1.0.dist-info/licenses/LICENSE +202 -0
caw/auth/README.md
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
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 by making `~/.caw/auth/` the canonical location for all credential files, mounting it read-write into containers, and using symlinks so writes propagate in real-time.
|
|
6
|
+
|
|
7
|
+
## How it works
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
HOST: CONTAINER:
|
|
11
|
+
|
|
12
|
+
~/.claude/.credentials.json /home/playground/.claude/.credentials.json
|
|
13
|
+
↓ symlink ↑ copy + inotify sync
|
|
14
|
+
~/.caw/auth/claude/credentials.json ←—RW mount—→ /tmp/caw_auth/claude/credentials.json
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
On the host, credential files are replaced with symlinks into `~/.caw/auth/`. Inside the container, credentials are copied from the bind mount and kept in sync bidirectionally via an inotify-based guard.
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
caw auth setup # collect + symlink all detected agents
|
|
23
|
+
caw auth setup --agents claude codex # specific agents only
|
|
24
|
+
caw auth setup --force # overwrite existing auth dir and backups
|
|
25
|
+
|
|
26
|
+
caw auth status # symlink state, token expiry, last modified
|
|
27
|
+
|
|
28
|
+
docker run $(caw auth docker-flags) -v ./project:/work my-image
|
|
29
|
+
|
|
30
|
+
caw auth teardown # restore original files from backups
|
|
31
|
+
caw auth teardown --dry-run # preview what would be restored
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## Commands
|
|
35
|
+
|
|
36
|
+
### `caw auth setup`
|
|
37
|
+
|
|
38
|
+
Reads credentials from `~/.claude/`, `~/.codex/`, `~/.gemini/`, `~/.config/cursor/`. Writes canonical files to `~/.caw/auth/`, generates `manifest.json` and `setup-container.sh`, then replaces host credential files with symlinks.
|
|
39
|
+
|
|
40
|
+
- Credential files (tokens, OAuth) are marked `strategy: symlink` — shared read-write
|
|
41
|
+
- Config files (.claude.json, config.toml) are marked `strategy: copy` — cleaned/stripped for containers
|
|
42
|
+
|
|
43
|
+
### `caw auth teardown`
|
|
44
|
+
|
|
45
|
+
Restores original credential files from `~/.caw/auth/.backups/`.
|
|
46
|
+
|
|
47
|
+
### `caw auth status`
|
|
48
|
+
|
|
49
|
+
Shows a table with symlink state, token expiry, and last modified time for all managed files.
|
|
50
|
+
|
|
51
|
+
### `caw auth docker-flags`
|
|
52
|
+
|
|
53
|
+
Outputs the `-v` flag for Docker:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
$ caw auth docker-flags
|
|
57
|
+
-v /home/user/.caw/auth:/tmp/caw_auth:rw
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## Container setup
|
|
61
|
+
|
|
62
|
+
The generated `setup-container.sh` runs inside the container (called from your entrypoint). It reads `manifest.json`, copies credentials, and starts a bidirectional inotify guard for credential sync.
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
# In your entrypoint.sh:
|
|
66
|
+
if [ -f /tmp/caw_auth/setup-container.sh ]; then
|
|
67
|
+
/tmp/caw_auth/setup-container.sh /tmp/caw_auth /home/playground playground
|
|
68
|
+
fi
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
Requires `jq` in the container image. `inotify-tools` is installed automatically if not present.
|
|
72
|
+
|
|
73
|
+
## Directory structure
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
~/.caw/auth/
|
|
77
|
+
├── manifest.json # file map + metadata
|
|
78
|
+
├── setup-container.sh # POSIX script for container setup
|
|
79
|
+
├── .backups/ # originals before symlinking
|
|
80
|
+
├── claude/
|
|
81
|
+
│ ├── credentials.json # credential (symlinked)
|
|
82
|
+
│ └── config.json # cleaned .claude.json (copied)
|
|
83
|
+
├── codex/
|
|
84
|
+
│ ├── auth.json # credential (symlinked)
|
|
85
|
+
│ └── config.toml # cleaned config (copied)
|
|
86
|
+
├── gemini/
|
|
87
|
+
│ ├── oauth_creds.json # credential (symlinked)
|
|
88
|
+
│ └── ... # config files (copied)
|
|
89
|
+
└── cursor/
|
|
90
|
+
├── auth.json # credential (symlinked)
|
|
91
|
+
└── cli-config.json # cleaned config (copied)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Supported agents
|
|
95
|
+
|
|
96
|
+
| Agent | Credential files | Config files |
|
|
97
|
+
|-------|-----------------|--------------|
|
|
98
|
+
| Claude Code | `.claude/.credentials.json` | `.claude.json` (stripped to essential keys) |
|
|
99
|
+
| Codex | `.codex/auth.json` | `.codex/config.toml` (local trust removed) |
|
|
100
|
+
| Gemini CLI | `.gemini/oauth_creds.json` | `google_accounts.json`, `settings.json`, `installation_id` |
|
|
101
|
+
| Cursor | `.config/cursor/auth.json` | `.cursor/cli-config.json` (stripped) |
|
|
102
|
+
|
|
103
|
+
## Programmatic API
|
|
104
|
+
|
|
105
|
+
```python
|
|
106
|
+
from caw.auth import setup, teardown, get_status, get_docker_flags
|
|
107
|
+
|
|
108
|
+
setup(agents=["claude"])
|
|
109
|
+
statuses = get_status()
|
|
110
|
+
flags = get_docker_flags()
|
|
111
|
+
teardown()
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
See [`examples/auth.py`](../../examples/auth.py) for a full example.
|
|
115
|
+
|
|
116
|
+
## Known limitation
|
|
117
|
+
|
|
118
|
+
OAuth token rotation means 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.
|
caw/auth/__init__.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""caw.auth — Credential management for Docker containers."""
|
|
2
|
+
|
|
3
|
+
from .collector import AUTH_DIR, setup
|
|
4
|
+
from .linker import teardown
|
|
5
|
+
from .manifest import AgentManifest, Manifest, ManifestFile
|
|
6
|
+
from .providers import PROVIDERS, AgentAuthProvider, CollectedFile
|
|
7
|
+
from .status import AuthFileStatus, get_docker_flags, get_status, status
|
|
8
|
+
|
|
9
|
+
__all__ = [
|
|
10
|
+
"AUTH_DIR",
|
|
11
|
+
"AgentAuthProvider",
|
|
12
|
+
"AgentManifest",
|
|
13
|
+
"AuthFileStatus",
|
|
14
|
+
"CollectedFile",
|
|
15
|
+
"Manifest",
|
|
16
|
+
"ManifestFile",
|
|
17
|
+
"PROVIDERS",
|
|
18
|
+
"get_docker_flags",
|
|
19
|
+
"get_status",
|
|
20
|
+
"setup",
|
|
21
|
+
"status",
|
|
22
|
+
"teardown",
|
|
23
|
+
]
|
caw/auth/cli.py
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
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, gemini, cursor, 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
|
+
force: Annotated[bool, typer.Option("--force", "-f", help="Overwrite existing auth dir and backups")] = False,
|
|
25
|
+
):
|
|
26
|
+
"""Collect credentials from host into ~/.caw/auth/ and symlink them."""
|
|
27
|
+
from .collector import setup as do_setup
|
|
28
|
+
|
|
29
|
+
do_setup(agents=agents or ["all"], source_home=source_home, force=force)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
@app.command()
|
|
33
|
+
def teardown(
|
|
34
|
+
agents: Annotated[
|
|
35
|
+
Optional[list[str]],
|
|
36
|
+
typer.Option("--agents", "-a", help="Agents to restore"),
|
|
37
|
+
] = None,
|
|
38
|
+
dry_run: Annotated[bool, typer.Option("--dry-run", "-n", help="Show what would be done")] = False,
|
|
39
|
+
):
|
|
40
|
+
"""Restore original credential files from backups."""
|
|
41
|
+
from .linker import teardown as do_teardown
|
|
42
|
+
|
|
43
|
+
do_teardown(agents=agents, dry_run=dry_run)
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@app.command("status")
|
|
47
|
+
def status_cmd(
|
|
48
|
+
agents: Annotated[
|
|
49
|
+
Optional[list[str]],
|
|
50
|
+
typer.Option("--agents", "-a", help="Agents to show"),
|
|
51
|
+
] = None,
|
|
52
|
+
):
|
|
53
|
+
"""Show symlink state, token expiry, and last modified time."""
|
|
54
|
+
from .status import status as do_status
|
|
55
|
+
|
|
56
|
+
do_status(agents=agents)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
@app.command("docker-flags")
|
|
60
|
+
def docker_flags():
|
|
61
|
+
"""Output the -v flag string for docker."""
|
|
62
|
+
from .status import get_docker_flags as do_get_docker_flags
|
|
63
|
+
|
|
64
|
+
try:
|
|
65
|
+
typer.echo(do_get_docker_flags())
|
|
66
|
+
except FileNotFoundError:
|
|
67
|
+
typer.echo("Error: manifest.json not found. Run `caw auth setup` first.", err=True)
|
|
68
|
+
raise typer.Exit(1)
|
caw/auth/collector.py
ADDED
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
"""Set up credentials from host into ~/.caw/auth/ and generate manifest + setup script."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import textwrap
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
|
|
10
|
+
from .manifest import AgentManifest, Manifest
|
|
11
|
+
from .providers import PROVIDERS, AgentAuthProvider, CollectedFile
|
|
12
|
+
|
|
13
|
+
console = Console()
|
|
14
|
+
|
|
15
|
+
# Canonical auth directory
|
|
16
|
+
AUTH_DIR = Path.home() / ".caw" / "auth"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _resolve_providers(agent_names: list[str]) -> list[AgentAuthProvider]:
|
|
20
|
+
"""Resolve provider names to provider objects."""
|
|
21
|
+
if "all" in agent_names:
|
|
22
|
+
return list(PROVIDERS.values())
|
|
23
|
+
result = []
|
|
24
|
+
for name in agent_names:
|
|
25
|
+
if name not in PROVIDERS:
|
|
26
|
+
available = ", ".join(PROVIDERS.keys())
|
|
27
|
+
raise ValueError(f"Unknown agent '{name}'. Available: {available}")
|
|
28
|
+
result.append(PROVIDERS[name])
|
|
29
|
+
return result
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _validate_providers(
|
|
33
|
+
providers: list[AgentAuthProvider], src_home: Path
|
|
34
|
+
) -> tuple[list[AgentAuthProvider], list[tuple[AgentAuthProvider, list[str]]]]:
|
|
35
|
+
"""Validate providers, returning (valid, skipped) lists."""
|
|
36
|
+
valid = []
|
|
37
|
+
skipped = []
|
|
38
|
+
for provider in providers:
|
|
39
|
+
missing = provider.validate(src_home)
|
|
40
|
+
if missing:
|
|
41
|
+
skipped.append((provider, missing))
|
|
42
|
+
else:
|
|
43
|
+
valid.append(provider)
|
|
44
|
+
return valid, skipped
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def _generate_setup_container_sh(manifest: Manifest) -> str:
|
|
48
|
+
"""Generate a standalone POSIX shell script for container-side setup."""
|
|
49
|
+
return textwrap.dedent("""\
|
|
50
|
+
#!/bin/sh
|
|
51
|
+
# setup-container.sh — generated by `caw auth setup`
|
|
52
|
+
# Sets up credential copies, config copies, and an inotify-based
|
|
53
|
+
# bidirectional credential sync guard inside a container.
|
|
54
|
+
# Usage: setup-container.sh <mount_point> <container_home> <username>
|
|
55
|
+
set -e
|
|
56
|
+
|
|
57
|
+
MOUNT_POINT="${1:-/tmp/caw_auth}"
|
|
58
|
+
CONTAINER_HOME="${2:-/home/playground}"
|
|
59
|
+
USERNAME="${3:-playground}"
|
|
60
|
+
|
|
61
|
+
MANIFEST="$MOUNT_POINT/manifest.json"
|
|
62
|
+
GUARD_PAIRS="/tmp/_caw_guard_pairs"
|
|
63
|
+
GUARD_PID_FILE="/tmp/caw_credential_guard.pid"
|
|
64
|
+
|
|
65
|
+
if [ ! -f "$MANIFEST" ]; then
|
|
66
|
+
echo "[setup-container] ERROR: manifest.json not found at $MANIFEST"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
69
|
+
|
|
70
|
+
echo "[setup-container] Setting up auth files from $MOUNT_POINT..."
|
|
71
|
+
|
|
72
|
+
# Clean up guard pairs file from any previous run
|
|
73
|
+
rm -f "$GUARD_PAIRS"
|
|
74
|
+
|
|
75
|
+
# Parse manifest with jq and process each file
|
|
76
|
+
jq -r '
|
|
77
|
+
.agents | to_entries[] | .value.files[] |
|
|
78
|
+
[.src, .container_target, .strategy, .mode] | @tsv
|
|
79
|
+
' "$MANIFEST" | while IFS=$(printf '\\t') read -r src target strategy mode; do
|
|
80
|
+
target_path="$CONTAINER_HOME/$target"
|
|
81
|
+
source_path="$MOUNT_POINT/$src"
|
|
82
|
+
|
|
83
|
+
# Create parent directory
|
|
84
|
+
target_dir=$(dirname "$target_path")
|
|
85
|
+
mkdir -p "$target_dir"
|
|
86
|
+
|
|
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.
|
|
92
|
+
rm -f "$target_path"
|
|
93
|
+
cp "$source_path" "$target_path"
|
|
94
|
+
chmod "$mode" "$target_path"
|
|
95
|
+
chown "$USERNAME:$USERNAME" "$target_dir" 2>/dev/null || true
|
|
96
|
+
chown "$USERNAME:$USERNAME" "$target_path" 2>/dev/null || true
|
|
97
|
+
echo "[setup-container] copy (credential): $target (mode $mode)"
|
|
98
|
+
# Record source:local pair for the guard
|
|
99
|
+
echo "$source_path|$target_path|$mode" >> "$GUARD_PAIRS"
|
|
100
|
+
elif [ "$strategy" = "copy" ]; then
|
|
101
|
+
cp "$source_path" "$target_path"
|
|
102
|
+
chmod "$mode" "$target_path"
|
|
103
|
+
chown "$USERNAME:$USERNAME" "$target_dir" 2>/dev/null || true
|
|
104
|
+
chown "$USERNAME:$USERNAME" "$target_path" 2>/dev/null || true
|
|
105
|
+
echo "[setup-container] copy (config): $target (mode $mode)"
|
|
106
|
+
fi
|
|
107
|
+
done
|
|
108
|
+
|
|
109
|
+
# -------------------------------------------------------------------
|
|
110
|
+
# Credential guard — bidirectional sync via inotify
|
|
111
|
+
# -------------------------------------------------------------------
|
|
112
|
+
|
|
113
|
+
_caw_credential_guard() {
|
|
114
|
+
PAIRS_FILE="$1"
|
|
115
|
+
OWNER="$2"
|
|
116
|
+
|
|
117
|
+
# Build inotifywait watch list: unique directories from both sides
|
|
118
|
+
WATCH_DIRS=""
|
|
119
|
+
while IFS='|' read -r src local _mode; do
|
|
120
|
+
src_dir=$(dirname "$src")
|
|
121
|
+
local_dir=$(dirname "$local")
|
|
122
|
+
# Deduplicate by checking if already present
|
|
123
|
+
case "$WATCH_DIRS" in
|
|
124
|
+
*"$src_dir"*) ;;
|
|
125
|
+
*) WATCH_DIRS="$WATCH_DIRS $src_dir" ;;
|
|
126
|
+
esac
|
|
127
|
+
case "$WATCH_DIRS" in
|
|
128
|
+
*"$local_dir"*) ;;
|
|
129
|
+
*) WATCH_DIRS="$WATCH_DIRS $local_dir" ;;
|
|
130
|
+
esac
|
|
131
|
+
done < "$PAIRS_FILE"
|
|
132
|
+
|
|
133
|
+
echo "[credential-guard] Watching directories:$WATCH_DIRS"
|
|
134
|
+
|
|
135
|
+
while true; do
|
|
136
|
+
# shellcheck disable=SC2086
|
|
137
|
+
inotifywait -m -q -e close_write $WATCH_DIRS 2>/dev/null | while read -r dir event filename; do
|
|
138
|
+
# dir has a trailing slash from inotifywait
|
|
139
|
+
changed_path="${dir}${filename}"
|
|
140
|
+
|
|
141
|
+
while IFS='|' read -r src local mode; do
|
|
142
|
+
if [ "$changed_path" = "$src" ]; then
|
|
143
|
+
# Source (bind mount) changed — sync to local copy
|
|
144
|
+
src_hash=$(md5sum "$src" 2>/dev/null | cut -d' ' -f1)
|
|
145
|
+
local_hash=$(md5sum "$local" 2>/dev/null | cut -d' ' -f1)
|
|
146
|
+
if [ "$src_hash" != "$local_hash" ]; then
|
|
147
|
+
cp "$src" "$local"
|
|
148
|
+
chmod "$mode" "$local"
|
|
149
|
+
chown "$OWNER:$OWNER" "$local" 2>/dev/null || true
|
|
150
|
+
echo "[credential-guard] synced $src -> $local"
|
|
151
|
+
fi
|
|
152
|
+
elif [ "$changed_path" = "$local" ]; then
|
|
153
|
+
# Local copy changed — sync back to bind mount
|
|
154
|
+
src_hash=$(md5sum "$src" 2>/dev/null | cut -d' ' -f1)
|
|
155
|
+
local_hash=$(md5sum "$local" 2>/dev/null | cut -d' ' -f1)
|
|
156
|
+
if [ "$src_hash" != "$local_hash" ]; then
|
|
157
|
+
cp "$local" "$src"
|
|
158
|
+
echo "[credential-guard] synced $local -> $src"
|
|
159
|
+
fi
|
|
160
|
+
fi
|
|
161
|
+
done < "$PAIRS_FILE"
|
|
162
|
+
done
|
|
163
|
+
echo "[credential-guard] inotifywait exited, restarting in 2s..."
|
|
164
|
+
sleep 2
|
|
165
|
+
done
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
# Start the guard if there are credential pairs to watch
|
|
169
|
+
# Skip credential guard when using Bedrock (no credential sync needed)
|
|
170
|
+
if [ "${CLAUDE_CODE_USE_BEDROCK:-0}" = "1" ]; then
|
|
171
|
+
echo "[setup-container] Bedrock mode — skipping credential guard."
|
|
172
|
+
elif [ -f "$GUARD_PAIRS" ] && [ -s "$GUARD_PAIRS" ]; then
|
|
173
|
+
# Install inotify-tools if not present
|
|
174
|
+
if ! command -v inotifywait >/dev/null 2>&1; then
|
|
175
|
+
echo "[setup-container] Installing inotify-tools..."
|
|
176
|
+
if apt-get update -qq >/dev/null 2>&1 && apt-get install -y -qq inotify-tools >/dev/null 2>&1; then
|
|
177
|
+
echo "[setup-container] inotify-tools installed."
|
|
178
|
+
else
|
|
179
|
+
echo "[setup-container] WARNING: Failed to install inotify-tools. Credential guard will not run."
|
|
180
|
+
echo "[setup-container] Auth setup complete (without credential guard)."
|
|
181
|
+
exit 0
|
|
182
|
+
fi
|
|
183
|
+
fi
|
|
184
|
+
|
|
185
|
+
# Kill any previous guard
|
|
186
|
+
if [ -f "$GUARD_PID_FILE" ]; then
|
|
187
|
+
old_pid=$(cat "$GUARD_PID_FILE" 2>/dev/null)
|
|
188
|
+
if [ -n "$old_pid" ] && kill -0 "$old_pid" 2>/dev/null; then
|
|
189
|
+
kill "$old_pid" 2>/dev/null || true
|
|
190
|
+
echo "[setup-container] Stopped previous credential guard (PID $old_pid)."
|
|
191
|
+
fi
|
|
192
|
+
rm -f "$GUARD_PID_FILE"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
_caw_credential_guard "$GUARD_PAIRS" "$USERNAME" &
|
|
196
|
+
echo $! > "$GUARD_PID_FILE"
|
|
197
|
+
echo "[setup-container] Credential guard started (PID $(cat "$GUARD_PID_FILE"))."
|
|
198
|
+
fi
|
|
199
|
+
|
|
200
|
+
echo "[setup-container] Auth setup complete."
|
|
201
|
+
""")
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
def setup(
|
|
205
|
+
agents: list[str] | None = None,
|
|
206
|
+
source_home: str | None = None,
|
|
207
|
+
force: bool = False,
|
|
208
|
+
dest_dir: str | Path | None = None,
|
|
209
|
+
) -> Path:
|
|
210
|
+
"""Collect credentials from host into an auth directory and link them.
|
|
211
|
+
|
|
212
|
+
Args:
|
|
213
|
+
agents: List of agent names, or None / ["all"] for all agents.
|
|
214
|
+
source_home: Home directory to read credentials from.
|
|
215
|
+
force: Overwrite existing auth dir without prompting.
|
|
216
|
+
dest_dir: Custom destination directory. Defaults to ~/.caw/auth/.
|
|
217
|
+
|
|
218
|
+
Returns:
|
|
219
|
+
Path to the auth directory.
|
|
220
|
+
"""
|
|
221
|
+
src_home = Path(source_home) if source_home else Path.home()
|
|
222
|
+
auth_dir = Path(dest_dir) if dest_dir else AUTH_DIR
|
|
223
|
+
|
|
224
|
+
console.print(f"[bold]Collecting credentials into {auth_dir}/[/bold]\n")
|
|
225
|
+
|
|
226
|
+
# Resolve providers
|
|
227
|
+
selected = _resolve_providers(agents or ["all"])
|
|
228
|
+
|
|
229
|
+
# Validate
|
|
230
|
+
valid_providers, skipped_providers = _validate_providers(selected, src_home)
|
|
231
|
+
|
|
232
|
+
if not valid_providers:
|
|
233
|
+
console.print("[red]Error: No valid agent credentials found.[/red]")
|
|
234
|
+
for provider, missing in skipped_providers:
|
|
235
|
+
console.print(f" [dim]{provider.name}:[/dim] missing {', '.join(missing)}")
|
|
236
|
+
raise SystemExit(1)
|
|
237
|
+
|
|
238
|
+
# Warn about missing agents
|
|
239
|
+
if skipped_providers:
|
|
240
|
+
for provider, missing in skipped_providers:
|
|
241
|
+
console.print(f"[yellow]Warning: {provider.name} credentials not found:[/yellow] {', '.join(missing)}")
|
|
242
|
+
console.print()
|
|
243
|
+
|
|
244
|
+
# Show what we'll process
|
|
245
|
+
names = [p.name for p in valid_providers]
|
|
246
|
+
console.print(f"[dim]Agents:[/dim] {', '.join(names)}\n")
|
|
247
|
+
|
|
248
|
+
# Describe each provider
|
|
249
|
+
for provider in valid_providers:
|
|
250
|
+
desc = provider.describe(src_home)
|
|
251
|
+
console.print(f"[bold]{provider.name}:[/bold] {desc}")
|
|
252
|
+
console.print()
|
|
253
|
+
|
|
254
|
+
# Collect files from all providers
|
|
255
|
+
all_files: list[CollectedFile] = []
|
|
256
|
+
manifest = Manifest.create(host_home=str(src_home))
|
|
257
|
+
|
|
258
|
+
for provider in valid_providers:
|
|
259
|
+
console.print(f"[bold]Collecting {provider.name} files...[/bold]")
|
|
260
|
+
collected = provider.collect(src_home)
|
|
261
|
+
all_files.extend(collected)
|
|
262
|
+
|
|
263
|
+
# Build manifest entry
|
|
264
|
+
agent_manifest = AgentManifest(files=[cf.manifest_file for cf in collected])
|
|
265
|
+
manifest.agents[provider.name] = agent_manifest
|
|
266
|
+
|
|
267
|
+
# Write everything
|
|
268
|
+
if auth_dir.exists():
|
|
269
|
+
import shutil
|
|
270
|
+
|
|
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
|
+
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)
|
|
287
|
+
|
|
288
|
+
# Write collected files
|
|
289
|
+
for cf in all_files:
|
|
290
|
+
out_path = auth_dir / cf.manifest_file.src
|
|
291
|
+
out_path.parent.mkdir(parents=True, exist_ok=True)
|
|
292
|
+
out_path.write_bytes(cf.content)
|
|
293
|
+
out_path.chmod(int(cf.manifest_file.mode, 8))
|
|
294
|
+
|
|
295
|
+
# Write manifest
|
|
296
|
+
manifest.save(auth_dir / "manifest.json")
|
|
297
|
+
|
|
298
|
+
# Write setup-container.sh
|
|
299
|
+
setup_script = auth_dir / "setup-container.sh"
|
|
300
|
+
setup_script.write_text(_generate_setup_container_sh(manifest))
|
|
301
|
+
setup_script.chmod(0o755)
|
|
302
|
+
|
|
303
|
+
# Set directory permissions
|
|
304
|
+
for d in auth_dir.rglob("*"):
|
|
305
|
+
if d.is_dir():
|
|
306
|
+
d.chmod(0o755)
|
|
307
|
+
|
|
308
|
+
console.print("\n[bold green]Done![/bold green]")
|
|
309
|
+
console.print(f"Auth files written to: {auth_dir}")
|
|
310
|
+
console.print("\n[dim]Contents:[/dim]")
|
|
311
|
+
for cf in all_files:
|
|
312
|
+
strategy = cf.manifest_file.strategy
|
|
313
|
+
ftype = cf.manifest_file.type
|
|
314
|
+
console.print(f" [dim]{cf.manifest_file.src}[/dim] ({ftype}, {strategy})")
|
|
315
|
+
console.print(" [dim]manifest.json[/dim]")
|
|
316
|
+
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)
|
|
323
|
+
|
|
324
|
+
return auth_dir
|