workstation: managed Claude config self-deploys from the repo [ci skip]
Viktor's claudeMd edits must keep reaching every user now that emo is out of the shared tree. Two reconciler additions: - sync_managed_config: installs scripts/workstation/managed-settings.json to /etc/claude-code whenever the repo copy changes — editing the org claudeMd is now edit + commit, no manual install step - refresh_codex_mirror: regenerates each user's ~/.codex/AGENTS.md (static mirror of the claudeMd; header-guarded so user-customized files are never clobbered) Verified live: corrupted emo's mirror -> reconcile restored it; wizard's stale mirror refreshed; in-sync managed config no-ops. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
8cfd0e5e5c
commit
35c89fa90c
2 changed files with 40 additions and 1 deletions
|
|
@ -541,7 +541,7 @@ Separate from the in-cluster namespace-owner model above, the **devvm** (`10.0.1
|
|||
|
||||
**RBAC tiers:** `admin` (Viktor — cluster-admin, unlocked tree, secrets) · `power-user` (cluster-wide read-only, NO Secrets, via a dedicated `oidc-power-user-readonly` ClusterRole) · `namespace-owner` (admin in own namespace only). Each session acts as the user's **own** OIDC identity (kubelogin), never the admin's.
|
||||
|
||||
**Config inheritance (live):** wizard authors the base (his chezmoi-versioned `~/.claude`). Two native layers carry it to every user — the enforced org `claudeMd` in `/etc/claude-code/managed-settings.json` (top precedence, all sessions) and per-user `~/.claude/{skills,rules,…}` **symlinks** to the base (seeded via `/etc/skel`; edits propagate live). Secrets stay per-user at mode 600, never symlinked.
|
||||
**Config inheritance (live):** wizard authors the base (his chezmoi-versioned `~/.claude`). Two native layers carry it to every user — the enforced org `claudeMd` in `/etc/claude-code/managed-settings.json` (top precedence, all sessions) and per-user `~/.claude/{skills,rules,…}` **symlinks** to the base (seeded via `/etc/skel`; edits propagate live). Secrets stay per-user at mode 600, never symlinked. **The managed config self-deploys from the repo** (2026-06-10): the hourly reconcile's `sync_managed_config` installs `scripts/workstation/managed-settings.json` to `/etc/claude-code/` whenever the repo copy changes — so editing the claudeMd = edit + commit, no manual install — and `refresh_codex_mirror` regenerates each user's `~/.codex/AGENTS.md` (a static mirror of the claudeMd; only files carrying the mirror header are touched, user-customized ones are left alone). Repo-level guidance (`.claude/CLAUDE.md`, `AGENTS.md`, `CONTEXT.md` in the infra repo) reaches non-admins through their auto-freshened clones — commit + push and every user has it within the hour.
|
||||
|
||||
**Infra access:** non-admins get their own **writable, git-crypt-LOCKED** clone of the (public) infra repo at `~/code` — code/docs plaintext, secret files (`*.tfvars`, `secrets/**`) stay ciphertext. The provisioner clones anonymously from the public GitHub mirror; **contribute access is wired per-user on top** (see below). The apply boundary still holds (`scripts/tg apply` needs an admin Vault token + cluster RBAC), but **pushing `master` is NOT inert** — the Forgejo→Woodpecker webhook fires `.woodpecker/default.yml` (`event: push, branch: master`, `require_approval: forks` only), which terragrunt-applies changed stacks. `master` is **branch-protected on Forgejo** (force-push disabled for everyone — history is append-only; push + merge whitelists = `viktor` + explicitly granted users, deploy keys allowed). **Allow-then-audit (Viktor, 2026-06-10):** `ebarzin` (emo) is on the whitelist and pushes straight to `master` — no PR gate. The tracking burden moves to: (a) **commit messages that record what + why** (the agent instructions in AGENTS.md and the managed claudeMd require the body to paraphrase the user's request), (b) the **`notify-nonadmin-push` Slack audit step** in `.woodpecker/default.yml` — every master push by a non-admin author is posted to Slack (admin pushes are not), and (c) non-admins **never use `[ci skip]`** so every change fires the pipeline (and thus the audit feed). Users NOT on the whitelist fall back to `<user>/<topic>` branches + PRs. **Clones stay fresh automatically** (2026-06-10): the hourly `t3-provision-users` reconcile runs `refresh_locked_clone` (fetch all remotes + fast-forward `master`, ONLY when on master with a clean tree and an upstream — dirty trees and local commits are left alone with a WARN), and `start-claude.sh` does the same freshen at session launch (15s-capped so an offline remote never stalls the session).
|
||||
|
||||
|
|
|
|||
|
|
@ -64,6 +64,41 @@ refresh_locked_clone() {
|
|||
|| log "WARN: $user master not fast-forwardable (local commits?) — left as-is"
|
||||
}
|
||||
|
||||
# Machine-wide Claude managed config: the repo file (in the admin tree, like the
|
||||
# roster) is the authoring surface; deploying it here means a plain infra commit
|
||||
# propagates claudeMd/model edits to /etc — and thus every user's NEXT session —
|
||||
# within one reconcile cycle. No manual install step.
|
||||
sync_managed_config() {
|
||||
local src="$WORKSTATION_DIR/managed-settings.json" dst=/etc/claude-code/managed-settings.json
|
||||
[[ -r "$src" ]] || return 0
|
||||
python3 -c "import json,sys; json.load(open(sys.argv[1]))" "$src" 2>/dev/null \
|
||||
|| { log "WARN: $src is invalid JSON — managed-config sync skipped"; return 0; }
|
||||
cmp -s "$src" "$dst" 2>/dev/null && return 0
|
||||
if [[ "$DRY_RUN" == 1 ]]; then echo "[dry-run] managed-settings.json -> $dst"; return 0; fi
|
||||
install -D -m 0644 "$src" "$dst"
|
||||
log "deployed managed-settings.json -> /etc/claude-code (repo copy changed)"
|
||||
}
|
||||
|
||||
# ~/.codex/AGENTS.md is a STATIC mirror of the managed claudeMd (codex has no
|
||||
# machine-wide managed layer). Regenerate stale mirrors so codex sessions inherit
|
||||
# claudeMd edits the same way Claude sessions do. Never clobbers a user-customized
|
||||
# file: only touches files carrying the mirror header (or creates absent ones).
|
||||
refresh_codex_mirror() {
|
||||
local user="$1" home dst tmp
|
||||
home="$(getent passwd "$user" | cut -d: -f6)"
|
||||
dst="$home/.codex/AGENTS.md"
|
||||
[[ -n "$home" && -d "$home/.codex" ]] || return 0
|
||||
if [[ -f "$dst" ]] && ! head -1 "$dst" | grep -q '^# Codex global instructions (devvm)'; then return 0; fi
|
||||
tmp="$(mktemp)"
|
||||
{ printf '# Codex global instructions (devvm)\n\n_Mirrors the machine-wide Claude managed policy._\n\n---\n\n'
|
||||
python3 -c 'import json; print(json.load(open("/etc/claude-code/managed-settings.json"))["claudeMd"])'
|
||||
} > "$tmp" 2>/dev/null || { rm -f "$tmp"; return 0; }
|
||||
if cmp -s "$tmp" "$dst" 2>/dev/null; then rm -f "$tmp"; return 0; fi
|
||||
if [[ "$DRY_RUN" == 1 ]]; then echo "[dry-run] codex AGENTS.md mirror -> $user"; rm -f "$tmp"; return 0; fi
|
||||
install -o "$user" -g "$user" -m 0644 "$tmp" "$dst"; rm -f "$tmp"
|
||||
log "refreshed codex AGENTS.md mirror -> $user"
|
||||
}
|
||||
|
||||
# Per-user OIDC kubeconfig (kubelogin/PKCE — the `kubernetes` Authentik client is
|
||||
# public, no secret). Identical for all users: identity comes from each user's own
|
||||
# interactive OIDC login, which the apiserver maps (email claim) to their RBAC.
|
||||
|
|
@ -179,6 +214,9 @@ desired_file="$(mktemp)"
|
|||
python3 "$ENGINE" derive --roster "$ROSTER" --ports-json "$ports_file" > "$desired_file"
|
||||
jq -e . "$desired_file" >/dev/null || { echo "[t3-provision] derive produced invalid JSON" >&2; exit 1; }
|
||||
|
||||
# 3b) machine-wide Claude managed config (repo -> /etc; per-user codex mirrors in the loop below)
|
||||
sync_managed_config
|
||||
|
||||
# 4) per-account: create-if-absent + ADDITIVE tier groups (never strip) + locked clone
|
||||
while IFS=$'\t' read -r os_user tier shell groups_csv; do
|
||||
if ! id "$os_user" >/dev/null 2>&1; then
|
||||
|
|
@ -202,6 +240,7 @@ while IFS=$'\t' read -r os_user tier shell groups_csv; do
|
|||
install_user_kubeconfig "$os_user"
|
||||
install_user_claude_token "$os_user"
|
||||
fi
|
||||
refresh_codex_mirror "$os_user" # all tiers — mirror of the managed claudeMd
|
||||
done < <(jq -r '.accounts[] | [.os_user, .tier, .shell, (.groups|join(","))] | @tsv' "$desired_file")
|
||||
|
||||
# 5) per-user .env (sticky port) + enable t3-serve@
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue