principles-disciple 1.158.0 → 1.160.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/dist/hooks/gate.js +1 -1
- package/dist/index.js +1 -1
- package/openclaw.plugin.json +1 -1
- package/package.json +1 -1
- package/dist/core/risk-calculator.d.ts +0 -22
- package/dist/core/risk-calculator.js +0 -88
- package/dist/core/surface-guard.d.ts +0 -31
- package/dist/core/surface-guard.js +0 -131
package/dist/hooks/gate.js
CHANGED
|
@@ -17,7 +17,7 @@ import { AGENT_TOOLS, BASH_TOOLS_SET, WRITE_TOOLS } from '../constants/tools.js'
|
|
|
17
17
|
import { getSession, hasRecentThinking } from '../core/session-tracker.js';
|
|
18
18
|
import { getEvolutionEngine } from '../core/evolution-engine.js';
|
|
19
19
|
import { EventLogService } from '../core/event-log.js';
|
|
20
|
-
import { estimateLineChanges } from '
|
|
20
|
+
import { estimateLineChanges } from '@principles/core/runtime-v2';
|
|
21
21
|
export function handleBeforeToolCall(event, ctx) {
|
|
22
22
|
const logger = ctx.logger || console;
|
|
23
23
|
// 1. Identify tool type
|
package/dist/index.js
CHANGED
|
@@ -38,7 +38,7 @@ import { SystemLogger } from './core/system-logger.js';
|
|
|
38
38
|
import { PathResolver } from './core/path-resolver.js';
|
|
39
39
|
import { resolveCommandWorkspaceDir, resolveToolHookWorkspaceDirSafe, resolveHookWorkspaceDir } from './utils/workspace-resolver.js';
|
|
40
40
|
import { validateWorkspaceDir } from './core/workspace-dir-validation.js';
|
|
41
|
-
import { checkSurfaceGuard, guardHook, guardService } from '
|
|
41
|
+
import { checkSurfaceGuard, guardHook, guardService } from '@principles/core/runtime-v2';
|
|
42
42
|
// Track started workspaces — one-time init + evolution worker per workspace
|
|
43
43
|
const startedWorkspaces = new Set();
|
|
44
44
|
// ── Conversation Access Health Check (PRI-343) ────────────────────────────
|
package/openclaw.plugin.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"id": "principles-disciple",
|
|
3
3
|
"name": "Principles Disciple",
|
|
4
4
|
"description": "Evolutionary programming agent framework with strategic guardrails and reflection loops.",
|
|
5
|
-
"version": "1.
|
|
5
|
+
"version": "1.160.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
export type RiskLevel = 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL';
|
|
2
|
-
export interface FileModification {
|
|
3
|
-
toolName: string;
|
|
4
|
-
params: Record<string, unknown>;
|
|
5
|
-
}
|
|
6
|
-
export declare function estimateLineChanges(modification: FileModification): number;
|
|
7
|
-
export declare function assessRiskLevel(filePath: string, modification: FileModification, riskPaths: string[]): RiskLevel;
|
|
8
|
-
/**
|
|
9
|
-
* Get the total line count of a target file.
|
|
10
|
-
* @param absoluteFilePath - Absolute path to the file
|
|
11
|
-
* @returns File line count, or null if file doesn't exist or can't be read
|
|
12
|
-
*/
|
|
13
|
-
export declare function getTargetFileLineCount(absoluteFilePath: string): number | null;
|
|
14
|
-
/**
|
|
15
|
-
* Calculate the effective line limit based on percentage of target file.
|
|
16
|
-
* @param targetLineCount - Total lines in target file
|
|
17
|
-
* @param percentage - Allowed percentage (0-100)
|
|
18
|
-
* @param minLines - Absolute minimum threshold
|
|
19
|
-
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
20
|
-
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
21
|
-
*/
|
|
22
|
-
export declare function calculatePercentageThreshold(targetLineCount: number, percentage: number, minLines: number, maxLines?: number): number;
|
|
@@ -1,88 +0,0 @@
|
|
|
1
|
-
/* global NodeJS */
|
|
2
|
-
import * as fs from 'fs';
|
|
3
|
-
import { isRisky } from '../utils/io.js';
|
|
4
|
-
export function estimateLineChanges(modification) {
|
|
5
|
-
const { toolName, params } = modification;
|
|
6
|
-
if (toolName === 'write_file' || toolName === 'write') {
|
|
7
|
-
const content = params.content || '';
|
|
8
|
-
return content.split('\n').length;
|
|
9
|
-
}
|
|
10
|
-
if (toolName === 'replace' || toolName === 'edit') {
|
|
11
|
-
const newContent = params.new_string || params.newText || '';
|
|
12
|
-
return newContent.split('\n').length;
|
|
13
|
-
}
|
|
14
|
-
if (toolName === 'apply_patch' || toolName === 'patch') {
|
|
15
|
-
const patch = params.patch || '';
|
|
16
|
-
// Rough estimate for patch files
|
|
17
|
-
return patch.split('\n').filter((l) => l.startsWith('+') || l.startsWith('-')).length;
|
|
18
|
-
}
|
|
19
|
-
if (toolName === 'delete_file') {
|
|
20
|
-
// Deleting a file is considered a significant change, but we don't know the size.
|
|
21
|
-
// We'll treat it as a medium-to-large size change.
|
|
22
|
-
return 50;
|
|
23
|
-
}
|
|
24
|
-
return 0;
|
|
25
|
-
}
|
|
26
|
-
export function assessRiskLevel(filePath, modification, riskPaths) {
|
|
27
|
-
const isRiskPath = isRisky(filePath, riskPaths);
|
|
28
|
-
const estimatedLines = estimateLineChanges(modification);
|
|
29
|
-
if (isRiskPath) {
|
|
30
|
-
if (estimatedLines > 100)
|
|
31
|
-
return 'CRITICAL';
|
|
32
|
-
return 'HIGH';
|
|
33
|
-
}
|
|
34
|
-
else {
|
|
35
|
-
if (estimatedLines > 100)
|
|
36
|
-
return 'HIGH';
|
|
37
|
-
if (estimatedLines > 10)
|
|
38
|
-
return 'MEDIUM';
|
|
39
|
-
return 'LOW';
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
/**
|
|
43
|
-
* Get the total line count of a target file.
|
|
44
|
-
* @param absoluteFilePath - Absolute path to the file
|
|
45
|
-
* @returns File line count, or null if file doesn't exist or can't be read
|
|
46
|
-
*/
|
|
47
|
-
export function getTargetFileLineCount(absoluteFilePath) {
|
|
48
|
-
try {
|
|
49
|
-
if (!fs.existsSync(absoluteFilePath)) {
|
|
50
|
-
return null; // File genuinely doesn't exist
|
|
51
|
-
}
|
|
52
|
-
const stats = fs.statSync(absoluteFilePath);
|
|
53
|
-
if (!stats.isFile()) {
|
|
54
|
-
return null; // Not a regular file (directory, device, etc.)
|
|
55
|
-
}
|
|
56
|
-
const content = fs.readFileSync(absoluteFilePath, 'utf-8');
|
|
57
|
-
return content.split('\n').length;
|
|
58
|
-
}
|
|
59
|
-
catch (e) {
|
|
60
|
-
// Log error before falling back to null - this is intentional for security gates
|
|
61
|
-
const error = e instanceof Error ? e : new Error(String(e));
|
|
62
|
-
const errorCode = e.code;
|
|
63
|
-
console.error(`[PD:RISK_CALC] Failed to read file for line count: ${absoluteFilePath}`, {
|
|
64
|
-
code: errorCode,
|
|
65
|
-
message: error.message,
|
|
66
|
-
});
|
|
67
|
-
return null;
|
|
68
|
-
}
|
|
69
|
-
}
|
|
70
|
-
/**
|
|
71
|
-
* Calculate the effective line limit based on percentage of target file.
|
|
72
|
-
* @param targetLineCount - Total lines in target file
|
|
73
|
-
* @param percentage - Allowed percentage (0-100)
|
|
74
|
-
* @param minLines - Absolute minimum threshold
|
|
75
|
-
* @param maxLines - Optional upper bound to prevent misconfiguration
|
|
76
|
-
* @returns Maximum allowed lines (at least minLines, at most maxLines if provided)
|
|
77
|
-
*/
|
|
78
|
-
export function calculatePercentageThreshold(targetLineCount, percentage, minLines, maxLines) {
|
|
79
|
-
// Clamp percentage to valid range [0, 100]
|
|
80
|
-
const clampedPercentage = Math.max(0, Math.min(100, percentage));
|
|
81
|
-
const calculated = Math.round(targetLineCount * (clampedPercentage / 100));
|
|
82
|
-
let effectiveLimit = Math.max(calculated, minLines);
|
|
83
|
-
// Apply optional upper bound
|
|
84
|
-
if (maxLines !== undefined && maxLines > 0) {
|
|
85
|
-
effectiveLimit = Math.min(effectiveLimit, maxLines);
|
|
86
|
-
}
|
|
87
|
-
return effectiveLimit;
|
|
88
|
-
}
|
|
@@ -1,31 +0,0 @@
|
|
|
1
|
-
import { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory, type PluginSurfaceEntry, type MvpCategory } from '@principles/core/runtime-v2';
|
|
2
|
-
import type { OpenClawPluginService } from '../openclaw-sdk.js';
|
|
3
|
-
export interface SurfaceGuardResult {
|
|
4
|
-
passed: boolean;
|
|
5
|
-
enabledCoreSurfaces: string[];
|
|
6
|
-
disabledNonCoreSurfaces: string[];
|
|
7
|
-
violations: string[];
|
|
8
|
-
warnings: string[];
|
|
9
|
-
}
|
|
10
|
-
type LoggerLike = {
|
|
11
|
-
info?: (msg: string) => void;
|
|
12
|
-
debug?: (msg: string) => void;
|
|
13
|
-
};
|
|
14
|
-
/**
|
|
15
|
-
* Reset the per-process surface-guard skip log bookkeeping. Intended for tests
|
|
16
|
-
* that need to assert on the first-fire log without cross-test pollution.
|
|
17
|
-
* Not part of the production API surface; do not call from runtime code.
|
|
18
|
-
*/
|
|
19
|
-
export declare function __resetSurfaceGuardSkipLogStateForTests(): void;
|
|
20
|
-
export declare function checkSurfaceGuard(): SurfaceGuardResult;
|
|
21
|
-
export declare function getSurfaceIdForHook(hookEvent: string, label?: string): string;
|
|
22
|
-
export declare function getSurfaceIdForService(serviceName: string): string;
|
|
23
|
-
export declare function isSurfaceEnabled(surfaceId: string, overrides?: Record<string, boolean>): {
|
|
24
|
-
enabled: boolean;
|
|
25
|
-
reason?: string;
|
|
26
|
-
};
|
|
27
|
-
export type HookHandler<E, C, R> = (event: E, ctx: C) => R | Promise<R>;
|
|
28
|
-
export declare function guardHook<E, C, R>(surfaceId: string, logger: LoggerLike | undefined, handler: HookHandler<E, C, R>): HookHandler<E, C, R>;
|
|
29
|
-
export declare function guardService<T extends OpenClawPluginService>(surfaceId: string, service: T, logger?: LoggerLike): T | null;
|
|
30
|
-
export { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory };
|
|
31
|
-
export type { PluginSurfaceEntry, MvpCategory };
|
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
import { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory, } from '@principles/core/runtime-v2';
|
|
2
|
-
// Surface-level once-only log state (PRI-298).
|
|
3
|
-
// The first time a quiet/non-core surface guard actually fires in this
|
|
4
|
-
// process, the disabled reason is emitted once. Subsequent fires for the
|
|
5
|
-
// same surfaceId are still observable (the no-op handler preserves
|
|
6
|
-
// behaviour) but no longer flood the log. Fresh processes start with an
|
|
7
|
-
// empty set, so each plugin load gets one observable skip per surface.
|
|
8
|
-
//
|
|
9
|
-
// The Set is only updated when the log was actually emitted, so passing
|
|
10
|
-
// `undefined` for the logger on a quiet first fire does NOT consume the
|
|
11
|
-
// one-shot slot — a later registration that supplies a logger still gets
|
|
12
|
-
// the first-fire reason (PRI-298 / ERR-002).
|
|
13
|
-
const loggedSkipSurfaces = new Set();
|
|
14
|
-
/**
|
|
15
|
-
* Emit the disabled-reason log line for `surfaceId` at most once per
|
|
16
|
-
* process. Returns true if the log was emitted, false if it was suppressed
|
|
17
|
-
* (already logged, or no logger available). Only marks the surface as
|
|
18
|
-
* logged when the log was actually written, so a missing logger on first
|
|
19
|
-
* call does not consume the one-shot slot.
|
|
20
|
-
*/
|
|
21
|
-
function logSkipOnce(surfaceId, logger, message) {
|
|
22
|
-
if (loggedSkipSurfaces.has(surfaceId)) {
|
|
23
|
-
return false;
|
|
24
|
-
}
|
|
25
|
-
if (!logger?.info) {
|
|
26
|
-
return false;
|
|
27
|
-
}
|
|
28
|
-
loggedSkipSurfaces.add(surfaceId);
|
|
29
|
-
logger.info(message);
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
/**
|
|
33
|
-
* Reset the per-process surface-guard skip log bookkeeping. Intended for tests
|
|
34
|
-
* that need to assert on the first-fire log without cross-test pollution.
|
|
35
|
-
* Not part of the production API surface; do not call from runtime code.
|
|
36
|
-
*/
|
|
37
|
-
export function __resetSurfaceGuardSkipLogStateForTests() {
|
|
38
|
-
loggedSkipSurfaces.clear();
|
|
39
|
-
}
|
|
40
|
-
export function checkSurfaceGuard() {
|
|
41
|
-
const validation = validateSurfaceRegistry(PLUGIN_SURFACE_REGISTRY);
|
|
42
|
-
const violations = [];
|
|
43
|
-
const warnings = [...validation.warnings];
|
|
44
|
-
const coreSurfaces = getSurfacesByCategory(PLUGIN_SURFACE_REGISTRY, 'core');
|
|
45
|
-
const enabledCore = coreSurfaces.filter(s => s.enabledByDefault);
|
|
46
|
-
const nonCoreEnabled = PLUGIN_SURFACE_REGISTRY.filter(s => s.category !== 'core' && s.enabledByDefault);
|
|
47
|
-
if (nonCoreEnabled.length > 0) {
|
|
48
|
-
for (const surface of nonCoreEnabled) {
|
|
49
|
-
violations.push(`non-core surface '${surface.id}' (${surface.category}) is enabledByDefault=true — must be false per ADR-0014`);
|
|
50
|
-
}
|
|
51
|
-
}
|
|
52
|
-
if (!validation.valid) {
|
|
53
|
-
violations.push(...validation.errors);
|
|
54
|
-
}
|
|
55
|
-
return {
|
|
56
|
-
passed: violations.length === 0,
|
|
57
|
-
enabledCoreSurfaces: enabledCore.map(s => s.id),
|
|
58
|
-
disabledNonCoreSurfaces: PLUGIN_SURFACE_REGISTRY
|
|
59
|
-
.filter(s => s.category !== 'core' && !s.enabledByDefault)
|
|
60
|
-
.map(s => s.id),
|
|
61
|
-
violations,
|
|
62
|
-
warnings,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
export function getSurfaceIdForHook(hookEvent, label) {
|
|
66
|
-
if (label) {
|
|
67
|
-
return `hook:${hookEvent}.${label}`;
|
|
68
|
-
}
|
|
69
|
-
return `hook:${hookEvent}`;
|
|
70
|
-
}
|
|
71
|
-
export function getSurfaceIdForService(serviceName) {
|
|
72
|
-
return `service:${serviceName}`;
|
|
73
|
-
}
|
|
74
|
-
export function isSurfaceEnabled(surfaceId, overrides = {}) {
|
|
75
|
-
const entry = PLUGIN_SURFACE_REGISTRY.find(s => s.id === surfaceId);
|
|
76
|
-
if (!entry) {
|
|
77
|
-
return {
|
|
78
|
-
enabled: false,
|
|
79
|
-
reason: `surface '${surfaceId}' not found in registry — classify before enabling (PRI-289)`,
|
|
80
|
-
};
|
|
81
|
-
}
|
|
82
|
-
if (Object.hasOwn(overrides, surfaceId)) {
|
|
83
|
-
const override = overrides[surfaceId];
|
|
84
|
-
if (typeof override !== 'boolean') {
|
|
85
|
-
return { enabled: entry.enabledByDefault, reason: `override for '${surfaceId}' is not boolean, using default` };
|
|
86
|
-
}
|
|
87
|
-
if (entry.category === 'gone') {
|
|
88
|
-
return { enabled: false, reason: `surface '${surfaceId}' is gone and cannot be re-enabled` };
|
|
89
|
-
}
|
|
90
|
-
if (entry.category === 'core' && !override) {
|
|
91
|
-
return { enabled: true, reason: `surface '${surfaceId}' is core and cannot be disabled` };
|
|
92
|
-
}
|
|
93
|
-
return { enabled: override };
|
|
94
|
-
}
|
|
95
|
-
if (!entry.enabledByDefault && entry.disabledReason) {
|
|
96
|
-
return { enabled: false, reason: entry.disabledReason };
|
|
97
|
-
}
|
|
98
|
-
return { enabled: entry.enabledByDefault };
|
|
99
|
-
}
|
|
100
|
-
export function guardHook(surfaceId, logger, handler) {
|
|
101
|
-
const check = isSurfaceEnabled(surfaceId);
|
|
102
|
-
if (check.enabled) {
|
|
103
|
-
return handler;
|
|
104
|
-
}
|
|
105
|
-
const reason = check.reason ?? 'surface not enabled';
|
|
106
|
-
// Log on the first ACTUAL no-op invocation, not at construction time
|
|
107
|
-
// (PRI-298). Construction-time logging would emit a `SKIP` line at
|
|
108
|
-
// plugin startup for every registered quiet hook, regardless of
|
|
109
|
-
// whether the hook ever fires — which is exactly the startup log
|
|
110
|
-
// noise this change is meant to prevent. The one-shot is consumed only
|
|
111
|
-
// when the log was actually written, so `undefined` logger on first
|
|
112
|
-
// call does not eat the slot.
|
|
113
|
-
return (_event, _ctx) => {
|
|
114
|
-
logSkipOnce(surfaceId, logger, `[PD:surface-guard] SKIP ${surfaceId}: ${reason}`);
|
|
115
|
-
return undefined;
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
export function guardService(surfaceId, service, logger) {
|
|
119
|
-
const check = isSurfaceEnabled(surfaceId);
|
|
120
|
-
if (check.enabled) {
|
|
121
|
-
return service;
|
|
122
|
-
}
|
|
123
|
-
const reason = check.reason ?? 'surface not enabled';
|
|
124
|
-
// guardService is called once per service during plugin registration,
|
|
125
|
-
// so the once-only check fires on the registration call itself. The
|
|
126
|
-
// shared helper makes the consumption rule identical to guardHook:
|
|
127
|
-
// a missing logger on first call does not consume the one-shot.
|
|
128
|
-
logSkipOnce(surfaceId, logger, `[PD:surface-guard] SKIP service ${surfaceId}: ${reason}`);
|
|
129
|
-
return null;
|
|
130
|
-
}
|
|
131
|
-
export { PLUGIN_SURFACE_REGISTRY, validateSurfaceRegistry, getSurfacesByCategory };
|