From 8b418e1c50855396927da7cf20d17e5c556b02bc Mon Sep 17 00:00:00 2001 From: wufangyong973 Date: Thu, 15 Oct 2026 10:05:05 +0800 Subject: [PATCH] docs(examples): add a deterministic Node.js/npm stable-v1 integration recipe (#122) Mirror the preserved Python/pytest recipe as examples/node-npm, but on the current stable ExecSurface v1.0.0 instead of the historical Alpha.5 pin. The recipe is a reproducible demonstration, not independent external validation. What changed: - examples/node-npm/fixture/{test.js,fixture.json,package.json}: a one-file, zero-dependency Node fixture that asserts a sum invariant and only writes a harmless drift.txt when the public write_extra flag is true. - examples/node-npm/run.js: a Node: built-ins-only runner that learns and checks the same real node test.js command, retaining unchanged PASS (exit 0) and deliberate-write REVIEW (exit 10) evidence plus native command receipts, baseline immutability and verdict JSON. It targets the stable 1.0.0 binary (asserting execsurface --version) and the unchanged built-in review policy. - examples/node-npm/README.md: copy/paste walkthrough, expected exit codes, the controlled drift, and the current Linux x86_64 native-ptrace support boundary. - examples/README.md: register the Node.js/npm recipe in the example-selection table as a current v1 entry. No changes to observer, normalization, baseline, lockfile, policy, verdict or GitHub Action semantics. The fixture is private to this repo and requires no network during the measured run. Closes #122. --- examples/README.md | 1 + examples/node-npm/README.md | 81 +++++++++ examples/node-npm/fixture/fixture.json | 1 + examples/node-npm/fixture/package.json | 10 ++ examples/node-npm/fixture/test.js | 22 +++ examples/node-npm/run.js | 230 +++++++++++++++++++++++++ 6 files changed, 345 insertions(+) create mode 100644 examples/node-npm/README.md create mode 100644 examples/node-npm/fixture/fixture.json create mode 100644 examples/node-npm/fixture/package.json create mode 100644 examples/node-npm/fixture/test.js create mode 100644 examples/node-npm/run.js diff --git a/examples/README.md b/examples/README.md index b3bf2f09..78ab9090 100644 --- a/examples/README.md +++ b/examples/README.md @@ -6,6 +6,7 @@ | --- | --- | --- | | [Stable v1 GitHub Action consumer](github-action-consumer-v1.yml) | **Current v1** (`AETHERXGLOBAL/execsurface@v1`) | Copy-ready Rust CI integration after explicit baseline and policy custody setup | | [Five-Minute Start](../docs/QUICKSTART_5_MIN.md) | **Current v1** | Test the published v1 binary against controlled PASS/REVIEW scenarios | +| [Node.js/npm PASS→REVIEW](node-npm/README.md) | **Current v1** | Deterministic Node.js fixture showing an unchanged PASS and one controlled-drift REVIEW on stable v1.0.0 | | [Python/pytest PASS→REVIEW](python-pytest/README.md) | **Historical Alpha.5** | Replay preserved third-party contributed evidence only; not v1 onboarding | | [Minimal controlled drift](hello-drift.sh) | Check selected release/CLI contract | Small local demonstration | diff --git a/examples/node-npm/README.md b/examples/node-npm/README.md new file mode 100644 index 00000000..cb02e59b --- /dev/null +++ b/examples/node-npm/README.md @@ -0,0 +1,81 @@ +# Node.js/npm: unchanged PASS and deliberate-write REVIEW + +Current example, aligned with stable ExecSurface **v1.0.0** on Linux x86_64 with +native `ptrace`; `doctor` must succeed on the host. This recipe learns and +checks the **same real Node.js command** against a tiny dependency-free +fixture, mirroring the [Python/pytest recipe](../python-pytest/README.md). It is +a reproducible integration example, not independent external validation or a +safety certification. + +## What it demonstrates + +The observed target is a one-file Node script that is deterministic: it reads a +public `fixture.json`, asserts a sum invariant, and — only when the fixture's +`write_extra` flag is set — writes a harmless `drift.txt` into its working +directory. That single new file write is the controlled runtime effect that +ExecSurface should surface as `REVIEW`. + +From the repository root, first obtain the stable v1 binary using the +installation notes of the +[v1.0.0 release](https://github.com/AETHERXGLOBAL/execsurface/releases/tag/v1.0.0). +Keep its checksum-verified path in `EXEC_SURFACE_BIN`; this recipe records its +digest and checks its version. Then run the runner with Node.js 18 or later. +The runner uses only `node:` built-ins and downloads nothing. + +```sh +EXEC_SURFACE_BIN=/absolute/path/to/checksum-verified/execsurface +node examples/node-npm/run.js \ + --execsurface "$EXEC_SURFACE_BIN" \ + --output /tmp/execsurface-node-npm-first-run +``` + +Use a new output directory each time. The runner refuses to overwrite a +retained run, copies the three public fixture files, and retains the native +command receipts, stdout/stderr, baseline and verdict JSON. A failed +expectation writes `failure.json` and exits nonzero, retaining the failure. +`--node` may explicitly select another Node executable; otherwise the runner +uses its own interpreter. + +The observed command is: + +```sh +node test.js +``` + +The runner performs the ordinary `learn --output ... --label ... --workspace ...` +and `check --baseline ... --workspace ... --json-output ...` flows: + +| Step | Node result | ExecSurface result | Asserted exit | +| --- | --- | --- | --- | +| Learn `write_extra: false` | `node test.js` exits 0 | Baseline written | 0 | +| Check unchanged fixture | `node test.js` exits 0 | PASS; no drift | 0 | +| Set public fixture `write_extra: true`, check again | `node test.js` exits 0 | REVIEW; added `$WORKSPACE/drift.txt` write | 10 | + +The same assertion, command, Node interpreter and baseline remain selected. +Only the public fixture's boolean changes. The test then writes the harmless +`drift.txt` inside its fresh workspace. The runner verifies that a matching +`file_create`/`file_write` finding appears and that the baseline bytes did not +change. + +The **baseline** records the selected observed execution surface. The **policy** +interprets differences; this recipe uses the unchanged built-in REVIEW policy +for unmatched drift. It does not relearn after drift or weaken policy to get +PASS. ERROR, BLOCK, an unexpected native exit, a lost report or an unavailable +observer fails the recipe rather than counting as either expected outcome. + +## Support boundary + +Stable v1.0.0 supports **Linux x86_64** with native `ptrace` (see +[`docs/STATUS.md`](../../docs/STATUS.md)). Windows and ARM64 are not claimed. +The observed `node test.js` command is a familiar Node.js/npm developer +workload; ExecSurface observes the resulting Linux process/file/network +effects, not Node.js-specific semantics. + +## What this example is and is not + +The fixture contains no secrets. It adds no environment enumeration or +secret/file-content collection. ExecSurface's existing metadata observer may +record local executable and filesystem paths; review retained receipts before +sharing them. A PASS here establishes this selected observed fixture +comparison; it does not establish containment, malware safety, complete +observation of every possible effect or independent custody. diff --git a/examples/node-npm/fixture/fixture.json b/examples/node-npm/fixture/fixture.json new file mode 100644 index 00000000..d3ce7175 --- /dev/null +++ b/examples/node-npm/fixture/fixture.json @@ -0,0 +1 @@ +{"left":2,"right":3,"write_extra":false} diff --git a/examples/node-npm/fixture/package.json b/examples/node-npm/fixture/package.json new file mode 100644 index 00000000..ff4eedf4 --- /dev/null +++ b/examples/node-npm/fixture/package.json @@ -0,0 +1,10 @@ +{ + "name": "execsurface-node-npm-fixture", + "private": true, + "version": "0.0.0", + "description": "Deterministic Node.js fixture for the ExecSurface stable-v1 node-npm integration recipe", + "scripts": { + "test": "node test.js" + }, + "license": "Apache-2.0" +} diff --git a/examples/node-npm/fixture/test.js b/examples/node-npm/fixture/test.js new file mode 100644 index 00000000..b2cfa590 --- /dev/null +++ b/examples/node-npm/fixture/test.js @@ -0,0 +1,22 @@ +'use strict'; +/* One deterministic node test; the optional extra write is deliberate runtime drift. */ +const fs = require('fs'); +const path = require('path'); + +// The public fixture is a two-value selection plus an optional drift switch. +const selection = JSON.parse(fs.readFileSync('fixture.json', 'utf8')); + +// Deterministic invariant — holds on the unchanged fixture. +if (selection.left + selection.right !== 5) { + console.error('fixture invariant broken: left + right !== 5'); + process.exit(1); +} + +// Deliberate controlled runtime effect: write a file only when the fixture +// enables it. This is the single NEW execution-surface effect that ExecSurface +// should surface as REVIEW when the fixture is flipped. +if (selection.write_extra) { + fs.writeFileSync(path.join(process.cwd(), 'drift.txt'), 'controlled fixture drift\n'); +} + +console.log('node fixture passed'); diff --git a/examples/node-npm/run.js b/examples/node-npm/run.js new file mode 100644 index 00000000..3d505b17 --- /dev/null +++ b/examples/node-npm/run.js @@ -0,0 +1,230 @@ +'use strict'; +/* + * Learn and check a real, deterministic Node.js command, retaining an + * unchanged PASS and a deliberate-write REVIEW against stable ExecSurface v1. + * + * Mirrors examples/python-pytest/run.py: the observed target command is + * executed BY ExecSurface (learn/check), not by this runner, and the same + * command + fixture stay selected across every step. Only the public fixture's + * boolean flips between the unchanged check and the drift check. + * + * Node.js 18+, zero dependencies, node: built-ins only. No network during the + * measured run. + */ +const { spawnSync } = require('node:child_process'); +const crypto = require('node:crypto'); +const fs = require('node:fs'); +const path = require('node:path'); +const process = require('node:process'); + +class RecipeError extends Error {} + +function parseArgs(argv) { + const args = { execsurface: null, node: process.execPath, output: null }; + for (let i = 0; i < argv.length; i += 1) { + const a = argv[i]; + if (a === '--execsurface') args.execsurface = argv[++i]; + else if (a === '--node') args.node = argv[++i]; + else if (a === '--output') args.output = argv[++i]; + else throw new RecipeError(`unknown argument: ${a}`); + } + if (!args.execsurface || !args.output) { + throw new RecipeError('both --execsurface and --output are required'); + } + return args; +} + +function invoke(command, cwd, name, expected) { + // Retain command, both streams and actual exit before checking expectation. + const result = spawnSync(command[0], command.slice(1), { + cwd, + encoding: 'buffer', + timeout: 60000, + }); + const outPath = path.join(cwd, `${name}.stdout`); + const errPath = path.join(cwd, `${name}.stderr`); + const receiptPath = path.join(cwd, `${name}-command.json`); + fs.writeFileSync(outPath, result.stdout || Buffer.alloc(0)); + fs.writeFileSync(errPath, result.stderr || Buffer.alloc(0)); + if (result.error) { + fs.writeFileSync( + receiptPath, + JSON.stringify( + { command, returncode: null, expectedReturncode: expected, outcome: 'spawn-error', error: String(result.error) }, + null, + 2, + ) + '\n', + ); + throw new RecipeError(`${name}: could not spawn ${command[0]}: ${result.error}`); + } + fs.writeFileSync( + receiptPath, + JSON.stringify({ command, returncode: result.status, expectedReturncode: expected }, null, 2) + '\n', + ); + if (result.status !== expected) { + throw new RecipeError( + `${name}: expected exit ${expected}, got ${result.status}; retained receipts: ${cwd}`, + ); + } +} + +function check(binary, node, root, name, expected, verdict) { + const command = [ + binary, + 'check', + '--baseline', + path.join(root, 'execsurface.lock.json'), + '--workspace', + root, + '--json-output', + path.join(root, `${name}-verdict.json`), + '--', + node, + 'test.js', + ]; + invoke(command, root, name, expected); + const report = JSON.parse(fs.readFileSync(path.join(root, `${name}-verdict.json`), 'utf8')); + if ( + !report.target || + report.target.exit_code !== 0 || + report.target.signal !== null + ) { + throw new RecipeError(`${name}: node test.js did not exit successfully; retained ${root}`); + } + if (report.policy && report.policy.source !== 'builtin:review-unmatched-drift') { + throw new RecipeError(`${name}: selected built-in policy differs; retained ${root}`); + } + if (verdict === 'REVIEW') { + // A deliberate new file write under the declared workspace is the single + // expected drift finding. fs.writeFileSync on a new file may be recorded + // as file_create or file_write; either is the documented effect here. + const hasDriftWrite = (report.findings || []).some( + (f) => + f.change === 'added' && + (f.effect_kind === 'file_write' || f.effect_kind === 'file_create') && + f.evidence && + f.evidence.effect && + f.evidence.effect.target && + f.evidence.effect.target.value === '$WORKSPACE/drift.txt', + ); + if (!hasDriftWrite) { + throw new RecipeError(`${name}: deliberate $WORKSPACE/drift.txt write finding absent; retained ${root}`); + } + } + if (report.verdict !== verdict.toLowerCase()) { + throw new RecipeError(`${name}: expected ${verdict} verdict, got ${report.verdict}; retained ${root}`); + } +} + +function sha256(file) { + return crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); +} + +function run(args) { + const binary = path.resolve(args.execsurface); + const node = path.resolve(args.node); + if (!fs.existsSync(binary)) throw new RecipeError(`execsurface binary not found: ${binary}`); + if (!fs.existsSync(node)) throw new RecipeError(`node executable not found: ${node}`); + + const out = path.resolve(args.output); + if (fs.existsSync(out)) { + throw new RecipeError( + `output directory already exists; use a new path each run: ${out}`, + ); + } + fs.mkdirSync(out, { recursive: true }); + + // Copy the public, dependency-free fixture into the retained run directory. + const fixtureDir = path.join(__dirname, 'fixture'); + for (const name of ['test.js', 'fixture.json', 'package.json']) { + fs.copyFileSync(path.join(fixtureDir, name), path.join(out, name)); + } + + const selection = { + binarySha256: sha256(binary), + nodeExecutable: node, + fixtureSha256: sha256(path.join(fixtureDir, 'test.js')), + policy: 'unchanged built-in REVIEW for unmatched drift', + privacy: + 'public synthetic fixture; no environment or secret/file-content collection added', + }; + fs.writeFileSync(path.join(out, 'selection.json'), JSON.stringify(selection, null, 2) + '\n'); + + try { + // Identity: this recipe targets the stable v1 binary, not a historical pin. + invoke([binary, '--version'], out, 'version', 0); + const versionLine = fs.readFileSync(path.join(out, 'version.stdout'), 'utf8').trim(); + if (versionLine !== 'execsurface 1.0.0') { + throw new RecipeError(`recipe requires stable ExecSurface 1.0.0, got: ${versionLine}`); + } + // Native readiness within the declared Linux x86_64 / ptrace boundary. + invoke([binary, 'doctor'], out, 'doctor', 0); + + const learn = [ + binary, + 'learn', + '--output', + path.join(out, 'execsurface.lock.json'), + '--label', + 'node-npm-fixture', + '--workspace', + out, + '--', + node, + 'test.js', + ]; + invoke(learn, out, 'learn', 0); + const before = fs.readFileSync(path.join(out, 'execsurface.lock.json')); + + check(binary, node, out, 'unchanged', 0, 'PASS'); + + // Only this public fixture value changes; command and fixture stay the same. + fs.writeFileSync(path.join(out, 'fixture.json'), '{"left":2,"right":3,"write_extra":true}\n'); + check(binary, node, out, 'drift', 10, 'REVIEW'); + + if (!bufferEqual(fs.readFileSync(path.join(out, 'execsurface.lock.json')), before)) { + throw new RecipeError('baseline changed while checking drift'); + } + if (fs.readFileSync(path.join(out, 'drift.txt'), 'utf8') !== 'controlled fixture drift\n') { + throw new RecipeError('deliberate native write is missing'); + } + + const report = { + status: 'verified-recipe', + unchanged: { verdict: 'PASS', exit: 0 }, + drift: { verdict: 'REVIEW', exit: 10 }, + baselineSha256: crypto.createHash('sha256').update(before).digest('hex'), + scope: + 'stable-v1 Node.js/npm fixture; runtime drift example, not independent security validation', + receipts: out, + }; + fs.writeFileSync(path.join(out, 'recipe-report.json'), JSON.stringify(report, null, 2) + '\n'); + return report; + } catch (error) { + fs.writeFileSync( + path.join(out, 'failure.json'), + JSON.stringify({ status: 'failed', reason: error.message }, null, 2) + '\n', + ); + throw error; + } +} + +function bufferEqual(a, b) { + if (!Buffer.isBuffer(a) || !Buffer.isBuffer(b) || a.length !== b.length) return false; + for (let i = 0; i < a.length; i += 1) if (a[i] !== b[i]) return false; + return true; +} + +if (require.main === module) { + try { + const args = parseArgs(process.argv.slice(2)); + console.log(JSON.stringify(run(args), null, 2)); + } catch (error) { + if (error instanceof RecipeError) { + console.error(`ExecSurface node-npm recipe: ERROR: ${error.message}`); + process.exitCode = 1; + } else { + throw error; + } + } +}