@nexrall/code-core 1.3.1 → 1.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/dist/agent/crossFile.d.ts +42 -0
- package/dist/agent/crossFile.d.ts.map +1 -0
- package/dist/agent/crossFile.js +119 -0
- package/dist/agent/editCompleteness.d.ts +47 -0
- package/dist/agent/editCompleteness.d.ts.map +1 -0
- package/dist/agent/editCompleteness.js +109 -0
- package/dist/agent/flaky.d.ts +47 -0
- package/dist/agent/flaky.d.ts.map +1 -0
- package/dist/agent/flaky.js +79 -0
- package/dist/agent/loop.d.ts +59 -0
- package/dist/agent/loop.d.ts.map +1 -1
- package/dist/agent/loop.js +392 -12
- package/dist/agent/testIntegrity.d.ts +56 -0
- package/dist/agent/testIntegrity.d.ts.map +1 -0
- package/dist/agent/testIntegrity.js +199 -0
- package/dist/api/client.d.ts.map +1 -1
- package/dist/api/client.js +4 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +5 -0
- package/dist/permissions/destructive.d.ts +43 -0
- package/dist/permissions/destructive.d.ts.map +1 -0
- package/dist/permissions/destructive.js +244 -0
- package/dist/tools/executor.d.ts.map +1 -1
- package/dist/tools/executor.js +342 -14
- package/dist/tools/tsLangService.d.ts +21 -0
- package/dist/tools/tsLangService.d.ts.map +1 -0
- package/dist/tools/tsLangService.js +282 -0
- package/package.json +1 -1
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Test-Integrity Guard — deterministic reward-hacking detection.
|
|
4
|
+
*
|
|
5
|
+
* THE PROBLEM (unsolved by every frontier coding agent today):
|
|
6
|
+
* A model asked to "make the tests pass" can satisfy that objective two ways:
|
|
7
|
+
* (a) fix the code so the existing tests pass ← what we want
|
|
8
|
+
* (b) weaken the tests so they pass regardless ← reward hacking
|
|
9
|
+
* (b) includes: deleting assertions, adding `.skip`/`.only`/`xit`, turning a
|
|
10
|
+
* real assertion into a tautology (`assert True`, `expect(true).toBe(true)`),
|
|
11
|
+
* or commenting out / deleting whole test cases. The agent then honestly
|
|
12
|
+
* reports "tests pass" and the user believes the work is done. Frontier agents
|
|
13
|
+
* mitigate this only with prompt instructions ("don't cheat") — there is no
|
|
14
|
+
* deterministic detector. METR / Anthropic sabotage evals confirm every strong
|
|
15
|
+
* model does this measurably under pressure.
|
|
16
|
+
*
|
|
17
|
+
* THE APPROACH:
|
|
18
|
+
* Analyse the EDIT PAYLOAD of any write to a test file (old_string→new_string
|
|
19
|
+
* for edit_file / multi_edit; content for write_file). Deterministically count
|
|
20
|
+
* assertion / test-case / skip-marker deltas. When a test edit's NET effect is
|
|
21
|
+
* "fewer or weaker tests" we flag it — with a concrete, human-readable reason.
|
|
22
|
+
* No model output is trusted; this is pure structural analysis of the diff, so
|
|
23
|
+
* it cannot itself be hallucinated or gamed.
|
|
24
|
+
*
|
|
25
|
+
* This is a heuristic signal, not a proof: legitimate refactors (renaming a test,
|
|
26
|
+
* splitting a file) can reduce counts. So the guard never BLOCKS — it surfaces a
|
|
27
|
+
* one-shot nudge asking the agent to justify the change, and records it in the
|
|
28
|
+
* progress ledger so it survives compaction. That converts a silent, invisible
|
|
29
|
+
* failure into an explicit, reviewable decision.
|
|
30
|
+
*/
|
|
31
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
32
|
+
exports.isTestFile = isTestFile;
|
|
33
|
+
exports.analyzeTestEdit = analyzeTestEdit;
|
|
34
|
+
exports.analyzeWriteToolForTestIntegrity = analyzeWriteToolForTestIntegrity;
|
|
35
|
+
// ── Is this a test file? ──────────────────────────────────────────────────────
|
|
36
|
+
// Covers the common conventions across JS/TS, Python, Go, Java/Kotlin, Ruby,
|
|
37
|
+
// Rust, PHP, C#. Path-based (fast, language-agnostic).
|
|
38
|
+
const TEST_PATH_RE = /(?:^|[\/\\])(?:tests?|spec|__tests__|testing)[\/\\]|(?:\.|_|-)(?:test|spec|tests)\.[a-z]+$|(?:^|[\/\\])test_[^\/\\]+\.py$|(?:^|[\/\\])[^\/\\]+_test\.(?:go|py|rb)$|Test[^\/\\]*\.(?:java|kt|cs)$|(?:^|[\/\\])[^\/\\]*Tests?\.(?:java|kt|cs)$/i;
|
|
39
|
+
function isTestFile(path) {
|
|
40
|
+
if (!path || typeof path !== 'string')
|
|
41
|
+
return false;
|
|
42
|
+
return TEST_PATH_RE.test(path);
|
|
43
|
+
}
|
|
44
|
+
// ── Token patterns ────────────────────────────────────────────────────────────
|
|
45
|
+
// Assertion-bearing tokens across frameworks. We count occurrences, not parse.
|
|
46
|
+
const ASSERTION_RES = [
|
|
47
|
+
/\bexpect\s*\(/g, // jest / chai / vitest / jasmine
|
|
48
|
+
/\bassert(?:Equals?|True|False|That|Null|NotNull|Same)?\b/g, // junit / python unittest / generic
|
|
49
|
+
/\bassert\s*[.(]/g, // node:assert (assert.equal / assert( ) )
|
|
50
|
+
/\.should\b/g, // chai should / rspec should
|
|
51
|
+
/\bshould\s*[.(]/g,
|
|
52
|
+
/\bEXPECT_[A-Z]+\s*\(/g, // gtest
|
|
53
|
+
/\bASSERT_[A-Z]+\s*\(/g, // gtest
|
|
54
|
+
/\brequire\.\w+\s*\(/g, // testify require
|
|
55
|
+
/\bassert\.\w+\s*\(/g, // testify assert
|
|
56
|
+
/\bt\.(?:Error|Fatal|Errorf|Fatalf)\b/g, // go testing
|
|
57
|
+
/\bassert!\s*\(|\bassert_eq!\s*\(|\bassert_ne!\s*\(/g, // rust
|
|
58
|
+
];
|
|
59
|
+
// Test-case declaration tokens (a "test" unit).
|
|
60
|
+
const TESTCASE_RES = [
|
|
61
|
+
/\b(?:it|test)\s*\(/g, // jest / mocha / jasmine
|
|
62
|
+
/\bdef\s+test_\w+/g, // python
|
|
63
|
+
/\bfunc\s+Test\w+/g, // go
|
|
64
|
+
/@Test\b/g, // junit
|
|
65
|
+
/\bfn\s+\w*test\w*\s*\(/gi, // rust (best-effort)
|
|
66
|
+
];
|
|
67
|
+
// Skip / focus markers — presence in NEW but absence in OLD is a strong signal.
|
|
68
|
+
const SKIP_RES = [
|
|
69
|
+
/\b(?:it|test|describe|context|suite)\s*\.\s*(?:skip|only)\b/g,
|
|
70
|
+
/\bx(?:it|describe|test|context)\s*\(/g, // xit / xdescribe
|
|
71
|
+
/\bf(?:it|describe)\s*\(/g, // fit / fdescribe (focus)
|
|
72
|
+
/@pytest\.mark\.skip\b|@pytest\.mark\.skipif\b|@unittest\.skip\b|@skip\b/g,
|
|
73
|
+
/\bpytest\.skip\s*\(/g,
|
|
74
|
+
/\bt\.Skip\s*\(|\bt\.SkipNow\s*\(/g, // go
|
|
75
|
+
/@Disabled\b|@Ignore\b/g, // junit / kotlin
|
|
76
|
+
/\.only\s*\(/g, // test.only leaks CI coverage
|
|
77
|
+
];
|
|
78
|
+
// Tautological assertions — always-true, structurally meaningless.
|
|
79
|
+
const TAUTOLOGY_RES = [
|
|
80
|
+
/\bassert\s+True\b|\bassert\s+1\b|\bassert\s+not\s+False\b/g, // python
|
|
81
|
+
/\bassert\s*\(\s*true\s*\)|\bassert\.ok\s*\(\s*true\s*\)/gi, // node
|
|
82
|
+
/\bexpect\s*\(\s*true\s*\)\s*\.\s*to(?:Be|Equal|BeTruthy)\s*\(\s*true\s*\)?/gi, // jest
|
|
83
|
+
/\bexpect\s*\(\s*(\w+)\s*\)\s*\.\s*toBe\s*\(\s*\1\s*\)/g, // expect(x).toBe(x)
|
|
84
|
+
/\bassert_eq!\s*\(\s*true\s*,\s*true\s*\)/g, // rust
|
|
85
|
+
/\bassert!\s*\(\s*true\s*\)/g,
|
|
86
|
+
];
|
|
87
|
+
function countMatches(res, text) {
|
|
88
|
+
let n = 0;
|
|
89
|
+
for (const re of res) {
|
|
90
|
+
re.lastIndex = 0;
|
|
91
|
+
const m = text.match(re);
|
|
92
|
+
if (m)
|
|
93
|
+
n += m.length;
|
|
94
|
+
}
|
|
95
|
+
return n;
|
|
96
|
+
}
|
|
97
|
+
// Count lines that newly comment out something containing an assertion/test kw.
|
|
98
|
+
function countCommentedOutTestLines(oldText, newText) {
|
|
99
|
+
const oldLines = new Set(oldText.split('\n').map((l) => l.trim()));
|
|
100
|
+
let n = 0;
|
|
101
|
+
for (const raw of newText.split('\n')) {
|
|
102
|
+
const line = raw.trim();
|
|
103
|
+
const isComment = /^(?:\/\/|#|\/\*|\*)/.test(line);
|
|
104
|
+
if (!isComment)
|
|
105
|
+
continue;
|
|
106
|
+
const body = line.replace(/^(?:\/\/+|#+|\/\*+|\*+)\s?/, '');
|
|
107
|
+
// Only count if the uncommented body looks like a test/assertion AND that
|
|
108
|
+
// exact code line existed (uncommented) in the old version.
|
|
109
|
+
const looksLikeTest = /\b(expect|assert|it\(|test\(|def test_|func Test|EXPECT_|ASSERT_)\b/.test(body);
|
|
110
|
+
if (looksLikeTest && oldLines.has(body))
|
|
111
|
+
n += 1;
|
|
112
|
+
}
|
|
113
|
+
return n;
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Analyse a single test-file edit (old fragment → new fragment). For write_file
|
|
117
|
+
* pass ('', fullNewContent) — only additive signals (skips, tautologies) fire.
|
|
118
|
+
*/
|
|
119
|
+
function analyzeTestEdit(path, oldText, newText) {
|
|
120
|
+
if (!isTestFile(path)) {
|
|
121
|
+
return { isTestFile: false, suspicious: false, findings: [] };
|
|
122
|
+
}
|
|
123
|
+
const findings = [];
|
|
124
|
+
const skipsBefore = countMatches(SKIP_RES, oldText);
|
|
125
|
+
const skipsAfter = countMatches(SKIP_RES, newText);
|
|
126
|
+
if (skipsAfter > skipsBefore) {
|
|
127
|
+
findings.push({
|
|
128
|
+
kind: 'skip-added',
|
|
129
|
+
reason: `added ${skipsAfter - skipsBefore} skip/only/disabled marker(s) — tests are being skipped, not fixed`,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const tautoBefore = countMatches(TAUTOLOGY_RES, oldText);
|
|
133
|
+
const tautoAfter = countMatches(TAUTOLOGY_RES, newText);
|
|
134
|
+
if (tautoAfter > tautoBefore) {
|
|
135
|
+
findings.push({
|
|
136
|
+
kind: 'tautology-added',
|
|
137
|
+
reason: `added ${tautoAfter - tautoBefore} always-true assertion(s) (e.g. assert True / expect(x).toBe(x)) — the check no longer tests anything`,
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
const assertBefore = countMatches(ASSERTION_RES, oldText);
|
|
141
|
+
const assertAfter = countMatches(ASSERTION_RES, newText);
|
|
142
|
+
if (assertAfter < assertBefore) {
|
|
143
|
+
findings.push({
|
|
144
|
+
kind: 'assertion-removed',
|
|
145
|
+
reason: `removed ${assertBefore - assertAfter} assertion(s) — the test verifies less than before`,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
const casesBefore = countMatches(TESTCASE_RES, oldText);
|
|
149
|
+
const casesAfter = countMatches(TESTCASE_RES, newText);
|
|
150
|
+
if (casesAfter < casesBefore) {
|
|
151
|
+
findings.push({
|
|
152
|
+
kind: 'testcase-removed',
|
|
153
|
+
reason: `removed ${casesBefore - casesAfter} test case(s)`,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
const commented = countCommentedOutTestLines(oldText, newText);
|
|
157
|
+
if (commented > 0) {
|
|
158
|
+
findings.push({
|
|
159
|
+
kind: 'body-commented',
|
|
160
|
+
reason: `commented out ${commented} line(s) of previously-active test/assertion code`,
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
return { isTestFile: true, suspicious: findings.length > 0, findings };
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Extract the (old, new) text fragments to analyse from a write-tool input.
|
|
167
|
+
* Returns null for non-test files or tools we can't inspect.
|
|
168
|
+
*
|
|
169
|
+
* - edit_file: { old_string, new_string }
|
|
170
|
+
* - multi_edit: concatenate all edits' old / new
|
|
171
|
+
* - write_file: ('', content) — additive-only detection (no prior content here)
|
|
172
|
+
*/
|
|
173
|
+
function analyzeWriteToolForTestIntegrity(toolName, input) {
|
|
174
|
+
if (!input)
|
|
175
|
+
return null;
|
|
176
|
+
const path = typeof input.path === 'string' ? input.path
|
|
177
|
+
: typeof input.file_path === 'string' ? input.file_path
|
|
178
|
+
: '';
|
|
179
|
+
if (!isTestFile(path))
|
|
180
|
+
return null;
|
|
181
|
+
if (toolName === 'edit_file') {
|
|
182
|
+
const oldText = typeof input.old_string === 'string' ? input.old_string : '';
|
|
183
|
+
const newText = typeof input.new_string === 'string' ? input.new_string : '';
|
|
184
|
+
return analyzeTestEdit(path, oldText, newText);
|
|
185
|
+
}
|
|
186
|
+
if (toolName === 'multi_edit' && Array.isArray(input.edits)) {
|
|
187
|
+
const edits = input.edits;
|
|
188
|
+
const oldText = edits.map((e) => (typeof e.old_string === 'string' ? e.old_string : '')).join('\n');
|
|
189
|
+
const newText = edits.map((e) => (typeof e.new_string === 'string' ? e.new_string : '')).join('\n');
|
|
190
|
+
return analyzeTestEdit(path, oldText, newText);
|
|
191
|
+
}
|
|
192
|
+
if (toolName === 'write_file') {
|
|
193
|
+
const content = typeof input.content === 'string' ? input.content : '';
|
|
194
|
+
// No prior content available at this layer → additive-only ('' → content).
|
|
195
|
+
return analyzeTestEdit(path, '', content);
|
|
196
|
+
}
|
|
197
|
+
return null;
|
|
198
|
+
}
|
|
199
|
+
//# sourceMappingURL=testIntegrity.js.map
|
package/dist/api/client.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../src/api/client.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,UAAU,EAA8B,UAAU,EAAE,aAAa,EAAE,MAAM,UAAU,CAAC;AAKrH,eAAO,MAAM,QAAQ,4BAA4B,CAAC;AAiBlD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,GAAG,CAAC,EAAE,UAAU,CAAC;IACjB,aAAa,CAAC,EAAE,aAAa,GAAG,IAAI,CAAC;IACrC,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE;QAAE,OAAO,EAAE,OAAO,CAAA;KAAE,CAAC;IACnC,oFAAoF;IACpF,UAAU,CAAC,EAAE,KAAK,CAAC;QAAE,IAAI,EAAE,MAAM,CAAC;QAAC,WAAW,EAAE,MAAM,CAAC;QAAC,YAAY,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAA;KAAE,CAAC,CAAC;IACjG,6EAA6E;IAC7E,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAWD,wBAAsB,UAAU,CAC9B,QAAQ,EAAE,OAAO,EAAE,EACnB,OAAO,EAAE,iBAAiB,EAC1B,OAAO,EAAE,CAAC,CAAC,EAAE,QAAQ,KAAK,IAAI,GAC7B,OAAO,CAAC,OAAO,CAAC,CA8WlB;AAID,wBAAsB,UAAU,IAAI,OAAO,CAAC,MAAM,CAAC,CAalD;AAID,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAgB1E;AAID,wBAAsB,KAAK,CAAC,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,UAAU,CAAC,CAkBhF"}
|
package/dist/api/client.js
CHANGED
|
@@ -75,6 +75,10 @@ async function streamChat(messages, options, onEvent) {
|
|
|
75
75
|
await sleep(RETRY_BASE_MS * Math.pow(2, attempt));
|
|
76
76
|
continue;
|
|
77
77
|
}
|
|
78
|
+
// 4xx (incl. 413 Payload Too Large, 400 Bad Request, 402 Insufficient balance)
|
|
79
|
+
// are DETERMINISTIC given the same body — retrying resends the identical oversized
|
|
80
|
+
// conversation and can never succeed. Fall through to the non-retryable !response.ok
|
|
81
|
+
// handler below instead of burning retries (this was the 16-request "retry storm").
|
|
78
82
|
break; // success or non-retryable error
|
|
79
83
|
}
|
|
80
84
|
catch (err) {
|
package/dist/index.d.ts
CHANGED
|
@@ -3,6 +3,10 @@ export * from './auth/index';
|
|
|
3
3
|
export * from './api/client';
|
|
4
4
|
export * from './tools/executor';
|
|
5
5
|
export * from './agent/loop';
|
|
6
|
+
export * from './agent/testIntegrity';
|
|
7
|
+
export * from './agent/editCompleteness';
|
|
8
|
+
export * from './agent/crossFile';
|
|
9
|
+
export * from './agent/flaky';
|
|
6
10
|
export * from './mcp/client';
|
|
7
11
|
export * from './mcp/httpClient';
|
|
8
12
|
export * from './mcp/manager';
|
|
@@ -10,6 +14,7 @@ export * from './checkpoint/manager';
|
|
|
10
14
|
export * from './commands/loader';
|
|
11
15
|
export * from './agent/agentTypes';
|
|
12
16
|
export * from './permissions/rules';
|
|
17
|
+
export * from './permissions/destructive';
|
|
13
18
|
export * from './plugins/index';
|
|
14
19
|
export * from './plugins/installer';
|
|
15
20
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,SAAS,CAAC;AACxB,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,cAAc,CAAC;AAC7B,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,mBAAmB,CAAC;AAClC,cAAc,eAAe,CAAC;AAC9B,cAAc,cAAc,CAAC;AAC7B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,sBAAsB,CAAC;AACrC,cAAc,mBAAmB,CAAC;AAClC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,2BAA2B,CAAC;AAC1C,cAAc,iBAAiB,CAAC;AAChC,cAAc,qBAAqB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -19,6 +19,10 @@ __exportStar(require("./auth/index"), exports);
|
|
|
19
19
|
__exportStar(require("./api/client"), exports);
|
|
20
20
|
__exportStar(require("./tools/executor"), exports);
|
|
21
21
|
__exportStar(require("./agent/loop"), exports);
|
|
22
|
+
__exportStar(require("./agent/testIntegrity"), exports);
|
|
23
|
+
__exportStar(require("./agent/editCompleteness"), exports);
|
|
24
|
+
__exportStar(require("./agent/crossFile"), exports);
|
|
25
|
+
__exportStar(require("./agent/flaky"), exports);
|
|
22
26
|
__exportStar(require("./mcp/client"), exports);
|
|
23
27
|
__exportStar(require("./mcp/httpClient"), exports);
|
|
24
28
|
__exportStar(require("./mcp/manager"), exports);
|
|
@@ -26,6 +30,7 @@ __exportStar(require("./checkpoint/manager"), exports);
|
|
|
26
30
|
__exportStar(require("./commands/loader"), exports);
|
|
27
31
|
__exportStar(require("./agent/agentTypes"), exports);
|
|
28
32
|
__exportStar(require("./permissions/rules"), exports);
|
|
33
|
+
__exportStar(require("./permissions/destructive"), exports);
|
|
29
34
|
__exportStar(require("./plugins/index"), exports);
|
|
30
35
|
__exportStar(require("./plugins/installer"), exports);
|
|
31
36
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Destructive-command classifier.
|
|
3
|
+
*
|
|
4
|
+
* `rm -rf /`, `dd`, `mkfs` are DENIED outright by executor.ts BLOCKED_REGEXES —
|
|
5
|
+
* they are never legitimate. This module covers the OTHER class: commands that
|
|
6
|
+
* are perfectly legitimate but IRREVERSIBLE and high-blast-radius — dropping a
|
|
7
|
+
* database, force-pushing over history, `terraform destroy`, `kubectl delete`,
|
|
8
|
+
* `aws s3 rb`, `docker system prune`. These must never run silently.
|
|
9
|
+
*
|
|
10
|
+
* This is the class of action that has caused real production incidents (an
|
|
11
|
+
* agent in "auto-approve everything" mode wiping a prod database). So a positive
|
|
12
|
+
* classification here forces an EXPLICIT confirmation that CANNOT be bypassed by
|
|
13
|
+
* a blanket "approve all" / auto-approve session flag — the user must confirm
|
|
14
|
+
* THIS specific command.
|
|
15
|
+
*
|
|
16
|
+
* Matching runs against the command with heredoc bodies and quoted strings
|
|
17
|
+
* stripped (stripDataSections), so a commit message, README, or echo that merely
|
|
18
|
+
* MENTIONS "DROP TABLE" or "terraform destroy" does not trip the guard — only a
|
|
19
|
+
* real command position does. This mirrors the tier-2 approach in executor.ts
|
|
20
|
+
* that eliminated the false-positive storms.
|
|
21
|
+
*/
|
|
22
|
+
export interface DestructiveMatch {
|
|
23
|
+
/** Coarse category, e.g. 'database' | 'git-history' | 'infra' | 'cloud' | 'container'. */
|
|
24
|
+
category: string;
|
|
25
|
+
/** Short human-readable reason shown in the confirmation prompt. */
|
|
26
|
+
reason: string;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
30
|
+
* null when the command is not recognised as destructive.
|
|
31
|
+
*
|
|
32
|
+
* Two passes with different data-stripping:
|
|
33
|
+
* - RAW_RULES run on the heredoc-stripped but quote-PRESERVING string, because
|
|
34
|
+
* real SQL lives inside quotes (`psql -c "DROP TABLE"`); they are gated on a
|
|
35
|
+
* DB-client token so a mention can't match.
|
|
36
|
+
* - STRIPPED_RULES run on the fully stripped string (quotes + heredocs gone) so
|
|
37
|
+
* mentions inside commit messages / echoes / grep patterns / heredoc'd docs
|
|
38
|
+
* never trip the bare-command rules (git/infra/cloud/container/publish).
|
|
39
|
+
*/
|
|
40
|
+
export declare function classifyDestructiveCommand(command: string): DestructiveMatch | null;
|
|
41
|
+
/** True when the given tool + input represents a destructive bash command. */
|
|
42
|
+
export declare function isDestructiveBash(tool: string, input: Record<string, unknown>): DestructiveMatch | null;
|
|
43
|
+
//# sourceMappingURL=destructive.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"destructive.d.ts","sourceRoot":"","sources":["../../src/permissions/destructive.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,MAAM,WAAW,gBAAgB;IAC/B,0FAA0F;IAC1F,QAAQ,EAAE,MAAM,CAAC;IACjB,oEAAoE;IACpE,MAAM,EAAE,MAAM,CAAC;CAChB;AAsMD;;;;;;;;;;;GAWG;AACH,wBAAgB,0BAA0B,CAAC,OAAO,EAAE,MAAM,GAAG,gBAAgB,GAAG,IAAI,CAenF;AAED,8EAA8E;AAC9E,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,gBAAgB,GAAG,IAAI,CAQvG"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Destructive-command classifier.
|
|
4
|
+
*
|
|
5
|
+
* `rm -rf /`, `dd`, `mkfs` are DENIED outright by executor.ts BLOCKED_REGEXES —
|
|
6
|
+
* they are never legitimate. This module covers the OTHER class: commands that
|
|
7
|
+
* are perfectly legitimate but IRREVERSIBLE and high-blast-radius — dropping a
|
|
8
|
+
* database, force-pushing over history, `terraform destroy`, `kubectl delete`,
|
|
9
|
+
* `aws s3 rb`, `docker system prune`. These must never run silently.
|
|
10
|
+
*
|
|
11
|
+
* This is the class of action that has caused real production incidents (an
|
|
12
|
+
* agent in "auto-approve everything" mode wiping a prod database). So a positive
|
|
13
|
+
* classification here forces an EXPLICIT confirmation that CANNOT be bypassed by
|
|
14
|
+
* a blanket "approve all" / auto-approve session flag — the user must confirm
|
|
15
|
+
* THIS specific command.
|
|
16
|
+
*
|
|
17
|
+
* Matching runs against the command with heredoc bodies and quoted strings
|
|
18
|
+
* stripped (stripDataSections), so a commit message, README, or echo that merely
|
|
19
|
+
* MENTIONS "DROP TABLE" or "terraform destroy" does not trip the guard — only a
|
|
20
|
+
* real command position does. This mirrors the tier-2 approach in executor.ts
|
|
21
|
+
* that eliminated the false-positive storms.
|
|
22
|
+
*/
|
|
23
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
24
|
+
exports.classifyDestructiveCommand = classifyDestructiveCommand;
|
|
25
|
+
exports.isDestructiveBash = isDestructiveBash;
|
|
26
|
+
// ── Data-section stripping (naive; feeds a heuristic, not an executor) ────────
|
|
27
|
+
function stripHeredocs(command) {
|
|
28
|
+
return command.replace(/<<-?\s*(['"]?)(\w+)\1[\s\S]*?\n\2(?=\s|;|&|$)/g, ' ');
|
|
29
|
+
}
|
|
30
|
+
function stripDataSections(command) {
|
|
31
|
+
return stripHeredocs(command)
|
|
32
|
+
.replace(/"(?:[^"\\]|\\[\s\S])*"/g, '""')
|
|
33
|
+
.replace(/'[^']*'/g, "''");
|
|
34
|
+
}
|
|
35
|
+
// A command "position" is start-of-string/line or right after a shell separator
|
|
36
|
+
// (; & | && || ` ( $( ) or a privilege / interpreter prefix. Reused as a prefix
|
|
37
|
+
// so rules only fire on a real invocation, never on an argument or mention.
|
|
38
|
+
const CMD_POS = '(?:^|[;&|`\\n(]|\\$\\(|\\bsudo\\s+|\\bdoas\\s+|\\bexec\\s+|\\bnohup\\s+|\\bxargs\\s+(?:-\\S+\\s+)*|\\btime\\s+|\\bnice\\s+(?:-\\S+\\s+)*)\\s*';
|
|
39
|
+
// SQL/DB clients that indicate the surrounding text is EXECUTED SQL, not a
|
|
40
|
+
// mention. Real SQL almost always lives inside quotes (`psql -c "DROP TABLE"`),
|
|
41
|
+
// so DB rules run on the quote-PRESERVING string (RAW_RULES) — but they must be
|
|
42
|
+
// gated on one of these client tokens at a command position, otherwise a
|
|
43
|
+
// `git commit -m "drop table logic"` mention would trip them.
|
|
44
|
+
const DB_CLIENT = '(?:psql|mysql|mariadb|mysqldump|mongo|mongosh|sqlite3|cockroach|clickhouse-client|pgcli|mycli|usql|sqlcmd)';
|
|
45
|
+
// ── DB rules: matched on the heredoc-stripped (but quote-PRESERVING) string ──
|
|
46
|
+
// Each is gated on a DB client so mentions in commit messages / echoes / grep
|
|
47
|
+
// patterns (which get quote-stripped for the STRIPPED_RULES pass) don't match.
|
|
48
|
+
const RAW_RULES = [
|
|
49
|
+
{
|
|
50
|
+
category: 'database',
|
|
51
|
+
reason: 'drops a database, table, or schema (irreversible data loss)',
|
|
52
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdrop\s+(?:database|schema|table)\b`, 'i'),
|
|
53
|
+
},
|
|
54
|
+
{
|
|
55
|
+
category: 'database',
|
|
56
|
+
reason: 'truncates a table (deletes all rows, irreversible)',
|
|
57
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\btruncate\s+(?:table\s+)?\w`, 'i'),
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
category: 'database',
|
|
61
|
+
reason: 'DELETE without a WHERE clause (deletes every row)',
|
|
62
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdelete\s+from\s+[\w.` + '`' + String.raw `"]+\s*(?:;|"|'|$)(?![\s\S]*\bwhere\b)`, 'i'),
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
category: 'database',
|
|
66
|
+
reason: 'drops a Postgres/MySQL role or user',
|
|
67
|
+
re: new RegExp(CMD_POS + DB_CLIENT + String.raw `\b[\s\S]*?\bdrop\s+(?:role|user)\b`, 'i'),
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
category: 'database',
|
|
71
|
+
reason: 'drops a MongoDB database or collection',
|
|
72
|
+
re: /\b(?:mongo|mongosh)\b[\s\S]*?\.(?:drop|dropDatabase)\s*\(\s*\)/i,
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
category: 'database',
|
|
76
|
+
reason: 'flushes all Redis keys (wipes the datastore)',
|
|
77
|
+
re: new RegExp(CMD_POS + String.raw `redis-cli\b[\s\S]*?\bflush(?:all|db)\b`, 'i'),
|
|
78
|
+
},
|
|
79
|
+
];
|
|
80
|
+
// Each `re` is expected to be anchored with CMD_POS where relevant. Case-insensitive.
|
|
81
|
+
const STRIPPED_RULES = [
|
|
82
|
+
// ── Migration reset verbs (bare commands, not quoted SQL) ───────────────────
|
|
83
|
+
{
|
|
84
|
+
category: 'database',
|
|
85
|
+
reason: 'reverses or resets database migrations (may drop columns/tables)',
|
|
86
|
+
re: new RegExp(CMD_POS + String.raw `(?:rails\s+db:(?:drop|reset)|rake\s+db:(?:drop|reset)|(?:npx\s+)?prisma\s+migrate\s+reset|(?:npx\s+)?sequelize(?:-cli)?\s+db:drop|python\s+manage\.py\s+flush|knex\s+migrate:rollback\s+--all|alembic\s+downgrade\s+base)`, 'i'),
|
|
87
|
+
},
|
|
88
|
+
// ── Git history / remotes ───────────────────────────────────────────────────
|
|
89
|
+
{
|
|
90
|
+
category: 'git-history',
|
|
91
|
+
reason: 'force-pushes (can overwrite/destroy remote history)',
|
|
92
|
+
re: /\bgit\s+push\b[^\n]*\s(?:--force\b(?!-with-lease)|-f\b)/i,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
category: 'git-history',
|
|
96
|
+
reason: 'deletes a remote branch',
|
|
97
|
+
re: /\bgit\s+push\b[^\n]*\s:(?:refs\/heads\/)?\S/i,
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
category: 'git-history',
|
|
101
|
+
reason: 'hard-resets the working tree (discards uncommitted work)',
|
|
102
|
+
re: /\bgit\s+reset\s+(?:--hard|--keep)\b/i,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
category: 'git-history',
|
|
106
|
+
reason: 'deletes untracked files (git clean)',
|
|
107
|
+
re: /\bgit\s+clean\s+-[a-z]*f/i,
|
|
108
|
+
},
|
|
109
|
+
{
|
|
110
|
+
category: 'git-history',
|
|
111
|
+
reason: 'force-deletes a git branch',
|
|
112
|
+
re: /\bgit\s+branch\s+-[a-z]*D/i,
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
category: 'git-history',
|
|
116
|
+
reason: 'rewrites git history (filter-branch/filter-repo)',
|
|
117
|
+
re: /\bgit\s+(?:filter-branch|filter-repo)\b/i,
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
category: 'git-history',
|
|
121
|
+
reason: 'reflog expire / gc prune (permanently drops recoverable commits)',
|
|
122
|
+
re: /\bgit\s+(?:reflog\s+expire|gc\s+[^\n]*--prune=(?:now|all))\b/i,
|
|
123
|
+
},
|
|
124
|
+
// ── Infrastructure as code ──────────────────────────────────────────────────
|
|
125
|
+
{
|
|
126
|
+
category: 'infra',
|
|
127
|
+
reason: 'destroys Terraform-managed infrastructure',
|
|
128
|
+
re: /\bterraform\s+(?:destroy|apply)\b[^\n]*(?:-auto-approve|destroy)/i,
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
category: 'infra',
|
|
132
|
+
reason: 'terraform destroy',
|
|
133
|
+
re: /\bterraform\s+destroy\b/i,
|
|
134
|
+
},
|
|
135
|
+
{
|
|
136
|
+
category: 'infra',
|
|
137
|
+
reason: 'destroys Pulumi-managed infrastructure',
|
|
138
|
+
re: /\bpulumi\s+destroy\b/i,
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
category: 'infra',
|
|
142
|
+
reason: 'deletes Kubernetes resources',
|
|
143
|
+
re: /\bkubectl\s+delete\b/i,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
category: 'infra',
|
|
147
|
+
reason: 'helm uninstall / delete (removes a release)',
|
|
148
|
+
re: /\bhelm\s+(?:uninstall|delete)\b/i,
|
|
149
|
+
},
|
|
150
|
+
// ── Cloud CLIs ──────────────────────────────────────────────────────────────
|
|
151
|
+
{
|
|
152
|
+
category: 'cloud',
|
|
153
|
+
reason: 'removes an S3 bucket or recursively deletes objects',
|
|
154
|
+
re: /\baws\s+s3\s+(?:rb\b|rm\b[^\n]*--recursive)/i,
|
|
155
|
+
},
|
|
156
|
+
{
|
|
157
|
+
category: 'cloud',
|
|
158
|
+
reason: 'deletes/terminates an AWS resource',
|
|
159
|
+
re: /\baws\s+\w[\w-]*\s+(?:delete-\w+|terminate-instances|delete\b|remove-\w+)/i,
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
category: 'cloud',
|
|
163
|
+
reason: 'deletes a GCP resource',
|
|
164
|
+
re: /\bgcloud\s+[\w-]+(?:\s+[\w-]+)*\s+delete\b/i,
|
|
165
|
+
},
|
|
166
|
+
{
|
|
167
|
+
category: 'cloud',
|
|
168
|
+
reason: 'deletes an Azure resource',
|
|
169
|
+
re: /\baz\s+[\w-]+(?:\s+[\w-]+)*\s+delete\b/i,
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
category: 'cloud',
|
|
173
|
+
reason: 'gsutil recursive remove (deletes cloud objects/buckets)',
|
|
174
|
+
re: /\bgsutil\s+(?:-m\s+)?rm\b[^\n]*-[a-z]*r/i,
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
category: 'cloud',
|
|
178
|
+
reason: 'wrangler delete (removes a Cloudflare Worker/resource)',
|
|
179
|
+
re: /\bwrangler\s+(?:delete|d1\s+delete|r2\s+bucket\s+delete|kv:namespace\s+delete)\b/i,
|
|
180
|
+
},
|
|
181
|
+
// ── Containers / volumes ────────────────────────────────────────────────────
|
|
182
|
+
{
|
|
183
|
+
category: 'container',
|
|
184
|
+
reason: 'docker system/volume prune (removes volumes & data)',
|
|
185
|
+
re: /\bdocker\s+(?:system|volume|image|container)\s+prune\b/i,
|
|
186
|
+
},
|
|
187
|
+
{
|
|
188
|
+
category: 'container',
|
|
189
|
+
reason: 'removes docker volumes (data loss)',
|
|
190
|
+
re: /\bdocker\s+volume\s+rm\b/i,
|
|
191
|
+
},
|
|
192
|
+
{
|
|
193
|
+
category: 'container',
|
|
194
|
+
reason: 'docker-compose down with -v (deletes named volumes)',
|
|
195
|
+
re: /\bdocker[\s-]compose\s+down\b[^\n]*\s-v\b|--volumes\b/i,
|
|
196
|
+
},
|
|
197
|
+
// ── Irreversible publishes ──────────────────────────────────────────────────
|
|
198
|
+
{
|
|
199
|
+
category: 'publish',
|
|
200
|
+
reason: 'publishes a package to a public registry (cannot be unpublished)',
|
|
201
|
+
re: new RegExp(CMD_POS + String.raw `(?:npm\s+publish|yarn\s+publish|pnpm\s+publish|cargo\s+publish|twine\s+upload|gem\s+push)\b`, 'i'),
|
|
202
|
+
},
|
|
203
|
+
];
|
|
204
|
+
/**
|
|
205
|
+
* Classify a bash command. Returns the FIRST matching destructive category, or
|
|
206
|
+
* null when the command is not recognised as destructive.
|
|
207
|
+
*
|
|
208
|
+
* Two passes with different data-stripping:
|
|
209
|
+
* - RAW_RULES run on the heredoc-stripped but quote-PRESERVING string, because
|
|
210
|
+
* real SQL lives inside quotes (`psql -c "DROP TABLE"`); they are gated on a
|
|
211
|
+
* DB-client token so a mention can't match.
|
|
212
|
+
* - STRIPPED_RULES run on the fully stripped string (quotes + heredocs gone) so
|
|
213
|
+
* mentions inside commit messages / echoes / grep patterns / heredoc'd docs
|
|
214
|
+
* never trip the bare-command rules (git/infra/cloud/container/publish).
|
|
215
|
+
*/
|
|
216
|
+
function classifyDestructiveCommand(command) {
|
|
217
|
+
if (!command || typeof command !== 'string')
|
|
218
|
+
return null;
|
|
219
|
+
const raw = stripHeredocs(command);
|
|
220
|
+
for (const rule of RAW_RULES) {
|
|
221
|
+
if (rule.re.test(raw)) {
|
|
222
|
+
return { category: rule.category, reason: rule.reason };
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const stripped = stripDataSections(command);
|
|
226
|
+
for (const rule of STRIPPED_RULES) {
|
|
227
|
+
if (rule.re.test(stripped)) {
|
|
228
|
+
return { category: rule.category, reason: rule.reason };
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return null;
|
|
232
|
+
}
|
|
233
|
+
/** True when the given tool + input represents a destructive bash command. */
|
|
234
|
+
function isDestructiveBash(tool, input) {
|
|
235
|
+
if (tool !== 'bash' && tool !== 'run_command' && tool !== 'execute_command')
|
|
236
|
+
return null;
|
|
237
|
+
const command = typeof input.command === 'string'
|
|
238
|
+
? input.command
|
|
239
|
+
: typeof input.cmd === 'string'
|
|
240
|
+
? input.cmd
|
|
241
|
+
: '';
|
|
242
|
+
return classifyDestructiveCommand(command);
|
|
243
|
+
}
|
|
244
|
+
//# sourceMappingURL=destructive.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"executor.d.ts","sourceRoot":"","sources":["../../src/tools/executor.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,UAAU,CAAC;AAC3C,OAAO,EAAyB,KAAK,aAAa,EAAE,MAAM,WAAW,CAAC;AAy8DtE,wBAAsB,WAAW,CAC/B,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC9B,WAAW,CAAC,EAAE;IAAE,OAAO,EAAE,OAAO,CAAA;CAAE,EAClC,OAAO,CAAC,EAAE,aAAa,EACvB,OAAO,CAAC,EAAE,MAAM,EAChB,UAAU,CAAC,EAAE,MAAM,GAClB,OAAO,CAAC,UAAU,CAAC,CAiBrB"}
|