create-principles-disciple 1.104.2 → 1.105.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.
Files changed (71) hide show
  1. package/core/dist/host/__tests__/host-adapter.test.js +12 -0
  2. package/core/dist/host/__tests__/host-adapter.test.js.map +1 -1
  3. package/core/dist/host/host-adapter.d.ts +4 -0
  4. package/core/dist/host/host-adapter.d.ts.map +1 -1
  5. package/core/dist/host/host-adapter.js +7 -0
  6. package/core/dist/host/host-adapter.js.map +1 -1
  7. package/core/dist/runtime-v2/__tests__/evidence-sanitizer.test.js +28 -0
  8. package/core/dist/runtime-v2/__tests__/evidence-sanitizer.test.js.map +1 -1
  9. package/core/dist/runtime-v2/__tests__/sqlite-connection-readonly.test.js +18 -0
  10. package/core/dist/runtime-v2/__tests__/sqlite-connection-readonly.test.js.map +1 -1
  11. package/core/dist/runtime-v2/evidence-sanitizer.d.ts.map +1 -1
  12. package/core/dist/runtime-v2/evidence-sanitizer.js +17 -1
  13. package/core/dist/runtime-v2/evidence-sanitizer.js.map +1 -1
  14. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js +15 -0
  15. package/core/dist/runtime-v2/feature-flags/__tests__/feature-flag-contract.test.js.map +1 -1
  16. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js +1 -1
  17. package/core/dist/runtime-v2/feature-flags/feature-flag-contract.js.map +1 -1
  18. package/core/dist/runtime-v2/store/sqlite-connection.d.ts +3 -0
  19. package/core/dist/runtime-v2/store/sqlite-connection.d.ts.map +1 -1
  20. package/core/dist/runtime-v2/store/sqlite-connection.js +6 -3
  21. package/core/dist/runtime-v2/store/sqlite-connection.js.map +1 -1
  22. package/dist/installer.d.ts.map +1 -1
  23. package/dist/installer.js +52 -2
  24. package/dist/installer.js.map +1 -1
  25. package/dist/installers/codex-host-installer.d.ts +14 -0
  26. package/dist/installers/codex-host-installer.d.ts.map +1 -1
  27. package/dist/installers/codex-host-installer.js +53 -8
  28. package/dist/installers/codex-host-installer.js.map +1 -1
  29. package/dist/mvp-config.d.ts +20 -0
  30. package/dist/mvp-config.d.ts.map +1 -1
  31. package/dist/mvp-config.js +76 -0
  32. package/dist/mvp-config.js.map +1 -1
  33. package/dist/utils/config-file-io.d.ts +26 -0
  34. package/dist/utils/config-file-io.d.ts.map +1 -0
  35. package/dist/utils/config-file-io.js +128 -0
  36. package/dist/utils/config-file-io.js.map +1 -0
  37. package/host-runtime/dist/active-principle-prompt.d.ts +17 -0
  38. package/host-runtime/dist/active-principle-prompt.js +102 -0
  39. package/host-runtime/dist/index.d.ts +45 -0
  40. package/host-runtime/dist/index.js +148 -0
  41. package/host-runtime/dist/pd-config.d.ts +36 -0
  42. package/host-runtime/dist/pd-config.js +98 -0
  43. package/host-runtime/dist/production-pain-evidence.d.ts +24 -0
  44. package/host-runtime/dist/production-pain-evidence.js +293 -0
  45. package/host-runtime/dist/production-rulehost-gate.d.ts +23 -0
  46. package/host-runtime/dist/production-rulehost-gate.js +294 -0
  47. package/host-runtime/dist/rule-implementation-runtime.d.ts +25 -0
  48. package/host-runtime/dist/rule-implementation-runtime.js +114 -0
  49. package/host-runtime/package.json +37 -0
  50. package/package.json +2 -1
  51. package/pd-cli/dist/commands/health-codex.d.ts +7 -0
  52. package/pd-cli/dist/commands/health-codex.d.ts.map +1 -0
  53. package/pd-cli/dist/commands/health-codex.js +209 -0
  54. package/pd-cli/dist/commands/health-codex.js.map +1 -0
  55. package/pd-cli/dist/commands/health.d.ts +24 -0
  56. package/pd-cli/dist/commands/health.d.ts.map +1 -1
  57. package/pd-cli/dist/commands/health.js +48 -0
  58. package/pd-cli/dist/commands/health.js.map +1 -1
  59. package/pd-cli/dist/index.js +2 -9
  60. package/pd-cli/dist/index.js.map +1 -1
  61. package/pd-cli/package.json +2 -0
  62. package/plugin/dist/bundle.js +615 -533
  63. package/plugin/dist/core/pd-config-loader.d.ts +7 -78
  64. package/plugin/dist/hooks/after-tool-call-helpers.d.ts +6 -2
  65. package/plugin/dist/hooks/gate.d.ts +29 -0
  66. package/plugin/dist/hooks/pain.d.ts +22 -0
  67. package/plugin/dist/hooks/prompt.d.ts +12 -2
  68. package/plugin/dist/host-runtime/openclaw-host-runtime.d.ts +19 -0
  69. package/plugin/dist/index.d.ts +8 -0
  70. package/plugin/openclaw.plugin.json +1 -1
  71. package/plugin/package.json +2 -1
@@ -0,0 +1,294 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import { buildRuleHostAction, estimateLineChanges, mergeDecisions, SqliteConnection, UNAVAILABLE_RULE_CONTEXT, validateRuleContextV2, validateRuleHostResult, } from '@principles/core/runtime-v2';
4
+ import { createNodeRuleImplementationRuntime, MAX_ACTIVE_RULES, RULE_BATCH_SOURCE_BYTES, RULE_SOURCE_BYTES, } from './rule-implementation-runtime.js';
5
+ const WARNING_LIMIT = 500;
6
+ const MAX_WARNINGS = 16;
7
+ const GATE_DEADLINE_MS = 3_000;
8
+ const ARTIFACT_CONTENT_BYTES = 512 * 1024;
9
+ function boundedWarning(reason, nextAction) {
10
+ return `${reason}; nextAction=${nextAction}`.replace(/\s+/g, ' ').slice(0, WARNING_LIMIT);
11
+ }
12
+ function addWarning(warnings, reason, nextAction) {
13
+ if (warnings.length < MAX_WARNINGS)
14
+ warnings.push(boundedWarning(reason, nextAction));
15
+ }
16
+ function remainingGateMs(startedAt) {
17
+ return GATE_DEADLINE_MS - (Date.now() - startedAt);
18
+ }
19
+ function createDeadlinePromise(remaining) {
20
+ let timer;
21
+ const promise = new Promise((_resolve, reject) => {
22
+ timer = setTimeout(() => reject(new Error('gate_deadline_exceeded')), remaining);
23
+ timer.unref?.();
24
+ });
25
+ return { promise, clear: () => { if (timer !== undefined)
26
+ clearTimeout(timer); } };
27
+ }
28
+ async function withinGateDeadline(value, startedAt) {
29
+ const remaining = remainingGateMs(startedAt);
30
+ if (remaining <= 0)
31
+ throw new Error('gate_deadline_exceeded');
32
+ const deadline = createDeadlinePromise(remaining);
33
+ try {
34
+ return await Promise.race([Promise.resolve(value), deadline.promise]);
35
+ }
36
+ finally {
37
+ deadline.clear();
38
+ }
39
+ }
40
+ function isRecord(value) {
41
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
42
+ }
43
+ function readToolInput(rawPayload) {
44
+ if (!isRecord(rawPayload) || !Object.hasOwn(rawPayload, 'toolInput'))
45
+ return null;
46
+ const input = rawPayload.toolInput;
47
+ if (!isRecord(input) || typeof input.toolName !== 'string' || input.toolName.trim().length === 0)
48
+ return null;
49
+ const { params } = input;
50
+ if (!isRecord(params))
51
+ return null;
52
+ return { toolName: input.toolName, params };
53
+ }
54
+ function isRuleMeta(value) {
55
+ return isRecord(value) && typeof value.name === 'string' && typeof value.version === 'string'
56
+ && typeof value.ruleId === 'string' && typeof value.coversCondition === 'string';
57
+ }
58
+ function isRuleContext(value) {
59
+ return validateRuleContextV2(value).valid;
60
+ }
61
+ function isRuleResult(value) {
62
+ return validateRuleHostResult(value).valid;
63
+ }
64
+ export function createProductionRuleHostGate(options = {}) {
65
+ const implementationRuntime = options.implementationRuntime ?? createNodeRuleImplementationRuntime();
66
+ return async (event) => {
67
+ const startedAt = Date.now();
68
+ const warnings = [];
69
+ const input = readToolInput(event.rawPayload);
70
+ if (!input || input.toolName !== event.context.toolName) {
71
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning('tool_input_invalid', 'decode and validate toolName and params before dispatch')] };
72
+ }
73
+ const isBash = new Set(['bash', 'exec', 'execute', 'run_shell_command']).has(input.toolName);
74
+ const isWrite = new Set(['write', 'write_file', 'edit', 'edit_file', 'replace', 'apply_patch']).has(input.toolName);
75
+ const action = buildRuleHostAction(input.toolName, input.params, event.context.workspaceDir, { isBashTool: isBash, isWriteTool: isWrite });
76
+ if (action.normalizedPath === null)
77
+ return { decision: 'allow', source: event.source, metadata: { evaluatedLiveRules: 0 } };
78
+ let context;
79
+ let enrichment = { currentGfi: 0, recentThinking: false, epTier: 0, bashRisk: 'unknown' };
80
+ const request = {
81
+ workspaceDir: event.context.workspaceDir, sessionId: event.context.sessionId,
82
+ targetPath: action.normalizedPath, toolName: input.toolName, rawPayload: event.rawPayload,
83
+ };
84
+ if (options.ruleInputEnrichmentProvider) {
85
+ try {
86
+ const candidate = await withinGateDeadline(options.ruleInputEnrichmentProvider(request), startedAt);
87
+ if (isRecord(candidate) && typeof candidate.currentGfi === 'number' && Number.isFinite(candidate.currentGfi)
88
+ && typeof candidate.recentThinking === 'boolean' && typeof candidate.epTier === 'number' && Number.isFinite(candidate.epTier)
89
+ && (candidate.bashRisk === 'safe' || candidate.bashRisk === 'normal' || candidate.bashRisk === 'dangerous' || candidate.bashRisk === 'unknown')) {
90
+ enrichment = { currentGfi: candidate.currentGfi, recentThinking: candidate.recentThinking, epTier: candidate.epTier, bashRisk: candidate.bashRisk };
91
+ }
92
+ else
93
+ addWarning(warnings, 'rule_input_enrichment_invalid', 'repair the host enrichment provider');
94
+ }
95
+ catch (error) {
96
+ addWarning(warnings, `rule_input_enrichment_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect the host enrichment provider');
97
+ }
98
+ }
99
+ if (options.ruleContextProvider) {
100
+ try {
101
+ const candidate = await withinGateDeadline(options.ruleContextProvider(request), startedAt);
102
+ if (candidate === undefined) {
103
+ context = undefined;
104
+ }
105
+ else {
106
+ const validation = validateRuleContextV2(candidate);
107
+ if (isRuleContext(candidate))
108
+ context = candidate;
109
+ else {
110
+ context = UNAVAILABLE_RULE_CONTEXT;
111
+ addWarning(warnings, `rule_context_invalid: ${validation.errors.join('; ')}`, 'repair the host context provider');
112
+ }
113
+ }
114
+ }
115
+ catch (error) {
116
+ context = UNAVAILABLE_RULE_CONTEXT;
117
+ addWarning(warnings, `rule_context_provider_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect the host context provider and retry');
118
+ }
119
+ }
120
+ const dbPath = path.join(event.context.workspaceDir, '.pd', 'state.db');
121
+ if (remainingGateMs(startedAt) <= 0) {
122
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'inspect host context providers and active RuleCode resource use')] };
123
+ }
124
+ if (!fs.existsSync(dbPath)) {
125
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning('activation_db_not_found', 'initialize_workspace_runtime_state')] };
126
+ }
127
+ const connection = new SqliteConnection({ workspaceDir: event.context.workspaceDir, readonly: true, bootstrapIfMissing: false });
128
+ const candidates = [];
129
+ try {
130
+ const rows = connection.getDb().prepare(`
131
+ SELECT a.activation_id, a.artifact_id, a.target_ref, a.action,
132
+ p.source_rule_id, p.source_principle_id,
133
+ length(CAST(p.content_json AS BLOB)) AS content_bytes
134
+ FROM activations a JOIN pi_artifacts p ON a.artifact_id = p.artifact_id
135
+ WHERE a.channel = 'code_tool_hook' AND a.deactivated_at IS NULL
136
+ ORDER BY a.activated_at ASC
137
+ LIMIT ?
138
+ `).all(MAX_ACTIVE_RULES + 1);
139
+ if (!Array.isArray(rows)) {
140
+ addWarning(warnings, 'activation_query_invalid', 'inspect state.db schema and integrity');
141
+ }
142
+ else if (rows.length > MAX_ACTIVE_RULES) {
143
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`active_rule_limit_exceeded: maximum ${MAX_ACTIVE_RULES}`, 'deactivate excess active RuleHost rules and retry')], metadata: { evaluatedLiveRules: 0 } };
144
+ }
145
+ else {
146
+ for (const row of rows) {
147
+ if (!isRecord(row) || typeof row.content_bytes !== 'number' || !Number.isSafeInteger(row.content_bytes) || row.content_bytes < 0) {
148
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning('artifact_content_size_invalid', 'inspect state.db artifact content integrity')], metadata: { evaluatedLiveRules: 0 } };
149
+ }
150
+ if (row.content_bytes > ARTIFACT_CONTENT_BYTES) {
151
+ const activation = typeof row.activation_id === 'string' ? row.activation_id : 'unknown';
152
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_budget_exceeded: activation=${activation} bytes=${row.content_bytes} maximum=${ARTIFACT_CONTENT_BYTES}`, 'reduce the active artifact envelope and reactivate the rule')], metadata: { evaluatedLiveRules: 0 } };
153
+ }
154
+ }
155
+ const groups = new Map();
156
+ for (const row of rows) {
157
+ if (!isRecord(row) || typeof row.target_ref !== 'string' || row.target_ref.length === 0) {
158
+ addWarning(warnings, 'activation_row_invalid', 'deactivate and recreate the malformed activation');
159
+ continue;
160
+ }
161
+ const group = groups.get(row.target_ref) ?? [];
162
+ group.push(row);
163
+ groups.set(row.target_ref, group);
164
+ }
165
+ for (const [targetRef, group] of groups) {
166
+ if (group.length !== 1) {
167
+ addWarning(warnings, `duplicate_active_activation: ${targetRef}`, 'deactivate all but one activation for this target_ref');
168
+ continue;
169
+ }
170
+ const [row] = group;
171
+ if (!row || row.action !== 'code_tool_hook_live_activate')
172
+ continue;
173
+ const activationId = row.activation_id;
174
+ const artifactId = row.artifact_id;
175
+ const expectedContentBytes = row.content_bytes;
176
+ if (typeof activationId !== 'string' || typeof artifactId !== 'string' || typeof expectedContentBytes !== 'number') {
177
+ addWarning(warnings, 'activation_required_fields_invalid', 'deactivate and recreate the activation');
178
+ continue;
179
+ }
180
+ const contentRow = connection.getDb().prepare(`
181
+ SELECT content_json, length(CAST(content_json AS BLOB)) AS content_bytes
182
+ FROM pi_artifacts WHERE artifact_id = ?
183
+ `).get(artifactId);
184
+ if (!isRecord(contentRow) || typeof contentRow.content_json !== 'string'
185
+ || typeof contentRow.content_bytes !== 'number' || !Number.isSafeInteger(contentRow.content_bytes)) {
186
+ addWarning(warnings, `activation_artifact_invalid: ${activationId}`, 'inspect state.db artifact content integrity');
187
+ continue;
188
+ }
189
+ const contentJson = contentRow.content_json;
190
+ const returnedContentBytes = Buffer.byteLength(contentJson, 'utf8');
191
+ if (contentRow.content_bytes > ARTIFACT_CONTENT_BYTES || returnedContentBytes > ARTIFACT_CONTENT_BYTES) {
192
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_budget_exceeded: activation=${activationId} bytes=${Math.max(contentRow.content_bytes, returnedContentBytes)} maximum=${ARTIFACT_CONTENT_BYTES}`, 'reduce the active artifact envelope and reactivate the rule')], metadata: { evaluatedLiveRules: 0 } };
193
+ }
194
+ if (contentRow.content_bytes !== expectedContentBytes || returnedContentBytes !== contentRow.content_bytes) {
195
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`artifact_content_size_changed: activation=${activationId}`, 'retry after the active artifact update completes')], metadata: { evaluatedLiveRules: 0 } };
196
+ }
197
+ try {
198
+ const content = JSON.parse(contentJson);
199
+ if (!isRecord(content) || typeof content.implementationCode !== 'string' || content.implementationCode.length === 0) {
200
+ addWarning(warnings, `activation_artifact_invalid: ${activationId}`, 'regenerate the rule artifact with implementationCode');
201
+ continue;
202
+ }
203
+ const sourceBytes = Buffer.byteLength(content.implementationCode, 'utf8');
204
+ if (sourceBytes > RULE_SOURCE_BYTES) {
205
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_source_budget_exceeded: activation=${activationId} bytes=${sourceBytes}`, `reduce each RuleCode source below ${RULE_SOURCE_BYTES} bytes`)], metadata: { evaluatedLiveRules: 0 } };
206
+ }
207
+ if (Object.hasOwn(content, 'requiresContextVersion')) {
208
+ if (content.requiresContextVersion !== 2) {
209
+ addWarning(warnings, `unsupported_context_version: ${String(content.requiresContextVersion)}`, 'regenerate the rule artifact for context version 2');
210
+ continue;
211
+ }
212
+ if (!context) {
213
+ addWarning(warnings, 'rule_context_v2_unavailable', 'enable and wire the host rule context provider');
214
+ continue;
215
+ }
216
+ }
217
+ const ruleId = typeof content.ruleId === 'string' ? content.ruleId : typeof row.source_rule_id === 'string' ? row.source_rule_id : artifactId;
218
+ const principleId = typeof content.principleId === 'string' ? content.principleId : typeof row.source_principle_id === 'string' ? row.source_principle_id : ruleId;
219
+ const fallbackMeta = { name: activationId, version: '1', ruleId, coversCondition: 'all' };
220
+ candidates.push({ implId: activationId, ruleId, principleId, meta: isRuleMeta(content.meta) ? content.meta : fallbackMeta, source: content.implementationCode });
221
+ }
222
+ catch (error) {
223
+ addWarning(warnings, `implementation_unhealthy: ${error instanceof Error ? error.message : String(error)}`, 'fix the RuleCode and reactivate the rule');
224
+ }
225
+ }
226
+ }
227
+ const hostInput = {
228
+ action,
229
+ workspace: { isRiskPath: false, planStatus: 'NONE', hasPlanFile: false },
230
+ session: { sessionId: event.context.sessionId, currentGfi: enrichment.currentGfi, recentThinking: enrichment.recentThinking },
231
+ evolution: { epTier: enrichment.epTier },
232
+ derived: { estimatedLineChanges: estimateLineChanges({ toolName: input.toolName, params: input.params }), bashRisk: enrichment.bashRisk },
233
+ ...(context ? { context } : {}),
234
+ };
235
+ const batchSourceBytes = candidates.reduce((sum, candidate) => sum + Buffer.byteLength(candidate.source, 'utf8'), 0);
236
+ if (batchSourceBytes > RULE_BATCH_SOURCE_BYTES) {
237
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_source_budget_exceeded: batchBytes=${batchSourceBytes}`, `reduce total active RuleCode below ${RULE_BATCH_SOURCE_BYTES} bytes`)], metadata: { evaluatedLiveRules: 0 } };
238
+ }
239
+ const batchSources = candidates.map((candidate) => ({ source: candidate.source, filename: `activation-${candidate.implId}` }));
240
+ const remaining = remainingGateMs(startedAt);
241
+ if (remaining <= 0) {
242
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning('gate_deadline_exceeded', 'reduce active RuleCode count or source size and retry')], metadata: { evaluatedLiveRules: 0 } };
243
+ }
244
+ const batch = implementationRuntime.evaluateBatch(batchSources, hostInput, remaining);
245
+ if (!batch.ok || !batch.results) {
246
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`${batch.reason ?? 'rule_batch_failed'}: ${batch.detail ?? 'unknown failure'}`, 'inspect active RuleCode resource use and repair or deactivate the unhealthy rule')], metadata: { evaluatedLiveRules: 0 } };
247
+ }
248
+ const timedOutChild = batch.results.find((candidate) => !candidate.ok && candidate.error?.includes('timed out'));
249
+ if (timedOutChild) {
250
+ return { decision: 'allow', source: event.source, warnings: [boundedWarning(`rule_batch_timeout: ${timedOutChild.error ?? 'unknown child timeout'}`, 'fix or deactivate the unhealthy RuleCode and retry')], metadata: { evaluatedLiveRules: 0 } };
251
+ }
252
+ const implementations = [];
253
+ for (let index = 0; index < candidates.length; index += 1) {
254
+ const candidate = candidates[index];
255
+ const batchResult = batch.results[index];
256
+ if (!candidate || !batchResult) {
257
+ addWarning(warnings, 'rule_batch_result_missing', 'inspect the RuleCode runtime result contract');
258
+ continue;
259
+ }
260
+ if (!batchResult.ok) {
261
+ addWarning(warnings, `implementation_unhealthy: ${batchResult.error ?? 'unknown child error'}`, 'fix the RuleCode and reactivate the rule');
262
+ continue;
263
+ }
264
+ const validation = validateRuleHostResult(batchResult.result);
265
+ if (!isRuleResult(batchResult.result)) {
266
+ addWarning(warnings, `invalid RuleHostResult: ${validation.errors.join('; ')}`, 'fix the RuleCode result and reactivate the rule');
267
+ continue;
268
+ }
269
+ const validatedResult = batchResult.result.matched
270
+ ? { ...batchResult.result, ruleId: candidate.ruleId, principleId: candidate.principleId }
271
+ : batchResult.result;
272
+ implementations.push({ ...candidate, evaluate: () => validatedResult });
273
+ }
274
+ const result = mergeDecisions(implementations, hostInput, {
275
+ warn(message) { addWarning(warnings, message, 'inspect the unhealthy activation and RuleCode output'); },
276
+ });
277
+ if (result?.decision === 'block') {
278
+ if (result.reason.trim().length === 0) {
279
+ addWarning(warnings, 'deny_reason_missing', 'fix the RuleCode to return a non-empty block reason');
280
+ return { decision: 'allow', source: event.source, warnings, metadata: { evaluatedLiveRules: implementations.length } };
281
+ }
282
+ return { decision: 'deny', reason: result.reason, source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleId: result.ruleId, principleId: result.principleId } };
283
+ }
284
+ return { decision: 'allow', source: event.source, ...(warnings.length ? { warnings } : {}), metadata: { evaluatedLiveRules: implementations.length, ruleDecision: result?.decision ?? 'allow' } };
285
+ }
286
+ catch (error) {
287
+ addWarning(warnings, `activation_read_failed: ${error instanceof Error ? error.message : String(error)}`, 'inspect state.db schema and integrity');
288
+ return { decision: 'allow', source: event.source, warnings, metadata: { evaluatedLiveRules: 0 } };
289
+ }
290
+ finally {
291
+ connection.close();
292
+ }
293
+ };
294
+ }
@@ -0,0 +1,25 @@
1
+ export declare const RULE_BATCH_TIMEOUT_MS = 2500;
2
+ export declare const RULE_BATCH_OUTPUT_BYTES: number;
3
+ export declare const RULE_SOURCE_BYTES: number;
4
+ export declare const RULE_BATCH_SOURCE_BYTES: number;
5
+ export declare const MAX_ACTIVE_RULES = 32;
6
+ export interface RuleBatchSource {
7
+ source: string;
8
+ filename: string;
9
+ }
10
+ export interface RuleBatchResult {
11
+ ok: boolean;
12
+ result?: unknown;
13
+ error?: string;
14
+ }
15
+ export type RuleBatchFailureReason = 'rule_batch_timeout' | 'rule_batch_output_exceeded' | 'rule_batch_failed';
16
+ export interface RuleBatchEvaluation {
17
+ ok: boolean;
18
+ results?: readonly RuleBatchResult[];
19
+ reason?: RuleBatchFailureReason;
20
+ detail?: string;
21
+ }
22
+ export interface RuleImplementationRuntime {
23
+ evaluateBatch(rules: readonly RuleBatchSource[], input: unknown, timeoutMs: number): RuleBatchEvaluation;
24
+ }
25
+ export declare function createNodeRuleImplementationRuntime(): RuleImplementationRuntime;
@@ -0,0 +1,114 @@
1
+ import { spawnSync } from 'node:child_process';
2
+ export const RULE_BATCH_TIMEOUT_MS = 2_500;
3
+ export const RULE_BATCH_OUTPUT_BYTES = 64 * 1024;
4
+ export const RULE_SOURCE_BYTES = 64 * 1024;
5
+ export const RULE_BATCH_SOURCE_BYTES = 256 * 1024;
6
+ export const MAX_ACTIVE_RULES = 32;
7
+ const EVALUATION_PROCESS_SOURCE = String.raw `
8
+ const vm = require('node:vm');
9
+ const fs = require('node:fs');
10
+ function normalize(sourceCode) {
11
+ const withoutExports = sourceCode
12
+ .replace(/export\s+const\s+meta\s*=/, 'const meta =')
13
+ .replace(/export\s+function\s+evaluate\s*\(/, 'function evaluate(');
14
+ return withoutExports + '\nglobalThis.__pdRuleModule = { meta: typeof meta === "undefined" ? undefined : meta, evaluate: typeof evaluate === "undefined" ? undefined : evaluate };';
15
+ }
16
+ function preview(error) { return String(error).slice(0, 500); }
17
+ try {
18
+ const workerData = JSON.parse(fs.readFileSync(0, 'utf8'));
19
+ const results = [];
20
+ for (const rule of workerData.rules) {
21
+ try {
22
+ const context = vm.createContext(Object.create(null));
23
+ new vm.Script(normalize(rule.source), { filename: rule.filename }).runInContext(context, { timeout: 1000 });
24
+ if (!context.__pdRuleModule || typeof context.__pdRuleModule.evaluate !== 'function') throw new Error('compiled module has no evaluate function');
25
+ // Sandbox boundary: hand the context ONLY a primitive JSON string and
26
+ // build the call input + helpers with the context's own intrinsics.
27
+ // Passing host-realm objects (the parsed input, a host-built helpers
28
+ // object) would let rule source walk .constructor.constructor back to
29
+ // the host realm's Function constructor and reach the child process
30
+ // and require — a full sandbox escape with user privileges.
31
+ context.__pdInputJson = JSON.stringify(workerData.input);
32
+ new vm.Script(
33
+ 'const __pdCallInput = JSON.parse(globalThis.__pdInputJson);' +
34
+ 'delete globalThis.__pdInputJson;' +
35
+ 'const __pdCallHelpers = Object.freeze({' +
36
+ ' isRiskPath: () => __pdCallInput.workspace.isRiskPath,' +
37
+ ' getToolName: () => __pdCallInput.action.toolName,' +
38
+ ' getEstimatedLineChanges: () => __pdCallInput.derived.estimatedLineChanges,' +
39
+ ' getBashRisk: () => __pdCallInput.derived.bashRisk,' +
40
+ ' hasPlanFile: () => __pdCallInput.workspace.hasPlanFile,' +
41
+ ' getPlanStatus: () => __pdCallInput.workspace.planStatus,' +
42
+ ' getEpTier: () => __pdCallInput.evolution.epTier' +
43
+ '});', { filename: rule.filename + '.bootstrap' }
44
+ ).runInContext(context, { timeout: 1000 });
45
+ const result = new vm.Script('__pdRuleModule.evaluate(__pdCallInput, __pdCallHelpers)', { filename: rule.filename + '.call' })
46
+ .runInContext(context, { timeout: 1000 });
47
+ results.push({ ok: true, result });
48
+ } catch (error) {
49
+ results.push({ ok: false, error: preview(error) });
50
+ }
51
+ }
52
+ process.stdout.write(JSON.stringify({ ok: true, results }));
53
+ } catch (error) {
54
+ process.stdout.write(JSON.stringify({ ok: false, error: preview(error) }));
55
+ process.exitCode = 1;
56
+ }
57
+ `;
58
+ function isRecord(value) {
59
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
60
+ }
61
+ function parseBatchResults(value, expectedLength) {
62
+ if (!isRecord(value) || value.ok !== true || !Array.isArray(value.results) || value.results.length !== expectedLength)
63
+ return null;
64
+ const results = [];
65
+ for (const candidate of value.results) {
66
+ if (!isRecord(candidate) || typeof candidate.ok !== 'boolean')
67
+ return null;
68
+ if (candidate.ok) {
69
+ if (!Object.hasOwn(candidate, 'result'))
70
+ return null;
71
+ results.push({ ok: true, result: candidate.result });
72
+ }
73
+ else {
74
+ if (typeof candidate.error !== 'string')
75
+ return null;
76
+ results.push({ ok: false, error: candidate.error.slice(0, 500) });
77
+ }
78
+ }
79
+ return results;
80
+ }
81
+ export function createNodeRuleImplementationRuntime() {
82
+ return {
83
+ evaluateBatch(rules, input, timeoutMs) {
84
+ const child = spawnSync(process.execPath, ['--max-old-space-size=32', '-e', EVALUATION_PROCESS_SOURCE], {
85
+ input: JSON.stringify({ rules, input }), encoding: 'utf8', timeout: Math.max(1, Math.min(timeoutMs, RULE_BATCH_TIMEOUT_MS)),
86
+ maxBuffer: RULE_BATCH_OUTPUT_BYTES, windowsHide: true,
87
+ });
88
+ if (child.error) {
89
+ const code = isRecord(child.error) && typeof child.error.code === 'string' ? child.error.code : '';
90
+ if (code === 'ETIMEDOUT')
91
+ return { ok: false, reason: 'rule_batch_timeout', detail: child.error.message.slice(0, 500) };
92
+ if (code === 'ENOBUFS')
93
+ return { ok: false, reason: 'rule_batch_output_exceeded', detail: child.error.message.slice(0, 500) };
94
+ return { ok: false, reason: 'rule_batch_failed', detail: child.error.message.slice(0, 500) };
95
+ }
96
+ if (Buffer.byteLength(child.stdout, 'utf8') >= RULE_BATCH_OUTPUT_BYTES) {
97
+ return { ok: false, reason: 'rule_batch_output_exceeded', detail: 'child output reached the configured byte bound' };
98
+ }
99
+ let parsed;
100
+ try {
101
+ parsed = JSON.parse(child.stdout);
102
+ }
103
+ catch {
104
+ return { ok: false, reason: 'rule_batch_failed', detail: `child returned invalid JSON: ${child.stderr.trim().slice(0, 300)}` };
105
+ }
106
+ const results = parseBatchResults(parsed, rules.length);
107
+ if (!results) {
108
+ const detail = isRecord(parsed) && typeof parsed.error === 'string' ? parsed.error.slice(0, 500) : 'child result shape invalid';
109
+ return { ok: false, reason: 'rule_batch_failed', detail };
110
+ }
111
+ return { ok: true, results };
112
+ },
113
+ };
114
+ }
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@principles/host-runtime",
3
+ "version": "0.1.0",
4
+ "description": "Shared host-neutral orchestration for Principles Disciple MVP-Core hook paths.",
5
+ "type": "module",
6
+ "main": "./dist/index.js",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "types": "./dist/index.d.ts",
11
+ "default": "./dist/index.js"
12
+ },
13
+ "./package.json": "./package.json"
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "scripts": {
19
+ "build": "tsc",
20
+ "test": "vitest run",
21
+ "lint": "eslint \"src/**/*.ts\""
22
+ },
23
+ "dependencies": {
24
+ "@principles/core": "file:../core",
25
+ "better-sqlite3": "^13.0.3",
26
+ "js-yaml": "^5.2.0"
27
+ },
28
+ "devDependencies": {
29
+ "@types/better-sqlite3": "^7.6.13",
30
+ "@types/node": "^26.1.2",
31
+ "@types/js-yaml": "^4.0.9",
32
+ "typescript": "^7.0.2",
33
+ "vite": "^8.0.16",
34
+ "vitest": "^4.1.10"
35
+ },
36
+ "license": "MIT"
37
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-principles-disciple",
3
- "version": "1.104.2",
3
+ "version": "1.105.0",
4
4
  "description": "Interactive CLI installer for Principles Disciple OpenClaw plugin",
5
5
  "type": "module",
6
6
  "bin": {
@@ -12,6 +12,7 @@
12
12
  "pd-cli",
13
13
  "console",
14
14
  "core",
15
+ "host-runtime",
15
16
  "templates",
16
17
  "README.md",
17
18
  "LICENSE"
@@ -0,0 +1,7 @@
1
+ interface CodexHealthOptions {
2
+ workspace?: string;
3
+ json?: boolean;
4
+ }
5
+ export declare function handleHealthCodex(opts?: CodexHealthOptions): Promise<void>;
6
+ export {};
7
+ //# sourceMappingURL=health-codex.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"health-codex.d.ts","sourceRoot":"","sources":["../../src/commands/health-codex.ts"],"names":[],"mappings":"AAsBA,UAAU,kBAAkB;IAC1B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB;AA2GD,wBAAsB,iBAAiB,CAAC,IAAI,GAAE,kBAAuB,GAAG,OAAO,CAAC,IAAI,CAAC,CAoHpF"}