@tencent-ai/codebuddy-code 2.109.2 → 2.110.0-dev.3827fe9.202606251855
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/CHANGELOG.md +47 -0
- package/dist/codebuddy-headless.js +368 -199
- package/dist/codebuddy.js +393 -224
- package/dist/web-ui/assets/{index-DZkG4N19.js → index-CTllOYCu.js} +5 -5
- package/dist/web-ui/index.html +1 -1
- package/dist/web-ui/sw.js +1 -1
- package/package.json +3 -2
- package/product.cloudhosted.json +2 -2
- package/product.internal.json +2 -2
- package/product.ioa.json +2 -2
- package/product.json +15 -3
- package/product.selfhosted.json +2 -2
- package/vendor/shim/genie-safe-delete.cjs +79 -7
- package/vendor/shim/safe-bin/safe-delete-common.sh +65 -19
- package/vendor/shim/safe-delete-bulk-guard.cjs +465 -0
- package/vendor/shim/sitecustomize.py +58 -6
- package/lib/node/index.js +0 -30
|
@@ -0,0 +1,465 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const fs = require('fs');
|
|
6
|
+
const path = require('path');
|
|
7
|
+
|
|
8
|
+
const CONFIRM_MARKER = '[safe-delete][SAFE_DELETE_BULK_CONFIRM_REQUIRED]';
|
|
9
|
+
const REJECTED_MARKER = '[safe-delete][SAFE_DELETE_BULK_REJECTED]';
|
|
10
|
+
const ERROR_MARKER = '[safe-delete][SAFE_DELETE_BULK_GUARD_ERROR]';
|
|
11
|
+
const DEFAULT_THRESHOLD = 20;
|
|
12
|
+
const LOCK_TIMEOUT_MS = 2000;
|
|
13
|
+
const STALE_LOCK_MS = 5 * 60 * 1000;
|
|
14
|
+
const TURN_STATE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
15
|
+
const TARGET_SAMPLE_LIMIT = 5;
|
|
16
|
+
const DISPLAY_COUNT_LIMIT = 9999;
|
|
17
|
+
|
|
18
|
+
class GuardError extends Error {}
|
|
19
|
+
|
|
20
|
+
function fail(message) {
|
|
21
|
+
throw new GuardError(message);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseArgs(argv) {
|
|
25
|
+
const command = argv[2];
|
|
26
|
+
const targets = [];
|
|
27
|
+
let scope;
|
|
28
|
+
for (let i = 3; i < argv.length; i++) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (arg === '--target') {
|
|
31
|
+
const target = argv[++i];
|
|
32
|
+
if (!target) fail('missing --target value');
|
|
33
|
+
targets.push(target);
|
|
34
|
+
} else if (arg === '--scope') {
|
|
35
|
+
scope = argv[++i];
|
|
36
|
+
if (!scope) fail('missing --scope value');
|
|
37
|
+
} else {
|
|
38
|
+
fail(`unknown argument: ${arg}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { command, targets, scope };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function requireEnv(name) {
|
|
45
|
+
const value = process.env[name];
|
|
46
|
+
if (!value) fail(`${name} is not set`);
|
|
47
|
+
return value;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function normalizeThreshold(value) {
|
|
51
|
+
const raw = value === undefined || value === null || value === '' ? DEFAULT_THRESHOLD : value;
|
|
52
|
+
const threshold = Number.parseInt(String(raw), 10);
|
|
53
|
+
if (!Number.isFinite(threshold) || threshold <= 0) fail(`invalid threshold: ${value}`);
|
|
54
|
+
return threshold;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function createSafeDeleteBulkGuardContext(input) {
|
|
58
|
+
if (!input || !input.stateRoot || !input.sessionId || !input.toolCallId) {
|
|
59
|
+
fail('missing bulk guard context');
|
|
60
|
+
}
|
|
61
|
+
const threshold = normalizeThreshold(input.threshold);
|
|
62
|
+
const sessionId = input.sessionId;
|
|
63
|
+
const conversationRequestId = input.conversationRequestId;
|
|
64
|
+
const sessionHash = crypto.createHash('sha256').update(sessionId).digest('hex');
|
|
65
|
+
const sessionDir = path.join(input.stateRoot, sessionHash);
|
|
66
|
+
return {
|
|
67
|
+
stateRoot: input.stateRoot,
|
|
68
|
+
sessionId,
|
|
69
|
+
toolCallId: input.toolCallId,
|
|
70
|
+
requestId: conversationRequestId || input.toolCallId,
|
|
71
|
+
threshold,
|
|
72
|
+
conversationRequestId,
|
|
73
|
+
sessionHash,
|
|
74
|
+
sessionDir,
|
|
75
|
+
statePath: path.join(sessionDir, 'state.json'),
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function getContext() {
|
|
80
|
+
return createSafeDeleteBulkGuardContext({
|
|
81
|
+
stateRoot: requireEnv('CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR'),
|
|
82
|
+
sessionId: requireEnv('CODEBUDDY_SESSION_ID'),
|
|
83
|
+
toolCallId: requireEnv('CODEBUDDY_TOOL_CALL_ID'),
|
|
84
|
+
conversationRequestId: process.env.CODEBUDDY_CONVERSATION_REQUEST_ID,
|
|
85
|
+
threshold: process.env.CODEBUDDY_SAFE_DELETE_BULK_THRESHOLD || DEFAULT_THRESHOLD,
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function defaultState() {
|
|
90
|
+
return { requests: {}, toolApprovals: {}, requestRejections: {} };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function normalizeState(parsed) {
|
|
94
|
+
if (!parsed || typeof parsed !== 'object') return defaultState();
|
|
95
|
+
return {
|
|
96
|
+
requests: parsed.requests && typeof parsed.requests === 'object' ? parsed.requests : {},
|
|
97
|
+
toolApprovals: parsed.toolApprovals && typeof parsed.toolApprovals === 'object' ? parsed.toolApprovals : {},
|
|
98
|
+
requestRejections: parsed.requestRejections && typeof parsed.requestRejections === 'object' ? parsed.requestRejections : {},
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function readState(statePath) {
|
|
103
|
+
try {
|
|
104
|
+
return normalizeState(JSON.parse(fs.readFileSync(statePath, 'utf-8')));
|
|
105
|
+
} catch (error) {
|
|
106
|
+
if (error && error.code === 'ENOENT') return defaultState();
|
|
107
|
+
fail(`failed to read state: ${error && error.message ? error.message : String(error)}`);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function writeState(statePath, state) {
|
|
112
|
+
const tmpPath = `${statePath}.${process.pid}.tmp`;
|
|
113
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(state)}\n`, 'utf-8');
|
|
114
|
+
fs.renameSync(tmpPath, statePath);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function normalizeRequest(request, now) {
|
|
118
|
+
if (!request || typeof request !== 'object') return { count: 0, updatedAt: now };
|
|
119
|
+
return {
|
|
120
|
+
count: Number.isFinite(request.count) ? request.count : 0,
|
|
121
|
+
updatedAt: Number.isFinite(request.updatedAt) ? request.updatedAt : now,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function normalizeToolApproval(approval, now) {
|
|
126
|
+
if (!approval || typeof approval !== 'object') return { approved: false, updatedAt: now };
|
|
127
|
+
return {
|
|
128
|
+
approved: approval.approved === true,
|
|
129
|
+
updatedAt: Number.isFinite(approval.updatedAt) ? approval.updatedAt : now,
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function normalizeRequestRejection(rejection, now) {
|
|
134
|
+
if (!rejection || typeof rejection !== 'object') {
|
|
135
|
+
return { rejected: false, updatedAt: now };
|
|
136
|
+
}
|
|
137
|
+
const targets = Array.isArray(rejection.targets)
|
|
138
|
+
? rejection.targets.filter(target => typeof target === 'string').slice(0, TARGET_SAMPLE_LIMIT)
|
|
139
|
+
: [];
|
|
140
|
+
return {
|
|
141
|
+
rejected: rejection.rejected === true,
|
|
142
|
+
updatedAt: Number.isFinite(rejection.updatedAt) ? rejection.updatedAt : now,
|
|
143
|
+
count: Number.isFinite(rejection.count) ? rejection.count : 0,
|
|
144
|
+
threshold: Number.isFinite(rejection.threshold) ? rejection.threshold : DEFAULT_THRESHOLD,
|
|
145
|
+
targets,
|
|
146
|
+
targetCount: Number.isFinite(rejection.targetCount) ? rejection.targetCount : targets.length,
|
|
147
|
+
};
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function pruneExpired(state, now) {
|
|
151
|
+
let changed = false;
|
|
152
|
+
const nextRequests = {};
|
|
153
|
+
for (const [requestId, rawRequest] of Object.entries(state.requests)) {
|
|
154
|
+
const request = normalizeRequest(rawRequest, now);
|
|
155
|
+
if (now - request.updatedAt > TURN_STATE_TTL_MS) {
|
|
156
|
+
changed = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
nextRequests[requestId] = request;
|
|
160
|
+
if (
|
|
161
|
+
!rawRequest
|
|
162
|
+
|| typeof rawRequest !== 'object'
|
|
163
|
+
|| rawRequest.count !== request.count
|
|
164
|
+
|| rawRequest.updatedAt !== request.updatedAt
|
|
165
|
+
) {
|
|
166
|
+
changed = true;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
if (changed) state.requests = nextRequests;
|
|
170
|
+
|
|
171
|
+
const nextToolApprovals = {};
|
|
172
|
+
for (const [toolCallId, rawApproval] of Object.entries(state.toolApprovals)) {
|
|
173
|
+
const approval = normalizeToolApproval(rawApproval, now);
|
|
174
|
+
if (now - approval.updatedAt > TURN_STATE_TTL_MS) {
|
|
175
|
+
changed = true;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
nextToolApprovals[toolCallId] = approval;
|
|
179
|
+
if (
|
|
180
|
+
!rawApproval
|
|
181
|
+
|| typeof rawApproval !== 'object'
|
|
182
|
+
|| rawApproval.approved !== approval.approved
|
|
183
|
+
|| rawApproval.updatedAt !== approval.updatedAt
|
|
184
|
+
) {
|
|
185
|
+
changed = true;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
if (changed) state.toolApprovals = nextToolApprovals;
|
|
189
|
+
|
|
190
|
+
const nextRequestRejections = {};
|
|
191
|
+
for (const [requestId, rawRejection] of Object.entries(state.requestRejections || {})) {
|
|
192
|
+
const rejection = normalizeRequestRejection(rawRejection, now);
|
|
193
|
+
if (now - rejection.updatedAt > TURN_STATE_TTL_MS) {
|
|
194
|
+
changed = true;
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
if (rejection.rejected) {
|
|
198
|
+
nextRequestRejections[requestId] = rejection;
|
|
199
|
+
}
|
|
200
|
+
if (
|
|
201
|
+
!rawRejection
|
|
202
|
+
|| typeof rawRejection !== 'object'
|
|
203
|
+
|| rawRejection.rejected !== rejection.rejected
|
|
204
|
+
|| rawRejection.updatedAt !== rejection.updatedAt
|
|
205
|
+
|| rawRejection.count !== rejection.count
|
|
206
|
+
|| rawRejection.threshold !== rejection.threshold
|
|
207
|
+
|| rawRejection.targetCount !== rejection.targetCount
|
|
208
|
+
) {
|
|
209
|
+
changed = true;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (changed) state.requestRejections = nextRequestRejections;
|
|
213
|
+
return changed;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function sleepMs(ms) {
|
|
217
|
+
try {
|
|
218
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
219
|
+
} catch (_) {
|
|
220
|
+
const end = Date.now() + ms;
|
|
221
|
+
while (Date.now() < end) {
|
|
222
|
+
// fallback for constrained Node runtimes
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function removeStaleLock(lockDir) {
|
|
228
|
+
try {
|
|
229
|
+
const stat = fs.statSync(lockDir);
|
|
230
|
+
if (!stat.isDirectory() || Date.now() - stat.mtimeMs <= STALE_LOCK_MS) return false;
|
|
231
|
+
fs.rmSync(lockDir, { recursive: true, force: true });
|
|
232
|
+
return true;
|
|
233
|
+
} catch (_) {
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function withLock(context, fn) {
|
|
239
|
+
fs.mkdirSync(context.sessionDir, { recursive: true });
|
|
240
|
+
const lockDir = path.join(context.sessionDir, '.lock');
|
|
241
|
+
const started = Date.now();
|
|
242
|
+
for (;;) {
|
|
243
|
+
try {
|
|
244
|
+
fs.mkdirSync(lockDir);
|
|
245
|
+
break;
|
|
246
|
+
} catch (error) {
|
|
247
|
+
if (!error || error.code !== 'EEXIST') fail(`failed to acquire lock: ${error && error.message ? error.message : String(error)}`);
|
|
248
|
+
if (removeStaleLock(lockDir)) continue;
|
|
249
|
+
if (Date.now() - started > LOCK_TIMEOUT_MS) fail('state lock timeout');
|
|
250
|
+
sleepMs(25);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
return fn(context.statePath);
|
|
255
|
+
} finally {
|
|
256
|
+
try { fs.rmdirSync(lockDir); } catch (_) { /* best effort */ }
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function normalizeTargetPath(target) {
|
|
261
|
+
if (process.platform !== 'win32' || typeof target !== 'string') return target;
|
|
262
|
+
if (/^[A-Za-z]:[\\/]/.test(target)) return target;
|
|
263
|
+
const msysMatch = target.match(/^\/([A-Za-z])(?:\/(.*))?$/);
|
|
264
|
+
if (!msysMatch) return target;
|
|
265
|
+
const drive = msysMatch[1].toUpperCase();
|
|
266
|
+
const rest = (msysMatch[2] || '').replace(/\//g, '\\');
|
|
267
|
+
return rest ? `${drive}:\\${rest}` : `${drive}:\\`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function countTarget(target, remaining) {
|
|
271
|
+
if (remaining <= 0) return 0;
|
|
272
|
+
target = normalizeTargetPath(target);
|
|
273
|
+
let stat;
|
|
274
|
+
try {
|
|
275
|
+
stat = fs.lstatSync(target);
|
|
276
|
+
} catch (error) {
|
|
277
|
+
if (error && error.code === 'ENOENT') return 0;
|
|
278
|
+
fail(`failed to stat target: ${target}`);
|
|
279
|
+
}
|
|
280
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) return 1;
|
|
281
|
+
|
|
282
|
+
let dir;
|
|
283
|
+
try {
|
|
284
|
+
dir = fs.opendirSync(target);
|
|
285
|
+
} catch (_) {
|
|
286
|
+
fail(`failed to read directory: ${target}`);
|
|
287
|
+
}
|
|
288
|
+
let count = 0;
|
|
289
|
+
try {
|
|
290
|
+
for (;;) {
|
|
291
|
+
if (count >= remaining) break;
|
|
292
|
+
const entry = dir.readSync();
|
|
293
|
+
if (!entry) break;
|
|
294
|
+
if (entry.isDirectory() && !entry.isSymbolicLink()) {
|
|
295
|
+
count += countTarget(path.join(target, entry.name), remaining - count);
|
|
296
|
+
} else {
|
|
297
|
+
count += 1;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
} finally {
|
|
301
|
+
try { dir.closeSync(); } catch (_) { /* best effort */ }
|
|
302
|
+
}
|
|
303
|
+
return count;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
function countTargets(targets, limit) {
|
|
307
|
+
let count = 0;
|
|
308
|
+
for (const target of targets) {
|
|
309
|
+
if (count >= limit) break;
|
|
310
|
+
count += countTarget(target, limit - count);
|
|
311
|
+
}
|
|
312
|
+
return count;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function checkSafeDeleteBulkGuard(context, targets) {
|
|
316
|
+
if (!targets || targets.length === 0) fail('check requires at least one target');
|
|
317
|
+
let decision = { kind: 'allow' };
|
|
318
|
+
withLock(context, statePath => {
|
|
319
|
+
const now = Date.now();
|
|
320
|
+
const state = readState(statePath);
|
|
321
|
+
const pruned = pruneExpired(state, now);
|
|
322
|
+
const rejection = normalizeRequestRejection(state.requestRejections && state.requestRejections[context.requestId], now);
|
|
323
|
+
if (rejection.rejected) {
|
|
324
|
+
decision = {
|
|
325
|
+
kind: 'rejected',
|
|
326
|
+
payload: {
|
|
327
|
+
count: rejection.count,
|
|
328
|
+
threshold: rejection.threshold,
|
|
329
|
+
scope: 'turn',
|
|
330
|
+
targets: rejection.targets,
|
|
331
|
+
targetCount: rejection.targetCount,
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
if (pruned) writeState(statePath, state);
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
const request = normalizeRequest(state.requests[context.requestId], now);
|
|
338
|
+
const approval = normalizeToolApproval(state.toolApprovals[context.toolCallId], now);
|
|
339
|
+
const deleteCount = countTargets(targets, DISPLAY_COUNT_LIMIT);
|
|
340
|
+
if (deleteCount === 0) {
|
|
341
|
+
if (pruned) writeState(statePath, state);
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
const totalCount = request.count + deleteCount;
|
|
345
|
+
if (approval.approved) {
|
|
346
|
+
state.requests[context.requestId] = { count: totalCount, updatedAt: now };
|
|
347
|
+
writeState(statePath, state);
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
if (totalCount >= context.threshold) {
|
|
351
|
+
decision = {
|
|
352
|
+
kind: 'confirmRequired',
|
|
353
|
+
payload: {
|
|
354
|
+
count: totalCount,
|
|
355
|
+
threshold: context.threshold,
|
|
356
|
+
scope: 'turn',
|
|
357
|
+
targets: targets.slice(0, TARGET_SAMPLE_LIMIT),
|
|
358
|
+
targetCount: targets.length,
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
if (pruned) writeState(statePath, state);
|
|
362
|
+
return;
|
|
363
|
+
}
|
|
364
|
+
state.requests[context.requestId] = { count: totalCount, updatedAt: now };
|
|
365
|
+
writeState(statePath, state);
|
|
366
|
+
});
|
|
367
|
+
return decision;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function signalFilePath(context) {
|
|
371
|
+
return path.join(context.sessionDir, `signal-${context.toolCallId}.json`);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function writeSignalFile(context, signal) {
|
|
375
|
+
try {
|
|
376
|
+
fs.mkdirSync(context.sessionDir, { recursive: true });
|
|
377
|
+
const tmpPath = `${signalFilePath(context)}.${process.pid}.tmp`;
|
|
378
|
+
fs.writeFileSync(tmpPath, `${JSON.stringify(signal)}\n`, 'utf-8');
|
|
379
|
+
fs.renameSync(tmpPath, signalFilePath(context));
|
|
380
|
+
} catch (_) {
|
|
381
|
+
// Best-effort: stderr marker is the fallback.
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
function handleCheck(args) {
|
|
386
|
+
if (args.targets.length === 0) fail('check requires at least one --target');
|
|
387
|
+
const context = getContext();
|
|
388
|
+
const decision = checkSafeDeleteBulkGuard(context, args.targets);
|
|
389
|
+
if (decision.kind === 'confirmRequired') {
|
|
390
|
+
writeSignalFile(context, { type: 'confirmRequired', payload: decision.payload });
|
|
391
|
+
process.stderr.write(`${CONFIRM_MARKER} ${JSON.stringify(decision.payload)}\n`);
|
|
392
|
+
process.exit(2);
|
|
393
|
+
}
|
|
394
|
+
if (decision.kind === 'rejected') {
|
|
395
|
+
writeSignalFile(context, { type: 'rejected', payload: decision.payload });
|
|
396
|
+
process.stderr.write(`${REJECTED_MARKER} ${JSON.stringify(decision.payload)}\n`);
|
|
397
|
+
process.exit(3);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function approveSafeDeleteBulkGuard(context, scope) {
|
|
402
|
+
if (scope && scope !== 'turn') fail('approve requires scope turn');
|
|
403
|
+
withLock(context, statePath => {
|
|
404
|
+
const now = Date.now();
|
|
405
|
+
const state = readState(statePath);
|
|
406
|
+
pruneExpired(state, now);
|
|
407
|
+
state.toolApprovals[context.toolCallId] = { approved: true, updatedAt: now };
|
|
408
|
+
writeState(statePath, state);
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function rejectSafeDeleteBulkGuard(context, payload) {
|
|
413
|
+
const normalized = normalizeRequestRejection({ ...payload, rejected: true }, Date.now());
|
|
414
|
+
withLock(context, statePath => {
|
|
415
|
+
const now = Date.now();
|
|
416
|
+
const state = readState(statePath);
|
|
417
|
+
pruneExpired(state, now);
|
|
418
|
+
state.requests[context.requestId] = {
|
|
419
|
+
count: normalized.count,
|
|
420
|
+
updatedAt: now,
|
|
421
|
+
};
|
|
422
|
+
state.requestRejections[context.requestId] = {
|
|
423
|
+
rejected: true,
|
|
424
|
+
updatedAt: now,
|
|
425
|
+
count: normalized.count,
|
|
426
|
+
threshold: normalized.threshold,
|
|
427
|
+
targets: normalized.targets,
|
|
428
|
+
targetCount: normalized.targetCount,
|
|
429
|
+
};
|
|
430
|
+
delete state.toolApprovals[context.toolCallId];
|
|
431
|
+
writeState(statePath, state);
|
|
432
|
+
});
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function handleApprove(args) {
|
|
436
|
+
if (args.scope && args.scope !== 'turn') fail('approve requires scope turn');
|
|
437
|
+
approveSafeDeleteBulkGuard(getContext(), args.scope);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function main() {
|
|
441
|
+
const args = parseArgs(process.argv);
|
|
442
|
+
if (args.command === 'check') return handleCheck(args);
|
|
443
|
+
if (args.command === 'approve') return handleApprove(args);
|
|
444
|
+
fail(`unknown command: ${args.command || '(empty)'}`);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
module.exports = {
|
|
448
|
+
approveSafeDeleteBulkGuard,
|
|
449
|
+
checkSafeDeleteBulkGuard,
|
|
450
|
+
createSafeDeleteBulkGuardContext,
|
|
451
|
+
rejectSafeDeleteBulkGuard,
|
|
452
|
+
DEFAULT_THRESHOLD,
|
|
453
|
+
CONFIRM_MARKER,
|
|
454
|
+
REJECTED_MARKER,
|
|
455
|
+
ERROR_MARKER,
|
|
456
|
+
};
|
|
457
|
+
|
|
458
|
+
if (require.main === module) {
|
|
459
|
+
try {
|
|
460
|
+
main();
|
|
461
|
+
} catch (error) {
|
|
462
|
+
process.stderr.write(`${ERROR_MARKER} ${error && error.message ? error.message : String(error)}\n`);
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
@@ -277,7 +277,7 @@ if _SESSION_ID:
|
|
|
277
277
|
_report_path = os.environ.get("CODEBUDDY_SAFE_DELETE_REPORT_PATH")
|
|
278
278
|
|
|
279
279
|
def _path_for_compare(path):
|
|
280
|
-
return os.path.normcase(os.path.abspath(path))
|
|
280
|
+
return os.path.normcase(os.path.realpath(os.path.abspath(path)))
|
|
281
281
|
|
|
282
282
|
def _is_usable_temp_root(path):
|
|
283
283
|
try:
|
|
@@ -324,6 +324,43 @@ if _SESSION_ID:
|
|
|
324
324
|
except Exception:
|
|
325
325
|
pass
|
|
326
326
|
|
|
327
|
+
def _exit_bulk_guard_control(message):
|
|
328
|
+
try:
|
|
329
|
+
sys.stderr.write(message)
|
|
330
|
+
if not message.endswith("\n"):
|
|
331
|
+
sys.stderr.write("\n")
|
|
332
|
+
sys.stderr.flush()
|
|
333
|
+
finally:
|
|
334
|
+
raise SystemExit(1)
|
|
335
|
+
|
|
336
|
+
def _check_bulk_delete_guard(abs_path):
|
|
337
|
+
guard_path = os.environ.get("CODEBUDDY_SAFE_DELETE_BULK_GUARD")
|
|
338
|
+
node_bin = os.environ.get("CODEBUDDY_NODE_BIN")
|
|
339
|
+
if not os.environ.get("CODEBUDDY_SAFE_DELETE_BULK_STATE_DIR") \
|
|
340
|
+
or not os.environ.get("CODEBUDDY_TOOL_CALL_ID"):
|
|
341
|
+
return
|
|
342
|
+
if not guard_path or not node_bin or not os.path.isfile(guard_path):
|
|
343
|
+
_exit_bulk_guard_control(
|
|
344
|
+
"[safe-delete][SAFE_DELETE_BULK_GUARD_ERROR] msg=helper-unavailable guard=%s"
|
|
345
|
+
% (guard_path or "unset")
|
|
346
|
+
)
|
|
347
|
+
env = dict(os.environ)
|
|
348
|
+
env["NODE_OPTIONS"] = ""
|
|
349
|
+
try:
|
|
350
|
+
result = subprocess.run(
|
|
351
|
+
[node_bin, guard_path, "check", "--target", abs_path],
|
|
352
|
+
capture_output=True, text=True, timeout=10, env=env
|
|
353
|
+
)
|
|
354
|
+
except Exception as e:
|
|
355
|
+
_exit_bulk_guard_control(
|
|
356
|
+
"[safe-delete][SAFE_DELETE_BULK_GUARD_ERROR] %s" % e
|
|
357
|
+
)
|
|
358
|
+
if result.returncode != 0:
|
|
359
|
+
_exit_bulk_guard_control(
|
|
360
|
+
result.stderr.strip()
|
|
361
|
+
or "[safe-delete][SAFE_DELETE_BULK_GUARD_ERROR] exit %d" % result.returncode
|
|
362
|
+
)
|
|
363
|
+
|
|
327
364
|
# -----------------------------------------------------------------------
|
|
328
365
|
# genie-trash native binary 支持(优先路径,不可用自动降级)
|
|
329
366
|
# 优先使用 GENIE_TRASH_DIR 环境变量(由 buildSafeDeleteEnv() 注入),
|
|
@@ -401,6 +438,7 @@ if _SESSION_ID:
|
|
|
401
438
|
except Exception:
|
|
402
439
|
pass
|
|
403
440
|
raise OSError(message)
|
|
441
|
+
_check_bulk_delete_guard(abs_path)
|
|
404
442
|
if _try_trash_via_binary(abs_path):
|
|
405
443
|
_record_trash(abs_path)
|
|
406
444
|
return True
|
|
@@ -437,6 +475,20 @@ if _SESSION_ID:
|
|
|
437
475
|
return True
|
|
438
476
|
return False
|
|
439
477
|
|
|
478
|
+
def _is_pip_site_packages_backup_dir(abs_path):
|
|
479
|
+
try:
|
|
480
|
+
target = _path_for_compare(os.fsdecode(abs_path))
|
|
481
|
+
except (TypeError, UnicodeError):
|
|
482
|
+
return False
|
|
483
|
+
parts = target.split(os.sep)
|
|
484
|
+
for index, part in enumerate(parts[:-1]):
|
|
485
|
+
if part not in ("site-packages", "dist-packages"):
|
|
486
|
+
continue
|
|
487
|
+
backup_root = parts[index + 1]
|
|
488
|
+
if backup_root.startswith("~"):
|
|
489
|
+
return True
|
|
490
|
+
return False
|
|
491
|
+
|
|
440
492
|
# -----------------------------------------------------------------------
|
|
441
493
|
# 包装:os.remove / os.unlink (文件,不能是目录)
|
|
442
494
|
# -----------------------------------------------------------------------
|
|
@@ -446,7 +498,7 @@ if _SESSION_ID:
|
|
|
446
498
|
if dir_fd is not None:
|
|
447
499
|
return _orig_remove(path, dir_fd=dir_fd)
|
|
448
500
|
abs_path = os.path.abspath(os.fspath(path))
|
|
449
|
-
if _is_under_os_tmp_dir(abs_path):
|
|
501
|
+
if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_backup_dir(abs_path):
|
|
450
502
|
return _orig_remove(path)
|
|
451
503
|
st = _safe_lstat(abs_path)
|
|
452
504
|
if not st:
|
|
@@ -463,7 +515,7 @@ if _SESSION_ID:
|
|
|
463
515
|
if dir_fd is not None:
|
|
464
516
|
return _orig_rmdir(path, dir_fd=dir_fd)
|
|
465
517
|
abs_path = os.path.abspath(os.fspath(path))
|
|
466
|
-
if _is_under_os_tmp_dir(abs_path):
|
|
518
|
+
if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_backup_dir(abs_path):
|
|
467
519
|
return _orig_rmdir(path)
|
|
468
520
|
st = _safe_lstat(abs_path)
|
|
469
521
|
if not st:
|
|
@@ -487,7 +539,7 @@ if _SESSION_ID:
|
|
|
487
539
|
path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
|
|
488
540
|
)
|
|
489
541
|
|
|
490
|
-
if _is_under_os_tmp_dir(abs_path):
|
|
542
|
+
if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_backup_dir(abs_path):
|
|
491
543
|
return _orig_shutil_rmtree(
|
|
492
544
|
path, ignore_errors=ignore_errors, onerror=onerror, **kwargs
|
|
493
545
|
)
|
|
@@ -519,7 +571,7 @@ if _SESSION_ID:
|
|
|
519
571
|
|
|
520
572
|
def _safe_path_unlink(self, missing_ok=False):
|
|
521
573
|
abs_path = os.path.abspath(str(self))
|
|
522
|
-
if _is_under_os_tmp_dir(abs_path):
|
|
574
|
+
if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_backup_dir(abs_path):
|
|
523
575
|
return _orig_path_unlink(self, missing_ok=missing_ok)
|
|
524
576
|
st = _safe_lstat(abs_path)
|
|
525
577
|
if not st:
|
|
@@ -532,7 +584,7 @@ if _SESSION_ID:
|
|
|
532
584
|
|
|
533
585
|
def _safe_path_rmdir(self):
|
|
534
586
|
abs_path = os.path.abspath(str(self))
|
|
535
|
-
if _is_under_os_tmp_dir(abs_path):
|
|
587
|
+
if _is_under_os_tmp_dir(abs_path) or _is_pip_site_packages_backup_dir(abs_path):
|
|
536
588
|
return _orig_path_rmdir(self)
|
|
537
589
|
st = _safe_lstat(abs_path)
|
|
538
590
|
if not st:
|
package/lib/node/index.js
DELETED
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* TUI mode entry point — full module set.
|
|
3
|
-
*
|
|
4
|
-
* Re-exports all shared modules from index-shared.ts, then adds TUI-only modules.
|
|
5
|
-
* The shared file is the DEFAULT place to add new modules.
|
|
6
|
-
* Only add exports here if the module has hard TUI dependencies
|
|
7
|
-
* (e.g., imports from ./ui, ink, react, TUI panels).
|
|
8
|
-
*
|
|
9
|
-
* See also: index-headless.ts (headless/SDK entry point)
|
|
10
|
-
*/
|
|
11
|
-
import './process';
|
|
12
|
-
export * from './index-shared';
|
|
13
|
-
// ── TUI-only modules ─────────────────────────────────────────────────
|
|
14
|
-
// These modules are excluded from headless builds because they depend on
|
|
15
|
-
// TUI rendering (ink/react), heavy optional deps, or interactive-only features.
|
|
16
|
-
export * from './doctor'; // CLI diagnostics wizard
|
|
17
|
-
export * from './install'; // CLI install / shell-setup
|
|
18
|
-
export * from './navigation'; // TUI page routing
|
|
19
|
-
export * from './remote-control'; // Remote control via WeChat etc.
|
|
20
|
-
export * from './remote-gateway'; // Remote gateway (local + cloud)
|
|
21
|
-
export * from './resume'; // ResumeMiddleware (imports TUI panel)
|
|
22
|
-
export * from './sandbox'; // e2b sandbox (heavy deps ~1250 modules)
|
|
23
|
-
export * from './shortcut'; // TUI keyboard shortcuts
|
|
24
|
-
export * from './tips'; // TUI tips / user state tracking
|
|
25
|
-
export * from './worktree'; // Git worktree management
|
|
26
|
-
export * from './ui'; // TUI UI components
|
|
27
|
-
export * from './feedback-survey'; // TUI feedback survey
|
|
28
|
-
export * from './undo-redo'; // TUI undo/redo
|
|
29
|
-
export * from './prompt-suggestion'; // TUI prompt suggestions
|
|
30
|
-
//# sourceMappingURL=index.js.map
|