dsh-use-codex 0.0.0-stage → 0.4.0
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/LICENSE +21 -0
- package/README.en.md +400 -0
- package/README.md +404 -2
- package/SECURITY.md +30 -0
- package/cordis.patch.yml +19 -0
- package/lib/index.js +506 -0
- package/package.json +54 -4
package/lib/index.js
ADDED
|
@@ -0,0 +1,506 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dsh-use-codex — 让 DeepSeek Harness 通过工具调用本机 Codex CLI。
|
|
3
|
+
*
|
|
4
|
+
* 安全边界:
|
|
5
|
+
* 1. 工作目录必须是白名单内的真实绝对路径;软链逃逸会被拒绝。
|
|
6
|
+
* 2. 沙箱默认 workspace-write;宿主配置 allowDangerFullAccess 后可用 codex_danger 运行时开关切到 danger-full-access。
|
|
7
|
+
* 3. 不经过 shell,任务作为单个 argv 传给 Codex。
|
|
8
|
+
* 4. 超时或取消时终止 Codex 进程组,并限制输出大小。
|
|
9
|
+
* 5. 不持久化任务正文、工作目录或输出;状态文件只记录即时开关。
|
|
10
|
+
*/
|
|
11
|
+
import { spawn } from 'node:child_process';
|
|
12
|
+
import {
|
|
13
|
+
existsSync,
|
|
14
|
+
mkdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
realpathSync,
|
|
17
|
+
renameSync,
|
|
18
|
+
statSync,
|
|
19
|
+
writeFileSync,
|
|
20
|
+
} from 'node:fs';
|
|
21
|
+
import { homedir, tmpdir } from 'node:os';
|
|
22
|
+
import { isAbsolute, join, resolve, sep } from 'node:path';
|
|
23
|
+
|
|
24
|
+
export const name = 'use-codex';
|
|
25
|
+
export const inject = ['tools'];
|
|
26
|
+
|
|
27
|
+
const DEFAULT_TIMEOUT_SEC = 900;
|
|
28
|
+
const DEFAULT_MAX_OUTPUT_CHARS = 20_000;
|
|
29
|
+
const MAX_TASK_CHARS = 100_000;
|
|
30
|
+
const STATE_FILE = process.env.USE_CODEX_STATE
|
|
31
|
+
|| join(process.env.DSH_HOME || join(homedir(), '.dsh'), 'use-codex.json');
|
|
32
|
+
|
|
33
|
+
function finiteNumber(value, fallback) {
|
|
34
|
+
const number = Number(value);
|
|
35
|
+
return Number.isFinite(number) ? number : fallback;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function clamp(value, min, max) {
|
|
39
|
+
return Math.min(Math.max(value, min), max);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function expandHome(input) {
|
|
43
|
+
const value = String(input ?? '').trim();
|
|
44
|
+
if (value === '~') return homedir();
|
|
45
|
+
if (value.startsWith(`~${sep}`)) return join(homedir(), value.slice(2));
|
|
46
|
+
return value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readState() {
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(readFileSync(STATE_FILE, 'utf8'));
|
|
52
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
|
|
53
|
+
} catch {
|
|
54
|
+
return {};
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function writeState(patch) {
|
|
59
|
+
const next = { ...readState(), ...patch, updatedAt: new Date().toISOString() };
|
|
60
|
+
const temp = `${STATE_FILE}.${process.pid}.tmp`;
|
|
61
|
+
try {
|
|
62
|
+
mkdirSync(join(STATE_FILE, '..'), { recursive: true, mode: 0o700 });
|
|
63
|
+
writeFileSync(temp, `${JSON.stringify(next, null, 2)}\n`, { encoding: 'utf8', mode: 0o600 });
|
|
64
|
+
renameSync(temp, STATE_FILE);
|
|
65
|
+
return true;
|
|
66
|
+
} catch {
|
|
67
|
+
try { writeFileSync(temp, '', 'utf8'); } catch { /* ignore */ }
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function resolveExistingDirectory(input) {
|
|
73
|
+
const expanded = expandHome(input);
|
|
74
|
+
if (!expanded) throw new Error('工作目录不能为空');
|
|
75
|
+
if (!isAbsolute(expanded)) throw new Error(`工作目录必须是绝对路径:${expanded}`);
|
|
76
|
+
|
|
77
|
+
const absolute = resolve(expanded);
|
|
78
|
+
if (!existsSync(absolute)) throw new Error(`工作目录不存在:${absolute}`);
|
|
79
|
+
|
|
80
|
+
let real;
|
|
81
|
+
try {
|
|
82
|
+
real = realpathSync.native(absolute);
|
|
83
|
+
} catch {
|
|
84
|
+
throw new Error(`无法解析工作目录:${absolute}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (!statSync(real).isDirectory()) throw new Error(`工作目录不是文件夹:${absolute}`);
|
|
88
|
+
return real;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function isInside(target, root) {
|
|
92
|
+
return target === root || target.startsWith(`${root}${sep}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
function createLimitedBuffer(limit) {
|
|
96
|
+
let text = '';
|
|
97
|
+
let totalChars = 0;
|
|
98
|
+
|
|
99
|
+
return {
|
|
100
|
+
append(value) {
|
|
101
|
+
const chunk = String(value ?? '');
|
|
102
|
+
totalChars += chunk.length;
|
|
103
|
+
if (text.length < limit) text += chunk.slice(0, limit - text.length);
|
|
104
|
+
},
|
|
105
|
+
finish() {
|
|
106
|
+
const trimmed = text.trim();
|
|
107
|
+
if (totalChars <= limit) return trimmed;
|
|
108
|
+
return `${trimmed}\n…(已截断:原始 ${totalChars} 字符,上限 ${limit} 字符)`;
|
|
109
|
+
},
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
function killProcessTree(child, signal) {
|
|
114
|
+
if (!child?.pid) return;
|
|
115
|
+
try {
|
|
116
|
+
process.kill(-child.pid, signal);
|
|
117
|
+
} catch {
|
|
118
|
+
try { child.kill(signal); } catch { /* 已退出 */ }
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function getAbortSignal(context) {
|
|
123
|
+
if (context?.signal && typeof context.signal.addEventListener === 'function') return context.signal;
|
|
124
|
+
if (context?.abortSignal && typeof context.abortSignal.addEventListener === 'function') {
|
|
125
|
+
return context.abortSignal;
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function runCodex(bin, argv, { cwd, timeoutMs, maxChars, signal }) {
|
|
131
|
+
return new Promise((resolvePromise) => {
|
|
132
|
+
const started = Date.now();
|
|
133
|
+
const stdout = createLimitedBuffer(maxChars);
|
|
134
|
+
const stderr = createLimitedBuffer(maxChars);
|
|
135
|
+
let child;
|
|
136
|
+
let timeoutTimer;
|
|
137
|
+
let hardKillTimer;
|
|
138
|
+
let finished = false;
|
|
139
|
+
let timedOut = false;
|
|
140
|
+
let aborted = false;
|
|
141
|
+
|
|
142
|
+
const cleanup = () => {
|
|
143
|
+
clearTimeout(timeoutTimer);
|
|
144
|
+
clearTimeout(hardKillTimer);
|
|
145
|
+
signal?.removeEventListener?.('abort', onAbort);
|
|
146
|
+
};
|
|
147
|
+
|
|
148
|
+
const finish = (code, spawnError = '') => {
|
|
149
|
+
if (finished) return;
|
|
150
|
+
finished = true;
|
|
151
|
+
cleanup();
|
|
152
|
+
const ok = code === 0 && !timedOut && !aborted && !spawnError;
|
|
153
|
+
resolvePromise({
|
|
154
|
+
ok,
|
|
155
|
+
code: timedOut ? 'timeout' : aborted ? 'aborted' : spawnError ? 'spawn-error' : code,
|
|
156
|
+
stdout: stdout.finish(),
|
|
157
|
+
stderr: stderr.finish(),
|
|
158
|
+
error: spawnError,
|
|
159
|
+
ms: Date.now() - started,
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
const onAbort = () => {
|
|
164
|
+
aborted = true;
|
|
165
|
+
killProcessTree(child, 'SIGTERM');
|
|
166
|
+
clearTimeout(hardKillTimer);
|
|
167
|
+
hardKillTimer = setTimeout(() => killProcessTree(child, 'SIGKILL'), 1_500);
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
if (signal?.aborted) {
|
|
171
|
+
aborted = true;
|
|
172
|
+
finish('aborted');
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
try {
|
|
177
|
+
child = spawn(bin, argv, {
|
|
178
|
+
cwd,
|
|
179
|
+
detached: process.platform !== 'win32',
|
|
180
|
+
windowsHide: true,
|
|
181
|
+
env: { ...process.env, NO_COLOR: '1', CI: '1' },
|
|
182
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
183
|
+
});
|
|
184
|
+
} catch (error) {
|
|
185
|
+
finish('spawn-error', String(error?.message || error));
|
|
186
|
+
return;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
child.stdout?.setEncoding('utf8');
|
|
190
|
+
child.stderr?.setEncoding('utf8');
|
|
191
|
+
child.stdout?.on('data', (chunk) => stdout.append(chunk));
|
|
192
|
+
child.stderr?.on('data', (chunk) => stderr.append(chunk));
|
|
193
|
+
|
|
194
|
+
child.on('error', (error) => finish('spawn-error', String(error?.message || error)));
|
|
195
|
+
child.on('close', (code, closeSignal) => {
|
|
196
|
+
finish(code ?? (closeSignal ? `signal:${closeSignal}` : 'unknown'));
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
timeoutTimer = setTimeout(() => {
|
|
200
|
+
timedOut = true;
|
|
201
|
+
killProcessTree(child, 'SIGTERM');
|
|
202
|
+
hardKillTimer = setTimeout(() => killProcessTree(child, 'SIGKILL'), 1_500);
|
|
203
|
+
}, timeoutMs);
|
|
204
|
+
|
|
205
|
+
signal?.addEventListener?.('abort', onAbort, { once: true });
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function textTool({ name: toolName, description, parameters, timeoutMs, execute }) {
|
|
210
|
+
return {
|
|
211
|
+
name: toolName,
|
|
212
|
+
description,
|
|
213
|
+
parameters,
|
|
214
|
+
output: {
|
|
215
|
+
schema: { type: 'string' },
|
|
216
|
+
render: (_args, value) => [{ type: 'text', text: String(value ?? '') }],
|
|
217
|
+
},
|
|
218
|
+
...(timeoutMs ? { timeoutMs } : {}),
|
|
219
|
+
execute,
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
export function apply(ctx, config = {}) {
|
|
224
|
+
if (config.enabled === false) {
|
|
225
|
+
ctx.logger?.info?.('[use-codex] disabled by configuration');
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const bin = expandHome(config.bin || process.env.CODEX_BIN || 'codex');
|
|
230
|
+
const defaultCwd = expandHome(config.defaultCwd || process.cwd());
|
|
231
|
+
const rawRoots = Array.isArray(config.allowedRoots) && config.allowedRoots.length
|
|
232
|
+
? config.allowedRoots
|
|
233
|
+
: [defaultCwd];
|
|
234
|
+
const allowDanger = config.allowDangerFullAccess === true;
|
|
235
|
+
const maxTimeoutSec = clamp(
|
|
236
|
+
finiteNumber(config.timeoutSec, DEFAULT_TIMEOUT_SEC),
|
|
237
|
+
1,
|
|
238
|
+
3_600,
|
|
239
|
+
);
|
|
240
|
+
const maxOutputChars = Math.round(clamp(
|
|
241
|
+
finiteNumber(config.maxOutputChars, DEFAULT_MAX_OUTPUT_CHARS),
|
|
242
|
+
1_000,
|
|
243
|
+
200_000,
|
|
244
|
+
));
|
|
245
|
+
|
|
246
|
+
const allowedRoots = [];
|
|
247
|
+
const rootWarnings = [];
|
|
248
|
+
for (const rawRoot of rawRoots) {
|
|
249
|
+
try {
|
|
250
|
+
const real = resolveExistingDirectory(rawRoot);
|
|
251
|
+
if (!allowedRoots.includes(real)) allowedRoots.push(real);
|
|
252
|
+
} catch (error) {
|
|
253
|
+
rootWarnings.push(String(error?.message || error));
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
const warnings = [...rootWarnings];
|
|
258
|
+
if (allowedRoots.length === 0) warnings.push('没有可用的白名单目录,codex_run 将拒绝执行');
|
|
259
|
+
try {
|
|
260
|
+
const realDefault = resolveExistingDirectory(defaultCwd);
|
|
261
|
+
if (!allowedRoots.some((root) => isInside(realDefault, root))) {
|
|
262
|
+
warnings.push('defaultCwd 不在白名单内,未显式提供 cwd 时 codex_run 将拒绝执行');
|
|
263
|
+
}
|
|
264
|
+
} catch (error) {
|
|
265
|
+
warnings.push(`defaultCwd 不可用:${String(error?.message || error)}`);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
const enabledNow = () => readState().enabled !== false;
|
|
269
|
+
// Runtime danger-mode switch: defaults to the host config; codex_danger toggles it per session.
|
|
270
|
+
const dangerNow = () => {
|
|
271
|
+
const state = readState();
|
|
272
|
+
if (typeof state.dangerFullAccess === 'boolean') return state.dangerFullAccess;
|
|
273
|
+
return allowDanger;
|
|
274
|
+
};
|
|
275
|
+
const defaultSandbox = () => (dangerNow() ? 'danger-full-access' : 'workspace-write');
|
|
276
|
+
let lastRun = null;
|
|
277
|
+
|
|
278
|
+
const resolveRunCwd = (input) => {
|
|
279
|
+
const requested = String(input ?? '').trim() || defaultCwd;
|
|
280
|
+
const real = resolveExistingDirectory(requested);
|
|
281
|
+
if (!allowedRoots.some((root) => isInside(real, root))) {
|
|
282
|
+
throw new Error(`工作目录不在白名单内:${real}`);
|
|
283
|
+
}
|
|
284
|
+
return real;
|
|
285
|
+
};
|
|
286
|
+
|
|
287
|
+
const safeStatusCwd = () => {
|
|
288
|
+
try { return resolveRunCwd(defaultCwd); } catch { return tmpdir(); }
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const tools = [
|
|
292
|
+
textTool({
|
|
293
|
+
name: 'codex_run',
|
|
294
|
+
description:
|
|
295
|
+
'让本机 OpenAI Codex CLI 执行一段独立任务并返回结果。'
|
|
296
|
+
+ '当用户说“让 Codex 做……”“交给 Codex 执行……”,或希望把编码、排障、批量文件处理任务委派出去时调用。'
|
|
297
|
+
+ '工作目录必须在 DSH 配置的白名单内;默认沙箱由宿主配置与运行时开关共同决定;失败时如实返回失败原因。',
|
|
298
|
+
parameters: {
|
|
299
|
+
type: 'object',
|
|
300
|
+
additionalProperties: false,
|
|
301
|
+
properties: {
|
|
302
|
+
task: {
|
|
303
|
+
type: 'string',
|
|
304
|
+
minLength: 1,
|
|
305
|
+
maxLength: MAX_TASK_CHARS,
|
|
306
|
+
description: '完整、明确的任务说明,包含目标、约束和验收标准。',
|
|
307
|
+
},
|
|
308
|
+
cwd: {
|
|
309
|
+
type: 'string',
|
|
310
|
+
description: '可选的绝对工作目录,必须真实存在且位于白名单内;省略时使用 defaultCwd。',
|
|
311
|
+
},
|
|
312
|
+
sandbox: {
|
|
313
|
+
type: 'string',
|
|
314
|
+
enum: ['read-only', 'workspace-write', 'danger-full-access'],
|
|
315
|
+
description: 'Codex 沙箱模式;省略时用 defaultSandbox()(宿主 allowDangerFullAccess 与 codex_danger 运行时开关共同决定)。',
|
|
316
|
+
},
|
|
317
|
+
timeoutSec: {
|
|
318
|
+
type: 'number',
|
|
319
|
+
minimum: 1,
|
|
320
|
+
description: '本次超时秒数,不超过插件配置的 timeoutSec。',
|
|
321
|
+
},
|
|
322
|
+
},
|
|
323
|
+
required: ['task'],
|
|
324
|
+
},
|
|
325
|
+
async execute(args, context) {
|
|
326
|
+
if (!enabledNow()) {
|
|
327
|
+
return '⛔ Codex 委派已关闭。请先用 codex_switch 打开。';
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const task = String(args?.task ?? '').trim();
|
|
331
|
+
if (!task) return '❌ 任务不能为空。';
|
|
332
|
+
if (task.length > MAX_TASK_CHARS) return `❌ 任务过长,最多 ${MAX_TASK_CHARS} 字符。`;
|
|
333
|
+
|
|
334
|
+
const sandbox = String(args?.sandbox || defaultSandbox());
|
|
335
|
+
if (!['read-only', 'workspace-write', 'danger-full-access'].includes(sandbox)) {
|
|
336
|
+
return `❌ 不支持的沙箱模式:${sandbox}`;
|
|
337
|
+
}
|
|
338
|
+
if (sandbox === 'danger-full-access' && !allowDanger) {
|
|
339
|
+
return '❌ danger-full-access 未获宿主配置许可,拒绝执行。';
|
|
340
|
+
}
|
|
341
|
+
if (sandbox === 'danger-full-access' && !dangerNow()) {
|
|
342
|
+
return '🛡 danger-full-access 已被 codex_danger 关闭;请先用 codex_danger 打开,或显式传 workspace-write。';
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
let cwd;
|
|
346
|
+
try {
|
|
347
|
+
cwd = resolveRunCwd(args?.cwd);
|
|
348
|
+
} catch (error) {
|
|
349
|
+
return `❌ ${String(error?.message || error)}`;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (!existsSync(bin) && bin.includes(sep)) {
|
|
353
|
+
return `❌ 找不到 Codex 可执行文件:${bin}`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const timeoutSec = clamp(
|
|
357
|
+
finiteNumber(args?.timeoutSec, maxTimeoutSec),
|
|
358
|
+
1,
|
|
359
|
+
maxTimeoutSec,
|
|
360
|
+
);
|
|
361
|
+
const argv = [
|
|
362
|
+
'exec',
|
|
363
|
+
'-C', cwd,
|
|
364
|
+
'-s', sandbox,
|
|
365
|
+
'--skip-git-repo-check',
|
|
366
|
+
'--ephemeral',
|
|
367
|
+
'--color', 'never',
|
|
368
|
+
'--',
|
|
369
|
+
task,
|
|
370
|
+
];
|
|
371
|
+
const result = await runCodex(bin, argv, {
|
|
372
|
+
cwd,
|
|
373
|
+
timeoutMs: Math.round(timeoutSec * 1_000),
|
|
374
|
+
maxChars: maxOutputChars,
|
|
375
|
+
signal: getAbortSignal(context),
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
lastRun = {
|
|
379
|
+
at: new Date().toISOString(),
|
|
380
|
+
ok: result.ok,
|
|
381
|
+
code: result.code,
|
|
382
|
+
ms: result.ms,
|
|
383
|
+
sandbox,
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const status = result.ok
|
|
387
|
+
? `✅ Codex 执行完成(${(result.ms / 1_000).toFixed(1)}s,${sandbox})`
|
|
388
|
+
: `❌ Codex 执行失败(${result.code},${(result.ms / 1_000).toFixed(1)}s,${sandbox})`;
|
|
389
|
+
const sections = [status];
|
|
390
|
+
if (result.stdout) sections.push(`--- stdout ---\n${result.stdout}`);
|
|
391
|
+
if (result.stderr) sections.push(`--- stderr ---\n${result.stderr}`);
|
|
392
|
+
if (result.error) sections.push(`--- error ---\n${result.error}`);
|
|
393
|
+
if (sections.length === 1) sections.push('(无输出)');
|
|
394
|
+
return sections.join('\n\n');
|
|
395
|
+
},
|
|
396
|
+
}),
|
|
397
|
+
|
|
398
|
+
textTool({
|
|
399
|
+
name: 'codex_status',
|
|
400
|
+
description: '检查 Codex 委派插件的开关、Codex CLI 版本、认证状态、目录白名单和安全配置。',
|
|
401
|
+
parameters: {
|
|
402
|
+
type: 'object',
|
|
403
|
+
additionalProperties: false,
|
|
404
|
+
properties: {},
|
|
405
|
+
},
|
|
406
|
+
timeoutMs: 35_000,
|
|
407
|
+
async execute() {
|
|
408
|
+
const statusCwd = safeStatusCwd();
|
|
409
|
+
const version = await runCodex(bin, ['--version'], {
|
|
410
|
+
cwd: statusCwd,
|
|
411
|
+
timeoutMs: 15_000,
|
|
412
|
+
maxChars: 2_000,
|
|
413
|
+
});
|
|
414
|
+
const login = await runCodex(bin, ['login', 'status'], {
|
|
415
|
+
cwd: statusCwd,
|
|
416
|
+
timeoutMs: 15_000,
|
|
417
|
+
maxChars: 2_000,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
const lines = [
|
|
421
|
+
`🔌 开关:${enabledNow() ? '打开' : '关闭'}`,
|
|
422
|
+
`📦 Codex:${version.ok ? version.stdout : `不可用(${version.error || version.stderr || version.code})`}`,
|
|
423
|
+
`🔑 认证:${login.stdout || login.stderr || (login.ok ? '未知' : login.code)}`,
|
|
424
|
+
`📁 白名单:${allowedRoots.length ? allowedRoots.join('、') : '(空)'}`,
|
|
425
|
+
`🛡 危险模式:${allowDanger ? (dangerNow() ? '允许且当前打开' : '允许但当前关闭') : '禁止'}`,
|
|
426
|
+
`⏱ 超时上限:${maxTimeoutSec}s`,
|
|
427
|
+
`📏 输出上限:${maxOutputChars} 字符`,
|
|
428
|
+
];
|
|
429
|
+
if (warnings.length) lines.push(`⚠️ 配置提示:${warnings.join(';')}`);
|
|
430
|
+
if (lastRun) {
|
|
431
|
+
lines.push(`🕘 本进程最近调用:${lastRun.at}|${lastRun.ok ? '成功' : '失败'}|${lastRun.ms}ms|${lastRun.sandbox}`);
|
|
432
|
+
}
|
|
433
|
+
return lines.join('\n');
|
|
434
|
+
},
|
|
435
|
+
}),
|
|
436
|
+
|
|
437
|
+
textTool({
|
|
438
|
+
name: 'codex_danger',
|
|
439
|
+
description: '切换 danger-full-access 运行时开关。'
|
|
440
|
+
+ '仅在宿主配置 allowDangerFullAccess: true 时有效;'
|
|
441
|
+
+ '打开后 codex_run 默认使用 danger-full-access(无沙箱),关闭则回到 workspace-write。'
|
|
442
|
+
+ '此开关影响下一次 codex_run 的默认沙箱,不影响显式传入的 sandbox 参数。',
|
|
443
|
+
parameters: {
|
|
444
|
+
type: 'object',
|
|
445
|
+
additionalProperties: false,
|
|
446
|
+
properties: {
|
|
447
|
+
enabled: {
|
|
448
|
+
type: 'boolean',
|
|
449
|
+
description: 'true 为打开危险模式(默认沙箱变 danger-full-access),false 为关闭(回到 workspace-write)。',
|
|
450
|
+
},
|
|
451
|
+
},
|
|
452
|
+
required: ['enabled'],
|
|
453
|
+
},
|
|
454
|
+
timeoutMs: 10_000,
|
|
455
|
+
async execute(args) {
|
|
456
|
+
if (!allowDanger) {
|
|
457
|
+
return '❌ 宿主配置未开启 allowDangerFullAccess,此开关无效。';
|
|
458
|
+
}
|
|
459
|
+
const danger = args?.enabled === true;
|
|
460
|
+
if (!writeState({ dangerFullAccess: danger })) {
|
|
461
|
+
return '❌ 无法写入 Codex 危险模式状态,请检查 DSH 状态目录权限。';
|
|
462
|
+
}
|
|
463
|
+
return danger
|
|
464
|
+
? '🔓 危险模式已打开:codex_run 默认沙箱为 danger-full-access。'
|
|
465
|
+
: '🛡 危险模式已关闭:codex_run 默认沙箱为 workspace-write。';
|
|
466
|
+
},
|
|
467
|
+
}),
|
|
468
|
+
|
|
469
|
+
textTool({
|
|
470
|
+
name: 'codex_switch',
|
|
471
|
+
description: '即时打开或关闭 Codex 委派,不需要重启 DSH;关闭后 codex_run 会拒绝执行。',
|
|
472
|
+
parameters: {
|
|
473
|
+
type: 'object',
|
|
474
|
+
additionalProperties: false,
|
|
475
|
+
properties: {
|
|
476
|
+
enabled: {
|
|
477
|
+
type: 'boolean',
|
|
478
|
+
description: 'true 为打开,false 为关闭。',
|
|
479
|
+
},
|
|
480
|
+
},
|
|
481
|
+
required: ['enabled'],
|
|
482
|
+
},
|
|
483
|
+
timeoutMs: 10_000,
|
|
484
|
+
async execute(args) {
|
|
485
|
+
const enabled = args?.enabled === true;
|
|
486
|
+
if (!writeState({ enabled })) {
|
|
487
|
+
return '❌ 无法写入 Codex 委派状态,请检查 DSH 状态目录权限。';
|
|
488
|
+
}
|
|
489
|
+
return enabled
|
|
490
|
+
? '🔓 Codex 委派已打开。'
|
|
491
|
+
: '🔒 Codex 委派已关闭;codex_run 从现在起拒绝执行。';
|
|
492
|
+
},
|
|
493
|
+
}),
|
|
494
|
+
];
|
|
495
|
+
|
|
496
|
+
for (const tool of tools) {
|
|
497
|
+
const dispose = ctx.tools.register(tool);
|
|
498
|
+
if (typeof dispose === 'function') {
|
|
499
|
+
ctx.effect?.(() => dispose);
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
ctx.logger?.info?.(`[use-codex] registered ${tools.length} tools; allowed roots=${allowedRoots.length}; enabled=${enabledNow()}; danger=${dangerNow()}`);
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
export default { name, inject, apply };
|
package/package.json
CHANGED
|
@@ -1,6 +1,56 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-use-codex",
|
|
3
|
-
"
|
|
4
|
-
"
|
|
5
|
-
"
|
|
6
|
-
|
|
3
|
+
"displayName": "DSH Use Codex",
|
|
4
|
+
"description": "通过 DeepSeek Harness 工具调用本机 Codex CLI 执行独立任务;danger-full-access 支持宿主许可 + 运行时开关双重授权。",
|
|
5
|
+
"version": "0.4.0",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "lib/index.js",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": "./lib/index.js",
|
|
10
|
+
"./package.json": "./package.json"
|
|
11
|
+
},
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=20",
|
|
15
|
+
"dsh": ">=0.2.1-rc.1 <0.3.0"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"lib/",
|
|
19
|
+
"README.md",
|
|
20
|
+
"README.en.md",
|
|
21
|
+
"SECURITY.md",
|
|
22
|
+
"LICENSE",
|
|
23
|
+
"cordis.patch.yml"
|
|
24
|
+
],
|
|
25
|
+
"keywords": [
|
|
26
|
+
"dsh-plugin",
|
|
27
|
+
"deepseek-harness",
|
|
28
|
+
"codex",
|
|
29
|
+
"codex-cli",
|
|
30
|
+
"agent",
|
|
31
|
+
"delegation"
|
|
32
|
+
],
|
|
33
|
+
"scripts": {
|
|
34
|
+
"test": "node selftest.mjs",
|
|
35
|
+
"prepack": "npm test"
|
|
36
|
+
},
|
|
37
|
+
"dsh": {
|
|
38
|
+
"bundle": {
|
|
39
|
+
"patch": "./cordis.patch.yml"
|
|
40
|
+
},
|
|
41
|
+
"install": {
|
|
42
|
+
"preferred": "tarball",
|
|
43
|
+
"tarball": "https://github.com/chaorenzeng/dsh-use-codex/archive/refs/heads/main.tar.gz",
|
|
44
|
+
"git": "https://github.com/chaorenzeng/dsh-use-codex.git"
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
"author": "chaorenzeng",
|
|
48
|
+
"repository": {
|
|
49
|
+
"type": "git",
|
|
50
|
+
"url": "git+https://github.com/chaorenzeng/dsh-use-codex.git"
|
|
51
|
+
},
|
|
52
|
+
"homepage": "https://github.com/chaorenzeng/dsh-use-codex#readme",
|
|
53
|
+
"bugs": {
|
|
54
|
+
"url": "https://github.com/chaorenzeng/dsh-use-codex/issues"
|
|
55
|
+
}
|
|
56
|
+
}
|