Browse Source

feat: support immutable Git package installs

David Nabraczky-Hajos 4 weeks ago
parent
commit
a1c4a6e95e

+ 3 - 1
.gitignore

@@ -1,5 +1,7 @@
 node_modules/
 node_modules/
-dist/
+dist/*
+!dist/cli.js
+!dist/config.js
 *.tgz
 *.tgz
 coverage/
 coverage/
 *.log
 *.log

+ 81 - 16
README.md

@@ -64,36 +64,101 @@ such as `account/logout` are forwarded unchanged. Logging out through any
 connected client therefore logs out the shared server and affects every client
 connected client therefore logs out the shared server and affects every client
 using that server.
 using that server.
 
 
-## Source-based release and installation
+## Install from an immutable Git revision
 
 
-No registry publication is used. From a clean source checkout:
+The canonical repository is
+`https://git.bodicsek.host/bodicsek/codex-app-server-bridge.git`. Git access and
+any required trust or authentication configuration are host prerequisites;
+keep credentials out of package URLs and command output. Preflight the exact
+release tag before installation:
 
 
 ```sh
 ```sh
-npm ci
-npm run typecheck
-npm run build
-npm test
-npm pack --dry-run
-npm pack
-npm install --global ./codex-app-server-bridge-0.1.0.tgz
+git ls-remote --exit-code \
+  https://git.bodicsek.host/bodicsek/codex-app-server-bridge.git \
+  refs/tags/v0.1.1
+```
+
+Install only an immutable release tag or full 40-character commit. Do not use
+`#main` for a reproducible installation. The repository contains the compiled
+runtime, so lifecycle scripts are disabled and no TypeScript toolchain is
+needed on the consumer machine:
+
+```sh
+npm run verify:git-package -- \
+  'git+https://git.bodicsek.host/bodicsek/codex-app-server-bridge.git#v0.1.1'
+npm install --global \
+  --ignore-scripts \
+  --cache /private/tmp/codex-app-server-bridge-npm-cache \
+  'git+https://git.bodicsek.host/bodicsek/codex-app-server-bridge.git#v0.1.1'
 command -v codex-app-server-bridge
 command -v codex-app-server-bridge
+npm list --global --depth=0 codex-app-server-bridge
 codex-app-server-bridge --unsupported
 codex-app-server-bridge --unsupported
 ```
 ```
 
 
-The dry-run file list is the review boundary for the installable unit. It
-should contain only `package.json`, `README.md`, `LICENSE`, `bin/`, and the
-generated `dist/` JavaScript. TypeScript source, tests, credentials, and local
-artifacts are excluded. The generated build output is reproducible and remains
-uncommitted.
+For a commit pin, replace `v0.1.1` in both commands with the full commit
+reported by `git ls-remote`; abbreviated commits and branches are rejected by
+the verifier. The verifier uses a temporary global prefix and never contacts a
+Codex App Server. The end-user install intentionally omits `--prefix` so the
+executable is placed on the normal global PATH used to launch Emacs.
+
+Installing `v0.1.1` upgrades the existing `0.1.0` local-tarball installation
+in place. The package name, executable, and `CODEX_PATH` value do not change.
+Existing bridge processes continue using the version with which they started;
+new agent-shell launches discover the replacement PATH executable.
+
+To roll back, reinstall the verified `0.1.0` tarball or a previously verified
+immutable Git revision:
+
+```sh
+npm install --global \
+  --ignore-scripts \
+  --cache /private/tmp/codex-app-server-bridge-npm-cache \
+  /Users/dnabracz/Work/packages/codex-app-server-bridge-0.1.0.tgz
+npm install --global \
+  --ignore-scripts \
+  --cache /private/tmp/codex-app-server-bridge-npm-cache \
+  'git+https://git.bodicsek.host/bodicsek/codex-app-server-bridge.git#v0.1.1'
+```
+
+Remove the globally installed package without changing Emacs configuration:
+
+```sh
+npm uninstall --global codex-app-server-bridge
+```
+
+## Build and verify a local npm package
+
+No registry publication is used. The package remains private. Maintainers use
+the explicit `pack:verified` workflow, which compiles, tests, checks committed
+runtime freshness, and invokes `npm pack --ignore-scripts`. A raw `npm pack`
+does not run those release gates.
+
+```sh
+mkdir -p /Users/dnabracz/Work/packages
+npm run pack:verified -- \
+  --pack-destination /Users/dnabracz/Work/packages \
+  --cache /private/tmp/codex-app-server-bridge-npm-cache
+npm run verify:package -- \
+  /Users/dnabracz/Work/packages/codex-app-server-bridge-0.1.1.tgz
+```
+
+The artifact contains only package metadata, `README.md`, `LICENSE`, the
+launcher under `bin/`, and the two compiled runtime files under `dist/`.
+TypeScript source, tests, credentials, caches, and local artifacts are
+excluded. The verifier installs the tarball in a temporary prefix and exercises
+the installed launcher without connecting to the shared App Server.
 
 
 ## Development
 ## Development
 
 
 ```sh
 ```sh
 npm ci
 npm ci
 npm run typecheck
 npm run typecheck
-npm run build
+npm run compile
 npm test
 npm test
+npm run verify:runtime
 ```
 ```
 
 
 Tests use only `node:test` and a loopback WebSocket peer built from Node core
 Tests use only `node:test` and a loopback WebSocket peer built from Node core
-APIs. They do not require Codex, credentials, or external network access.
+APIs. They do not require Codex, credentials, or external network access. When
+source changes, commit the matching `dist/cli.js` and `dist/config.js` output;
+`npm run verify:runtime` fails when those files are stale.

+ 165 - 0
dist/cli.js

@@ -0,0 +1,165 @@
+import { createInterface } from "node:readline";
+import { ConfigurationError, parseEndpoint, validateInvocation, } from "./config.js";
+const STARTUP_TIMEOUT_MS = 5_000;
+function writeDiagnostic(output, message) {
+    output.write(`codex-app-server-bridge: ${message}\n`);
+}
+export async function runBridge(options) {
+    try {
+        validateInvocation(options.arguments);
+    }
+    catch (error) {
+        const message = error instanceof ConfigurationError ? error.message : "invalid invocation";
+        writeDiagnostic(options.errorOutput, message);
+        return 1;
+    }
+    let endpoint;
+    try {
+        endpoint = parseEndpoint(options.environment.CODEX_APP_SERVER_URL);
+    }
+    catch (error) {
+        const message = error instanceof ConfigurationError ? error.message : "invalid configuration";
+        writeDiagnostic(options.errorOutput, `configuration error: ${message}`);
+        return 1;
+    }
+    return await new Promise((resolve) => {
+        let socket;
+        let lineReader;
+        let completed = false;
+        let localShutdown = false;
+        let failure = false;
+        let startupTimer;
+        const removeProcessListeners = () => {
+            process.removeListener("SIGINT", handleSignal);
+            process.removeListener("SIGTERM", handleSignal);
+        };
+        const finish = (exitCode) => {
+            if (completed) {
+                return;
+            }
+            completed = true;
+            if (startupTimer !== undefined) {
+                clearTimeout(startupTimer);
+            }
+            lineReader?.close();
+            options.input.pause();
+            removeProcessListeners();
+            resolve(exitCode);
+        };
+        const closeSocket = (code = 1000, reason = "bridge shutdown") => {
+            if (socket.readyState === WebSocket.OPEN) {
+                socket.close(code, reason);
+            }
+            else if (socket.readyState === WebSocket.CONNECTING) {
+                try {
+                    socket.close();
+                }
+                catch {
+                    // Process completion still closes a connection that has not opened.
+                }
+            }
+        };
+        const fail = (message, closeCode = 1011) => {
+            if (failure || completed) {
+                return;
+            }
+            failure = true;
+            writeDiagnostic(options.errorOutput, message);
+            closeSocket(closeCode, "bridge failure");
+            finish(1);
+        };
+        const beginLocalShutdown = () => {
+            if (completed || localShutdown) {
+                return;
+            }
+            localShutdown = true;
+            if (startupTimer !== undefined) {
+                clearTimeout(startupTimer);
+            }
+            lineReader?.close();
+            options.input.pause();
+            closeSocket();
+            if (socket.readyState !== WebSocket.OPEN && socket.readyState !== WebSocket.CLOSING) {
+                finish(0);
+            }
+        };
+        function handleSignal() {
+            beginLocalShutdown();
+        }
+        process.once("SIGINT", handleSignal);
+        process.once("SIGTERM", handleSignal);
+        try {
+            socket = new WebSocket(endpoint.url);
+            socket.binaryType = "arraybuffer";
+        }
+        catch {
+            removeProcessListeners();
+            writeDiagnostic(options.errorOutput, `connection error for ${endpoint.display}`);
+            resolve(1);
+            return;
+        }
+        startupTimer = setTimeout(() => {
+            fail(`startup timeout after ${STARTUP_TIMEOUT_MS} ms for ${endpoint.display}`);
+        }, STARTUP_TIMEOUT_MS);
+        socket.addEventListener("open", () => {
+            if (completed) {
+                closeSocket();
+                return;
+            }
+            if (startupTimer !== undefined) {
+                clearTimeout(startupTimer);
+                startupTimer = undefined;
+            }
+            lineReader = createInterface({
+                input: options.input,
+                crlfDelay: Infinity,
+                terminal: false,
+            });
+            lineReader.on("line", (line) => {
+                if (line.trim().length === 0) {
+                    return;
+                }
+                try {
+                    socket.send(line);
+                }
+                catch {
+                    fail(`transport error for ${endpoint.display}`);
+                }
+            });
+            lineReader.once("close", beginLocalShutdown);
+        });
+        socket.addEventListener("message", (event) => {
+            if (typeof event.data !== "string") {
+                fail("protocol error: binary WebSocket frames are not supported", 1003);
+                return;
+            }
+            options.output.write(`${event.data}\n`);
+        });
+        socket.addEventListener("error", () => {
+            if (localShutdown) {
+                return;
+            }
+            fail(`connection error for ${endpoint.display}`);
+        });
+        socket.addEventListener("close", () => {
+            if (localShutdown) {
+                finish(0);
+            }
+            else if (failure) {
+                finish(1);
+            }
+            else {
+                fail(`connection closed unexpectedly for ${endpoint.display}`);
+            }
+        });
+    });
+}
+export async function main() {
+    process.exitCode = await runBridge({
+        arguments: process.argv.slice(2),
+        environment: process.env,
+        input: process.stdin,
+        output: process.stdout,
+        errorOutput: process.stderr,
+    });
+}

+ 32 - 0
dist/config.js

@@ -0,0 +1,32 @@
+export const USAGE = "Usage: codex-app-server-bridge app-server";
+export class ConfigurationError extends Error {
+    constructor(message) {
+        super(message);
+        this.name = "ConfigurationError";
+    }
+}
+export function validateInvocation(arguments_) {
+    if (arguments_.length !== 1 || arguments_[0] !== "app-server") {
+        throw new ConfigurationError(USAGE);
+    }
+}
+export function parseEndpoint(value) {
+    if (value === undefined || value.length === 0) {
+        throw new ConfigurationError("CODEX_APP_SERVER_URL is required");
+    }
+    const match = /^ws:\/\/(127\.0\.0\.1|\[::1\]):([0-9]+)\/?$/.exec(value);
+    if (match === null) {
+        throw new ConfigurationError("CODEX_APP_SERVER_URL must be ws://127.0.0.1:<port> or ws://[::1]:<port>");
+    }
+    const host = match[1];
+    const portText = match[2];
+    if (host === undefined || portText === undefined) {
+        throw new ConfigurationError("CODEX_APP_SERVER_URL is invalid");
+    }
+    const port = Number(portText);
+    if (!Number.isSafeInteger(port) || port < 1 || port > 65_535) {
+        throw new ConfigurationError("CODEX_APP_SERVER_URL must contain a valid explicit port");
+    }
+    const display = `ws://${host}:${port}`;
+    return { url: display, display };
+}

+ 2 - 2
package-lock.json

@@ -1,12 +1,12 @@
 {
 {
   "name": "codex-app-server-bridge",
   "name": "codex-app-server-bridge",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "lockfileVersion": 3,
   "lockfileVersion": 3,
   "requires": true,
   "requires": true,
   "packages": {
   "packages": {
     "": {
     "": {
       "name": "codex-app-server-bridge",
       "name": "codex-app-server-bridge",
-      "version": "0.1.0",
+      "version": "0.1.1",
       "bin": {
       "bin": {
         "codex-app-server-bridge": "bin/codex-app-server-bridge.js"
         "codex-app-server-bridge": "bin/codex-app-server-bridge.js"
       },
       },

+ 9 - 5
package.json

@@ -1,6 +1,6 @@
 {
 {
   "name": "codex-app-server-bridge",
   "name": "codex-app-server-bridge",
-  "version": "0.1.0",
+  "version": "0.1.1",
   "description": "Bridge codex-acp stdio JSONL to an existing loopback Codex App Server WebSocket.",
   "description": "Bridge codex-acp stdio JSONL to an existing loopback Codex App Server WebSocket.",
   "private": true,
   "private": true,
   "type": "module",
   "type": "module",
@@ -9,15 +9,19 @@
   },
   },
   "files": [
   "files": [
     "bin/",
     "bin/",
-    "dist/",
+    "dist/cli.js",
+    "dist/config.js",
     "README.md",
     "README.md",
     "LICENSE"
     "LICENSE"
   ],
   ],
   "scripts": {
   "scripts": {
     "typecheck": "tsc --project tsconfig.json",
     "typecheck": "tsc --project tsconfig.json",
-    "build": "tsc --project tsconfig.build.json",
-    "test": "npm run build && node --test --test-concurrency=1 test/*.test.ts",
-    "prepack": "npm run build"
+    "compile": "tsc --project tsconfig.build.json",
+    "test": "npm run compile && node --test --test-concurrency=1 test/*.test.ts",
+    "verify:runtime": "node scripts/verify-generated-runtime.mjs",
+    "verify:git-package": "node scripts/verify-git-package.mjs",
+    "verify:package": "node scripts/verify-packed-package.mjs",
+    "pack:verified": "npm run compile && npm test && npm run verify:runtime && npm pack --ignore-scripts"
   },
   },
   "engines": {
   "engines": {
     "node": ">=24"
     "node": ">=24"

+ 49 - 0
scripts/verify-generated-runtime.mjs

@@ -0,0 +1,49 @@
+#!/usr/bin/env node
+
+import { execFile } from "node:child_process";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const cwd = process.cwd();
+const runtimePaths = ["dist/cli.js", "dist/config.js"];
+
+async function compileRuntime() {
+  try {
+    await execFileAsync("npm", ["run", "compile", "--silent"], {
+      cwd,
+      maxBuffer: 10 * 1024 * 1024,
+    });
+  } catch (error) {
+    const code = typeof error === "object" && error !== null && "code" in error
+      ? String(error.code)
+      : "unknown";
+    throw new Error(`runtime compilation failed with exit code ${code}`);
+  }
+}
+
+async function generatedRuntimeDrift() {
+  const [{ stdout: changed }, { stdout: untracked }] = await Promise.all([
+    execFileAsync("git", ["diff", "--name-status", "--", ...runtimePaths], { cwd }),
+    execFileAsync(
+      "git",
+      ["ls-files", "--others", "--exclude-standard", "--", ...runtimePaths],
+      { cwd },
+    ),
+  ]);
+  return [changed.trim(), untracked.trim()].filter(Boolean).join("\n");
+}
+
+async function verify() {
+  await compileRuntime();
+  const drift = await generatedRuntimeDrift();
+  if (drift !== "") {
+    throw new Error(`generated runtime drift detected:\n${drift}`);
+  }
+  process.stdout.write("Generated runtime is fresh.\n");
+}
+
+verify().catch((error) => {
+  const message = error instanceof Error ? error.message : String(error);
+  process.stderr.write(`Generated-runtime verification failed: ${message}\n`);
+  process.exitCode = 1;
+});

+ 135 - 0
scripts/verify-git-package.mjs

@@ -0,0 +1,135 @@
+#!/usr/bin/env node
+
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { constants } from "node:fs";
+import { access, lstat, mkdtemp, readFile, realpath, rm } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { isAbsolute, join, relative } from "node:path";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const PACKAGE_NAME = "codex-app-server-bridge";
+const DEFAULT_CACHE = "/private/tmp/codex-app-server-bridge-npm-cache";
+const USAGE = "usage: npm run verify:git-package -- <git+https|git+ssh|git+file URL>#<vX.Y.Z|40-character-commit>";
+
+function selectedGitPackage(argv) {
+  if (argv.length !== 1) throw new Error(USAGE);
+  const packageSpec = argv[0];
+  const fragmentAt = packageSpec.lastIndexOf("#");
+  const repository = packageSpec.slice(0, fragmentAt);
+  const revision = packageSpec.slice(fragmentAt + 1);
+  const supportedRepository = /^(?:git\+https|git\+ssh|git\+file):\/\/.+\.git$/.test(repository);
+  const immutableRevision = /^(?:[0-9a-fA-F]{40}|v\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$/.test(revision);
+  if (fragmentAt <= 0 || !supportedRepository || !immutableRevision) {
+    throw new Error("Git package must use a supported URL pinned to a full commit or release tag");
+  }
+  return { packageSpec, revision };
+}
+
+function isWithin(parent, child) {
+  const pathFromParent = relative(parent, child);
+  return pathFromParent === "" || (!pathFromParent.startsWith("..") && !isAbsolute(pathFromParent));
+}
+
+function commandExit(error) {
+  return typeof error === "object" && error !== null && "code" in error
+    ? String(error.code)
+    : "unknown";
+}
+
+async function installPackage(prefix, cache, packageSpec) {
+  try {
+    await execFileAsync("npm", [
+      "install",
+      "--global",
+      "--prefix",
+      prefix,
+      "--ignore-scripts",
+      "--cache",
+      cache,
+      packageSpec,
+    ], { maxBuffer: 10 * 1024 * 1024 });
+  } catch (error) {
+    throw new Error(`isolated npm installation failed with exit code ${commandExit(error)}`);
+  }
+}
+
+async function installedPackage(prefix, cache) {
+  const packageRoot = join(prefix, "lib", "node_modules", PACKAGE_NAME);
+  const executable = join(prefix, "bin", PACKAGE_NAME);
+  await Promise.all([
+    access(executable, constants.X_OK),
+    access(join(packageRoot, "dist", "cli.js"), constants.R_OK),
+    access(join(packageRoot, "dist", "config.js"), constants.R_OK),
+  ]);
+
+  const packageMetadata = await lstat(packageRoot);
+  if (packageMetadata.isSymbolicLink()) {
+    throw new Error("installed package must not be a temporary link");
+  }
+
+  const [realPrefix, realCache, realPackage, realExecutable] = await Promise.all([
+    realpath(prefix),
+    realpath(cache),
+    realpath(packageRoot),
+    realpath(executable),
+  ]);
+  if (!isWithin(realPrefix, realPackage) || isWithin(realCache, realPackage)) {
+    throw new Error("installed package resolved outside the isolated prefix");
+  }
+  if (!isWithin(realPackage, realExecutable) || isWithin(realCache, realExecutable)) {
+    throw new Error("installed executable resolved outside the installed package");
+  }
+
+  const manifest = JSON.parse(await readFile(join(realPackage, "package.json"), "utf8"));
+  assert.equal(manifest.name, PACKAGE_NAME);
+  assert.deepEqual(manifest.bin, { [PACKAGE_NAME]: `bin/${PACKAGE_NAME}.js` });
+  return { executable, version: String(manifest.version) };
+}
+
+async function verifyExecutable(executable) {
+  try {
+    await execFileAsync(executable, ["--unsupported"], {
+      env: process.env,
+      maxBuffer: 1024 * 1024,
+    });
+    throw new Error("installed executable unexpectedly accepted an unsupported invocation");
+  } catch (error) {
+    if (typeof error !== "object" || error === null || !("code" in error) || error.code !== 1) {
+      throw error;
+    }
+    const stdout = "stdout" in error ? String(error.stdout) : "";
+    const stderr = "stderr" in error ? String(error.stderr) : "";
+    assert.equal(stdout, "");
+    assert.match(stderr, /Usage: codex-app-server-bridge app-server/);
+  }
+}
+
+async function verify() {
+  const { packageSpec, revision } = selectedGitPackage(process.argv.slice(2));
+  const prefix = await mkdtemp(join(tmpdir(), "codex-app-server-bridge-git-package-"));
+  const cache = process.env.CODEX_APP_SERVER_BRIDGE_NPM_CACHE ?? DEFAULT_CACHE;
+
+  try {
+    await installPackage(prefix, cache, packageSpec);
+    const installed = await installedPackage(prefix, cache);
+    await verifyExecutable(installed.executable);
+    process.stdout.write(`${JSON.stringify({
+      ok: true,
+      package: `${PACKAGE_NAME}@${installed.version}`,
+      revision,
+      executable: `bin/${PACKAGE_NAME}`,
+      storage: "isolated-prefix",
+      invocation: "verified",
+    })}\n`);
+  } finally {
+    await rm(prefix, { recursive: true, force: true });
+  }
+}
+
+verify().catch((error) => {
+  const message = error instanceof Error ? error.message : String(error);
+  process.stderr.write(`Git-package verification failed: ${message}\n`);
+  process.exitCode = 1;
+});

+ 73 - 0
scripts/verify-packed-package.mjs

@@ -0,0 +1,73 @@
+#!/usr/bin/env node
+
+import assert from "node:assert/strict";
+import { execFile } from "node:child_process";
+import { constants } from "node:fs";
+import { access, mkdtemp, rm, stat } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { basename, join, resolve } from "node:path";
+import { promisify } from "node:util";
+
+const execFileAsync = promisify(execFile);
+const PACKAGE_NAME = "codex-app-server-bridge";
+const DEFAULT_CACHE = "/private/tmp/codex-app-server-bridge-npm-cache";
+
+async function selectedTarball(argv) {
+  if (argv.length !== 1) {
+    throw new Error("usage: npm run verify:package -- /absolute/path/to/codex-app-server-bridge-VERSION.tgz");
+  }
+  const tarball = resolve(argv[0]);
+  if (!tarball.endsWith(".tgz")) throw new Error("package path must end in .tgz");
+  const metadata = await stat(tarball);
+  if (!metadata.isFile()) throw new Error("package path must identify a readable file");
+  await access(tarball, constants.R_OK);
+  return tarball;
+}
+
+async function verify() {
+  const tarball = await selectedTarball(process.argv.slice(2));
+  const prefix = await mkdtemp(join(tmpdir(), "codex-app-server-bridge-package-"));
+  const cache = process.env.CODEX_APP_SERVER_BRIDGE_NPM_CACHE ?? DEFAULT_CACHE;
+
+  try {
+    await execFileAsync("npm", [
+      "install",
+      "--prefix",
+      prefix,
+      "--ignore-scripts",
+      "--cache",
+      cache,
+      tarball,
+    ], { maxBuffer: 10 * 1024 * 1024 });
+
+    const executable = join(prefix, "node_modules", ".bin", PACKAGE_NAME);
+    await access(executable, constants.X_OK);
+    try {
+      await execFileAsync(executable, ["--unsupported"], { maxBuffer: 1024 * 1024 });
+      throw new Error("installed executable unexpectedly accepted an unsupported invocation");
+    } catch (error) {
+      if (typeof error !== "object" || error === null || !("code" in error) || error.code !== 1) {
+        throw error;
+      }
+      const stdout = "stdout" in error ? String(error.stdout) : "";
+      const stderr = "stderr" in error ? String(error.stderr) : "";
+      assert.equal(stdout, "");
+      assert.match(stderr, /Usage: codex-app-server-bridge app-server/);
+    }
+
+    process.stdout.write(`${JSON.stringify({
+      ok: true,
+      package: basename(tarball),
+      executable: `node_modules/.bin/${PACKAGE_NAME}`,
+      invocation: "verified",
+    })}\n`);
+  } finally {
+    await rm(prefix, { recursive: true, force: true });
+  }
+}
+
+verify().catch((error) => {
+  const message = error instanceof Error ? error.message : String(error);
+  process.stderr.write(`Packed-package verification failed: ${message}\n`);
+  process.exitCode = 1;
+});

+ 55 - 0
test/docs.test.ts

@@ -0,0 +1,55 @@
+import assert from "node:assert/strict";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+
+test("README documents the lifecycle-free immutable Git package workflow", async () => {
+  const [readme, packageText, gitVerifier] = await Promise.all([
+    readFile(new URL("../README.md", import.meta.url), "utf8"),
+    readFile(new URL("../package.json", import.meta.url), "utf8"),
+    readFile(new URL("../scripts/verify-git-package.mjs", import.meta.url), "utf8"),
+  ]);
+  const packageJson = JSON.parse(packageText) as {
+    bin: Record<string, string>;
+    files: string[];
+    scripts: Record<string, string>;
+  };
+  for (const script of [
+    "typecheck",
+    "compile",
+    "test",
+    "verify:runtime",
+    "verify:git-package",
+    "verify:package",
+    "pack:verified",
+  ]) {
+    assert.equal(typeof packageJson.scripts[script], "string");
+    assert.equal(
+      readme.includes(`npm run ${script}`) || (script === "test" && readme.includes("npm test")),
+      true,
+    );
+  }
+  assert.equal(
+    packageJson.bin["codex-app-server-bridge"],
+    "bin/codex-app-server-bridge.js",
+  );
+  assert.deepEqual(packageJson.files, [
+    "bin/",
+    "dist/cli.js",
+    "dist/config.js",
+    "README.md",
+    "LICENSE",
+  ]);
+  assert.match(readme, /npm install --global/);
+  assert.doesNotMatch(readme, /npm run build|prepack/);
+  assert.match(readme, /git\+https:\/\/git\.bodicsek\.host\/bodicsek\/codex-app-server-bridge\.git#v0\.1\.1/);
+  assert.match(readme, /full 40-character commit/i);
+  assert.match(readme, /Do not use\s+`#main`/);
+  assert.match(readme, /end-user install intentionally omits `--prefix`/);
+  assert.match(readme, /0\.1\.0\.tgz/);
+  assert.match(readme, /npm uninstall --global codex-app-server-bridge/);
+  assert.match(readme, /command -v codex-app-server-bridge/);
+  assert.match(readme, /CODEX_PATH/);
+  for (const argument of ["--global", "--prefix", "--ignore-scripts", "--cache"]) {
+    assert.equal(gitVerifier.includes(`"${argument}"`), true, `Git verifier is missing ${argument}`);
+  }
+});

+ 66 - 0
test/generated-runtime.test.ts

@@ -0,0 +1,66 @@
+import assert from "node:assert/strict";
+import { execFileSync, spawnSync } from "node:child_process";
+import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import test from "node:test";
+
+const verifier = new URL("../scripts/verify-generated-runtime.mjs", import.meta.url);
+const gitEnvironment = { ...process.env };
+delete gitEnvironment.GIT_INDEX_FILE;
+
+async function runtimeFixture() {
+  const root = await mkdtemp(join(tmpdir(), "codex-app-server-bridge-runtime-test-"));
+  await Promise.all([
+    mkdir(join(root, "dist"), { recursive: true }),
+    mkdir(join(root, "scripts"), { recursive: true }),
+    mkdir(join(root, "src"), { recursive: true }),
+  ]);
+  await writeFile(join(root, "package.json"), JSON.stringify({
+    private: true,
+    scripts: { compile: "node scripts/compile.mjs" },
+  }));
+  await writeFile(
+    join(root, "scripts", "compile.mjs"),
+    "import { copyFile } from 'node:fs/promises';\nawait Promise.all([copyFile('src/cli.js', 'dist/cli.js'), copyFile('src/config.js', 'dist/config.js')]);\n",
+  );
+  await writeFile(join(root, "src", "cli.js"), "export const version = 1;\n");
+  await writeFile(join(root, "src", "config.js"), "export const configured = true;\n");
+  await writeFile(join(root, "dist", "cli.js"), "export const version = 1;\n");
+  await writeFile(join(root, "dist", "config.js"), "export const configured = true;\n");
+  execFileSync("git", ["init", "--quiet"], { cwd: root, env: gitEnvironment });
+  execFileSync("git", ["add", "."], { cwd: root, env: gitEnvironment });
+  return root;
+}
+
+test("generated-runtime verifier accepts a fresh staged baseline", async () => {
+  const root = await runtimeFixture();
+  try {
+    const result = spawnSync(process.execPath, [verifier.pathname], {
+      cwd: root,
+      encoding: "utf8",
+      env: gitEnvironment,
+    });
+    assert.equal(result.status, 0, result.stderr);
+    assert.match(result.stdout, /Generated runtime is fresh/);
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});
+
+test("generated-runtime verifier rejects source and runtime drift", async () => {
+  const root = await runtimeFixture();
+  try {
+    await writeFile(join(root, "src", "cli.js"), "export const version = 2;\n");
+    const result = spawnSync(process.execPath, [verifier.pathname], {
+      cwd: root,
+      encoding: "utf8",
+      env: gitEnvironment,
+    });
+    assert.notEqual(result.status, 0);
+    assert.match(result.stderr, /generated runtime drift detected/i);
+    assert.match(result.stderr, /cli\.js/);
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});

+ 105 - 0
test/git-package.test.ts

@@ -0,0 +1,105 @@
+import assert from "node:assert/strict";
+import { execFileSync, spawnSync } from "node:child_process";
+import { chmod, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { pathToFileURL } from "node:url";
+import test from "node:test";
+
+const verifier = new URL("../scripts/verify-git-package.mjs", import.meta.url);
+
+function isolatedGitEnvironment() {
+  const environment = { ...process.env };
+  delete environment.GIT_INDEX_FILE;
+  return environment;
+}
+
+function runVerifier(args: string[], cache?: string) {
+  const environment = isolatedGitEnvironment();
+  if (cache !== undefined) environment.CODEX_APP_SERVER_BRIDGE_NPM_CACHE = cache;
+  return spawnSync(process.execPath, [verifier.pathname, ...args], {
+    encoding: "utf8",
+    env: environment,
+    timeout: 60_000,
+  });
+}
+
+test("Git-package verifier requires exactly one immutable package spec without leaking credentials", () => {
+  const secret = "credential-must-not-appear";
+  const invalid = [
+    [],
+    ["git+https://git.example.invalid/team/codex-app-server-bridge.git#main"],
+    [`git+https://user:${secret}@git.example.invalid/team/codex-app-server-bridge.git#main`],
+    ["git+https://git.example.invalid/team/codex-app-server-bridge.git#abc123"],
+    [
+      "git+https://git.example.invalid/team/codex-app-server-bridge.git#v0.1.1",
+      "git+https://git.example.invalid/team/codex-app-server-bridge.git#v0.1.0",
+    ],
+  ];
+
+  for (const args of invalid) {
+    const result = runVerifier(args);
+    assert.notEqual(result.status, 0);
+    assert.equal(result.stderr.includes(secret), false);
+    assert.match(result.stderr, /Git-package verification failed:/);
+  }
+});
+
+test("Git-package verifier installs and launches a disposable prebuilt Git revision", async () => {
+  const root = await mkdtemp(join(tmpdir(), "codex-app-server-bridge-git-test-"));
+  const repository = join(root, "codex-app-server-bridge.git");
+  const cache = join(root, "npm-cache");
+  try {
+    await Promise.all([
+      mkdir(join(repository, "bin"), { recursive: true }),
+      mkdir(join(repository, "dist"), { recursive: true }),
+    ]);
+    await writeFile(join(repository, "package.json"), `${JSON.stringify({
+      name: "codex-app-server-bridge",
+      version: "9.9.9",
+      private: true,
+      type: "module",
+      bin: { "codex-app-server-bridge": "bin/codex-app-server-bridge.js" },
+      files: ["bin/", "dist/cli.js", "dist/config.js"],
+      engines: { node: ">=24" },
+    }, null, 2)}\n`);
+    const launcher = `#!/usr/bin/env node
+if (process.argv.slice(2).join(" ") !== "app-server") {
+  process.stderr.write("codex-app-server-bridge: Usage: codex-app-server-bridge app-server\\n");
+  process.exitCode = 1;
+}
+`;
+    const executable = join(repository, "bin", "codex-app-server-bridge.js");
+    await writeFile(executable, launcher);
+    await chmod(executable, 0o755);
+    await writeFile(join(repository, "dist", "cli.js"), "export async function runBridge() { return 1; }\n");
+    await writeFile(join(repository, "dist", "config.js"), "export const USAGE = 'fixture';\n");
+    const gitEnvironment = isolatedGitEnvironment();
+    execFileSync("git", ["init", "--quiet"], { cwd: repository, env: gitEnvironment });
+    execFileSync("git", ["config", "user.name", "Codex Test"], { cwd: repository, env: gitEnvironment });
+    execFileSync("git", ["config", "user.email", "codex-test@example.invalid"], { cwd: repository, env: gitEnvironment });
+    execFileSync("git", ["add", "."], { cwd: repository, env: gitEnvironment });
+    execFileSync("git", ["commit", "--quiet", "-m", "prebuilt fixture"], { cwd: repository, env: gitEnvironment });
+    const revision = execFileSync("git", ["rev-parse", "HEAD"], {
+      cwd: repository,
+      encoding: "utf8",
+      env: gitEnvironment,
+    }).trim();
+    const packageSpec = `git+${pathToFileURL(repository).href}#${revision}`;
+
+    const result = runVerifier([packageSpec], cache);
+    assert.equal(result.status, 0, result.stderr);
+    const report = JSON.parse(result.stdout) as {
+      ok: boolean;
+      revision: string;
+      storage: string;
+      invocation: string;
+    };
+    assert.equal(report.ok, true);
+    assert.equal(report.revision, revision);
+    assert.equal(report.storage, "isolated-prefix");
+    assert.equal(report.invocation, "verified");
+  } finally {
+    await rm(root, { recursive: true, force: true });
+  }
+});

+ 75 - 0
test/package.test.ts

@@ -0,0 +1,75 @@
+import assert from "node:assert/strict";
+import { spawnSync } from "node:child_process";
+import { readFile } from "node:fs/promises";
+import test from "node:test";
+
+interface PackageContract {
+  version?: string;
+  private?: boolean;
+  bin?: Record<string, string>;
+  files?: string[];
+  scripts?: Record<string, string>;
+}
+
+test("npm package contract exposes the prebuilt private CLI", async () => {
+  const [packageText, launcher, compiledCli, compiledConfig] = await Promise.all([
+    readFile(new URL("../package.json", import.meta.url), "utf8"),
+    readFile(new URL("../bin/codex-app-server-bridge.js", import.meta.url), "utf8"),
+    readFile(new URL("../dist/cli.js", import.meta.url), "utf8"),
+    readFile(new URL("../dist/config.js", import.meta.url), "utf8"),
+  ]);
+  const packageJson = JSON.parse(packageText) as PackageContract;
+
+  assert.equal(packageJson.private, true);
+  assert.equal(packageJson.version, "0.1.1");
+  assert.deepEqual(packageJson.bin, {
+    "codex-app-server-bridge": "bin/codex-app-server-bridge.js",
+  });
+  assert.deepEqual(packageJson.files, [
+    "bin/",
+    "dist/cli.js",
+    "dist/config.js",
+    "README.md",
+    "LICENSE",
+  ]);
+  assert.equal(packageJson.scripts?.compile, "tsc --project tsconfig.build.json");
+  assert.equal(packageJson.scripts?.build, undefined);
+  for (const lifecycle of ["prepack", "prepare", "preinstall", "install", "postinstall"]) {
+    assert.equal(packageJson.scripts?.[lifecycle], undefined);
+  }
+  assert.match(packageJson.scripts?.test ?? "", /npm run compile/);
+  assert.equal(
+    packageJson.scripts?.["verify:runtime"],
+    "node scripts/verify-generated-runtime.mjs",
+  );
+  assert.equal(
+    packageJson.scripts?.["verify:git-package"],
+    "node scripts/verify-git-package.mjs",
+  );
+  assert.match(packageJson.scripts?.["pack:verified"] ?? "", /npm run verify:runtime/);
+  assert.match(packageJson.scripts?.["pack:verified"] ?? "", /npm pack --ignore-scripts/);
+  assert.match(launcher, /^#!\/usr\/bin\/env node\n/);
+  assert.match(compiledCli, /export async function runBridge/);
+  assert.match(compiledConfig, /export function parseEndpoint/);
+});
+
+test("Git ignore rules admit only the compiled bridge runtime", () => {
+  const repository = new URL("..", import.meta.url);
+  const checkIgnored = (path: string) => spawnSync(
+    "git",
+    ["check-ignore", "--no-index", "--quiet", path],
+    { cwd: repository, encoding: "utf8" },
+  ).status;
+
+  assert.equal(checkIgnored("dist/cli.js"), 1);
+  assert.equal(checkIgnored("dist/config.js"), 1);
+  assert.equal(checkIgnored("dist/test/bridge.test.js"), 0);
+  assert.equal(checkIgnored("dist/other-output.js"), 0);
+});
+
+test("packed-artifact verifier rejects a missing tarball argument", () => {
+  const verifier = new URL("../scripts/verify-packed-package.mjs", import.meta.url);
+  const result = spawnSync(process.execPath, [verifier.pathname], { encoding: "utf8" });
+  assert.notEqual(result.status, 0);
+  assert.match(result.stderr, /Packed-package verification failed: usage:/);
+});