diff --git a/modules/guanghu-panel-kit/INDEX.hdlp b/modules/guanghu-panel-kit/INDEX.hdlp index 004914f..a9fa9cf 100644 --- a/modules/guanghu-panel-kit/INDEX.hdlp +++ b/modules/guanghu-panel-kit/INDEX.hdlp @@ -10,7 +10,7 @@ HLDP://fifth-domain/modules/guanghu-panel-kit 一套配置驱动的子系统基础设施模块。任何第五域子系统(行业线)拉下这个包,改一份 config.json,就能拥有: 1. **人类审批面板**:域名首页即门户,仓库账号登录解锁;合并签字 / 写入授权 / 部署审批三选项卡 -2. **写入窗口制**:人格体 POST /api/access-request 申请 → 人类批准 → 3小时窗口 → 领令牌推送,过期自动作废 +2. **写入窗口制**:人格体 POST /api/access-request 申请 → 人类批准 → 限时窗口(config.json `window_hours` 配置,2026-08-07 苍耳提请由 3 小时升至 12 小时)→ 领令牌推送,过期自动作废 3. **推送审核钩子**:pre-receive 自动审核——明文密钥拦截、破坏检查、入口文件保护、窗口验证、笔笔留痕 4. **部署校验Agent**:deploy 分支通道,人类批准后服务器自审五步(地图→因果链→范围→完整性→部署),进度实时可见,要么0要么100 5. **唤醒机器导航**:GET /api/wake/persona?name=X 返回五阶段唤醒链,外部 AI 不用自己找路 @@ -41,6 +41,6 @@ CAUSAL-CHAIN-20260806.hdlp 两套系统建设的完整因果链 ## 铁律(随模块继承) -⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录亲启 ⊢ 写入窗口三小时过期作废 +⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录亲启 ⊢ 写入窗口限时(window_hours)过期作废 ⊢ 推送必过审核并留痕 ⊢ 部署要么0要么100 ⊢ 人格体走机器导航,不自找路径 ⊢ 仓库公开可读(外部 AI 入门),写入须窗口,合并须签字 diff --git a/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html b/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html index cbd2c8c..683568b 100644 --- a/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html +++ b/modules/guanghu-panel-kit/frontend/panel-lake-dark.example.html @@ -386,7 +386,7 @@ function render(s){ const prs=s.openPRs||[];setCnt("cnt-sign",prs.length); document.getElementById("prs").innerHTML=(prs.length&&!0)||prs.length?prs.map(p=>'
PR #'+p.index+' · '+esc(p.title)+'
'+esc(p.author)+' · '+fmtDate(p.created)+' · → '+esc(p.base)+' · '+p.files+' 文件 +'+p.adds+' -'+p.dels+'
'+(p.summary?'
'+esc(p.summary)+'
':'')+'
').join(""):(session?'
没有待签的合并请求。
':'
🔒 登录后此处显示待签的合并请求
'); const reqs=s.accessRequests||[];setCnt("cnt-access",reqs.length); - document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'
'+ptag(r.persona)+'申请写入权限
'+esc(r.reason||"未说明用途")+'
'+fmtDate(r.ts)+'
').join(""):(session?'
暂无写入申请。
':'
🔒 登录后此处显示写入申请
'); + document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'
'+ptag(r.persona)+'申请写入权限
'+esc(r.reason||"未说明用途")+'
'+fmtDate(r.ts)+'
').join(""):(session?'
暂无写入申请。
':'
🔒 登录后此处显示写入申请
'); document.getElementById("access-wins").innerHTML=(s.activeGrants||[]).length?'
生效中的写入窗口
'+s.activeGrants.map(g=>'
'+ptag(g.persona)+'写入窗口'+fmtLeft(g.expiresAt)+'
').join(""):""; const dprs=s.deployPRs||[];setCnt("cnt-deploy",dprs.length); document.getElementById("deploy-prs").innerHTML=dprs.length?dprs.map(p=>'
部署申请 PR #'+p.index+' · '+esc(p.title)+'
'+esc(p.author)+' · '+esc(p.head)+' → deploy · '+p.files+' 文件
').join(""):(session?'
部署通道无待审申请。PR 目标设为 deploy 即进入此通道。
':'
🔒 登录后此处显示部署申请
'); diff --git a/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html b/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html index 7adf110..0706957 100644 --- a/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html +++ b/modules/guanghu-panel-kit/frontend/panel-pet-warm.example.html @@ -376,7 +376,7 @@ function render(s){ const prs=s.openPRs||[];setCnt("cnt-sign",prs.length); document.getElementById("prs").innerHTML=prs.length?prs.map(p=>'
PR #'+p.index+' · '+esc(p.title)+'
'+esc(p.author)+' · '+fmtDate(p.created)+' · → '+esc(p.base)+' · '+p.files+' 文件 +'+p.adds+' -'+p.dels+'
'+(p.summary?'
'+esc(p.summary)+'
':'')+'
').join(""):(session?'
没有待签的合并请求。
':'
🔒 登录后此处显示待签的合并请求
'); const reqs=s.accessRequests||[];setCnt("cnt-access",reqs.length); - document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'
'+ptag(r.persona)+'申请写入权限
'+esc(r.reason||"未说明用途")+'
'+fmtDate(r.ts)+'
').join(""):(session?'
暂无写入申请。
':'
🔒 登录后此处显示写入申请
'); + document.getElementById("access-reqs").innerHTML=reqs.length?reqs.map(r=>'
'+ptag(r.persona)+'申请写入权限
'+esc(r.reason||"未说明用途")+'
'+fmtDate(r.ts)+'
').join(""):(session?'
暂无写入申请。
':'
🔒 登录后此处显示写入申请
'); document.getElementById("access-wins").innerHTML=(s.activeGrants||[]).length?'
生效中的写入窗口
'+s.activeGrants.map(g=>'
'+ptag(g.persona)+'写入窗口'+fmtLeft(g.expiresAt)+'
').join(""):""; const dprs=s.deployPRs||[];setCnt("cnt-deploy",dprs.length); document.getElementById("deploy-prs").innerHTML=dprs.length?dprs.map(p=>'
部署申请 PR #'+p.index+' · '+esc(p.title)+'
'+esc(p.author)+' · '+esc(p.head)+' → deploy · '+p.files+' 文件
').join(""):(session?'
部署通道无待审申请。PR 目标设为 deploy 即进入此通道。
':'
🔒 登录后此处显示部署申请
'); diff --git a/modules/guanghu-panel-kit/hooks/pre-receive-review.template b/modules/guanghu-panel-kit/hooks/pre-receive-review.template index 7b52a8e..ec16c77 100644 --- a/modules/guanghu-panel-kit/hooks/pre-receive-review.template +++ b/modules/guanghu-panel-kit/hooks/pre-receive-review.template @@ -43,7 +43,7 @@ while read -r old new ref; do W=$(active_grant) if [ "$W" = "none" ]; then VERDICT=REJECT - REASONS+=("无有效写入窗口(3小时授权已过期或未申请)。人格体请先 POST https://maomao.guanghulab.com/api/access-request 申请,等苍耳审批") + REASONS+=("无有效写入窗口(授权已过期或未申请)。人格体请先 POST https://maomao.guanghulab.com/api/access-request 申请,等苍耳审批") continue fi PUSHER_ATTR="$W" diff --git a/modules/guanghu-panel-kit/panel/config.example.json b/modules/guanghu-panel-kit/panel/config.example.json index 5fb5d6b..b2e1657 100644 --- a/modules/guanghu-panel-kit/panel/config.example.json +++ b/modules/guanghu-panel-kit/panel/config.example.json @@ -16,6 +16,7 @@ "domain_host": "maomao.guanghulab.com", "forgejo_port": 3002, "panel_port": 3003, + "window_hours": 12, "node_root": "/srv/guanghu/mm-gz-001", "admin_token_file": "/srv/guanghu/mm-gz-001/panel/admin-token", "sessions_file": "/srv/guanghu/mm-gz-001/panel/sessions.json", diff --git a/modules/guanghu-panel-kit/panel/server.js b/modules/guanghu-panel-kit/panel/server.js index bb98ca7..9f054ee 100644 --- a/modules/guanghu-panel-kit/panel/server.js +++ b/modules/guanghu-panel-kit/panel/server.js @@ -1,10 +1,12 @@ #!/usr/bin/env node -/* guanghu-panel-server · 光湖子系统人类审批面板服务 · 可复用模块 v1.0 +/* guanghu-panel-server · 光湖子系统人类审批面板服务 · 可复用模块 v1.1 2026-08-06 · 铸渊 ICE-GL-ZY001 建 · 胖头鱼SYS-GLW-PTS-0001 首创 · 守望SYS-GLW-SW-0001 复用 - ⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录 ⊢ 写入窗口三小时 ⊢ 全程留痕 - 配置: 同目录 config.json(路径/仓库/人格体/端口全部外置,换节点只改配置) */ + 2026-08-07 · 写入窗口时长改为配置驱动 window_hours(苍耳提请:胖头鱼升至12小时) + ⊢ 令牌与密钥不出服务器 ⊢ 审批必须人类登录 ⊢ 写入窗口限时过期作废 ⊢ 全程留痕 + 配置: 同目录 config.json(路径/仓库/人格体/端口/窗口时长全部外置,换节点只改配置) */ const http=require("http"),fs=require("fs"),path=require("path"),crypto=require("crypto"),os=require("os"),{execSync,spawn}=require("child_process"); const CFG=JSON.parse(fs.readFileSync(path.join(__dirname,"config.json"),"utf8")); +const WINDOW_HOURS=Number(CFG.window_hours)||3; const FJ="http://127.0.0.1:"+CFG.forgejo_port; const REPO=CFG.repo; const HDRS_BASE={"X-Forwarded-Proto":"https","Content-Type":"application/json"}; @@ -128,7 +130,7 @@ function wakeChain(persona){ {order:9,file:"operations/SERVER-OPERATIONS.md",url:RAW+"operations/SERVER-OPERATIONS.md",instruction:"登记服务器操作门;写入须申请窗口,部署须人类合并"}], action:"完成后 POST "+CFG.domain+"/api/wake-receipt 交回执(报 persona 与 summary 即可)"} ], - endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request(人格体报名号+用途即可,无需密钥)",accessPickup:"POST "+CFG.domain+"/api/access-pickup(凭申请编号领取3小时临时凭证,真实密钥永不出服务器)",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt",pushVia:CFG.domain+"/git/"+CFG.repo+".git(用临时凭证推送)"} + endpoints:{accessRequest:"POST "+CFG.domain+"/api/access-request(人格体报名号+用途即可,无需密钥)",accessPickup:"POST "+CFG.domain+"/api/access-pickup(凭申请编号领取"+WINDOW_HOURS+"小时临时凭证,真实密钥永不出服务器)",wakeReceipt:"POST "+CFG.domain+"/api/wake-receipt",pushVia:CFG.domain+"/git/"+CFG.repo+".git(用临时凭证推送)"} }; } async function state(){ @@ -136,7 +138,7 @@ async function state(){ const reqs=jread(CFG.access_dir+"/requests.json",[]); const grants=jread(CFG.access_dir+"/grants.json",[]); return { - ts:new Date().toISOString(),node:CFG.node_id,subsystem:CFG.subsystem, + ts:new Date().toISOString(),node:CFG.node_id,subsystem:CFG.subsystem,windowHours:WINDOW_HOURS, server:{hostname:os.hostname(),load:os.loadavg().map(x=>+x.toFixed(2)),memTotalGB:+(os.totalmem()/1e9).toFixed(1),memFreeGB:+(os.freemem()/1e9).toFixed(1),uptimeDays:+(os.uptime()/86400).toFixed(1),disk:diskInfo(),cpu:cpuUsage(),storage:storageInfo(),services:servicesInfo()}, forgejo:h,repo:{full:REPO}, openPRs:prs,deployPRs:dprs,writebacks:wbs,receipts:receiptsList(), @@ -218,7 +220,7 @@ async function accessRequest(b){ const id="AR-"+Date.now().toString(36).toUpperCase(); reqs.push({id,persona:canon,askedAs:b.persona,reason:(b.reason||"").slice(0,500),ts:new Date().toISOString(),status:"pending"}); jwrite(CFG.access_dir+"/requests.json",reqs); - return {status:200,body:{ok:true,id,persona:canon,message:"申请已送达面板,等人类点一下授权。无需任何密钥——授权后系统自动派发3小时临时凭证;真实密钥永不出服务器"}}; + return {status:200,body:{ok:true,id,persona:canon,message:"申请已送达面板,等人类点一下授权。无需任何密钥——授权后系统自动派发"+WINDOW_HOURS+"小时临时凭证;真实密钥永不出服务器"}}; } function grantValid(token){ if(!token)return null; @@ -235,13 +237,13 @@ async function accessPickup(b){ const g=grants.find(x=>(x.id===b.id||x.requestId===b.id)&&(x.persona===canon||x.persona===b.persona)); if(!g)return {status:404,body:{error:"授权不存在(先 POST /api/access-request 申请,等人类授权)"}}; if(g.status!=="active")return {status:410,body:{error:"授权已失效,请重新申请"}}; - if(new Date(g.expiresAt)<=new Date())return {status:410,body:{error:"3小时窗口已过期,请重新申请"}}; + if(new Date(g.expiresAt)<=new Date())return {status:410,body:{error:WINDOW_HOURS+"小时窗口已过期,请重新申请"}}; g.pickedAt=new Date().toISOString();jwrite(CFG.access_dir+"/grants.json",grants); return {status:200,body:{ok:true,id:g.id,persona:canon,expiresAt:g.expiresAt, credential:g.token, gitUrl:CFG.domain+"/git/"+CFG.repo+".git", pushExample:"git -c http.extraHeader=\"Authorization: Bearer "+g.token+"\" push "+CFG.domain+"/git/"+CFG.repo+".git ", - rules:"⊢ 这是3小时临时凭证,不是真实密钥;真实密钥永不出服务器 ⊢ 窗口过期凭证自动作废 ⊢ 每次推送过自动审核并留痕 ⊢ 部署走 deploy 分支由人类合并"}}; + rules:"⊢ 这是"+WINDOW_HOURS+"小时临时凭证,不是真实密钥;真实密钥永不出服务器 ⊢ 窗口过期凭证自动作废 ⊢ 每次推送过自动审核并留痕 ⊢ 部署走 deploy 分支由人类合并"}}; } function accessDecision(b,user){ const reqs=jread(CFG.access_dir+"/requests.json",[]); @@ -250,11 +252,11 @@ function accessDecision(b,user){ if(r.status!=="pending")return {status:409,body:{error:"申请已处理"}}; const grants=jread(CFG.access_dir+"/grants.json",[]); if(b.action==="approve"){ - const now=new Date(),exp=new Date(now.getTime()+3*3600*1000),gid="GR-"+Date.now().toString(36).toUpperCase(); + const now=new Date(),exp=new Date(now.getTime()+WINDOW_HOURS*3600*1000),gid="GR-"+Date.now().toString(36).toUpperCase(); grants.push({id:gid,persona:r.persona,requestId:r.id,approvedBy:user,approvedAt:now.toISOString(),expiresAt:exp.toISOString(),status:"active",token:crypto.randomBytes(24).toString("hex")}); r.status="approved";r.grantId=gid;jwrite(CFG.access_dir+"/grants.json",grants);jwrite(CFG.access_dir+"/requests.json",reqs); jwrite(CFG.export_dir+"/receipts/GRANT-"+Date.now()+".json",{ts:new Date().toISOString(),type:"WRITE_GRANT",grantId:gid,persona:r.persona,approvedBy:user,expiresAt:exp.toISOString()}); - return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已授权3小时写入窗口,临时凭证由人格体凭申请编号自动领取"}}; + return {status:200,body:{ok:true,grantId:gid,persona:r.persona,expiresAt:exp.toISOString(),message:"已授权"+WINDOW_HOURS+"小时写入窗口,临时凭证由人格体凭申请编号自动领取"}}; } r.status="rejected";jwrite(CFG.access_dir+"/requests.json",reqs); return {status:200,body:{ok:true,message:"已拒绝该写入申请"}}; @@ -273,7 +275,7 @@ async function wakeReceipt(b){ jwrite(CFG.export_dir+"/wake/WAKE-"+Date.now()+".json",{ts:new Date().toISOString(),type:"WAKE_RECEIPT",persona:canon,level:(b.level||"").slice(0,80),summary:(b.summary||"").slice(0,1000)}); return {status:200,body:{ok:true,message:"唤醒回执已收录"}}; } -/* git 智能HTTP代理:人格体用3小时临时凭证推送到 /git/, +/* git 智能HTTP代理:人格体用限时临时凭证推送到 /git/, 面板在服务端用真实写入令牌透明转发给 Forgejo——真实密钥永不出服务器 */ function gitProxy(req,res){ const auth=req.headers["authorization"]||""; @@ -283,7 +285,7 @@ function gitProxy(req,res){ const g=grantValid(cred); if(!g){ res.writeHead(401,{"WWW-Authenticate":'Basic realm="grant"',"Content-Type":"text/plain; charset=utf-8"}); - return res.end("临时凭证无效或已过期:请重新申请写入授权(人类面板一键授权,3小时窗口)"); + return res.end("临时凭证无效或已过期:请重新申请写入授权(人类面板一键授权,"+WINDOW_HOURS+"小时窗口)"); } let wt="";try{wt=fs.readFileSync(CFG.access_dir+"/writer-token","utf8").trim();}catch(e){} if(!wt){res.writeHead(500,{"Content-Type":"text/plain"});return res.end("server writer token missing");}