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:
copilot-swe-agent[bot] 2026-03-25 08:54:40 +00:00
parent 721974ca76
commit 6fab64b1b9
4 changed files with 143 additions and 5 deletions

View File

@ -43,6 +43,21 @@
] ]
}, },
"deploymentFlow": { "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": [ "stages": [
{ {
"stage": 1, "stage": 1,

View File

@ -1,8 +1,9 @@
/** /**
* 部署授权流程路由 · Phase 8 * 部署授权流程路由 · Phase 8 + S5 直通规则
* *
* 天眼审核通过后 → 推送授权请求给授权人 → 授权人确认/拒绝 → 自动发布 * 部署流分两条路径:
* 系统绝不自动跳过授权步骤。没有人类点确认,就不发布。 * A) 冰朔直通:TCS-0002 或 ZY- 指令签发 → 直接部署到正式站,跳过预览/天眼/授权
* B) 开发者流程:天眼审核 → 推送授权请求给授权人 → 确认/拒绝 → 自动发布
* *
* POST /api/approval/request — 创建授权请求(天眼/系统内部调用) * POST /api/approval/request — 创建授权请求(天眼/系统内部调用)
* POST /api/approval/:id/decide — 授权人确认/拒绝 * POST /api/approval/:id/decide — 授权人确认/拒绝
@ -25,6 +26,36 @@ var approversConfig = require('../config/approvers.json');
// ====== 内存中的授权记录(生产环境可迁移到持久化存储)====== // ====== 内存中的授权记录(生产环境可迁移到持久化存储)======
var approvalStore = new Map(); 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) { 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 approvers = getApprovers(channel);
var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6); var approvalId = 'APPROVAL-' + Date.now() + '-' + Math.random().toString(36).substring(2, 6);

View File

@ -113,11 +113,32 @@ function getDualLineConfig() {
return autonomyRules.dualLineArchitecture; 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 = { module.exports = {
checkAutonomyCompliance: checkAutonomyCompliance, checkAutonomyCompliance: checkAutonomyCompliance,
detectInteractionMode: detectInteractionMode, detectInteractionMode: detectInteractionMode,
logAutonomousAction: logAutonomousAction, logAutonomousAction: logAutonomousAction,
getDeploymentStages: getDeploymentStages, getDeploymentStages: getDeploymentStages,
getDualLineConfig: getDualLineConfig, getDualLineConfig: getDualLineConfig,
writeAutonomyLog: writeAutonomyLog writeAutonomyLog: writeAutonomyLog,
isDirectDeploySource: isDirectDeploySource
}; };

View File

@ -34,7 +34,9 @@ function startWatchdog() {
var activeLocks = executionLock.getActiveLocks(); var activeLocks = executionLock.getActiveLocks();
var now = Date.now(); 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; if (lock.state !== 'running') continue;