principles-disciple 1.97.0 → 1.98.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.
@@ -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.97.0",
5
+ "version": "1.98.0",
6
6
  "activation": {
7
7
  "onCapabilities": [
8
8
  "hook"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "principles-disciple",
3
- "version": "1.97.0",
3
+ "version": "1.98.0",
4
4
  "description": "Native OpenClaw plugin for Principles Disciple",
5
5
  "type": "module",
6
6
  "main": "./dist/bundle.js",
@@ -57,6 +57,7 @@
57
57
  "eslint": "^10.4.1",
58
58
  "jsdom": "^29.1.1",
59
59
  "typescript": "^6.0.3",
60
+ "vite": "^8.0.16",
60
61
  "vitest": "^4.1.8",
61
62
  "ws": "^8.18.0"
62
63
  },
package/src/index.ts CHANGED
@@ -70,6 +70,59 @@ const startedWorkspaces = new Set<string>();
70
70
  // Used to complete shadow observations when subagent ends
71
71
  const pendingShadowObservations = new Map<string, string>();
72
72
 
73
+ // ── Conversation Access Health Check (PRI-343) ────────────────────────────
74
+ // Pure function for checking whether OpenClaw plugin config has
75
+ // allowConversationAccess set to true. When missing, llm_output and
76
+ // trajectory hooks are silently blocked by OpenClaw, causing evidence
77
+ // to always be empty (PRI-338 root cause).
78
+
79
+ /** Keep in sync with @principles/core CONVERSATION_ACCESS_CONFIG_KEY */
80
+ const CONVERSATION_ACCESS_CONFIG_KEY = 'allowConversationAccess' as const;
81
+
82
+ export interface ConversationAccessCheckResult {
83
+ authorized: boolean;
84
+ reason?: string;
85
+ nextAction?: string;
86
+ }
87
+
88
+ const CONVERSATION_ACCESS_FIX_COMMAND =
89
+ 'openclaw config set plugins.entries.principles-disciple.hooks.allowConversationAccess true --strict-json';
90
+
91
+ /**
92
+ * PRI-343: Pure function — checks if pluginConfig has hooks.allowConversationAccess === true.
93
+ * Returns a structured result with reason and nextAction when not authorized (ERR-002).
94
+ */
95
+ export function checkConversationAccessConfig(pluginConfig: unknown): ConversationAccessCheckResult {
96
+ if (pluginConfig === null || pluginConfig === undefined || typeof pluginConfig !== 'object' || Array.isArray(pluginConfig)) {
97
+ return {
98
+ authorized: false,
99
+ reason: 'pluginConfig is missing or invalid — conversation hooks cannot be registered',
100
+ nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
101
+ };
102
+ }
103
+
104
+ const config = pluginConfig as Record<string, unknown>;
105
+
106
+ if (typeof config.hooks !== 'object' || config.hooks === null || Array.isArray(config.hooks)) {
107
+ return {
108
+ authorized: false,
109
+ reason: 'allowConversationAccess is not set to true',
110
+ nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
111
+ };
112
+ }
113
+
114
+ const hooks = config.hooks as Record<string, unknown>;
115
+ if (hooks[CONVERSATION_ACCESS_CONFIG_KEY] !== true) {
116
+ return {
117
+ authorized: false,
118
+ reason: 'allowConversationAccess is not set to true',
119
+ nextAction: CONVERSATION_ACCESS_FIX_COMMAND,
120
+ };
121
+ }
122
+
123
+ return { authorized: true };
124
+ }
125
+
73
126
  // ── Feature Flag Loader (plugin I/O boundary) ─────────────────────────────
74
127
  // Reads workspace feature-flags.yaml and checks a specific flag.
75
128
  // Returns the flag definition with effective enabled state.
@@ -161,6 +214,18 @@ const plugin = {
161
214
  } else {
162
215
  api.logger.info(`[PD:health] Tool hook workspaceDir OK: "${toolWorkspaceDir}"`);
163
216
  }
217
+
218
+ // PRI-343: Check allowConversationAccess — warn if llm_output/trajectory hooks blocked
219
+ const accessCheck = checkConversationAccessConfig(api.pluginConfig);
220
+ if (!accessCheck.authorized) {
221
+ api.logger.error(
222
+ `[PD:health] conversation hooks (llm_output / trajectory) will be BLOCKED by OpenClaw.\n` +
223
+ ` reason: ${accessCheck.reason}\n` +
224
+ ` nextAction: ${accessCheck.nextAction}`,
225
+ );
226
+ } else {
227
+ api.logger.info(`[PD:health] conversation hooks (allowConversationAccess) OK`);
228
+ }
164
229
  }, 1000);
165
230
  healthCheckTimer.unref(); // Don't keep process alive for health check
166
231
 
@@ -1,5 +1,6 @@
1
1
  import { describe, it, expect } from 'vitest';
2
2
  import plugin from '../src/index';
3
+ import { checkConversationAccessConfig } from '../src/index';
3
4
  import type { PluginCommandDefinition, OpenClawPluginApi, PluginCommandContext } from '../src/openclaw-sdk.js';
4
5
 
5
6
  function createMockApi(): { registeredCommands: PluginCommandDefinition[]; api: OpenClawPluginApi } {
@@ -100,3 +101,42 @@ describe('Command Registration', () => {
100
101
  expect(ctx.workspaceDir).toBe('/mock/workspace');
101
102
  });
102
103
  });
104
+
105
+ describe('checkConversationAccessConfig — PRI-343', () => {
106
+ it('returns authorized:false with reason and nextAction when allowConversationAccess is not true', () => {
107
+ const result = checkConversationAccessConfig({ hooks: { allowConversationAccess: false } });
108
+ expect(result.authorized).toBe(false);
109
+ expect(result.reason).toBeDefined();
110
+ expect(typeof result.reason).toBe('string');
111
+ expect(result.reason!.length).toBeGreaterThan(0);
112
+ expect(result.nextAction).toBeDefined();
113
+ expect(typeof result.nextAction).toBe('string');
114
+ expect(result.nextAction!.length).toBeGreaterThan(0);
115
+ });
116
+
117
+ it('returns authorized:true when allowConversationAccess is true', () => {
118
+ const result = checkConversationAccessConfig({ hooks: { allowConversationAccess: true } });
119
+ expect(result.authorized).toBe(true);
120
+ expect(result.reason).toBeUndefined();
121
+ expect(result.nextAction).toBeUndefined();
122
+ });
123
+
124
+ it('returns authorized:false when hooks object is missing', () => {
125
+ const result = checkConversationAccessConfig({ enabled: true });
126
+ expect(result.authorized).toBe(false);
127
+ expect(result.reason).toBeDefined();
128
+ expect(result.nextAction).toBeDefined();
129
+ });
130
+
131
+ it('returns authorized:false when pluginConfig is null', () => {
132
+ const result = checkConversationAccessConfig(null);
133
+ expect(result.authorized).toBe(false);
134
+ expect(result.reason).toBeDefined();
135
+ });
136
+
137
+ it('returns authorized:false when pluginConfig is undefined', () => {
138
+ const result = checkConversationAccessConfig(undefined);
139
+ expect(result.authorized).toBe(false);
140
+ expect(result.reason).toBeDefined();
141
+ });
142
+ });