@yu_robotics/remote-cli 1.6.53 → 1.6.68
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 +18 -8
- package/dist/client/MessageHandler.d.ts +7 -0
- package/dist/client/MessageHandler.d.ts.map +1 -1
- package/dist/client/MessageHandler.js +96 -3
- package/dist/client/MessageHandler.js.map +1 -1
- package/dist/client/TaskRecovery.d.ts +7 -3
- package/dist/client/TaskRecovery.d.ts.map +1 -1
- package/dist/client/TaskRecovery.js +80 -27
- package/dist/client/TaskRecovery.js.map +1 -1
- package/dist/client/WebSocketClient.d.ts.map +1 -1
- package/dist/client/WebSocketClient.js +2 -10
- package/dist/client/WebSocketClient.js.map +1 -1
- package/dist/commands/start.d.ts +3 -11
- package/dist/commands/start.d.ts.map +1 -1
- package/dist/commands/start.js +22 -99
- package/dist/commands/start.js.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.d.ts +40 -2
- package/dist/executor/ClaudePersistentExecutor.d.ts.map +1 -1
- package/dist/executor/ClaudePersistentExecutor.js +358 -9
- package/dist/executor/ClaudePersistentExecutor.js.map +1 -1
- package/dist/executor/CodexAppServerExecutor.d.ts +13 -0
- package/dist/executor/CodexAppServerExecutor.d.ts.map +1 -1
- package/dist/executor/CodexAppServerExecutor.js +208 -5
- package/dist/executor/CodexAppServerExecutor.js.map +1 -1
- package/dist/executor/CodexSandbox.d.ts +23 -0
- package/dist/executor/CodexSandbox.d.ts.map +1 -0
- package/dist/executor/CodexSandbox.js +211 -0
- package/dist/executor/CodexSandbox.js.map +1 -0
- package/dist/executor/IExecutor.d.ts +5 -1
- package/dist/executor/IExecutor.d.ts.map +1 -1
- package/dist/executor/claude/ClaudeFilePolicy.d.ts +7 -0
- package/dist/executor/claude/ClaudeFilePolicy.d.ts.map +1 -0
- package/dist/executor/claude/ClaudeFilePolicy.js +88 -0
- package/dist/executor/claude/ClaudeFilePolicy.js.map +1 -0
- package/dist/executor/claude/ClaudeSandbox.d.ts +32 -0
- package/dist/executor/claude/ClaudeSandbox.d.ts.map +1 -0
- package/dist/executor/claude/ClaudeSandbox.js +245 -0
- package/dist/executor/claude/ClaudeSandbox.js.map +1 -0
- package/dist/executor/claude/approvalMcpServer.d.ts +2 -0
- package/dist/executor/claude/approvalMcpServer.d.ts.map +1 -0
- package/dist/executor/claude/approvalMcpServer.js +160 -0
- package/dist/executor/claude/approvalMcpServer.js.map +1 -0
- package/dist/executor/claude/filePolicyHook.d.ts +2 -0
- package/dist/executor/claude/filePolicyHook.d.ts.map +1 -0
- package/dist/executor/claude/filePolicyHook.js +63 -0
- package/dist/executor/claude/filePolicyHook.js.map +1 -0
- package/dist/executor/index.d.ts.map +1 -1
- package/dist/executor/index.js +4 -3
- package/dist/executor/index.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/thread/ThreadManager.d.ts.map +1 -1
- package/dist/thread/ThreadManager.js +2 -0
- package/dist/thread/ThreadManager.js.map +1 -1
- package/dist/types/config.d.ts +32 -2
- package/dist/types/config.d.ts.map +1 -1
- package/dist/types/config.js.map +1 -1
- package/dist/types/index.d.ts +39 -0
- package/dist/types/index.d.ts.map +1 -1
- package/dist/update/AutomaticUpdater.d.ts +7 -1
- package/dist/update/AutomaticUpdater.d.ts.map +1 -1
- package/dist/update/AutomaticUpdater.js +25 -2
- package/dist/update/AutomaticUpdater.js.map +1 -1
- package/package.json +1 -1
|
@@ -6,9 +6,13 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.ClaudePersistentExecutor = void 0;
|
|
7
7
|
const child_process_1 = require("child_process");
|
|
8
8
|
const ClaudeCodeHooks_1 = require("../hooks/ClaudeCodeHooks");
|
|
9
|
+
const ClaudeSandbox_1 = require("./claude/ClaudeSandbox");
|
|
10
|
+
const ClaudeFilePolicy_1 = require("./claude/ClaudeFilePolicy");
|
|
9
11
|
const fs_1 = __importDefault(require("fs"));
|
|
10
12
|
const path_1 = __importDefault(require("path"));
|
|
11
13
|
const os_1 = __importDefault(require("os"));
|
|
14
|
+
const net_1 = __importDefault(require("net"));
|
|
15
|
+
const crypto_1 = require("crypto");
|
|
12
16
|
const events_1 = require("events");
|
|
13
17
|
/**
|
|
14
18
|
* Claude Persistent Executor
|
|
@@ -61,12 +65,32 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
61
65
|
inputRequestCallbacks = [];
|
|
62
66
|
inputDetectionTimer;
|
|
63
67
|
lastOutputTime = 0;
|
|
68
|
+
// Sandbox and approval state
|
|
69
|
+
sandbox;
|
|
70
|
+
claudeCommand;
|
|
71
|
+
approvalServer = null;
|
|
72
|
+
approvalSocketPath;
|
|
73
|
+
approvalGeneration = 0;
|
|
74
|
+
approvalTurnActive = false;
|
|
75
|
+
pendingApprovals = new Map();
|
|
76
|
+
currentApprovalRequestCallback;
|
|
77
|
+
currentApprovalResolvedCallback;
|
|
78
|
+
filePolicyToken;
|
|
79
|
+
filePolicyRoots = [];
|
|
80
|
+
filePolicyStarted = false;
|
|
81
|
+
onFilePolicyStarted;
|
|
82
|
+
static APPROVAL_TIMEOUT_MS = 5 * 60 * 1000;
|
|
83
|
+
static APPROVAL_SERVER_SCRIPT = path_1.default.join(__dirname, 'claude', 'approvalMcpServer.js');
|
|
84
|
+
static APPROVAL_TOOL_NAME = 'mcp__remote-cli-approval__approval_prompt';
|
|
85
|
+
static FILE_POLICY_SCRIPT = path_1.default.join(__dirname, 'claude', 'filePolicyHook.js');
|
|
64
86
|
// Activity tracking for timeout extension (optional, can be disabled)
|
|
65
87
|
activityTrackingEnabled = false;
|
|
66
|
-
constructor(directoryGuard, initialWorkingDirectory, threadId, model) {
|
|
88
|
+
constructor(directoryGuard, initialWorkingDirectory, threadId, model, sandbox, claudeCommand) {
|
|
67
89
|
super();
|
|
68
90
|
this.directoryGuard = directoryGuard;
|
|
69
91
|
this.model = model;
|
|
92
|
+
this.sandbox = new ClaudeSandbox_1.ClaudeSandbox(directoryGuard, sandbox, threadId);
|
|
93
|
+
this.claudeCommand = claudeCommand ?? 'claude';
|
|
70
94
|
// Use provided working directory or fall back to process.cwd()
|
|
71
95
|
// If a working directory is provided, validate it first
|
|
72
96
|
if (initialWorkingDirectory) {
|
|
@@ -213,14 +237,48 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
213
237
|
}
|
|
214
238
|
// Build arguments
|
|
215
239
|
// Note: --output-format=stream-json requires --verbose
|
|
240
|
+
const restricted = this.sandbox.isRestricted();
|
|
216
241
|
const args = [
|
|
217
242
|
'--input-format=stream-json',
|
|
218
243
|
'--output-format=stream-json',
|
|
219
244
|
'--include-partial-messages',
|
|
220
245
|
'--verbose',
|
|
221
|
-
'--dangerously-skip-permissions',
|
|
222
|
-
'--disallowedTools=AskUserQuestion'
|
|
223
246
|
];
|
|
247
|
+
if (restricted) {
|
|
248
|
+
// Sandboxed mode: OS-enforced Bash isolation; widening requests surface
|
|
249
|
+
// through the permission-prompt MCP tool as approval cards. Skipping
|
|
250
|
+
// permissions would silence that channel, so it is omitted on purpose.
|
|
251
|
+
this.sandbox.assertAvailable(this.currentWorkingDirectory);
|
|
252
|
+
// Override native acceptEdits/auto/bypass defaults for this process.
|
|
253
|
+
args.push('--permission-mode', 'default');
|
|
254
|
+
const socketPath = await this.ensureApprovalServer();
|
|
255
|
+
let fileHookCommand;
|
|
256
|
+
if (this.sandbox.getConfig()?.mode === 'workspace-write') {
|
|
257
|
+
this.filePolicyToken = (0, crypto_1.randomUUID)();
|
|
258
|
+
this.filePolicyStarted = false;
|
|
259
|
+
this.filePolicyRoots = [this.sandbox.normalize(this.currentWorkingDirectory), ...this.sandbox.extraWritableRoots()];
|
|
260
|
+
const quote = (value) => `'${value.replace(/'/g, "'\\''")}'`;
|
|
261
|
+
// A failed hook launch must block the tool, not fall through to native allow rules.
|
|
262
|
+
fileHookCommand = [process.execPath, ClaudePersistentExecutor.FILE_POLICY_SCRIPT, socketPath, this.filePolicyToken]
|
|
263
|
+
.map(quote).join(' ') + ' || exit 2';
|
|
264
|
+
}
|
|
265
|
+
const settings = this.sandbox.spawnSettings(this.currentWorkingDirectory, fileHookCommand);
|
|
266
|
+
if (settings)
|
|
267
|
+
args.push('--settings', JSON.stringify(settings));
|
|
268
|
+
args.push('--mcp-config', JSON.stringify({
|
|
269
|
+
mcpServers: {
|
|
270
|
+
'remote-cli-approval': {
|
|
271
|
+
command: process.execPath,
|
|
272
|
+
args: [ClaudePersistentExecutor.APPROVAL_SERVER_SCRIPT, socketPath],
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
}));
|
|
276
|
+
args.push('--permission-prompt-tool', ClaudePersistentExecutor.APPROVAL_TOOL_NAME);
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
args.push('--dangerously-skip-permissions');
|
|
280
|
+
}
|
|
281
|
+
args.push('--disallowedTools=AskUserQuestion');
|
|
224
282
|
if (this.model) {
|
|
225
283
|
args.push('--model', this.model);
|
|
226
284
|
}
|
|
@@ -231,10 +289,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
231
289
|
else {
|
|
232
290
|
console.log('[ClaudePersistent] Starting new session');
|
|
233
291
|
}
|
|
234
|
-
console.log(`[ClaudePersistent] Starting:
|
|
292
|
+
console.log(`[ClaudePersistent] Starting: ${this.claudeCommand} ${args.join(' ')}`);
|
|
235
293
|
console.log(`[ClaudePersistent] Working directory: ${this.currentWorkingDirectory}`);
|
|
236
294
|
// Spawn the process
|
|
237
|
-
const child = (0, child_process_1.spawn)(
|
|
295
|
+
const child = (0, child_process_1.spawn)(this.claudeCommand, args, {
|
|
238
296
|
cwd: this.currentWorkingDirectory,
|
|
239
297
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
240
298
|
env: {
|
|
@@ -299,6 +357,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
299
357
|
// Use 'close' event (fires after all I/O streams are closed, i.e. after all stdout
|
|
300
358
|
// data events have been processed) instead of 'exit' for command completion logic.
|
|
301
359
|
child.on('close', (code, signal) => {
|
|
360
|
+
this.clearApprovals('Claude process exited');
|
|
302
361
|
this.claudeProcess = null;
|
|
303
362
|
// Check if this was an intentional stop (abort/reset)
|
|
304
363
|
const wasIntentionalStop = this.isStopping;
|
|
@@ -363,9 +422,21 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
363
422
|
}
|
|
364
423
|
throw err;
|
|
365
424
|
}
|
|
425
|
+
if (this.filePolicyToken) {
|
|
426
|
+
try {
|
|
427
|
+
// SessionStart runs before stdin is consumed. Do not send a user task
|
|
428
|
+
// unless the native hook is active; disabled/managed-only hooks fail closed.
|
|
429
|
+
await this.waitForFilePolicy(child);
|
|
430
|
+
}
|
|
431
|
+
catch (error) {
|
|
432
|
+
await this.stopProcess();
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
}
|
|
366
436
|
// Long-running error handler for errors that occur after startup
|
|
367
437
|
child.on('error', (error) => {
|
|
368
438
|
console.error('[ClaudePersistent] Process error:', error);
|
|
439
|
+
this.clearApprovals('Claude process failed');
|
|
369
440
|
this.claudeProcess = null;
|
|
370
441
|
this.isStarting = false;
|
|
371
442
|
if (this.currentCommandReject) {
|
|
@@ -397,6 +468,10 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
397
468
|
* Stop the persistent Claude process
|
|
398
469
|
*/
|
|
399
470
|
async stopProcess() {
|
|
471
|
+
this.filePolicyToken = undefined;
|
|
472
|
+
this.filePolicyRoots = [];
|
|
473
|
+
this.filePolicyStarted = false;
|
|
474
|
+
this.clearApprovals('Claude process stopped');
|
|
400
475
|
if (!this.claudeProcess) {
|
|
401
476
|
return;
|
|
402
477
|
}
|
|
@@ -425,6 +500,257 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
425
500
|
this.claudeProcess = null;
|
|
426
501
|
console.log('[ClaudePersistent] Process stopped');
|
|
427
502
|
}
|
|
503
|
+
// ── Sandbox approval channel ─────────────────────────────────────────────
|
|
504
|
+
waitForFilePolicy(child) {
|
|
505
|
+
if (this.filePolicyStarted && this.claudeProcess === child)
|
|
506
|
+
return Promise.resolve();
|
|
507
|
+
return new Promise((resolve, reject) => {
|
|
508
|
+
const finish = (error) => {
|
|
509
|
+
clearTimeout(timer);
|
|
510
|
+
this.onFilePolicyStarted = undefined;
|
|
511
|
+
child.removeListener('close', onClose);
|
|
512
|
+
child.removeListener('error', onError);
|
|
513
|
+
error ? reject(error) : resolve();
|
|
514
|
+
};
|
|
515
|
+
const onClose = () => finish(new Error('Claude exited before sandbox file checks started.'));
|
|
516
|
+
const onError = (error) => finish(error);
|
|
517
|
+
const timer = setTimeout(() => finish(new Error('Claude sandbox file checks did not start. Check native hook restrictions (disableAllHooks or allowManagedHooksOnly).')), 10000);
|
|
518
|
+
this.onFilePolicyStarted = () => finish();
|
|
519
|
+
child.once('close', onClose);
|
|
520
|
+
child.once('error', onError);
|
|
521
|
+
if (this.claudeProcess !== child)
|
|
522
|
+
onClose();
|
|
523
|
+
});
|
|
524
|
+
}
|
|
525
|
+
async ensureApprovalServer() {
|
|
526
|
+
if (this.approvalServer && this.approvalSocketPath)
|
|
527
|
+
return this.approvalSocketPath;
|
|
528
|
+
const directory = path_1.default.join(os_1.default.homedir(), '.remote-cli', 'claude-approval');
|
|
529
|
+
fs_1.default.mkdirSync(directory, { recursive: true, mode: 0o700 });
|
|
530
|
+
const socketPath = path_1.default.join(directory, `${encodeURIComponent(this.threadId || (0, crypto_1.randomUUID)())}.sock`);
|
|
531
|
+
if (fs_1.default.existsSync(socketPath))
|
|
532
|
+
fs_1.default.unlinkSync(socketPath); // stale socket from a previous run
|
|
533
|
+
this.approvalServer = net_1.default.createServer((socket) => {
|
|
534
|
+
const generation = this.approvalGeneration;
|
|
535
|
+
let buffer = '';
|
|
536
|
+
socket.on('error', () => { });
|
|
537
|
+
socket.on('data', (chunk) => {
|
|
538
|
+
buffer += chunk;
|
|
539
|
+
let index;
|
|
540
|
+
while ((index = buffer.indexOf('\n')) >= 0) {
|
|
541
|
+
const line = buffer.slice(0, index);
|
|
542
|
+
buffer = buffer.slice(index + 1);
|
|
543
|
+
if (!line.trim())
|
|
544
|
+
continue;
|
|
545
|
+
try {
|
|
546
|
+
this.handleApprovalSocketRequest(JSON.parse(line), socket, generation);
|
|
547
|
+
}
|
|
548
|
+
catch (error) {
|
|
549
|
+
console.error('[ClaudePersistent] Failed to handle approval request:', error);
|
|
550
|
+
socket.end();
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
});
|
|
555
|
+
await new Promise((resolve, reject) => {
|
|
556
|
+
this.approvalServer.once('error', reject);
|
|
557
|
+
this.approvalServer.listen(socketPath, () => resolve());
|
|
558
|
+
});
|
|
559
|
+
fs_1.default.chmodSync(socketPath, 0o600);
|
|
560
|
+
this.approvalSocketPath = socketPath;
|
|
561
|
+
return socketPath;
|
|
562
|
+
}
|
|
563
|
+
handleApprovalSocketRequest(payload, socket, generation) {
|
|
564
|
+
if (payload?.type === 'file_policy') {
|
|
565
|
+
let answer = { decision: 'deny', reason: 'File policy request is no longer active.' };
|
|
566
|
+
if (this.filePolicyToken && payload.token === this.filePolicyToken && this.claudeProcess
|
|
567
|
+
&& !this.isDestroyed && !this.isStopping) {
|
|
568
|
+
if (payload.startup === true) {
|
|
569
|
+
this.filePolicyStarted = true;
|
|
570
|
+
this.onFilePolicyStarted?.();
|
|
571
|
+
answer = { decision: 'allow', reason: 'File policy ready.' };
|
|
572
|
+
}
|
|
573
|
+
else if (this.isProcessing && this.approvalTurnActive && generation === this.approvalGeneration) {
|
|
574
|
+
answer = (0, ClaudeFilePolicy_1.evaluateFileWrite)(payload.tool_name, payload.input ?? {}, this.filePolicyRoots);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
socket.end(JSON.stringify(answer) + '\n');
|
|
578
|
+
return;
|
|
579
|
+
}
|
|
580
|
+
const requestId = typeof payload?.id === 'string' ? payload.id : (0, crypto_1.randomUUID)();
|
|
581
|
+
if (this.isDestroyed || this.isStopping || !this.isProcessing || !this.approvalTurnActive
|
|
582
|
+
|| (!this.currentCommandResolve && !this.currentCommandReject)
|
|
583
|
+
|| generation !== this.approvalGeneration || this.pendingApprovals.has(requestId)) {
|
|
584
|
+
socket.end(JSON.stringify({ id: requestId, behavior: 'deny', message: 'Approval request is no longer active.' }) + '\n');
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
const toolName = typeof payload?.tool_name === 'string' ? payload.tool_name : 'unknown';
|
|
588
|
+
const input = (payload?.input && typeof payload.input === 'object') ? payload.input : {};
|
|
589
|
+
const kind = toolName === 'Bash' ? 'command'
|
|
590
|
+
: (toolName === 'Write' || toolName === 'Edit' || toolName === 'NotebookEdit') ? 'file'
|
|
591
|
+
: 'permissions';
|
|
592
|
+
const description = toolName === 'Bash'
|
|
593
|
+
? String(input.command ?? '').slice(0, 500)
|
|
594
|
+
: `${toolName}: ${String(input.file_path ?? input.notebook_path ?? input.path ?? JSON.stringify(input)).slice(0, 500)}`;
|
|
595
|
+
// Remember support: file writes can map to a persistent directory grant;
|
|
596
|
+
// arbitrary commands and permission bundles cannot.
|
|
597
|
+
let canRemember = false;
|
|
598
|
+
let grantRoots;
|
|
599
|
+
if (kind === 'file') {
|
|
600
|
+
const target = input.file_path ?? input.notebook_path ?? input.path;
|
|
601
|
+
if (typeof target === 'string' && target) {
|
|
602
|
+
try {
|
|
603
|
+
grantRoots = [this.sandbox.normalize(path_1.default.dirname(target), this.currentWorkingDirectory)];
|
|
604
|
+
canRemember = true;
|
|
605
|
+
}
|
|
606
|
+
catch { /* A path that cannot be resolved cannot be remembered. */ }
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
const timer = setTimeout(() => {
|
|
610
|
+
this.answerApproval(requestId, { behavior: 'deny', message: 'Approval request expired' }, 'expired');
|
|
611
|
+
}, ClaudePersistentExecutor.APPROVAL_TIMEOUT_MS);
|
|
612
|
+
timer.unref?.();
|
|
613
|
+
socket.on('close', () => {
|
|
614
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
615
|
+
if (pending?.socket === socket)
|
|
616
|
+
this.answerApproval(requestId, { behavior: 'deny', message: 'Approval connection closed' }, 'expired');
|
|
617
|
+
});
|
|
618
|
+
this.pendingApprovals.set(requestId, { socket, timer, input, description, generation,
|
|
619
|
+
grantRoots, fileTool: kind === 'file' ? toolName : undefined,
|
|
620
|
+
resolved: this.currentApprovalResolvedCallback });
|
|
621
|
+
let cardShown = false;
|
|
622
|
+
try {
|
|
623
|
+
cardShown = this.currentApprovalRequestCallback?.({ requestId, kind, description, canRemember, writableRoots: grantRoots }) === true;
|
|
624
|
+
}
|
|
625
|
+
catch { /* A rendering failure can fall back to the text prompt. */ }
|
|
626
|
+
if (!cardShown && this.pendingApprovals.has(requestId)) {
|
|
627
|
+
if (this.currentStreamCallback) {
|
|
628
|
+
this.currentStreamCallback(`\nApproval required (${requestId}): ${description}\nReply yes or no. For multiple requests, reply yes <request ID> or no <request ID>.\n`);
|
|
629
|
+
}
|
|
630
|
+
else {
|
|
631
|
+
this.answerApproval(requestId, { behavior: 'deny', message: 'No interactive approval channel is available.' }, 'denied');
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
answerApproval(requestId, answer, status) {
|
|
636
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
637
|
+
if (!pending)
|
|
638
|
+
return false;
|
|
639
|
+
this.pendingApprovals.delete(requestId);
|
|
640
|
+
clearTimeout(pending.timer);
|
|
641
|
+
let delivered = !pending.socket.destroyed && pending.socket.writable;
|
|
642
|
+
if (delivered) {
|
|
643
|
+
try {
|
|
644
|
+
pending.socket.end(JSON.stringify({ id: requestId, ...answer }) + '\n');
|
|
645
|
+
}
|
|
646
|
+
catch {
|
|
647
|
+
delivered = false;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
try {
|
|
651
|
+
pending.resolved?.(requestId, delivered ? status : 'expired');
|
|
652
|
+
}
|
|
653
|
+
catch { /* Card cleanup failures must not reactivate an answered request. */ }
|
|
654
|
+
return delivered;
|
|
655
|
+
}
|
|
656
|
+
/** MessageHandler entry point for approval card actions. */
|
|
657
|
+
respondToApproval(requestId, action) {
|
|
658
|
+
const pending = this.pendingApprovals.get(requestId);
|
|
659
|
+
if (!pending || !['approve', 'deny', 'remember'].includes(action))
|
|
660
|
+
return false;
|
|
661
|
+
if (action === 'remember' && !pending.grantRoots?.length)
|
|
662
|
+
return false;
|
|
663
|
+
if (pending.generation !== this.approvalGeneration || !this.approvalTurnActive
|
|
664
|
+
|| !this.isProcessing || this.isStopping || this.isDestroyed) {
|
|
665
|
+
this.answerApproval(requestId, { behavior: 'deny', message: 'Approval request expired' }, 'expired');
|
|
666
|
+
return false;
|
|
667
|
+
}
|
|
668
|
+
if (action === 'deny') {
|
|
669
|
+
return this.answerApproval(requestId, { behavior: 'deny', message: 'Denied by user' }, 'denied');
|
|
670
|
+
}
|
|
671
|
+
if (action === 'remember') {
|
|
672
|
+
const delivered = this.answerApproval(requestId, {
|
|
673
|
+
behavior: 'allow', updatedInput: pending.input,
|
|
674
|
+
updatedPermissions: this.buildRememberRules(pending),
|
|
675
|
+
}, 'remembered');
|
|
676
|
+
if (delivered) {
|
|
677
|
+
// Also persist the directory for future Bash sandbox scope; it applies
|
|
678
|
+
// at the next process spawn because settings are spawn-time arguments.
|
|
679
|
+
const config = this.sandbox.getConfig() ?? { mode: 'workspace-write' };
|
|
680
|
+
const roots = pending.grantRoots;
|
|
681
|
+
this.sandbox.configure({ ...config, writableRoots: [...new Set([...(config.writableRoots ?? []), ...roots])] });
|
|
682
|
+
// Let the file policy hook honor the grant immediately in this session.
|
|
683
|
+
this.filePolicyRoots = [...new Set([...this.filePolicyRoots, ...roots])];
|
|
684
|
+
}
|
|
685
|
+
return delivered;
|
|
686
|
+
}
|
|
687
|
+
return this.answerApproval(requestId, { behavior: 'allow', updatedInput: pending.input }, 'approved');
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Build Claude Code permission rules that let future file edits under the
|
|
691
|
+
* granted directories skip the prompt, written to the project's local
|
|
692
|
+
* settings via the permission-prompt tool's updatedPermissions channel.
|
|
693
|
+
*/
|
|
694
|
+
buildRememberRules(pending) {
|
|
695
|
+
const toolNames = new Set(['Write', 'Edit']);
|
|
696
|
+
if (pending.fileTool === 'NotebookEdit')
|
|
697
|
+
toolNames.add('NotebookEdit');
|
|
698
|
+
const rules = [...toolNames].flatMap(toolName => (pending.grantRoots ?? []).map(root => ({ toolName, ruleContent: `${root}/**` })));
|
|
699
|
+
return [{ type: 'addRules', rules, behavior: 'allow', destination: 'localSettings' }];
|
|
700
|
+
}
|
|
701
|
+
denyAllPendingApprovals(message) {
|
|
702
|
+
for (const requestId of Array.from(this.pendingApprovals.keys())) {
|
|
703
|
+
this.answerApproval(requestId, { behavior: 'deny', message }, 'expired');
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
clearApprovals(message) {
|
|
707
|
+
this.approvalGeneration++;
|
|
708
|
+
this.approvalTurnActive = false;
|
|
709
|
+
this.currentApprovalRequestCallback = undefined;
|
|
710
|
+
this.currentApprovalResolvedCallback = undefined;
|
|
711
|
+
this.denyAllPendingApprovals(message);
|
|
712
|
+
}
|
|
713
|
+
getSandboxStatus() {
|
|
714
|
+
return this.sandbox.describe(this.currentWorkingDirectory);
|
|
715
|
+
}
|
|
716
|
+
async configureSandbox(command) {
|
|
717
|
+
if (this.isProcessing || this.commandQueue.length > 0) {
|
|
718
|
+
return { success: false, error: 'Wait for the running task before changing sandbox settings.' };
|
|
719
|
+
}
|
|
720
|
+
try {
|
|
721
|
+
const match = /^(\S+)(?:\s+([\s\S]*))?$/.exec(command.trim());
|
|
722
|
+
const action = match?.[1] ?? '';
|
|
723
|
+
const value = match?.[2] ?? '';
|
|
724
|
+
const config = this.sandbox.getConfig() ?? { mode: 'workspace-write' };
|
|
725
|
+
let next = { ...config };
|
|
726
|
+
if (action === 'default' && !value)
|
|
727
|
+
next = undefined;
|
|
728
|
+
else if (['on', 'off', 'read-only'].includes(action) && !value) {
|
|
729
|
+
next.mode = action === 'on' ? 'workspace-write' : action === 'off' ? 'danger-full-access' : 'read-only';
|
|
730
|
+
}
|
|
731
|
+
else if (action === 'network' && ['on', 'off'].includes(value))
|
|
732
|
+
next.networkAccess = value === 'on';
|
|
733
|
+
else if (['allow', 'remove'].includes(action) && value) {
|
|
734
|
+
const directory = this.sandbox.normalize(value, this.currentWorkingDirectory);
|
|
735
|
+
next.writableRoots = action === 'allow'
|
|
736
|
+
? [...new Set([...(config.writableRoots ?? []), directory])]
|
|
737
|
+
: (config.writableRoots ?? []).filter(root => root !== directory);
|
|
738
|
+
if (action === 'remove' && next.writableRoots.length === (config.writableRoots ?? []).length) {
|
|
739
|
+
return { success: false, error: 'That directory is not an extra grant.' };
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
else
|
|
743
|
+
return { success: false, error: 'Usage: /sandbox on|off|read-only|default; /sandbox network on|off; /sandbox allow|remove <directory>' };
|
|
744
|
+
this.sandbox.configure(next);
|
|
745
|
+
// Sandbox settings are spawn-time arguments; restart to apply. The saved
|
|
746
|
+
// session id keeps the conversation intact across the respawn.
|
|
747
|
+
await this.stopProcess();
|
|
748
|
+
return { success: true, output: this.getSandboxStatus() };
|
|
749
|
+
}
|
|
750
|
+
catch (error) {
|
|
751
|
+
return { success: false, error: error instanceof Error ? error.message : String(error) };
|
|
752
|
+
}
|
|
753
|
+
}
|
|
428
754
|
/**
|
|
429
755
|
* Reset the command timeout timer when activity is detected
|
|
430
756
|
* This prevents timeout during long-running tasks with continuous output
|
|
@@ -866,6 +1192,16 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
866
1192
|
* This is called by MessageHandler when user sends a message while waiting for input
|
|
867
1193
|
*/
|
|
868
1194
|
sendInput(input) {
|
|
1195
|
+
if (this.pendingApprovals.size > 0) {
|
|
1196
|
+
const match = /^(yes|y|approve|accept|no|n|deny|decline|cancel)(?:\s+(\S+))?$/i.exec(input.trim());
|
|
1197
|
+
const requestId = match?.[2] ?? (this.pendingApprovals.size === 1 ? this.pendingApprovals.keys().next().value : undefined);
|
|
1198
|
+
if (!match || !requestId || !this.pendingApprovals.has(requestId)) {
|
|
1199
|
+
this.currentStreamCallback?.(`\nReply yes or no for a single approval, or specify yes <request ID> / no <request ID>:\n${Array.from(this.pendingApprovals, ([id, pending]) => `${id}: ${pending.description}`).join('\n')}\n`);
|
|
1200
|
+
return false;
|
|
1201
|
+
}
|
|
1202
|
+
const action = ['yes', 'y', 'approve', 'accept'].includes(match[1].toLowerCase()) ? 'approve' : 'deny';
|
|
1203
|
+
return this.respondToApproval(requestId, action);
|
|
1204
|
+
}
|
|
869
1205
|
if (!this.claudeProcess || !this.isWaitingForInput) {
|
|
870
1206
|
console.log('[ClaudePersistent] Cannot send input - process not running or not waiting for input');
|
|
871
1207
|
return false;
|
|
@@ -892,7 +1228,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
892
1228
|
* Check if currently waiting for user input
|
|
893
1229
|
*/
|
|
894
1230
|
isWaitingInput() {
|
|
895
|
-
return this.isWaitingForInput;
|
|
1231
|
+
return this.pendingApprovals.size > 0 || this.isWaitingForInput;
|
|
896
1232
|
}
|
|
897
1233
|
/**
|
|
898
1234
|
* Complete the current command
|
|
@@ -958,6 +1294,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
958
1294
|
* Reset current command state
|
|
959
1295
|
*/
|
|
960
1296
|
resetCurrentCommand() {
|
|
1297
|
+
this.clearApprovals('Task is no longer active');
|
|
961
1298
|
this.currentOutputBuffer = [];
|
|
962
1299
|
this.textStreams.clear();
|
|
963
1300
|
this.currentStreamCallback = undefined;
|
|
@@ -984,7 +1321,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
984
1321
|
* Process the command queue
|
|
985
1322
|
*/
|
|
986
1323
|
async processQueue() {
|
|
987
|
-
if (this.isProcessing || this.commandQueue.length === 0) {
|
|
1324
|
+
if (this.isProcessing || this.isStarting || this.commandQueue.length === 0) {
|
|
988
1325
|
return;
|
|
989
1326
|
}
|
|
990
1327
|
// Ensure process is running
|
|
@@ -1022,6 +1359,9 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1022
1359
|
this.currentToolResultCallback = command.options.onToolResult;
|
|
1023
1360
|
this.currentRedactedThinkingCallback = command.options.onRedactedThinking;
|
|
1024
1361
|
this.currentPlanModeCallback = command.options.onPlanMode;
|
|
1362
|
+
this.currentApprovalRequestCallback = command.options.onApprovalRequest;
|
|
1363
|
+
this.currentApprovalResolvedCallback = command.options.onApprovalResolved;
|
|
1364
|
+
this.approvalTurnActive = true;
|
|
1025
1365
|
this.currentCommandResolve = command.resolve;
|
|
1026
1366
|
this.currentCommandReject = command.reject;
|
|
1027
1367
|
// Reset output buffer: Claude Code 2.x can emit autonomous turns while
|
|
@@ -1122,7 +1462,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1122
1462
|
* Stops the current process and rejects the pending command
|
|
1123
1463
|
*/
|
|
1124
1464
|
async abort() {
|
|
1125
|
-
if (!this.isProcessing && !this.
|
|
1465
|
+
if (!this.isProcessing && !this.isWaitingInput()) {
|
|
1126
1466
|
console.log('[ClaudePersistent] No command is currently executing');
|
|
1127
1467
|
return false;
|
|
1128
1468
|
}
|
|
@@ -1136,6 +1476,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1136
1476
|
}
|
|
1137
1477
|
}
|
|
1138
1478
|
console.log('[ClaudePersistent] Aborting current command...');
|
|
1479
|
+
this.denyAllPendingApprovals('Task aborted by user');
|
|
1139
1480
|
// Emit task aborted hook before stopping
|
|
1140
1481
|
if (this.currentTaskId) {
|
|
1141
1482
|
ClaudeCodeHooks_1.claudeCodeHooks.notifyTaskAborted({
|
|
@@ -1268,7 +1609,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1268
1609
|
await this.stopProcess();
|
|
1269
1610
|
// Step 2: Run compact as a one-shot process outside the full context
|
|
1270
1611
|
const compactResult = await new Promise((resolve) => {
|
|
1271
|
-
const child = (0, child_process_1.spawn)(
|
|
1612
|
+
const child = (0, child_process_1.spawn)(this.claudeCommand, ['--resume', sessionIdBeforeCompact, '--print', '/compact'], {
|
|
1272
1613
|
cwd: this.currentWorkingDirectory,
|
|
1273
1614
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1274
1615
|
env: { ...process.env, FORCE_COLOR: '0', CLAUDECODE: '', CLAUDE_CODE: '' },
|
|
@@ -1317,6 +1658,13 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1317
1658
|
command.reject(new Error('Executor has been destroyed'));
|
|
1318
1659
|
}
|
|
1319
1660
|
this.commandQueue = [];
|
|
1661
|
+
this.clearApprovals('Executor destroyed');
|
|
1662
|
+
if (this.approvalServer) {
|
|
1663
|
+
const server = this.approvalServer;
|
|
1664
|
+
this.approvalServer = null;
|
|
1665
|
+
this.approvalSocketPath = undefined;
|
|
1666
|
+
await new Promise(resolve => server.close(() => resolve()));
|
|
1667
|
+
}
|
|
1320
1668
|
if (this.currentCommandReject) {
|
|
1321
1669
|
this.currentCommandReject(new Error('Executor has been destroyed'));
|
|
1322
1670
|
this.resetCurrentCommand();
|
|
@@ -1338,6 +1686,7 @@ class ClaudePersistentExecutor extends events_1.EventEmitter {
|
|
|
1338
1686
|
catch {
|
|
1339
1687
|
// File may not exist — not an error
|
|
1340
1688
|
}
|
|
1689
|
+
this.sandbox.deleteData();
|
|
1341
1690
|
}
|
|
1342
1691
|
/**
|
|
1343
1692
|
* Check if process is running
|