Merge pull request 'fix: disinto init: bootstrap shared CLAUDE_CONFIG_DIR for OAuth lock coherence (#641)' (#654) from fix/issue-641 into main
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
All checks were successful
ci/woodpecker/push/ci Pipeline was successful
This commit is contained in:
commit
b593635d64
4 changed files with 223 additions and 2 deletions
22
bin/disinto
22
bin/disinto
|
|
@ -32,6 +32,7 @@ source "${FACTORY_ROOT}/lib/generators.sh"
|
|||
source "${FACTORY_ROOT}/lib/forge-push.sh"
|
||||
source "${FACTORY_ROOT}/lib/ci-setup.sh"
|
||||
source "${FACTORY_ROOT}/lib/release.sh"
|
||||
source "${FACTORY_ROOT}/lib/claude-config.sh"
|
||||
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
|
|
@ -948,6 +949,18 @@ p.write_text(text)
|
|||
fi
|
||||
fi
|
||||
|
||||
# ── Claude shared config directory (#641) ───────────────────────────
|
||||
# Create CLAUDE_CONFIG_DIR for cross-container OAuth lock coherence.
|
||||
# proper-lockfile uses atomic mkdir(${CLAUDE_CONFIG_DIR}.lock), so all
|
||||
# containers sharing this path get native cross-container locking.
|
||||
if ! setup_claude_config_dir "$auto_yes"; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Write CLAUDE_SHARED_DIR and CLAUDE_CONFIG_DIR to .env (idempotent)
|
||||
_env_set_idempotent "CLAUDE_SHARED_DIR" "$CLAUDE_SHARED_DIR" "$env_file"
|
||||
_env_set_idempotent "CLAUDE_CONFIG_DIR" "$CLAUDE_CONFIG_DIR" "$env_file"
|
||||
|
||||
# Activate default agents (zero-cost when idle — they only invoke Claude
|
||||
# when there is actual work, so an empty project burns no LLM tokens)
|
||||
mkdir -p "${FACTORY_ROOT}/state"
|
||||
|
|
@ -976,10 +989,17 @@ p.write_text(text)
|
|||
echo "── Claude authentication ──────────────────────────────"
|
||||
echo " OAuth (shared across containers):"
|
||||
echo " Run 'claude auth login' on the host once."
|
||||
echo " Credentials in ~/.claude are mounted into containers."
|
||||
echo " Credentials in ${CLAUDE_CONFIG_DIR} are shared across containers."
|
||||
echo " API key (alternative — metered billing, no rotation issues):"
|
||||
echo " Set ANTHROPIC_API_KEY in .env to skip OAuth entirely."
|
||||
echo ""
|
||||
echo "── Claude config directory ────────────────────────────"
|
||||
echo " CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR}"
|
||||
echo " Add this to your shell rc (~/.bashrc or ~/.zshrc):"
|
||||
echo " export CLAUDE_CONFIG_DIR=${CLAUDE_CONFIG_DIR}"
|
||||
echo " This ensures interactive Claude Code sessions on this host"
|
||||
echo " share the same OAuth lock and token store as the factory."
|
||||
echo ""
|
||||
echo " Run 'disinto status' to verify."
|
||||
}
|
||||
|
||||
|
|
|
|||
103
lib/claude-config.sh
Normal file
103
lib/claude-config.sh
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#!/usr/bin/env bash
|
||||
# lib/claude-config.sh — Shared Claude config directory helpers (#641)
|
||||
#
|
||||
# Provides setup_claude_config_dir() for creating/migrating CLAUDE_CONFIG_DIR
|
||||
# and _env_set_idempotent() for writing env vars to .env files.
|
||||
#
|
||||
# Requires: CLAUDE_CONFIG_DIR, CLAUDE_SHARED_DIR (set by lib/env.sh)
|
||||
|
||||
# Idempotent .env writer.
|
||||
# Usage: _env_set_idempotent KEY VALUE FILE
|
||||
_env_set_idempotent() {
|
||||
local key="$1" value="$2" file="$3"
|
||||
if grep -q "^${key}=" "$file" 2>/dev/null; then
|
||||
local existing
|
||||
existing=$(grep "^${key}=" "$file" | head -1 | cut -d= -f2-)
|
||||
if [ "$existing" != "$value" ]; then
|
||||
sed -i "s|^${key}=.*|${key}=${value}|" "$file"
|
||||
fi
|
||||
else
|
||||
printf '%s=%s\n' "$key" "$value" >> "$file"
|
||||
fi
|
||||
}
|
||||
|
||||
# Create the shared CLAUDE_CONFIG_DIR, optionally migrating ~/.claude.
|
||||
# Usage: setup_claude_config_dir [auto_yes]
|
||||
setup_claude_config_dir() {
|
||||
local auto_yes="${1:-false}"
|
||||
local home_claude="${HOME}/.claude"
|
||||
|
||||
# Create the shared config directory (idempotent)
|
||||
install -d -m 0700 -o "$USER" "$CLAUDE_CONFIG_DIR"
|
||||
echo "Claude: ${CLAUDE_CONFIG_DIR} (ready)"
|
||||
|
||||
# If ~/.claude is already a symlink to CLAUDE_CONFIG_DIR, nothing to do
|
||||
if [ -L "$home_claude" ]; then
|
||||
local link_target
|
||||
link_target=$(readlink -f "$home_claude")
|
||||
local config_real
|
||||
config_real=$(readlink -f "$CLAUDE_CONFIG_DIR")
|
||||
if [ "$link_target" = "$config_real" ]; then
|
||||
echo "Claude: ${home_claude} -> ${CLAUDE_CONFIG_DIR} (symlink OK)"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
local home_exists=false home_nonempty=false
|
||||
local config_nonempty=false
|
||||
|
||||
# Check ~/.claude (skip if it's a symlink — already handled above)
|
||||
if [ -d "$home_claude" ] && [ ! -L "$home_claude" ]; then
|
||||
home_exists=true
|
||||
if [ -n "$(ls -A "$home_claude" 2>/dev/null)" ]; then
|
||||
home_nonempty=true
|
||||
fi
|
||||
fi
|
||||
|
||||
# Check CLAUDE_CONFIG_DIR contents
|
||||
if [ -n "$(ls -A "$CLAUDE_CONFIG_DIR" 2>/dev/null)" ]; then
|
||||
config_nonempty=true
|
||||
fi
|
||||
|
||||
# Case: both non-empty — abort, operator must reconcile
|
||||
if [ "$home_nonempty" = true ] && [ "$config_nonempty" = true ]; then
|
||||
echo "ERROR: both ${home_claude} and ${CLAUDE_CONFIG_DIR} exist and are non-empty" >&2
|
||||
echo " Reconcile manually: merge or remove one, then re-run disinto init" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Case: ~/.claude exists and CLAUDE_CONFIG_DIR is empty — offer migration
|
||||
if [ "$home_nonempty" = true ] && [ "$config_nonempty" = false ]; then
|
||||
local do_migrate=false
|
||||
if [ "$auto_yes" = true ]; then
|
||||
do_migrate=true
|
||||
elif [ -t 0 ]; then
|
||||
read -rp "Migrate ${home_claude} to ${CLAUDE_CONFIG_DIR}? [Y/n] " confirm
|
||||
if [[ ! "$confirm" =~ ^[Nn] ]]; then
|
||||
do_migrate=true
|
||||
fi
|
||||
else
|
||||
echo "Warning: ${home_claude} exists but cannot prompt for migration (no TTY)" >&2
|
||||
echo " Re-run with --yes to auto-migrate, or move files manually" >&2
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$do_migrate" = true ]; then
|
||||
# Move contents (not the dir itself) to preserve CLAUDE_CONFIG_DIR ownership
|
||||
cp -a "$home_claude/." "$CLAUDE_CONFIG_DIR/"
|
||||
rm -rf "$home_claude"
|
||||
ln -sfn "$CLAUDE_CONFIG_DIR" "$home_claude"
|
||||
echo "Claude: migrated ${home_claude} -> ${CLAUDE_CONFIG_DIR}"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
|
||||
# Case: ~/.claude exists but is empty, or doesn't exist — create symlink
|
||||
if [ "$home_exists" = true ] && [ "$home_nonempty" = false ]; then
|
||||
rmdir "$home_claude" 2>/dev/null || true
|
||||
fi
|
||||
if [ ! -e "$home_claude" ]; then
|
||||
ln -sfn "$CLAUDE_CONFIG_DIR" "$home_claude"
|
||||
echo "Claude: ${home_claude} -> ${CLAUDE_CONFIG_DIR} (symlink created)"
|
||||
fi
|
||||
}
|
||||
|
|
@ -132,6 +132,13 @@ export CLAUDE_TIMEOUT="${CLAUDE_TIMEOUT:-7200}"
|
|||
unset GITHUB_TOKEN 2>/dev/null || true
|
||||
unset CLAWHUB_TOKEN 2>/dev/null || true
|
||||
|
||||
# Shared Claude config directory for cross-container OAuth lock coherence (#641).
|
||||
# All containers and the host resolve to the same CLAUDE_CONFIG_DIR on a shared
|
||||
# bind-mounted filesystem, so proper-lockfile's atomic mkdir works across them.
|
||||
: "${CLAUDE_SHARED_DIR:=/var/lib/disinto/claude-shared}"
|
||||
: "${CLAUDE_CONFIG_DIR:=${CLAUDE_SHARED_DIR}/config}"
|
||||
export CLAUDE_SHARED_DIR CLAUDE_CONFIG_DIR
|
||||
|
||||
# Disable Claude Code auto-updater, telemetry, error reporting in factory sessions.
|
||||
# Factory processes must never phone home or auto-update mid-session (#725).
|
||||
export CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1
|
||||
|
|
|
|||
|
|
@ -28,7 +28,8 @@ cleanup() {
|
|||
# Kill any leftover mock-forgejo.py processes by name
|
||||
pkill -f "mock-forgejo.py" 2>/dev/null || true
|
||||
rm -rf "$MOCK_BIN" /tmp/smoke-test-repo \
|
||||
"${FACTORY_ROOT}/projects/smoke-repo.toml"
|
||||
"${FACTORY_ROOT}/projects/smoke-repo.toml" \
|
||||
/tmp/smoke-claude-shared /tmp/smoke-home-claude
|
||||
# Restore .env only if we created the backup
|
||||
if [ -f "${FACTORY_ROOT}/.env.smoke-backup" ]; then
|
||||
mv "${FACTORY_ROOT}/.env.smoke-backup" "${FACTORY_ROOT}/.env"
|
||||
|
|
@ -286,6 +287,96 @@ else
|
|||
fi
|
||||
fi
|
||||
|
||||
# ── 7. Verify CLAUDE_CONFIG_DIR setup ─────────────────────────────────────
|
||||
echo "=== 7/7 Verifying CLAUDE_CONFIG_DIR setup ==="
|
||||
|
||||
# .env should contain CLAUDE_SHARED_DIR and CLAUDE_CONFIG_DIR
|
||||
if grep -q '^CLAUDE_SHARED_DIR=' "$env_file"; then
|
||||
pass ".env contains CLAUDE_SHARED_DIR"
|
||||
else
|
||||
fail ".env missing CLAUDE_SHARED_DIR"
|
||||
fi
|
||||
if grep -q '^CLAUDE_CONFIG_DIR=' "$env_file"; then
|
||||
pass ".env contains CLAUDE_CONFIG_DIR"
|
||||
else
|
||||
fail ".env missing CLAUDE_CONFIG_DIR"
|
||||
fi
|
||||
|
||||
# Test migration path with a temporary HOME
|
||||
echo "--- Testing claude config migration ---"
|
||||
ORIG_HOME="$HOME"
|
||||
ORIG_CLAUDE_SHARED_DIR="${CLAUDE_SHARED_DIR:-}"
|
||||
ORIG_CLAUDE_CONFIG_DIR="${CLAUDE_CONFIG_DIR:-}"
|
||||
|
||||
export HOME="/tmp/smoke-home-claude"
|
||||
export CLAUDE_SHARED_DIR="/tmp/smoke-claude-shared"
|
||||
export CLAUDE_CONFIG_DIR="${CLAUDE_SHARED_DIR}/config"
|
||||
mkdir -p "$HOME"
|
||||
|
||||
# Source claude-config.sh for setup_claude_config_dir
|
||||
source "${FACTORY_ROOT}/lib/claude-config.sh"
|
||||
|
||||
# Sub-test 1: fresh install (no ~/.claude, no config dir)
|
||||
rm -rf "$HOME/.claude" "$CLAUDE_SHARED_DIR"
|
||||
setup_claude_config_dir "true"
|
||||
if [ -d "$CLAUDE_CONFIG_DIR" ]; then
|
||||
pass "Fresh install: CLAUDE_CONFIG_DIR created"
|
||||
else
|
||||
fail "Fresh install: CLAUDE_CONFIG_DIR not created"
|
||||
fi
|
||||
if [ -L "$HOME/.claude" ]; then
|
||||
pass "Fresh install: ~/.claude symlink created"
|
||||
else
|
||||
fail "Fresh install: ~/.claude symlink not created"
|
||||
fi
|
||||
|
||||
# Sub-test 2: migration (pre-existing ~/.claude with content)
|
||||
rm -rf "$HOME/.claude" "$CLAUDE_SHARED_DIR"
|
||||
mkdir -p "$HOME/.claude"
|
||||
echo "test-token" > "$HOME/.claude/credentials.json"
|
||||
setup_claude_config_dir "true"
|
||||
if [ -f "$CLAUDE_CONFIG_DIR/credentials.json" ]; then
|
||||
pass "Migration: credentials.json moved to CLAUDE_CONFIG_DIR"
|
||||
else
|
||||
fail "Migration: credentials.json not found in CLAUDE_CONFIG_DIR"
|
||||
fi
|
||||
if [ -L "$HOME/.claude" ]; then
|
||||
link_target=$(readlink -f "$HOME/.claude")
|
||||
config_real=$(readlink -f "$CLAUDE_CONFIG_DIR")
|
||||
if [ "$link_target" = "$config_real" ]; then
|
||||
pass "Migration: ~/.claude is symlink to CLAUDE_CONFIG_DIR"
|
||||
else
|
||||
fail "Migration: ~/.claude symlink points to wrong target"
|
||||
fi
|
||||
else
|
||||
fail "Migration: ~/.claude is not a symlink"
|
||||
fi
|
||||
|
||||
# Sub-test 3: idempotency (re-run after migration)
|
||||
setup_claude_config_dir "true"
|
||||
if [ -L "$HOME/.claude" ] && [ -f "$CLAUDE_CONFIG_DIR/credentials.json" ]; then
|
||||
pass "Idempotency: re-run is a no-op"
|
||||
else
|
||||
fail "Idempotency: re-run broke the layout"
|
||||
fi
|
||||
|
||||
# Sub-test 4: both non-empty — must abort
|
||||
rm -rf "$HOME/.claude" "$CLAUDE_SHARED_DIR"
|
||||
mkdir -p "$HOME/.claude" "$CLAUDE_CONFIG_DIR"
|
||||
echo "home-data" > "$HOME/.claude/home.txt"
|
||||
echo "config-data" > "$CLAUDE_CONFIG_DIR/config.txt"
|
||||
if setup_claude_config_dir "true" 2>/dev/null; then
|
||||
fail "Both non-empty: should have aborted but didn't"
|
||||
else
|
||||
pass "Both non-empty: correctly aborted"
|
||||
fi
|
||||
|
||||
# Restore
|
||||
export HOME="$ORIG_HOME"
|
||||
export CLAUDE_SHARED_DIR="$ORIG_CLAUDE_SHARED_DIR"
|
||||
export CLAUDE_CONFIG_DIR="$ORIG_CLAUDE_CONFIG_DIR"
|
||||
rm -rf /tmp/smoke-claude-shared /tmp/smoke-home-claude
|
||||
|
||||
# ── Summary ──────────────────────────────────────────────────────────────────
|
||||
echo ""
|
||||
if [ "$FAILED" -ne 0 ]; then
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue