mstro-app 0.4.12 → 0.4.15

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 (80) hide show
  1. package/dist/server/index.js +11 -0
  2. package/dist/server/index.js.map +1 -1
  3. package/dist/server/services/file-explorer-ops.d.ts +1 -1
  4. package/dist/server/services/file-explorer-ops.d.ts.map +1 -1
  5. package/dist/server/services/file-explorer-ops.js +7 -2
  6. package/dist/server/services/file-explorer-ops.js.map +1 -1
  7. package/dist/server/services/plan/composer.d.ts +1 -1
  8. package/dist/server/services/plan/composer.d.ts.map +1 -1
  9. package/dist/server/services/plan/composer.js +3 -2
  10. package/dist/server/services/plan/composer.js.map +1 -1
  11. package/dist/server/services/plan/executor.d.ts +5 -0
  12. package/dist/server/services/plan/executor.d.ts.map +1 -1
  13. package/dist/server/services/plan/executor.js +32 -1
  14. package/dist/server/services/plan/executor.js.map +1 -1
  15. package/dist/server/services/plan/parser-core.d.ts.map +1 -1
  16. package/dist/server/services/plan/parser-core.js +1 -0
  17. package/dist/server/services/plan/parser-core.js.map +1 -1
  18. package/dist/server/services/plan/review-gate.d.ts +2 -0
  19. package/dist/server/services/plan/review-gate.d.ts.map +1 -1
  20. package/dist/server/services/plan/review-gate.js +25 -3
  21. package/dist/server/services/plan/review-gate.js.map +1 -1
  22. package/dist/server/services/plan/types.d.ts +2 -0
  23. package/dist/server/services/plan/types.d.ts.map +1 -1
  24. package/dist/server/services/websocket/file-explorer-handlers.js +2 -1
  25. package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
  26. package/dist/server/services/websocket/git-log-handlers.d.ts.map +1 -1
  27. package/dist/server/services/websocket/git-log-handlers.js +29 -9
  28. package/dist/server/services/websocket/git-log-handlers.js.map +1 -1
  29. package/dist/server/services/websocket/git-worktree-handlers.js +8 -0
  30. package/dist/server/services/websocket/git-worktree-handlers.js.map +1 -1
  31. package/dist/server/services/websocket/handler.d.ts.map +1 -1
  32. package/dist/server/services/websocket/handler.js +5 -3
  33. package/dist/server/services/websocket/handler.js.map +1 -1
  34. package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
  35. package/dist/server/services/websocket/plan-execution-handlers.js +4 -1
  36. package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
  37. package/dist/server/services/websocket/plan-helpers.js +1 -1
  38. package/dist/server/services/websocket/plan-helpers.js.map +1 -1
  39. package/dist/server/services/websocket/quality-handlers.d.ts +1 -1
  40. package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
  41. package/dist/server/services/websocket/quality-handlers.js +67 -14
  42. package/dist/server/services/websocket/quality-handlers.js.map +1 -1
  43. package/dist/server/services/websocket/quality-persistence.d.ts +2 -0
  44. package/dist/server/services/websocket/quality-persistence.d.ts.map +1 -1
  45. package/dist/server/services/websocket/quality-persistence.js +33 -2
  46. package/dist/server/services/websocket/quality-persistence.js.map +1 -1
  47. package/dist/server/services/websocket/quality-review-agent.d.ts +33 -0
  48. package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
  49. package/dist/server/services/websocket/quality-review-agent.js +360 -72
  50. package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
  51. package/dist/server/services/websocket/quality-service.js +1 -1
  52. package/dist/server/services/websocket/quality-service.js.map +1 -1
  53. package/dist/server/services/websocket/quality-tools.d.ts +7 -1
  54. package/dist/server/services/websocket/quality-tools.d.ts.map +1 -1
  55. package/dist/server/services/websocket/quality-tools.js +43 -3
  56. package/dist/server/services/websocket/quality-tools.js.map +1 -1
  57. package/dist/server/services/websocket/quality-types.d.ts +5 -1
  58. package/dist/server/services/websocket/quality-types.d.ts.map +1 -1
  59. package/dist/server/services/websocket/quality-types.js +12 -4
  60. package/dist/server/services/websocket/quality-types.js.map +1 -1
  61. package/package.json +1 -1
  62. package/server/index.ts +11 -0
  63. package/server/services/file-explorer-ops.ts +7 -2
  64. package/server/services/plan/composer.ts +3 -1
  65. package/server/services/plan/executor.ts +32 -1
  66. package/server/services/plan/parser-core.ts +1 -0
  67. package/server/services/plan/review-gate.ts +28 -3
  68. package/server/services/plan/types.ts +2 -0
  69. package/server/services/websocket/file-explorer-handlers.ts +2 -1
  70. package/server/services/websocket/git-log-handlers.ts +30 -9
  71. package/server/services/websocket/git-worktree-handlers.ts +9 -0
  72. package/server/services/websocket/handler.ts +6 -3
  73. package/server/services/websocket/plan-execution-handlers.ts +4 -1
  74. package/server/services/websocket/plan-helpers.ts +1 -1
  75. package/server/services/websocket/quality-handlers.ts +69 -9
  76. package/server/services/websocket/quality-persistence.ts +32 -2
  77. package/server/services/websocket/quality-review-agent.ts +427 -72
  78. package/server/services/websocket/quality-service.ts +1 -1
  79. package/server/services/websocket/quality-tools.ts +48 -3
  80. package/server/services/websocket/quality-types.ts +15 -4
@@ -11,6 +11,7 @@
11
11
  */
12
12
 
13
13
  import { join } from 'node:path';
14
+ import { validatePathWithinWorkingDir } from '../pathUtils.js';
14
15
  import type { HandlerContext } from './handler-context.js';
15
16
  import type { FindingForFix } from './quality-fix-agent.js';
16
17
  import { handleFixIssues } from './quality-fix-agent.js';
@@ -39,6 +40,26 @@ function resolvePath(workingDir: string, dirPath?: string): string {
39
40
  return join(workingDir, dirPath);
40
41
  }
41
42
 
43
+ /**
44
+ * Resolve and validate a directory path for sandboxed users.
45
+ * Returns null if the path escapes the working directory.
46
+ */
47
+ function resolveAndValidatePath(
48
+ workingDir: string,
49
+ dirPath: string | undefined,
50
+ isSandboxed: boolean,
51
+ ): { resolved: string; error?: string } {
52
+ const resolved = resolvePath(workingDir, dirPath);
53
+ if (isSandboxed) {
54
+ const validation = validatePathWithinWorkingDir(resolved, workingDir);
55
+ if (!validation.valid) {
56
+ return { resolved: '', error: validation.error || 'Path outside project directory' };
57
+ }
58
+ return { resolved: validation.resolvedPath };
59
+ }
60
+ return { resolved };
61
+ }
62
+
42
63
  // ── Message router ────────────────────────────────────────────
43
64
 
44
65
  export function handleQualityMessage(
@@ -47,25 +68,33 @@ export function handleQualityMessage(
47
68
  msg: WebSocketMessage,
48
69
  _tabId: string,
49
70
  workingDir: string,
71
+ permission?: 'control' | 'view',
50
72
  ): void {
73
+ const isSandboxed = permission === 'control' || permission === 'view';
74
+ const sendPathError = (path: string, error: string) => {
75
+ ctx.send(ws, { type: 'qualityError', data: { path, error } });
76
+ };
77
+
51
78
  const handlers: Record<string, () => void> = {
52
- qualityDetectTools: () => handleDetectTools(ctx, ws, msg, workingDir),
53
- qualityScan: () => handleScan(ctx, ws, msg, workingDir),
54
- qualityInstallTools: () => handleInstallTools(ctx, ws, msg, workingDir),
79
+ qualityDetectTools: () => handleDetectTools(ctx, ws, msg, workingDir, isSandboxed),
80
+ qualityScan: () => handleScan(ctx, ws, msg, workingDir, isSandboxed),
81
+ qualityInstallTools: () => handleInstallTools(ctx, ws, msg, workingDir, isSandboxed),
55
82
  qualityCodeReview: () => {
56
- const dirPath = resolvePath(workingDir, msg.data?.path);
83
+ const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
84
+ if (error) { sendPathError(msg.data?.path || '.', error); return; }
57
85
  const reportPath = msg.data?.path || '.';
58
86
  handleCodeReview(ctx, ws, reportPath, dirPath, workingDir, activeReviews, getPersistence);
59
87
  },
60
88
  qualityFixIssues: () => {
61
- const dirPath = resolvePath(workingDir, msg.data?.path);
89
+ const { resolved: dirPath, error } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
90
+ if (error) { sendPathError(msg.data?.path || '.', error); return; }
62
91
  const reportPath = msg.data?.path || '.';
63
92
  const section: string | undefined = msg.data?.section;
64
93
  const findings: FindingForFix[] = msg.data?.findings || [];
65
94
  handleFixIssues(ctx, ws, reportPath, dirPath, workingDir, section, findings, getPersistence);
66
95
  },
67
96
  qualityLoadState: () => handleLoadState(ctx, ws, workingDir),
68
- qualitySaveDirectories: () => handleSaveDirectories(ctx, ws, msg, workingDir),
97
+ qualitySaveDirectories: () => handleSaveDirectories(ctx, ws, msg, workingDir, isSandboxed),
69
98
  };
70
99
 
71
100
  const handler = handlers[msg.type];
@@ -106,10 +135,26 @@ async function handleSaveDirectories(
106
135
  ws: WSContext,
107
136
  msg: WebSocketMessage,
108
137
  workingDir: string,
138
+ isSandboxed = false,
109
139
  ): Promise<void> {
110
140
  try {
111
141
  const persistence = getPersistence(workingDir);
112
142
  const directories: Array<{ path: string; label: string }> = msg.data?.directories || [];
143
+
144
+ // Validate all directory paths when sandboxed
145
+ if (isSandboxed) {
146
+ for (const dir of directories) {
147
+ const { error } = resolveAndValidatePath(workingDir, dir.path, true);
148
+ if (error) {
149
+ ctx.send(ws, {
150
+ type: 'qualityError',
151
+ data: { path: dir.path, error: `Cannot save directory: ${error}` },
152
+ });
153
+ return;
154
+ }
155
+ }
156
+ }
157
+
113
158
  persistence.saveConfig(directories);
114
159
  } catch (error) {
115
160
  ctx.send(ws, {
@@ -124,8 +169,13 @@ async function handleDetectTools(
124
169
  ws: WSContext,
125
170
  msg: WebSocketMessage,
126
171
  workingDir: string,
172
+ isSandboxed = false,
127
173
  ): Promise<void> {
128
- const dirPath = resolvePath(workingDir, msg.data?.path);
174
+ const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
175
+ if (pathError) {
176
+ ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
177
+ return;
178
+ }
129
179
  try {
130
180
  const { tools, ecosystem } = await detectTools(dirPath);
131
181
  ctx.send(ws, {
@@ -145,8 +195,13 @@ async function handleScan(
145
195
  ws: WSContext,
146
196
  msg: WebSocketMessage,
147
197
  workingDir: string,
198
+ isSandboxed = false,
148
199
  ): Promise<void> {
149
- const dirPath = resolvePath(workingDir, msg.data?.path);
200
+ const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
201
+ if (pathError) {
202
+ ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
203
+ return;
204
+ }
150
205
  const reportPath = msg.data?.path || '.';
151
206
 
152
207
  try {
@@ -184,8 +239,13 @@ async function handleInstallTools(
184
239
  ws: WSContext,
185
240
  msg: WebSocketMessage,
186
241
  workingDir: string,
242
+ isSandboxed = false,
187
243
  ): Promise<void> {
188
- const dirPath = resolvePath(workingDir, msg.data?.path);
244
+ const { resolved: dirPath, error: pathError } = resolveAndValidatePath(workingDir, msg.data?.path, isSandboxed);
245
+ if (pathError) {
246
+ ctx.send(ws, { type: 'qualityError', data: { path: msg.data?.path || '.', error: pathError } });
247
+ return;
248
+ }
189
249
  const reportPath = msg.data?.path || '.';
190
250
  const toolNames: string[] | undefined = msg.data?.tools;
191
251
 
@@ -11,7 +11,7 @@
11
11
  * .mstro/quality/history.json — Score history entries for trend tracking
12
12
  */
13
13
 
14
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
14
+ import { existsSync, mkdirSync, readdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';
15
15
  import { join } from 'node:path';
16
16
  import type { QualityResults } from './quality-service.js';
17
17
 
@@ -56,6 +56,7 @@ export interface QualityPersistedState {
56
56
  // ============================================================================
57
57
 
58
58
  const MAX_HISTORY_ENTRIES = 100;
59
+ const MAX_REPORT_HISTORY_FILES = 200;
59
60
 
60
61
  function slugify(dirPath: string): string {
61
62
  if (dirPath === '.' || dirPath === './') return '_root';
@@ -94,15 +95,18 @@ function writeJson(filePath: string, data: unknown): void {
94
95
  export class QualityPersistence {
95
96
  private qualityDir: string;
96
97
  private reportsDir: string;
98
+ private reportHistoryDir: string;
97
99
  private configPath: string;
98
100
  private historyPath: string;
99
101
 
100
102
  constructor(workingDir: string) {
101
103
  this.qualityDir = join(workingDir, '.mstro', 'quality');
102
104
  this.reportsDir = join(this.qualityDir, 'reports');
105
+ this.reportHistoryDir = join(this.reportsDir, 'history');
103
106
  this.configPath = join(this.qualityDir, 'config.json');
104
107
  this.historyPath = join(this.qualityDir, 'history.json');
105
108
  ensureDir(this.reportsDir);
109
+ ensureDir(this.reportHistoryDir);
106
110
  }
107
111
 
108
112
  // ---- Config (directory list) ----
@@ -141,6 +145,12 @@ export class QualityPersistence {
141
145
  const slug = slugify(dirPath);
142
146
  const reportPath = join(this.reportsDir, `${slug}.json`);
143
147
  writeJson(reportPath, results);
148
+
149
+ // Archive timestamped copy for historical tracking
150
+ const ts = Date.now();
151
+ const archivePath = join(this.reportHistoryDir, `${ts}_${slug}.json`);
152
+ writeJson(archivePath, results);
153
+ this.pruneReportHistory();
144
154
  }
145
155
 
146
156
  loadAllReports(directories: QualityDirectoryConfig[]): Record<string, QualityResults> {
@@ -154,6 +164,19 @@ export class QualityPersistence {
154
164
  return reports;
155
165
  }
156
166
 
167
+ // ---- Report history pruning ----
168
+
169
+ private pruneReportHistory(): void {
170
+ try {
171
+ const files = readdirSync(this.reportHistoryDir).filter((f) => f.endsWith('.json')).sort();
172
+ if (files.length <= MAX_REPORT_HISTORY_FILES) return;
173
+ const toRemove = files.slice(0, files.length - MAX_REPORT_HISTORY_FILES);
174
+ for (const file of toRemove) {
175
+ try { unlinkSync(join(this.reportHistoryDir, file)); } catch { /* ignore */ }
176
+ }
177
+ } catch { /* directory may not exist yet */ }
178
+ }
179
+
157
180
  // ---- History (trend tracking) ----
158
181
 
159
182
  loadHistory(): QualityHistoryEntry[] {
@@ -219,7 +242,14 @@ export class QualityPersistence {
219
242
  saveCodeReview(dirPath: string, findings: Record<string, unknown>[], summary: string): void {
220
243
  const slug = slugify(dirPath);
221
244
  const reviewPath = join(this.reportsDir, `${slug}-review.json`);
222
- writeJson(reviewPath, { findings, summary, timestamp: new Date().toISOString() });
245
+ const data = { findings, summary, timestamp: new Date().toISOString() };
246
+ writeJson(reviewPath, data);
247
+
248
+ // Archive timestamped copy for historical tracking
249
+ const ts = Date.now();
250
+ const archivePath = join(this.reportHistoryDir, `${ts}_${slug}-review.json`);
251
+ writeJson(archivePath, data);
252
+ this.pruneReportHistory();
223
253
  }
224
254
 
225
255
  // ---- Full state load ----