Bind svc user CLI to instance storage and reject lock bypass

Why: U1 requires account ops to hit manifest data_root/DSN with OpenExisting and refuse missing flock or default ./data.
Evidence: python3 -m unittest tests.test_svc_user_binding (T1–T5) + test_svc_identity
Impact: svc <env> user … calls ENV_BIN with --open-existing; deploy/user fail closed without flock.
Refs: docs/svc-deploy/u1-instance-account-binding.md; Backend OpenExisting (PR #171)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-12 03:55:24 +00:00
co-authored by Cursor
parent d978de4715
commit 2a03dd58f9
9 changed files with 531 additions and 19 deletions
+74 -6
View File
@@ -871,18 +871,25 @@ port_owner() {
# ---- 生命周期互斥锁与期望运行态(autostart;见 help recovery)----
# svc_lock MODE:对当前服务取 .start.lock 互斥锁。MODE=wait 阻塞(最长 300s),
# MODE=-n 非阻塞(抢不到返回 1)。flock 缺失时降级为无锁直通(尽力而为)。
# MODE=-n 非阻塞(抢不到返回 1)。flock 缺失或等待超时 → 拒绝(不得直通冲突操作)。
# 锁随进程退出自动释放(fd 关闭);长驻循环(recover)用 svc_unlock 逐服务显式释放。
SVC_LOCK_HELD=0
svc_lock() {
[ "$SVC_LOCK_HELD" = 1 ] && return 0
command -v flock >/dev/null 2>&1 || { SVC_LOCK_HELD=1; return 0; }
if ! command -v flock >/dev/null 2>&1; then
c_err "[$ENV_NAME] 缺少 flock:拒绝生命周期/账户冲突操作(安装 util-linux 后重试)"
return 1
fi
mkdir -p "$ENV_DIR" 2>/dev/null || return 1
exec 9>>"$ENV_DIR/.start.lock" || return 1
if [ "$1" = "-n" ]; then
flock -n 9 || { exec 9>&-; return 1; }
else
flock -w 300 9 || c_warn "[$ENV_NAME] 生命周期锁等待超时(继续执行,注意并发)"
if ! flock -w 300 9; then
exec 9>&-
c_err "[$ENV_NAME] 生命周期锁等待超时:拒绝继续(避免并发冲突)"
return 1
fi
fi
SVC_LOCK_HELD=1
return 0
@@ -1384,6 +1391,66 @@ materialize_release() {
install_artifacts "$bin" "$art/plugins" || return 1
}
# ---- agent:账户 CLI(绑定实例存储;禁止回落默认 ./data)----
# 用法:scripts/svc.sh <env> user <create|list|...> [--owner …]
# 从实例最终配置 / manifest identity 解析 data/DSN,强制 --open-existing,
# 调用 ENV_BIN 对应版本。拒绝调用方覆盖 --data/--dsn/--open-existing。
agent_do_user() {
local a
for a in "$@"; do
case "$a" in
--data|--dsn|--open-existing|--data=*|--dsn=*|--open-existing=*)
c_err "[$ENV_NAME] 账户操作禁止覆盖存储绑定(勿传 --data/--dsn/--open-existing;由实例 manifest 决定)"
return 2 ;;
esac
done
svc_lock wait || { c_err "[$ENV_NAME] 无法获取生命周期锁(账户操作与启停互斥)"; return 1; }
# 有 manifest 时以 identity.data_root 为权威,拒绝 ENV_DATA 漂移到默认 ./data。
if [ -f "$ENV_DIR/manifest.json" ] && command -v python3 >/dev/null 2>&1; then
local mf_root
mf_root="$(python3 -c 'import json,sys; m=json.load(open(sys.argv[1])); print(m["identity"]["data_root"])' \
"$ENV_DIR/manifest.json" 2>/dev/null || true)"
if [ -n "$mf_root" ]; then
ENV_DATA="$mf_root"
fi
fi
case "${ENV_DATA:-}" in
""|./data|data)
c_err "[$ENV_NAME] 未绑定数据目录:拒绝账户操作(不回落默认 ./data)"
return 1 ;;
esac
resolve_db_config || {
c_err "[$ENV_NAME] 数据库绑定无效:拒绝账户操作(不回落默认 ./data)"
return 1
}
if [ "$DB_MODE" = mock ]; then
c_err "[$ENV_NAME] mock 档不支持账户操作"
return 1
fi
if [ ! -x "${ENV_BIN:-}" ]; then
c_err "[$ENV_NAME] 实例二进制不存在:$ENV_BIN(先 deploy)"
return 1
fi
if [ "$DB_MODE" = pg ] && [ -z "${DB_DSN:-}" ] && [ "${DB_SOURCE:-}" != conffile ]; then
c_err "[$ENV_NAME] PostgreSQL 档缺少 DSN:拒绝账户操作"
return 1
fi
# 嵌入式档:数据文件必须已存在(OpenExisting);PG 档由 --open-existing 在后端核验。
if [ "$DB_MODE" != pg ]; then
if [ ! -d "$ENV_DATA" ] || [ ! -f "$ENV_DATA/agent-system.db" ]; then
c_err "[$ENV_NAME] 既有库不存在:$ENV_DATA/agent-system.db(OpenExisting:拒绝创建空库)"
return 1
fi
fi
local bin_args=(--open-existing --data "$ENV_DATA")
if [ "$DB_MODE" = pg ] && [ -n "${DB_DSN:-}" ]; then
bin_args+=(--dsn "$DB_DSN")
fi
c_info "[$ENV_NAME] 账户 CLI → $ENV_BIN user $* (data=$ENV_DATA open-existing)"
# 子命令参数在前,绑定 flags 在后(Go flag 不区分顺序)。
AGENT_SYSTEM_DSN="${DB_DSN:-}" "$ENV_BIN" user "$@" "${bin_args[@]}"
}
# ---- agent:部署与启动 ----
agent_do_deploy() {
@@ -3954,17 +4021,18 @@ main() {
# 动作词可省略(省略 = deploy);flags 可直接跟随服务名。
local action="deploy"
case "${1:-}" in
deploy|stop|start|restart|status|logs|db|config) action="$1"; shift ;;
deploy|stop|start|restart|status|logs|db|config|init|user) action="$1"; shift ;;
esac
if [ "$action" = logs ] && [ "${1:-}" != "" ] && [[ "$1" =~ ^[0-9]+$ ]]; then
CLI_LOGS_N="$1"; shift
fi
# db / config 子命令:自带参数解析(位置参数不走 parse_args;未知 flags 由其自行拒绝)。
if [ "$action" = db ] || [ "$action" = config ]; then
# db / config / user 子命令:自带参数解析(位置参数不走 parse_args)。
if [ "$action" = db ] || [ "$action" = config ] || [ "$action" = user ]; then
env_config "$ENV_NAME"
case "$action" in
db) svc_db_bind "$@" || exit $? ;;
config) svc_config "$@" || exit $? ;;
user) agent_do_user "$@" || exit $? ;;
esac
exit 0
fi