Compare commits
4
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
154dc18eb9 | ||
|
|
9fb0e8bcae | ||
|
|
dd228c1ff9 | ||
|
|
6df2acfe24 |
@@ -45,27 +45,13 @@ Already have a clone? Just run it directly:
|
||||
kubectx/kubens, stern, yq, OpenTofu, terragrunt, task (go-task, the
|
||||
Taskfile runner), the 1Password CLI `op` for secrets storage, the
|
||||
opencode coding agent, zarf (DevSecOps for Offline and Airgapped
|
||||
Kubernetes), packer (HashiCorp's infrastructure build tool), and the
|
||||
coder CLI for Coder workspaces.
|
||||
Kubernetes), and packer (HashiCorp's infrastructure build tool).
|
||||
kind is installed only when full (non-rootless) Docker is available —
|
||||
it doesn't work against rootless Docker, so it is skipped on Kasm.
|
||||
No sudo, nothing outside `$HOME`, so tools persist across Kasm
|
||||
redeploys with no reinstall. Idempotent: each tool records its version
|
||||
under `~/.local/bin/.versions` and is skipped when the pinned version
|
||||
is already present. `zsh/zshrc` puts `~/.local/bin` on `PATH`.
|
||||
- `secrets/` — reads your 1Password vault once and writes every field to
|
||||
`~/.config/dotfiles/secrets.env` (0600), which the managed `~/.zshrc`
|
||||
sources, so opening a shell never calls `op`. One vault per user and one
|
||||
item in it, both named after your login by default; each field's label is
|
||||
the variable name (`GITEA_KEY`, `LITELLM_KEY`, `PROXMOX_TOKEN_SECRET`, …).
|
||||
The first run prompts for the service-account token (and vault/item), and
|
||||
keeps them in `~/.config/dotfiles/op.env` (0600, not loaded into shells).
|
||||
Runs after `bin/` (needs `op` and `jq`); bootstrap loads the result so later
|
||||
modules get the keys without prompting. Day to day: `opr NAME` reads a
|
||||
field, `opc NAME [VALUE]` creates or updates one (prompts, hidden, without
|
||||
VALUE), `opd NAME` deletes one; both of those refresh the file and this
|
||||
shell. `opl [VAULT [ITEM]]` shows every field with its current value, and
|
||||
`ops` refreshes after a change made elsewhere. No sudo.
|
||||
|
||||
opencode is configured by `mcp-config/`: it points at the covert-gpt
|
||||
LiteLLM gateway (`https://llm.covert-gpt.com/v1`) for models. MCP tools do
|
||||
@@ -100,8 +86,7 @@ rc sources last (so it can override anything) and bootstrap never touches:
|
||||
cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
|
||||
```
|
||||
|
||||
Variables modules look for. With `secrets/` set up, put these in your vault
|
||||
item instead of `~/.zshrc.local`:
|
||||
Variables modules look for:
|
||||
|
||||
- `GITEA_KEY` — personal access token for `git.nic-oconnor.com`. Used for
|
||||
API/CLI work, and by `bootstrap.sh`/`mcp-config/` to read this repo when it
|
||||
@@ -112,9 +97,8 @@ item instead of `~/.zshrc.local`:
|
||||
don't set this by hand: run `mcp-config/install.sh` once and its shell
|
||||
hook exports it from `~/.config/mcp-sync/key`. Set it in `~/.zshrc.local`
|
||||
only on a box without mcp-sync.
|
||||
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token. Not a shell
|
||||
variable any more: `secrets/install.sh` asks for it once and stores it in
|
||||
`~/.config/dotfiles/op.env`. Pass it in the env only for an unattended run.
|
||||
- `OP_SERVICE_ACCOUNT_TOKEN` — 1Password service-account token for
|
||||
non-interactive `op read` / `op run`.
|
||||
- `GITEA_KEY`, `PROXMOX_TOKEN_ID`, `PROXMOX_TOKEN_SECRET` — **your own**
|
||||
credentials for the client-side MCP servers in `mcp-config/`. opencode reads
|
||||
them from your environment (`{env:VAR}`) and passes them to the MCP server it
|
||||
|
||||
@@ -204,17 +204,6 @@ install_terragrunt() {
|
||||
mark_version terragrunt "$ver"
|
||||
}
|
||||
|
||||
# coder — CLI for Coder workspaces (coder login, coder ssh, coder config-ssh).
|
||||
# Release tag is vX but asset drops the v; binary is at the archive root.
|
||||
install_coder() {
|
||||
ver=2.36.5
|
||||
have_version coder "$ver" && { fsay "coder $ver already installed."; return 0; }
|
||||
install_tarball_bin \
|
||||
"https://github.com/coder/coder/releases/download/v$ver/coder_${ver}_linux_$DEB.tar.gz" \
|
||||
coder coder
|
||||
mark_version coder "$ver"
|
||||
}
|
||||
|
||||
# try <install_fn> — run one tool's installer; on failure, warn and keep going.
|
||||
#
|
||||
# WHY: these installs used to be bare calls, so under `set -eu` the FIRST
|
||||
@@ -253,7 +242,6 @@ try install_1p
|
||||
try install_opencode
|
||||
try install_zarf
|
||||
try install_packer
|
||||
try install_coder
|
||||
|
||||
if [ -n "$FAILED" ]; then
|
||||
fsay ""
|
||||
|
||||
@@ -82,15 +82,6 @@ run_module git
|
||||
# Runs before bin/ so the kind install gate can see the rootful daemon.
|
||||
run_module docker
|
||||
run_module bin
|
||||
# Needs op and jq from bin/. Runs before everything that consumes a secret, and
|
||||
# its output is loaded into THIS process so later modules see the vault's
|
||||
# values: mcp-config/ takes LITELLM_KEY from the env instead of prompting for
|
||||
# it, and GITEA_KEY is there for anything that reads the repo over the API.
|
||||
run_module secrets
|
||||
if [ -f "$HOME/.config/dotfiles/secrets.env" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$HOME/.config/dotfiles/secrets.env"
|
||||
fi
|
||||
# Provides npx, which opencode needs to spawn the npm-based MCP servers in
|
||||
# mcp-config/. Without it every MCP server silently fails to start.
|
||||
run_module node
|
||||
|
||||
+1
-22
@@ -30,25 +30,4 @@ say "fetch.prune = true (stale remote-tracking refs cleaned on every fetch)"
|
||||
git config --global credential.helper store
|
||||
say "credential.helper = store (~/.git-credentials, created on first auth)"
|
||||
|
||||
# Our own Gitea authenticates from $GITEA_KEY (exported by secrets/ from the
|
||||
# vault), not from ~/.git-credentials.
|
||||
#
|
||||
# WHY: git never reads GITEA_KEY by itself. With only `store`, the token was
|
||||
# whatever got typed at the first prompt, copied into ~/.git-credentials, and
|
||||
# it silently went stale when the vault's key was rotated — or, on a box that
|
||||
# never pushed, there was no credential at all and the push just failed. Now
|
||||
# `opc GITEA_KEY` is the only step when the token changes.
|
||||
#
|
||||
# The empty helper first clears the inherited `store` for this host, so a
|
||||
# rejected token is never cached and a stale cached one can never win. Gitea
|
||||
# accepts the token as the password; the username is informational (GITEA_USER
|
||||
# overrides it). With GITEA_KEY unset the helper prints nothing and git falls
|
||||
# back to prompting, as before.
|
||||
GITEA_URL="https://git.nic-oconnor.com"
|
||||
git config --global --unset-all "credential.$GITEA_URL.helper" 2>/dev/null || true
|
||||
git config --global --add "credential.$GITEA_URL.helper" ''
|
||||
git config --global --add "credential.$GITEA_URL.helper" \
|
||||
'!f() { test "$1" = get && test -n "${GITEA_KEY:-}" && printf "username=%s\npassword=%s\n" "${GITEA_USER:-$(id -un)}" "$GITEA_KEY"; }; f'
|
||||
say "credential helper for $GITEA_URL = \$GITEA_KEY (from secrets.env)"
|
||||
|
||||
say "Done. $GITEA_URL uses \$GITEA_KEY; other HTTPS hosts prompt once and cache in ~/.git-credentials."
|
||||
say "Done. On the next HTTPS push/pull to a new host, git prompts once and caches the credential in ~/.git-credentials."
|
||||
|
||||
Executable → Regular
+2
-14
@@ -120,23 +120,11 @@ say "Key stored at $KEYFILE (0600)."
|
||||
# bootstrap run — and then models and skills silently vanish, because sync.sh
|
||||
# never runs and OPENCODE_CONFIG is never exported. ~/.zshrc.local is the
|
||||
# unmanaged file the managed zshrc sources last, so the hook persists.
|
||||
#
|
||||
# Do NOT pick the file from $SHELL alone. On a fresh box bootstrap runs this
|
||||
# from the user's ORIGINAL shell: zsh/ has just switched the login shell to zsh,
|
||||
# but $SHELL in this process still says bash. The hook then went into ~/.bashrc,
|
||||
# below zsh/'s auto-exec hook, so every new terminal exec'd into zsh before
|
||||
# reaching it, zsh never sourced sync.sh, and opencode came up unconfigured.
|
||||
# The dotfiles-managed ~/.zshrc (or zsh as the account's login shell) wins.
|
||||
_login_shell="$(getent passwd "$(id -un)" 2>/dev/null | cut -d: -f7)"
|
||||
if grep -qs 'Managed by dotfiles/zsh/install.sh' "$HOME/.zshrc"; then
|
||||
RC="$HOME/.zshrc.local"
|
||||
else
|
||||
case "${_login_shell:-${SHELL:-}}" in
|
||||
case "${SHELL:-}" in
|
||||
*zsh) RC="$HOME/.zshrc.local" ;;
|
||||
*bash) RC="$HOME/.bashrc" ;;
|
||||
*) [ -f "$HOME/.zshrc" ] && RC="$HOME/.zshrc.local" || RC="$HOME/.bashrc" ;;
|
||||
esac
|
||||
fi
|
||||
esac
|
||||
touch "$RC"
|
||||
# It sits alongside the user's secrets; keep it non-readable by others.
|
||||
[ "$RC" = "$HOME/.zshrc.local" ] && chmod 0600 "$RC" 2>/dev/null || true
|
||||
|
||||
+6
-72
@@ -315,50 +315,21 @@ OUTPUT_RESERVE_CAP = int(os.environ.get("MCP_SYNC_PY_OUTPUT_CAP") or 32768)
|
||||
CONTEXT_SAFETY_MARGIN = 1024
|
||||
|
||||
|
||||
# NEVER trust /model/info's number TYPES. The litellm-helm chart renders the
|
||||
# proxy config with Go's toYaml, which writes 0.000005 as `5e-06`; LiteLLM
|
||||
# parses it with PyYAML (YAML 1.1), which only reads an exponent float if it has
|
||||
# a dot. So `5e-06` arrives as the STRING "5e-06" while `2.5e-05` is a float.
|
||||
# `"5e-06" * 1_000_000` is not an error in Python — it is the string repeated a
|
||||
# million times. That wrote a 20 MB opencode.gateway.json with a string where
|
||||
# opencode requires a number, opencode refused to start, and every new shell
|
||||
# redeployed it. Coerce every number through here; anything that is not a
|
||||
# finite number is dropped, never passed through.
|
||||
def num(v):
|
||||
if isinstance(v, bool):
|
||||
return None
|
||||
if isinstance(v, (int, float)):
|
||||
f = float(v)
|
||||
elif isinstance(v, str) and len(v) <= 64:
|
||||
try:
|
||||
f = float(v.strip())
|
||||
except ValueError:
|
||||
return None
|
||||
else:
|
||||
return None
|
||||
if f != f or f in (float("inf"), float("-inf")):
|
||||
return None
|
||||
return int(f) if isinstance(v, int) or f.is_integer() else f
|
||||
|
||||
|
||||
def spec_for(mi):
|
||||
spec = {}
|
||||
limit = {}
|
||||
max_in, max_out = num(mi.get("max_input_tokens")), num(mi.get("max_output_tokens"))
|
||||
if max_in and max_in > 0:
|
||||
limit["context"] = int(max(1, max_in - CONTEXT_SAFETY_MARGIN))
|
||||
if max_out and max_out > 0:
|
||||
limit["output"] = int(min(max_out, OUTPUT_RESERVE_CAP))
|
||||
if mi.get("max_input_tokens"):
|
||||
limit["context"] = max(1, mi["max_input_tokens"] - CONTEXT_SAFETY_MARGIN)
|
||||
if mi.get("max_output_tokens"):
|
||||
limit["output"] = min(mi["max_output_tokens"], OUTPUT_RESERVE_CAP)
|
||||
if limit:
|
||||
spec["limit"] = limit
|
||||
# LiteLLM prices PER TOKEN; opencode (like models.dev) uses $ per 1M tokens.
|
||||
cost = {}
|
||||
for src, dst in (("input_cost_per_token", "input"),
|
||||
("output_cost_per_token", "output")):
|
||||
per_token = num(mi.get(src))
|
||||
if per_token is not None and per_token >= 0:
|
||||
# round() so 5e-06 * 1e6 is 5.0, not 4.999999999999999.
|
||||
cost[dst] = round(per_token * 1_000_000, 6)
|
||||
if mi.get(src) is not None:
|
||||
cost[dst] = mi[src] * 1_000_000
|
||||
if cost:
|
||||
spec["cost"] = cost
|
||||
for src, dst in (("supports_function_calling", "tool_call"),
|
||||
@@ -407,47 +378,10 @@ if not models:
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Validate EVERY spec before it is written, whatever its source. The carry-over
|
||||
# path above re-reads the deployed file, so a bad value that was ever written
|
||||
# (e.g. the 20 MB repeated-string cost) would otherwise be preserved forever;
|
||||
# this makes the next sync heal it instead. opencode rejects the WHOLE config
|
||||
# over one wrongly typed field, so dropping a bad field is always the better
|
||||
# failure.
|
||||
def clean_spec(spec):
|
||||
if not isinstance(spec, dict):
|
||||
return {}
|
||||
out = {}
|
||||
for group, keys, cast in (("limit", ("context", "output"), int),
|
||||
("cost", ("input", "output", "cache_read", "cache_write"), float)):
|
||||
vals = {}
|
||||
for k in keys:
|
||||
v = num((spec.get(group) or {}).get(k)) if isinstance(spec.get(group), dict) else None
|
||||
if v is not None and v >= 0:
|
||||
vals[k] = cast(v)
|
||||
if vals:
|
||||
out[group] = vals
|
||||
for k, v in spec.items():
|
||||
if k in ("limit", "cost"):
|
||||
continue
|
||||
if isinstance(v, (bool, int, float)) or (isinstance(v, str) and len(v) <= 256):
|
||||
out[k] = v
|
||||
elif isinstance(v, (dict, list)) and len(json.dumps(v)) <= 4096:
|
||||
out[k] = v
|
||||
return out
|
||||
|
||||
models = {k: clean_spec(v) for k, v in (models or {}).items()
|
||||
if isinstance(k, str) and len(k) <= 256}
|
||||
|
||||
prov = cfg.get("provider", {}).get(provider)
|
||||
if prov is not None and models:
|
||||
prov["models"] = models
|
||||
|
||||
# Last line of defence: the shared config is a few KB. Anything wildly larger is
|
||||
# a bug upstream of here, and deploying it is what breaks opencode — keep the
|
||||
# last-good file (or none) instead.
|
||||
if len(json.dumps(cfg)) > 1_000_000:
|
||||
raise SystemExit(0)
|
||||
|
||||
fd, tmp = tempfile.mkstemp(dir=os.path.dirname(dest))
|
||||
with os.fdopen(fd, "w") as f:
|
||||
json.dump(cfg, f, indent=2)
|
||||
|
||||
@@ -1,155 +0,0 @@
|
||||
#!/usr/bin/env sh
|
||||
# secrets/install.sh — write this user's 1Password vault item to a local,
|
||||
# 0600 env file that the managed ~/.zshrc sources.
|
||||
#
|
||||
# WHY: secrets used to be pulled live in ~/.zshrc.local, one `op read` per
|
||||
# field on EVERY new shell. That made each terminal wait on 1Password, broke
|
||||
# every shell when the network or the service account was down, and meant each
|
||||
# box carried its own hand-written copy of the loop. Now the vault is read once
|
||||
# here (at bootstrap, or on demand with `ops`), and shells read a local file.
|
||||
#
|
||||
# Layout: one vault per user (default: the login name), holding one item
|
||||
# (default: also the login name) whose field LABELS are the variable names —
|
||||
# GITEA_KEY, LITELLM_KEY, PROXMOX_TOKEN_SECRET, ... Every such field is
|
||||
# exported. Add a secret by adding a field (`opc NAME`), not by editing
|
||||
# this repo.
|
||||
#
|
||||
# Files, all under ~/.config/dotfiles (0700), none in the repo:
|
||||
# op.env OP_SERVICE_ACCOUNT_TOKEN, OP_VAULT, OP_ITEM (0600). Kept so a
|
||||
# refresh needs no prompt. Deliberately NOT sourced by shells:
|
||||
# the token stays out of every process's environment, and only
|
||||
# the op* helpers in zsh/zshrc load it, in a subshell.
|
||||
# secrets.env the exported fields (0600), replaced atomically, so a failed
|
||||
# fetch leaves the previous copy in place.
|
||||
#
|
||||
# Prompts for the token on the controlling TTY the first time (bootstrap is
|
||||
# often piped into sh, so stdin is not the terminal). Without a TTY, pass
|
||||
# OP_SERVICE_ACCOUNT_TOKEN (and optionally OP_VAULT / OP_ITEM) in the env.
|
||||
# Needs op and jq, which bin/ installs. No sudo.
|
||||
set -eu
|
||||
|
||||
say() { printf '%s\n' "$*" >&2; }
|
||||
|
||||
CONF_DIR="${DOTFILES_CONFIG_DIR:-$HOME/.config/dotfiles}"
|
||||
OP_ENV="$CONF_DIR/op.env"
|
||||
SECRETS_ENV="$CONF_DIR/secrets.env"
|
||||
MCP_SYNC_KEYFILE="$HOME/.config/mcp-sync/key"
|
||||
|
||||
umask 077
|
||||
mkdir -p "$CONF_DIR"
|
||||
chmod 0700 "$CONF_DIR"
|
||||
|
||||
PATH="$HOME/.local/bin:$PATH"
|
||||
for _tool in op jq; do
|
||||
if ! "$_tool" --version >/dev/null 2>&1; then
|
||||
say "secrets: $_tool is not installed or does not run; run bin/install.sh first."
|
||||
exit 1
|
||||
fi
|
||||
done
|
||||
|
||||
# --- 1. resolve the service account, vault and item -------------------------
|
||||
# Precedence: the environment, then op.env, then a prompt (token) / defaults.
|
||||
_env_token="${OP_SERVICE_ACCOUNT_TOKEN:-}"
|
||||
_env_vault="${OP_VAULT:-}"
|
||||
_env_item="${OP_ITEM:-}"
|
||||
if [ -f "$OP_ENV" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$OP_ENV"
|
||||
fi
|
||||
OP_SERVICE_ACCOUNT_TOKEN="${_env_token:-${OP_SERVICE_ACCOUNT_TOKEN:-}}"
|
||||
OP_VAULT="${_env_vault:-${OP_VAULT:-}}"
|
||||
OP_ITEM="${_env_item:-${OP_ITEM:-}}"
|
||||
_user="$(id -un)"
|
||||
|
||||
# ask <prompt> <default> — read one line from the TTY; echoes the default on empty.
|
||||
ask() {
|
||||
printf '%s [%s]: ' "$1" "$2" >&2
|
||||
IFS= read -r _ans < /dev/tty || _ans=""
|
||||
printf '%s' "${_ans:-$2}"
|
||||
}
|
||||
|
||||
if [ -z "$OP_SERVICE_ACCOUNT_TOKEN" ]; then
|
||||
if [ -f "$SECRETS_ENV" ]; then
|
||||
say "secrets: no service-account token; keeping existing $SECRETS_ENV."
|
||||
exit 0
|
||||
fi
|
||||
# Probe in a subshell: in POSIX sh a failed redirect on a special builtin
|
||||
# like `:` exits the whole script, which would skip the message below.
|
||||
if ! ( : < /dev/tty ) 2>/dev/null; then
|
||||
say "secrets: no OP_SERVICE_ACCOUNT_TOKEN and no TTY to prompt."
|
||||
say " re-run as: OP_SERVICE_ACCOUNT_TOKEN=ops_... $0"
|
||||
exit 1
|
||||
fi
|
||||
printf '1Password service-account token (ops_..., input hidden): ' >&2
|
||||
stty -echo < /dev/tty 2>/dev/null || true
|
||||
IFS= read -r OP_SERVICE_ACCOUNT_TOKEN < /dev/tty || true
|
||||
stty echo < /dev/tty 2>/dev/null || true
|
||||
printf '\n' >&2
|
||||
OP_SERVICE_ACCOUNT_TOKEN="$(printf '%s' "$OP_SERVICE_ACCOUNT_TOKEN" | tr -d ' \t\r\n')"
|
||||
[ -n "$OP_SERVICE_ACCOUNT_TOKEN" ] || { say "secrets: no token entered."; exit 1; }
|
||||
[ -n "$OP_VAULT" ] || OP_VAULT="$(ask 'Vault' "$_user")"
|
||||
[ -n "$OP_ITEM" ] || OP_ITEM="$(ask 'Item' "$_user")"
|
||||
fi
|
||||
OP_VAULT="${OP_VAULT:-$_user}"
|
||||
OP_ITEM="${OP_ITEM:-$_user}"
|
||||
export OP_SERVICE_ACCOUNT_TOKEN
|
||||
|
||||
# --- 2. fetch the item once and render it -----------------------------------
|
||||
# One `op item get` instead of one `op read` per field. Labels that are not
|
||||
# valid shell identifiers (and the item's built-in notes field) are skipped
|
||||
# rather than eval'd; values go through jq's @sh, so quotes, $ and newlines in
|
||||
# a secret cannot break or inject into the file.
|
||||
if ! _json="$(op item get "$OP_ITEM" --vault "$OP_VAULT" --reveal --format json)"; then
|
||||
say "secrets: could not read item '$OP_ITEM' in vault '$OP_VAULT'."
|
||||
[ -f "$SECRETS_ENV" ] && say "secrets: keeping the previous $SECRETS_ENV."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
_skipped="$(printf '%s' "$_json" | jq -r '
|
||||
[.fields[] | select(.purpose != "NOTES")
|
||||
| select((.label // "") | test("^[A-Za-z_][A-Za-z0-9_]*$") | not) | .label]
|
||||
| join(", ")')"
|
||||
[ -z "$_skipped" ] || say "secrets: skipped fields whose labels are not variable names: $_skipped"
|
||||
|
||||
_tmp="$(mktemp "$CONF_DIR/secrets.env.XXXXXX")"
|
||||
trap 'rm -f "$_tmp"' EXIT
|
||||
{
|
||||
printf '# Generated by dotfiles secrets/install.sh from op://%s/%s — do not edit.\n' "$OP_VAULT" "$OP_ITEM"
|
||||
printf '# Refresh with: ops (change the vault with: opc NAME / opd NAME)\n'
|
||||
printf '%s' "$_json" | jq -r '
|
||||
.fields[]
|
||||
| select(.purpose != "NOTES")
|
||||
| select((.label // "") | test("^[A-Za-z_][A-Za-z0-9_]*$"))
|
||||
| select((.value // "") != "")
|
||||
| "export \(.label)=\(.value | @sh)"'
|
||||
} > "$_tmp"
|
||||
chmod 0600 "$_tmp"
|
||||
_count="$(grep -c '^export ' "$_tmp" || true)"
|
||||
if [ "$_count" -eq 0 ]; then
|
||||
say "secrets: item '$OP_ITEM' has no exportable fields; keeping the previous file."
|
||||
exit 1
|
||||
fi
|
||||
mv -f "$_tmp" "$SECRETS_ENV"
|
||||
trap - EXIT
|
||||
say "secrets: wrote $_count variables to $SECRETS_ENV (0600)."
|
||||
|
||||
# Remember the account only after it has actually worked.
|
||||
{
|
||||
printf 'OP_SERVICE_ACCOUNT_TOKEN=%s\n' "$(printf '%s' "$OP_SERVICE_ACCOUNT_TOKEN" | jq -Rr @sh)"
|
||||
printf 'OP_VAULT=%s\n' "$(printf '%s' "$OP_VAULT" | jq -Rr @sh)"
|
||||
printf 'OP_ITEM=%s\n' "$(printf '%s' "$OP_ITEM" | jq -Rr @sh)"
|
||||
} > "$OP_ENV"
|
||||
chmod 0600 "$OP_ENV"
|
||||
|
||||
# --- 3. keep mcp-sync's copy of the gateway key in step ----------------------
|
||||
# mcp-config/sync.sh exports LITELLM_KEY from its own key file on every shell,
|
||||
# AFTER secrets.env is loaded, so a key rotated in the vault would otherwise be
|
||||
# silently overridden by the stale copy. Only update a file that already exists:
|
||||
# creating it is mcp-config/install.sh's job.
|
||||
_litellm="$(printf '%s' "$_json" | jq -r '.fields[] | select(.label == "LITELLM_KEY") | .value // empty' | head -n 1)"
|
||||
if [ -n "$_litellm" ] && [ -f "$MCP_SYNC_KEYFILE" ] \
|
||||
&& [ "$(tr -d ' \t\r\n' < "$MCP_SYNC_KEYFILE")" != "$_litellm" ]; then
|
||||
printf '%s' "$_litellm" > "$MCP_SYNC_KEYFILE"
|
||||
chmod 0600 "$MCP_SYNC_KEYFILE"
|
||||
say "secrets: LITELLM_KEY changed; updated $MCP_SYNC_KEYFILE (re-run hermes/install.sh for Hermes)."
|
||||
fi
|
||||
@@ -74,73 +74,6 @@ grmb() {
|
||||
return $rc
|
||||
}
|
||||
|
||||
# Secrets from this user's 1Password vault, written by dotfiles/secrets/install.sh.
|
||||
# Reading a local 0600 file keeps shell startup off the network: nothing here
|
||||
# calls op. opc/opd refresh the file themselves; after editing the vault
|
||||
# anywhere else (the 1Password app, another box), run `ops`.
|
||||
_DOTFILES_CONF="$HOME/.config/dotfiles"
|
||||
[ -f "$_DOTFILES_CONF/secrets.env" ] && source "$_DOTFILES_CONF/secrets.env"
|
||||
|
||||
# opr NAME read one field straight from the vault
|
||||
# opc NAME [VALUE] create or update a field (prompts, hidden, when VALUE is
|
||||
# omitted, which keeps the secret out of shell history),
|
||||
# then refresh and reload this shell
|
||||
# opd NAME delete a field, refresh, and unset it in this shell
|
||||
# opl [VAULT [ITEM]] show every field and its current value (default: your
|
||||
# vault and item from op.env)
|
||||
# ops re-fetch the vault into secrets.env and reload this shell
|
||||
#
|
||||
# The service-account token lives in op.env and is loaded only inside these
|
||||
# subshells, so it never lands in the environment of everything you run.
|
||||
_dotfiles_op() {
|
||||
[ -f "$_DOTFILES_CONF/op.env" ] || { print -u2 "no $_DOTFILES_CONF/op.env; run: ops"; return 1; }
|
||||
( set -a; source "$_DOTFILES_CONF/op.env"; set +a; "$@" )
|
||||
}
|
||||
_dotfiles_env_name() {
|
||||
[[ "$1" =~ '^[A-Za-z_][A-Za-z0-9_]*$' ]] && return 0
|
||||
print -u2 "$2: '$1' is not a valid variable name"; return 2
|
||||
}
|
||||
opr() {
|
||||
_dotfiles_env_name "$1" opr || return
|
||||
_dotfiles_op sh -c 'op read "op://$OP_VAULT/$OP_ITEM/$1"' opr "$1"
|
||||
}
|
||||
opc() {
|
||||
emulate -L zsh
|
||||
local name=$1 value
|
||||
_dotfiles_env_name "$name" opc || return
|
||||
if (( $# >= 2 )); then
|
||||
value=$2
|
||||
else
|
||||
read -rs "value?$name: " && print
|
||||
fi
|
||||
# [concealed] makes it a password field; an existing field of that name is updated.
|
||||
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[concealed]=$2" >/dev/null' \
|
||||
opc "$name" "$value" && ops
|
||||
}
|
||||
opd() {
|
||||
_dotfiles_env_name "$1" opd || return
|
||||
_dotfiles_op sh -c 'op item edit "$OP_ITEM" --vault "$OP_VAULT" "$1[delete]" >/dev/null' opd "$1" \
|
||||
&& unset "$1" && ops
|
||||
}
|
||||
opl() {
|
||||
_dotfiles_op sh -c '
|
||||
vault=${1:-$OP_VAULT} item=${2:-$OP_ITEM}
|
||||
printf "op://%s/%s\n" "$vault" "$item" >&2
|
||||
op item get "$item" --vault "$vault" --reveal --format json' opl "$@" \
|
||||
| jq -r '
|
||||
[.fields[] | select(.purpose != "NOTES")] as $f
|
||||
| ([$f[] | .label // "" | length] | max // 0) as $w
|
||||
| $f[]
|
||||
| ((.label // "") + " " * ($w - ((.label // "") | length))) as $name
|
||||
| ((.value // "") | split("\n")) as $lines
|
||||
| "\($name) \($lines[0] // "")",
|
||||
($lines[1:][] | (" " * ($w + 2)) + .)'
|
||||
}
|
||||
ops() {
|
||||
"${DOTFILES_DIR:-$HOME/.dotfiles}/secrets/install.sh" \
|
||||
&& source "$_DOTFILES_CONF/secrets.env"
|
||||
}
|
||||
|
||||
# Per-machine customizations (secrets, keys, local overrides) live in
|
||||
# ~/.zshrc.local, which bootstrap never touches. See zsh/zshrc.local.example
|
||||
# for the variables modules look for. Loaded last so it can override anything.
|
||||
|
||||
+6
-10
@@ -1,9 +1,7 @@
|
||||
# ~/.zshrc.local — per-machine customizations. Copy this file to
|
||||
# ~/.zshrc.local and fill in what this box needs. It is sourced at the end
|
||||
# of the managed ~/.zshrc, is never overwritten by bootstrap, and never
|
||||
# lives in the repo. Secrets normally come from your 1Password vault instead
|
||||
# (secrets/install.sh -> ~/.config/dotfiles/secrets.env); set one here only on
|
||||
# a box without 1Password, or to override the vault's value on this machine.
|
||||
# lives in the repo — this is the ONLY place to put secrets.
|
||||
#
|
||||
# cp zsh/zshrc.local.example ~/.zshrc.local && chmod 600 ~/.zshrc.local
|
||||
|
||||
@@ -32,11 +30,9 @@
|
||||
# Only the bootstrap-managed checkout (~/.dotfiles) is ever `git pull`-ed by the
|
||||
# hook, and only when clean — a tree you work in is read, never touched.
|
||||
|
||||
# --- 1Password -----------------------------------------------------------
|
||||
# Nothing to set here. Do NOT loop over `op read` in this file: that ran on
|
||||
# every shell. secrets/install.sh stores the service-account token in
|
||||
# ~/.config/dotfiles/op.env and writes the vault's fields to secrets.env, which
|
||||
# the managed ~/.zshrc loads. Use opr / opc / opd (see zsh/zshrc).
|
||||
# --- 1Password CLI -------------------------------------------------------
|
||||
# Service-account token for non-interactive `op read` / `op run`.
|
||||
# export OP_SERVICE_ACCOUNT_TOKEN=ops_...
|
||||
|
||||
# --- Proxmox -------------------------------------------------------------
|
||||
# One pair of credentials, several consumers: the proxmox MCP server
|
||||
@@ -44,8 +40,8 @@
|
||||
# rke2-devops-airgap, whose Taskfile composes PROXMOX_VE_API_TOKEN from them.
|
||||
# Unprefixed on purpose — these are Proxmox credentials, not MCP ones.
|
||||
#
|
||||
# secrets.env already exports them from your vault item, so these lines are
|
||||
# only needed without 1Password. Never committed.
|
||||
# The op-vault loop above already exports them from the `noconnor` item, so
|
||||
# these lines are only needed without 1Password. Never committed.
|
||||
# export PROXMOX_TOKEN_ID="root@pam!terragrunt-proxmox"
|
||||
# export PROXMOX_TOKEN_SECRET="00000000-0000-0000-0000-000000000000"
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user