@ps-neko/nekowork 0.2.0-alpha.6 → 0.2.0-alpha.8
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 +27 -8
- package/package.json +9 -3
- package/scripts/cli.js +18 -8
- package/scripts/core/git-mutation-guard.js +65 -0
- package/scripts/lib/acceptance-criteria.js +2 -9
- package/scripts/lib/ast/analyze.js +986 -0
- package/scripts/lib/ast/parse.js +131 -0
- package/scripts/lib/decision.js +4 -58
- package/scripts/lib/diff-parser.js +75 -4
- package/scripts/lib/risk-classifier.js +1 -0
- package/scripts/lib/rules/_helpers.js +90 -10
- package/scripts/lib/rules/ast-dataflow.js +103 -0
- package/scripts/lib/rules/auto-apply-commit-push.js +44 -0
- package/scripts/lib/rules/command-injection.js +72 -0
- package/scripts/lib/rules/cors-wildcard.js +84 -0
- package/scripts/lib/rules/eval-usage.js +102 -0
- package/scripts/lib/rules/hardcoded-credential.js +134 -2
- package/scripts/lib/rules/insecure-tls.js +86 -0
- package/scripts/lib/rules/package-lockfile-risk.js +23 -0
- package/scripts/lib/rules/secret-fallback.js +206 -24
- package/scripts/lib/rules/sql-injection.js +68 -0
- package/scripts/lib/rules/test-or-security-disable.js +102 -0
- package/scripts/lib/session-constants.js +30 -0
- package/scripts/lib/session-io.js +81 -0
- package/scripts/lib/session-resolver.js +17 -0
- package/scripts/lib/verify-helpers.js +442 -0
- package/scripts/orchestrators/_handoff-utils.js +45 -0
- package/scripts/orchestrators/apply.js +33 -17
- package/scripts/orchestrators/gate.js +17 -18
- package/scripts/orchestrators/report.js +4 -48
- package/scripts/orchestrators/verify-pr.js +49 -313
- package/scripts/benchmark/capture-live-ai-diff.js +0 -230
- package/scripts/benchmark/rules.js +0 -214
- package/scripts/benchmark/scrape-oss-positives.js +0 -237
- package/scripts/benchmark/verify-candidates.js +0 -110
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 HARNESS contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -4,9 +4,21 @@
|
|
|
4
4
|
|
|
5
5
|
AI can write 100 lines in 10 seconds. Who checks them before they hit `main`?
|
|
6
6
|
|
|
7
|
-
This package reviews every change your AI tool makes, flags
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
This package reviews every change your AI tool makes, **flags a defined set of
|
|
8
|
+
AI-introduced risk patterns** (11 deterministic rules: secrets, secret fallbacks,
|
|
9
|
+
hardcoded credentials, auto-push/commit, test/security disables, risky package
|
|
10
|
+
hooks, eval, insecure TLS, CORS wildcard, basic SQL/command injection, and AST
|
|
11
|
+
dataflow taint for variable-mediated injection) and routes everything else to a
|
|
12
|
+
human decision. It is **not an exhaustive security audit** — the AST rule is
|
|
13
|
+
intraprocedural (single-function, JS/TS); cross-function and whole-program dataflow
|
|
14
|
+
are out of scope. The verdict is deterministic (same diff, same result), and it never
|
|
15
|
+
commits, pushes, or deploys on its own. **You** make the final call.
|
|
16
|
+
|
|
17
|
+
> Note: the published `@alpha` (0.2.0-alpha.7) now ships all **11 rules** described
|
|
18
|
+
> above (incl. eval, insecure TLS, CORS wildcard, SQL/command injection, AST dataflow)
|
|
19
|
+
> and adds **one tiny, well-known dependency** (`acorn`, the JS parser — MIT, zero
|
|
20
|
+
> transitive dependencies) for the AST engine. Always install with the **`@alpha`**
|
|
21
|
+
> tag: the `latest` dist-tag is a stale `0.2.0-alpha.0` (5 rules, zero deps).
|
|
10
22
|
|
|
11
23
|
## Status
|
|
12
24
|
|
|
@@ -33,14 +45,21 @@ npx -y @ps-neko/nekowork@alpha verify-pr # scan the diff → get a verdict
|
|
|
33
45
|
`verify-pr` reads the diff, writes a plain-English `REPORT.md`, and tells you
|
|
34
46
|
whether the change is safe to merge.
|
|
35
47
|
|
|
36
|
-
## The
|
|
48
|
+
## The verbs
|
|
49
|
+
|
|
50
|
+
**Primary — the 1.0 front surface. Start here:**
|
|
37
51
|
|
|
38
52
|
| Verb | What it does |
|
|
39
53
|
|---|---|
|
|
40
54
|
| `check` | Probe environment readiness (Node version, git repo, etc.) |
|
|
41
|
-
| `verify-pr` | Scan working-tree diff. Produce REPORT.md + .nekowork/decision.json |
|
|
42
|
-
|
|
43
|
-
|
|
55
|
+
| `verify-pr` | Scan the working-tree diff. Produce REPORT.md + .nekowork/decision.json |
|
|
56
|
+
|
|
57
|
+
**Compatibility — session-based (legacy/advanced; not needed for the normal flow):**
|
|
58
|
+
|
|
59
|
+
| Verb | What it does |
|
|
60
|
+
|---|---|
|
|
61
|
+
| `report --session <id>` | Render that session's evidence to REPORT.md. The normal `verify-pr` path already writes REPORT.md directly — you don't need `report` for it. |
|
|
62
|
+
| `apply --session <id>` | Apply a stored `.diff`. Requires a completed work cycle (SHIP_READY marker + cleared Human Gate). NOT driven by verify-pr's decision.json. See [ADVANCED.md](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/ADVANCED.md). |
|
|
44
63
|
|
|
45
64
|
Anything else (`ask`, `plan`, `team`, `work`, `ship`, `build`, `auto`,
|
|
46
65
|
`pr-prep`, `review`, ...) belongs to `@ps-neko/nekowork-harness` (legacy and
|
|
@@ -66,7 +85,7 @@ step — it is not triggered by `decision.json`.
|
|
|
66
85
|
|
|
67
86
|
- [Quickstart](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/QUICKSTART.md)
|
|
68
87
|
- [How verification works](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/SCOPE-1.0.md)
|
|
69
|
-
- [Benchmark](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/BENCHMARK.md) —
|
|
88
|
+
- [Benchmark](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/BENCHMARK.md) — 11 rules, 184/184 (100%) recall, 0/120 FP; 30 real OSS positives on `secret-fallback`, the newer rules (incl. sql/command injection and `ast-dataflow`) are synthetic-only
|
|
70
89
|
- [Integration](https://github.com/Ps-Neko/NEKOWORK/blob/main/packages/nekowork-cli/docs/INTEGRATION.md)
|
|
71
90
|
|
|
72
91
|
## License
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ps-neko/nekowork",
|
|
3
|
-
"version": "0.2.0-alpha.
|
|
3
|
+
"version": "0.2.0-alpha.8",
|
|
4
4
|
"description": "Local verification gate for AI-written code diffs. Deterministic rules decide the verdict, never the LLM. No auto-commit, push, or deploy — you decide at the Human Gate.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-code-review",
|
|
@@ -33,7 +33,11 @@
|
|
|
33
33
|
"nekowork": "scripts/cli.js"
|
|
34
34
|
},
|
|
35
35
|
"files": [
|
|
36
|
-
"scripts/",
|
|
36
|
+
"scripts/cli.js",
|
|
37
|
+
"scripts/check.js",
|
|
38
|
+
"scripts/core/",
|
|
39
|
+
"scripts/lib/",
|
|
40
|
+
"scripts/orchestrators/",
|
|
37
41
|
"README.md",
|
|
38
42
|
"LICENSE"
|
|
39
43
|
],
|
|
@@ -43,5 +47,7 @@
|
|
|
43
47
|
"test": "node --test tests/unit/*.test.js",
|
|
44
48
|
"bench:rules": "node scripts/benchmark/rules.js"
|
|
45
49
|
},
|
|
46
|
-
"dependencies": {
|
|
50
|
+
"dependencies": {
|
|
51
|
+
"acorn": "^8.16.0"
|
|
52
|
+
}
|
|
47
53
|
}
|
package/scripts/cli.js
CHANGED
|
@@ -13,8 +13,6 @@ import {
|
|
|
13
13
|
verifyPrCycle,
|
|
14
14
|
parseVerifyPrArgs,
|
|
15
15
|
printVerifyPrSummary,
|
|
16
|
-
EXIT_CODE,
|
|
17
|
-
VERDICT,
|
|
18
16
|
} from './orchestrators/verify-pr.js';
|
|
19
17
|
|
|
20
18
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
@@ -38,6 +36,7 @@ verify-pr options:
|
|
|
38
36
|
--from-staged scan staged diff
|
|
39
37
|
--range <baseSha...head> scan commit range
|
|
40
38
|
--from-patch <file> scan a patch file
|
|
39
|
+
--full-scan scan the whole tree (onboarding; no PR/diff yet)
|
|
41
40
|
--include <path> force-scan a path even if gitignored
|
|
42
41
|
--comment-file <path> write a PR-comment markdown
|
|
43
42
|
--ci-exit-soft NEEDS_HUMAN_REVIEW / INSUFFICIENT_EVIDENCE → exit 0
|
|
@@ -93,7 +92,20 @@ async function runInternal(verb, rest) {
|
|
|
93
92
|
|
|
94
93
|
if (verb === 'verify-pr') {
|
|
95
94
|
const opts = parseVerifyPrArgs(rest);
|
|
96
|
-
|
|
95
|
+
let result;
|
|
96
|
+
try {
|
|
97
|
+
result = await verifyPrCycle(opts);
|
|
98
|
+
} catch (e) {
|
|
99
|
+
const msg = String(e?.message || e);
|
|
100
|
+
if (/not a git repository/i.test(msg)) {
|
|
101
|
+
console.error('verify-pr could not find a git repository here.');
|
|
102
|
+
console.error(' Run it inside a git repo, or scan a patch file instead:');
|
|
103
|
+
console.error(' nekowork verify-pr --from-patch <file>');
|
|
104
|
+
process.exit(2);
|
|
105
|
+
}
|
|
106
|
+
console.error(msg);
|
|
107
|
+
process.exit(1);
|
|
108
|
+
}
|
|
97
109
|
if (opts.json) {
|
|
98
110
|
console.log(JSON.stringify({
|
|
99
111
|
decision: result.decision,
|
|
@@ -104,11 +116,9 @@ async function runInternal(verb, rest) {
|
|
|
104
116
|
} else {
|
|
105
117
|
printVerifyPrSummary(result);
|
|
106
118
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
process.exit(exitCode);
|
|
119
|
+
// Single source of truth: verifyPrCycle already computed exitCode and
|
|
120
|
+
// honored --ci-exit-soft. Do not recompute here.
|
|
121
|
+
process.exit(result.exitCode ?? 1);
|
|
112
122
|
}
|
|
113
123
|
|
|
114
124
|
if (verb === 'report') {
|
|
@@ -47,6 +47,71 @@ export async function withGitMutationGuard(root, fn, options = {}) {
|
|
|
47
47
|
return result;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Synchronous mutation guard for `apply`: the apply step DOES legitimately
|
|
52
|
+
* mutate the working tree, so a blunt before≠after check is wrong here. Instead
|
|
53
|
+
* we sanction the EXPECTED files (the diff's own files) and reject only changes
|
|
54
|
+
* to git-tracked paths OUTSIDE that set — i.e. an unexpected EXTRA mutation
|
|
55
|
+
* (a stray commit/checkout/branch op or an edit to an unrelated file the apply
|
|
56
|
+
* was not supposed to touch).
|
|
57
|
+
*
|
|
58
|
+
* Behavior-preserving: when only the expected files changed (the normal case)
|
|
59
|
+
* this returns fn()'s result unchanged. No-op outside a git worktree or when
|
|
60
|
+
* `allowEnvKey` is set to '1'.
|
|
61
|
+
*
|
|
62
|
+
* @param {string} root
|
|
63
|
+
* @param {() => T} fn synchronous function performing the apply
|
|
64
|
+
* @param {object} [options]
|
|
65
|
+
* @param {string[]} [options.expectedPaths] repo-relative paths the apply may touch
|
|
66
|
+
* @param {string} [options.label]
|
|
67
|
+
* @param {string} [options.allowEnvKey]
|
|
68
|
+
* @param {NodeJS.ProcessEnv} [options.env]
|
|
69
|
+
* @returns {T}
|
|
70
|
+
* @template T
|
|
71
|
+
*/
|
|
72
|
+
export function withGitMutationGuardSync(root, fn, options = {}) {
|
|
73
|
+
const label = options.label || 'apply';
|
|
74
|
+
const allowEnvKey = options.allowEnvKey || 'HARNESS_ALLOW_WORKSPACE_MUTATION';
|
|
75
|
+
const env = options.env || process.env;
|
|
76
|
+
const expected = new Set((options.expectedPaths || []).map(p => String(p).split('\\').join('/')));
|
|
77
|
+
|
|
78
|
+
if (!root || env[allowEnvKey] === '1' || !isGitWorkTree(root)) {
|
|
79
|
+
return fn();
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const beforeSet = changedPathSet(root);
|
|
83
|
+
const result = fn();
|
|
84
|
+
const afterSet = changedPathSet(root);
|
|
85
|
+
|
|
86
|
+
const unexpected = [...afterSet].filter(p => !beforeSet.has(p) && !expected.has(p));
|
|
87
|
+
if (unexpected.length) {
|
|
88
|
+
throw new Error([
|
|
89
|
+
`${label} produced unexpected git changes outside the applied diff.`,
|
|
90
|
+
`Set ${allowEnvKey}=1 only when these extra mutations are intentional.`,
|
|
91
|
+
'',
|
|
92
|
+
'Unexpected paths:',
|
|
93
|
+
...unexpected.map(p => ` ${p}`),
|
|
94
|
+
].join('\n'));
|
|
95
|
+
}
|
|
96
|
+
return result;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function changedPathSet(root) {
|
|
100
|
+
const text = runGit(root, ['status', '--porcelain=v1']);
|
|
101
|
+
const set = new Set();
|
|
102
|
+
for (const line of (text || '').split(/\r?\n/)) {
|
|
103
|
+
if (!line.trim()) continue;
|
|
104
|
+
// porcelain v1: "XY <path>" (and "XY <old> -> <new>" for renames).
|
|
105
|
+
let p = line.slice(3).trim();
|
|
106
|
+
const arrow = p.indexOf(' -> ');
|
|
107
|
+
if (arrow !== -1) p = p.slice(arrow + 4);
|
|
108
|
+
// strip optional surrounding quotes git adds for unusual paths
|
|
109
|
+
p = p.replace(/^"(.*)"$/, '$1').split('\\').join('/');
|
|
110
|
+
set.add(p);
|
|
111
|
+
}
|
|
112
|
+
return set;
|
|
113
|
+
}
|
|
114
|
+
|
|
50
115
|
export function readGitStatus(root) {
|
|
51
116
|
if (!isGitWorkTree(root)) return null;
|
|
52
117
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
// Shared library module — consumed by the heavy @ps-neko/nekowork-harness package; kept in slim as the single source of truth.
|
|
1
2
|
import fs from 'node:fs';
|
|
2
3
|
import path from 'node:path';
|
|
4
|
+
import { readJson } from './session-io.js';
|
|
3
5
|
|
|
4
6
|
const DEFAULT_COUNT = 3;
|
|
5
7
|
|
|
@@ -94,12 +96,3 @@ export function buildDefaultAcceptanceCriteria(task = '', minimum = DEFAULT_COUN
|
|
|
94
96
|
source: 'task-derived-minimum',
|
|
95
97
|
}));
|
|
96
98
|
}
|
|
97
|
-
|
|
98
|
-
function readJson(file) {
|
|
99
|
-
if (!fs.existsSync(file)) return null;
|
|
100
|
-
try {
|
|
101
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
102
|
-
} catch {
|
|
103
|
-
return null;
|
|
104
|
-
}
|
|
105
|
-
}
|