ai2rules-harness 0.1.1 → 0.2.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/README.md +31 -8
- package/bin/harness.js +47 -15
- package/package.json +7 -5
- package/install.js +0 -144
package/README.md
CHANGED
|
@@ -3,18 +3,39 @@
|
|
|
3
3
|
**Govern a local coding agent in one command.**
|
|
4
4
|
|
|
5
5
|
```bash
|
|
6
|
-
|
|
6
|
+
npm install -g ai2rules-harness
|
|
7
|
+
harness init
|
|
7
8
|
```
|
|
8
9
|
|
|
9
10
|
That writes a governance manifest, a `PreToolUse` shim, and the host settings entry
|
|
10
11
|
into the current project. It is safe to run twice, it never replaces a manifest you
|
|
11
12
|
have tuned, and turning it off is one file.
|
|
12
13
|
|
|
13
|
-
> **
|
|
14
|
-
>
|
|
15
|
-
>
|
|
16
|
-
>
|
|
17
|
-
>
|
|
14
|
+
> **Install it globally, not into a project.** `harness init` **refuses** to run
|
|
15
|
+
> when the binary lives inside the project it would govern — a local
|
|
16
|
+
> `node_modules` install puts the kernel in the agent's own writable root, where
|
|
17
|
+
> replacing one file turns governance off silently. `npx ai2rules-harness init`
|
|
18
|
+
> is fine: its cache is outside your project.
|
|
19
|
+
|
|
20
|
+
## How it installs — and why there is no install script
|
|
21
|
+
|
|
22
|
+
The binary arrives as a **platform package** (`ai2rules-harness-linux-x64` and
|
|
23
|
+
friends), listed in `optionalDependencies`. npm resolves exactly one by `os`/`cpu`
|
|
24
|
+
and skips the rest.
|
|
25
|
+
|
|
26
|
+
That means installing this package runs **no install script, makes no network
|
|
27
|
+
request, executes no shell, and chmods nothing**. Earlier versions downloaded a
|
|
28
|
+
binary in a `postinstall` and verified a checksum fetched from the same host — which
|
|
29
|
+
proves the file arrived intact, not that it is the right file. Now the binary is
|
|
30
|
+
covered by the integrity hash npm writes into *your* lockfile, and the tarballs
|
|
31
|
+
carry [npm provenance](https://docs.npmjs.com/generating-provenance-statements)
|
|
32
|
+
signed against the workflow and commit that built them.
|
|
33
|
+
|
|
34
|
+
Practical upshots: installs are reproducible, work offline from a cache, and work
|
|
35
|
+
behind a proxy or a mirrored registry — none of which was true before.
|
|
36
|
+
|
|
37
|
+
If your platform has no prebuilt binary, nothing silently breaks: the CLI tells you
|
|
38
|
+
how to build one, or you point `AI2RULES_HARNESS` at an existing binary.
|
|
18
39
|
|
|
19
40
|
## What it does
|
|
20
41
|
|
|
@@ -54,10 +75,12 @@ looks like.
|
|
|
54
75
|
Effective on the very next tool call, no restart:
|
|
55
76
|
|
|
56
77
|
```bash
|
|
57
|
-
touch
|
|
58
|
-
touch ~/.claude/gate-off
|
|
78
|
+
touch ~/.claude/ai2rules/off/<your-project> # this project
|
|
79
|
+
touch ~/.claude/gate-off # everywhere
|
|
59
80
|
```
|
|
60
81
|
|
|
82
|
+
`harness init` prints the exact per-project path. Both live **outside** the
|
|
83
|
+
governed project on purpose: a switch inside it is one the agent can throw itself.
|
|
61
84
|
Delete the file to re-enable. The shim runs per call, so the toggle is immediate.
|
|
62
85
|
|
|
63
86
|
## Honest boundaries
|
package/bin/harness.js
CHANGED
|
@@ -1,26 +1,58 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
//
|
|
2
|
+
// Launcher: find the prebuilt binary and exec it, passing argv through.
|
|
3
3
|
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
4
|
+
// There is no install script. The binary arrives as an `optionalDependencies`
|
|
5
|
+
// entry that npm resolves by `os`/`cpu` — exactly one matches, the rest are
|
|
6
|
+
// skipped — so installing this package performs **no network access, no shell
|
|
7
|
+
// execution, and no chmod**. That is not a score optimisation: it means the
|
|
8
|
+
// binary you run is covered by the integrity hash npm already wrote into your
|
|
9
|
+
// lockfile, rather than by a checksum this package fetched from the same host it
|
|
10
|
+
// fetched the binary from. The old design proved integrity; this one has
|
|
11
|
+
// provenance.
|
|
12
|
+
//
|
|
13
|
+
// Escape hatch for unsupported platforms and for development: set
|
|
14
|
+
// AI2RULES_HARNESS to an absolute path and it is used verbatim.
|
|
6
15
|
|
|
7
16
|
const fs = require('fs');
|
|
8
17
|
const path = require('path');
|
|
9
18
|
const { spawnSync } = require('child_process');
|
|
10
19
|
|
|
11
|
-
const
|
|
12
|
-
|
|
13
|
-
'
|
|
14
|
-
'
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
20
|
+
const PKG = {
|
|
21
|
+
'linux-x64': 'ai2rules-harness-linux-x64',
|
|
22
|
+
'darwin-x64': 'ai2rules-harness-darwin-x64',
|
|
23
|
+
'darwin-arm64': 'ai2rules-harness-darwin-arm64',
|
|
24
|
+
'win32-x64': 'ai2rules-harness-windows-x64', // `win32` in an unscoped
|
|
25
|
+
// package name is rejected by npm's spam filter; the platform key is unchanged
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
function resolveBinary() {
|
|
29
|
+
const override = process.env.AI2RULES_HARNESS;
|
|
30
|
+
if (override) return { bin: override, how: 'AI2RULES_HARNESS' };
|
|
31
|
+
|
|
32
|
+
const key = `${process.platform}-${process.arch}`;
|
|
33
|
+
const pkg = PKG[key];
|
|
34
|
+
if (!pkg) return { bin: null, why: `no prebuilt binary is published for ${key}` };
|
|
35
|
+
|
|
36
|
+
const exe = process.platform === 'win32' ? 'harness.exe' : 'harness';
|
|
37
|
+
try {
|
|
38
|
+
// Resolve through the package's own manifest so this works with npm,
|
|
39
|
+
// pnpm and yarn layouts alike rather than assuming node_modules is flat.
|
|
40
|
+
const manifest = require.resolve(`${pkg}/package.json`);
|
|
41
|
+
return { bin: path.join(path.dirname(manifest), exe), how: pkg };
|
|
42
|
+
} catch {
|
|
43
|
+
return {
|
|
44
|
+
bin: null,
|
|
45
|
+
why: `the optional dependency ${pkg} is not installed (it may have been skipped by --no-optional, or filtered by a lockfile from a different platform)`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const { bin, why, how } = resolveBinary();
|
|
18
51
|
|
|
19
|
-
if (!fs.existsSync(bin)) {
|
|
20
|
-
console.error(`ai2rules-harness: no binary
|
|
52
|
+
if (!bin || !fs.existsSync(bin)) {
|
|
53
|
+
console.error(`ai2rules-harness: no usable binary — ${why || `${bin} is missing`}
|
|
21
54
|
|
|
22
|
-
|
|
23
|
-
Build it, or point at an existing binary:
|
|
55
|
+
Build one, or point at an existing binary:
|
|
24
56
|
|
|
25
57
|
cargo install --git https://github.com/sv-pro/ai2rules cli-harness
|
|
26
58
|
export AI2RULES_HARNESS=/absolute/path/to/harness
|
|
@@ -30,7 +62,7 @@ Build it, or point at an existing binary:
|
|
|
30
62
|
|
|
31
63
|
const res = spawnSync(bin, process.argv.slice(2), { stdio: 'inherit' });
|
|
32
64
|
if (res.error) {
|
|
33
|
-
console.error(`ai2rules-harness: cannot execute ${bin}: ${res.error.message}`);
|
|
65
|
+
console.error(`ai2rules-harness: cannot execute ${bin} (via ${how}): ${res.error.message}`);
|
|
34
66
|
process.exit(126);
|
|
35
67
|
}
|
|
36
68
|
process.exit(res.status === null ? 1 : res.status);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ai2rules-harness",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Deterministic governance for local CLI coding agents. `npx ai2rules-harness init` governs a project in one command.",
|
|
5
5
|
"license": "MIT OR Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -22,17 +22,19 @@
|
|
|
22
22
|
"bin": {
|
|
23
23
|
"harness": "bin/harness.js"
|
|
24
24
|
},
|
|
25
|
-
"scripts": {
|
|
26
|
-
"postinstall": "node install.js"
|
|
27
|
-
},
|
|
28
25
|
"files": [
|
|
29
26
|
"bin/harness.js",
|
|
30
|
-
"install.js",
|
|
31
27
|
"README.md",
|
|
32
28
|
"LICENSE-MIT",
|
|
33
29
|
"LICENSE-APACHE"
|
|
34
30
|
],
|
|
35
31
|
"engines": {
|
|
36
32
|
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"optionalDependencies": {
|
|
35
|
+
"ai2rules-harness-darwin-arm64": "0.2.0",
|
|
36
|
+
"ai2rules-harness-darwin-x64": "0.2.0",
|
|
37
|
+
"ai2rules-harness-linux-x64": "0.2.0",
|
|
38
|
+
"ai2rules-harness-windows-x64": "0.2.0"
|
|
37
39
|
}
|
|
38
40
|
}
|
package/install.js
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// postinstall — fetch the prebuilt `harness` for this platform.
|
|
3
|
-
//
|
|
4
|
-
// The asset names, targets and archive formats here mirror
|
|
5
|
-
// `.github/workflows/release.yml` exactly. If that matrix changes, this map has
|
|
6
|
-
// to change with it; `npm/verify-targets.js` fails the build when they drift.
|
|
7
|
-
//
|
|
8
|
-
// Deliberate properties, because this runs on other people's machines as part of
|
|
9
|
-
// installing a *security* tool:
|
|
10
|
-
//
|
|
11
|
-
// * One file, one host, and the URL derives from this package's version —
|
|
12
|
-
// never from anything in the user's project.
|
|
13
|
-
// * The SHA-256 is verified against the digest published beside the asset. A
|
|
14
|
-
// missing or mismatched checksum is a hard failure, not a warning.
|
|
15
|
-
// * On any failure it exits 0 with instructions rather than breaking the
|
|
16
|
-
// install of whatever depends on it. `bin/harness.js` reports the missing
|
|
17
|
-
// binary if and when someone actually runs it.
|
|
18
|
-
//
|
|
19
|
-
// Offline / air-gapped: set AI2RULES_HARNESS to an existing binary and nothing
|
|
20
|
-
// is downloaded at all.
|
|
21
|
-
|
|
22
|
-
const fs = require('fs');
|
|
23
|
-
const os = require('os');
|
|
24
|
-
const path = require('path');
|
|
25
|
-
const crypto = require('crypto');
|
|
26
|
-
const { spawnSync } = require('child_process');
|
|
27
|
-
const { pipeline } = require('stream/promises');
|
|
28
|
-
|
|
29
|
-
const REPO = 'sv-pro/ai2rules';
|
|
30
|
-
const VERSION = require('./package.json').version;
|
|
31
|
-
const VENDOR = path.join(__dirname, 'vendor');
|
|
32
|
-
const EXE = process.platform === 'win32' ? 'harness.exe' : 'harness';
|
|
33
|
-
|
|
34
|
-
// npm `${platform}-${arch}` -> what release.yml actually publishes.
|
|
35
|
-
// Linux is **musl** (statically linked, so it runs on any distro). There is no
|
|
36
|
-
// aarch64-linux build in the matrix yet; that platform correctly falls through
|
|
37
|
-
// to the build-from-source hint rather than downloading something wrong.
|
|
38
|
-
const TARGETS = {
|
|
39
|
-
'linux-x64': { target: 'x86_64-unknown-linux-musl', archive: 'tar.gz' },
|
|
40
|
-
'darwin-arm64': { target: 'aarch64-apple-darwin', archive: 'tar.gz' },
|
|
41
|
-
'darwin-x64': { target: 'x86_64-apple-darwin', archive: 'tar.gz' },
|
|
42
|
-
'win32-x64': { target: 'x86_64-pc-windows-msvc', archive: 'zip' },
|
|
43
|
-
};
|
|
44
|
-
|
|
45
|
-
function hint(reason) {
|
|
46
|
-
console.error(`
|
|
47
|
-
ai2rules-harness: ${reason}
|
|
48
|
-
|
|
49
|
-
No prebuilt binary was installed. You can still build one:
|
|
50
|
-
|
|
51
|
-
cargo install --git https://github.com/${REPO} cli-harness
|
|
52
|
-
|
|
53
|
-
then run \`harness init\` in a project. If you already have a binary:
|
|
54
|
-
|
|
55
|
-
export AI2RULES_HARNESS=/absolute/path/to/harness
|
|
56
|
-
`);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
async function download(url, dest) {
|
|
60
|
-
const res = await fetch(url, { redirect: 'follow' });
|
|
61
|
-
if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status}`);
|
|
62
|
-
await fs.promises.mkdir(path.dirname(dest), { recursive: true });
|
|
63
|
-
await pipeline(res.body, fs.createWriteStream(dest));
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function fetchText(url) {
|
|
67
|
-
const res = await fetch(url, { redirect: 'follow' });
|
|
68
|
-
if (!res.ok) throw new Error(`GET ${url} -> HTTP ${res.status}`);
|
|
69
|
-
return (await res.text()).trim();
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function sha256(file) {
|
|
73
|
-
return new Promise((resolve, reject) => {
|
|
74
|
-
const h = crypto.createHash('sha256');
|
|
75
|
-
fs.createReadStream(file)
|
|
76
|
-
.on('data', (d) => h.update(d))
|
|
77
|
-
.on('end', () => resolve(h.digest('hex')))
|
|
78
|
-
.on('error', reject);
|
|
79
|
-
});
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
// `tar` extracts both .tar.gz and .zip: GNU/bsdtar on Unix, and bsdtar has
|
|
83
|
-
// shipped as tar.exe on Windows 10+ since 1803. Avoids a dependency for a
|
|
84
|
-
// package whose whole point is that it installs cleanly.
|
|
85
|
-
function extract(archive, dir) {
|
|
86
|
-
const res = spawnSync('tar', ['-xf', archive, '-C', dir], { stdio: 'pipe' });
|
|
87
|
-
if (res.error) throw new Error(`cannot run tar: ${res.error.message}`);
|
|
88
|
-
if (res.status !== 0) {
|
|
89
|
-
throw new Error(`tar failed: ${res.stderr?.toString().trim() || `exit ${res.status}`}`);
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
async function main() {
|
|
94
|
-
const override = process.env.AI2RULES_HARNESS;
|
|
95
|
-
if (override) {
|
|
96
|
-
if (!fs.existsSync(override)) {
|
|
97
|
-
hint(`AI2RULES_HARNESS points at ${override}, which does not exist.`);
|
|
98
|
-
return;
|
|
99
|
-
}
|
|
100
|
-
console.log(`ai2rules-harness: using AI2RULES_HARNESS=${override}`);
|
|
101
|
-
return;
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
const key = `${process.platform}-${process.arch}`;
|
|
105
|
-
const spec = TARGETS[key];
|
|
106
|
-
if (!spec) {
|
|
107
|
-
hint(`no prebuilt binary for ${key} (${os.type()}).`);
|
|
108
|
-
return;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
const name = `harness-${spec.target}.${spec.archive}`;
|
|
112
|
-
const url = `https://github.com/${REPO}/releases/download/v${VERSION}/${name}`;
|
|
113
|
-
const tmp = await fs.promises.mkdtemp(path.join(os.tmpdir(), 'ai2rules-'));
|
|
114
|
-
const archive = path.join(tmp, name);
|
|
115
|
-
|
|
116
|
-
try {
|
|
117
|
-
await download(url, archive);
|
|
118
|
-
|
|
119
|
-
const expected = (await fetchText(`${url}.sha256`)).split(/\s+/)[0];
|
|
120
|
-
const actual = await sha256(archive);
|
|
121
|
-
if (!expected || expected !== actual) {
|
|
122
|
-
throw new Error(`checksum mismatch\n expected ${expected || '(none published)'}\n actual ${actual}`);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
extract(archive, tmp);
|
|
126
|
-
// The archive contains `harness-<target>/harness` — staged that way so it
|
|
127
|
-
// has no nested target/.../release path.
|
|
128
|
-
const unpacked = path.join(tmp, `harness-${spec.target}`, EXE);
|
|
129
|
-
if (!fs.existsSync(unpacked)) throw new Error(`archive did not contain ${EXE}`);
|
|
130
|
-
|
|
131
|
-
await fs.promises.mkdir(VENDOR, { recursive: true });
|
|
132
|
-
await fs.promises.copyFile(unpacked, path.join(VENDOR, EXE));
|
|
133
|
-
if (process.platform !== 'win32') await fs.promises.chmod(path.join(VENDOR, EXE), 0o755);
|
|
134
|
-
|
|
135
|
-
console.log(`ai2rules-harness: installed ${spec.target} (sha256 ${actual.slice(0, 12)}…)`);
|
|
136
|
-
} catch (err) {
|
|
137
|
-
await fs.promises.rm(VENDOR, { recursive: true, force: true }).catch(() => {});
|
|
138
|
-
hint(`could not install a prebuilt binary: ${err.message}`);
|
|
139
|
-
} finally {
|
|
140
|
-
await fs.promises.rm(tmp, { recursive: true, force: true }).catch(() => {});
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
main().catch((err) => hint(String(err && err.message ? err.message : err)));
|