ai2rules-harness 0.1.2 → 0.2.1-rc.3

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 CHANGED
@@ -11,14 +11,31 @@ That writes a governance manifest, a `PreToolUse` shim, and the host settings en
11
11
  into the current project. It is safe to run twice, it never replaces a manifest you
12
12
  have tuned, and turning it off is one file.
13
13
 
14
- > **Try it without installing:** `npx ai2rules-harness init` works and is the
15
- > fastest way to see a verdict — but **prefer the global install for real use.**
16
- > The shim records the absolute path of the binary that ran `init`, and under
17
- > `npx` that path is inside npm's transient `_npx` cache. When the cache is
18
- > cleaned or evicted the binary disappears, and the shim **fails open** — your
19
- > tool calls go back to the host's normal permissions with no error. That is the
20
- > correct behaviour for a missing kernel and the wrong place to be surprised by
21
- > it. After a global install, re-run `harness init` to re-bake the stable path.
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.
22
39
 
23
40
  ## What it does
24
41
 
package/bin/harness.js CHANGED
@@ -1,26 +1,58 @@
1
1
  #!/usr/bin/env node
2
- // Thin launcher: exec the real Rust binary, pass through argv, exit with its code.
2
+ // Launcher: find the prebuilt binary and exec it, passing argv through.
3
3
  //
4
- // No logic lives here. This file exists so `npx ai2rules-harness init` works, and
5
- // so a missing binary produces one clear sentence instead of a stack trace.
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 vendored = path.join(
12
- __dirname,
13
- '..',
14
- 'vendor',
15
- process.platform === 'win32' ? 'harness.exe' : 'harness'
16
- );
17
- const bin = process.env.AI2RULES_HARNESS || vendored;
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 at ${bin}
52
+ if (!bin || !fs.existsSync(bin)) {
53
+ console.error(`ai2rules-harness: no usable binary — ${why || `${bin} is missing`}
21
54
 
22
- The postinstall step did not install one for ${process.platform}-${process.arch}.
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.1.2",
3
+ "version": "0.2.1-rc.3",
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.1-rc.3",
36
+ "ai2rules-harness-darwin-x64": "0.2.1-rc.3",
37
+ "ai2rules-harness-linux-x64": "0.2.1-rc.3",
38
+ "ai2rules-harness-windows-x64": "0.2.1-rc.3"
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)));