Merge authoritative main into the task branch
Why: include the current integration baseline before verification. Evidence: fetched origin/main=b25846d2. Impact: integrate upstream changes without rewriting task history. Refs: task-sync
This commit is contained in:
@@ -443,16 +443,44 @@ jobs:
|
|||||||
git -C rdm submodule update --init --recursive
|
git -C rdm submodule update --init --recursive
|
||||||
done
|
done
|
||||||
echo "RDM commit: $(git -C rdm rev-parse HEAD)"
|
echo "RDM commit: $(git -C rdm rev-parse HEAD)"
|
||||||
|
# 凭据保留到构建循环内的 submodule update;make 经 without_creds
|
||||||
|
# 剥离,避免 token 进入构建进程。不写凭据文件、不把 token 放进命令行。
|
||||||
|
without_creds() {
|
||||||
|
env -u GIT_CONFIG_COUNT \
|
||||||
|
-u GIT_CONFIG_KEY_0 -u GIT_CONFIG_VALUE_0 \
|
||||||
|
-u GIT_CONFIG_KEY_1 -u GIT_CONFIG_VALUE_1 \
|
||||||
|
-u GIT_CONFIG_KEY_2 -u GIT_CONFIG_VALUE_2 \
|
||||||
|
-u GIT_CONFIG_KEY_3 -u GIT_CONFIG_VALUE_3 \
|
||||||
|
-u SUBMODULE_TOKEN \
|
||||||
|
"$@"
|
||||||
|
}
|
||||||
|
: > rdm/pin-build-digests.txt
|
||||||
|
for rev in "${pins[@]}"; do
|
||||||
|
git -C rdm checkout --detach "$rev"
|
||||||
|
git -C rdm submodule update --init --recursive
|
||||||
|
# 不能用「有无 go.mod」判定落地:pin 集合混有 Go 仓与非 Go 仓
|
||||||
|
#(例如前端仓根目录没有 go.mod)。真正要保证的是每个 pins/*
|
||||||
|
# gitlink 子模块 HEAD 等于记录的提交,且工作树已有已跟踪文件——
|
||||||
|
# 否则 Go 可能静默回退到远端解析。
|
||||||
|
while read -r mode _type gitlink path; do
|
||||||
|
[ "$mode" = "160000" ] || continue
|
||||||
|
head="$(git -C rdm -C "$path" rev-parse HEAD)"
|
||||||
|
if [ "$head" != "$gitlink" ]; then
|
||||||
|
echo "rdm pin ${rev} 的 ${path} 未落地:HEAD 不等于 gitlink 提交" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
if [ -z "$(git -C rdm -C "$path" ls-files)" ]; then
|
||||||
|
echo "rdm pin ${rev} 的 ${path} 未落地:工作树无已跟踪文件" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
done < <(git -C rdm ls-tree "$rev" pins/)
|
||||||
|
without_creds make -C rdm build
|
||||||
|
sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt
|
||||||
|
done
|
||||||
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \
|
unset GIT_CONFIG_COUNT GIT_CONFIG_KEY_0 GIT_CONFIG_VALUE_0 \
|
||||||
GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \
|
GIT_CONFIG_KEY_1 GIT_CONFIG_VALUE_1 GIT_CONFIG_KEY_2 \
|
||||||
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
|
GIT_CONFIG_VALUE_2 GIT_CONFIG_KEY_3 GIT_CONFIG_VALUE_3
|
||||||
unset SUBMODULE_TOKEN
|
unset SUBMODULE_TOKEN
|
||||||
: > rdm/pin-build-digests.txt
|
|
||||||
for rev in "${pins[@]}"; do
|
|
||||||
git -C rdm checkout --detach "$rev"
|
|
||||||
make -C rdm build
|
|
||||||
sha256sum rdm/bin/agent-system-rdm | awk -v rev="$rev" '{print rev, $1}' >> rdm/pin-build-digests.txt
|
|
||||||
done
|
|
||||||
- name: Install Node 22
|
- name: Install Node 22
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|||||||
@@ -0,0 +1,165 @@
|
|||||||
|
# fix/svc-superadmin-envfile-surface-conv 任务卡
|
||||||
|
|
||||||
|
任务主记录:PR #683 https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/683;状态:已完成(合入前闭合);owner:weichen
|
||||||
|
|
||||||
|
- 权威仓:ssh://git@192.168.3.2:2222/weichen/Agent-System-Dev.git;目标分支:main;R0(initial_base_sha)= d9e9c0560a2767539f4415b579adbeff86a22ba6
|
||||||
|
- 工作区:/home/weichen/projects/Agent-System-Dev-wt-fix-svc-superadmin-envfile-surface-conv(worktree);分支:fix/svc-superadmin-envfile-surface-conv
|
||||||
|
- 分支类型:standalone;root_target:main;immediate_target:main;accepting_requirements:false
|
||||||
|
- 需求分流:verdict=NEW;selected_initiative=无;requirement_sha256=1c2e2ec0089a4a524959716deb24ea3e48312199979f87e70337c4b40197747b;triage_receipt=/tmp/svc-sa-surface-20261021/triage.json;receipt_sha256=2adcb48f6e44ae1996bb1e0b09ba34860e4e0a2ae3e5b3dce4e0a84a19a6375e
|
||||||
|
|
||||||
|
- 授权与交付终点:用户 2026-10-21「未处理的其他记录也解决修复一下」(svc 不持久化超级管理员凭据;`POST /api/surfaces` 接受空 `conversationId`);Backend 单元 failed-unknown 后用户选择「全新重跑一次」。终点:Backend 与 Dev 经 PR 合入,并以 `AGENT_SYSTEM_ENV_FILE` 重启 prod 一次登记凭据路径。未授权:prod 业务角色配置、高后果 git 操作
|
||||||
|
- 交付模式:goal-first(目标、范围和交付终点一次确认;范围内连续推进)
|
||||||
|
- 实施执行:agent-per-unit(配置:AGENT-EXECUTION.md)
|
||||||
|
- 合入执行:agent(已授权交付范围内,Review 通过、冲突解决且当前 HEAD 的 CI 通过后合入)
|
||||||
|
- 流程依据:仓库根目录 policy/delivery.md(归档后仍按仓库根目录定位)
|
||||||
|
|
||||||
|
## 目标与非目标
|
||||||
|
|
||||||
|
- 目标 1(`scripts/svc.sh`):agent 服务新增登记键 `superadmin_envfile=`,只存凭据文件绝对路径。deploy/start/restart 时若环境给出 `AGENT_SYSTEM_ENV_FILE`,先在停旧服务前校验(普通文件、属当前用户、无组/其他权限位),通过后登记;空值清除登记。拉起 serve 时本次环境未显式给超管凭据则转发登记路径,recover/watchdog 的清空环境子进程因此也能拉起认证实例。`status` 显示登记路径;`docs/svc.md` §4.2 补说明;新增 `tests/test_svc_superadmin_envfile.py`。
|
||||||
|
- 目标 2(Backend):`POST /api/surfaces` 新建页面(无 `surfaceId`)时要求本人存在的 `conversationId`:空值 400,不存在或他人会话 404;修订语义不变。原因:空/未知会话使页面无项目归属,动作接口随之跳过项目生命周期检查。
|
||||||
|
- 非目标:svc 读取或复制凭据内容;surfacesvc/模型工具路径改动;prod 业务角色配置。
|
||||||
|
|
||||||
|
## Agent 实施收据
|
||||||
|
|
||||||
|
路由与 fail-stop 规则见根目录 AGENT-EXECUTION.md 与 policy/delivery.md;每次尝试一行,原始日志留在源码外收据路径。
|
||||||
|
|
||||||
|
| 单元 ID | 预期范围 | 配置摘要 | 角色 | 候选顺序 | attempt | CLI/模型 | 终态 | transport | fallback | 源码外收据路径 |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| dev-svc-superadmin-envfile-persist | scripts/svc.sh、tests/test_svc_superadmin_envfile.py、docs/svc.md | AGENT-EXECUTION.md | implementation | 按配置 | 1(fresh_session) | cursor | success | apply 快进 0e6453b | 未用 | /tmp/svc-sa-surface-20261021/dev-run.json |
|
||||||
|
| backend-surface-submit-conversation-required | Backend internal/server/platform_api.go、surface_api_test.go | Backend AGENT-EXECUTION.md | implementation | 按配置 | 1(fresh_session) | pi | failed-unknown(隔离区执行 `git submodule update --init` 致 local_config 变化,违反单元约束;源 worktree 未变,现场保留)→ fail-stop | 无 | 未用 | /tmp/svc-sa-surface-20261021/backend-run-attempt1-failed-unknown.json |
|
||||||
|
| backend-surface-submit-conversation-required(用户批准全新重跑,单元补子模块初始化与配置逐字节恢复约束) | 同上 | Backend AGENT-EXECUTION.md | implementation | 按配置 | 1(fresh_session) | pi | success | apply 快进 1e73822→954e744(amend 规范提交,tree 不变) | 未用 | /tmp/svc-sa-surface-20261021/backend-run.json |
|
||||||
|
| dev-svc-superadmin-surface-backend-repin | modules/backend → 528e3a4(Backend #239) | AGENT-EXECUTION.md | implementation | 按配置 | 1(fresh_session) | cursor | failed-unknown(worker 改动正确但终态记录未从信封解出;源 worktree 未变,隔离现场保留)→ fail-stop | 无 | 未用 | /tmp/svc-sa-surface-20261021/repin-run-attempt1-failed-unknown.json |
|
||||||
|
| dev-svc-superadmin-surface-backend-repin(用户批准全新重跑) | 同上 | AGENT-EXECUTION.md | implementation | 按配置 | 1(fresh_session) | cursor | success | apply 快进 → 3c0faf76(amend 规范提交,tree 不变) | 未用 | /tmp/svc-sa-surface-20261021/repin-run.json |
|
||||||
|
|
||||||
|
## 同步记录
|
||||||
|
|
||||||
|
| 节点 | sync_base_sha | tested_head_sha | checked_at |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 启动 | d9e9c0560a2767539f4415b579adbeff86a22ba6 | d9e9c0560a2767539f4415b579adbeff86a22ba6 | 2026-10-21T14:04:06Z |
|
||||||
|
|
||||||
|
## 证据(Evidence)
|
||||||
|
|
||||||
|
<!-- CMD="<命令>" make evidence 写入;手抄/转写输出视为无效证据(AGENTS.md 证据与验证门禁)。 -->
|
||||||
|
| 时间(UTC) | 命令 | 被测HEAD | exit | 来源 | sha256(16) |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
|
||||||
|
## 结果与验证
|
||||||
|
|
||||||
|
- 结果:两项完成。(1) svc.sh 登记 `superadmin_envfile=`(只存路径),deploy/start/restart 校验后登记,裸 restart/recover 自动转发;(2) Backend #239 合入(`528e3a4`),`POST /api/surfaces` 新建页要求本人会话;本分支 gitlink 已钉到该提交。合入后将以 `AGENT_SYSTEM_ENV_FILE` 重启 prod 一次以登记路径。
|
||||||
|
- 验证:`tests.test_svc_superadmin_envfile`+`test_svc_recover`+`test_svc_identity` 51 项 OK(1 跳过,SELF,见证据表);`bash -n scripts/svc.sh` 通过;Backend #239 CI success(GATE,954e744);本 PR 当前 HEAD 的 CI 为合入依据。
|
||||||
|
- 影响与剩余项:svc 成功路径不变,只增路径登记与转发;Backend 只改新建页校验。剩余(非目标):prod 无业务角色时 `admin` 仍不能新建项目。
|
||||||
|
|
||||||
|
## 合入与清理
|
||||||
|
|
||||||
|
- Review 记录:当前主模型(host,非实施会话)审阅实际 diff:svc.sh/docs/测试、Backend handler+测试(经 #239)、gitlink;结论通过,无阻塞项;重放依据为上方测试证据与 Backend CI。
|
||||||
|
- 冲突记录:NOT_RUN(最新目标同步、冲突处置、组合兼容核验及复审/补测)
|
||||||
|
- CI 记录:NOT_RUN(当前 HEAD 完整 SHA、全部必需检查、运行链接、执行身份及 GATE 结果)
|
||||||
|
- 合入记录:NOT_RUN(交付授权范围、PR、被核验 HEAD、合入 SHA、目标分支及内容核验)
|
||||||
|
- 清理记录:NOT_RUN(授权依据、写者退出确认、worktree/分支/任务卡处置)
|
||||||
|
|
||||||
|
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"summary_schema": 1,
|
||||||
|
"source": "SELF",
|
||||||
|
"timestamp": "2026-10-21T14:03:32.182767+00:00",
|
||||||
|
"command": "make precheck",
|
||||||
|
"exit_code": 0,
|
||||||
|
"cwd": "/home/weichen/projects/Agent-System-Dev",
|
||||||
|
"before_head": "d9e9c0560a2767539f4415b579adbeff86a22ba6",
|
||||||
|
"after_head": "d9e9c0560a2767539f4415b579adbeff86a22ba6",
|
||||||
|
"branch": "main",
|
||||||
|
"target_branch": "main",
|
||||||
|
"target_head": "d9e9c0560a2767539f4415b579adbeff86a22ba6",
|
||||||
|
"root_target": "main",
|
||||||
|
"immediate_target": "main",
|
||||||
|
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"output_tail": [
|
||||||
|
"make[1]: Entering directory '/home/weichen/projects/Agent-System-Dev'",
|
||||||
|
"[repo] branch=main head=d9e9c0560a",
|
||||||
|
"[remote] origin/main=d9e9c0560a ahead=0 behind=0",
|
||||||
|
"[tree] clean submodules_drift=0 uninit=0",
|
||||||
|
"[worktrees] linked=3",
|
||||||
|
"[initiatives] local cards (not authoritative): none",
|
||||||
|
"[hooks] core.hooksPath=scripts/hooks(pre-commit/post-merge 实际接线)",
|
||||||
|
"[facts] policy/facts.md 6 条事实均在有效期内",
|
||||||
|
"[required-reading] core/baseline.md, workflows/delivery.md §1, policy/delivery.md",
|
||||||
|
"[R0] d9e9c0560a(完整:d9e9c0560a2767539f4415b579adbeff86a22ba6)",
|
||||||
|
"[worktree] before-sync: recursive component checks passed",
|
||||||
|
"[verdict] READY — 保留本次 precheck receipt 后建任务工作区:PRECHECK_RECEIPT=/tmp/task-precheck.json make task-start NAME=feature/your-branch",
|
||||||
|
"make[1]: Leaving directory '/home/weichen/projects/Agent-System-Dev'"
|
||||||
|
],
|
||||||
|
"output_sha256": "d56c09db1d3fa308d17976569429fb7ab950587f2a97e9438432c21db6de7f25",
|
||||||
|
"receipt_sha256": "ec446f0a238e6f6c64167ff09c23059b47bf08df92852d1ac12ee540348affed",
|
||||||
|
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/ec446f0a238e6f6c64167ff09c23059b47bf08df92852d1ac12ee540348affed.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"summary_schema": 1,
|
||||||
|
"source": "SELF",
|
||||||
|
"timestamp": "2026-10-21T14:22:12.910602+00:00",
|
||||||
|
"command": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_superadmin_envfile tests.test_svc_recover tests.test_svc_identity",
|
||||||
|
"exit_code": 0,
|
||||||
|
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-fix-svc-superadmin-envfile-surface-conv",
|
||||||
|
"before_head": "0e6453bd530feedd973ccfb05f587c5d9d9a6ffc",
|
||||||
|
"after_head": "0e6453bd530feedd973ccfb05f587c5d9d9a6ffc",
|
||||||
|
"branch": "fix/svc-superadmin-envfile-surface-conv",
|
||||||
|
"target_branch": "main",
|
||||||
|
"target_head": "d9e9c0560a2767539f4415b579adbeff86a22ba6",
|
||||||
|
"root_target": "main",
|
||||||
|
"immediate_target": "main",
|
||||||
|
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"output_tail": [
|
||||||
|
"......................................s............",
|
||||||
|
"----------------------------------------------------------------------",
|
||||||
|
"Ran 51 tests in 52.867s",
|
||||||
|
"",
|
||||||
|
"OK (skipped=1)"
|
||||||
|
],
|
||||||
|
"output_sha256": "5cde79b38a542c3831ae872cd4d7a2b9cd0ba84b0b129442ded6d4adea577a27",
|
||||||
|
"receipt_sha256": "3d71bc9d4f8e5ed81db87aae76cc3e649dd90f09ee17dafc1bb8ab494725a5ce",
|
||||||
|
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/3d71bc9d4f8e5ed81db87aae76cc3e649dd90f09ee17dafc1bb8ab494725a5ce.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- Local receipt: SELF. Digests detect corruption; they do not authenticate a reviewer or CI. -->
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"summary_schema": 1,
|
||||||
|
"source": "SELF",
|
||||||
|
"timestamp": "2026-10-21T15:12:21.352177+00:00",
|
||||||
|
"command": "PYTHONDONTWRITEBYTECODE=1 python3 -m unittest tests.test_svc_superadmin_envfile tests.test_svc_recover tests.test_svc_identity",
|
||||||
|
"exit_code": 0,
|
||||||
|
"cwd": "/home/weichen/projects/Agent-System-Dev-wt-fix-svc-superadmin-envfile-surface-conv",
|
||||||
|
"before_head": "3c0faf76b06b999855733bc2a452476793c5d2a0",
|
||||||
|
"after_head": "3c0faf76b06b999855733bc2a452476793c5d2a0",
|
||||||
|
"branch": "fix/svc-superadmin-envfile-surface-conv",
|
||||||
|
"target_branch": "main",
|
||||||
|
"target_head": "d9e9c0560a2767539f4415b579adbeff86a22ba6",
|
||||||
|
"root_target": "main",
|
||||||
|
"immediate_target": "main",
|
||||||
|
"before_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"after_tracked_diff_sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
|
||||||
|
"output_tail": [
|
||||||
|
"......................................s............",
|
||||||
|
"----------------------------------------------------------------------",
|
||||||
|
"Ran 51 tests in 54.457s",
|
||||||
|
"",
|
||||||
|
"OK (skipped=1)"
|
||||||
|
],
|
||||||
|
"output_sha256": "28f0a1224bc1d46a43b8faad062d463820003eb8fd3b4c688f06cd8173380a22",
|
||||||
|
"receipt_sha256": "ebaf7682157339ba8813c1ff743a25fa6c0a85eedd8e06cdba3d22c208c242e8",
|
||||||
|
"receipt_path": "/home/weichen/.cache/agent-system-dev/evidence/ebaf7682157339ba8813c1ff743a25fa6c0a85eedd8e06cdba3d22c208c242e8.json"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## 闭合记录(合入前折叠)
|
||||||
|
|
||||||
|
- 本卡在 PR #683(https://git-home.u1680832.nyat.app:30319/weichen/Agent-System-Dev/pulls/683)合入前闭合并归档,随该 PR 一同合入。
|
||||||
|
- 该 PR 的合并提交即本任务的落地记录。
|
||||||
|
- 合入后无需回填,也无需单独的闭合 PR。
|
||||||
@@ -189,6 +189,12 @@ scripts/svc.sh <name> status # 单服务详情
|
|||||||
- 显式给任何运行参数(或 `--config` 文件、`SVC_*` 变量)时**整体重建**参数;
|
- 显式给任何运行参数(或 `--config` 文件、`SVC_*` 变量)时**整体重建**参数;
|
||||||
- agent/static 换端口:`restart --port N`(进程与登记同步更新);
|
- agent/static 换端口:`restart --port N`(进程与登记同步更新);
|
||||||
- postgres 换端口:`set --apply`(见 §7.4)或 `restart --port N` 同样有效。
|
- postgres 换端口:`set --apply`(见 §7.4)或 `restart --port N` 同样有效。
|
||||||
|
- **超级管理员凭据文件(agent,`superadmin_envfile=`)**:在 deploy/start/restart 时传一次
|
||||||
|
`AGENT_SYSTEM_ENV_FILE=<文件>`,svc 先校验(普通文件、属当前用户、权限 0600 即无组/其他位,
|
||||||
|
不合格则在停旧服务前拒绝),再只把**绝对路径**记入 svc.env(从不读取、复制或打印文件内容);
|
||||||
|
此后裸 `restart`、`recover`/watchdog 拉起都复用该路径。本次环境显式设置的
|
||||||
|
`SUPER_ADMIN_OWNER`/`SUPER_ADMIN_PASSWORD`/`AGENT_SYSTEM_ENV_FILE` 优先于登记;
|
||||||
|
`AGENT_SYSTEM_ENV_FILE=`(空值)清除登记;登记文件已不存在时告警且不转发(serve 按缺凭据拒启)。
|
||||||
|
|
||||||
`status` 输出示例(postgres):
|
`status` 输出示例(postgres):
|
||||||
|
|
||||||
|
|||||||
@@ -942,6 +942,7 @@ svc_merge_tags() {
|
|||||||
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
|
# (托管绑定不重复存 DSN,外部直连存受保护 dsn=);未走解析的调用保持旧值。
|
||||||
record_state() {
|
record_state() {
|
||||||
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn prev_envfile prev_plugins plugins_out merged_tags
|
local pid prev_src prev_dbref prev_conffile prev_db prev_dsn prev_envfile prev_plugins plugins_out merged_tags
|
||||||
|
local prev_saenv
|
||||||
pid="$(read_pid)"
|
pid="$(read_pid)"
|
||||||
prev_src=""
|
prev_src=""
|
||||||
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
|
[ -f "$ENV_DIR/svc.env" ] && prev_src="$(svc_get source)"
|
||||||
@@ -950,6 +951,7 @@ record_state() {
|
|||||||
prev_db="$(svc_get db)"
|
prev_db="$(svc_get db)"
|
||||||
prev_dsn="$(svc_get dsn)"
|
prev_dsn="$(svc_get dsn)"
|
||||||
prev_envfile="$(svc_get envfile)"
|
prev_envfile="$(svc_get envfile)"
|
||||||
|
prev_saenv="$(svc_get superadmin_envfile)"
|
||||||
prev_plugins="$(svc_get plugins)"
|
prev_plugins="$(svc_get plugins)"
|
||||||
plugins_out="$prev_plugins"
|
plugins_out="$prev_plugins"
|
||||||
# 只有激活校验和 UI 探活都通过之后才提交新的 plugins=。中途 record_state
|
# 只有激活校验和 UI 探活都通过之后才提交新的 plugins=。中途 record_state
|
||||||
@@ -999,6 +1001,11 @@ record_state() {
|
|||||||
elif [ -n "$prev_envfile" ]; then
|
elif [ -n "$prev_envfile" ]; then
|
||||||
printf 'envfile=%s\n' "$prev_envfile"
|
printf 'envfile=%s\n' "$prev_envfile"
|
||||||
fi
|
fi
|
||||||
|
if [ -n "${SUPERADMIN_ENVFILE_NEW:-}" ]; then
|
||||||
|
printf 'superadmin_envfile=%s\n' "$SUPERADMIN_ENVFILE_NEW"
|
||||||
|
elif [ "${SUPERADMIN_ENVFILE_CLEAR:-0}" != 1 ] && [ -n "$prev_saenv" ]; then
|
||||||
|
printf 'superadmin_envfile=%s\n' "$prev_saenv"
|
||||||
|
fi
|
||||||
[ -n "$plugins_out" ] && printf 'plugins=%s\n' "$plugins_out"
|
[ -n "$plugins_out" ] && printf 'plugins=%s\n' "$plugins_out"
|
||||||
[ $# -gt 0 ] && printf '%s\n' "$@"
|
[ $# -gt 0 ] && printf '%s\n' "$@"
|
||||||
} > "$tmp"
|
} > "$tmp"
|
||||||
@@ -1529,6 +1536,38 @@ svc_write_serve_env() {
|
|||||||
chmod 0600 "$f" 2>/dev/null || true
|
chmod 0600 "$f" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# svc_superadmin_envfile_check:agent 的 start/restart/deploy 在停旧服务前校验本次
|
||||||
|
# AGENT_SYSTEM_ENV_FILE(只看路径与元数据,绝不读内容)。非空须为当前用户所有、
|
||||||
|
# 无组/其他权限位的普通文件,通过后由 record_state 记为 superadmin_envfile=;
|
||||||
|
# 已设置但为空则清除登记。
|
||||||
|
SUPERADMIN_ENVFILE_NEW=""
|
||||||
|
SUPERADMIN_ENVFILE_CLEAR=0
|
||||||
|
svc_superadmin_envfile_check() {
|
||||||
|
[ -n "${AGENT_SYSTEM_ENV_FILE+x}" ] || return 0
|
||||||
|
if [ -z "$AGENT_SYSTEM_ENV_FILE" ]; then
|
||||||
|
SUPERADMIN_ENVFILE_CLEAR=1
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
local f="$AGENT_SYSTEM_ENV_FILE" dir meta owner mode
|
||||||
|
dir="$(cd "$(dirname "$f")" 2>/dev/null && pwd)" || dir=""
|
||||||
|
[ -n "$dir" ] && f="${dir%/}/$(basename "$f")"
|
||||||
|
if [ -z "$dir" ] || [ ! -f "$f" ]; then
|
||||||
|
c_err "[$ENV_NAME] 超级管理员凭据文件不存在或不是普通文件:$f(运行中的服务未改动)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
meta="$(stat -L -c '%u %a' "$f" 2>/dev/null)" || meta=""
|
||||||
|
owner="${meta%% *}"; mode="${meta##* }"
|
||||||
|
if [ -z "$meta" ] || [ "$owner" != "$(id -u)" ]; then
|
||||||
|
c_err "[$ENV_NAME] 超级管理员凭据文件不属于当前用户:$f(运行中的服务未改动)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
if [ $(( 8#$mode & 8#077 )) -ne 0 ]; then
|
||||||
|
c_err "[$ENV_NAME] 超级管理员凭据文件权限过宽($mode):$f,须 chmod 600(运行中的服务未改动)"
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
SUPERADMIN_ENVFILE_NEW="$f"
|
||||||
|
}
|
||||||
|
|
||||||
# svc_launch_serve ARGS:env -i 只带 serve.env 与本次构造的 guard_env。
|
# svc_launch_serve ARGS:env -i 只带 serve.env 与本次构造的 guard_env。
|
||||||
svc_launch_serve() {
|
svc_launch_serve() {
|
||||||
local exec_args="$1"
|
local exec_args="$1"
|
||||||
@@ -1558,11 +1597,25 @@ svc_launch_serve() {
|
|||||||
args+=("RDM_DISABLE_EXEC_TOOLS=$RDM_DISABLE_EXEC_TOOLS")
|
args+=("RDM_DISABLE_EXEC_TOOLS=$RDM_DISABLE_EXEC_TOOLS")
|
||||||
fi
|
fi
|
||||||
# 超级管理员凭据只从本次环境转发,不写入 serve.env;缺失时由 serve 自行 fail closed。
|
# 超级管理员凭据只从本次环境转发,不写入 serve.env;缺失时由 serve 自行 fail closed。
|
||||||
|
# 本次环境三者皆未设置时回落到 svc.env 登记的 superadmin_envfile=(只转发路径),
|
||||||
|
# recover/watchdog 的清空环境子进程因此也能拉起认证实例。
|
||||||
|
local sa_explicit=0 sa_file
|
||||||
for k in SUPER_ADMIN_OWNER SUPER_ADMIN_PASSWORD AGENT_SYSTEM_ENV_FILE; do
|
for k in SUPER_ADMIN_OWNER SUPER_ADMIN_PASSWORD AGENT_SYSTEM_ENV_FILE; do
|
||||||
if [ -n "${!k+x}" ]; then
|
if [ -n "${!k+x}" ]; then
|
||||||
args+=("$k=${!k}")
|
args+=("$k=${!k}")
|
||||||
|
sa_explicit=1
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
if [ "$sa_explicit" = 0 ]; then
|
||||||
|
sa_file="$(svc_get superadmin_envfile)"
|
||||||
|
if [ -n "$sa_file" ]; then
|
||||||
|
if [ -f "$sa_file" ]; then
|
||||||
|
args+=("AGENT_SYSTEM_ENV_FILE=$sa_file")
|
||||||
|
else
|
||||||
|
c_warn "[$ENV_NAME] 登记的超级管理员凭据文件已不存在:$sa_file(不转发,serve 将按缺凭据拒启)"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
fi
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
nohup "${args[@]}" "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 9>&- &
|
nohup "${args[@]}" "$ENV_BIN" serve $exec_args >> "$ENV_DIR/svc.log" 2>&1 9>&- &
|
||||||
}
|
}
|
||||||
@@ -5576,6 +5629,8 @@ env_status_detail() {
|
|||||||
cfile="$(svc_get conffile)"
|
cfile="$(svc_get conffile)"
|
||||||
[ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
[ -z "$cfile" ] && cfile="$(printf '%s' "$(svc_get exec)" | sed -n 's/.*--config \([^ ]*\).*/\1/p')"
|
||||||
printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态;<name> config <file> 绑定)}"
|
printf ' 配置文件 %s\n' "${cfile:-无(命令行参数形态;<name> config <file> 绑定)}"
|
||||||
|
local saf; saf="$(svc_get superadmin_envfile)"
|
||||||
|
[ -n "$saf" ] && printf ' 超管凭据文件 %s(仅登记路径)\n' "$saf"
|
||||||
printf ' 监听守卫 insecureListen=%s allowedHosts=%s\n' \
|
printf ' 监听守卫 insecureListen=%s allowedHosts=%s\n' \
|
||||||
"$(env_var "$ENV_NAME" INSECURE_LISTEN)" \
|
"$(env_var "$ENV_NAME" INSECURE_LISTEN)" \
|
||||||
"$(env_var "$ENV_NAME" ALLOWED_HOSTS)"
|
"$(env_var "$ENV_NAME" ALLOWED_HOSTS)"
|
||||||
@@ -6452,6 +6507,12 @@ main() {
|
|||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
env_config "$ENV_NAME"
|
env_config "$ENV_NAME"
|
||||||
|
case "$action" in
|
||||||
|
start|restart|deploy)
|
||||||
|
if [ "$SVC_KIND" = agent ]; then
|
||||||
|
svc_superadmin_envfile_check || exit 1
|
||||||
|
fi ;;
|
||||||
|
esac
|
||||||
# 生命周期动作持 .start.lock(wait 模式;进程退出自动释放)——deploy 全程含
|
# 生命周期动作持 .start.lock(wait 模式;进程退出自动释放)——deploy 全程含
|
||||||
# 构建窗口,防 recover/并发 start 在停机-换产物间隙抢跑。只读动作不加锁。
|
# 构建窗口,防 recover/并发 start 在停机-换产物间隙抢跑。只读动作不加锁。
|
||||||
case "$action" in
|
case "$action" in
|
||||||
|
|||||||
@@ -0,0 +1,194 @@
|
|||||||
|
"""svc.sh 超级管理员凭据文件路径登记回归(superadmin_envfile=)。
|
||||||
|
|
||||||
|
deploy/start/restart 时传一次 AGENT_SYSTEM_ENV_FILE:svc 校验(普通文件、属当前用户、
|
||||||
|
无组/其他权限位)后只登记路径;裸 restart 与 recover(清空环境的生命周期子进程)
|
||||||
|
复用该路径转发给 serve;权限过宽在停旧服务前拒绝;空值清除登记;svc.env/svc.log
|
||||||
|
绝不出现文件内容。桩二进制与夹具复用 tests/test_svc_recover.py。
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
import unittest
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
SOURCE = Path(__file__).resolve().parents[1]
|
||||||
|
|
||||||
|
_spec = importlib.util.spec_from_file_location("tsr", SOURCE / "tests" / "test_svc_recover.py")
|
||||||
|
tsr = importlib.util.module_from_spec(_spec)
|
||||||
|
_spec.loader.exec_module(tsr)
|
||||||
|
|
||||||
|
# 桩 serve 额外记录实际收到的 AGENT_SYSTEM_ENV_FILE(未设置记 <unset>)。
|
||||||
|
_ANCHOR = '> "$trace_dir/serve.txt"\n'
|
||||||
|
assert _ANCHOR in tsr.STUB
|
||||||
|
STUB = tsr.STUB.replace(
|
||||||
|
_ANCHOR,
|
||||||
|
_ANCHOR + r""" printf 'AGENT_SYSTEM_ENV_FILE=%s\n' "${AGENT_SYSTEM_ENV_FILE-<unset>}" >> "$trace_dir/serve.txt"
|
||||||
|
""", 1)
|
||||||
|
|
||||||
|
PASSWORD = "Dummy-SA-Pw-9f4c2e71-do-not-leak"
|
||||||
|
UNSET = "<unset>"
|
||||||
|
|
||||||
|
|
||||||
|
class SuperAdminEnvfileTests(tsr.SvcAutoRecoverTests):
|
||||||
|
def setUp(self):
|
||||||
|
super().setUp()
|
||||||
|
shutil.copy2(SOURCE / "scripts" / "svc_manifest.py",
|
||||||
|
self.root / "scripts" / "svc_manifest.py")
|
||||||
|
self.stub.write_text(STUB)
|
||||||
|
for key in ("AGENT_SYSTEM_ENV_FILE", "SUPER_ADMIN_OWNER", "SUPER_ADMIN_PASSWORD"):
|
||||||
|
self.env.pop(key, None)
|
||||||
|
self.secrets = self.root / "secrets"
|
||||||
|
self.secrets.mkdir(mode=0o700)
|
||||||
|
self.cred = self.make_cred("sa.env", 0o600)
|
||||||
|
|
||||||
|
# ---- 夹具 ----
|
||||||
|
|
||||||
|
def make_cred(self, name, mode):
|
||||||
|
p = self.secrets / name
|
||||||
|
p.write_text("SUPER_ADMIN_OWNER=op\nSUPER_ADMIN_PASSWORD=%s\n" % PASSWORD)
|
||||||
|
p.chmod(mode)
|
||||||
|
return p
|
||||||
|
|
||||||
|
def env_with(self, **extra):
|
||||||
|
env = dict(self.env)
|
||||||
|
env.update(extra)
|
||||||
|
return env
|
||||||
|
|
||||||
|
def served_envfile(self):
|
||||||
|
for line in (self.trace / "serve.txt").read_text().splitlines():
|
||||||
|
if line.startswith("AGENT_SYSTEM_ENV_FILE="):
|
||||||
|
return line.split("=", 1)[1]
|
||||||
|
return None
|
||||||
|
|
||||||
|
def fresh_trace(self):
|
||||||
|
f = self.trace / "serve.txt"
|
||||||
|
if f.exists():
|
||||||
|
f.unlink()
|
||||||
|
|
||||||
|
def assert_same_path(self, recorded, expected):
|
||||||
|
self.assertIsNotNone(recorded)
|
||||||
|
self.assertTrue(os.path.isabs(recorded), recorded)
|
||||||
|
self.assertEqual(os.path.realpath(recorded), os.path.realpath(expected))
|
||||||
|
|
||||||
|
def assert_no_secret(self, name, *outputs):
|
||||||
|
for fname in ("svc.env", "svc.log", "manifest.json", "serve.env", "history.jsonl"):
|
||||||
|
p = self.runtime(name) / fname
|
||||||
|
if p.exists():
|
||||||
|
self.assertNotIn(PASSWORD, p.read_text(errors="replace"), fname)
|
||||||
|
for out in outputs:
|
||||||
|
self.assertNotIn(PASSWORD, out)
|
||||||
|
|
||||||
|
def start_with_cred(self, name, cred=None):
|
||||||
|
result = self.svc(name, "start",
|
||||||
|
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(cred or self.cred)))
|
||||||
|
self.assertIn("已启动", result.stdout)
|
||||||
|
return result
|
||||||
|
|
||||||
|
# ---- 用例 ----
|
||||||
|
|
||||||
|
def test_sa_start_records_path_and_bare_restart_reuses_it(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
r1 = self.start_with_cred("dev")
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
|
||||||
|
self.assert_same_path(self.served_envfile(), self.cred)
|
||||||
|
status = self.svc("dev", "status")
|
||||||
|
self.assertIn("超管凭据文件", status.stdout)
|
||||||
|
self.fresh_trace()
|
||||||
|
r2 = self.svc("dev", "restart") # 环境里不再有 AGENT_SYSTEM_ENV_FILE
|
||||||
|
self.assertIn("已启动", r2.stdout)
|
||||||
|
self.assert_same_path(self.served_envfile(), self.cred)
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
|
||||||
|
self.assert_no_secret("dev", r1.stdout, r2.stdout, status.stdout)
|
||||||
|
|
||||||
|
def test_sa_relative_path_recorded_absolute(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
result = self.svc("dev", "start",
|
||||||
|
env=self.env_with(AGENT_SYSTEM_ENV_FILE="secrets/sa.env"))
|
||||||
|
self.assertIn("已启动", result.stdout)
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
|
||||||
|
|
||||||
|
def test_sa_recover_forwards_recorded_path(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_with_cred("dev")
|
||||||
|
old_pid = self.kill9("dev")
|
||||||
|
self.fresh_trace()
|
||||||
|
result = self.svc("recover", "--quiet")
|
||||||
|
self.assertIn("recover 汇总:拉回 1", result.stdout)
|
||||||
|
new_pid = self.pid_of("dev")
|
||||||
|
self.assertTrue(self.pid_alive(new_pid))
|
||||||
|
self.assertNotEqual(new_pid, old_pid)
|
||||||
|
self.assert_same_path(self.served_envfile(), self.cred)
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
|
||||||
|
self.assert_no_secret("dev", result.stdout)
|
||||||
|
|
||||||
|
def test_sa_loose_permissions_refused_before_stop(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_ok("dev")
|
||||||
|
pid = self.pid_of("dev")
|
||||||
|
before = (self.runtime("dev") / "svc.env").read_bytes()
|
||||||
|
loose = self.make_cred("loose.env", 0o644)
|
||||||
|
result = self.svc("dev", "restart", check=False,
|
||||||
|
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(loose)))
|
||||||
|
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||||
|
self.assertIn("权限过宽", result.stdout)
|
||||||
|
self.assertIn(str(loose), result.stdout)
|
||||||
|
self.assertEqual(self.pid_of("dev"), pid)
|
||||||
|
self.assertTrue(self.pid_alive(pid), "拒绝时不得停掉运行中的服务")
|
||||||
|
self.assertEqual((self.runtime("dev") / "svc.env").read_bytes(), before)
|
||||||
|
self.assert_no_secret("dev", result.stdout)
|
||||||
|
|
||||||
|
def test_sa_missing_file_refused_before_stop(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_ok("dev")
|
||||||
|
pid = self.pid_of("dev")
|
||||||
|
result = self.svc("dev", "restart", check=False,
|
||||||
|
env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(self.secrets / "nope.env")))
|
||||||
|
self.assertNotEqual(result.returncode, 0, result.stdout)
|
||||||
|
self.assertIn("nope.env", result.stdout)
|
||||||
|
self.assertTrue(self.pid_alive(pid))
|
||||||
|
self.assertEqual(self.pid_of("dev"), pid)
|
||||||
|
|
||||||
|
def test_sa_empty_value_clears_key(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_with_cred("dev")
|
||||||
|
self.assertIsNotNone(self.kv("dev", "superadmin_envfile"))
|
||||||
|
self.fresh_trace()
|
||||||
|
self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE=""))
|
||||||
|
self.assertIsNone(self.kv("dev", "superadmin_envfile"))
|
||||||
|
self.assertEqual(self.served_envfile(), "")
|
||||||
|
self.fresh_trace()
|
||||||
|
self.svc("dev", "restart")
|
||||||
|
self.assertEqual(self.served_envfile(), UNSET)
|
||||||
|
|
||||||
|
def test_sa_explicit_environment_overrides_record(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_with_cred("dev")
|
||||||
|
self.fresh_trace()
|
||||||
|
r = self.svc("dev", "restart", env=self.env_with(SUPER_ADMIN_OWNER="op"))
|
||||||
|
self.assertEqual(self.served_envfile(), UNSET, "显式凭据环境优先,不回落登记路径")
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), self.cred)
|
||||||
|
other = self.make_cred("other.env", 0o600)
|
||||||
|
self.fresh_trace()
|
||||||
|
self.svc("dev", "restart", env=self.env_with(AGENT_SYSTEM_ENV_FILE=str(other)))
|
||||||
|
self.assert_same_path(self.served_envfile(), other)
|
||||||
|
self.assert_same_path(self.kv("dev", "superadmin_envfile"), other)
|
||||||
|
self.assert_no_secret("dev", r.stdout)
|
||||||
|
|
||||||
|
def test_sa_recorded_file_gone_warns_and_skips(self):
|
||||||
|
self.seed_agent("dev", tsr.free_fixture_port())
|
||||||
|
self.start_with_cred("dev")
|
||||||
|
self.cred.unlink()
|
||||||
|
self.fresh_trace()
|
||||||
|
result = self.svc("dev", "restart")
|
||||||
|
self.assertIn("已不存在", result.stdout)
|
||||||
|
self.assertEqual(self.served_envfile(), UNSET)
|
||||||
|
|
||||||
|
|
||||||
|
# 只跑本文件用例;继承来的自恢复用例由 tests/test_svc_recover.py 负责。
|
||||||
|
for _name in dir(tsr.SvcAutoRecoverTests):
|
||||||
|
if _name.startswith("test_") and _name not in SuperAdminEnvfileTests.__dict__:
|
||||||
|
setattr(SuperAdminEnvfileTests, _name, None)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
unittest.main()
|
||||||
Reference in New Issue
Block a user