Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 4 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ jobs:
- macos-latest
- windows-latest
node:
- 20
- 22.12.0
- 22
- 24
- 26
runs-on: ${{ matrix.os }}
steps:
- name: Check out repository
Expand All @@ -40,7 +41,8 @@ jobs:
cache: npm

- name: Install dependencies
run: npm ci
# On the minimum Node lane, fail if any dependency needs a newer Node.
run: npm ci ${{ matrix.node == '22.12.0' && '--engine-strict' || '' }}

- name: Typecheck, test, and build
run: npm run check
Expand Down
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,25 @@ All notable changes to this project are documented here. The format follows

## [Unreleased]

### Breaking

- The CLI now requires Node.js 22.12 or newer (`engines.node` is
`>=22.12`). Node.js 20 reached end of life in April 2026 and no longer
gets security fixes, and the CLI's own dependencies (`commander@15`,
`vitest@5`) already need 22.12. Upgrade Node.js before installing this
release. `@augmentworks/cli@0.3.9` stays the last version for Node.js 20.
CI now tests Node.js 22.12.0, 22, 24 and 26, with an engine-strict install
on the 22.12.0 lane. The build targets `node22`.

### Fixed

- Windows file-identity checks work on Node.js 22.12.0 through 22.16.x
(and 24.0.0 through 24.1.x). Those releases ship a libuv that reports
`dev: 0` from path `stat`/`lstat` and the volume serial from `fstat`, so
opening a config, packet, lock, or journal looked like the file had been
replaced. The file index still has to match. Two nonzero device ids still
have to match.

### Changed

- Recorded the independent inspection of npm `@augmentworks/cli@0.3.9`.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ claims matter more than connector cleverness.

## Development setup

Use Node.js 20 or 22 and npm:
Use Node.js 22.12 or newer (22 or 24) and npm:

```bash
npm ci
Expand Down
12 changes: 6 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,8 @@ target SDK, and no coding assistant is used in either runtime path.

| Path | What it is | Needs | Status |
| --- | --- | --- | --- |
| Packaged `demo` | Loopback-only synthetic refund target, isolated fixtures, and the real local runner/scorer. Shows a policy bug, then the same packet passing after the policy is enforced. | Node.js 20+ | In this `@augmentworks/cli@0.3.9` package |
| Local `test --local` | Customer-executed scoring of a data-only packet against *your* configured target. Requires no AugmentWorks account and contacts no AugmentWorks service. | Node.js 20+, a connector YAML, and a target you are authorized to call. The bundled starter is an isolated synthetic fixture. | In this `@augmentworks/cli@0.3.9` package |
| Packaged `demo` | Loopback-only synthetic refund target, isolated fixtures, and the real local runner/scorer. Shows a policy bug, then the same packet passing after the policy is enforced. | Node.js 22.12+ | In this `@augmentworks/cli@0.3.9` package |
| Local `test --local` | Customer-executed scoring of a data-only packet against *your* configured target. Requires no AugmentWorks account and contacts no AugmentWorks service. | Node.js 22.12+, a connector YAML, and a target you are authorized to call. The bundled starter is an isolated synthetic fixture. | In this `@augmentworks/cli@0.3.9` package |
| Hosted `test` | Outbound HTTPS relay assessment with a live dashboard. Browser approval does not start a run. Pending hosted judging is never a pass. | Invited workspace, login, and an authorized isolated synthetic or staging target while hosted real-data is release-disabled | In this `@augmentworks/cli@0.3.9` package, including quoted `--estimate` / `--max-credits` |

Product site: [https://augmentworks.ai](https://augmentworks.ai).
Expand All @@ -49,7 +49,7 @@ published package. Do not run `@latest`.

## Support and FAQ example

Prerequisites: Node.js 20 or newer. No AugmentWorks account. Run the published
Prerequisites: Node.js 22.12 or newer. No AugmentWorks account. Run the published
CLI from a directory that is not this checkout; inside the checkout, `npx` can
exit `127` with `augmentworks: not found`.

Expand Down Expand Up @@ -126,7 +126,7 @@ Executable `npx` examples pin **0.3.9**, the identity of this package. This publ

## Packaged demo (this 0.3.9 package)

Prerequisites: Node.js 20 or newer. No API key, login, second terminal, Docker,
Prerequisites: Node.js 22.12 or newer. No API key, login, second terminal, Docker,
database, or model.

From a clone of this repository after `npm ci && npm run build`:
Expand Down Expand Up @@ -173,7 +173,7 @@ still names 0.3.6; the inspection receipt is the later evidence file.

## Hosted quickstart

Prerequisites: Node.js 20 or newer, an invited AugmentWorks workspace, and an
Prerequisites: Node.js 22.12 or newer, an invited AugmentWorks workspace, and an
authorized, isolated synthetic or staging target while hosted real-data is
release-disabled. Hosted access is not a public self-serve signup; do not
assume a trial entitlement.
Expand Down Expand Up @@ -996,7 +996,7 @@ for a safe coding-assistant workflow.

## Compatibility and releases

The package requires Node.js 20+. Configuration and relay envelopes carry an
The package requires Node.js 22.12 or newer. Configuration and relay envelopes carry an
explicit protocol version. Patch releases remain compatible with their v1
schema; incompatible configuration or protocol changes require a new version.
Published releases are expected to use npm trusted publishing with provenance.
Expand Down
2 changes: 1 addition & 1 deletion contracts/discovery-manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"releaseStatus": "development"
},
"runtime": {
"node": ">=20"
"node": ">=22.12"
},
"capabilities": {
"localDemo": true,
Expand Down
2 changes: 1 addition & 1 deletion docs/agent-setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ Do not run `@latest`. Do not pin immutable npm `0.3.5` / `0.3.4` / `0.3.3` or ov
Independent inspection of 0.3.5 is recorded in
`docs/feature-readiness/published-registry-evidence.json`.

Prerequisites: Node.js 20+ and either the packaged fictional demo or a target
Prerequisites: Node.js 22.12 or newer and either the packaged fictional demo or a target
you are authorized to call. Test your chatbot with your questions and business
rules. That hosted scope stays off until a compatible published release is
enabled. Until then, hosted runs
Expand Down
6 changes: 3 additions & 3 deletions examples/faq-chatbot/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,8 +54,8 @@ third mode.

## Prerequisites

- Node.js 20 or newer (`package.json` `engines.node` is `>=20` on
`@augmentworks/cli@0.3.9`).
- Node.js 22.12 or newer (`package.json` `engines.node` is `>=22.12`).
Node.js 20 reached end of life in April 2026.
- A shell whose current directory is **not** a checkout of this CLI
repository. Inside the checkout, `npx` can bind the local package and exit
`127` with `augmentworks: not found`.
Expand Down Expand Up @@ -199,7 +199,7 @@ an AugmentWorks workspace.
| Situation | What you should see | Exit |
| --- | --- | --- |
| `node` is not on `PATH` | The shell reports that `node` or `npx` was not found. The CLI does not start. | Shell status, often `127` |
| Node.js older than 20 | Outside this package's `engines.node` range. The clean-room run used Node.js 20 or newer. | Not a tested CLI exit |
| Node.js older than 22.12 | Outside this package's `engines.node` range. Upgrade Node.js to 22.12 or newer. | Not a tested CLI exit |
| No `.env` and neither variable is set | `ERROR ENV_REQUIRED` for `CHATBOT_BASE_URL` (`target.base_url`) and `CHATBOT_API_KEY` (`target.auth.bearer_env`). `Doctor found configuration errors.` | `2` |
| `.env` created with `cp` and left world-readable | `WARN ENV_FILE_PERMISSIONS` and doctor can still exit `0`. `chmod 600 .env` removes that warning. | `0` |
| `augmentworks.malformed.yaml` | `ERROR YAML_PARSE_ERROR` and the parser message. For this file: `Flow sequence in block collection must be sufficiently indented and end with a ]`, then the config path. `Doctor found configuration errors.` | `2` |
Expand Down
2 changes: 1 addition & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
"THIRD_PARTY_NOTICES.md"
],
"engines": {
"node": ">=20"
"node": ">=22.12"
},
"scripts": {
"build": "tsup",
Expand Down
2 changes: 1 addition & 1 deletion scripts/verify-published-discovery.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ process.stdout.write(
version,
releaseStatus: "published"
},
runtime: { node: metadata.engines?.node ?? ">=20" },
runtime: { node: metadata.engines?.node ?? ">=22.12" },
capabilities: {
localDemo: false,
localTest: true,
Expand Down
3 changes: 2 additions & 1 deletion src/action-gate/ledger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { dirname, join, resolve } from "node:path";
import { AwError } from "../errors.js";
import { realDataError } from "../real-data/errors.js";
import { acquireSecureLock, ensureSecureDirectory } from "../relay/secure-lock.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { findUnsafeSymbolicLinkComponent } from "../system/path-safety.js";
import {
ActionIntentSchema,
Expand Down Expand Up @@ -460,7 +461,7 @@ function isCurrentOwner(uid: number): boolean {
}

function sameIdentity(left: Stats, right: Stats): boolean {
return left.dev === right.dev && left.ino === right.ino;
return sameFileIdentity(left, right);
}

function unsafeLedger(message: string, cause?: unknown): AwError {
Expand Down
3 changes: 2 additions & 1 deletion src/assessment/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { lstat, open } from "node:fs/promises";
import { dirname, relative, resolve, sep } from "node:path";

import { AwError } from "../errors.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { findUnsafeSymbolicLinkComponent } from "../system/path-safety.js";
import { canonicalize, sha256 } from "../util/canonical.js";
import { parseYamlStrict, StrictYamlError } from "../config/yaml.js";
Expand Down Expand Up @@ -301,7 +302,7 @@ async function readBoundedUtf8File(
: 0;
handle = await open(path, constants.O_RDONLY | noFollow);
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.dev !== beforeOpen.dev || metadata.ino !== beforeOpen.ino) {
if (!metadata.isFile() || !sameFileIdentity(metadata, beforeOpen)) {
throw assessmentError(invalidCode, "The file changed while it was being opened.");
}
const bytes = Buffer.alloc(limit + 1);
Expand Down
3 changes: 2 additions & 1 deletion src/commands/preview-mapping.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
} from "../connector/mapping-preview.js";
import { selectResponse } from "../connector/mapping.js";
import { EXIT, AwError, sanitizeTerminal } from "../errors.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { findUnsafeSymbolicLinkComponent } from "../system/path-safety.js";
import { LIMITS } from "../util/limits.js";

Expand Down Expand Up @@ -459,7 +460,7 @@ async function readBoundedRegularFile(path: string, maxBytes: number, label: str
try {
handle = await open(path, fsConstants.O_RDONLY | noFollow | nonBlocking);
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.dev !== beforeOpen.dev || metadata.ino !== beforeOpen.ino) {
if (!metadata.isFile() || !sameFileIdentity(metadata, beforeOpen)) {
throw new BoundedFileError(
label === "fixture" ? "FIXTURE_UNREADABLE" : "CONFIG_FILE_UNREADABLE",
`The ${label} file changed while it was being opened.`,
Expand Down
3 changes: 2 additions & 1 deletion src/config/environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { lstat, open } from "node:fs/promises";
import type { FileHandle } from "node:fs/promises";
import type { Stats } from "node:fs";

import { sameFileIdentity } from "../system/file-identity.js";
import type { Diagnostic } from "./types.js";

export const ENV_NAME_PATTERN = /^[A-Z_][A-Z0-9_]*$/;
Expand Down Expand Up @@ -71,7 +72,7 @@ async function openAndReadEnvironment(envPath: string): Promise<{ readonly metad
if (!metadata.isFile()) throw new Error("The .env path is not a regular file.");
if (
preOpenMetadata !== undefined &&
(preOpenMetadata.dev !== metadata.dev || preOpenMetadata.ino !== metadata.ino)
!sameFileIdentity(preOpenMetadata, metadata)
) {
throw new Error("The .env file changed while it was being opened.");
}
Expand Down
7 changes: 2 additions & 5 deletions src/config/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { lstat, open } from "node:fs/promises";
import { dirname, resolve } from "node:path";

import { AwError } from "../errors.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { findUnsafeSymbolicLinkComponent } from "../system/path-safety.js";
import { loadEnvironment } from "./environment.js";
import { resolveConfig } from "./resolve.js";
Expand Down Expand Up @@ -116,11 +117,7 @@ async function readConfigFile(path: string): Promise<string> {
try {
handle = await open(path, fsConstants.O_RDONLY | noFollow | nonBlocking);
const metadata = await handle.stat();
if (
!metadata.isFile() ||
metadata.dev !== beforeOpen.dev ||
metadata.ino !== beforeOpen.ino
) {
if (!metadata.isFile() || !sameFileIdentity(metadata, beforeOpen)) {
throw new ConfigFileError(
"CONFIG_FILE_UNREADABLE",
"The configuration file changed while it was being opened."
Expand Down
19 changes: 14 additions & 5 deletions src/discovery.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { z } from "zod";
import {
LAST_VERIFIED_PUBLISHED_AT,
LAST_VERIFIED_PUBLISHED_GIT_HEAD,
LAST_VERIFIED_PUBLISHED_NODE_RANGE,
LAST_VERIFIED_PUBLISHED_PACKAGE_VERSION,
LOCAL_PACKET_REFERENCE,
NPM_PACKAGE,
Expand Down Expand Up @@ -178,10 +179,11 @@ const discoveryManifestSchema = z
path: ["provenance", "verifiedAt"]
});
}
if (!value.runtime.node.includes("20")) {
const nodeFloor = /^>=\s*(\d+)(?:\.\d+){0,2}$/.exec(value.runtime.node.trim());
if (nodeFloor == null || Number(nodeFloor[1]) < 20) {
ctx.addIssue({
code: "custom",
message: "runtime.node must describe the supported Node 20+ range",
message: "runtime.node must be a >= range starting at Node 20 or later",
path: ["runtime", "node"]
});
}
Expand All @@ -204,12 +206,16 @@ export function parseDiscoveryManifest(value: unknown): DiscoveryManifestParseRe
return { ok: true, manifest: parsed.data as DiscoveryManifestV1 };
}

/** Must match `engines.node` in package.json. */
const SOURCE_NODE_RANGE = ">=22.12";

export interface BuildDiscoveryManifestOptions {
readonly releaseStatus?: DiscoveryReleaseStatus;
readonly version?: string;
readonly localDemo?: boolean;
readonly sourceCommit?: string | null;
readonly verifiedAt?: string | null;
readonly nodeRange?: string;
}

export function buildDiscoveryManifest(
Expand All @@ -230,7 +236,7 @@ export function buildDiscoveryManifest(
version,
releaseStatus
},
runtime: { node: ">=20" },
runtime: { node: options.nodeRange ?? SOURCE_NODE_RANGE },
capabilities: {
localDemo,
localTest: true,
Expand Down Expand Up @@ -263,13 +269,15 @@ export function publishedDiscoveryManifest(options: {
readonly localDemo: boolean;
readonly sourceCommit: string | null;
readonly verifiedAt: string;
readonly nodeRange?: string;
}): DiscoveryManifestV1 {
return buildDiscoveryManifest({
releaseStatus: "published",
version: options.version,
localDemo: options.localDemo,
sourceCommit: options.sourceCommit,
verifiedAt: options.verifiedAt
verifiedAt: options.verifiedAt,
...(options.nodeRange === undefined ? {} : { nodeRange: options.nodeRange })
});
}

Expand Down Expand Up @@ -311,5 +319,6 @@ export const LAST_VERIFIED_PUBLISHED_DISCOVERY = publishedDiscoveryManifest({
version: LAST_VERIFIED_PUBLISHED_PACKAGE_VERSION,
localDemo: true,
sourceCommit: LAST_VERIFIED_PUBLISHED_GIT_HEAD,
verifiedAt: LAST_VERIFIED_PUBLISHED_AT
verifiedAt: LAST_VERIFIED_PUBLISHED_AT,
nodeRange: LAST_VERIFIED_PUBLISHED_NODE_RANGE
});
3 changes: 2 additions & 1 deletion src/investigation/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { lstat, open } from "node:fs/promises";
import { resolve } from "node:path";

import { AwError } from "../errors.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { findUnsafeSymbolicLinkComponent } from "../system/path-safety.js";
import { SecretRedactor, isSensitiveKey } from "../system/redact.js";
import { MAX_INVESTIGATION_FILE_BYTES, type InvestigationExport } from "./schema.js";
Expand Down Expand Up @@ -71,7 +72,7 @@ async function readBoundedUtf8File(path: string, limit: number): Promise<Buffer>
process.platform !== "win32" && typeof constants.O_NOFOLLOW === "number" ? constants.O_NOFOLLOW : 0;
handle = await open(path, constants.O_RDONLY | noFollow);
const metadata = await handle.stat();
if (!metadata.isFile() || metadata.dev !== beforeOpen.dev || metadata.ino !== beforeOpen.ino) {
if (!metadata.isFile() || !sameFileIdentity(metadata, beforeOpen)) {
throw investigationError(
"INVESTIGATION_NOT_A_FILE",
"The investigation file changed while it was being opened.",
Expand Down
7 changes: 3 additions & 4 deletions src/local/artifacts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import { dirname, isAbsolute, join, parse, relative, resolve, sep } from "node:p
import { redactSecrets } from "../connector/mapping.js";
import { inspectOutbound, type DataPolicyContext } from "../data-policy/index.js";
import { AwError } from "../errors.js";
import { sameFileIdentity } from "../system/file-identity.js";
import { assertJsonLimits } from "../util/limits.js";
import { sha256Json } from "./canonical.js";
import { LOCAL_TRUST_NOTICE, renderLocalReportHtml } from "./report-html.js";
Expand Down Expand Up @@ -402,8 +403,7 @@ async function removeOwnedOutput(
metadata === undefined ||
metadata.isSymbolicLink() ||
!metadata.isDirectory() ||
metadata.dev !== identity.dev ||
metadata.ino !== identity.ino
!sameFileIdentity(metadata, identity)
) {
return;
}
Expand All @@ -419,8 +419,7 @@ async function unlinkOwnedFile(path: string, identity: FileIdentity): Promise<vo
metadata === undefined ||
metadata.isSymbolicLink() ||
!metadata.isFile() ||
metadata.dev !== identity.dev ||
metadata.ino !== identity.ino
!sameFileIdentity(metadata, identity)
) {
return;
}
Expand Down
Loading
Loading