#!/usr/bin/env bash # code-init — 一条命令让一台“什么都没登录”的 Linux 机器直接可用 codex 与 agy-pool。 # 机密只从 Vault 读取;token 只经环境变量 / 交互输入 / 0600 文件传递,从不出现在命令行参数和日志里。 # # VAULT_TOKEN=... bash code-init.sh # 安装 / 修复(幂等,可重复跑) # code-init check # 只做自检 # code-init codex-sync [pull|push|auto] # codex auth.json 与 Vault 双向“取较新” # # 可调环境变量: # CODEX_AUTH_KEY (默认 codex-auth:X5WT-AR5P8) # AG_POOL_REPO / AG_POOL_REF (默认 ChangfengHU/ag-account-pool @ 固定提交) # AG_POOL_SRC 本地已有的 ag-account-pool 源码目录(跳过 GitHub 下载) # AG_POOL_MACHINE_ID (默认 host-) # CODE_INIT_PERSIST_TOKEN=1 把 Vault token 存到 ~/.config/code-init/vault-token(0600) 并装 cron 定时同步 codex 登录 # CODE_INIT_SKIP_CODEX=1 / CODE_INIT_SKIP_AGY=1 set -euo pipefail umask 077 CODE_INIT_VERSION=1 CODEX_AUTH_KEY=${CODEX_AUTH_KEY:-codex-auth:X5WT-AR5P8} AG_POOL_REPO=${AG_POOL_REPO:-ChangfengHU/ag-account-pool} # 首选:188 现网 CLI 快照分支(含 --takeover、关闭 agy 自动更新);没推到 GitHub 时退回 main 固定提交(无 --takeover)。 AG_POOL_REF=${AG_POOL_REF:-code-init/host-188-cli} AG_POOL_FALLBACK_REF=${AG_POOL_FALLBACK_REF:-db011e713d0d375482e96a91c25fff08ecf55fad} AG_POOL_SYNC_KEY=${AG_POOL_SYNC_KEY:-ag-account-pool:sync:client} NODE_VERSION=26.13.0 AGY_VERSION=1.2.16 AGY_BUILD=5594158052802560 CI_HOME="$HOME/.local/share/code-init" BIN="$HOME/.local/bin" POOL_DATA="$HOME/.local/share/ag-account-pool" POOL_PORT=18743 say(){ printf '\033[1;36m[code-init]\033[0m %s\n' "$*"; } warn(){ printf '\033[1;33m[code-init] %s\033[0m\n' "$*" >&2; } die(){ printf '\033[1;31m[code-init] %s\033[0m\n' "$*" >&2; exit 1; } case "$(uname -s)/$(uname -m)" in Linux/x86_64) ARCH=x64; AGY_DIR=linux-x64; AGY_TGZ=cli_linux_x64.tar.gz NODE_SHA=d28a96af6c38596f6d9d97c0ee531c13d238d757742387d1928de822d959e997 AGY_SHA=a759ce7c7a235d9b6c281a25ead97cbbf2e92314a3ffd224e2f9144f3fae7a86 ;; Linux/aarch64|Linux/arm64) ARCH=arm64; AGY_DIR=linux-arm; AGY_TGZ=cli_linux_arm64.tar.gz NODE_SHA=406fe3fbde1c054adff8b297c16fdb5af6659db327c1ac8acd46b56ef1af9a92 AGY_SHA=d0c06173f4ab2d6da7c17ba8d52a688234f30a79a7f65e25ace15863a295695f ;; *) die "只支持 Linux x86_64 / aarch64(当前 $(uname -s)/$(uname -m))" ;; esac NODE="$CI_HOME/node/bin/node" export PATH="$BIN:$CI_HOME/node/bin:$PATH" need(){ command -v "$1" >/dev/null 2>&1 || die "缺少命令 $1(请先 apt/yum 安装)"; } sha_ok(){ [ -f "$1" ] && [ "$(sha256sum "$1" | cut -d' ' -f1)" = "$2" ]; } write_helper(){ mkdir -p "$CI_HOME" cat > "$CI_HOME/vault.mjs.next" <<'VAULT_MJS' // code-init vault helper. The Vault token is read ONLY from env CODE_INIT_VAULT_TOKEN // (or the 0600 file ~/.config/code-init/vault-token). Secret values are never printed // to the terminal: they go to 0600 files or to a child's stdin. import fs from 'node:fs/promises'; import os from 'node:os'; import path from 'node:path'; import {spawn} from 'node:child_process'; process.umask(0o077); const ENDPOINT=process.env.CODE_INIT_VAULT_URL||'https://fleet.vyibc.com/mcp/vault'; const TOKEN_FILE=path.join(os.homedir(),'.config/code-init/vault-token'); const die=m=>{console.error('code-init: '+m);process.exit(1);}; async function token(){ if(process.env.CODE_INIT_VAULT_TOKEN)return process.env.CODE_INIT_VAULT_TOKEN.trim(); try{return (await fs.readFile(TOKEN_FILE,'utf8')).trim();}catch{die('缺少 Vault token(环境变量 VAULT_TOKEN 或 '+TOKEN_FILE+')');} } let rpcId=0; async function call(name,args){ const r=await fetch(ENDPOINT,{method:'POST',headers:{authorization:'Bearer '+await token(),'content-type':'application/json',accept:'application/json, text/event-stream'}, body:JSON.stringify({jsonrpc:'2.0',id:++rpcId,method:'tools/call',params:{name:'vyibc-vault_'+name,arguments:args}}),signal:AbortSignal.timeout(30000)}); if(r.status===401||r.status===403)die('Vault 拒绝了 token(HTTP '+r.status+')'); if(!r.ok)die('Vault HTTP '+r.status); const d=await r.json();if(d.error)die('Vault 错误:'+(d.error.message||'unknown')); const text=d.result?.content?.[0]?.text||'';let o;try{o=JSON.parse(text);}catch{die('Vault 返回无法解析('+name+')');} if(d.result.isError||o.ok===false)return null; return o; } async function get(key,{optional=false}={}){const o=await call('get_config',{key});if(!o){if(optional)return null;die('Vault 中没有 '+key+',或 token 无权读取');}return o;} const ts=v=>{const t=Date.parse(v?.last_refresh||'');return Number.isFinite(t)?t:0;}; async function writeAtomic(file,data){await fs.mkdir(path.dirname(file),{recursive:true,mode:0o700});const tmp=file+'.code-init-'+process.pid;await fs.writeFile(tmp,data,{mode:0o600});await fs.chmod(tmp,0o600);await fs.rename(tmp,file);} const [cmd,...a]=process.argv.slice(2); if(cmd==='ping'){ const o=await get(a[0]);console.log('ok '+o.key); }else if(cmd==='codex-pull'){ // codex-pull : take Vault copy if newer const [key,file]=a;const o=await get(key);const remote=o.value; if(!remote?.tokens?.refresh_token)die(key+' 不是 codex auth.json 格式'); let local=null;try{local=JSON.parse(await fs.readFile(file,'utf8'));}catch{} if(local&&ts(local)>=ts(remote)){console.log('codex auth:本机已是最新('+(local.last_refresh||'?')+'),未覆盖');process.exit(0);} if(local){const bak=file+'.before-code-init';await fs.copyFile(file,bak).catch(()=>{});await fs.chmod(bak,0o600).catch(()=>{});} await writeAtomic(file,JSON.stringify(remote,null,2)+'\n'); console.log('codex auth:已从 Vault 写入 '+file+'(last_refresh '+remote.last_refresh+')'); }else if(cmd==='codex-push'){ // codex-push : write back if local is newer const [key,file]=a;let local;try{local=JSON.parse(await fs.readFile(file,'utf8'));}catch{die('读不到 '+file);} const o=await get(key); if(ts(local)<=ts(o.value)){console.log('codex auth:Vault 不比本机旧,无需写回');process.exit(0);} if(local.tokens?.account_id&&o.value?.tokens?.account_id&&local.tokens.account_id!==o.value.tokens.account_id)die('本机 codex 登录的是另一个账号,拒绝覆盖 '+key); const r=await call('set_config',{key,value:local,description:o.description||'Codex CLI auth.json'}); if(!r)die('写回 Vault 失败');console.log('codex auth:本机较新,已写回 Vault(last_refresh '+local.last_refresh+')'); }else if(cmd==='pipe-json'){ // pipe-json -- : value as one JSON line on child stdin const key=a[0],sep=a.indexOf('--');const o=await get(key); const child=spawn(a[sep+1],a.slice(sep+2),{stdio:['pipe','inherit','inherit']}); child.stdin.end(JSON.stringify(o.value)+'\n'); child.on('exit',c=>process.exit(c??1)); }else if(cmd==='github-tarball'){ // github-tarball const [repo,ref,out]=a; const o=await get(process.env.CODE_INIT_GITHUB_KEY||'code-init:github-read',{optional:true})||await get('service:github'); const gh=o.value?.token||o.value;if(typeof gh!=='string')die('GitHub token 格式不对'); const r=await fetch(`https://api.github.com/repos/${repo}/tarball/${encodeURIComponent(ref)}`,{headers:{authorization:'Bearer '+gh,'user-agent':'code-init',accept:'application/vnd.github+json'},signal:AbortSignal.timeout(120000)}); if(r.status===404){console.error('code-init: GitHub 上没有 '+repo+'@'+ref);process.exit(2);} if(!r.ok)die('下载 '+repo+'@'+ref+' 失败:HTTP '+r.status); await fs.writeFile(out,Buffer.from(await r.arrayBuffer()),{mode:0o600});console.log('已下载 '+repo+'@'+ref); }else if(cmd==='save-token'){ await writeAtomic(TOKEN_FILE,await token()+'\n');console.log('Vault token 已保存到 '+TOKEN_FILE+'(0600)'); }else die('用法:vault.mjs ping|codex-pull|codex-push|pipe-json|github-tarball|save-token'); VAULT_MJS mv "$CI_HOME/vault.mjs.next" "$CI_HOME/vault.mjs" } vault(){ CODE_INIT_VAULT_TOKEN="${VAULT_TOKEN:-}" "$NODE" "$CI_HOME/vault.mjs" "$@"; } get_token(){ if [ -n "${VAULT_TOKEN:-}" ]; then return; fi if [ -s "$HOME/.config/code-init/vault-token" ]; then return; fi # vault.mjs 自己读文件 { : /dev/null || die "需要 Vault token:用 VAULT_TOKEN=... 环境变量,或在交互终端运行" printf '[code-init] 输入 Vault token(不回显): ' >/dev/tty IFS= read -rs VAULT_TOKEN /dev/tty [ -n "$VAULT_TOKEN" ] || die "未输入 token" export VAULT_TOKEN } install_node(){ if [ -x "$NODE" ] && [ "$("$NODE" -v)" = "v$NODE_VERSION" ]; then say "Node $NODE_VERSION 已就绪(私有运行时,不动系统 Node)"; return; fi say "安装私有 Node $NODE_VERSION ($ARCH)…"; mkdir -p "$CI_HOME" local t; t=$(mktemp -d) curl -fsSL -o "$t/node.tar.xz" "https://nodejs.org/dist/v$NODE_VERSION/node-v$NODE_VERSION-linux-$ARCH.tar.xz" sha_ok "$t/node.tar.xz" "$NODE_SHA" || die "Node 下载校验失败" mkdir -p "$t/x" && tar -xJf "$t/node.tar.xz" -C "$t/x" --strip-components=1 rm -rf "$CI_HOME/node.next" && mv "$t/x" "$CI_HOME/node.next" rm -rf "$CI_HOME/node" && mv "$CI_HOME/node.next" "$CI_HOME/node"; rm -rf "$t" } install_codex(){ if command -v codex >/dev/null 2>&1 && codex --version >/dev/null 2>&1; then say "codex 已安装:$(codex --version 2>/dev/null)($(command -v codex))" else say "安装 codex CLI(npm @openai/codex,装在 $CI_HOME/npm)…" NPM_CONFIG_PREFIX="$CI_HOME/npm" "$CI_HOME/node/bin/npm" i -g @openai/codex --no-fund --no-audit --loglevel=error mkdir -p "$BIN" cat > "$BIN/codex.next" </dev/null | grep -q 'AG Account Pool managed'; then say "官方 agy 已由账号池托管(native/$(sed -n 's/.*"file":"\([^"]*\)".*/\1/p' "$POOL_DATA/native/entry.json"))"; return fi if sha_ok "$entry" "$AGY_SHA"; then say "官方 agy $AGY_VERSION 已就绪"; return; fi if [ -e "$entry" ]; then mv "$entry" "$entry.before-code-init.$(date +%s)"; warn "已把原有 $entry 改名备份"; fi say "安装官方 agy $AGY_VERSION($ARCH,SHA256 固定)…" local t; t=$(mktemp -d) curl -fsSL -o "$t/agy.tgz" "https://storage.googleapis.com/antigravity-public/antigravity-cli/$AGY_VERSION-$AGY_BUILD/$AGY_DIR/$AGY_TGZ" tar -xzf "$t/agy.tgz" -C "$t" antigravity sha_ok "$t/antigravity" "$AGY_SHA" || die "agy 二进制校验失败" mkdir -p "$BIN" && install -m 700 "$t/antigravity" "$entry"; rm -rf "$t" } has_systemd_user(){ systemctl --user show-environment >/dev/null 2>&1; } write_systemctl_shim(){ # 无 systemd 用户总线(容器 / tini / WSL)时:只接管 ag-account-pool.service,其余调用透传真 systemctl。 mkdir -p "$CI_HOME/shim" cat > "$CI_HOME/shim/systemctl.next" <>"$POOL_DATA/service.log" 2>&1 /dev/null); [ -n "\$pid" ] && kill "\$pid" 2>/dev/null; exit 0 ;; is-active|status) curl -fsS --max-time 2 http://127.0.0.1:$POOL_PORT/health >/dev/null 2>&1 && { echo active; exit 0; } || { echo inactive; exit 3; } ;; esac fi [ -n "\$real" ] && exec "\$real" "\$@" exit 1 SH chmod 700 "$CI_HOME/shim/systemctl.next" && mv "$CI_HOME/shim/systemctl.next" "$CI_HOME/shim/systemctl" for rc in "$HOME/.bashrc" "$HOME/.profile"; do grep -q '# code-init systemctl shim' "$rc" 2>/dev/null || printf '\n# code-init systemctl shim\nexport PATH="$HOME/.local/share/code-init/shim:$PATH"\n' >>"$rc" done if command -v crontab >/dev/null 2>&1 && ! crontab -l 2>/dev/null | grep -q 'code-init-pool-boot'; then (crontab -l 2>/dev/null; echo "@reboot $CI_HOME/shim/systemctl --user start ag-account-pool.service # code-init-pool-boot") | crontab - || true fi } pool_stop(){ "$NODE" "$POOL_DATA/app/scripts/service.mjs" stop >/dev/null 2>&1 || true; } install_agy_pool(){ need git; need tar local src="${AG_POOL_SRC:-}" t="" local installed_ref; installed_ref=$(cat "$CI_HOME/ag-pool.ref" 2>/dev/null || true) if [ -x "$BIN/agy-pool" ] && [ "$installed_ref" = "$AG_POOL_REF" ] && [ -z "$src" ] && [ -z "${CODE_INIT_FORCE:-}" ]; then say "agy-pool 已安装($AG_POOL_REPO@${AG_POOL_REF:0:7})" else t=$(mktemp -d) if [ -z "$src" ]; then local rc=0; vault github-tarball "$AG_POOL_REPO" "$AG_POOL_REF" "$t/src.tgz" || rc=$? if [ $rc = 2 ] && [ -n "$AG_POOL_FALLBACK_REF" ]; then warn "退回 $AG_POOL_REPO@${AG_POOL_FALLBACK_REF:0:7}(main 版本没有 --takeover)" AG_POOL_REF=$AG_POOL_FALLBACK_REF; vault github-tarball "$AG_POOL_REPO" "$AG_POOL_REF" "$t/src.tgz" elif [ $rc != 0 ]; then die "下载 agy-pool 源码失败"; fi mkdir "$t/src" && tar -xzf "$t/src.tgz" -C "$t/src" --strip-components=1 && src="$t/src" fi say "安装 agy-pool 依赖…" (cd "$src" && "$CI_HOME/node/bin/npm" ci --omit=dev --no-fund --no-audit --loglevel=error) if ! has_systemd_user; then write_systemctl_shim; export PATH="$CI_HOME/shim:$PATH"; fi pool_stop local short; short=$(uname -n | cut -d. -f1) local mid=${AG_POOL_MACHINE_ID:-host-$(printf %s "$short" | tr 'A-Z' 'a-z' | tr -c 'a-z0-9-' - | cut -c1-49 | sed 's/-*$//')} say "安装账号池后台 + agy-pool 启动器(机器 $mid)…" (cd "$src" && "$NODE" scripts/install.mjs --machine-id "$mid" --machine-name "${AG_POOL_MACHINE_NAME:-$short}" --port "$POOL_PORT" --default-cli --no-start) echo "${AG_POOL_SRC:+local:}$AG_POOL_REF" > "$CI_HOME/ag-pool.ref" [ -n "$t" ] && rm -rf "$t" fi has_systemd_user || { write_systemctl_shim; export PATH="$CI_HOME/shim:$PATH"; } # 账号池专用同步能力(不是 Vault 管理 token),只经 stdin 进本机加密缓存;然后自动下载全部账号。 if "$BIN/agy-pool" sync status 2>/dev/null | grep -qE '金库同步:(ready|syncing|idle)' && [ -z "${CODE_INIT_FORCE:-}" ]; then say "账号池同步已配置且在线,跳过(不打断正在运行的 agy 会话)" else pool_stop vault pipe-json "$AG_POOL_SYNC_KEY" -- "$NODE" "$POOL_DATA/app/scripts/configure-sync.mjs" fi "$NODE" "$POOL_DATA/app/scripts/service.mjs" | sed 's/^/[code-init] /' } install_self(){ mkdir -p "$BIN" if [ -f "${BASH_SOURCE[0]:-}" ] && [ "$(readlink -f "${BASH_SOURCE[0]}")" != "$(readlink -f "$BIN/code-init" 2>/dev/null)" ]; then install -m 700 "${BASH_SOURCE[0]}" "$BIN/code-init" fi grep -q '# code-init path' "$HOME/.bashrc" 2>/dev/null || printf '\n# code-init path\nexport PATH="$HOME/.local/bin:$PATH"\n' >>"$HOME/.bashrc" grep -q '# code-init path' "$HOME/.profile" 2>/dev/null || printf '\n# code-init path\nexport PATH="$HOME/.local/bin:$PATH"\n' >>"$HOME/.profile" if [ "${CODE_INIT_PERSIST_TOKEN:-}" = 1 ]; then vault save-token if command -v crontab >/dev/null 2>&1 && ! crontab -l 2>/dev/null | grep -q 'code-init codex-sync'; then (crontab -l 2>/dev/null; echo "17 * * * * $BIN/code-init codex-sync auto >/dev/null 2>&1 # code-init codex-sync") | crontab - || true say "已加 cron:每小时 codex-sync auto" fi fi } check(){ local ok=0 say "—— 自检 ——" if command -v codex >/dev/null && codex login status 2>&1 | grep -qiE '^logged in'; then say "✔ codex:$(codex --version 2>/dev/null | tail -1) · $(codex login status 2>&1 | grep -iE '^logged in' | head -1)"; else warn "✘ codex 未登录"; ok=1; fi if [ "${CODE_INIT_SKIP_AGY:-}" = 1 ]; then : elif [ -x "$BIN/agy-pool" ]; then local s; s=$("$BIN/agy-pool" status 2>&1 || true) echo "$s" | grep -E '金库同步|账号池|在线' | head -3 | sed 's/^/[code-init] /' local i=0 n=0; while :; do n=$(echo "$s" | grep -c $'\t启用\tready' || true); [ "$n" -gt 0 ] || [ $i -ge 12 ] && break; sleep 5; i=$((i+1)); s=$("$BIN/agy-pool" status 2>&1 || true); done if [ "$n" -gt 0 ]; then say "✔ agy-pool:$n 个账号 ready(共 $(echo "$s" | grep -c $'\t启用\t' || true) 个启用)"; else warn "✘ agy-pool 还没有可用账号(同步可能还在进行,稍后 agy-pool sync now)"; ok=1; fi "$BIN/agy-pool" doctor 2>&1 | grep -E '默认 agy|官方|版本' | head -3 | sed 's/^/[code-init] /' || true else warn "✘ 没有 agy-pool"; ok=1; fi [ $ok = 0 ] && say "全部就绪:新终端里直接用 codex / agy-pool --dangerously-skip-permissions" || warn "有项未通过,见上" return $ok } codex_sync(){ local mode=${1:-auto} f="$HOME/.codex/auth.json" case $mode in pull) vault codex-pull "$CODEX_AUTH_KEY" "$f" ;; push) vault codex-push "$CODEX_AUTH_KEY" "$f" ;; auto) vault codex-push "$CODEX_AUTH_KEY" "$f" && vault codex-pull "$CODEX_AUTH_KEY" "$f" ;; *) die "用法:code-init codex-sync pull|push|auto" ;; esac } main(){ need curl; need tar; need sha256sum local cmd=${1:-install} case $cmd in install|"") get_token; install_node; write_helper vault ping "$CODEX_AUTH_KEY" >/dev/null || die "Vault 不可用" [ "${CODE_INIT_SKIP_CODEX:-}" = 1 ] || install_codex if [ "${CODE_INIT_SKIP_AGY:-}" != 1 ]; then install_agy_native; install_agy_pool; fi install_self check || true say "提示:当前终端先执行 'source ~/.bashrc'(或开新终端)" ;; check) check ;; codex-sync) get_token; [ -x "$NODE" ] || install_node; write_helper; codex_sync "${2:-auto}" ;; -h|--help|help) sed -n '2,20p' "$0" ;; *) die "未知子命令 $cmd(install|check|codex-sync)" ;; esac } main "$@"