소스 검색

security: 凭据链审计——git token 无法由 sessionToken 自举(实测Gogs API不识别网页session)+移除硬编码密码

- clone-runner.mjs: ensureRepo() 移除硬编码 fmode:fmgo Basic Auth,改用第1-4级解析出的 token (Authorization: token header)
- token 缺失时明确报错并给出一次性初始化命令,不再静默失败
- README/SKILL.md: 写明第0级审计结论——Gogs 网页 session (i_like_gogs cookie) 仅网页 UI 有效、API 无 sessionToken→git token 签发端点,git token 需一次性初始化(附命令)
- version 0.1.0 → 0.1.1

Co-Authored-By: Claude Code <noreply@anthropic.com>
liuyuyang 1 주 전
부모
커밋
e708e05620
4개의 변경된 파일39개의 추가작업 그리고 8개의 파일을 삭제
  1. 18 1
      README.md
  2. 1 1
      package.json
  3. 3 2
      skills/fmode-agent-clone/SKILL.md
  4. 17 4
      skills/fmode-agent-clone/scripts/clone-runner.mjs

+ 18 - 1
README.md

@@ -51,7 +51,23 @@ node ... sync --name yuyang --level L1,L2
 node ... restore --repo https://git.fmode.cn/<user>/agent-yuyang.git --into /opt/data
 ```
 
-## 凭据(4 级解析,同 skill-storage 链)
+## 凭据(第0级审计结论 + 4 级解析)
+
+**第0级审计结论:git token 无法由 sessionToken 自举。** 经实测(2026-09):
+- Gogs 网页 session(cookie `i_like_gogs`)仅对网页 UI 有效,Gogs API 层不识别;
+- Gogs API 无"sessionToken → git token"签发端点(token 创建本身要求 Basic Auth 或已有 token)。
+
+因此 **git token 需要一次性初始化**(一条命令,之后永久使用):
+
+```bash
+# 一次性创建 git token(用网页登录密码,非 sessionToken)
+curl -u "<你的用户名>:<你的密码>" -X POST \
+  https://git.fmode.cn/api/v1/users/<你的用户名>/tokens \
+  -H "Content-Type: application/json" -d '{"name":"agent-clone"}'
+# → 响应里的 sha1 即 git token,写入 ~/.fmode/config.json 的 gitToken 字段
+```
+
+初始化后,运行器按以下 4 级链解析(第1级起与 skill-storage 同构):
 
 1. `FMODE_GIT_TOKEN` 环境变量
 2. `~/.fmode/config.json` → `gitToken` / `gitUser`
@@ -59,6 +75,7 @@ node ... restore --repo https://git.fmode.cn/<user>/agent-yuyang.git --into /opt
 4. 平台凭据链(git credential helper / ssh key)
 
 > **仓库里零密钥**:token/密码永远不进仓;`credentials-map.md` 只写"哪个密钥存在哪"。
+> token 缺失时运行器明确报错并给出初始化命令,不再使用任何硬编码凭据。
 
 ## License
 

+ 1 - 1
package.json

@@ -1,6 +1,6 @@
 {
   "name": "fmode-agent-clone",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "license": "MIT",
   "repository": {
     "type": "git",

+ 3 - 2
skills/fmode-agent-clone/SKILL.md

@@ -27,9 +27,10 @@ node <skill_dir>/scripts/clone-runner.mjs sync --name yuyang --level L1,L2
 node <skill_dir>/scripts/clone-runner.mjs restore --repo <url> --into /opt/data
 ```
 
-## 凭据(4 级解析)
+## 凭据(第0级审计 + 4 级解析)
 
-`FMODE_GIT_TOKEN` → `~/.fmode/config.json`(gitToken/gitUser) → 项目 .fmode → git credential helper。**零密钥入库。**
+- 第0级:**sessionToken 无法自举 git token**(Gogs API 不识别网页 session,也无签发端点)→ git token 需一次性初始化:`curl -u "user:pass" -X POST https://git.fmode.cn/api/v1/users/<user>/tokens -d '{"name":"agent-clone"}'`(详见 README)。
+- 第1-4级:`FMODE_GIT_TOKEN` → `~/.fmode/config.json`(gitToken/gitUser) → 项目 .fmode → git credential helper。**零密钥入库。**
 
 ## 详细文档
 

+ 17 - 4
skills/fmode-agent-clone/scripts/clone-runner.mjs

@@ -14,7 +14,10 @@ import { execSync } from 'child_process';
 
 const HOME = os.homedir();
 
-/** 4 级凭据解析: env → ~/.fmode/config.json → ./.fmode/config.json → git credential helper */
+/** 5 级凭据解析(第0级为文档级自举说明):
+ * 第0级: sessionToken —— ⚠️ Gogs API 不接受 sessionToken(网页 cookie i_like_gogs 仅网页会话有效),
+ *        且 Gogs 无"sessionToken 换 git token"的签发端点 → git token 必须一次性初始化(见 README)。
+ * 第1-4级: env → ~/.fmode/config.json → ./.fmode/config.json → git credential helper */
 export function resolveGit() {
   const out = { user: null, token: null, apiBase: 'https://git.fmode.cn/api/v1' };
   if (process.env.FMODE_GIT_TOKEN) { out.token = process.env.FMODE_GIT_TOKEN; }
@@ -50,10 +53,16 @@ function sh(cmd, cwd) {
 
 function ensureRepo(name, g) {
   const url = `${g.apiBase}/admin/users/${g.user}/repos`;
+  if (!g.token) {
+    console.error('git token 缺失——Gogs API 不接受 sessionToken 自举,请先完成一次性初始化(见 README「凭据」章节):');
+    console.error(`  curl -u "<user>:<password>" -X POST "${g.apiBase.replace('/api/v1', '')}/api/v1/users/<user>/tokens" -H "Content-Type: application/json" -d '{"name":"agent-clone"}'`);
+    return false;
+  }
   try {
-    const r = sh(`curl -s --max-time 20 -X POST -u "${g.user === 'fmode' ? 'fmode:fmgo' : 'fmode:fmgo'}" -H "Content-Type: application/json" -d '{"name":"${name}","private":true}' "${url}"`);
+    const r = sh(`curl -s --max-time 20 -X POST -H "Authorization: token ${g.token}" -H "Content-Type: application/json" -d '{"name":"${name}","private":true}' "${url}"`);
     return /"name"\s*:\s*"/.test(r) || /already/i.test(r);
-  } catch { return false; }
+  } catch { return false;
+  }
 }
 
 function main() {
@@ -89,7 +98,11 @@ function main() {
     if (dry) { console.log('[dry] 同步项:', manifest.length); return; }
     // 4) commit+push
     try { sh(`git add -A && git -c user.name="${g.user}" -c user.email="${g.user}@fmode.cn" commit -m "clone sync ${new Date().toISOString().slice(0,16)}"`, work); } catch {}
-    try { ensureRepo(repoName, g); sh(`git push -u origin main`, work); console.log('✅ 已同步', manifest.length, '项 →', repoName); }
+    if (!g.token) {
+      console.error('push 失败: git token 缺失(FMODE_GIT_TOKEN 或 ~/.fmode/config.json 的 gitToken)。Gogs 不支持 sessionToken 自举,请先一次性初始化 token(见 README「凭据」章节)。');
+      return;
+    }
+    try { if (ensureRepo(repoName, g)) { sh(`git push -u origin main`, work); console.log('✅ 已同步', manifest.length, '项 →', repoName); } }
     catch (e) { console.error('push 失败:', String(e).slice(0, 200)); }
     return;
   }