feat(agent-db): 引入 db-conn.sh 统一数据库入口,各 agent 改用专用账号
- 新增 scripts/db-conn.sh:按 agent id 反查中文名,读 .env 后经 MYSQL_PWD 转发给 docker exec,口令不进 argv/stdout,--print 只输出占位符 - new-agent.sh 新增 --adopt 模式(为既有 agent 补建库/账号),与 --reset-password 互斥 - AGENTS.md.tpl 与 11 个 workspace-*/AGENTS.md 改用 db-conn.sh,禁用 root 与 docker exec 直连 - mysql-schema-sync 技能改用 MYSQL_PWD_SQL 环境变量,不再硬编码密码 - 设计文档补附录 A 第 8 条:中文键名无法注入 agent 环境的三条实测依据
This commit is contained in:
@@ -12,8 +12,8 @@
|
||||
## 数据库规范
|
||||
- **默认库:** `{{AGENT_ID}}` —— 这就是我自己的库
|
||||
- **账号:** `agent_{{AGENT_ID}}`
|
||||
- **密码:** 环境变量 `DB_PASSWORD_{{AGENT_NAME}}`
|
||||
- **连接:** `mysql -h 127.0.0.1 -u agent_{{AGENT_ID}} -p"${DB_PASSWORD_{{AGENT_NAME}}}" {{AGENT_ID}}`
|
||||
- **连接(推荐):** `~/.openclaw/scripts/db-conn.sh {{AGENT_ID}}`(交互式)或 `~/.openclaw/scripts/db-conn.sh {{AGENT_ID}} --sql "select database()"`
|
||||
- **口令:** 由该脚本自动从 `~/.openclaw/.env` 读取(键名 `DB_PASSWORD_{{AGENT_NAME}}`),口令不会出现在命令行中
|
||||
- **禁止:** 使用 root、使用 `docker exec`、访问其他 agent 的库、跨库写入
|
||||
- **建表前**先确认字段与索引;**破坏性语句**(DROP / TRUNCATE / 无条件 DELETE)必须先请示
|
||||
|
||||
|
||||
Executable
+185
@@ -0,0 +1,185 @@
|
||||
#!/usr/bin/env bash
|
||||
# =============================================================================
|
||||
# db-conn.sh —— agent 访问自己专属 MySQL 库的唯一入口(统一 helper)
|
||||
#
|
||||
# 【用途】
|
||||
# agent 需要查/写自己的库(库名 = 自己的 agent id)时,一律通过本脚本。
|
||||
# 不要自己拼 mysql 命令,不要用 root,不要用 docker exec 直连。
|
||||
#
|
||||
# 【为什么需要它】
|
||||
# 口令存放在 ~/.openclaw/.env,键名是中文的 DB_PASSWORD_<中文名>(便于人阅读)。
|
||||
# 实测确认:中文键名**无法**作为环境变量注入 agent 的执行环境,也无法交给
|
||||
# `openclaw secrets store --kind env` 托管(其名称语法限制为纯大写 ASCII)。
|
||||
# 依据见 ~/.openclaw/docs/agent-创建规范与自动化-设计.md 附录 A 第 8 条。
|
||||
# 因此由本脚本按 agent id 反查中文名 → 读 .env → 代入容器内的 docker MySQL。
|
||||
#
|
||||
# 【用法】
|
||||
# db-conn.sh <agent_id> 交互式 mysql(已切到自己的库)
|
||||
# db-conn.sh <agent_id> --sql "select 1" 执行一条 SQL,结果打到 stdout
|
||||
# db-conn.sh <agent_id> --print 只打印等价命令(口令用占位符,可安全复制)
|
||||
# db-conn.sh --help
|
||||
#
|
||||
# 【安全约定】
|
||||
# 1. 口令只通过环境变量 MYSQL_PWD 传给容器内的 mysql 客户端(客户端原生支持),
|
||||
# 绝不出现在 argv/命令行,也绝不打印到 stdout/stderr,不落盘。
|
||||
# 2. 传参写成 `docker exec -e MYSQL_PWD`(**不带 =值**):docker 会从本脚本进程
|
||||
# 的环境里转发该变量。若写成 `-e MYSQL_PWD="$值"`,口令会进入 docker 进程的
|
||||
# argv,`ps -eo args` 可见(已实测),故不采用。
|
||||
# 3. 账号固定 agent_<id>、库固定 <id>;脚本不提供切换账号/库的开关。
|
||||
# 4. --print 用占位符替换口令,输出可安全粘贴进聊天记录/工单。
|
||||
# 5. 本脚本不读取、也不使用任何 root 凭据。
|
||||
#
|
||||
# 【退出码】
|
||||
# 0 成功
|
||||
# 2 用法/环境错误(参数缺失或非法、agent 不存在、中文名取不到、口令缺失)
|
||||
# 其他 docker 或 mysql 的原始退出码
|
||||
# =============================================================================
|
||||
set -euo pipefail
|
||||
|
||||
# —— 绝对路径与固定 PATH:不依赖登录 shell / rc 文件,cron 与 agent exec 都能跑 ——
|
||||
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
|
||||
OPENCLAW_ROOT="${OPENCLAW_ROOT:-/home/yangxuan/.openclaw}"
|
||||
ENV_FILE="$OPENCLAW_ROOT/.env"
|
||||
CONFIG_FILE="$OPENCLAW_ROOT/openclaw.json"
|
||||
MYSQL_CONTAINER="mysql"
|
||||
|
||||
usage() {
|
||||
cat <<'USAGE'
|
||||
用法:
|
||||
db-conn.sh <agent_id> 交互式连接自己的库
|
||||
db-conn.sh <agent_id> --sql "<SQL>" 执行一条 SQL 并输出结果
|
||||
db-conn.sh <agent_id> --print 只打印等价命令(口令以占位符显示)
|
||||
db-conn.sh --help
|
||||
|
||||
<agent_id> 即 openclaw.json 中 agents.entries 的键(如 main、juaner、sql)。
|
||||
账号固定为 agent_<agent_id>,库固定为 <agent_id>;口令自动从 ~/.openclaw/.env
|
||||
按中文键名 DB_PASSWORD_<中文名> 读取,不会出现在命令行里。
|
||||
USAGE
|
||||
}
|
||||
|
||||
# —— 打印用:把 SQL 包成可直接复制的 shell 字面量(优先单引号,含单引号时退回 %q)——
|
||||
quote_sql() {
|
||||
local s="$1"
|
||||
if [[ "$s" != *"'"* ]]; then
|
||||
printf "'%s'" "$s"
|
||||
else
|
||||
printf '%q' "$s"
|
||||
fi
|
||||
}
|
||||
|
||||
# —— ⓪ 参数解析 ——
|
||||
AGENT_ID=""
|
||||
SQL=""
|
||||
PRINT_ONLY=0
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
-h|--help) usage; exit 0 ;;
|
||||
--print) PRINT_ONLY=1; shift ;;
|
||||
--sql)
|
||||
[[ $# -ge 2 ]] || { echo "错误:--sql 后面缺少 SQL 语句" >&2; usage >&2; exit 2; }
|
||||
SQL="$2"; shift 2 ;;
|
||||
--sql=*) SQL="${1#--sql=}"; shift ;;
|
||||
--*) echo "错误:未知参数 $1" >&2; usage >&2; exit 2 ;;
|
||||
*)
|
||||
if [[ -z "$AGENT_ID" ]]; then
|
||||
AGENT_ID="$1"; shift
|
||||
else
|
||||
echo "错误:多余的参数 $1(只接受一个 <agent_id>)" >&2; usage >&2; exit 2
|
||||
fi ;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ -n "$AGENT_ID" ]] || { echo "错误:缺少 <agent_id>" >&2; usage >&2; exit 2; }
|
||||
[[ "$AGENT_ID" =~ ^[a-z][a-z0-9-]{0,23}$ ]] || {
|
||||
echo "错误:agent_id 非法(只允许小写字母开头、[a-z0-9-]、最长 24 字符):$AGENT_ID" >&2
|
||||
exit 2
|
||||
}
|
||||
|
||||
DB_USER="agent_$AGENT_ID"
|
||||
DB_NAME="$AGENT_ID"
|
||||
|
||||
# —— ① 环境依赖 ——
|
||||
command -v python3 >/dev/null 2>&1 || { echo "错误:找不到 python3(本脚本用它解析 $CONFIG_FILE)" >&2; exit 2; }
|
||||
command -v docker >/dev/null 2>&1 || { echo "错误:找不到 docker 命令" >&2; exit 2; }
|
||||
[[ -f "$CONFIG_FILE" ]] || { echo "错误:找不到配置文件 $CONFIG_FILE" >&2; exit 2; }
|
||||
[[ -f "$ENV_FILE" ]] || { echo "错误:找不到口令文件 $ENV_FILE" >&2; exit 2; }
|
||||
|
||||
# —— ② agent_id → 中文名(agents.entries.<id>.name;取不到即报错)——
|
||||
AGENT_NAME=""
|
||||
NAME_RC=0
|
||||
AGENT_NAME="$(python3 - "$CONFIG_FILE" "$AGENT_ID" <<'PY'
|
||||
import json, sys
|
||||
|
||||
cfg_path, agent_id = sys.argv[1], sys.argv[2]
|
||||
try:
|
||||
with open(cfg_path, encoding="utf-8") as fh:
|
||||
cfg = json.load(fh)
|
||||
except Exception as exc:
|
||||
sys.stderr.write("读取 %s 失败:%s\n" % (cfg_path, exc))
|
||||
sys.exit(3)
|
||||
|
||||
entries = ((cfg.get("agents") or {}).get("entries")) or {}
|
||||
entry = entries.get(agent_id)
|
||||
if not isinstance(entry, dict):
|
||||
sys.exit(4)
|
||||
|
||||
# 主键是 name;个别条目若只有 identity.name,则兜底取它,避免 helper 对拓扑差异敏感
|
||||
name = str(entry.get("name") or "").strip()
|
||||
if not name and isinstance(entry.get("identity"), dict):
|
||||
name = str(entry["identity"].get("name") or "").strip()
|
||||
if not name:
|
||||
sys.exit(5)
|
||||
sys.stdout.write(name)
|
||||
PY
|
||||
)" || NAME_RC=$?
|
||||
|
||||
case "$NAME_RC" in
|
||||
0) ;;
|
||||
4) echo "错误:agent '$AGENT_ID' 不在 $CONFIG_FILE 的 agents.entries 中" >&2; exit 2 ;;
|
||||
5) echo "错误:agent '$AGENT_ID' 在配置里没有 name(中文名),无法定位其口令键" >&2; exit 2 ;;
|
||||
*) echo "错误:解析 $CONFIG_FILE 失败(python3 退出码 $NAME_RC)" >&2; exit 2 ;;
|
||||
esac
|
||||
|
||||
# —— ③ 中文名 → 口令(.env 内键名 DB_PASSWORD_<中文名>)——
|
||||
PWD_KEY="DB_PASSWORD_$AGENT_NAME"
|
||||
PWD_VALUE=""
|
||||
PWD_VALUE="$(
|
||||
awk -v k="$PWD_KEY" 'index($0, k "=") == 1 { print substr($0, length(k) + 2); exit }' "$ENV_FILE"
|
||||
)"
|
||||
# 去掉行尾 CR 与成对引号(.env 可能被编辑器写坏)
|
||||
PWD_VALUE="${PWD_VALUE%$'\r'}"
|
||||
case "$PWD_VALUE" in
|
||||
\"*\") PWD_VALUE="${PWD_VALUE:1:${#PWD_VALUE}-2}" ;;
|
||||
\'*\') PWD_VALUE="${PWD_VALUE:1:${#PWD_VALUE}-2}" ;;
|
||||
esac
|
||||
if [[ -z "$PWD_VALUE" ]]; then
|
||||
echo "错误:$ENV_FILE 里没有 $PWD_KEY(agent '$AGENT_ID'/中文名 '$AGENT_NAME')" >&2
|
||||
echo " 该 agent 可能尚未纳入 —— 请联系杨轩用 new-agent.sh 补建/纳入后重试" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
# —— ④ --print:只打印等价命令,口令用占位符(绝不打印真实值)——
|
||||
if [[ "$PRINT_ONLY" == "1" ]]; then
|
||||
if [[ -n "$SQL" ]]; then
|
||||
printf "MYSQL_PWD='<从 .env 读取:%s>' docker exec -e MYSQL_PWD %s mysql -u%s -N -B -e %s %s\n" \
|
||||
"$PWD_KEY" "$MYSQL_CONTAINER" "$DB_USER" "$(quote_sql "$SQL")" "$DB_NAME"
|
||||
else
|
||||
# 交互式:本地有 TTY 才能 -it(与下面实际执行时的判定保持一致)
|
||||
if [[ -t 0 && -t 1 ]]; then TTY_FLAGS="-it"; else TTY_FLAGS="-i"; fi
|
||||
printf "MYSQL_PWD='<从 .env 读取:%s>' docker exec %s -e MYSQL_PWD %s mysql -u%s %s\n" \
|
||||
"$PWD_KEY" "$TTY_FLAGS" "$MYSQL_CONTAINER" "$DB_USER" "$DB_NAME"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# —— ⑤ 连接:口令经 MYSQL_PWD 环境变量转发(不进 argv、不进 stdout)——
|
||||
if [[ -n "$SQL" ]]; then
|
||||
MYSQL_PWD="$PWD_VALUE" docker exec -e MYSQL_PWD "$MYSQL_CONTAINER" \
|
||||
mysql -u"$DB_USER" -N -B -e "$SQL" "$DB_NAME"
|
||||
else
|
||||
if [[ -t 0 && -t 1 ]]; then TTY_FLAGS="-it"; else TTY_FLAGS="-i"; fi
|
||||
echo "→ 已连接:库 $DB_NAME,账号 $DB_USER(输入 exit 或按 Ctrl-D 退出)" >&2
|
||||
MYSQL_PWD="$PWD_VALUE" docker exec $TTY_FLAGS -e MYSQL_PWD "$MYSQL_CONTAINER" \
|
||||
mysql -u"$DB_USER" "$DB_NAME"
|
||||
fi
|
||||
+26
-3
@@ -1,5 +1,6 @@
|
||||
#!/usr/bin/env bash
|
||||
# 新建 OpenClaw agent:建库 → 建号 → 授权 → 写 .env → 创建 agent → 渲染中文模板 → 自检
|
||||
# 另含 --reset-password(重置既有账号密码)与 --adopt(为既有 agent 补建缺失的库/账号)两个模式
|
||||
set -euo pipefail
|
||||
|
||||
ENV_FILE="$HOME/.openclaw/.env"
|
||||
@@ -13,19 +14,21 @@ MYSQL_CONTAINER="mysql"
|
||||
TEMPLATES=(IDENTITY SOUL AGENTS MEMORY USER)
|
||||
TEMPLATE_LIST="$(printf '%s.md ' "${TEMPLATES[@]}")"
|
||||
|
||||
usage() { echo "用法: new-agent.sh <id> <中文名> [--dry-run] [--reset-password]"; }
|
||||
usage() { echo "用法: new-agent.sh <id> <中文名> [--dry-run] [--reset-password] [--adopt]"; }
|
||||
|
||||
# —— ⓪ 参数解析(参数不足直接给用法,不当作未知参数)——
|
||||
[[ $# -ge 2 ]] || { usage; exit 2; }
|
||||
AGENT_ID="$1"; AGENT_NAME="$2"; shift 2
|
||||
DRY_RUN=0; RESET_PASSWORD=0
|
||||
DRY_RUN=0; RESET_PASSWORD=0; ADOPT=0
|
||||
for arg in "$@"; do
|
||||
case "$arg" in
|
||||
--dry-run) DRY_RUN=1 ;;
|
||||
--reset-password) RESET_PASSWORD=1 ;;
|
||||
--adopt) ADOPT=1 ;;
|
||||
*) echo "未知参数: $arg" >&2; usage; exit 2 ;;
|
||||
esac
|
||||
done
|
||||
[[ $((RESET_PASSWORD + ADOPT)) -le 1 ]] || { echo "--reset-password 与 --adopt 互斥,不能同时使用" >&2; exit 2; }
|
||||
|
||||
# —— ① 入参校验 ——
|
||||
[[ -n "$AGENT_ID" && -n "$AGENT_NAME" ]] || { usage; exit 2; }
|
||||
@@ -64,6 +67,12 @@ WS_EXISTS=$([[ -d "$WORKSPACE" ]] && echo 1 || echo 0)
|
||||
|
||||
if [[ "$RESET_PASSWORD" == "1" ]]; then
|
||||
[[ "$USER_EXISTS" == "1" ]] || { echo "账号 $DB_USER 不存在,无法重置密码"; exit 1; }
|
||||
elif [[ "$ADOPT" == "1" ]]; then
|
||||
# 纳入模式:只补「库/账号缺失」的存量 agent,跳过 创建 agent / 模板渲染 / set-identity。
|
||||
# 库或账号已存在时仍拒绝 —— 保持与 create 模式一致的幂等语义,避免覆盖既有数据。
|
||||
[[ "$DB_EXISTS" == "0" ]] || { echo "❌ 库 $AGENT_ID 已存在 —— --adopt 只补建缺失的库/账号;库已存在请改用 new-agent.sh --reset-password 或手工授权"; exit 1; }
|
||||
[[ "$USER_EXISTS" == "0" ]] || { echo "❌ 账号 $DB_USER 已存在 —— --adopt 拒绝;如需重置密码请用 --reset-password"; exit 1; }
|
||||
[[ "$AGENT_EXISTS" == "1" || "$WS_EXISTS" == "1" ]] || { echo "❌ agent 条目与目录 $WORKSPACE 都不存在 —— 这属于新建场景,请去掉 --adopt"; exit 1; }
|
||||
else
|
||||
[[ "$DB_EXISTS" == "0" ]] || { echo "❌ 库 $AGENT_ID 已存在 —— 已存在请使用 --reset-password,或改用 new-agent.sh 管理"; exit 1; }
|
||||
[[ "$USER_EXISTS" == "0" ]] || { echo "❌ 账号 $DB_USER 已存在 —— 请使用 --reset-password"; exit 1; }
|
||||
@@ -80,7 +89,9 @@ SQL_CREATE_USER="CREATE USER '$DB_USER'@'%' IDENTIFIED BY '$NEW_PWD';"
|
||||
SQL_ALTER_USER="ALTER USER '$DB_USER'@'%' IDENTIFIED BY '$NEW_PWD';"
|
||||
SQL_GRANT="GRANT ALL PRIVILEGES ON \`$AGENT_ID\`.* TO '$DB_USER'@'%';"
|
||||
|
||||
if [[ "$RESET_PASSWORD" == "1" ]]; then MODE="reset-password"; else MODE="create"; fi
|
||||
if [[ "$RESET_PASSWORD" == "1" ]]; then MODE="reset-password"
|
||||
elif [[ "$ADOPT" == "1" ]]; then MODE="adopt"
|
||||
else MODE="create"; fi
|
||||
|
||||
if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "[dry-run] 模式:$MODE"
|
||||
@@ -98,6 +109,8 @@ if [[ "$DRY_RUN" == "1" ]]; then
|
||||
echo "[dry-run] 将写入 $ENV_FILE:$PASSWORD_VAR=<32位随机>"
|
||||
if [[ "$MODE" == "reset-password" ]]; then
|
||||
echo "[dry-run] 将跳过:openclaw agents add / 模板渲染 / set-identity(agent 与 workspace 保持不变)"
|
||||
elif [[ "$MODE" == "adopt" ]]; then
|
||||
echo "[dry-run] 将跳过:openclaw agents add / 模板渲染 / set-identity(agent 条目已存在=$AGENT_EXISTS,目录已存在=$WS_EXISTS,不覆盖既有内容)"
|
||||
else
|
||||
echo "[dry-run] 将创建 workspace:$WORKSPACE"
|
||||
echo "[dry-run] 将执行:openclaw agents add $AGENT_ID --workspace $WORKSPACE --non-interactive"
|
||||
@@ -118,6 +131,7 @@ if [[ "$MODE" == "reset-password" ]]; then
|
||||
fi
|
||||
mysql_root "$SQL_ALTER_USER" >/dev/null
|
||||
else
|
||||
# create 与 adopt 共用:建库 + 建号(adopt 已在上面的检查中确认两者都不存在)
|
||||
mysql_root "$SQL_CREATE_DB" >/dev/null
|
||||
mysql_root "$SQL_CREATE_USER" >/dev/null
|
||||
fi
|
||||
@@ -141,6 +155,11 @@ if [[ "$MODE" == "reset-password" ]]; then
|
||||
# —— 重置模式:跳过 ⑥ 创建 agent / ⑦ 模板渲染 / ⑧ set-identity ——
|
||||
# agent 条目、workspace 与模板都已存在,重复执行只会失败或覆盖既有内容。
|
||||
echo "ℹ️ 重置模式:跳过 创建 agent / 模板渲染 / set-identity"
|
||||
elif [[ "$MODE" == "adopt" ]]; then
|
||||
# —— 纳入模式:同样跳过 ⑥⑦⑧ ——
|
||||
# agent 条目与 workspace(含既有 AGENTS.md 等文档)都已存在,
|
||||
# 本模式只负责「建库 → 建账号 → 授权 → 写 .env」,绝不触碰既有 workspace 内容。
|
||||
echo "ℹ️ 纳入模式(adopt):跳过 创建 agent / 模板渲染 / set-identity(不覆盖既有 workspace 内容)"
|
||||
else
|
||||
# —— ⑥ 创建 agent ——
|
||||
export NVM_DIR="$HOME/.nvm"; . "$NVM_DIR/nvm.sh"; nvm use 26 >/dev/null 2>&1
|
||||
@@ -191,6 +210,9 @@ printf '[%s] id=%s name=%s user=%s mode=%s selfcheck=%s dry_run=%s\n' \
|
||||
if [[ "$MODE" == "reset-password" ]]; then
|
||||
echo ""
|
||||
echo "✅ 已重置 agent 密码:$AGENT_NAME($AGENT_ID)"
|
||||
elif [[ "$MODE" == "adopt" ]]; then
|
||||
echo ""
|
||||
echo "✅ 已为既有 agent 补建专属库与专用账号:$AGENT_NAME($AGENT_ID)"
|
||||
else
|
||||
echo ""
|
||||
echo "✅ 已创建 agent:$AGENT_NAME($AGENT_ID)"
|
||||
@@ -202,5 +224,6 @@ USER=$DB_USER
|
||||
PASSWORD_ENV=$PASSWORD_VAR
|
||||
WORKSPACE=$WORKSPACE
|
||||
自检=$SELFCHECK
|
||||
连接:$OPENCLAW_ROOT/scripts/db-conn.sh $AGENT_ID (交互式;或加 --sql "<SQL>" 执行单条 SQL;口令自动从 .env 读取,不进命令行)
|
||||
EOF
|
||||
[[ "$SELFCHECK" == "ok" ]] || { echo "⚠️ 自检失败:请检查授权"; exit 1; }
|
||||
|
||||
Reference in New Issue
Block a user