feat: implement S5 冰朔直通部署规则 (direct-deploy bypass)
S5 Rule: When instructions come from 冰朔 (TCS-0002) or are signed by trusted signers (AG-SY-01/TCS-0002/ICE-0002) with ZY- prefix, deployment goes directly to production bypassing preview/SkyEye/approval. - Add isDirectDeploySource() in autonomy-engine.js (single source of truth) - Add direct-deploy bypass in approval.js POST /request endpoint - Add directDeployRule config in autonomy-rules.json - Add deploy audit log (deploy-YYYY-MM-DD.jsonl) for traceability - Developer (Level 0-2) changes still follow full S2 approval flow - Fix watchdog for-of destructuring to match codebase var style Co-authored-by: qinfendebingshuo <207279273+qinfendebingshuo@users.noreply.github.com> Agent-Logs-Url: https://github.com/qinfendebingshuo/guanghulab/sessions/6ee737eb-684b-44b8-aeaa-f73eb7c6f9c2
This commit is contained in:
parent
721974ca76
commit
6fab64b1b9
|
|
@ -43,6 +43,21 @@
|
|||
]
|
||||
},
|
||||
"deploymentFlow": {
|
||||
"directDeployRule": {
|
||||
"id": "S5",
|
||||
"description": "冰朔直通部署规则:冰朔签发或口头下达的所有指令,执行完毕后直接部署到正式站",
|
||||
"conditions": [
|
||||
"TCS-0002 发起的请求",
|
||||
"ZY- 开头的指令编号,且签发者为霜砚(AG-SY-01)或冰朔(TCS-0002)"
|
||||
],
|
||||
"bypass": ["预览站", "天眼审核", "肥猫/桔子授权"],
|
||||
"auditRequired": true,
|
||||
"reason": "冰朔本人就是系统最高权限(Level 3),冰朔说话 = 最终授权"
|
||||
},
|
||||
"developerFlow": {
|
||||
"description": "开发者(Level 1-2)提交的变更走完整S2审批流程",
|
||||
"stages": ["预览站部署", "天眼审核", "授权人审批", "自动发布"]
|
||||
},
|
||||
"stages": [
|
||||
{
|
||||
"stage": 1,
|
||||
|
|
|
|||
|
|
@ -1,8 +1,9 @@
|
|||
/**
|
||||
* 部署授权流程路由 · Phase 8
|
||||
* 部署授权流程路由 · Phase 8 + S5 直通规则
|
||||
*
|
||||
* 天眼审核通过后 → 推送授权请求给授权人 → 授权人确认/拒绝 → 自动发布
|
||||
* 系统绝不自动跳过授权步骤。没有人类点确认,就不发布。
|
||||
* 部署流分两条路径:
|
||||
* A) 冰朔直通:TCS-0002 或 ZY- 指令签发 → 直接部署到正式站,跳过预览/天眼/授权
|
||||
* B) 开发者流程:天眼审核 → 推送授权请求给授权人 → 确认/拒绝 → 自动发布
|
||||
*
|
||||
* POST /api/approval/request — 创建授权请求(天眼/系统内部调用)
|
||||
* POST /api/approval/:id/decide — 授权人确认/拒绝
|
||||
|
|
@ -25,6 +26,36 @@ var approversConfig = require('../config/approvers.json');
|
|||
// ====== 内存中的授权记录(生产环境可迁移到持久化存储)======
|
||||
var approvalStore = new Map();
|
||||
|
||||
// ====== S5 直通部署判断 ======
|
||||
|
||||
var autonomyEngine = require('../services/autonomy-engine');
|
||||
|
||||
/**
|
||||
* 判断部署请求是否来自冰朔直通路径(委托给 autonomy-engine 单一来源)
|
||||
*/
|
||||
function isDirectDeploySource(user, body) {
|
||||
var instructionId = body.instructionId || body.deployId || '';
|
||||
var signedBy = body.signedBy || '';
|
||||
return autonomyEngine.isDirectDeploySource(user.devId, instructionId, signedBy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 写入部署日志(直通和审批部署共用,保持可追溯性)
|
||||
*/
|
||||
function writeDeployLog(entry) {
|
||||
var logDir = process.env.AUDIT_LOG_DIR || path.join(__dirname, '../../logs/audit');
|
||||
try {
|
||||
fs.mkdirSync(logDir, { recursive: true });
|
||||
var today = new Date().toISOString().split('T')[0];
|
||||
var logFile = path.join(logDir, 'deploy-' + today + '.jsonl');
|
||||
fs.appendFile(logFile, JSON.stringify(entry) + '\n', function(err) {
|
||||
if (err) console.error('部署日志写入失败:', err.message);
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('部署日志写入失败:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// ====== 辅助函数 ======
|
||||
|
||||
/**
|
||||
|
|
@ -60,6 +91,7 @@ router.use(auditMiddleware.auditLog);
|
|||
|
||||
/**
|
||||
* 创建授权请求(天眼审核通过后由系统调用)
|
||||
* S5: 冰朔直通路径 → 跳过审批,直接部署到正式站
|
||||
*/
|
||||
router.post('/request', function(req, res) {
|
||||
// 仅系统内部或管理员可创建授权请求
|
||||
|
|
@ -85,6 +117,74 @@ router.post('/request', function(req, res) {
|
|||
});
|
||||
}
|
||||
|
||||
// ====== S5 直通判断 ======
|
||||
if (isDirectDeploySource(req.user, body)) {
|
||||
// 冰朔/霜砚指令 → 直接部署,不走审批流程
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
channel: channel,
|
||||
source: req.user.devId,
|
||||
signedBy: body.signedBy || req.user.devId,
|
||||
instructionId: body.instructionId || deployId,
|
||||
reason: 'S5 冰朔直通部署规则:冰朔签发或口头下达的指令直接部署到正式站',
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
|
||||
// 直接触发正式站部署
|
||||
var githubService;
|
||||
try {
|
||||
githubService = require('../services/github');
|
||||
} catch (_) {
|
||||
githubService = null;
|
||||
}
|
||||
|
||||
if (githubService && githubService.triggerWorkflow) {
|
||||
githubService.triggerWorkflow('deploy-to-server.yml', {
|
||||
module: module,
|
||||
deploy_id: deployId,
|
||||
approved_by: req.user.devId,
|
||||
target: 'production',
|
||||
direct_deploy: 'true'
|
||||
}).then(function() {
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy_triggered',
|
||||
deployId: deployId,
|
||||
module: module,
|
||||
source: req.user.devId,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}).catch(function(err) {
|
||||
console.error('直通部署触发失败:', err.message);
|
||||
writeDeployLog({
|
||||
action: 'direct_deploy_failed',
|
||||
deployId: deployId,
|
||||
error: err.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
directDeploy: true,
|
||||
deployId: deployId,
|
||||
reply: '🚀 冰朔直通部署:' + module + ' 已直接触发正式站(guanghulab.com)部署。' +
|
||||
'不经预览站、不经天眼审核、不经授权审批。部署日志已记录。'
|
||||
});
|
||||
}
|
||||
|
||||
// ====== 开发者流程:走完整 S2 审批 ======
|
||||
|
||||
if (!deployId || !module) {
|
||||
return res.status(400).json({
|
||||
error: true,
|
||||
code: 'MISSING_FIELDS',
|
||||
reply: '❌ 缺少必要字段:deployId, module'
|
||||
});
|
||||
}
|
||||
|
||||
var approvers = getApprovers(channel);
|
||||
var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6);
|
||||
|
||||
|
|
|
|||
|
|
@ -113,11 +113,32 @@ function getDualLineConfig() {
|
|||
return autonomyRules.dualLineArchitecture;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断请求是否来自冰朔直通部署路径 (S5)
|
||||
* @param {string} devId - 开发者编号
|
||||
* @param {string} instructionId - 指令编号
|
||||
* @param {string} signedBy - 签发者编号
|
||||
* @returns {boolean}
|
||||
*/
|
||||
function isDirectDeploySource(devId, instructionId, signedBy) {
|
||||
// 冰朔本人
|
||||
if (devId === 'TCS-0002') return true;
|
||||
|
||||
// ZY- 指令 + 可信签发者
|
||||
if (instructionId && /^ZY-/.test(instructionId)) {
|
||||
var trustedSigners = ['AG-SY-01', 'TCS-0002', 'ICE-0002'];
|
||||
if (signedBy && trustedSigners.indexOf(signedBy) !== -1) return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
checkAutonomyCompliance: checkAutonomyCompliance,
|
||||
detectInteractionMode: detectInteractionMode,
|
||||
logAutonomousAction: logAutonomousAction,
|
||||
getDeploymentStages: getDeploymentStages,
|
||||
getDualLineConfig: getDualLineConfig,
|
||||
writeAutonomyLog: writeAutonomyLog
|
||||
writeAutonomyLog: writeAutonomyLog,
|
||||
isDirectDeploySource: isDirectDeploySource
|
||||
};
|
||||
|
|
|
|||
|
|
@ -34,7 +34,9 @@ function startWatchdog() {
|
|||
var activeLocks = executionLock.getActiveLocks();
|
||||
var now = Date.now();
|
||||
|
||||
for (var [devId, lock] of activeLocks) {
|
||||
for (var entry of activeLocks) {
|
||||
var devId = entry[0];
|
||||
var lock = entry[1];
|
||||
|
||||
if (lock.state !== 'running') continue;
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue