principles-disciple 1.61.0 → 1.63.0
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/openclaw.plugin.json +4 -4
- package/package.json +3 -1
- package/scripts/sync-plugin.mjs +28 -36
- package/src/core/event-log.ts +71 -5
- package/src/core/workflow-funnel-loader.ts +170 -0
- package/src/hooks/gate-block-helper.ts +1 -1
- package/src/hooks/gate.ts +62 -203
- package/src/service/evolution-worker.ts +10 -0
- package/src/service/nocturnal-service.ts +24 -3
- package/src/service/subagent-workflow/nocturnal-workflow-manager.ts +16 -0
- package/src/types/event-types.ts +103 -2
- package/tests/core/event-log.test.ts +56 -1
- package/tests/hooks/gate-rule-host-pipeline.test.ts +161 -316
- package/tests/service/evolution-worker.compilation-backfill.test.ts +5 -1
- package/src/hooks/bash-risk.ts +0 -175
- package/src/hooks/edit-verification.ts +0 -302
- package/src/hooks/gfi-gate.ts +0 -186
- package/src/hooks/progressive-trust-gate.ts +0 -183
- package/src/hooks/thinking-checkpoint.ts +0 -76
- package/tests/hooks/bash-risk-integration.test.ts +0 -137
- package/tests/hooks/bash-risk.test.ts +0 -81
- package/tests/hooks/edit-verification.test.ts +0 -678
- package/tests/hooks/gate-edit-verification-p1.test.ts +0 -632
- package/tests/hooks/gate-pipeline-integration.test.ts +0 -404
- package/tests/hooks/gate.test.ts +0 -271
- package/tests/hooks/gfi-gate-unit.test.ts +0 -422
- package/tests/hooks/gfi-gate.test.ts +0 -669
- package/tests/hooks/thinking-gate.test.ts +0 -313
package/src/hooks/gate.ts
CHANGED
|
@@ -1,45 +1,27 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Security Gate Hook -
|
|
2
|
+
* Security Gate Hook - Rule Host Only
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* This is the SINGLE AUTHORITATIVE orchestration path.
|
|
5
|
+
* All blocking logic is now dynamic via Rule Host — no hardcoded gates remain.
|
|
5
6
|
*
|
|
7
|
+
* Flow:
|
|
6
8
|
* 1. Early Return: Skip if not write/bash/agent tool or no workspace
|
|
7
|
-
* 2.
|
|
8
|
-
* 3. GFI Gate: Fatigue index-based blocking
|
|
9
|
-
* 4. Bash Mutation Detection: Heuristic for bash file modifications
|
|
10
|
-
* 4.5. Rule Host: Active code implementation evaluation (Phase 12)
|
|
11
|
-
* 5. Progressive Gate: EP tier-based access control
|
|
12
|
-
* 6. Edit Verification (P-03): Exact/fuzzy match for edit operations
|
|
13
|
-
*
|
|
14
|
-
* IMPORTANT: This is the SINGLE AUTHORITATIVE orchestration path.
|
|
15
|
-
* All policy modules (gfi-gate, progressive-trust-gate, rule-host) use the shared
|
|
16
|
-
* `recordGateBlockAndReturn` helper to ensure consistent block persistence.
|
|
17
|
-
*
|
|
18
|
-
* Zero-width character detection is handled in bash-risk.ts.
|
|
9
|
+
* 2. Rule Host: Dynamic principle-based evaluation (sole gate)
|
|
19
10
|
*/
|
|
20
11
|
|
|
21
12
|
import * as fs from 'fs';
|
|
22
13
|
import * as path from 'path';
|
|
23
|
-
import {
|
|
24
|
-
import { normalizeProfile } from '../core/profile.js';
|
|
25
|
-
import { estimateLineChanges } from '../core/risk-calculator.js';
|
|
14
|
+
import { normalizePath, planStatus } from '../utils/io.js';
|
|
26
15
|
import { WorkspaceContext } from '../core/workspace-context.js';
|
|
27
|
-
import { checkThinkingCheckpoint } from './thinking-checkpoint.js';
|
|
28
|
-
import { handleEditVerification } from './edit-verification.js';
|
|
29
|
-
import { checkGfiGate } from './gfi-gate.js';
|
|
30
|
-
import { checkProgressiveTrustGate } from './progressive-trust-gate.js';
|
|
31
16
|
import { recordGateBlockAndReturn } from './gate-block-helper.js';
|
|
32
17
|
import { RuleHost } from '../core/rule-host.js';
|
|
33
18
|
import type { RuleHostInput } from '../core/rule-host-types.js';
|
|
34
19
|
import type { PluginHookBeforeToolCallEvent, PluginHookToolContext, PluginHookBeforeToolCallResult, PluginLogger } from '../openclaw-sdk.js';
|
|
35
|
-
import {
|
|
36
|
-
AGENT_TOOLS,
|
|
37
|
-
BASH_TOOLS_SET,
|
|
38
|
-
WRITE_TOOLS,
|
|
39
|
-
} from '../constants/tools.js';
|
|
20
|
+
import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js';
|
|
40
21
|
import { getSession, hasRecentThinking } from '../core/session-tracker.js';
|
|
41
22
|
import { getEvolutionEngine } from '../core/evolution-engine.js';
|
|
42
23
|
import { EventLogService } from '../core/event-log.js';
|
|
24
|
+
import { estimateLineChanges } from '../core/risk-calculator.js';
|
|
43
25
|
|
|
44
26
|
export function handleBeforeToolCall(
|
|
45
27
|
event: PluginHookBeforeToolCallEvent,
|
|
@@ -58,238 +40,122 @@ export function handleBeforeToolCall(
|
|
|
58
40
|
|
|
59
41
|
const wctx = WorkspaceContext.fromHookContext(ctx);
|
|
60
42
|
|
|
61
|
-
// 2.
|
|
62
|
-
const profilePath = wctx.resolve('PROFILE');
|
|
63
|
-
let profile = {
|
|
64
|
-
risk_paths: [] as string[],
|
|
65
|
-
gate: { require_plan_for_risk_paths: true },
|
|
66
|
-
progressive_gate: {
|
|
67
|
-
enabled: true,
|
|
68
|
-
plan_approvals: {
|
|
69
|
-
enabled: false,
|
|
70
|
-
max_lines_override: -1,
|
|
71
|
-
allowed_patterns: [] as string[],
|
|
72
|
-
allowed_operations: [] as string[],
|
|
73
|
-
}
|
|
74
|
-
},
|
|
75
|
-
edit_verification: {
|
|
76
|
-
enabled: true,
|
|
77
|
-
max_file_size_bytes: 10 * 1024 * 1024,
|
|
78
|
-
fuzzy_match_enabled: true,
|
|
79
|
-
fuzzy_match_threshold: 0.8,
|
|
80
|
-
skip_large_file_action: 'warn' as 'warn' | 'block',
|
|
81
|
-
},
|
|
82
|
-
thinking_checkpoint: {
|
|
83
|
-
enabled: false, // Default OFF
|
|
84
|
-
window_ms: 5 * 60 * 1000,
|
|
85
|
-
high_risk_tools: ['run_shell_command', 'delete_file', 'move_file'],
|
|
86
|
-
}
|
|
87
|
-
};
|
|
88
|
-
|
|
89
|
-
if (fs.existsSync(profilePath)) {
|
|
90
|
-
try {
|
|
91
|
-
const rawProfile = JSON.parse(fs.readFileSync(profilePath, 'utf8'));
|
|
92
|
-
profile = normalizeProfile(rawProfile);
|
|
93
|
-
} catch (e) {
|
|
94
|
-
logger?.error?.(`[PD_GATE] Failed to parse PROFILE.json: ${String(e)}`);
|
|
95
|
-
}
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
99
|
-
// POLICY STEP 1: Thinking OS Checkpoint (P-10)
|
|
100
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
101
|
-
// Only enforced when thinking_checkpoint.enabled = true in PROFILE.json
|
|
102
|
-
const thinkingResult = checkThinkingCheckpoint(
|
|
103
|
-
event,
|
|
104
|
-
profile.thinking_checkpoint || {},
|
|
105
|
-
ctx.sessionId,
|
|
106
|
-
logger
|
|
107
|
-
);
|
|
108
|
-
if (thinkingResult) {
|
|
109
|
-
return thinkingResult;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
113
|
-
// POLICY STEP 2: GFI Gate - Hard Intercept
|
|
114
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
115
|
-
// 根据 GFI (疲劳指数) 精细化拦截工具调用
|
|
116
|
-
// 注意:TIER 0 (只读工具) 已在早期过滤中放行,此处不检查
|
|
117
|
-
const gfiGateConfig = wctx.config.get('gfi_gate');
|
|
118
|
-
const gfiResult = checkGfiGate(event, wctx, ctx.sessionId, gfiGateConfig, logger);
|
|
119
|
-
if (gfiResult) {
|
|
120
|
-
return gfiResult;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
// Merge pluginConfig (OpenClaw UI settings)
|
|
124
|
-
const configRiskPaths = (ctx.pluginConfig?.riskPaths as string[] | undefined) ?? [];
|
|
125
|
-
if (configRiskPaths.length > 0) {
|
|
126
|
-
profile.risk_paths = [...new Set([...profile.risk_paths, ...configRiskPaths])];
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
// 3. Resolve the target file path
|
|
43
|
+
// 2. Resolve the target file path
|
|
130
44
|
let filePath = event.params.file_path || event.params.path || event.params.file || event.params.target;
|
|
131
45
|
|
|
132
46
|
// Heuristic for bash mutation detection
|
|
133
47
|
if (isBash && !filePath) {
|
|
134
|
-
const command = String(event.params.command || event.params.args ||
|
|
48
|
+
const command = String(event.params.command || event.params.args || '');
|
|
135
49
|
const mutationMatch = /(?:>|>>|sed\s+-i|rm|mv|mkdir|touch|cp)\s+(?:-[a-zA-Z]+\s+)*([^\s;&|<>]+)/.exec(command);
|
|
136
50
|
|
|
137
51
|
if (mutationMatch) {
|
|
138
|
-
|
|
139
|
-
|
|
140
52
|
filePath = mutationMatch[1];
|
|
141
53
|
} else {
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
if (hasRiskPath && isMutation) {
|
|
146
|
-
filePath = command;
|
|
147
|
-
} else {
|
|
148
|
-
return;
|
|
149
|
-
}
|
|
54
|
+
// Bash command without a clear file target — let it through to Rule Host
|
|
55
|
+
filePath = command;
|
|
150
56
|
}
|
|
151
57
|
}
|
|
152
58
|
|
|
153
59
|
if (typeof filePath !== 'string') return;
|
|
154
60
|
|
|
155
61
|
const relPath = normalizePath(filePath, ctx.workspaceDir);
|
|
156
|
-
const risky = (isBash && filePath.includes(' '))
|
|
157
|
-
? profile.risk_paths.some(rp => filePath.includes(rp))
|
|
158
|
-
: isRisky(relPath, profile.risk_paths);
|
|
159
62
|
|
|
160
|
-
//
|
|
161
|
-
// POLICY STEP 2.5: Rule Host Evaluation (Phase 12, D-01/D-03)
|
|
162
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
163
|
-
// Inserted between GFI gate and Progressive Gate so principle rules can act
|
|
164
|
-
// before the capability-boundary fallback. Active code implementations run
|
|
165
|
-
// through a constrained vm context with minimal helpers only.
|
|
63
|
+
// 3. Rule Host Evaluation — sole gate
|
|
166
64
|
try {
|
|
167
65
|
const ruleHost = new RuleHost(wctx.stateDir, logger);
|
|
168
66
|
const hostInput: RuleHostInput = {
|
|
169
67
|
action: {
|
|
170
68
|
toolName: event.toolName,
|
|
171
69
|
normalizedPath: relPath,
|
|
172
|
-
|
|
173
|
-
|
|
174
70
|
paramsSummary: _extractParamsSummary(event.params),
|
|
175
71
|
},
|
|
176
72
|
workspace: {
|
|
177
|
-
isRiskPath:
|
|
178
|
-
|
|
179
|
-
|
|
73
|
+
isRiskPath: false, // Rule Host determines risk dynamically
|
|
180
74
|
planStatus: _getPlanStatus(ctx.workspaceDir),
|
|
181
|
-
|
|
182
|
-
|
|
183
75
|
hasPlanFile: _hasPlanFile(ctx.workspaceDir),
|
|
184
76
|
},
|
|
185
77
|
session: {
|
|
186
78
|
sessionId: ctx.sessionId,
|
|
187
|
-
|
|
188
|
-
|
|
189
79
|
currentGfi: _getCurrentGfi(ctx.sessionId),
|
|
190
|
-
|
|
191
|
-
|
|
192
80
|
recentThinking: _hasRecentThinking(ctx.sessionId),
|
|
193
81
|
},
|
|
194
82
|
evolution: {
|
|
195
|
-
|
|
196
|
-
|
|
197
83
|
epTier: _getEpTier(wctx.workspaceDir),
|
|
198
84
|
},
|
|
199
85
|
derived: {
|
|
200
86
|
estimatedLineChanges: estimateLineChanges({ toolName: event.toolName, params: event.params }),
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
bashRisk: _getBashRisk(event, profile),
|
|
87
|
+
bashRisk: _getBashRisk(event),
|
|
204
88
|
},
|
|
205
89
|
};
|
|
206
90
|
|
|
207
91
|
const hostResult = ruleHost.evaluate(hostInput);
|
|
208
|
-
|
|
209
|
-
|
|
92
|
+
|
|
93
|
+
// Always emit rulehost_evaluated
|
|
94
|
+
try {
|
|
95
|
+
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
96
|
+
eventLog.recordRuleHostEvaluated({
|
|
97
|
+
toolName: event.toolName,
|
|
98
|
+
filePath: relPath,
|
|
99
|
+
matched: hostResult?.matched ?? false,
|
|
100
|
+
decision: hostResult?.decision ?? 'allow',
|
|
101
|
+
ruleId: hostResult?.ruleId,
|
|
102
|
+
});
|
|
103
|
+
} catch (evErr) {
|
|
104
|
+
logger?.warn?.(`[PD_GATE] Failed to record rulehost_evaluated: ${String(evErr)}`);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (hostResult?.decision === 'block') {
|
|
210
108
|
try {
|
|
211
109
|
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
212
110
|
eventLog.recordRuleEnforced({
|
|
213
111
|
ruleId: hostResult.ruleId || 'unknown',
|
|
214
112
|
principleId: hostResult.principleId || 'unknown',
|
|
215
|
-
enforcement:
|
|
113
|
+
enforcement: 'block',
|
|
114
|
+
toolName: event.toolName,
|
|
115
|
+
filePath: relPath,
|
|
116
|
+
});
|
|
117
|
+
eventLog.recordRuleHostBlocked({
|
|
216
118
|
toolName: event.toolName,
|
|
217
119
|
filePath: relPath,
|
|
120
|
+
reason: hostResult.reason,
|
|
121
|
+
ruleId: hostResult.ruleId,
|
|
218
122
|
});
|
|
219
123
|
} catch (evErr) {
|
|
220
|
-
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced
|
|
124
|
+
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_blocked: ${String(evErr)}`);
|
|
221
125
|
}
|
|
222
126
|
|
|
223
|
-
const reason = hostResult.decision === 'requireApproval'
|
|
224
|
-
? `[Rule Host] Approval required: ${hostResult.reason}`
|
|
225
|
-
: hostResult.reason;
|
|
226
127
|
return recordGateBlockAndReturn(wctx, {
|
|
227
128
|
filePath: relPath,
|
|
228
|
-
reason,
|
|
129
|
+
reason: hostResult.reason,
|
|
229
130
|
toolName: event.toolName,
|
|
230
131
|
sessionId: ctx.sessionId,
|
|
231
132
|
blockSource: 'rule-host',
|
|
232
133
|
}, logger);
|
|
233
134
|
}
|
|
234
|
-
} catch (hostError: unknown) {
|
|
235
|
-
// D-08: Conservative degradation — log and continue to Progressive Gate
|
|
236
|
-
logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, degrading conservatively: ${String(hostError)}`);
|
|
237
|
-
}
|
|
238
135
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
event,
|
|
248
|
-
wctx,
|
|
249
|
-
relPath,
|
|
250
|
-
risky,
|
|
251
|
-
lineChanges,
|
|
252
|
-
logger,
|
|
253
|
-
ctx,
|
|
254
|
-
profile
|
|
255
|
-
);
|
|
256
|
-
if (progressiveGateResult) {
|
|
257
|
-
return progressiveGateResult;
|
|
258
|
-
}
|
|
259
|
-
// NOTE: Do NOT return here! Continue to edit verification.
|
|
260
|
-
// All allowed operations (regardless of EP tier) should still run edit verification.
|
|
261
|
-
} else {
|
|
262
|
-
// FALLBACK: Legacy Gate Logic (when progressive gate is disabled)
|
|
263
|
-
if (risky && profile.gate?.require_plan_for_risk_paths) {
|
|
264
|
-
const planStatus = getPlanStatus(ctx.workspaceDir);
|
|
265
|
-
if (planStatus !== 'READY') {
|
|
266
|
-
return recordGateBlockAndReturn(wctx, {
|
|
136
|
+
if (hostResult?.decision === 'requireApproval') {
|
|
137
|
+
try {
|
|
138
|
+
const eventLog = EventLogService.get(wctx.stateDir, logger as PluginLogger | undefined);
|
|
139
|
+
eventLog.recordRuleEnforced({
|
|
140
|
+
ruleId: hostResult.ruleId || 'unknown',
|
|
141
|
+
principleId: hostResult.principleId || 'unknown',
|
|
142
|
+
enforcement: 'requireApproval',
|
|
143
|
+
toolName: event.toolName,
|
|
267
144
|
filePath: relPath,
|
|
268
|
-
|
|
145
|
+
});
|
|
146
|
+
eventLog.recordRuleHostRequireApproval({
|
|
269
147
|
toolName: event.toolName,
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
148
|
+
filePath: relPath,
|
|
149
|
+
reason: hostResult.reason,
|
|
150
|
+
ruleId: hostResult.ruleId,
|
|
151
|
+
});
|
|
152
|
+
} catch (evErr) {
|
|
153
|
+
logger?.warn?.(`[PD_GATE] Failed to record rule_enforced/rulehost_requireApproval: ${String(evErr)}`);
|
|
273
154
|
}
|
|
274
155
|
}
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
// POLICY STEP 4: Edit Tool Verification (P-03)
|
|
279
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
280
|
-
// This MUST run after all other gate checks for ALL tools.
|
|
281
|
-
// Edit verification ensures oldText matches the actual file content.
|
|
282
|
-
if (event.toolName === 'edit' && profile.edit_verification?.enabled !== false) {
|
|
283
|
-
const verifyResult = handleEditVerification(event, wctx, ctx, {
|
|
284
|
-
enabled: profile.edit_verification.enabled,
|
|
285
|
-
max_file_size_bytes: profile.edit_verification.max_file_size_bytes,
|
|
286
|
-
fuzzy_match_enabled: profile.edit_verification.fuzzy_match_enabled,
|
|
287
|
-
fuzzy_match_threshold: profile.edit_verification.fuzzy_match_threshold,
|
|
288
|
-
skip_large_file_action: profile.edit_verification.skip_large_file_action as 'warn' | 'block' | undefined,
|
|
289
|
-
});
|
|
290
|
-
if (verifyResult) {
|
|
291
|
-
return verifyResult; // Block or modify params
|
|
292
|
-
}
|
|
156
|
+
} catch (hostError: unknown) {
|
|
157
|
+
// D-08: Conservative degradation — log and allow on Rule Host failure
|
|
158
|
+
logger.warn?.(`[PD_GATE:RULE_HOST] Host evaluation failed, allowing conservatively: ${String(hostError)}`);
|
|
293
159
|
}
|
|
294
160
|
|
|
295
161
|
// All checks passed - allow the operation
|
|
@@ -297,9 +163,7 @@ export function handleBeforeToolCall(
|
|
|
297
163
|
}
|
|
298
164
|
|
|
299
165
|
// ---------------------------------------------------------------------------
|
|
300
|
-
// Private helpers
|
|
301
|
-
// These are NOT passed to hosted implementations — they only populate the
|
|
302
|
-
// frozen snapshot that implementations receive.
|
|
166
|
+
// Private helpers
|
|
303
167
|
// ---------------------------------------------------------------------------
|
|
304
168
|
|
|
305
169
|
function _extractParamsSummary(params: Record<string, unknown>): Record<string, unknown> {
|
|
@@ -315,7 +179,7 @@ function _extractParamsSummary(params: Record<string, unknown>): Record<string,
|
|
|
315
179
|
|
|
316
180
|
function _getPlanStatus(workspaceDir: string): 'NONE' | 'DRAFT' | 'READY' | 'UNKNOWN' {
|
|
317
181
|
try {
|
|
318
|
-
const status =
|
|
182
|
+
const status = planStatus(workspaceDir);
|
|
319
183
|
if (status === 'READY') return 'READY';
|
|
320
184
|
if (status === 'DRAFT') return 'DRAFT';
|
|
321
185
|
if (status === '') return 'NONE';
|
|
@@ -360,12 +224,7 @@ function _getEpTier(workspaceDir: string): number {
|
|
|
360
224
|
}
|
|
361
225
|
}
|
|
362
226
|
|
|
363
|
-
|
|
364
|
-
function _getBashRisk(
|
|
365
|
-
event: PluginHookBeforeToolCallEvent,
|
|
366
|
-
_profile: { risk_paths: string[] }
|
|
367
|
-
): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
368
|
-
|
|
227
|
+
function _getBashRisk(event: PluginHookBeforeToolCallEvent): 'safe' | 'normal' | 'dangerous' | 'unknown' {
|
|
369
228
|
if (!BASH_TOOLS_SET.has(event.toolName)) return 'unknown';
|
|
370
229
|
try {
|
|
371
230
|
const command = String(event.params.command || event.params.args || '');
|
|
@@ -957,6 +957,16 @@ async function processEvolutionQueue(wctx: WorkspaceContext, logger: PluginLogge
|
|
|
957
957
|
if (presentPhases.length < requiredPhases.length) {
|
|
958
958
|
const missing = requiredPhases.filter(p => !phases[p]);
|
|
959
959
|
if (logger) logger.warn(`[PD:EvolutionWorker] Report for task ${task.id} incomplete — missing phases: ${missing.join(', ')} (present: ${presentPhases.length}/${requiredPhases.length})`);
|
|
960
|
+
// PD-FUNNEL-1.1: Record incomplete_fields event BEFORE retry so funnel can see it.
|
|
961
|
+
// The phase-completeness check requeues incomplete reports with continue; without
|
|
962
|
+
// this record the funnel would have no signal for JSON-present-but-incomplete cases.
|
|
963
|
+
if (eventLog) {
|
|
964
|
+
eventLog.recordDiagnosticianReport({
|
|
965
|
+
taskId: task.id,
|
|
966
|
+
reportPath,
|
|
967
|
+
category: 'incomplete_fields',
|
|
968
|
+
});
|
|
969
|
+
}
|
|
960
970
|
// Treat as retryable failure: don't mark success, let retry logic kick in
|
|
961
971
|
reportParsed = false;
|
|
962
972
|
// Also delete the incomplete marker so next heartbeat re-runs the diagnostician
|
|
@@ -104,6 +104,7 @@ import { registerSample } from '../core/nocturnal-dataset.js';
|
|
|
104
104
|
import { getPrincipleState, setPrincipleState } from '../core/principle-training-state.js';
|
|
105
105
|
import type { Implementation } from '../types/principle-tree-schema.js';
|
|
106
106
|
import { validateNocturnalSnapshotIngress } from '../core/nocturnal-snapshot-contract.js';
|
|
107
|
+
import { EventLogService } from '../core/event-log.js';
|
|
107
108
|
|
|
108
109
|
|
|
109
110
|
// ---------------------------------------------------------------------------
|
|
@@ -522,9 +523,20 @@ function persistCodeCandidate(
|
|
|
522
523
|
try {
|
|
523
524
|
refreshPrincipleLifecycle(workspaceDir, stateDir);
|
|
524
525
|
} catch (err) {
|
|
525
|
-
|
|
526
526
|
console.warn('[nocturnal-service] Lifecycle refresh failed after code candidate persistence:', err instanceof Error ? err.stack : err);
|
|
527
527
|
}
|
|
528
|
+
// PD-FUNNEL-2.3: Emit nocturnal_code_candidate_created event
|
|
529
|
+
try {
|
|
530
|
+
const eventLog = EventLogService.get(stateDir, undefined);
|
|
531
|
+
eventLog.recordNocturnalCodeCandidateCreated({
|
|
532
|
+
implementationId,
|
|
533
|
+
artifactId,
|
|
534
|
+
ruleId: parsedArtificer.ruleId,
|
|
535
|
+
persistedPath: assetRoot,
|
|
536
|
+
});
|
|
537
|
+
} catch (evErr) {
|
|
538
|
+
console.warn(`[nocturnal-service] Failed to record nocturnal_code_candidate_created: ${String(evErr)}`);
|
|
539
|
+
}
|
|
528
540
|
return {
|
|
529
541
|
status: 'persisted_candidate',
|
|
530
542
|
ruleResolution: {
|
|
@@ -1039,13 +1051,22 @@ export function executeNocturnalReflection(
|
|
|
1039
1051
|
boundedAction: execResult.boundedAction,
|
|
1040
1052
|
};
|
|
1041
1053
|
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
1054
|
let persistedPath: string;
|
|
1045
1055
|
try {
|
|
1046
1056
|
persistedPath = persistArtifact(workspaceDir, artifactWithBoundedAction);
|
|
1047
1057
|
diagnostics.persisted = true;
|
|
1048
1058
|
diagnostics.persistedPath = persistedPath;
|
|
1059
|
+
// PD-FUNNEL-2.3: Emit nocturnal_artifact_persisted event
|
|
1060
|
+
try {
|
|
1061
|
+
const eventLog = EventLogService.get(stateDir, undefined);
|
|
1062
|
+
eventLog.recordNocturnalArtifactPersisted({
|
|
1063
|
+
artifactId: artifactWithBoundedAction.artifactId,
|
|
1064
|
+
principleId: artifactWithBoundedAction.principleId,
|
|
1065
|
+
persistedPath,
|
|
1066
|
+
});
|
|
1067
|
+
} catch (evErr) {
|
|
1068
|
+
console.warn(`[nocturnal-service] Failed to record nocturnal_artifact_persisted: ${String(evErr)}`);
|
|
1069
|
+
}
|
|
1049
1070
|
} catch (err) {
|
|
1050
1071
|
void recordRunEnd(stateDir, 'failed', { reason: `persistence error: ${String(err)}` }).catch((e) => {
|
|
1051
1072
|
warn(`[nocturnal-service] Failed to record run end (persistence failed): ${String(e)}`);
|
|
@@ -41,6 +41,7 @@ import * as fs from 'fs';
|
|
|
41
41
|
import * as path from 'path';
|
|
42
42
|
import { validateNocturnalSnapshotIngress } from '../../core/nocturnal-snapshot-contract.js';
|
|
43
43
|
import { isSubagentRuntimeAvailable } from '../../utils/subagent-probe.js';
|
|
44
|
+
import { EventLogService } from '../../core/event-log.js';
|
|
44
45
|
|
|
45
46
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
46
47
|
// NocturnalResult Type Alias
|
|
@@ -290,6 +291,21 @@ export class NocturnalWorkflowManager implements WorkflowManager {
|
|
|
290
291
|
artifactId: result.diagnostics?.persistedPath,
|
|
291
292
|
});
|
|
292
293
|
this.completedWorkflows.set(workflowId, Date.now());
|
|
294
|
+
// PD-FUNNEL-2.3: Emit nocturnal_dreamer_completed event
|
|
295
|
+
const chainMode: 'trinity' | 'single-reflector' =
|
|
296
|
+
result.trinityTelemetry ? 'trinity' : 'single-reflector';
|
|
297
|
+
try {
|
|
298
|
+
const eventLog = EventLogService.get(this.stateDir, this.logger as unknown as import('../../openclaw-sdk.js').PluginLogger);
|
|
299
|
+
eventLog.recordNocturnalDreamerCompleted({
|
|
300
|
+
workflowId,
|
|
301
|
+
principleId: result.artifact?.principleId ?? 'unknown',
|
|
302
|
+
sessionId: result.snapshot?.sessionId ?? 'unknown',
|
|
303
|
+
candidateCount: result.trinityTelemetry?.candidateCount ?? 1,
|
|
304
|
+
chainMode,
|
|
305
|
+
});
|
|
306
|
+
} catch (evErr) {
|
|
307
|
+
this.logger.warn?.(`[PD:NocturnalWorkflow] Failed to record nocturnal_dreamer_completed: ${String(evErr)}`);
|
|
308
|
+
}
|
|
293
309
|
} else {
|
|
294
310
|
const reason = result.noTargetSelected ? 'no_target_selected' : 'validation_failed';
|
|
295
311
|
const failuresSummary = result.validationFailures?.length > 0
|
package/src/types/event-types.ts
CHANGED
|
@@ -23,7 +23,15 @@ export type EventType =
|
|
|
23
23
|
| 'heartbeat_diagnosis' // Heartbeat injected diagnostician tasks
|
|
24
24
|
| 'diagnostician_report' // Diagnostician completed and wrote report
|
|
25
25
|
| 'principle_candidate' // Principle candidate created from report
|
|
26
|
-
| 'rule_enforced'
|
|
26
|
+
| 'rule_enforced' // Rule enforced (matched) during tool call
|
|
27
|
+
// C: Nocturnal funnel stage events (PD-FUNNEL-2.3)
|
|
28
|
+
| 'nocturnal_dreamer_completed'
|
|
29
|
+
| 'nocturnal_artifact_persisted'
|
|
30
|
+
| 'nocturnal_code_candidate_created'
|
|
31
|
+
// C: RuleHost funnel events (PD-FUNNEL-2.4)
|
|
32
|
+
| 'rulehost_evaluated'
|
|
33
|
+
| 'rulehost_blocked'
|
|
34
|
+
| 'rulehost_requireApproval';
|
|
27
35
|
|
|
28
36
|
export type EventCategory =
|
|
29
37
|
| 'success'
|
|
@@ -42,7 +50,11 @@ export type EventCategory =
|
|
|
42
50
|
| 'written'
|
|
43
51
|
| 'injected'
|
|
44
52
|
| 'created'
|
|
45
|
-
| 'matched'
|
|
53
|
+
| 'matched'
|
|
54
|
+
// C: New categories for RuleHost funnel (PD-FUNNEL-2.4) — completed/created already exist
|
|
55
|
+
| 'evaluated' // Used by: rulehost_evaluated
|
|
56
|
+
| 'blocked' // Used by: rulehost_blocked
|
|
57
|
+
| 'requireApproval'; // Used by: rulehost_requireApproval
|
|
46
58
|
|
|
47
59
|
/**
|
|
48
60
|
* Base event structure for JSONL logging.
|
|
@@ -237,6 +249,77 @@ export interface RuleEnforcedEventData {
|
|
|
237
249
|
filePath: string;
|
|
238
250
|
}
|
|
239
251
|
|
|
252
|
+
// ============== Nocturnal Funnel Events (PD-FUNNEL-2.3) ==============
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* nocturnal_dreamer_completed — Trinity Dreamer stage completed.
|
|
256
|
+
* Emitted from nocturnal-workflow-manager.ts after Trinity chain success.
|
|
257
|
+
*/
|
|
258
|
+
export interface NocturnalDreamerCompletedEventData {
|
|
259
|
+
workflowId: string;
|
|
260
|
+
principleId: string;
|
|
261
|
+
sessionId: string;
|
|
262
|
+
candidateCount: number;
|
|
263
|
+
chainMode: 'trinity' | 'single-reflector';
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* nocturnal_artifact_persisted — Artifact saved to .state/nocturnal/samples/.
|
|
268
|
+
* Emitted from nocturnal-service.ts persistArtifact() after atomicWriteFileSync.
|
|
269
|
+
*/
|
|
270
|
+
export interface NocturnalArtifactPersistedEventData {
|
|
271
|
+
artifactId: string;
|
|
272
|
+
principleId: string;
|
|
273
|
+
persistedPath: string;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* nocturnal_code_candidate_created — Rule implementation candidate persisted.
|
|
278
|
+
* Emitted from nocturnal-service.ts persistCodeCandidate() after successful creation.
|
|
279
|
+
*/
|
|
280
|
+
export interface NocturnalCodeCandidateCreatedEventData {
|
|
281
|
+
implementationId: string;
|
|
282
|
+
artifactId: string;
|
|
283
|
+
ruleId: string;
|
|
284
|
+
persistedPath: string;
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// ============== RuleHost Funnel Events (PD-FUNNEL-2.4) ==============
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* rulehost_evaluated — RuleHost.evaluate() was called.
|
|
291
|
+
* Emitted from gate.ts for every evaluate() call (matched or not).
|
|
292
|
+
*/
|
|
293
|
+
export interface RuleHostEvaluatedEventData {
|
|
294
|
+
toolName: string;
|
|
295
|
+
filePath: string;
|
|
296
|
+
matched: boolean;
|
|
297
|
+
decision: 'allow' | 'block' | 'requireApproval';
|
|
298
|
+
ruleId?: string;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* rulehost_blocked — Tool call was blocked by RuleHost.
|
|
303
|
+
* Emitted from gate.ts when hostResult.decision === 'block'.
|
|
304
|
+
*/
|
|
305
|
+
export interface RuleHostBlockedEventData {
|
|
306
|
+
toolName: string;
|
|
307
|
+
filePath: string;
|
|
308
|
+
reason: string;
|
|
309
|
+
ruleId?: string;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
/**
|
|
313
|
+
* rulehost_requireApproval — Tool call requires approval by RuleHost.
|
|
314
|
+
* Emitted from gate.ts when hostResult.decision === 'requireApproval'.
|
|
315
|
+
*/
|
|
316
|
+
export interface RuleHostRequireApprovalEventData {
|
|
317
|
+
toolName: string;
|
|
318
|
+
filePath: string;
|
|
319
|
+
reason: string;
|
|
320
|
+
ruleId?: string;
|
|
321
|
+
}
|
|
322
|
+
|
|
240
323
|
// ============== Daily Statistics ==============
|
|
241
324
|
|
|
242
325
|
export interface ToolCallStats {
|
|
@@ -335,6 +418,15 @@ export interface EvolutionStats {
|
|
|
335
418
|
reportsIncompleteFields: number;
|
|
336
419
|
principleCandidatesCreated: number;
|
|
337
420
|
rulesEnforced: number;
|
|
421
|
+
// C: Nocturnal funnel counters (PD-FUNNEL-2.3)
|
|
422
|
+
nocturnalDreamerCompleted: number;
|
|
423
|
+
nocturnalTrinityCompleted: number;
|
|
424
|
+
nocturnalArtifactPersisted: number;
|
|
425
|
+
nocturnalCodeCandidateCreated: number;
|
|
426
|
+
// C: RuleHost funnel counters (PD-FUNNEL-2.4)
|
|
427
|
+
rulehostEvaluated: number;
|
|
428
|
+
rulehostBlocked: number;
|
|
429
|
+
rulehostRequireApproval: number;
|
|
338
430
|
}
|
|
339
431
|
|
|
340
432
|
export interface HookStats {
|
|
@@ -501,6 +593,15 @@ export function createEmptyDailyStats(date: string): DailyStats {
|
|
|
501
593
|
reportsIncompleteFields: 0,
|
|
502
594
|
principleCandidatesCreated: 0,
|
|
503
595
|
rulesEnforced: 0,
|
|
596
|
+
// C: Nocturnal funnel counters (PD-FUNNEL-2.3)
|
|
597
|
+
nocturnalDreamerCompleted: 0,
|
|
598
|
+
nocturnalTrinityCompleted: 0,
|
|
599
|
+
nocturnalArtifactPersisted: 0,
|
|
600
|
+
nocturnalCodeCandidateCreated: 0,
|
|
601
|
+
// C: RuleHost funnel counters (PD-FUNNEL-2.4)
|
|
602
|
+
rulehostEvaluated: 0,
|
|
603
|
+
rulehostBlocked: 0,
|
|
604
|
+
rulehostRequireApproval: 0,
|
|
504
605
|
},
|
|
505
606
|
hooks: {
|
|
506
607
|
total: 0,
|