principles-disciple 1.7.4 → 1.7.5

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 (40) hide show
  1. package/dist/commands/focus.js +30 -155
  2. package/dist/constants/diagnostician.d.ts +16 -0
  3. package/dist/constants/diagnostician.js +60 -0
  4. package/dist/constants/tools.d.ts +2 -2
  5. package/dist/constants/tools.js +1 -1
  6. package/dist/core/config.d.ts +12 -0
  7. package/dist/core/config.js +7 -0
  8. package/dist/core/evolution-engine.js +1 -1
  9. package/dist/core/focus-history.d.ts +92 -0
  10. package/dist/core/focus-history.js +490 -0
  11. package/dist/core/init.js +2 -2
  12. package/dist/core/profile.js +1 -1
  13. package/dist/hooks/gate.js +3 -3
  14. package/dist/hooks/prompt.js +73 -22
  15. package/dist/hooks/subagent.js +1 -2
  16. package/dist/http/principles-console-route.d.ts +7 -0
  17. package/dist/http/principles-console-route.js +243 -1
  18. package/dist/index.js +0 -2
  19. package/dist/service/central-database.d.ts +104 -0
  20. package/dist/service/central-database.js +648 -0
  21. package/dist/service/evolution-worker.js +3 -3
  22. package/dist/tools/deep-reflect.js +1 -2
  23. package/dist/utils/subagent-probe.d.ts +11 -0
  24. package/dist/utils/subagent-probe.js +46 -1
  25. package/package.json +2 -1
  26. package/templates/langs/en/core/AGENTS.md +1 -1
  27. package/templates/langs/en/core/TOOLS.md +1 -1
  28. package/templates/langs/zh/core/AGENTS.md +1 -1
  29. package/templates/langs/zh/core/TOOLS.md +1 -1
  30. package/{agents/auditor.md → templates/langs/zh/skills/pd-auditor/SKILL.md} +3 -3
  31. package/{agents/diagnostician.md → templates/langs/zh/skills/pd-diagnostician/SKILL.md} +39 -29
  32. package/{agents/explorer.md → templates/langs/zh/skills/pd-explorer/SKILL.md} +3 -3
  33. package/{agents/implementer.md → templates/langs/zh/skills/pd-implementer/SKILL.md} +3 -3
  34. package/{agents/planner.md → templates/langs/zh/skills/pd-planner/SKILL.md} +3 -3
  35. package/{agents/reporter.md → templates/langs/zh/skills/pd-reporter/SKILL.md} +3 -3
  36. package/{agents/reviewer.md → templates/langs/zh/skills/pd-reviewer/SKILL.md} +3 -3
  37. package/dist/core/agent-loader.d.ts +0 -44
  38. package/dist/core/agent-loader.js +0 -147
  39. package/dist/tools/agent-spawn.d.ts +0 -54
  40. package/dist/tools/agent-spawn.js +0 -456
@@ -3,7 +3,7 @@ import * as path from 'path';
3
3
  import { clearInjectedProbationIds, getSession, resetFriction, setInjectedProbationIds } from '../core/session-tracker.js';
4
4
  import { WorkspaceContext } from '../core/workspace-context.js';
5
5
  import { defaultContextConfig } from '../types.js';
6
- import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection } from '../core/focus-history.js';
6
+ import { extractSummary, getHistoryVersions, parseWorkingMemorySection, workingMemoryToInjection, autoCompressFocus, safeReadCurrentFocus } from '../core/focus-history.js';
7
7
  import { empathyObserverManager } from '../service/empathy-observer-manager.js';
8
8
  import { PathResolver } from '../core/path-resolver.js';
9
9
  /**
@@ -130,6 +130,8 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
130
130
  const preview = typeof inProgressTask.trigger_text_preview === 'string' && inProgressTask.trigger_text_preview.trim()
131
131
  ? inProgressTask.trigger_text_preview.trim()
132
132
  : 'N/A';
133
+ const sessionId = typeof inProgressTask.session_id === 'string' ? inProgressTask.session_id.trim() : '';
134
+ const agentId = typeof inProgressTask.agent_id === 'string' ? inProgressTask.agent_id.trim() : '';
133
135
  const conversationContext = includeConversationContext
134
136
  ? extractRecentConversationContext(messages, maxContextMessages, maxCharsPerMsg)
135
137
  : '';
@@ -142,11 +144,27 @@ function resolveEvolutionTask(inProgressTask, messages, maxContextMessages = 4,
142
144
  `;
143
145
  taskDescription += `**Trigger Text**: "${preview}"
144
146
  `;
147
+ if (sessionId) {
148
+ taskDescription += `**Session ID**: ${sessionId}
149
+ `;
150
+ }
151
+ if (agentId) {
152
+ taskDescription += `**Agent ID**: ${agentId}
153
+ `;
154
+ }
145
155
  if (conversationContext) {
146
156
  taskDescription += `
147
157
  ---
148
158
  **Recent Conversation Context**:
149
159
  ${conversationContext}`;
160
+ }
161
+ else if (!sessionId) {
162
+ taskDescription += `
163
+ ---
164
+ **Note**: 对话上下文不可用。请主动收集证据:
165
+ 1. 从 Reason 字段提取关键词,搜索相关代码
166
+ 2. 读取 .state/logs/events.jsonl 最近日志
167
+ 3. 基于 Reason 中的文件路径定位问题`;
150
168
  }
151
169
  taskDescription += `
152
170
 
@@ -227,7 +245,6 @@ export function resolveModelFromConfig(modelConfig, logger) {
227
245
  }
228
246
  // 闁哄秶鍘х槐?3: 闁轰焦澹嗙划宥夊冀閻撳海纭€闁挎稑鐗呯粭澶愬绩椤栨稑鐦柨娑樿嫰瑜板倿宕欐ウ娆惧妳闁告稑顭槐?
229
247
  if (Array.isArray(modelConfig)) {
230
- console.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
231
248
  logger?.warn(`[PD:Prompt] Array model config not supported. Expected "provider/model" string or { primary: "..." } object.`);
232
249
  return null;
233
250
  }
@@ -388,8 +405,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
388
405
  - Use the current session for the normal user reply.
389
406
  - Use sessions_send for cross-session messaging.
390
407
  - Use agents_list / sessions_list / sessions_spawn for peer-agent or peer-session orchestration.
391
- - Use subagents to inspect already-dispatched internal workers such as diagnostician/explorer.
392
- - Use pd_run_worker only for Principles Disciple internal workers such as diagnostician/explorer.
408
+ - Use sessions_spawn with pd-diagnostician/pd-explorer/etc skills for internal worker tasks.
393
409
 
394
410
  ## 妫e啯鎯?INTERNAL SYSTEM LAYOUT
395
411
  - Your core plugin logic is rooted at: ${PathResolver.getExtensionRoot() || 'EXTENSION_ROOT (unresolved)'}
@@ -407,7 +423,7 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
407
423
  trustContext += `Hygiene: ${hygiene.persistenceCount} persists today\n`;
408
424
  // Stage-based restrictions
409
425
  if (safeStage === 1) {
410
- trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use the internal pd_run_worker diagnostician worker to recover trust before writing files. Do not use it for peer-session messaging.\n`;
426
+ trustContext += `ACTION CONSTRAINT: You are in READ-ONLY MODE. You MUST use sessions_spawn with the pd-diagnostician skill to recover trust before writing files.\n`;
411
427
  }
412
428
  else if (safeStage === 2) {
413
429
  trustContext += `ACTION CONSTRAINT: LIMITED MODE. You are restricted to a maximum of 50 lines per edit.\n`;
@@ -444,13 +460,30 @@ You are a **self-evolving AI agent** powered by Principles Disciple.
444
460
  evolutionDirective = `<evolution_task priority="high">
445
461
  TASK: ${escapedTask}
446
462
 
447
- REQUIRED ACTION:
448
- - First respond to the user's current request normally.
449
- - After finishing the user-facing response, call:
450
- pd_run_worker agentType="diagnostician" task=${escapedTask} runInBackground=true
451
- - Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
463
+ REQUIRED ACTION (两阶段回复):
464
+
465
+ **阶段 1 - 立即回复用户:**
466
+ 简要告知用户你检测到的问题,例如:
467
+ "我注意到刚才的操作遇到了一些问题(简述问题),我已安排诊断代理在后台分析根因。分析完成后会继续告诉你结果。"
468
+
469
+ 然后调用:
470
+ sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
471
+
472
+ **阶段 2 - 收到子代理结果后:**
473
+ 当收到诊断代理的完成消息后,用以下格式回复用户:
474
+
475
+ "**诊断完成** 🔍
476
+
477
+ 基于后台分析结果:
478
+ - **根因**: [一句话总结]
479
+ - **分类**: [People/Design/Assumption/Tooling]
480
+ - **建议**: [可执行的改进建议]
481
+
482
+ 相关原则已记录到 PRINCIPLES.md。"
483
+
484
+ IMPORTANT:
452
485
  - Do NOT reply with "[EVOLUTION_ACK]".
453
- - Do NOT let this task interrupt the current user interaction.
486
+ - 子代理结果会作为新消息到达,届时再进行阶段2的回复。
454
487
  </evolution_task>\n`;
455
488
  }
456
489
  else {
@@ -459,8 +492,7 @@ TASK: ${escapedTask}
459
492
 
460
493
  REQUIRED ACTION:
461
494
  - Start diagnostics immediately by calling:
462
- pd_run_worker agentType="diagnostician" task=${escapedTask} runInBackground=true
463
- - Treat pd_run_worker as an internal Principles Disciple worker, not a peer-session messaging tool.
495
+ sessions_spawn(task="使用 pd-diagnostician skill 执行诊断任务。\n\n任务: ${escapedTask}")
464
496
  - Do NOT reply with "[EVOLUTION_ACK]".
465
497
  </evolution_task>\n`;
466
498
  }
@@ -581,34 +613,53 @@ ACTION: Run self-audit. If stable, reply ONLY with "HEARTBEAT_OK".
581
613
  let workingMemoryContent = '';
582
614
  if (!isMinimalMode && contextConfig.projectFocus !== 'off') {
583
615
  const focusPath = wctx.resolve('CURRENT_FOCUS');
584
- if (fs.existsSync(focusPath)) {
616
+ const extensionRoot = PathResolver.getExtensionRoot();
617
+ // 🔒 安全读取:自动验证格式,损坏时从模板恢复
618
+ const { content: currentFocus, recovered, validationErrors } = safeReadCurrentFocus(focusPath, extensionRoot || '', logger);
619
+ if (recovered) {
620
+ logger?.info?.(`[PD:Prompt] CURRENT_FOCUS.md was recovered from template`);
621
+ }
622
+ if (validationErrors.length > 0) {
623
+ logger?.warn?.(`[PD:Prompt] CURRENT_FOCUS validation errors: ${validationErrors.join(', ')}`);
624
+ }
625
+ if (currentFocus.trim()) {
585
626
  try {
586
- const currentFocus = fs.readFileSync(focusPath, 'utf8').trim();
587
- if (currentFocus) {
627
+ // 🚀 自动压缩门禁:检查文件大小,超过阈值自动压缩
628
+ const stateDir = wctx.stateDir;
629
+ const compressResult = autoCompressFocus(focusPath, workspaceDir, stateDir);
630
+ if (compressResult.compressed) {
631
+ logger?.info?.(`[PD:Prompt] Auto-compressed CURRENT_FOCUS: ${compressResult.oldLines} → ${compressResult.newLines} lines. Milestones archived: ${compressResult.milestonesArchived}`);
632
+ }
633
+ else if (compressResult.reason === 'Rate limited (24h interval)') {
634
+ logger?.debug?.(`[PD:Prompt] Auto-compress skipped: ${compressResult.reason}`);
635
+ }
636
+ // 重新读取(可能被压缩更新了)
637
+ const finalContent = fs.readFileSync(focusPath, 'utf8').trim();
638
+ if (finalContent) {
588
639
  // 解析工作记忆部分(用于独立注入)
589
- const workingMemorySnapshot = parseWorkingMemorySection(currentFocus);
640
+ const workingMemorySnapshot = parseWorkingMemorySection(finalContent);
590
641
  if (workingMemorySnapshot) {
591
642
  workingMemoryContent = workingMemoryToInjection(workingMemorySnapshot);
592
643
  }
593
644
  if (contextConfig.projectFocus === 'summary') {
594
645
  // Summary mode: intelligent extraction prioritizing key sections
595
- projectContextContent = extractSummary(currentFocus, 30);
646
+ projectContextContent = extractSummary(finalContent, 30);
596
647
  }
597
648
  else {
598
649
  // Full mode: current version + recent history (3 versions)
599
650
  const historyVersions = getHistoryVersions(focusPath, 3);
600
651
  if (historyVersions.length > 0) {
601
- const historySections = historyVersions.map((v, i) => `\n---\n\n**闁告ê妫楄ぐ鍫曟偋閸喐鎷?v${historyVersions.length - i}**\n\n${v}`).join('');
602
- projectContextContent = `${currentFocus}${historySections}`;
652
+ const historySections = historyVersions.map((v, i) => `\n---\n\n**历史版本 v${historyVersions.length - i}**\n\n${v}`).join('');
653
+ projectContextContent = `${finalContent}${historySections}`;
603
654
  }
604
655
  else {
605
- projectContextContent = currentFocus;
656
+ projectContextContent = finalContent;
606
657
  }
607
658
  }
608
659
  }
609
660
  }
610
661
  catch (e) {
611
- logger?.error(`[PD:Prompt] Failed to read CURRENT_FOCUS: ${String(e)}`);
662
+ logger?.error(`[PD:Prompt] Failed to process CURRENT_FOCUS: ${String(e)}`);
612
663
  }
613
664
  }
614
665
  }
@@ -3,7 +3,6 @@ import { writePainFlag } from '../core/pain.js';
3
3
  import { WorkspaceContext } from '../core/workspace-context.js';
4
4
  import { empathyObserverManager } from '../service/empathy-observer-manager.js';
5
5
  import { acquireQueueLock } from '../service/evolution-worker.js';
6
- import { isSubagentRuntimeAvailable } from '../utils/subagent-probe.js';
7
6
  const COMPLETION_RETRY_DELAY_MS = 250;
8
7
  const COMPLETION_MAX_RETRIES = 3;
9
8
  const COMPLETION_RETRY_TTL_MS = 60 * 60 * 1000; // 1 hour TTL for retry entries
@@ -252,7 +251,7 @@ export async function handleSubagentEnded(event, ctx) {
252
251
  }
253
252
  }
254
253
  // Read diagnostician output and create principle with generalized pattern
255
- if (completedTaskId && isSubagentRuntimeAvailable(ctx.api?.runtime?.subagent)) {
254
+ if (completedTaskId && ctx.api?.runtime?.subagent) {
256
255
  try {
257
256
  const messages = await ctx.api?.runtime?.subagent?.getSessionMessages?.({
258
257
  sessionKey: targetSessionKey,
@@ -1,2 +1,9 @@
1
1
  import type { OpenClawPluginApi, OpenClawPluginHttpRouteParams } from '../openclaw-sdk.js';
2
+ /**
3
+ * Create routes for Principles Console.
4
+ * Returns an array of routes:
5
+ * 1. Static files route (no auth required for HTML/CSS/JS)
6
+ * 2. API route (gateway auth required)
7
+ */
8
+ export declare function createPrinciplesConsoleRoutes(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams[];
2
9
  export declare function createPrinciplesConsoleRoute(api: OpenClawPluginApi): OpenClawPluginHttpRouteParams;
@@ -3,6 +3,7 @@ import path from 'path';
3
3
  import { ControlUiQueryService } from '../service/control-ui-query-service.js';
4
4
  import { getEvolutionQueryService } from '../service/evolution-query-service.js';
5
5
  import { TrajectoryRegistry } from '../core/trajectory.js';
6
+ import { getCentralDatabase } from '../service/central-database.js';
6
7
  const ROUTE_PREFIX = '/plugins/principles';
7
8
  const API_PREFIX = `${ROUTE_PREFIX}/api`;
8
9
  const ASSETS_PREFIX = `${ROUTE_PREFIX}/assets`;
@@ -82,6 +83,11 @@ function createService(api) {
82
83
  return new ControlUiQueryService(workspaceDir);
83
84
  }
84
85
  function handleApiRoute(api, pathname, req, res) {
86
+ // Check authentication for API routes
87
+ if (!validateGatewayAuth(req)) {
88
+ json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
89
+ return true;
90
+ }
85
91
  const service = createService(api);
86
92
  const url = new URL(req.url || pathname, 'http://127.0.0.1');
87
93
  const method = (req.method || 'GET').toUpperCase();
@@ -103,6 +109,153 @@ function handleApiRoute(api, pathname, req, res) {
103
109
  if (pathname === `${API_PREFIX}/overview` && method === 'GET') {
104
110
  return done(() => service.getOverview());
105
111
  }
112
+ if (pathname === `${API_PREFIX}/central/overview` && method === 'GET') {
113
+ return done(() => {
114
+ const centralDb = getCentralDatabase();
115
+ const stats = centralDb.getOverviewStats();
116
+ const trend = centralDb.getDailyTrend(14);
117
+ const regressions = centralDb.getTopRegressions(5);
118
+ const thinkingStats = centralDb.getThinkingModelStats();
119
+ const workspaces = centralDb.getWorkspaces();
120
+ return {
121
+ workspaceDir: 'central',
122
+ generatedAt: new Date().toISOString(),
123
+ dataFreshness: workspaces.length > 0 ? (workspaces[0].lastSync ?? null) : null,
124
+ dataSource: 'central_aggregated_db',
125
+ runtimeControlPlaneSource: 'all_workspaces',
126
+ summary: {
127
+ repeatErrorRate: stats.totalToolCalls > 0
128
+ ? stats.totalFailures / stats.totalToolCalls
129
+ : 0,
130
+ userCorrectionRate: stats.totalToolCalls > 0
131
+ ? stats.totalCorrections / stats.totalToolCalls
132
+ : 0,
133
+ pendingSamples: stats.pendingSamples,
134
+ approvedSamples: stats.approvedSamples,
135
+ thinkingCoverageRate: stats.totalToolCalls > 0
136
+ ? stats.totalThinkingEvents / stats.totalToolCalls
137
+ : 0,
138
+ painEvents: stats.totalPainEvents,
139
+ principleEventCount: 0,
140
+ gateBlocks: 0,
141
+ taskOutcomes: 0,
142
+ },
143
+ dailyTrend: trend,
144
+ topRegressions: regressions,
145
+ sampleQueue: {
146
+ counters: {
147
+ pending: stats.pendingSamples,
148
+ approved: stats.approvedSamples,
149
+ rejected: stats.rejectedSamples,
150
+ },
151
+ preview: [],
152
+ },
153
+ thinkingSummary: {
154
+ activeModels: thinkingStats.activeModels,
155
+ dormantModels: thinkingStats.totalModels - thinkingStats.activeModels,
156
+ effectiveModels: thinkingStats.models.filter(m => m.coverageRate > 0.1).length,
157
+ coverageRate: stats.totalToolCalls > 0
158
+ ? stats.totalThinkingEvents / stats.totalToolCalls
159
+ : 0,
160
+ },
161
+ centralInfo: {
162
+ workspaceCount: stats.workspaceCount,
163
+ enabledWorkspaceCount: stats.enabledWorkspaceCount,
164
+ workspaces: stats.workspaceNames,
165
+ enabledWorkspaces: stats.enabledWorkspaceNames,
166
+ },
167
+ };
168
+ });
169
+ }
170
+ if (pathname === `${API_PREFIX}/central/sync` && method === 'POST') {
171
+ return done(() => {
172
+ const centralDb = getCentralDatabase();
173
+ const results = centralDb.syncEnabled();
174
+ const summary = {};
175
+ results.forEach((count, name) => {
176
+ summary[name] = count;
177
+ });
178
+ return { synced: summary, timestamp: new Date().toISOString() };
179
+ });
180
+ }
181
+ if (pathname === `${API_PREFIX}/central/workspaces` && method === 'GET') {
182
+ return done(() => {
183
+ const centralDb = getCentralDatabase();
184
+ const configs = centralDb.getWorkspaceConfigs();
185
+ const workspaces = centralDb.getWorkspaces();
186
+ return {
187
+ configs,
188
+ workspaces: workspaces.map(ws => ({
189
+ name: ws.name,
190
+ path: ws.path,
191
+ lastSync: ws.lastSync,
192
+ config: configs.find(c => c.workspaceName === ws.name) ?? null,
193
+ })),
194
+ };
195
+ });
196
+ }
197
+ const workspaceConfigMatch = pathname.match(/^\/plugins\/principles\/api\/central\/workspaces\/([^/]+)$/);
198
+ if (workspaceConfigMatch && method === 'GET') {
199
+ return done(() => {
200
+ const centralDb = getCentralDatabase();
201
+ const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
202
+ const configs = centralDb.getWorkspaceConfigs();
203
+ const config = configs.find(c => c.workspaceName === workspaceName);
204
+ return config ?? { workspaceName, enabled: true, displayName: workspaceName, syncEnabled: true };
205
+ });
206
+ }
207
+ if (workspaceConfigMatch && method === 'PATCH') {
208
+ return (async () => {
209
+ try {
210
+ const body = await readJsonBody(req);
211
+ const centralDb = getCentralDatabase();
212
+ const workspaceName = decodeURIComponent(workspaceConfigMatch[1]);
213
+ centralDb.updateWorkspaceConfig(workspaceName, {
214
+ enabled: body.enabled,
215
+ displayName: body.displayName,
216
+ syncEnabled: body.syncEnabled,
217
+ });
218
+ const configs = centralDb.getWorkspaceConfigs();
219
+ json(res, 200, configs.find(c => c.workspaceName === workspaceName));
220
+ return true;
221
+ }
222
+ catch (error) {
223
+ if (error instanceof Error && error.message === 'invalid_json') {
224
+ json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
225
+ return true;
226
+ }
227
+ api.logger.warn(`[PD:ControlUI] Workspace config update failed: ${String(error)}`);
228
+ json(res, 500, { error: 'internal_error', message: String(error) });
229
+ return true;
230
+ }
231
+ })();
232
+ }
233
+ if (pathname === `${API_PREFIX}/central/workspaces` && method === 'POST') {
234
+ return (async () => {
235
+ try {
236
+ const body = await readJsonBody(req);
237
+ const name = typeof body.name === 'string' ? body.name : '';
238
+ const workspacePath = typeof body.path === 'string' ? body.path : '';
239
+ if (!name || !workspacePath) {
240
+ json(res, 400, { error: 'bad_request', message: 'name and path are required.' });
241
+ return true;
242
+ }
243
+ const centralDb = getCentralDatabase();
244
+ centralDb.addCustomWorkspace(name, workspacePath);
245
+ json(res, 201, { success: true, workspace: name });
246
+ return true;
247
+ }
248
+ catch (error) {
249
+ if (error instanceof Error && error.message === 'invalid_json') {
250
+ json(res, 400, { error: 'bad_request', message: 'Request body must be valid JSON.' });
251
+ return true;
252
+ }
253
+ api.logger.warn(`[PD:ControlUI] Add workspace failed: ${String(error)}`);
254
+ json(res, 500, { error: 'internal_error', message: String(error) });
255
+ return true;
256
+ }
257
+ })();
258
+ }
106
259
  if (pathname === `${API_PREFIX}/samples` && method === 'GET') {
107
260
  return done(() => service.listSamples({
108
261
  status: url.searchParams.get('status') ?? undefined,
@@ -275,10 +428,93 @@ function handleApiRoute(api, pathname, req, res) {
275
428
  json(res, 404, { error: 'not_found', message: 'Unknown Principles Console API route.' });
276
429
  return true;
277
430
  }
431
+ function getGatewayToken() {
432
+ try {
433
+ const configPath = path.join(process.env.HOME || '', '.openclaw', 'openclaw.json');
434
+ if (!fs.existsSync(configPath))
435
+ return null;
436
+ const config = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
437
+ return config?.gateway?.auth?.token || null;
438
+ }
439
+ catch {
440
+ return null;
441
+ }
442
+ }
443
+ function validateGatewayAuth(req) {
444
+ const gatewayToken = getGatewayToken();
445
+ if (!gatewayToken) {
446
+ // No token configured, allow all requests
447
+ return true;
448
+ }
449
+ const authHeader = req.headers?.['authorization'] || '';
450
+ const tokenMatch = authHeader.match(/^Bearer\s+(.+)$/i);
451
+ const providedToken = tokenMatch?.[1];
452
+ return providedToken === gatewayToken;
453
+ }
454
+ /**
455
+ * Create routes for Principles Console.
456
+ * Returns an array of routes:
457
+ * 1. Static files route (no auth required for HTML/CSS/JS)
458
+ * 2. API route (gateway auth required)
459
+ */
460
+ export function createPrinciplesConsoleRoutes(api) {
461
+ // Route 1: Static files (HTML, CSS, JS) - no auth check
462
+ const staticRoute = {
463
+ path: ROUTE_PREFIX,
464
+ auth: 'plugin',
465
+ match: 'prefix',
466
+ async handler(req, res) {
467
+ const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
468
+ const pathname = url.pathname;
469
+ const method = (req.method || 'GET').toUpperCase();
470
+ // Skip API routes - they'll be handled by the API route
471
+ if (pathname.startsWith(API_PREFIX)) {
472
+ return false; // Let the API route handle this
473
+ }
474
+ // Serve assets
475
+ if (pathname.startsWith(ASSETS_PREFIX)) {
476
+ if (method !== 'GET' && method !== 'HEAD') {
477
+ text(res, 405, 'Method Not Allowed');
478
+ return true;
479
+ }
480
+ const assetPath = safeStaticPath(api.rootDir, pathname);
481
+ if (!assetPath || !serveFile(res, assetPath)) {
482
+ text(res, 404, 'Asset Not Found');
483
+ }
484
+ return true;
485
+ }
486
+ // Serve index.html for the main route
487
+ if (method !== 'GET' && method !== 'HEAD') {
488
+ text(res, 405, 'Method Not Allowed');
489
+ return true;
490
+ }
491
+ const indexPath = path.join(api.rootDir, 'dist', 'web', 'index.html');
492
+ if (!serveFile(res, indexPath)) {
493
+ text(res, 503, 'Principles Console UI is not built yet.');
494
+ }
495
+ return true;
496
+ },
497
+ };
498
+ // Route 2: API endpoints - gateway auth required
499
+ const apiRoute = {
500
+ path: API_PREFIX,
501
+ auth: 'gateway',
502
+ match: 'prefix',
503
+ async handler(req, res) {
504
+ const url = new URL(req.url || API_PREFIX, 'http://127.0.0.1');
505
+ const pathname = url.pathname;
506
+ return handleApiRoute(api, pathname, req, res);
507
+ },
508
+ };
509
+ return [staticRoute, apiRoute];
510
+ }
511
+ // Legacy export for backwards compatibility
278
512
  export function createPrinciplesConsoleRoute(api) {
513
+ const routes = createPrinciplesConsoleRoutes(api);
514
+ // Return the combined behavior - this will be called from index.ts
279
515
  return {
280
516
  path: ROUTE_PREFIX,
281
- auth: 'gateway',
517
+ auth: 'plugin',
282
518
  match: 'prefix',
283
519
  async handler(req, res) {
284
520
  const url = new URL(req.url || ROUTE_PREFIX, 'http://127.0.0.1');
@@ -287,9 +523,15 @@ export function createPrinciplesConsoleRoute(api) {
287
523
  if (!pathname.startsWith(ROUTE_PREFIX)) {
288
524
  return false;
289
525
  }
526
+ // For API routes, check auth manually
290
527
  if (pathname.startsWith(API_PREFIX)) {
528
+ if (!validateGatewayAuth(req)) {
529
+ json(res, 401, { error: 'unauthorized', message: 'Valid Gateway token required.' });
530
+ return true;
531
+ }
291
532
  return handleApiRoute(api, pathname, req, res);
292
533
  }
534
+ // Static files - no auth required
293
535
  if (pathname.startsWith(ASSETS_PREFIX)) {
294
536
  if (method !== 'GET' && method !== 'HEAD') {
295
537
  text(res, 405, 'Method Not Allowed');
package/dist/index.js CHANGED
@@ -26,7 +26,6 @@ import { ensureWorkspaceTemplates } from './core/init.js';
26
26
  import { migrateDirectoryStructure } from './core/migration.js';
27
27
  import { SystemLogger } from './core/system-logger.js';
28
28
  import { createDeepReflectTool } from './tools/deep-reflect.js';
29
- import { createAgentSpawnTool } from './tools/agent-spawn.js';
30
29
  import { PathResolver } from './core/path-resolver.js';
31
30
  import { createPrinciplesConsoleRoute } from './http/principles-console-route.js';
32
31
  // Track initialization to avoid repeated calls
@@ -476,7 +475,6 @@ const plugin = {
476
475
  }
477
476
  });
478
477
  api.registerTool(createDeepReflectTool(api));
479
- api.registerTool(createAgentSpawnTool(api));
480
478
  }
481
479
  };
482
480
  export default plugin;
@@ -0,0 +1,104 @@
1
+ export interface WorkspaceInfo {
2
+ name: string;
3
+ path: string;
4
+ lastSync: string | null;
5
+ }
6
+ /**
7
+ * Central database that aggregates data from all agent workspaces.
8
+ * Stored in ~/.openclaw/.central/ (NOT in memory/ which is for embeddings)
9
+ */
10
+ export declare class CentralDatabase {
11
+ private readonly dbPath;
12
+ private readonly db;
13
+ private readonly workspaces;
14
+ constructor();
15
+ dispose(): void;
16
+ private tableExists;
17
+ private initSchema;
18
+ private discoverWorkspaces;
19
+ /**
20
+ * Sync data from a single workspace into the central database
21
+ */
22
+ syncWorkspace(workspaceName: string): number;
23
+ syncEnabled(): Map<string, number>;
24
+ /**
25
+ * Sync all workspaces (legacy method - syncs all regardless of config)
26
+ */
27
+ syncAll(): Map<string, number>;
28
+ private getEnabledWorkspaceFilter;
29
+ /**
30
+ * Get aggregated overview stats (only from enabled workspaces)
31
+ */
32
+ getOverviewStats(): {
33
+ totalSessions: number;
34
+ totalToolCalls: number;
35
+ totalFailures: number;
36
+ totalPainEvents: number;
37
+ totalCorrections: number;
38
+ totalThinkingEvents: number;
39
+ totalSamples: number;
40
+ pendingSamples: number;
41
+ approvedSamples: number;
42
+ rejectedSamples: number;
43
+ workspaceCount: number;
44
+ enabledWorkspaceCount: number;
45
+ workspaceNames: string[];
46
+ enabledWorkspaceNames: string[];
47
+ };
48
+ /**
49
+ * Get daily trend data
50
+ */
51
+ getDailyTrend(days?: number): Array<{
52
+ day: string;
53
+ toolCalls: number;
54
+ failures: number;
55
+ userCorrections: number;
56
+ thinkingTurns: number;
57
+ }>;
58
+ /**
59
+ * Get top regressions
60
+ */
61
+ getTopRegressions(limit?: number): Array<{
62
+ toolName: string;
63
+ errorType: string;
64
+ occurrences: number;
65
+ }>;
66
+ /**
67
+ * Get thinking model stats
68
+ */
69
+ getThinkingModelStats(): {
70
+ totalModels: number;
71
+ activeModels: number;
72
+ models: Array<{
73
+ modelId: string;
74
+ hits: number;
75
+ coverageRate: number;
76
+ }>;
77
+ };
78
+ /**
79
+ * Get workspace list
80
+ */
81
+ getWorkspaces(): WorkspaceInfo[];
82
+ getWorkspaceConfigs(): Array<{
83
+ workspaceName: string;
84
+ enabled: boolean;
85
+ displayName: string | null;
86
+ syncEnabled: boolean;
87
+ }>;
88
+ updateWorkspaceConfig(workspaceName: string, updates: {
89
+ enabled?: boolean;
90
+ displayName?: string | null;
91
+ syncEnabled?: boolean;
92
+ }): void;
93
+ isWorkspaceEnabled(workspaceName: string): boolean;
94
+ getEnabledWorkspaces(): WorkspaceInfo[];
95
+ addCustomWorkspace(name: string, workspacePath: string): void;
96
+ removeWorkspace(workspaceName: string): void;
97
+ getGlobalConfig(key: string): string | null;
98
+ setGlobalConfig(key: string, value: string): void;
99
+ /**
100
+ * Clear all aggregated data (for testing/reset)
101
+ */
102
+ clearAll(): void;
103
+ }
104
+ export declare function getCentralDatabase(): CentralDatabase;