principles-disciple 1.117.0 → 1.118.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 +1 -1
- package/package.json +1 -1
- package/scripts/sync-plugin.mjs +43 -0
- package/src/core/rule-host.ts +192 -17
- package/src/hooks/gate.ts +1 -1
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.118.0",
|
|
6
6
|
"activation": {
|
|
7
7
|
"onCapabilities": [
|
|
8
8
|
"hook"
|
package/package.json
CHANGED
package/scripts/sync-plugin.mjs
CHANGED
|
@@ -923,6 +923,48 @@ function installTargetDependencies() {
|
|
|
923
923
|
}
|
|
924
924
|
}
|
|
925
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Verify injected workspace packages have their transitive dependencies installed.
|
|
928
|
+
* npm install --omit=dev in the target doesn't resolve deps of manually-copied packages,
|
|
929
|
+
* so transitive deps like @earendil-works/pi-agent-core may be missing after production install.
|
|
930
|
+
*/
|
|
931
|
+
function verifyInjectedWorkspaceDeps() {
|
|
932
|
+
const corePkgDir = join(INSTALL_DIR, 'node_modules', '@principles', 'core');
|
|
933
|
+
if (!existsSync(corePkgDir)) return;
|
|
934
|
+
|
|
935
|
+
const corePkgPath = join(corePkgDir, 'package.json');
|
|
936
|
+
if (!existsSync(corePkgPath)) return;
|
|
937
|
+
|
|
938
|
+
let corePkg;
|
|
939
|
+
try {
|
|
940
|
+
corePkg = JSON.parse(readFileSync(corePkgPath, 'utf-8'));
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.error(` ❌ Failed to parse @principles/core/package.json: ${error.message}`);
|
|
943
|
+
process.exit(1);
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
const deps = corePkg.dependencies || {};
|
|
947
|
+
const missing = [];
|
|
948
|
+
for (const [dep, version] of Object.entries(deps)) {
|
|
949
|
+
if (!existsSync(join(INSTALL_DIR, 'node_modules', dep))) {
|
|
950
|
+
missing.push(`${dep}@${version}`);
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
|
|
954
|
+
if (missing.length === 0) return;
|
|
955
|
+
|
|
956
|
+
console.log(` ⚠️ ${missing.length} transitive dependenc${missing.length === 1 ? 'y' : 'ies'} of @principles/core missing, installing...`);
|
|
957
|
+
try {
|
|
958
|
+
execSync(`npm install ${missing.join(' ')} --no-audit --no-fund --prefer-offline`, {
|
|
959
|
+
cwd: INSTALL_DIR,
|
|
960
|
+
stdio: 'pipe'
|
|
961
|
+
});
|
|
962
|
+
console.log(' ✅ Transitive dependencies installed');
|
|
963
|
+
} catch (error) {
|
|
964
|
+
console.warn(` ⚠️ Failed to install transitive dependencies: ${error.message}`);
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
|
|
926
968
|
/**
|
|
927
969
|
* Clean stale backups.
|
|
928
970
|
*/
|
|
@@ -1269,6 +1311,7 @@ function main() {
|
|
|
1269
1311
|
|
|
1270
1312
|
injectLocalWorkspacePackages();
|
|
1271
1313
|
installTargetDependencies();
|
|
1314
|
+
verifyInjectedWorkspaceDeps();
|
|
1272
1315
|
verifyPdCliShim();
|
|
1273
1316
|
|
|
1274
1317
|
console.log('\n🔍 Verifying installed plugin can load native dependencies...');
|
package/src/core/rule-host.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Rule Host — Constrained execution layer for active code implementations
|
|
3
3
|
*
|
|
4
|
-
* PURPOSE: Load active code implementations from the principle-tree ledger
|
|
5
|
-
*
|
|
4
|
+
* PURPOSE: Load active code implementations from the principle-tree ledger
|
|
5
|
+
* AND from the activations table (code_tool_hook channel), execute them in a
|
|
6
|
+
* constrained node:vm context, and merge their decisions.
|
|
6
7
|
*
|
|
7
8
|
* ARCHITECTURE:
|
|
8
9
|
* - Constructor takes stateDir to access the principle-tree ledger
|
|
10
|
+
* - Optional workspaceDir enables reading code_tool_hook activations from SQLite
|
|
9
11
|
* - evaluate(input) loads active code implementations and runs them
|
|
10
12
|
* - Each implementation executes in an isolated vm context with minimal helpers
|
|
11
13
|
* - Decision merge: block short-circuits, requireApproval collects, allow is implicit
|
|
@@ -27,6 +29,7 @@ import {
|
|
|
27
29
|
import { loadEntrySource } from './code-implementation-storage.js';
|
|
28
30
|
import { createRuleHostHelpers } from '@principles/core/runtime-v2';
|
|
29
31
|
import { mergeDecisions } from '@principles/core/runtime-v2';
|
|
32
|
+
import { SqliteConnection } from '@principles/core/runtime-v2';
|
|
30
33
|
import { loadRuleImplementationModule } from './rule-implementation-runtime.js';
|
|
31
34
|
import type {
|
|
32
35
|
RuleHostInput,
|
|
@@ -39,13 +42,37 @@ import type { Implementation } from '../types/principle-tree-schema.js';
|
|
|
39
42
|
import type { RuleHostLogger } from '@principles/core/runtime-v2';
|
|
40
43
|
export type { RuleHostLogger } from '@principles/core/runtime-v2';
|
|
41
44
|
|
|
45
|
+
export interface RuleHostOptions {
|
|
46
|
+
/** Workspace directory for SQLite access. When provided, RuleHost also loads code_tool_hook activations from the activations table. */
|
|
47
|
+
workspaceDir?: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Type guard for RuleHostMeta from untrusted module exports.
|
|
52
|
+
* Validates all four required string fields (EP-01: no `as` bypass at trust boundary).
|
|
53
|
+
*/
|
|
54
|
+
function isRuleHostMeta(value: unknown): value is RuleHostMeta {
|
|
55
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
const v = value as Record<string, unknown>;
|
|
59
|
+
return (
|
|
60
|
+
typeof v['name'] === 'string' &&
|
|
61
|
+
typeof v['version'] === 'string' &&
|
|
62
|
+
typeof v['ruleId'] === 'string' &&
|
|
63
|
+
typeof v['coversCondition'] === 'string'
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
42
67
|
export class RuleHost {
|
|
43
68
|
private readonly stateDir: string;
|
|
44
69
|
private readonly logger: RuleHostLogger;
|
|
70
|
+
private readonly workspaceDir: string | null;
|
|
45
71
|
|
|
46
|
-
constructor(stateDir: string, logger: RuleHostLogger = console) {
|
|
72
|
+
constructor(stateDir: string, logger: RuleHostLogger = console, options?: RuleHostOptions) {
|
|
47
73
|
this.stateDir = stateDir;
|
|
48
74
|
this.logger = logger;
|
|
75
|
+
this.workspaceDir = options?.workspaceDir ?? null;
|
|
49
76
|
}
|
|
50
77
|
|
|
51
78
|
/**
|
|
@@ -72,11 +99,21 @@ export class RuleHost {
|
|
|
72
99
|
}
|
|
73
100
|
|
|
74
101
|
/**
|
|
75
|
-
* Load active code implementations from the ledger.
|
|
76
|
-
*
|
|
77
|
-
*
|
|
102
|
+
* Load active code implementations from the ledger AND the activations table.
|
|
103
|
+
*
|
|
104
|
+
* Sources (merged, deduplicated by implId):
|
|
105
|
+
* 1. principle-tree-ledger.json: implementations with lifecycleState='active' and type='code'
|
|
106
|
+
* 2. activations table: code_tool_hook channel activations (when workspaceDir is provided)
|
|
107
|
+
*
|
|
108
|
+
* This bridges the gap between RuleHostWriter (which records activation metadata in SQLite)
|
|
109
|
+
* and RuleHost enforcement (which previously only read from the ledger JSON file).
|
|
110
|
+
* See BUG-001 / ERR-011 / ERR-035.
|
|
78
111
|
*/
|
|
79
112
|
private _loadActiveCodeImplementations(): LoadedImplementation[] {
|
|
113
|
+
const loaded: LoadedImplementation[] = [];
|
|
114
|
+
const seenImplIds = new Set<string>();
|
|
115
|
+
|
|
116
|
+
// Source 1: principle-tree-ledger.json (existing path)
|
|
80
117
|
try {
|
|
81
118
|
const activeAllTypes = listImplementationsByLifecycleState(
|
|
82
119
|
this.stateDir,
|
|
@@ -86,17 +123,12 @@ export class RuleHost {
|
|
|
86
123
|
// Filter to code-type implementations only
|
|
87
124
|
const codeImpls = activeAllTypes.filter((impl) => impl.type === 'code');
|
|
88
125
|
|
|
89
|
-
if (codeImpls.length === 0) {
|
|
90
|
-
return [];
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
const loaded: LoadedImplementation[] = [];
|
|
94
|
-
|
|
95
126
|
for (const impl of codeImpls) {
|
|
96
127
|
try {
|
|
97
128
|
const loadedImpl = this._loadSingleImplementation(impl);
|
|
98
|
-
if (loadedImpl) {
|
|
129
|
+
if (loadedImpl && !seenImplIds.has(loadedImpl.implId)) {
|
|
99
130
|
loaded.push(loadedImpl);
|
|
131
|
+
seenImplIds.add(loadedImpl.implId);
|
|
100
132
|
}
|
|
101
133
|
} catch (loadError: unknown) {
|
|
102
134
|
// Individual load failure: log and skip
|
|
@@ -105,14 +137,157 @@ export class RuleHost {
|
|
|
105
137
|
);
|
|
106
138
|
}
|
|
107
139
|
}
|
|
108
|
-
|
|
109
|
-
return loaded;
|
|
110
140
|
} catch (ledgerError: unknown) {
|
|
111
|
-
// Ledger access failure: log and
|
|
141
|
+
// Ledger access failure: log and continue to activations table
|
|
112
142
|
this.logger.warn?.(
|
|
113
143
|
`[RuleHost] Failed to access ledger: ${String(ledgerError)}`
|
|
114
144
|
);
|
|
115
|
-
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Source 2: activations table (code_tool_hook channel) — bridges BUG-001
|
|
148
|
+
if (this.workspaceDir) {
|
|
149
|
+
try {
|
|
150
|
+
const activationImpls = this._loadFromActivationsTable(this.workspaceDir);
|
|
151
|
+
for (const impl of activationImpls) {
|
|
152
|
+
if (!seenImplIds.has(impl.implId)) {
|
|
153
|
+
loaded.push(impl);
|
|
154
|
+
seenImplIds.add(impl.implId);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
} catch (activationError: unknown) {
|
|
158
|
+
// Activations table access failure: log and continue with ledger-only results
|
|
159
|
+
this.logger.warn?.(
|
|
160
|
+
`[RuleHost] Failed to load code_tool_hook activations: ${String(activationError)}`
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return loaded;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Load active code implementations from the activations table (code_tool_hook channel).
|
|
170
|
+
*
|
|
171
|
+
* For each activation record:
|
|
172
|
+
* 1. Query the pi_artifacts table for the artifact content
|
|
173
|
+
* 2. Parse content_json to extract implementationCode
|
|
174
|
+
* 3. Compile via loadRuleImplementationModule (same vm isolation as ledger path)
|
|
175
|
+
*
|
|
176
|
+
* This mirrors the PromptActivationReader pattern for SQLite access.
|
|
177
|
+
*/
|
|
178
|
+
private _loadFromActivationsTable(workspaceDir: string): LoadedImplementation[] {
|
|
179
|
+
const sqliteConn = new SqliteConnection(workspaceDir);
|
|
180
|
+
try {
|
|
181
|
+
const db = sqliteConn.getDb();
|
|
182
|
+
const rows = db.prepare(`
|
|
183
|
+
SELECT a.activation_id, a.artifact_id, a.target_ref,
|
|
184
|
+
p.content_json, p.source_rule_id
|
|
185
|
+
FROM activations a
|
|
186
|
+
JOIN pi_artifacts p ON a.artifact_id = p.artifact_id
|
|
187
|
+
WHERE a.channel = 'code_tool_hook' AND a.deactivated_at IS NULL
|
|
188
|
+
ORDER BY a.activated_at ASC
|
|
189
|
+
`).all() as unknown;
|
|
190
|
+
|
|
191
|
+
if (!Array.isArray(rows)) {
|
|
192
|
+
this.logger.warn?.(
|
|
193
|
+
'[RuleHost] Activations table query returned non-array, skipping'
|
|
194
|
+
);
|
|
195
|
+
return [];
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const loaded: LoadedImplementation[] = [];
|
|
199
|
+
|
|
200
|
+
for (const row of rows) {
|
|
201
|
+
if (!row || typeof row !== 'object') {
|
|
202
|
+
continue;
|
|
203
|
+
}
|
|
204
|
+
const r = row as Record<string, unknown>;
|
|
205
|
+
const activationId = typeof r['activation_id'] === 'string' ? r['activation_id'] : '';
|
|
206
|
+
const artifactId = typeof r['artifact_id'] === 'string' ? r['artifact_id'] : '';
|
|
207
|
+
const contentJson = typeof r['content_json'] === 'string' ? r['content_json'] : '';
|
|
208
|
+
const sourceRuleId = typeof r['source_rule_id'] === 'string' ? r['source_rule_id'] : null;
|
|
209
|
+
|
|
210
|
+
if (!activationId || !artifactId || !contentJson) {
|
|
211
|
+
this.logger.warn?.(
|
|
212
|
+
`[RuleHost] Activation row missing required fields, skipping`
|
|
213
|
+
);
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
try {
|
|
217
|
+
const content = JSON.parse(contentJson) as unknown;
|
|
218
|
+
if (!content || typeof content !== 'object' || Array.isArray(content)) {
|
|
219
|
+
this.logger.warn?.(
|
|
220
|
+
`[RuleHost] Activation ${activationId}: content_json is not an object, skipping`
|
|
221
|
+
);
|
|
222
|
+
continue;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const contentObj = content as Record<string, unknown>;
|
|
226
|
+
const implementationCode = contentObj['implementationCode'];
|
|
227
|
+
if (typeof implementationCode !== 'string' || implementationCode.length === 0) {
|
|
228
|
+
this.logger.warn?.(
|
|
229
|
+
`[RuleHost] Activation ${activationId}: no implementationCode in artifact, skipping`
|
|
230
|
+
);
|
|
231
|
+
continue;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
const ruleId = typeof contentObj['ruleId'] === 'string'
|
|
235
|
+
? contentObj['ruleId']
|
|
236
|
+
: (sourceRuleId ?? artifactId);
|
|
237
|
+
|
|
238
|
+
const implId = `act-impl-${activationId}`;
|
|
239
|
+
const moduleExports = loadRuleImplementationModule(implementationCode, implId);
|
|
240
|
+
|
|
241
|
+
if (!moduleExports || typeof moduleExports.evaluate !== 'function') {
|
|
242
|
+
this.logger.warn?.(
|
|
243
|
+
`[RuleHost] Activation ${activationId}: compiled module has no evaluate function, skipping`
|
|
244
|
+
);
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const fallbackMeta: RuleHostMeta = {
|
|
249
|
+
name: implId,
|
|
250
|
+
version: '1',
|
|
251
|
+
ruleId,
|
|
252
|
+
coversCondition: 'all',
|
|
253
|
+
};
|
|
254
|
+
const meta: RuleHostMeta = isRuleHostMeta(moduleExports.meta)
|
|
255
|
+
? moduleExports.meta
|
|
256
|
+
: fallbackMeta;
|
|
257
|
+
|
|
258
|
+
const rawEvaluate = moduleExports.evaluate as (
|
|
259
|
+
_input: RuleHostInput,
|
|
260
|
+
_helpers: ReturnType<typeof createRuleHostHelpers>
|
|
261
|
+
) => RuleHostResult;
|
|
262
|
+
|
|
263
|
+
loaded.push({
|
|
264
|
+
implId,
|
|
265
|
+
ruleId,
|
|
266
|
+
meta,
|
|
267
|
+
evaluate: (input: RuleHostInput): RuleHostResult => {
|
|
268
|
+
const frozenHelpers = createRuleHostHelpers(input);
|
|
269
|
+
const result = rawEvaluate(input, frozenHelpers);
|
|
270
|
+
if (result.matched && (result.decision === 'block' || result.decision === 'requireApproval')) {
|
|
271
|
+
result.ruleId = ruleId;
|
|
272
|
+
result.principleId = meta.ruleId ?? ruleId;
|
|
273
|
+
}
|
|
274
|
+
return result;
|
|
275
|
+
},
|
|
276
|
+
});
|
|
277
|
+
} catch (loadError: unknown) {
|
|
278
|
+
this.logger.warn?.(
|
|
279
|
+
`[RuleHost] Failed to load activation ${activationId}: ${String(loadError)}`
|
|
280
|
+
);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return loaded;
|
|
285
|
+
} finally {
|
|
286
|
+
try {
|
|
287
|
+
sqliteConn.close();
|
|
288
|
+
} catch {
|
|
289
|
+
// best-effort cleanup
|
|
290
|
+
}
|
|
116
291
|
}
|
|
117
292
|
}
|
|
118
293
|
|
package/src/hooks/gate.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function handleBeforeToolCall(
|
|
|
67
67
|
|
|
68
68
|
// 3. Rule Host Evaluation — sole gate
|
|
69
69
|
try {
|
|
70
|
-
const ruleHost = new RuleHost(wctx.stateDir, logger);
|
|
70
|
+
const ruleHost = new RuleHost(wctx.stateDir, logger, { workspaceDir: ctx.workspaceDir });
|
|
71
71
|
const hostInput: RuleHostInput = {
|
|
72
72
|
action: {
|
|
73
73
|
toolName: event.toolName,
|