soloforge 1.1.35 → 1.1.37
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/engine/audit/evolver.d.ts.map +1 -1
- package/dist/engine/audit/evolver.js +29 -2
- package/dist/engine/audit/evolver.js.map +1 -1
- package/dist/engine/contracts/architecture_decision_workshop.d.ts +6 -1
- package/dist/engine/contracts/architecture_decision_workshop.d.ts.map +1 -1
- package/dist/engine/contracts/architecture_decision_workshop.js +5 -1
- package/dist/engine/contracts/architecture_decision_workshop.js.map +1 -1
- package/dist/engine/knowledge/knowledge_injection_boundary.d.ts.map +1 -1
- package/dist/engine/knowledge/knowledge_injection_boundary.js +7 -3
- package/dist/engine/knowledge/knowledge_injection_boundary.js.map +1 -1
- package/dist/engine/pipeline/classifier.js +6 -4
- package/dist/engine/pipeline/classifier.js.map +1 -1
- package/dist/engine/pipeline/intent_expander/knowledge.d.ts.map +1 -1
- package/dist/engine/pipeline/intent_expander/knowledge.js +17 -8
- package/dist/engine/pipeline/intent_expander/knowledge.js.map +1 -1
- package/dist/engine/pipeline/intent_expander/templates.d.ts +1 -0
- package/dist/engine/pipeline/intent_expander/templates.d.ts.map +1 -1
- package/dist/engine/pipeline/intent_expander/templates.js +73 -0
- package/dist/engine/pipeline/intent_expander/templates.js.map +1 -1
- package/dist/engine/pipeline/intent_router.d.ts +22 -1
- package/dist/engine/pipeline/intent_router.d.ts.map +1 -1
- package/dist/engine/pipeline/intent_router.js +130 -0
- package/dist/engine/pipeline/intent_router.js.map +1 -1
- package/dist/engine/pipeline/intent_signal_extractor.d.ts +2 -2
- package/dist/engine/pipeline/intent_signal_extractor.d.ts.map +1 -1
- package/dist/engine/pipeline/intent_signal_extractor.js +77 -3
- package/dist/engine/pipeline/intent_signal_extractor.js.map +1 -1
- package/dist/engine/pipeline/task_context/manager.d.ts +39 -0
- package/dist/engine/pipeline/task_context/manager.d.ts.map +1 -1
- package/dist/engine/pipeline/task_context/manager.js +293 -109
- package/dist/engine/pipeline/task_context/manager.js.map +1 -1
- package/dist/engine/pipeline/task_context/manager_setters.d.ts +23 -1
- package/dist/engine/pipeline/task_context/manager_setters.d.ts.map +1 -1
- package/dist/engine/pipeline/task_context/manager_setters.js +237 -119
- package/dist/engine/pipeline/task_context/manager_setters.js.map +1 -1
- package/dist/engine/pipeline/task_stage_detector.d.ts.map +1 -1
- package/dist/engine/pipeline/task_stage_detector.js +13 -0
- package/dist/engine/pipeline/task_stage_detector.js.map +1 -1
- package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.d.ts.map +1 -1
- package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.js +84 -2
- package/dist/engine/release/release_issue_scenario_registry/scenarios_template_contract.js.map +1 -1
- package/dist/engine/templates/explicit_asset_registry/procedures_part2.d.ts.map +1 -1
- package/dist/engine/templates/explicit_asset_registry/procedures_part2.js +17 -0
- package/dist/engine/templates/explicit_asset_registry/procedures_part2.js.map +1 -1
- package/dist/engine/workflow/legacy_type_migration.d.ts.map +1 -1
- package/dist/engine/workflow/legacy_type_migration.js +5 -0
- package/dist/engine/workflow/legacy_type_migration.js.map +1 -1
- package/dist/engine/workflow/project_stage_detector.d.ts.map +1 -1
- package/dist/engine/workflow/project_stage_detector.js +9 -7
- package/dist/engine/workflow/project_stage_detector.js.map +1 -1
- package/dist/engine/workflow/workflow_contract_registry.d.ts.map +1 -1
- package/dist/engine/workflow/workflow_contract_registry.js +24 -0
- package/dist/engine/workflow/workflow_contract_registry.js.map +1 -1
- package/dist/knowledge/index_manager.d.ts +6 -0
- package/dist/knowledge/index_manager.d.ts.map +1 -1
- package/dist/knowledge/index_manager.js +168 -55
- package/dist/knowledge/index_manager.js.map +1 -1
- package/dist/knowledge/writer.d.ts +10 -0
- package/dist/knowledge/writer.d.ts.map +1 -1
- package/dist/knowledge/writer.js +40 -0
- package/dist/knowledge/writer.js.map +1 -1
- package/dist/server/tools/gate_checks.d.ts +9 -0
- package/dist/server/tools/gate_checks.d.ts.map +1 -1
- package/dist/server/tools/gate_checks.js +55 -1
- package/dist/server/tools/gate_checks.js.map +1 -1
- package/dist/server/tools/middleware.d.ts.map +1 -1
- package/dist/server/tools/middleware.js +444 -381
- package/dist/server/tools/middleware.js.map +1 -1
- package/dist/server/tools/schemas.d.ts +38 -5
- package/dist/server/tools/schemas.d.ts.map +1 -1
- package/dist/server/tools/schemas.js +23 -7
- package/dist/server/tools/schemas.js.map +1 -1
- package/dist/server/tools/tool_groups/expand_handler.d.ts.map +1 -1
- package/dist/server/tools/tool_groups/expand_handler.js +40 -50
- package/dist/server/tools/tool_groups/expand_handler.js.map +1 -1
- package/dist/server/tools/tool_groups/status_plan_analyze_review.d.ts.map +1 -1
- package/dist/server/tools/tool_groups/status_plan_analyze_review.js +26 -0
- package/dist/server/tools/tool_groups/status_plan_analyze_review.js.map +1 -1
- package/dist/server/tools/tool_groups/verify_learn.d.ts.map +1 -1
- package/dist/server/tools/tool_groups/verify_learn.js +155 -7
- package/dist/server/tools/tool_groups/verify_learn.js.map +1 -1
- package/dist/types/pipeline.d.ts +2 -2
- package/dist/types/pipeline.d.ts.map +1 -1
- package/dist/types/task.d.ts +26 -0
- package/dist/types/task.d.ts.map +1 -1
- package/package.json +3 -3
- package/templates/procedures//350/256/276/350/256/241/345/256/241/350/256/241/346/265/201/347/250/213.md +186 -0
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
// 从 tools.ts 第 1096-1658 行提取。
|
|
7
7
|
import path from "node:path";
|
|
8
8
|
import { promises as fsp } from "node:fs";
|
|
9
|
-
import { debug } from "../../engine/core/logger.js";
|
|
9
|
+
import { debug, internalWarn as warn } from "../../engine/core/logger.js";
|
|
10
10
|
import { ENV } from "../../engine/core/env.js";
|
|
11
11
|
import { findToolInvocationContractByName, createToolTrace, validateToolInvocation, hasWriteSideEffect, } from "../../engine/contracts/tool_invocation_contract_registry.js";
|
|
12
12
|
import { TOOL_DIAGNOSTIC_CODES } from "../../engine/audit/diagnostic_registry.js";
|
|
@@ -36,7 +36,9 @@ export function createToolRegistrar(ctx) {
|
|
|
36
36
|
/** 状态预检: 在合同守卫之前验证任务状态 */
|
|
37
37
|
const STATE_PRECHECKS = {
|
|
38
38
|
sf_accept: ["executing", "verifying", "retrying"],
|
|
39
|
-
|
|
39
|
+
// P0-C2: sf_verify 接受 verifying 状态,与 sf_status resume 提示对齐
|
|
40
|
+
// 防止任务卡 verifying 时 sf_status 提示调用 sf_verify 但被状态预检拒绝
|
|
41
|
+
sf_verify: ["executing", "retrying", "verifying"],
|
|
40
42
|
sf_record_verification_execution: ["verifying", "executing"],
|
|
41
43
|
sf_learn: ["verifying", "executing"],
|
|
42
44
|
sf_expand: ["classifying", "expanding", "clarifying"],
|
|
@@ -112,314 +114,184 @@ export function createToolRegistrar(ctx) {
|
|
|
112
114
|
}
|
|
113
115
|
// ── 安全工具注册 ──
|
|
114
116
|
function registerSafeTool(name, desc, schema, handler) {
|
|
115
|
-
// @ts-expect-error -- MCP SDK 桥接点: schema 参数类型不兼容 Record<string, ZodTypeAny>
|
|
116
117
|
server.tool(name, desc, schema, async (args) => {
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
const
|
|
120
|
-
|
|
121
|
-
|
|
118
|
+
// P0-B3: 工具调用超时保护 — 防止 MCP 客户端被永久阻塞
|
|
119
|
+
const timeoutMs = getToolTimeoutMs(name);
|
|
120
|
+
const timeoutToken = { timedOut: false };
|
|
121
|
+
const timeoutPromise = new Promise((_, reject) => {
|
|
122
|
+
setTimeout(() => {
|
|
123
|
+
timeoutToken.timedOut = true;
|
|
124
|
+
reject(new Error(`工具 ${name} 执行超时 (${timeoutMs}ms)`));
|
|
125
|
+
}, timeoutMs);
|
|
126
|
+
});
|
|
127
|
+
const handlerPromise = performToolCall(name, args, invocationIdSeed(), handler);
|
|
128
|
+
try {
|
|
129
|
+
return await Promise.race([handlerPromise, timeoutPromise]);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
// 超时或其他错误统一走错误返回
|
|
122
133
|
return {
|
|
123
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
134
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
135
|
+
error: errorMessage(err),
|
|
136
|
+
tool_name: name,
|
|
137
|
+
timed_out: timeoutToken.timedOut,
|
|
138
|
+
}) }],
|
|
124
139
|
isError: true,
|
|
125
140
|
};
|
|
126
141
|
}
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
// 加载 TaskContext
|
|
130
|
-
let tCtx = null;
|
|
131
|
-
if (taskId) {
|
|
132
|
-
tCtx = await taskContext.load(taskId);
|
|
142
|
+
finally {
|
|
143
|
+
// 注意: setTimeout 已触发后无法 clear,但 handler 完成后 timeoutPromise 不再影响结果
|
|
133
144
|
}
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
/** P0-B3: 工具超时配置(毫秒) */
|
|
148
|
+
const DEFAULT_TOOL_TIMEOUT_MS = 5 * 60 * 1000; // 5 分钟
|
|
149
|
+
const TOOL_TIMEOUTS_MS = {
|
|
150
|
+
sf_expand: 10 * 60 * 1000, // 10 分钟
|
|
151
|
+
sf_plan: 10 * 60 * 1000, // 10 分钟
|
|
152
|
+
sf_classify: 60 * 1000, // 1 分钟
|
|
153
|
+
sf_verify: 10 * 60 * 1000, // 10 分钟
|
|
154
|
+
sf_learn: 60 * 1000, // 1 分钟
|
|
155
|
+
sf_review: 10 * 60 * 1000, // 10 分钟
|
|
156
|
+
sf_status: 30 * 1000, // 30 秒
|
|
157
|
+
sf_next: 30 * 1000, // 30 秒
|
|
158
|
+
};
|
|
159
|
+
function getToolTimeoutMs(toolName) {
|
|
160
|
+
return TOOL_TIMEOUTS_MS[toolName] ?? DEFAULT_TOOL_TIMEOUT_MS;
|
|
161
|
+
}
|
|
162
|
+
function invocationIdSeed() {
|
|
163
|
+
return `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
164
|
+
}
|
|
165
|
+
/** P0-B3: 实际工具执行逻辑(从 server.tool handler 抽出,便于超时包裹) */
|
|
166
|
+
async function performToolCall(name, args, invocationId, handler) {
|
|
167
|
+
const taskId = typeof args.task_id === "string" ? args.task_id : undefined;
|
|
168
|
+
const contract = findToolInvocationContractByName(name);
|
|
169
|
+
// 无合同 → hard fail
|
|
170
|
+
if (!contract) {
|
|
171
|
+
return {
|
|
172
|
+
content: [{ type: "text", text: JSON.stringify({ error: `工具 ${name} 未注册契约` }) }],
|
|
173
|
+
isError: true,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
const effectiveCategory = computeEffectiveCategory(name, args, contract);
|
|
177
|
+
const effectiveSideEffects = computeEffectiveSideEffects(name, args, contract);
|
|
178
|
+
// 加载 TaskContext
|
|
179
|
+
let tCtx = null;
|
|
180
|
+
if (taskId) {
|
|
181
|
+
tCtx = await taskContext.load(taskId);
|
|
182
|
+
}
|
|
183
|
+
// 状态预检: 在合同守卫之前验证任务状态
|
|
184
|
+
if (taskId && STATE_PRECHECKS[name] && tCtx) {
|
|
185
|
+
const validStates = STATE_PRECHECKS[name];
|
|
186
|
+
if (!validStates.includes(tCtx.status)) {
|
|
187
|
+
const label = STATE_ERROR_LABELS[name] ?? "状态不正确";
|
|
188
|
+
// 写入 blocked trace + 清除旧 violations,防止旧 trace 数据残留
|
|
189
|
+
// 注意:authorization/bypass 在此路径尚未计算,传 undefined
|
|
190
|
+
const blockedTrace = createToolTrace({
|
|
191
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
192
|
+
actual_side_effects: effectiveSideEffects,
|
|
193
|
+
next_allowed_tools: contract.default_next_tools,
|
|
194
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
195
|
+
});
|
|
196
|
+
try {
|
|
197
|
+
await taskContext.setToolTrace(taskId, blockedTrace, []);
|
|
198
|
+
}
|
|
199
|
+
catch (e) {
|
|
200
|
+
warn("中间件: setToolTrace 失败 (blocked)", errorMessage(e));
|
|
158
201
|
}
|
|
159
|
-
}
|
|
160
|
-
// 严格受控但缺少 task_id
|
|
161
|
-
const isStandaloneStrict = !contract.requires_workflow;
|
|
162
|
-
if (contract.category === "strict_controlled" && !taskId && !isStandaloneStrict) {
|
|
163
202
|
return {
|
|
164
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
203
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
204
|
+
error: `任务状态 ${tCtx.status} ${label},需要 ${validStates.join(" 或 ")}`,
|
|
205
|
+
status: tCtx.status,
|
|
206
|
+
}) }],
|
|
165
207
|
isError: true,
|
|
166
208
|
};
|
|
167
209
|
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
210
|
+
}
|
|
211
|
+
// 严格受控但缺少 task_id
|
|
212
|
+
const isStandaloneStrict = !contract.requires_workflow;
|
|
213
|
+
if (contract.category === "strict_controlled" && !taskId && !isStandaloneStrict) {
|
|
214
|
+
return {
|
|
215
|
+
content: [{ type: "text", text: JSON.stringify({ error: `strict controlled 工具 ${name} 需要 task_id` }) }],
|
|
216
|
+
isError: true,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
// 授权检查
|
|
220
|
+
const authorization = checkAuth(name, args, contract, tCtx ?? undefined, effectiveCategory);
|
|
221
|
+
// 授权拒绝 → 仅对网关级拒绝执行 hard fail
|
|
222
|
+
// (动态写入升级、破坏性操作、无授权源的 strict_controlled)
|
|
223
|
+
// Handler 级授权(如 sf_capability_update confirm)由 handler 自行处理
|
|
224
|
+
const gatewayDenial = !authorization.granted &&
|
|
225
|
+
authorization.reason !== "tool requires explicit authorization";
|
|
226
|
+
if (gatewayDenial) {
|
|
227
|
+
const blockedTrace = createToolTrace({
|
|
228
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
229
|
+
actual_side_effects: effectiveSideEffects,
|
|
230
|
+
next_allowed_tools: contract.default_next_tools,
|
|
231
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
232
|
+
authorization, bypass: undefined,
|
|
233
|
+
});
|
|
234
|
+
const authViolation = {
|
|
235
|
+
invocation_id: invocationId, tool_name: name,
|
|
236
|
+
violation_type: "missing_authorization", severity: "hard_fail",
|
|
237
|
+
reason: authorization.reason ?? "authorization denied",
|
|
238
|
+
recovery: "提供 confirm=true / authorized=true / authorization_token",
|
|
239
|
+
};
|
|
240
|
+
if (taskId && tCtx) {
|
|
241
|
+
await taskContext.setToolTrace(taskId, blockedTrace, [authViolation]);
|
|
242
|
+
}
|
|
243
|
+
return {
|
|
244
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
245
|
+
error: authViolation.reason, violation: authViolation,
|
|
246
|
+
tool_trace: blockedTrace,
|
|
247
|
+
next_allowed_tools: contract.default_next_tools,
|
|
248
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
249
|
+
}) }],
|
|
250
|
+
isError: true,
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
// read_only_bypass 的旁路处理
|
|
254
|
+
const bypass = effectiveCategory === "read_only_bypass"
|
|
255
|
+
? { allowed: true, reason: "read_only" } : undefined;
|
|
256
|
+
// 计划前置门: 在合同验证之前检查 — 写工具必须有 plan_proposal_gate
|
|
257
|
+
// sf_classify 创建任务、sf_expand 创建 plan_proposal_gate — 两者豁免
|
|
258
|
+
const hasWriteEffect = hasWriteSideEffect(effectiveSideEffects);
|
|
259
|
+
const planGateExempt = name === "sf_classify" || name === "sf_expand";
|
|
260
|
+
if (hasWriteEffect && taskId && !planGateExempt) {
|
|
261
|
+
const pgResult = checkWriteToolPlanGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
|
|
262
|
+
if (!pgResult.allowed) {
|
|
263
|
+
const planViolation = {
|
|
264
|
+
invocation_id: invocationId, tool_name: name,
|
|
265
|
+
violation_type: "guard_blocked", severity: "hard_fail",
|
|
266
|
+
reason: pgResult.reason ?? "执行前缺少 Plan Proposal Gate",
|
|
267
|
+
recovery: "先调用 sf_classify → sf_expand → 设置 plan_proposal_gate 后再执行此工具",
|
|
268
|
+
};
|
|
176
269
|
const blockedTrace = createToolTrace({
|
|
177
270
|
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
178
271
|
actual_side_effects: effectiveSideEffects,
|
|
179
272
|
next_allowed_tools: contract.default_next_tools,
|
|
180
273
|
forbidden_tools: contract.forbidden_next_tools,
|
|
181
|
-
authorization, bypass
|
|
274
|
+
authorization, bypass,
|
|
182
275
|
});
|
|
183
|
-
|
|
184
|
-
invocation_id: invocationId, tool_name: name,
|
|
185
|
-
violation_type: "missing_authorization", severity: "hard_fail",
|
|
186
|
-
reason: authorization.reason ?? "authorization denied",
|
|
187
|
-
recovery: "提供 confirm=true / authorized=true / authorization_token",
|
|
188
|
-
};
|
|
189
|
-
if (taskId && tCtx) {
|
|
190
|
-
await taskContext.setToolTrace(taskId, blockedTrace, [authViolation]);
|
|
191
|
-
}
|
|
276
|
+
await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
|
|
192
277
|
return {
|
|
193
278
|
content: [{ type: "text", text: JSON.stringify({
|
|
194
|
-
error:
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
279
|
+
error: planViolation.reason,
|
|
280
|
+
violation: planViolation,
|
|
281
|
+
diagnostic_code: pgResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.planGate,
|
|
282
|
+
recovery: planViolation.recovery,
|
|
198
283
|
}) }],
|
|
199
284
|
isError: true,
|
|
200
285
|
};
|
|
201
286
|
}
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
const pgResult = checkWriteToolPlanGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
|
|
211
|
-
if (!pgResult.allowed) {
|
|
212
|
-
const planViolation = {
|
|
213
|
-
invocation_id: invocationId, tool_name: name,
|
|
214
|
-
violation_type: "guard_blocked", severity: "hard_fail",
|
|
215
|
-
reason: pgResult.reason ?? "执行前缺少 Plan Proposal Gate",
|
|
216
|
-
recovery: "先调用 sf_classify → sf_expand → 设置 plan_proposal_gate 后再执行此工具",
|
|
217
|
-
};
|
|
218
|
-
const blockedTrace = createToolTrace({
|
|
219
|
-
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
220
|
-
actual_side_effects: effectiveSideEffects,
|
|
221
|
-
next_allowed_tools: contract.default_next_tools,
|
|
222
|
-
forbidden_tools: contract.forbidden_next_tools,
|
|
223
|
-
authorization, bypass,
|
|
224
|
-
});
|
|
225
|
-
await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
|
|
226
|
-
return {
|
|
227
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
228
|
-
error: planViolation.reason,
|
|
229
|
-
violation: planViolation,
|
|
230
|
-
diagnostic_code: pgResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.planGate,
|
|
231
|
-
recovery: planViolation.recovery,
|
|
232
|
-
}) }],
|
|
233
|
-
isError: true,
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const planGateCheck = taskContext.checkPlanGateBeforeWrite(tCtx);
|
|
237
|
-
if (!planGateCheck.allowed) {
|
|
238
|
-
const planViolation = {
|
|
239
|
-
invocation_id: invocationId, tool_name: name,
|
|
240
|
-
violation_type: "guard_blocked", severity: "hard_fail",
|
|
241
|
-
reason: planGateCheck.reason_zh,
|
|
242
|
-
recovery: "请先通过 PlanProposalFirstGate 生成并确认执行计划",
|
|
243
|
-
};
|
|
244
|
-
const blockedTrace = createToolTrace({
|
|
245
|
-
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
246
|
-
actual_side_effects: effectiveSideEffects,
|
|
247
|
-
next_allowed_tools: contract.default_next_tools,
|
|
248
|
-
forbidden_tools: contract.forbidden_next_tools,
|
|
249
|
-
authorization, bypass,
|
|
250
|
-
});
|
|
251
|
-
await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
|
|
252
|
-
return {
|
|
253
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
254
|
-
error: planViolation.reason,
|
|
255
|
-
violation: planViolation,
|
|
256
|
-
diagnostic_code: TOOL_DIAGNOSTIC_CODES.planGate,
|
|
257
|
-
recovery: planViolation.recovery,
|
|
258
|
-
}) }],
|
|
259
|
-
isError: true,
|
|
260
|
-
};
|
|
261
|
-
}
|
|
262
|
-
}
|
|
263
|
-
// 施工指令契约门: 写操作前检查 instruction_contract 状态
|
|
264
|
-
if (hasWriteEffect && taskId) {
|
|
265
|
-
const instrResult = await checkInstructionContractGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects, task_id: taskId, taskContextMgr: taskContext });
|
|
266
|
-
if (!instrResult.allowed) {
|
|
267
|
-
const instrViolation = {
|
|
268
|
-
invocation_id: invocationId, tool_name: name,
|
|
269
|
-
violation_type: "guard_blocked", severity: "hard_fail",
|
|
270
|
-
reason: instrResult.reason ?? "施工指令未就绪",
|
|
271
|
-
recovery: "请先完善施工指令(目标、范围、非目标、落点、验收标准、禁止绕过项)",
|
|
272
|
-
};
|
|
273
|
-
const instrTrace = createToolTrace({
|
|
274
|
-
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
275
|
-
actual_side_effects: effectiveSideEffects,
|
|
276
|
-
next_allowed_tools: contract.default_next_tools,
|
|
277
|
-
forbidden_tools: contract.forbidden_next_tools,
|
|
278
|
-
authorization, bypass,
|
|
279
|
-
});
|
|
280
|
-
await taskContext.setToolTrace(taskId, instrTrace, [instrViolation]);
|
|
281
|
-
return {
|
|
282
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
283
|
-
error: instrViolation.reason,
|
|
284
|
-
violation: instrViolation,
|
|
285
|
-
diagnostic_code: instrResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.instructionContract,
|
|
286
|
-
recovery: instrViolation.recovery,
|
|
287
|
-
}) }],
|
|
288
|
-
isError: true,
|
|
289
|
-
};
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
// 问题六十二: MCP 写入路径同样必须消费设计产物包状态,不能绕开 CLI hook。
|
|
293
|
-
if (hasWriteEffect && taskId) {
|
|
294
|
-
const designWriteGate = checkDesignArtifactWriteGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
|
|
295
|
-
if (!designWriteGate.allowed) {
|
|
296
|
-
const designViolation = {
|
|
297
|
-
invocation_id: invocationId, tool_name: name,
|
|
298
|
-
violation_type: "guard_blocked", severity: "hard_fail",
|
|
299
|
-
reason: designWriteGate.reason ?? "设计产物包未达到实现就绪状态",
|
|
300
|
-
recovery: "仅可继续补充设计资产并执行 sf_verify 真实复验;通过前不得写入业务实现",
|
|
301
|
-
};
|
|
302
|
-
const designTrace = createToolTrace({
|
|
303
|
-
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
304
|
-
actual_side_effects: effectiveSideEffects,
|
|
305
|
-
next_allowed_tools: contract.default_next_tools,
|
|
306
|
-
forbidden_tools: contract.forbidden_next_tools,
|
|
307
|
-
authorization, bypass,
|
|
308
|
-
});
|
|
309
|
-
await taskContext.setToolTrace(taskId, designTrace, [designViolation]);
|
|
310
|
-
return {
|
|
311
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
312
|
-
error: designViolation.reason,
|
|
313
|
-
violation: designViolation,
|
|
314
|
-
diagnostic_code: designWriteGate.diagnostic_code,
|
|
315
|
-
recovery: designViolation.recovery,
|
|
316
|
-
}) }],
|
|
317
|
-
isError: true,
|
|
318
|
-
};
|
|
319
|
-
}
|
|
320
|
-
}
|
|
321
|
-
// 用户项目知识门禁: review/verify/deliver 不能绕过不可消费的项目 hard rule。
|
|
322
|
-
if (["sf_review", "sf_verify", "sf_deliver"].includes(name)) {
|
|
323
|
-
const { evaluateLifecycleKnowledgeDecision } = await import("../../engine/contracts/lifecycle_knowledge_contract.js");
|
|
324
|
-
const lifecycleStage = name === "sf_review" ? "implementation" : name === "sf_verify" ? "testing" : "delivery";
|
|
325
|
-
const lifecycleDecision = evaluateLifecycleKnowledgeDecision({
|
|
326
|
-
projectPath,
|
|
327
|
-
consumer: name,
|
|
328
|
-
intent: tCtx?.intent ?? name,
|
|
329
|
-
route: name === "sf_review" ? "review" : name === "sf_verify" ? "verification" : "delivery",
|
|
330
|
-
lifecycle_stage: lifecycleStage,
|
|
331
|
-
changed_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
|
|
332
|
-
verified_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
|
|
333
|
-
traceability_ids: [
|
|
334
|
-
...(tCtx?.traceability_binding?.requirement_ids ?? []),
|
|
335
|
-
...(tCtx?.traceability_binding?.prototype_ids ?? []),
|
|
336
|
-
...(tCtx?.traceability_binding?.architecture_ids ?? []),
|
|
337
|
-
...(tCtx?.traceability_binding?.detail_design_ids ?? []),
|
|
338
|
-
...(tCtx?.traceability_binding?.phase_ids ?? []),
|
|
339
|
-
...(tCtx?.traceability_binding?.slice_ids ?? []),
|
|
340
|
-
...(tCtx?.traceability_binding?.acceptance_ids ?? []),
|
|
341
|
-
],
|
|
342
|
-
config,
|
|
343
|
-
task_id: taskId,
|
|
344
|
-
require_design_audit: false,
|
|
345
|
-
enforce_control_plane: name !== "sf_review",
|
|
346
|
-
generation_traces: [
|
|
347
|
-
...(tCtx?.classification ? [{ tool_name: "sf_classify", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
|
|
348
|
-
...(tCtx?.expansion ? [{ tool_name: "sf_expand", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
|
|
349
|
-
...(name === "sf_verify" ? [{ tool_name: "sf_verify", task_id: taskId, status: "blocked", evidence_kind: "plan" }] : []),
|
|
350
|
-
],
|
|
351
|
-
selection_limit: 8,
|
|
352
|
-
});
|
|
353
|
-
if (lifecycleDecision.hard_fail_count > 0) {
|
|
354
|
-
const hardFindings = lifecycleDecision.findings
|
|
355
|
-
.filter((finding) => finding.severity === "hard_fail")
|
|
356
|
-
.map((finding) => `[${finding.code}] ${finding.message_zh}`);
|
|
357
|
-
const pkViolation = {
|
|
358
|
-
invocation_id: invocationId,
|
|
359
|
-
tool_name: name,
|
|
360
|
-
violation_type: "guard_blocked",
|
|
361
|
-
severity: "hard_fail",
|
|
362
|
-
reason: lifecycleDecision.decision_summary_zh,
|
|
363
|
-
recovery: lifecycleDecision.recovery_commands.join(";") || "运行 soloforge next 查看统一生命周期知识合同阻断项",
|
|
364
|
-
};
|
|
365
|
-
const pkTrace = createToolTrace({
|
|
366
|
-
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
367
|
-
actual_side_effects: effectiveSideEffects,
|
|
368
|
-
next_allowed_tools: ["sf_expand", "sf_status"],
|
|
369
|
-
forbidden_tools: ["sf_review", "sf_verify", "sf_deliver"],
|
|
370
|
-
authorization, bypass,
|
|
371
|
-
});
|
|
372
|
-
if (taskId && tCtx)
|
|
373
|
-
await taskContext.setToolTrace(taskId, pkTrace, [pkViolation]);
|
|
374
|
-
return {
|
|
375
|
-
content: [{ type: "text", text: JSON.stringify({
|
|
376
|
-
error: pkViolation.reason,
|
|
377
|
-
violation: pkViolation,
|
|
378
|
-
diagnostic_code: TOOL_DIAGNOSTIC_CODES.projectKnowledgeBlocked,
|
|
379
|
-
findings: hardFindings,
|
|
380
|
-
lifecycle_knowledge_decision: {
|
|
381
|
-
contract_id: lifecycleDecision.contract_id,
|
|
382
|
-
lifecycle_stage: lifecycleDecision.lifecycle_stage,
|
|
383
|
-
authoritative_paths: lifecycleDecision.authoritative_paths,
|
|
384
|
-
recovery_commands: lifecycleDecision.recovery_commands,
|
|
385
|
-
},
|
|
386
|
-
recovery: pkViolation.recovery,
|
|
387
|
-
}) }],
|
|
388
|
-
isError: true,
|
|
389
|
-
};
|
|
390
|
-
}
|
|
391
|
-
}
|
|
392
|
-
// 合同验证
|
|
393
|
-
const lastTrace = tCtx?.last_tool_trace;
|
|
394
|
-
// 为动态工具构建有效的合同覆盖(如 sf_status cancel)
|
|
395
|
-
let contractOverride;
|
|
396
|
-
if (effectiveCategory !== contract.category || effectiveSideEffects !== contract.side_effects) {
|
|
397
|
-
contractOverride = { ...contract, category: effectiveCategory, side_effects: effectiveSideEffects };
|
|
398
|
-
}
|
|
399
|
-
// Workflow ID: 仅来自真实的 expansion trace
|
|
400
|
-
const explicitWorkflowId = tCtx?.expansion?.workflow_trace?.workflow_id;
|
|
401
|
-
const violations = validateToolInvocation({
|
|
402
|
-
tool_name: name,
|
|
403
|
-
current_next_allowed: lastTrace?.next_allowed_tools ?? [],
|
|
404
|
-
current_forbidden: lastTrace?.forbidden_tools ?? [],
|
|
405
|
-
authorization,
|
|
406
|
-
bypass,
|
|
407
|
-
actual_side_effects: effectiveSideEffects,
|
|
408
|
-
workflow_id: explicitWorkflowId,
|
|
409
|
-
_contractOverride: contractOverride,
|
|
410
|
-
});
|
|
411
|
-
// 基于状态的回退: 将 contract_state_mismatch 从 hard_fail 降级为 require_human
|
|
412
|
-
if (authorization.reason === "task in valid state for tool") {
|
|
413
|
-
for (const v of violations) {
|
|
414
|
-
if (v.violation_type === "contract_state_mismatch" && v.severity === "hard_fail") {
|
|
415
|
-
v.severity = "require_human";
|
|
416
|
-
v.reason += " (state-based fallback: no real workflow_id)";
|
|
417
|
-
}
|
|
418
|
-
}
|
|
419
|
-
}
|
|
420
|
-
// Hard-fail 违规 → 阻止
|
|
421
|
-
const hardFail = violations.find(v => v.severity === "hard_fail");
|
|
422
|
-
if (hardFail) {
|
|
287
|
+
const planGateCheck = taskContext.checkPlanGateBeforeWrite(tCtx);
|
|
288
|
+
if (!planGateCheck.allowed) {
|
|
289
|
+
const planViolation = {
|
|
290
|
+
invocation_id: invocationId, tool_name: name,
|
|
291
|
+
violation_type: "guard_blocked", severity: "hard_fail",
|
|
292
|
+
reason: planGateCheck.reason_zh,
|
|
293
|
+
recovery: "请先通过 PlanProposalFirstGate 生成并确认执行计划",
|
|
294
|
+
};
|
|
423
295
|
const blockedTrace = createToolTrace({
|
|
424
296
|
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
425
297
|
actual_side_effects: effectiveSideEffects,
|
|
@@ -427,124 +299,313 @@ export function createToolRegistrar(ctx) {
|
|
|
427
299
|
forbidden_tools: contract.forbidden_next_tools,
|
|
428
300
|
authorization, bypass,
|
|
429
301
|
});
|
|
430
|
-
|
|
431
|
-
await taskContext.setToolTrace(taskId, blockedTrace, violations);
|
|
432
|
-
}
|
|
302
|
+
await taskContext.setToolTrace(taskId, blockedTrace, [planViolation]);
|
|
433
303
|
return {
|
|
434
304
|
content: [{ type: "text", text: JSON.stringify({
|
|
435
|
-
error:
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
305
|
+
error: planViolation.reason,
|
|
306
|
+
violation: planViolation,
|
|
307
|
+
diagnostic_code: TOOL_DIAGNOSTIC_CODES.planGate,
|
|
308
|
+
recovery: planViolation.recovery,
|
|
439
309
|
}) }],
|
|
440
310
|
isError: true,
|
|
441
311
|
};
|
|
442
312
|
}
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
forbidden_tools: contract.forbidden_next_tools,
|
|
456
|
-
authorization, bypass,
|
|
457
|
-
});
|
|
458
|
-
try {
|
|
459
|
-
await taskContext.setToolTrace(taskId, passthroughTrace, violations);
|
|
460
|
-
}
|
|
461
|
-
catch { /* best effort */ }
|
|
462
|
-
}
|
|
463
|
-
return raw;
|
|
464
|
-
}
|
|
465
|
-
// 从 { result } 模式中提取数据
|
|
466
|
-
const rawRec = raw;
|
|
467
|
-
const data = rawRec?.result !== undefined ? rawRec.result : rawRec;
|
|
468
|
-
const hasError = !!(data && typeof data === "object" && "error" in data);
|
|
469
|
-
const dataRec = data;
|
|
470
|
-
// 门禁阻断后通用重试机制:任何工具在 error 状态下都应可重试自身。
|
|
471
|
-
// handler 显式提供的 recovery 值优先;未提供时自动将自身加入 next_allowed 并移出 forbidden。
|
|
472
|
-
// 覆盖 sf_expand/sf_deliver/sf_scaffold 及未来所有 forbidden_self 工具。
|
|
473
|
-
const recoveryNextTools = hasError && Array.isArray(dataRec.recovery_next_tools)
|
|
474
|
-
? dataRec.recovery_next_tools
|
|
475
|
-
: hasError
|
|
476
|
-
? [...contract.default_next_tools, name]
|
|
477
|
-
: contract.default_next_tools;
|
|
478
|
-
const recoveryForbiddenTools = hasError && Array.isArray(dataRec.recovery_forbidden_tools)
|
|
479
|
-
? dataRec.recovery_forbidden_tools
|
|
480
|
-
: hasError
|
|
481
|
-
? contract.forbidden_next_tools.filter((t) => t !== name)
|
|
482
|
-
: contract.forbidden_next_tools;
|
|
483
|
-
// 构建 trace;失败处理器可显式开放修复重验路径
|
|
484
|
-
const trace = createToolTrace({
|
|
313
|
+
}
|
|
314
|
+
// 施工指令契约门: 写操作前检查 instruction_contract 状态
|
|
315
|
+
if (hasWriteEffect && taskId) {
|
|
316
|
+
const instrResult = await checkInstructionContractGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects, task_id: taskId, taskContextMgr: taskContext });
|
|
317
|
+
if (!instrResult.allowed) {
|
|
318
|
+
const instrViolation = {
|
|
319
|
+
invocation_id: invocationId, tool_name: name,
|
|
320
|
+
violation_type: "guard_blocked", severity: "hard_fail",
|
|
321
|
+
reason: instrResult.reason ?? "施工指令未就绪",
|
|
322
|
+
recovery: "请先完善施工指令(目标、范围、非目标、落点、验收标准、禁止绕过项)",
|
|
323
|
+
};
|
|
324
|
+
const instrTrace = createToolTrace({
|
|
485
325
|
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
486
|
-
workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
|
|
487
326
|
actual_side_effects: effectiveSideEffects,
|
|
488
|
-
next_allowed_tools:
|
|
489
|
-
forbidden_tools:
|
|
327
|
+
next_allowed_tools: contract.default_next_tools,
|
|
328
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
490
329
|
authorization, bypass,
|
|
491
330
|
});
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
forbidden_tools: recoveryForbiddenTools,
|
|
331
|
+
await taskContext.setToolTrace(taskId, instrTrace, [instrViolation]);
|
|
332
|
+
return {
|
|
333
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
334
|
+
error: instrViolation.reason,
|
|
335
|
+
violation: instrViolation,
|
|
336
|
+
diagnostic_code: instrResult.diagnostic_code ?? TOOL_DIAGNOSTIC_CODES.instructionContract,
|
|
337
|
+
recovery: instrViolation.recovery,
|
|
338
|
+
}) }],
|
|
339
|
+
isError: true,
|
|
502
340
|
};
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
// 问题六十二: MCP 写入路径同样必须消费设计产物包状态,不能绕开 CLI hook。
|
|
344
|
+
if (hasWriteEffect && taskId) {
|
|
345
|
+
const designWriteGate = checkDesignArtifactWriteGate({ ctx: tCtx, toolName: name, sideEffects: effectiveSideEffects });
|
|
346
|
+
if (!designWriteGate.allowed) {
|
|
347
|
+
const designViolation = {
|
|
348
|
+
invocation_id: invocationId, tool_name: name,
|
|
349
|
+
violation_type: "guard_blocked", severity: "hard_fail",
|
|
350
|
+
reason: designWriteGate.reason ?? "设计产物包未达到实现就绪状态",
|
|
351
|
+
recovery: "仅可继续补充设计资产并执行 sf_verify 真实复验;通过前不得写入业务实现",
|
|
352
|
+
};
|
|
353
|
+
const designTrace = createToolTrace({
|
|
354
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
355
|
+
actual_side_effects: effectiveSideEffects,
|
|
356
|
+
next_allowed_tools: contract.default_next_tools,
|
|
357
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
358
|
+
authorization, bypass,
|
|
359
|
+
});
|
|
360
|
+
await taskContext.setToolTrace(taskId, designTrace, [designViolation]);
|
|
508
361
|
return {
|
|
509
|
-
content: [{ type: "text", text: JSON.stringify(
|
|
510
|
-
|
|
362
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
363
|
+
error: designViolation.reason,
|
|
364
|
+
violation: designViolation,
|
|
365
|
+
diagnostic_code: designWriteGate.diagnostic_code,
|
|
366
|
+
recovery: designViolation.recovery,
|
|
367
|
+
}) }],
|
|
368
|
+
isError: true,
|
|
511
369
|
};
|
|
512
370
|
}
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
371
|
+
}
|
|
372
|
+
// 用户项目知识门禁: review/verify/deliver 不能绕过不可消费的项目 hard rule。
|
|
373
|
+
if (["sf_review", "sf_verify", "sf_deliver"].includes(name)) {
|
|
374
|
+
const { evaluateLifecycleKnowledgeDecision } = await import("../../engine/contracts/lifecycle_knowledge_contract.js");
|
|
375
|
+
const lifecycleStage = name === "sf_review" ? "implementation" : name === "sf_verify" ? "testing" : "delivery";
|
|
376
|
+
const lifecycleDecision = evaluateLifecycleKnowledgeDecision({
|
|
377
|
+
projectPath,
|
|
378
|
+
consumer: name,
|
|
379
|
+
intent: tCtx?.intent ?? name,
|
|
380
|
+
route: name === "sf_review" ? "review" : name === "sf_verify" ? "verification" : "delivery",
|
|
381
|
+
lifecycle_stage: lifecycleStage,
|
|
382
|
+
changed_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
|
|
383
|
+
verified_files: Array.isArray(args.changed_files) ? args.changed_files : tCtx?.execution?.changed_files ?? [],
|
|
384
|
+
traceability_ids: [
|
|
385
|
+
...(tCtx?.traceability_binding?.requirement_ids ?? []),
|
|
386
|
+
...(tCtx?.traceability_binding?.prototype_ids ?? []),
|
|
387
|
+
...(tCtx?.traceability_binding?.architecture_ids ?? []),
|
|
388
|
+
...(tCtx?.traceability_binding?.detail_design_ids ?? []),
|
|
389
|
+
...(tCtx?.traceability_binding?.phase_ids ?? []),
|
|
390
|
+
...(tCtx?.traceability_binding?.slice_ids ?? []),
|
|
391
|
+
...(tCtx?.traceability_binding?.acceptance_ids ?? []),
|
|
392
|
+
],
|
|
393
|
+
config,
|
|
394
|
+
task_id: taskId,
|
|
395
|
+
require_design_audit: false,
|
|
396
|
+
enforce_control_plane: name !== "sf_review",
|
|
397
|
+
generation_traces: [
|
|
398
|
+
...(tCtx?.classification ? [{ tool_name: "sf_classify", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
|
|
399
|
+
...(tCtx?.expansion ? [{ tool_name: "sf_expand", task_id: taskId, status: "passed", evidence_kind: "result" }] : []),
|
|
400
|
+
...(name === "sf_verify" ? [{ tool_name: "sf_verify", task_id: taskId, status: "blocked", evidence_kind: "plan" }] : []),
|
|
401
|
+
],
|
|
402
|
+
selection_limit: 8,
|
|
403
|
+
});
|
|
404
|
+
if (lifecycleDecision.hard_fail_count > 0) {
|
|
405
|
+
const hardFindings = lifecycleDecision.findings
|
|
406
|
+
.filter((finding) => finding.severity === "hard_fail")
|
|
407
|
+
.map((finding) => `[${finding.code}] ${finding.message_zh}`);
|
|
408
|
+
const pkViolation = {
|
|
409
|
+
invocation_id: invocationId,
|
|
410
|
+
tool_name: name,
|
|
411
|
+
violation_type: "guard_blocked",
|
|
412
|
+
severity: "hard_fail",
|
|
413
|
+
reason: lifecycleDecision.decision_summary_zh,
|
|
414
|
+
recovery: lifecycleDecision.recovery_commands.join(";") || "运行 soloforge next 查看统一生命周期知识合同阻断项",
|
|
415
|
+
};
|
|
416
|
+
const pkTrace = createToolTrace({
|
|
525
417
|
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
526
418
|
actual_side_effects: effectiveSideEffects,
|
|
527
|
-
next_allowed_tools:
|
|
528
|
-
forbidden_tools:
|
|
419
|
+
next_allowed_tools: ["sf_expand", "sf_status"],
|
|
420
|
+
forbidden_tools: ["sf_review", "sf_verify", "sf_deliver"],
|
|
529
421
|
authorization, bypass,
|
|
530
422
|
});
|
|
531
|
-
if (taskId)
|
|
532
|
-
|
|
533
|
-
await taskContext.setToolTrace(taskId, errorTrace, []);
|
|
534
|
-
}
|
|
535
|
-
catch { /* best effort */ }
|
|
536
|
-
}
|
|
423
|
+
if (taskId && tCtx)
|
|
424
|
+
await taskContext.setToolTrace(taskId, pkTrace, [pkViolation]);
|
|
537
425
|
return {
|
|
538
426
|
content: [{ type: "text", text: JSON.stringify({
|
|
539
|
-
error:
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
427
|
+
error: pkViolation.reason,
|
|
428
|
+
violation: pkViolation,
|
|
429
|
+
diagnostic_code: TOOL_DIAGNOSTIC_CODES.projectKnowledgeBlocked,
|
|
430
|
+
findings: hardFindings,
|
|
431
|
+
lifecycle_knowledge_decision: {
|
|
432
|
+
contract_id: lifecycleDecision.contract_id,
|
|
433
|
+
lifecycle_stage: lifecycleDecision.lifecycle_stage,
|
|
434
|
+
authoritative_paths: lifecycleDecision.authoritative_paths,
|
|
435
|
+
recovery_commands: lifecycleDecision.recovery_commands,
|
|
436
|
+
},
|
|
437
|
+
recovery: pkViolation.recovery,
|
|
543
438
|
}) }],
|
|
544
439
|
isError: true,
|
|
545
440
|
};
|
|
546
441
|
}
|
|
442
|
+
}
|
|
443
|
+
// 合同验证
|
|
444
|
+
const lastTrace = tCtx?.last_tool_trace;
|
|
445
|
+
// 为动态工具构建有效的合同覆盖(如 sf_status cancel)
|
|
446
|
+
let contractOverride;
|
|
447
|
+
if (effectiveCategory !== contract.category || effectiveSideEffects !== contract.side_effects) {
|
|
448
|
+
contractOverride = { ...contract, category: effectiveCategory, side_effects: effectiveSideEffects };
|
|
449
|
+
}
|
|
450
|
+
// Workflow ID: 仅来自真实的 expansion trace
|
|
451
|
+
const explicitWorkflowId = tCtx?.expansion?.workflow_trace?.workflow_id;
|
|
452
|
+
const violations = validateToolInvocation({
|
|
453
|
+
tool_name: name,
|
|
454
|
+
current_next_allowed: lastTrace?.next_allowed_tools ?? [],
|
|
455
|
+
current_forbidden: lastTrace?.forbidden_tools ?? [],
|
|
456
|
+
authorization,
|
|
457
|
+
bypass,
|
|
458
|
+
actual_side_effects: effectiveSideEffects,
|
|
459
|
+
workflow_id: explicitWorkflowId,
|
|
460
|
+
_contractOverride: contractOverride,
|
|
547
461
|
});
|
|
462
|
+
// 基于状态的回退: 将 contract_state_mismatch 从 hard_fail 降级为 require_human
|
|
463
|
+
if (authorization.reason === "task in valid state for tool") {
|
|
464
|
+
for (const v of violations) {
|
|
465
|
+
if (v.violation_type === "contract_state_mismatch" && v.severity === "hard_fail") {
|
|
466
|
+
v.severity = "require_human";
|
|
467
|
+
v.reason += " (state-based fallback: no real workflow_id)";
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
// Hard-fail 违规 → 阻止
|
|
472
|
+
const hardFail = violations.find(v => v.severity === "hard_fail");
|
|
473
|
+
if (hardFail) {
|
|
474
|
+
const blockedTrace = createToolTrace({
|
|
475
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
476
|
+
actual_side_effects: effectiveSideEffects,
|
|
477
|
+
next_allowed_tools: contract.default_next_tools,
|
|
478
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
479
|
+
authorization, bypass,
|
|
480
|
+
});
|
|
481
|
+
if (taskId && tCtx) {
|
|
482
|
+
await taskContext.setToolTrace(taskId, blockedTrace, violations);
|
|
483
|
+
}
|
|
484
|
+
return {
|
|
485
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
486
|
+
error: hardFail.reason, violation: hardFail,
|
|
487
|
+
tool_trace: blockedTrace,
|
|
488
|
+
next_allowed_tools: contract.default_next_tools,
|
|
489
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
490
|
+
}) }],
|
|
491
|
+
isError: true,
|
|
492
|
+
};
|
|
493
|
+
}
|
|
494
|
+
// 执行 handler
|
|
495
|
+
try {
|
|
496
|
+
const raw = await handler(args);
|
|
497
|
+
// 如果 handler 直接返回 { content } 则透传,但补充 trace 写入
|
|
498
|
+
if (raw && typeof raw === "object" && "content" in raw && Array.isArray(raw.content)) {
|
|
499
|
+
// [ENG-6] content 格式路径也需要写入 trace
|
|
500
|
+
if (taskId) {
|
|
501
|
+
const passthroughTrace = createToolTrace({
|
|
502
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
503
|
+
workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
|
|
504
|
+
actual_side_effects: effectiveSideEffects,
|
|
505
|
+
next_allowed_tools: contract.default_next_tools,
|
|
506
|
+
forbidden_tools: contract.forbidden_next_tools,
|
|
507
|
+
authorization, bypass,
|
|
508
|
+
});
|
|
509
|
+
try {
|
|
510
|
+
await taskContext.setToolTrace(taskId, passthroughTrace, violations);
|
|
511
|
+
}
|
|
512
|
+
catch (e) {
|
|
513
|
+
warn("中间件: setToolTrace 失败 (passthrough)", errorMessage(e));
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
return raw;
|
|
517
|
+
}
|
|
518
|
+
// 从 { result } 模式中提取数据
|
|
519
|
+
const rawRec = raw;
|
|
520
|
+
const data = rawRec?.result !== undefined ? rawRec.result : rawRec;
|
|
521
|
+
const hasError = !!(data && typeof data === "object" && "error" in data);
|
|
522
|
+
const dataRec = data;
|
|
523
|
+
// 门禁阻断后通用重试机制:任何工具在 error 状态下都应可重试自身。
|
|
524
|
+
// handler 显式提供的 recovery 值优先;未提供时自动将自身加入 next_allowed 并移出 forbidden。
|
|
525
|
+
// 覆盖 sf_expand/sf_deliver/sf_scaffold 及未来所有 forbidden_self 工具。
|
|
526
|
+
const recoveryNextTools = hasError && Array.isArray(dataRec.recovery_next_tools)
|
|
527
|
+
? dataRec.recovery_next_tools
|
|
528
|
+
: hasError
|
|
529
|
+
? [...contract.default_next_tools, name]
|
|
530
|
+
: contract.default_next_tools;
|
|
531
|
+
const recoveryForbiddenTools = hasError && Array.isArray(dataRec.recovery_forbidden_tools)
|
|
532
|
+
? dataRec.recovery_forbidden_tools
|
|
533
|
+
: hasError
|
|
534
|
+
? contract.forbidden_next_tools.filter((t) => t !== name)
|
|
535
|
+
: contract.forbidden_next_tools;
|
|
536
|
+
// 构建 trace;失败处理器可显式开放修复重验路径
|
|
537
|
+
const trace = createToolTrace({
|
|
538
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
539
|
+
workflow_id: tCtx?.expansion?.workflow_trace?.workflow_id,
|
|
540
|
+
actual_side_effects: effectiveSideEffects,
|
|
541
|
+
next_allowed_tools: recoveryNextTools,
|
|
542
|
+
forbidden_tools: recoveryForbiddenTools,
|
|
543
|
+
authorization, bypass,
|
|
544
|
+
});
|
|
545
|
+
// 将 trace 写入 TaskContext
|
|
546
|
+
if (taskId) {
|
|
547
|
+
await taskContext.setToolTrace(taskId, trace, violations);
|
|
548
|
+
}
|
|
549
|
+
// 用 trace + next/forbidden 装饰响应
|
|
550
|
+
const response = {
|
|
551
|
+
...data,
|
|
552
|
+
tool_trace: trace,
|
|
553
|
+
next_allowed_tools: recoveryNextTools,
|
|
554
|
+
forbidden_tools: recoveryForbiddenTools,
|
|
555
|
+
};
|
|
556
|
+
// 基于状态的回退: 在响应中暴露降级 workflow
|
|
557
|
+
if (authorization.reason === "task in valid state for tool") {
|
|
558
|
+
response.degraded_workflow = true;
|
|
559
|
+
response.workflow_source = "state-based-fallback";
|
|
560
|
+
}
|
|
561
|
+
return {
|
|
562
|
+
content: [{ type: "text", text: JSON.stringify(response, null, 2) }],
|
|
563
|
+
isError: hasError ? true : undefined,
|
|
564
|
+
};
|
|
565
|
+
}
|
|
566
|
+
catch (err) {
|
|
567
|
+
// 通用重试原则:异常路径同样允许工具重试自身(与 handler error 路径一致)
|
|
568
|
+
const errorNextTools = [...contract.default_next_tools, name];
|
|
569
|
+
const errorForbiddenTools = contract.forbidden_next_tools.filter((t) => t !== name);
|
|
570
|
+
// P0-C1: 所有状态转换工具失败时统一标记 failed,不仅 sf_expand
|
|
571
|
+
// 防止 sf_classify/sf_plan/sf_verify/sf_learn 异常后任务卡 executing 30 分钟
|
|
572
|
+
const STATE_TRANSITION_TOOLS = new Set([
|
|
573
|
+
"sf_classify", "sf_plan", "sf_expand", "sf_verify", "sf_learn",
|
|
574
|
+
]);
|
|
575
|
+
if (STATE_TRANSITION_TOOLS.has(name) && taskId) {
|
|
576
|
+
// [ENG-7] 记录失败信息到 trace 再标记 failed,不跳过失败分析流程
|
|
577
|
+
try {
|
|
578
|
+
await taskContext.updateStatus(taskId, "failed");
|
|
579
|
+
}
|
|
580
|
+
catch (e) {
|
|
581
|
+
warn("中间件: 标记任务 failed 失败", errorMessage(e));
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
const errorTrace = createToolTrace({
|
|
585
|
+
tool_name: name, invocation_id: invocationId, task_id: taskId,
|
|
586
|
+
actual_side_effects: effectiveSideEffects,
|
|
587
|
+
next_allowed_tools: errorNextTools,
|
|
588
|
+
forbidden_tools: errorForbiddenTools,
|
|
589
|
+
authorization, bypass,
|
|
590
|
+
});
|
|
591
|
+
if (taskId) {
|
|
592
|
+
try {
|
|
593
|
+
await taskContext.setToolTrace(taskId, errorTrace, []);
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
warn("中间件: setToolTrace 失败 (error)", errorMessage(e));
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
content: [{ type: "text", text: JSON.stringify({
|
|
601
|
+
error: errorMessage(err),
|
|
602
|
+
tool_trace: errorTrace,
|
|
603
|
+
next_allowed_tools: errorNextTools,
|
|
604
|
+
forbidden_tools: errorForbiddenTools,
|
|
605
|
+
}) }],
|
|
606
|
+
isError: true,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
548
609
|
}
|
|
549
610
|
// ── 加载认知锚点上下文 ──
|
|
550
611
|
// 加载认知锚点上下文 — 必须提供相关性参数,禁止全量加载
|
|
@@ -568,7 +629,9 @@ export function createToolRegistrar(ctx) {
|
|
|
568
629
|
}
|
|
569
630
|
}
|
|
570
631
|
}
|
|
571
|
-
catch {
|
|
632
|
+
catch (e) {
|
|
633
|
+
warn("中间件: 认知锚点文件存在性检查失败", errorMessage(e));
|
|
634
|
+
}
|
|
572
635
|
const checked = (await lazyAnchor()).checkAnchorStaleness(mapData.anchors, existingFiles);
|
|
573
636
|
// 必须有 module 或 file_paths,否则不返回任何锚点(禁止全量加载)
|
|
574
637
|
if (!query.module && (!query.file_paths || query.file_paths.length === 0))
|