mstro-app 0.4.17 → 0.4.20
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/README.md +148 -75
- package/dist/server/cli/headless/claude-invoker-process.d.ts +1 -1
- package/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +4 -10
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/claude-invoker.js +1 -1
- package/dist/server/cli/headless/claude-invoker.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +7 -2
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +28 -4
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +0 -1
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +1 -4
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.d.ts +1 -1
- package/dist/server/cli/improvisation-retry.d.ts.map +1 -1
- package/dist/server/cli/improvisation-retry.js +1 -2
- package/dist/server/cli/improvisation-retry.js.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +0 -1
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +44 -9
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/index.js +17 -2
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/bouncer-haiku.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-haiku.js +10 -5
- package/dist/server/mcp/bouncer-haiku.js.map +1 -1
- package/dist/server/mcp/bouncer-integration.d.ts +3 -1
- package/dist/server/mcp/bouncer-integration.d.ts.map +1 -1
- package/dist/server/mcp/bouncer-integration.js +16 -5
- package/dist/server/mcp/bouncer-integration.js.map +1 -1
- package/dist/server/mcp/server.js +3 -1
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/services/plan/composer.d.ts +1 -1
- package/dist/server/services/plan/composer.d.ts.map +1 -1
- package/dist/server/services/plan/composer.js +2 -3
- package/dist/server/services/plan/composer.js.map +1 -1
- package/dist/server/services/plan/executor.d.ts +0 -3
- package/dist/server/services/plan/executor.d.ts.map +1 -1
- package/dist/server/services/plan/executor.js +1 -8
- package/dist/server/services/plan/executor.js.map +1 -1
- package/dist/server/services/plan/review-gate.d.ts.map +1 -1
- package/dist/server/services/plan/review-gate.js +19 -2
- package/dist/server/services/plan/review-gate.js.map +1 -1
- package/dist/server/services/plan/state-reconciler.d.ts +6 -0
- package/dist/server/services/plan/state-reconciler.d.ts.map +1 -1
- package/dist/server/services/plan/state-reconciler.js +68 -1
- package/dist/server/services/plan/state-reconciler.js.map +1 -1
- package/dist/server/services/platform.d.ts.map +1 -1
- package/dist/server/services/platform.js +18 -6
- package/dist/server/services/platform.js.map +1 -1
- package/dist/server/services/terminal/pty-manager.d.ts +2 -4
- package/dist/server/services/terminal/pty-manager.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-manager.js +5 -28
- package/dist/server/services/terminal/pty-manager.js.map +1 -1
- package/dist/server/services/terminal/pty-utils.d.ts +2 -13
- package/dist/server/services/terminal/pty-utils.d.ts.map +1 -1
- package/dist/server/services/terminal/pty-utils.js +2 -74
- package/dist/server/services/terminal/pty-utils.js.map +1 -1
- package/dist/server/services/websocket/autocomplete.d.ts +1 -1
- package/dist/server/services/websocket/autocomplete.d.ts.map +1 -1
- package/dist/server/services/websocket/autocomplete.js +37 -24
- package/dist/server/services/websocket/autocomplete.js.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.d.ts +2 -2
- package/dist/server/services/websocket/file-explorer-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/file-explorer-handlers.js +11 -4
- package/dist/server/services/websocket/file-explorer-handlers.js.map +1 -1
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +6 -1
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.d.ts +5 -5
- package/dist/server/services/websocket/plan-board-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-board-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.d.ts +6 -6
- package/dist/server/services/websocket/plan-execution-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-execution-handlers.js +1 -4
- package/dist/server/services/websocket/plan-execution-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts +1 -1
- package/dist/server/services/websocket/plan-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts +1 -1
- package/dist/server/services/websocket/plan-helpers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-helpers.js.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.d.ts +4 -4
- package/dist/server/services/websocket/plan-issue-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-issue-handlers.js +10 -0
- package/dist/server/services/websocket/plan-issue-handlers.js.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts +3 -3
- package/dist/server/services/websocket/plan-sprint-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/plan-sprint-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts +1 -1
- package/dist/server/services/websocket/quality-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-handlers.js +9 -5
- package/dist/server/services/websocket/quality-handlers.js.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.d.ts.map +1 -1
- package/dist/server/services/websocket/quality-review-agent.js +7 -4
- package/dist/server/services/websocket/quality-review-agent.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +5 -2
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.d.ts +1 -1
- package/dist/server/services/websocket/terminal-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/terminal-handlers.js +9 -21
- package/dist/server/services/websocket/terminal-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +2 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/utils/port.d.ts +0 -11
- package/dist/server/utils/port.d.ts.map +1 -1
- package/dist/server/utils/port.js +0 -31
- package/dist/server/utils/port.js.map +1 -1
- package/package.json +1 -2
- package/server/cli/headless/claude-invoker-process.ts +5 -12
- package/server/cli/headless/claude-invoker.ts +1 -1
- package/server/cli/headless/mcp-config.ts +31 -4
- package/server/cli/headless/runner.ts +0 -1
- package/server/cli/headless/types.ts +1 -4
- package/server/cli/improvisation-retry.ts +0 -2
- package/server/cli/improvisation-session-manager.ts +45 -10
- package/server/index.ts +16 -2
- package/server/mcp/bouncer-haiku.ts +11 -5
- package/server/mcp/bouncer-integration.ts +14 -5
- package/server/mcp/server.ts +3 -1
- package/server/services/plan/composer.ts +1 -3
- package/server/services/plan/executor.ts +1 -9
- package/server/services/plan/review-gate.ts +13 -2
- package/server/services/plan/state-reconciler.ts +70 -1
- package/server/services/platform.ts +17 -6
- package/server/services/terminal/pty-manager.ts +6 -33
- package/server/services/terminal/pty-utils.ts +2 -80
- package/server/services/websocket/autocomplete.ts +48 -26
- package/server/services/websocket/file-explorer-handlers.ts +14 -7
- package/server/services/websocket/handler.ts +8 -2
- package/server/services/websocket/plan-board-handlers.ts +5 -5
- package/server/services/websocket/plan-execution-handlers.ts +7 -10
- package/server/services/websocket/plan-handlers.ts +1 -1
- package/server/services/websocket/plan-helpers.ts +1 -1
- package/server/services/websocket/plan-issue-handlers.ts +14 -4
- package/server/services/websocket/plan-sprint-handlers.ts +3 -3
- package/server/services/websocket/quality-handlers.ts +9 -5
- package/server/services/websocket/quality-review-agent.ts +7 -4
- package/server/services/websocket/session-handlers.ts +8 -3
- package/server/services/websocket/terminal-handlers.ts +10 -24
- package/server/services/websocket/types.ts +2 -2
- package/server/utils/port.ts +0 -41
- package/dist/server/mcp/bouncer-sandbox.d.ts +0 -60
- package/dist/server/mcp/bouncer-sandbox.d.ts.map +0 -1
- package/dist/server/mcp/bouncer-sandbox.js +0 -182
- package/dist/server/mcp/bouncer-sandbox.js.map +0 -1
- package/dist/server/services/credentials.d.ts +0 -39
- package/dist/server/services/credentials.d.ts.map +0 -1
- package/dist/server/services/credentials.js +0 -110
- package/dist/server/services/credentials.js.map +0 -1
- package/dist/server/services/sandbox-utils.d.ts +0 -8
- package/dist/server/services/sandbox-utils.d.ts.map +0 -1
- package/dist/server/services/sandbox-utils.js +0 -75
- package/dist/server/services/sandbox-utils.js.map +0 -1
- package/server/mcp/bouncer-sandbox.ts +0 -214
- package/server/services/credentials.ts +0 -134
- package/server/services/sandbox-utils.ts +0 -82
|
@@ -109,6 +109,36 @@ function buildStateMarkdown(
|
|
|
109
109
|
return `---\n${frontMatter}\n---\n\n${sections.join('\n')}`;
|
|
110
110
|
}
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* Derive epic status from its children's actual statuses.
|
|
114
|
+
* All children done/cancelled → done (auto-complete the epic).
|
|
115
|
+
*/
|
|
116
|
+
function deriveEpicDone(epic: Issue, issueByPath: Map<string, Issue>): boolean {
|
|
117
|
+
if (epic.children.length === 0) return false;
|
|
118
|
+
if (epic.status === 'done' || epic.status === 'cancelled') return false;
|
|
119
|
+
|
|
120
|
+
return epic.children.every(childPath => {
|
|
121
|
+
const child = issueByPath.get(childPath);
|
|
122
|
+
return child && (child.status === 'done' || child.status === 'cancelled');
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function reconcileEpicStatuses(pmDir: string, issues: Issue[], issueByPath: Map<string, Issue>): void {
|
|
127
|
+
const epics = issues.filter(i => i.type === 'epic');
|
|
128
|
+
for (const epic of epics) {
|
|
129
|
+
if (!deriveEpicDone(epic, issueByPath)) continue;
|
|
130
|
+
|
|
131
|
+
const epicPath = join(pmDir, epic.path);
|
|
132
|
+
try {
|
|
133
|
+
let content = readFileSync(epicPath, 'utf-8');
|
|
134
|
+
content = replaceFrontMatterField(content, 'status', 'done');
|
|
135
|
+
writeFileSync(epicPath, content, 'utf-8');
|
|
136
|
+
} catch {
|
|
137
|
+
// Epic file may be missing or unwritable
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
112
142
|
/**
|
|
113
143
|
* Derive sprint status from its issues' actual statuses.
|
|
114
144
|
* - All issues done/cancelled → completed
|
|
@@ -155,6 +185,40 @@ function reconcileSprintStatuses(pmDir: string, sprints: Sprint[], issueByPath:
|
|
|
155
185
|
}
|
|
156
186
|
}
|
|
157
187
|
|
|
188
|
+
/**
|
|
189
|
+
* After an issue is updated, check if its parent epic should be auto-completed.
|
|
190
|
+
* Returns the epic's relative path if it was marked done, null otherwise.
|
|
191
|
+
*/
|
|
192
|
+
export function tryCompleteParentEpic(workingDir: string, updatedIssue: Issue): string | null {
|
|
193
|
+
if (!updatedIssue.epic) return null;
|
|
194
|
+
|
|
195
|
+
const pmDir = resolvePmDir(workingDir);
|
|
196
|
+
if (!pmDir) return null;
|
|
197
|
+
|
|
198
|
+
// Determine which board the issue belongs to from its path
|
|
199
|
+
const boardMatch = updatedIssue.path.match(/^boards\/([^/]+)\//);
|
|
200
|
+
const issues = boardMatch
|
|
201
|
+
? parseBoardDirectory(pmDir, boardMatch[1])?.issues
|
|
202
|
+
: parsePlanDirectory(workingDir)?.issues;
|
|
203
|
+
if (!issues) return null;
|
|
204
|
+
|
|
205
|
+
const epic = issues.find(i => i.path === updatedIssue.epic);
|
|
206
|
+
if (!epic) return null;
|
|
207
|
+
|
|
208
|
+
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
209
|
+
if (!deriveEpicDone(epic, issueByPath)) return null;
|
|
210
|
+
|
|
211
|
+
const epicFullPath = join(pmDir, epic.path);
|
|
212
|
+
try {
|
|
213
|
+
let content = readFileSync(epicFullPath, 'utf-8');
|
|
214
|
+
content = replaceFrontMatterField(content, 'status', 'done');
|
|
215
|
+
writeFileSync(epicFullPath, content, 'utf-8');
|
|
216
|
+
return epic.path;
|
|
217
|
+
} catch {
|
|
218
|
+
return null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
158
222
|
export function reconcileState(workingDir: string, boardId?: string): void {
|
|
159
223
|
const pmDir = resolvePmDir(workingDir);
|
|
160
224
|
if (!pmDir) return;
|
|
@@ -183,7 +247,8 @@ export function reconcileState(workingDir: string, boardId?: string): void {
|
|
|
183
247
|
const fmMatch = content.match(/^---\n([\s\S]*?)\n---/);
|
|
184
248
|
const frontMatter = fmMatch ? fmMatch[1] : `project: "${project.name}"\ncurrent_sprint: null\nactive_milestone: null\npaused: false\nlast_session: null`;
|
|
185
249
|
|
|
186
|
-
// Reconcile sprint statuses from actual issue statuses
|
|
250
|
+
// Reconcile epic and sprint statuses from actual issue statuses
|
|
251
|
+
reconcileEpicStatuses(pmDir, issues, issueByPath);
|
|
187
252
|
reconcileSprintStatuses(pmDir, sprints, issueByPath);
|
|
188
253
|
|
|
189
254
|
// Update current_sprint in front matter based on actual sprint statuses
|
|
@@ -211,6 +276,10 @@ function reconcileBoardState(pmDir: string, _workingDir: string, boardId?: strin
|
|
|
211
276
|
const { board, issues } = boardState;
|
|
212
277
|
|
|
213
278
|
const issueByPath = new Map(issues.map(i => [i.path, i]));
|
|
279
|
+
|
|
280
|
+
// Reconcile epic statuses before categorizing
|
|
281
|
+
reconcileEpicStatuses(pmDir, issues, issueByPath);
|
|
282
|
+
|
|
214
283
|
const categories = categorizeIssues(issues, issueByPath);
|
|
215
284
|
const warnings = computeWarnings(issues);
|
|
216
285
|
|
|
@@ -21,7 +21,6 @@ import {
|
|
|
21
21
|
updateCredentials,
|
|
22
22
|
} from './platform-credentials.js'
|
|
23
23
|
import { captureException } from './sentry.js'
|
|
24
|
-
import { isBwrapAvailable } from './terminal/pty-utils.js'
|
|
25
24
|
|
|
26
25
|
/**
|
|
27
26
|
* Get machine identification string
|
|
@@ -40,8 +39,12 @@ let WebSocketImpl: typeof WebSocket
|
|
|
40
39
|
if (typeof WebSocket !== 'undefined') {
|
|
41
40
|
WebSocketImpl = WebSocket
|
|
42
41
|
} else {
|
|
43
|
-
|
|
44
|
-
|
|
42
|
+
try {
|
|
43
|
+
const { default: WS } = await import('ws')
|
|
44
|
+
WebSocketImpl = WS as unknown as typeof WebSocket
|
|
45
|
+
} catch {
|
|
46
|
+
throw new Error('WebSocket not available: install the "ws" package or use Node.js 21+')
|
|
47
|
+
}
|
|
45
48
|
}
|
|
46
49
|
|
|
47
50
|
// PLATFORM_URL is set via --server / --dev flag in mstro.js
|
|
@@ -122,7 +125,7 @@ export class PlatformConnection {
|
|
|
122
125
|
|
|
123
126
|
private startHeartbeat(): void {
|
|
124
127
|
this.missedPongs = 0
|
|
125
|
-
this.heartbeatInterval = setInterval(() => this.heartbeatTick(),
|
|
128
|
+
this.heartbeatInterval = setInterval(() => this.heartbeatTick(), 25_000)
|
|
126
129
|
}
|
|
127
130
|
|
|
128
131
|
private heartbeatTick(): void {
|
|
@@ -186,7 +189,7 @@ export class PlatformConnection {
|
|
|
186
189
|
osType,
|
|
187
190
|
cpuArch,
|
|
188
191
|
cliVersion: CLI_VERSION,
|
|
189
|
-
capabilities: JSON.stringify({
|
|
192
|
+
capabilities: JSON.stringify({}),
|
|
190
193
|
startedAt: this.startedAt,
|
|
191
194
|
})
|
|
192
195
|
|
|
@@ -231,6 +234,7 @@ export class PlatformConnection {
|
|
|
231
234
|
}
|
|
232
235
|
|
|
233
236
|
this.ws.onclose = (event) => {
|
|
237
|
+
clearTimeout(connectionTimeout)
|
|
234
238
|
this.stopHeartbeat()
|
|
235
239
|
this.isConnected = false
|
|
236
240
|
|
|
@@ -254,6 +258,7 @@ export class PlatformConnection {
|
|
|
254
258
|
}
|
|
255
259
|
|
|
256
260
|
this.ws.onerror = () => {
|
|
261
|
+
clearTimeout(connectionTimeout)
|
|
257
262
|
// onclose will be called after this
|
|
258
263
|
}
|
|
259
264
|
}
|
|
@@ -275,6 +280,10 @@ export class PlatformConnection {
|
|
|
275
280
|
this.callbacks.onWebDisconnected?.()
|
|
276
281
|
trackEvent(AnalyticsEvents.WEB_CLIENT_DISCONNECTED)
|
|
277
282
|
break
|
|
283
|
+
case 'ping':
|
|
284
|
+
// Server-initiated ping — respond with pong to reset stale detection
|
|
285
|
+
this.send({ type: 'pong' })
|
|
286
|
+
break
|
|
278
287
|
case 'pong':
|
|
279
288
|
this.missedPongs = 0
|
|
280
289
|
break
|
|
@@ -293,7 +302,9 @@ export class PlatformConnection {
|
|
|
293
302
|
}
|
|
294
303
|
|
|
295
304
|
this.reconnectAttempts++
|
|
296
|
-
const
|
|
305
|
+
const base = Math.min(1000 * 2 ** (this.reconnectAttempts - 1), 30000)
|
|
306
|
+
const jitter = base * 0.25 * (2 * Math.random() - 1)
|
|
307
|
+
const delay = Math.max(0, Math.round(base + jitter))
|
|
297
308
|
|
|
298
309
|
this.reconnectTimeout = setTimeout(() => {
|
|
299
310
|
this.reconnectTimeout = null
|
|
@@ -10,17 +10,14 @@
|
|
|
10
10
|
|
|
11
11
|
import { EventEmitter } from 'node:events';
|
|
12
12
|
import { homedir, platform } from 'node:os';
|
|
13
|
-
import { sanitizeEnvForSandbox } from '../sandbox-utils.js';
|
|
14
13
|
import type { PTYSession } from './pty-utils.js';
|
|
15
14
|
import {
|
|
16
|
-
buildBwrapArgs,
|
|
17
15
|
detectShell,
|
|
18
16
|
getPty,
|
|
19
17
|
getPtyInstallInstructions,
|
|
20
18
|
getShellName,
|
|
21
|
-
isBwrapAvailable,
|
|
22
19
|
isPtyAvailable,
|
|
23
|
-
|
|
20
|
+
SCROLLBACK_MAX_LENGTH,
|
|
24
21
|
ScrollbackBuffer,
|
|
25
22
|
} from './pty-utils.js';
|
|
26
23
|
|
|
@@ -54,14 +51,13 @@ export class PTYManager extends EventEmitter {
|
|
|
54
51
|
return getPtyInstallInstructions();
|
|
55
52
|
}
|
|
56
53
|
|
|
57
|
-
create(
|
|
54
|
+
async create(
|
|
58
55
|
terminalId: string,
|
|
59
56
|
workingDir: string,
|
|
60
57
|
cols: number = 80,
|
|
61
58
|
rows: number = 24,
|
|
62
59
|
requestedShell?: string,
|
|
63
|
-
|
|
64
|
-
): { shell: string; cwd: string; isReconnect: boolean; platform: string } {
|
|
60
|
+
): Promise<{ shell: string; cwd: string; isReconnect: boolean; platform: string }> {
|
|
65
61
|
const pty = getPty();
|
|
66
62
|
if (!pty) {
|
|
67
63
|
throw new Error(`PTY_NOT_AVAILABLE:${getPtyInstallInstructions()}`);
|
|
@@ -80,32 +76,9 @@ export class PTYManager extends EventEmitter {
|
|
|
80
76
|
const cwd = workingDir || homedir();
|
|
81
77
|
|
|
82
78
|
try {
|
|
83
|
-
const
|
|
84
|
-
? sanitizeEnvForSandbox(process.env, cwd)
|
|
85
|
-
: { ...process.env, HOME: homedir() };
|
|
86
|
-
const env = { ...baseEnv, TERM: 'xterm-256color', COLORTERM: 'truecolor' };
|
|
79
|
+
const env = { ...process.env, HOME: homedir(), TERM: 'xterm-256color', COLORTERM: 'truecolor' };
|
|
87
80
|
|
|
88
|
-
|
|
89
|
-
// The shell is spawned inside a namespace that only sees the project directory (rw)
|
|
90
|
-
// and system directories (ro). Without bwrap, sandboxed terminals are not available.
|
|
91
|
-
let spawnCommand: string;
|
|
92
|
-
let spawnArgs: string[];
|
|
93
|
-
let spawnCwd: string;
|
|
94
|
-
|
|
95
|
-
if (options?.sandboxed) {
|
|
96
|
-
if (!isBwrapAvailable()) {
|
|
97
|
-
throw new Error('SANDBOX_UNAVAILABLE:Terminal sandbox (bubblewrap) is not installed on this machine. Shared terminal sessions require bubblewrap for filesystem isolation.');
|
|
98
|
-
}
|
|
99
|
-
spawnCommand = '/usr/bin/bwrap';
|
|
100
|
-
spawnArgs = buildBwrapArgs(cwd, shell);
|
|
101
|
-
spawnCwd = '/'; // bwrap manages cwd internally via --chdir
|
|
102
|
-
} else {
|
|
103
|
-
spawnCommand = shell;
|
|
104
|
-
spawnArgs = [];
|
|
105
|
-
spawnCwd = cwd;
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const ptyProcess = pty.spawn(spawnCommand, spawnArgs, { name: 'xterm-256color', cols, rows, cwd: spawnCwd, env });
|
|
81
|
+
const ptyProcess = pty.spawn(shell, [], { name: 'xterm-256color', cols, rows, cwd, env });
|
|
109
82
|
|
|
110
83
|
const session: PTYSession = {
|
|
111
84
|
id: terminalId,
|
|
@@ -118,7 +91,7 @@ export class PTYManager extends EventEmitter {
|
|
|
118
91
|
rows,
|
|
119
92
|
_outputBuffer: '',
|
|
120
93
|
_outputTimer: null,
|
|
121
|
-
scrollback: new ScrollbackBuffer(
|
|
94
|
+
scrollback: new ScrollbackBuffer(SCROLLBACK_MAX_LENGTH),
|
|
122
95
|
};
|
|
123
96
|
this.terminals.set(terminalId, session);
|
|
124
97
|
|
|
@@ -8,8 +8,6 @@
|
|
|
8
8
|
* on session lifecycle orchestration.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { execSync } from 'node:child_process';
|
|
12
|
-
import { accessSync, constants as fsConstants, lstatSync } from 'node:fs';
|
|
13
11
|
import { createRequire } from 'node:module';
|
|
14
12
|
import { platform } from 'node:os';
|
|
15
13
|
|
|
@@ -117,89 +115,13 @@ export function getShellName(shellPath: string): string {
|
|
|
117
115
|
return parts[parts.length - 1] || 'shell';
|
|
118
116
|
}
|
|
119
117
|
|
|
120
|
-
// ── Bubblewrap (bwrap) sandbox detection ─────────────────────
|
|
121
|
-
|
|
122
|
-
let _bwrapAvailable: boolean | null = null;
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
* Check if bubblewrap (bwrap) is available for filesystem sandboxing.
|
|
126
|
-
* Required for sandboxed terminal sessions (shared "can control" users).
|
|
127
|
-
* Caches the result after first check.
|
|
128
|
-
*/
|
|
129
|
-
export function isBwrapAvailable(): boolean {
|
|
130
|
-
if (_bwrapAvailable !== null) return _bwrapAvailable;
|
|
131
|
-
|
|
132
|
-
if (platform() !== 'linux') {
|
|
133
|
-
_bwrapAvailable = false;
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
try {
|
|
138
|
-
accessSync('/usr/bin/bwrap', fsConstants.X_OK);
|
|
139
|
-
execSync('bwrap --ro-bind / / -- /bin/true', { timeout: 5000, stdio: 'ignore' });
|
|
140
|
-
_bwrapAvailable = true;
|
|
141
|
-
} catch {
|
|
142
|
-
_bwrapAvailable = false;
|
|
143
|
-
}
|
|
144
|
-
return _bwrapAvailable;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Build bwrap arguments to sandbox a shell to a specific directory.
|
|
149
|
-
* Provides read-only access to system directories, read-write to the project dir only.
|
|
150
|
-
*/
|
|
151
|
-
export function buildBwrapArgs(cwd: string, shell: string): string[] {
|
|
152
|
-
const mergedUsr = (() => {
|
|
153
|
-
try { return lstatSync('/bin').isSymbolicLink(); }
|
|
154
|
-
catch { return false; }
|
|
155
|
-
})();
|
|
156
|
-
|
|
157
|
-
const args: string[] = [
|
|
158
|
-
'--ro-bind', '/usr', '/usr',
|
|
159
|
-
'--ro-bind', '/etc', '/etc',
|
|
160
|
-
// Hide sensitive /etc files by binding /dev/null over them
|
|
161
|
-
'--ro-bind', '/dev/null', '/etc/shadow',
|
|
162
|
-
'--ro-bind', '/dev/null', '/etc/gshadow',
|
|
163
|
-
];
|
|
164
|
-
|
|
165
|
-
if (mergedUsr) {
|
|
166
|
-
// Merged-usr distros (Fedora, Ubuntu 20.04+, Arch, Debian 12+)
|
|
167
|
-
args.push('--symlink', 'usr/bin', '/bin');
|
|
168
|
-
args.push('--symlink', 'usr/sbin', '/sbin');
|
|
169
|
-
args.push('--symlink', 'usr/lib', '/lib');
|
|
170
|
-
try { lstatSync('/lib64'); args.push('--symlink', 'usr/lib64', '/lib64'); } catch { /* skip */ }
|
|
171
|
-
} else {
|
|
172
|
-
args.push('--ro-bind', '/bin', '/bin');
|
|
173
|
-
args.push('--ro-bind', '/sbin', '/sbin');
|
|
174
|
-
args.push('--ro-bind', '/lib', '/lib');
|
|
175
|
-
try { lstatSync('/lib64'); args.push('--ro-bind', '/lib64', '/lib64'); } catch { /* skip */ }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
args.push(
|
|
179
|
-
'--proc', '/proc',
|
|
180
|
-
'--dev', '/dev',
|
|
181
|
-
'--tmpfs', '/tmp',
|
|
182
|
-
'--tmpfs', '/run',
|
|
183
|
-
// Read-write access to the project directory only
|
|
184
|
-
'--bind', cwd, cwd,
|
|
185
|
-
'--unshare-pid',
|
|
186
|
-
'--unshare-ipc',
|
|
187
|
-
'--die-with-parent',
|
|
188
|
-
'--chdir', cwd,
|
|
189
|
-
'--',
|
|
190
|
-
shell,
|
|
191
|
-
);
|
|
192
|
-
|
|
193
|
-
return args;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
118
|
// ── Scrollback buffer ─────────────────────────────────────────
|
|
197
119
|
|
|
198
|
-
export const
|
|
120
|
+
export const SCROLLBACK_MAX_LENGTH = 256 * 1024; // ~256K characters
|
|
199
121
|
|
|
200
122
|
/**
|
|
201
123
|
* Fixed-size buffer that retains the most recent PTY output for replay on reconnect.
|
|
202
|
-
* Stores raw string chunks and evicts oldest data when the total exceeds
|
|
124
|
+
* Stores raw string chunks and evicts oldest data when the total exceeds maxLength.
|
|
203
125
|
*/
|
|
204
126
|
export class ScrollbackBuffer {
|
|
205
127
|
private chunks: string[] = [];
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
10
|
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
11
|
-
import { join, } from 'node:path';
|
|
11
|
+
import { join, normalize, resolve } from 'node:path';
|
|
12
12
|
import Fuse, { type FuseResult } from 'fuse.js';
|
|
13
13
|
import {
|
|
14
|
-
CACHE_TTL_MS,
|
|
14
|
+
CACHE_TTL_MS,
|
|
15
15
|
directoryCache,
|
|
16
16
|
getFileType,
|
|
17
17
|
isIgnored,
|
|
@@ -100,6 +100,30 @@ function shouldIncludeEntry(
|
|
|
100
100
|
return true;
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
interface PathScope {
|
|
104
|
+
scopedDir: string;
|
|
105
|
+
searchQuery: string;
|
|
106
|
+
pathPrefix: string;
|
|
107
|
+
maxDepth: number;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Parse a partial path into its directory scope, search query, and depth limit. */
|
|
111
|
+
function resolvePathScope(cleanPath: string, workingDir: string, sandboxed?: boolean): PathScope | null {
|
|
112
|
+
const lastSlashIndex = cleanPath.lastIndexOf('/');
|
|
113
|
+
if (lastSlashIndex === -1) {
|
|
114
|
+
return { scopedDir: workingDir, searchQuery: cleanPath, pathPrefix: '', maxDepth: cleanPath === '' ? 4 : 10 };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const dirPath = cleanPath.substring(0, lastSlashIndex);
|
|
118
|
+
if (sandboxed && !isPathWithinDir(dirPath, workingDir)) return null;
|
|
119
|
+
|
|
120
|
+
const candidateDir = join(workingDir, dirPath);
|
|
121
|
+
if (existsSync(candidateDir) && statSync(candidateDir).isDirectory()) {
|
|
122
|
+
return { scopedDir: candidateDir, searchQuery: cleanPath.substring(lastSlashIndex + 1), pathPrefix: `${dirPath}/`, maxDepth: 3 };
|
|
123
|
+
}
|
|
124
|
+
return { scopedDir: workingDir, searchQuery: cleanPath, pathPrefix: '', maxDepth: 10 };
|
|
125
|
+
}
|
|
126
|
+
|
|
103
127
|
export class AutocompleteService {
|
|
104
128
|
private frecencyData: FrecencyData = {};
|
|
105
129
|
|
|
@@ -175,12 +199,18 @@ export class AutocompleteService {
|
|
|
175
199
|
/**
|
|
176
200
|
* Get file completions for autocomplete with directory-scoped navigation
|
|
177
201
|
*/
|
|
178
|
-
getFileCompletions(partialPath: string, workingDir: string): AutocompleteResult[] {
|
|
202
|
+
getFileCompletions(partialPath: string, workingDir: string, sandboxed?: boolean): AutocompleteResult[] {
|
|
179
203
|
try {
|
|
180
204
|
// Handle @ symbol prefix for file autocomplete
|
|
181
205
|
const isAtSymbol = partialPath.startsWith('@');
|
|
182
206
|
const cleanPath = isAtSymbol ? partialPath.substring(1) : partialPath;
|
|
183
207
|
|
|
208
|
+
// Sandboxed users: block path traversal outside the working directory.
|
|
209
|
+
// Resolves the target path and checks it stays within workingDir boundaries.
|
|
210
|
+
if (sandboxed && cleanPath && !isPathWithinDir(cleanPath, workingDir)) {
|
|
211
|
+
return [];
|
|
212
|
+
}
|
|
213
|
+
|
|
184
214
|
// Parse .gitignore patterns
|
|
185
215
|
const gitignorePatterns = parseGitignore(workingDir);
|
|
186
216
|
|
|
@@ -189,28 +219,10 @@ export class AutocompleteService {
|
|
|
189
219
|
return this.getDirectoryContentsEnhanced(cleanPath, workingDir, gitignorePatterns);
|
|
190
220
|
}
|
|
191
221
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
let scopedDir = workingDir;
|
|
195
|
-
let searchQuery = cleanPath;
|
|
196
|
-
let pathPrefix = '';
|
|
197
|
-
let maxDepth = 10;
|
|
198
|
-
|
|
199
|
-
if (lastSlashIndex !== -1) {
|
|
200
|
-
const dirPath = cleanPath.substring(0, lastSlashIndex);
|
|
201
|
-
const candidateDir = join(workingDir, dirPath);
|
|
202
|
-
|
|
203
|
-
if (existsSync(candidateDir) && statSync(candidateDir).isDirectory()) {
|
|
204
|
-
scopedDir = candidateDir;
|
|
205
|
-
searchQuery = cleanPath.substring(lastSlashIndex + 1);
|
|
206
|
-
pathPrefix = `${dirPath}/`;
|
|
207
|
-
maxDepth = 3;
|
|
208
|
-
}
|
|
209
|
-
} else if (cleanPath === '') {
|
|
210
|
-
maxDepth = 4;
|
|
211
|
-
}
|
|
222
|
+
const scope = resolvePathScope(cleanPath, workingDir, sandboxed);
|
|
223
|
+
if (!scope) return [];
|
|
212
224
|
|
|
213
|
-
const filesWithMetadata = this.getFilesWithCache(scopedDir, gitignorePatterns, maxDepth, pathPrefix);
|
|
225
|
+
const filesWithMetadata = this.getFilesWithCache(scope.scopedDir, gitignorePatterns, scope.maxDepth, scope.pathPrefix);
|
|
214
226
|
|
|
215
227
|
// Track which files are recent
|
|
216
228
|
const recentFiles = new Set<string>();
|
|
@@ -220,9 +232,9 @@ export class AutocompleteService {
|
|
|
220
232
|
}
|
|
221
233
|
}
|
|
222
234
|
|
|
223
|
-
const scoredMatches = searchQuery === ''
|
|
235
|
+
const scoredMatches = scope.searchQuery === ''
|
|
224
236
|
? this.scoreEmptyQuery(filesWithMetadata)
|
|
225
|
-
: this.scoreWithQuery(filesWithMetadata, searchQuery, recentFiles);
|
|
237
|
+
: this.scoreWithQuery(filesWithMetadata, scope.searchQuery, recentFiles);
|
|
226
238
|
|
|
227
239
|
const results: AutocompleteResult[] = scoredMatches.slice(0, 15).map(file => {
|
|
228
240
|
const displayPath = file.isDirectory ? `${file.relativePath}/` : file.relativePath;
|
|
@@ -440,3 +452,13 @@ export class AutocompleteService {
|
|
|
440
452
|
}
|
|
441
453
|
}
|
|
442
454
|
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* Check if a relative path resolves to a location within the working directory.
|
|
458
|
+
* Used to prevent path traversal (../) in sandboxed autocomplete.
|
|
459
|
+
*/
|
|
460
|
+
function isPathWithinDir(relativePath: string, workingDir: string): boolean {
|
|
461
|
+
const resolved = normalize(resolve(workingDir, relativePath));
|
|
462
|
+
const normalizedWorkDir = resolve(workingDir);
|
|
463
|
+
return resolved === normalizedWorkDir || resolved.startsWith(`${normalizedWorkDir}/`);
|
|
464
|
+
}
|
|
@@ -16,24 +16,31 @@ import { readFileContent } from './file-utils.js';
|
|
|
16
16
|
import type { HandlerContext } from './handler-context.js';
|
|
17
17
|
import type { WebSocketMessage, WebSocketResponse, WSContext } from './types.js';
|
|
18
18
|
|
|
19
|
-
export function handleFileMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: '
|
|
19
|
+
export function handleFileMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): void {
|
|
20
|
+
const isSandboxed = !!permission;
|
|
20
21
|
switch (msg.type) {
|
|
21
22
|
case 'autocomplete':
|
|
22
23
|
if (!msg.data?.partialPath) throw new Error('Partial path is required');
|
|
23
|
-
ctx.send(ws, { type: 'autocomplete', tabId, data: { completions: ctx.autocompleteService.getFileCompletions(msg.data.partialPath, workingDir) } });
|
|
24
|
+
ctx.send(ws, { type: 'autocomplete', tabId, data: { completions: ctx.autocompleteService.getFileCompletions(msg.data.partialPath, workingDir, isSandboxed || undefined) } });
|
|
24
25
|
break;
|
|
25
26
|
case 'readFile':
|
|
26
27
|
handleReadFile(ctx, ws, msg, tabId, workingDir, permission);
|
|
27
28
|
break;
|
|
28
29
|
case 'recordSelection':
|
|
29
|
-
if (msg.data?.filePath)
|
|
30
|
+
if (msg.data?.filePath) {
|
|
31
|
+
if (isSandboxed) {
|
|
32
|
+
const validation = validatePathWithinWorkingDir(msg.data.filePath, workingDir);
|
|
33
|
+
if (!validation.valid) break; // Silently ignore out-of-bounds selections
|
|
34
|
+
}
|
|
35
|
+
ctx.recordFileSelection(msg.data.filePath);
|
|
36
|
+
}
|
|
30
37
|
break;
|
|
31
38
|
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
|
-
function handleReadFile(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: '
|
|
41
|
+
function handleReadFile(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): void {
|
|
35
42
|
if (!msg.data?.filePath) throw new Error('File path is required');
|
|
36
|
-
const isSandboxed = permission
|
|
43
|
+
const isSandboxed = !!permission;
|
|
37
44
|
if (isSandboxed) {
|
|
38
45
|
const validation = validatePathWithinWorkingDir(msg.data.filePath, workingDir);
|
|
39
46
|
if (!validation.valid) {
|
|
@@ -51,8 +58,8 @@ function sendFileResult(ctx: HandlerContext, ws: WSContext, type: WebSocketRespo
|
|
|
51
58
|
ctx.send(ws, { type, tabId, data });
|
|
52
59
|
}
|
|
53
60
|
|
|
54
|
-
export function handleFileExplorerMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: '
|
|
55
|
-
const isSandboxed = permission
|
|
61
|
+
export function handleFileExplorerMessage(ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): void {
|
|
62
|
+
const isSandboxed = !!permission;
|
|
56
63
|
const handlers: Record<string, () => void> = {
|
|
57
64
|
listDirectory: () => {
|
|
58
65
|
if (isSandboxed && msg.data?.dirPath) {
|
|
@@ -140,7 +140,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
140
140
|
fileUploadStart: 'fileUpload', fileUploadChunk: 'fileUpload', fileUploadComplete: 'fileUpload', fileUploadCancel: 'fileUpload',
|
|
141
141
|
};
|
|
142
142
|
|
|
143
|
-
private async dispatchMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: '
|
|
143
|
+
private async dispatchMessage(ws: WSContext, msg: WebSocketMessage, tabId: string, workingDir: string, permission?: 'view'): Promise<void> {
|
|
144
144
|
// Handle messages with custom inline logic first
|
|
145
145
|
switch (msg.type) {
|
|
146
146
|
case 'ping':
|
|
@@ -185,7 +185,7 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
185
185
|
case 'session': return handleSessionMessage(this, ws, msg, tabId, permission);
|
|
186
186
|
case 'history': return handleHistoryMessage(this, ws, msg, tabId, workingDir);
|
|
187
187
|
case 'file': return handleFileMessage(this, ws, msg, tabId, effectiveDir, permission);
|
|
188
|
-
case 'terminal': return handleTerminalMessage(this, ws, msg, tabId, workingDir
|
|
188
|
+
case 'terminal': return handleTerminalMessage(this, ws, msg, tabId, workingDir);
|
|
189
189
|
case 'fileExplorer': return handleFileExplorerMessage(this, ws, msg, tabId, effectiveDir, permission);
|
|
190
190
|
case 'git': return handleGitMessage(this, ws, msg, tabId, workingDir);
|
|
191
191
|
case 'quality': return handleQualityMessage(this, ws, msg, tabId, workingDir, permission);
|
|
@@ -233,6 +233,12 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
233
233
|
this.connections.delete(ws);
|
|
234
234
|
this.allConnections.delete(ws);
|
|
235
235
|
cleanupTerminalSubscribers(this, ws);
|
|
236
|
+
|
|
237
|
+
// Clean up file upload handler when no connections remain
|
|
238
|
+
if (this.allConnections.size === 0 && this.fileUploadHandler) {
|
|
239
|
+
this.fileUploadHandler.destroy();
|
|
240
|
+
this.fileUploadHandler = null;
|
|
241
|
+
}
|
|
236
242
|
}
|
|
237
243
|
|
|
238
244
|
send(ws: WSContext, response: WebSocketResponse): void {
|
|
@@ -16,7 +16,7 @@ import type { WebSocketMessage, WSContext } from './types.js';
|
|
|
16
16
|
|
|
17
17
|
export function handleCreateBoard(
|
|
18
18
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
19
|
-
workingDir: string, permission?: '
|
|
19
|
+
workingDir: string, permission?: 'view',
|
|
20
20
|
): void {
|
|
21
21
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
22
22
|
|
|
@@ -99,7 +99,7 @@ paused: false
|
|
|
99
99
|
|
|
100
100
|
export function handleUpdateBoard(
|
|
101
101
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
102
|
-
workingDir: string, permission?: '
|
|
102
|
+
workingDir: string, permission?: 'view',
|
|
103
103
|
): void {
|
|
104
104
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
105
105
|
|
|
@@ -133,7 +133,7 @@ export function handleUpdateBoard(
|
|
|
133
133
|
|
|
134
134
|
export function handleArchiveBoard(
|
|
135
135
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
136
|
-
workingDir: string, permission?: '
|
|
136
|
+
workingDir: string, permission?: 'view',
|
|
137
137
|
): void {
|
|
138
138
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
139
139
|
|
|
@@ -204,7 +204,7 @@ export function handleGetBoardState(
|
|
|
204
204
|
|
|
205
205
|
export function handleReorderBoards(
|
|
206
206
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
207
|
-
workingDir: string, permission?: '
|
|
207
|
+
workingDir: string, permission?: 'view',
|
|
208
208
|
): void {
|
|
209
209
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
210
210
|
|
|
@@ -229,7 +229,7 @@ export function handleReorderBoards(
|
|
|
229
229
|
|
|
230
230
|
export function handleSetActiveBoard(
|
|
231
231
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
232
|
-
workingDir: string, permission?: '
|
|
232
|
+
workingDir: string, permission?: 'view',
|
|
233
233
|
): void {
|
|
234
234
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
235
235
|
|
|
@@ -14,7 +14,7 @@ import type { WebSocketMessage, WSContext } from './types.js';
|
|
|
14
14
|
|
|
15
15
|
export function handlePrompt(
|
|
16
16
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
17
|
-
workingDir: string, permission?: '
|
|
17
|
+
workingDir: string, permission?: 'view',
|
|
18
18
|
): void {
|
|
19
19
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
20
20
|
|
|
@@ -24,8 +24,7 @@ export function handlePrompt(
|
|
|
24
24
|
ctx.send(ws, { type: 'planError', data: { error: 'Prompt required' } });
|
|
25
25
|
return;
|
|
26
26
|
}
|
|
27
|
-
|
|
28
|
-
handlePlanPrompt(ctx, ws, prompt, workingDir, boardId, sandboxed).catch(error => {
|
|
27
|
+
handlePlanPrompt(ctx, ws, prompt, workingDir, boardId).catch(error => {
|
|
29
28
|
ctx.send(ws, {
|
|
30
29
|
type: 'planError',
|
|
31
30
|
data: { error: error instanceof Error ? error.message : String(error) },
|
|
@@ -96,12 +95,11 @@ function wireExecutorEvents(executor: PlanExecutor, ctx: HandlerContext, working
|
|
|
96
95
|
|
|
97
96
|
export function handleExecute(
|
|
98
97
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
99
|
-
workingDir: string, permission?: '
|
|
98
|
+
workingDir: string, permission?: 'view',
|
|
100
99
|
): void {
|
|
101
100
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
102
101
|
|
|
103
102
|
const executor = getExecutor(workingDir);
|
|
104
|
-
executor.setSandboxed(permission === 'control');
|
|
105
103
|
|
|
106
104
|
if (executor.getStatus() === 'executing' || executor.getStatus() === 'starting') {
|
|
107
105
|
ctx.send(ws, { type: 'planError', data: { error: 'Execution already in progress' } });
|
|
@@ -123,7 +121,7 @@ export function handleExecute(
|
|
|
123
121
|
|
|
124
122
|
export function handleExecuteEpic(
|
|
125
123
|
ctx: HandlerContext, ws: WSContext, msg: WebSocketMessage,
|
|
126
|
-
workingDir: string, permission?: '
|
|
124
|
+
workingDir: string, permission?: 'view',
|
|
127
125
|
): void {
|
|
128
126
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
129
127
|
|
|
@@ -134,7 +132,6 @@ export function handleExecuteEpic(
|
|
|
134
132
|
}
|
|
135
133
|
|
|
136
134
|
const executor = getExecutor(workingDir);
|
|
137
|
-
executor.setSandboxed(permission === 'control');
|
|
138
135
|
|
|
139
136
|
if (executor.getStatus() === 'executing' || executor.getStatus() === 'starting') {
|
|
140
137
|
ctx.send(ws, { type: 'planError', data: { error: 'Execution already in progress' } });
|
|
@@ -154,7 +151,7 @@ export function handleExecuteEpic(
|
|
|
154
151
|
|
|
155
152
|
export function handlePause(
|
|
156
153
|
ctx: HandlerContext, ws: WSContext,
|
|
157
|
-
workingDir: string, permission?: '
|
|
154
|
+
workingDir: string, permission?: 'view',
|
|
158
155
|
): void {
|
|
159
156
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
160
157
|
const executor = executorCache.get(workingDir);
|
|
@@ -163,7 +160,7 @@ export function handlePause(
|
|
|
163
160
|
|
|
164
161
|
export function handleStop(
|
|
165
162
|
ctx: HandlerContext, ws: WSContext,
|
|
166
|
-
workingDir: string, permission?: '
|
|
163
|
+
workingDir: string, permission?: 'view',
|
|
167
164
|
): void {
|
|
168
165
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
169
166
|
const executor = executorCache.get(workingDir);
|
|
@@ -172,7 +169,7 @@ export function handleStop(
|
|
|
172
169
|
|
|
173
170
|
export function handleResume(
|
|
174
171
|
ctx: HandlerContext, ws: WSContext,
|
|
175
|
-
workingDir: string, permission?: '
|
|
172
|
+
workingDir: string, permission?: 'view',
|
|
176
173
|
): void {
|
|
177
174
|
if (denyIfViewOnly(ctx, ws, permission)) return;
|
|
178
175
|
const executor = executorCache.get(workingDir);
|