shingan-lint 0.8.7 → 0.9.1
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/README.md +14 -6
- package/package.json +3 -2
- package/scripts/postinstall.js +72 -25
package/README.md
CHANGED
|
@@ -3,18 +3,26 @@
|
|
|
3
3
|
> AI Agent Workflow Static Analyzer — `npx`-installable wrapper for [Shingan](https://github.com/hatyibei/shingan).
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
#
|
|
7
|
-
npx shingan-lint
|
|
6
|
+
# 30-second smoke test — no input file needed
|
|
7
|
+
npx --yes shingan-lint demo
|
|
8
8
|
|
|
9
|
-
#
|
|
9
|
+
# Analyze your own JSON workflow
|
|
10
|
+
npx --yes shingan-lint analyze --input workflow.json --output markdown
|
|
11
|
+
|
|
12
|
+
# Analyze ADK-Go agents
|
|
13
|
+
npx --yes shingan-lint analyze --format adk-go --input ./agents/
|
|
14
|
+
|
|
15
|
+
# Project-local install
|
|
10
16
|
pnpm add -D shingan-lint
|
|
11
|
-
pnpm exec shingan
|
|
17
|
+
pnpm exec shingan demo
|
|
12
18
|
|
|
13
|
-
#
|
|
19
|
+
# Global install
|
|
14
20
|
npm install -g shingan-lint
|
|
15
|
-
shingan
|
|
21
|
+
shingan demo
|
|
16
22
|
```
|
|
17
23
|
|
|
24
|
+
Run `shingan --help` (or `npx shingan-lint --help`) to see every command and flag with copy-paste examples.
|
|
25
|
+
|
|
18
26
|
## What it does
|
|
19
27
|
|
|
20
28
|
Shingan detects 20 classes of design-time bugs in AI agent workflows **before they ship**:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shingan-lint",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.1",
|
|
4
4
|
"description": "AI Agent Workflow Static Analyzer — detect infinite loops, cost explosions, PII leaks, prompt injection sinks before execution. CLI wrapper that downloads the platform-specific Go binary from GitHub Releases.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai-agent",
|
|
@@ -42,7 +42,8 @@
|
|
|
42
42
|
"shingan": "./bin/shingan.js"
|
|
43
43
|
},
|
|
44
44
|
"scripts": {
|
|
45
|
-
"postinstall": "node ./scripts/postinstall.js"
|
|
45
|
+
"postinstall": "node ./scripts/postinstall.js",
|
|
46
|
+
"test": "node --test \"test/**/*.test.js\""
|
|
46
47
|
},
|
|
47
48
|
"files": [
|
|
48
49
|
"bin/shingan.js",
|
package/scripts/postinstall.js
CHANGED
|
@@ -4,9 +4,14 @@
|
|
|
4
4
|
// checksums.txt sha256 entry, extracts it from the goreleaser tarball,
|
|
5
5
|
// and installs it under ~/.cache/shingan-lint/v<version>/.
|
|
6
6
|
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
7
|
+
// Integrity is FAIL-CLOSED (#29): the install aborts (non-zero exit) on a
|
|
8
|
+
// checksum mismatch, a missing/unreachable checksums.txt, OR the archive
|
|
9
|
+
// being absent from checksums.txt. There is no "download but skip
|
|
10
|
+
// verification" path.
|
|
11
|
+
//
|
|
12
|
+
// The one intentional, explicit escape hatch is SHINGAN_SKIP_POSTINSTALL=1
|
|
13
|
+
// — it skips the download entirely (for air-gapped/CI-mirror builds where
|
|
14
|
+
// the binary is provided externally). It is opt-in and never the default.
|
|
10
15
|
|
|
11
16
|
'use strict';
|
|
12
17
|
|
|
@@ -14,8 +19,10 @@ const fs = require('fs');
|
|
|
14
19
|
const path = require('path');
|
|
15
20
|
const os = require('os');
|
|
16
21
|
const crypto = require('crypto');
|
|
17
|
-
|
|
18
|
-
|
|
22
|
+
// NOTE: `tar` is require()d lazily inside extractTar(), not at module load.
|
|
23
|
+
// Extraction only happens AFTER the fail-closed checksum gate passes, so a
|
|
24
|
+
// verification failure never reaches the archive-handling code — and the
|
|
25
|
+
// regression test can require() this module without the `tar` dep present.
|
|
19
26
|
|
|
20
27
|
const PACKAGE_VERSION = require('../package.json').version;
|
|
21
28
|
|
|
@@ -84,12 +91,41 @@ function findExpectedHash(checksumsText, archiveName) {
|
|
|
84
91
|
return null;
|
|
85
92
|
}
|
|
86
93
|
|
|
94
|
+
// verifyChecksum is the fail-CLOSED integrity gate (#29). It throws on
|
|
95
|
+
// *any* condition that prevents proving the downloaded archive is
|
|
96
|
+
// authentic — a sha256 mismatch, or the archive being absent from
|
|
97
|
+
// checksums.txt — so the only way past it is a genuine, matching hash.
|
|
98
|
+
// Earlier this logic warned-and-continued (fail-OPEN): a tampered
|
|
99
|
+
// binary, or a stripped/empty checksums.txt, would silently install.
|
|
100
|
+
// Pure + synchronous so it is unit-testable without any network.
|
|
101
|
+
function verifyChecksum(archiveBuf, archive, checksumsText) {
|
|
102
|
+
const expected = findExpectedHash(checksumsText, archive);
|
|
103
|
+
if (!expected) {
|
|
104
|
+
throw new Error(
|
|
105
|
+
`${archive} not found in checksums.txt — cannot verify integrity. ` +
|
|
106
|
+
`Refusing to install an unverifiable binary. ` +
|
|
107
|
+
`If the release is genuinely missing checksums and you accept the risk, ` +
|
|
108
|
+
`set SHINGAN_SKIP_POSTINSTALL=1 and install the binary yourself.`
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
const actual = sha256(archiveBuf);
|
|
112
|
+
if (actual !== expected) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`sha256 mismatch for ${archive}: expected ${expected}, got ${actual}. ` +
|
|
115
|
+
`The downloaded archive does NOT match the published checksum — ` +
|
|
116
|
+
`aborting install (possible tampering or a corrupted download).`
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
return expected;
|
|
120
|
+
}
|
|
121
|
+
|
|
87
122
|
async function extractTar(buf, dest, tag) {
|
|
88
123
|
// Write to temp file so `tar` can stream it back.
|
|
89
124
|
const tmpFile = path.join(dest, `_archive.${tag.ext}`);
|
|
90
125
|
fs.writeFileSync(tmpFile, buf);
|
|
91
126
|
try {
|
|
92
127
|
if (tag.ext === 'tar.gz') {
|
|
128
|
+
const tar = require('tar');
|
|
93
129
|
await tar.x({ file: tmpFile, cwd: dest, strict: true });
|
|
94
130
|
} else if (tag.ext === 'zip') {
|
|
95
131
|
// Minimal zip extraction without an extra dependency: shell out.
|
|
@@ -145,25 +181,28 @@ async function main() {
|
|
|
145
181
|
process.exit(1);
|
|
146
182
|
}
|
|
147
183
|
|
|
148
|
-
//
|
|
149
|
-
//
|
|
150
|
-
//
|
|
151
|
-
//
|
|
184
|
+
// Fail-CLOSED integrity check (#29). The checksums.txt file MUST be
|
|
185
|
+
// reachable and MUST contain a matching sha256 for this archive, or we
|
|
186
|
+
// abort the install. A checksum mismatch, a missing/unreachable
|
|
187
|
+
// checksums.txt, or the archive being absent from it all raise here and
|
|
188
|
+
// propagate to main().catch → exit(1). The documented escape hatch for
|
|
189
|
+
// air-gapped/dev environments is SHINGAN_SKIP_POSTINSTALL=1 (handled at
|
|
190
|
+
// the top of main()), which skips the download entirely — there is no
|
|
191
|
+
// "download but skip verification" mode by design.
|
|
192
|
+
let checksumsText;
|
|
152
193
|
try {
|
|
153
|
-
|
|
154
|
-
const expected = findExpectedHash(checksumsText, archive);
|
|
155
|
-
if (expected) {
|
|
156
|
-
const actual = sha256(archiveBuf);
|
|
157
|
-
if (actual !== expected) {
|
|
158
|
-
throw new Error(`sha256 mismatch: expected ${expected}, got ${actual}`);
|
|
159
|
-
}
|
|
160
|
-
console.log(`shingan-lint: sha256 verified (${expected.slice(0, 12)}…)`);
|
|
161
|
-
} else {
|
|
162
|
-
console.warn(`shingan-lint: ${archive} not found in checksums.txt — proceeding without verification`);
|
|
163
|
-
}
|
|
194
|
+
checksumsText = await fetchText(checksumURL());
|
|
164
195
|
} catch (e) {
|
|
165
|
-
|
|
196
|
+
throw new Error(
|
|
197
|
+
`unable to fetch checksums.txt for integrity verification: ${e.message}. ` +
|
|
198
|
+
`Refusing to install an unverified binary. ` +
|
|
199
|
+
`If the release was just published, GitHub may need a few seconds to ` +
|
|
200
|
+
`propagate — retry shortly. For air-gapped installs set ` +
|
|
201
|
+
`SHINGAN_SKIP_POSTINSTALL=1 and provide the binary yourself.`
|
|
202
|
+
);
|
|
166
203
|
}
|
|
204
|
+
const expected = verifyChecksum(archiveBuf, archive, checksumsText);
|
|
205
|
+
console.log(`shingan-lint: sha256 verified (${expected.slice(0, 12)}…)`);
|
|
167
206
|
|
|
168
207
|
await extractTar(archiveBuf, dest, tag);
|
|
169
208
|
|
|
@@ -180,7 +219,15 @@ async function main() {
|
|
|
180
219
|
console.log(`shingan-lint: installed ${tag.exe} → ${binPath}`);
|
|
181
220
|
}
|
|
182
221
|
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
222
|
+
// Export the pure helpers so the regression test (test/postinstall.test.js)
|
|
223
|
+
// can exercise the fail-closed checksum gate without any network.
|
|
224
|
+
module.exports = { verifyChecksum, findExpectedHash, sha256, main };
|
|
225
|
+
|
|
226
|
+
// Only auto-run the installer when invoked directly (`node postinstall.js`,
|
|
227
|
+
// i.e. as npm's postinstall hook). When `require()`d by the test, do nothing.
|
|
228
|
+
if (require.main === module) {
|
|
229
|
+
main().catch((err) => {
|
|
230
|
+
console.error(`shingan-lint: postinstall failed: ${err.stack || err.message}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
});
|
|
233
|
+
}
|