codex-toolkit 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.md +274 -0
- package/bin/codex-toolkit.js +6 -0
- package/examples/auto-lint.config.example.json +11 -0
- package/examples/scope-guard.config.example.json +15 -0
- package/package.json +65 -0
- package/src/auto-lint.js +254 -0
- package/src/diff-budget.js +173 -0
- package/src/hook-protocol.js +146 -0
- package/src/index.js +12 -0
- package/src/installer.js +331 -0
- package/src/scope-guard.js +180 -0
- package/src/shield-destructive-cmd.js +179 -0
- package/src/shield-env-guard.js +192 -0
- package/src/state-store.js +91 -0
- package/src/tool-pace-check.js +130 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
// tool-pace-check — slow Codex down when it tries to chain many tool calls
|
|
2
|
+
// in a short window.
|
|
3
|
+
//
|
|
4
|
+
// The failure mode this targets: the model gets into a "just one more thing"
|
|
5
|
+
// loop — read, edit, run, edit, run, edit, run — without stopping to reflect
|
|
6
|
+
// or check in. Each individual call looks reasonable; the *pattern* is what
|
|
7
|
+
// hurts. By the time the user looks up, five files have been silently
|
|
8
|
+
// rewritten and the original task is buried in noise.
|
|
9
|
+
//
|
|
10
|
+
// We count tool calls per session in a sliding time window. When the count
|
|
11
|
+
// crosses the configured threshold, we ask the user to confirm before
|
|
12
|
+
// continuing.
|
|
13
|
+
|
|
14
|
+
import fs from 'node:fs';
|
|
15
|
+
import path from 'node:path';
|
|
16
|
+
import process from 'node:process';
|
|
17
|
+
import {
|
|
18
|
+
DECISIONS,
|
|
19
|
+
emitDecision,
|
|
20
|
+
emitError,
|
|
21
|
+
parseHookInput,
|
|
22
|
+
} from './hook-protocol.js';
|
|
23
|
+
import { readState, resolveStateFile, sessionKey, updateState } from './state-store.js';
|
|
24
|
+
|
|
25
|
+
const DEFAULT_CONFIG = {
|
|
26
|
+
mode: 'enforce', // 'enforce' | 'ask' | 'off'
|
|
27
|
+
max_calls_in_window: 8, // N tool calls
|
|
28
|
+
window_seconds: 60, // over the last K seconds
|
|
29
|
+
log: true,
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function loadConfig() {
|
|
33
|
+
const candidates = [
|
|
34
|
+
process.env.CODEX_TOOLKIT_TOOL_PACE_CONFIG,
|
|
35
|
+
path.join(process.cwd(), '.codex-toolkit', 'tool-pace.json'),
|
|
36
|
+
path.join(process.env.HOME || '', '.codex', 'tool-pace.json'),
|
|
37
|
+
].filter(Boolean);
|
|
38
|
+
for (const file of candidates) {
|
|
39
|
+
try {
|
|
40
|
+
const parsed = JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
41
|
+
return { ...DEFAULT_CONFIG, ...parsed };
|
|
42
|
+
} catch (err) {
|
|
43
|
+
if (err.code !== 'ENOENT') {
|
|
44
|
+
emitError(`tool-pace-check: failed to read ${file}: ${err.message}`);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
return { ...DEFAULT_CONFIG };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function pruneOldTimestamps(calls, windowMs, now) {
|
|
52
|
+
return calls.filter((t) => now - t <= windowMs);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function evaluate(event) {
|
|
56
|
+
const config = loadConfig();
|
|
57
|
+
if (config.mode === 'off') {
|
|
58
|
+
return { decision: DECISIONS.ALLOW, reason: null, skipped: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const stateFile = resolveStateFile('tool-pace');
|
|
62
|
+
const key = sessionKey(event);
|
|
63
|
+
const windowMs = config.window_seconds * 1000;
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
|
|
66
|
+
const state = updateState(
|
|
67
|
+
stateFile,
|
|
68
|
+
(s) => {
|
|
69
|
+
s.sessions = s.sessions || {};
|
|
70
|
+
const sess = s.sessions[key] || { calls: [] };
|
|
71
|
+
sess.calls = pruneOldTimestamps(sess.calls, windowMs, now);
|
|
72
|
+
sess.calls.push(now);
|
|
73
|
+
s.sessions[key] = sess;
|
|
74
|
+
return s;
|
|
75
|
+
},
|
|
76
|
+
{ sessions: {} }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
const sess = state.sessions[key] || { calls: [] };
|
|
80
|
+
const callCount = sess.calls.length;
|
|
81
|
+
if (config.log) {
|
|
82
|
+
process.stderr.write(
|
|
83
|
+
`[tool-pace-check] tool=${event.toolName ?? '(unknown)'} calls-in-window=${callCount}/${config.max_calls_in_window}\n`
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
if (callCount <= config.max_calls_in_window) {
|
|
87
|
+
return { decision: DECISIONS.ALLOW, reason: null, stats: { callCount } };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Threshold exceeded.
|
|
91
|
+
const reason = `tool-pace-check: ${callCount} tool calls in the last ${config.window_seconds}s (limit ${config.max_calls_in_window}). Codex may be in a runaway-edit loop.`;
|
|
92
|
+
if (config.mode === 'ask') {
|
|
93
|
+
return { decision: DECISIONS.ASK, reason };
|
|
94
|
+
}
|
|
95
|
+
return { decision: DECISIONS.DENY, reason };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// --- CLI entry point ---------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
function readStdin() {
|
|
101
|
+
return new Promise((resolve) => {
|
|
102
|
+
let data = '';
|
|
103
|
+
process.stdin.setEncoding('utf8');
|
|
104
|
+
process.stdin.on('data', (chunk) => (data += chunk));
|
|
105
|
+
process.stdin.on('end', () => resolve(data));
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
async function main() {
|
|
110
|
+
const raw = await readStdin();
|
|
111
|
+
const parsed = parseHookInput(raw);
|
|
112
|
+
if (!parsed.ok) {
|
|
113
|
+
emitError(`tool-pace-check: ${parsed.error}`);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
const result = evaluate(parsed);
|
|
117
|
+
emitDecision(result.decision, result.reason);
|
|
118
|
+
if (result.decision === DECISIONS.DENY) {
|
|
119
|
+
process.exit(2);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const isMain =
|
|
124
|
+
import.meta.url === `file://${process.argv[1]}` ||
|
|
125
|
+
process.argv[1]?.endsWith('tool-pace-check.js');
|
|
126
|
+
if (isMain) {
|
|
127
|
+
main().catch((err) => emitError(err.stack || err.message));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export default { evaluate };
|