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
25 changes: 14 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -93,12 +93,12 @@ jobs:
- name: Test native crate
run: cargo test --workspace --locked

- name: Build host binding
run: pnpm native:build

- name: Build package
run: pnpm build

- name: Build and stage host binding
run: pnpm native:build

- name: Test native mode auto
run: node scripts/native-mode-smoke.mjs auto

Expand Down Expand Up @@ -126,8 +126,8 @@ jobs:
npm install --global pnpm@10.34.5
pnpm install --frozen-lockfile
cargo test --workspace --locked
pnpm native:build
pnpm build
pnpm native:build
node scripts/native-mode-smoke.mjs auto
node scripts/native-mode-smoke.mjs require
node scripts/native-mode-smoke.mjs off
Expand Down Expand Up @@ -160,8 +160,8 @@ jobs:
- name: Audit
run: cargo audit --deny warnings

release-graph-smoke:
name: Release graph smoke (${{ matrix.os }})
package-smoke:
name: Bundled package smoke (${{ matrix.os }})
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
Expand Down Expand Up @@ -191,15 +191,18 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Build host binding
- name: Build package
run: pnpm build

- name: Build and stage host binding
run: pnpm native:build

- name: Pack and smoke test release graph
run: pnpm release-graph:smoke
- name: Pack and smoke test bundled package
run: pnpm package:smoke

- name: Upload behavior proof
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: release-graph-proof-${{ runner.os }}-${{ runner.arch }}
path: release-graph-proof.json
name: bundled-package-proof-${{ runner.os }}-${{ runner.arch }}
path: release-artifacts/manifest.json
if-no-files-found: error
127 changes: 28 additions & 99 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Validate tag, changelog, and package versions
- name: Validate tag, changelog, and versions
env:
RELEASE_TAG: ${{ github.ref_name }}
run: |
Expand All @@ -63,25 +63,19 @@ jobs:
git merge-base --is-ancestor "${release_commit}" origin/main
node <<'NODE'
const { readFileSync } = require("node:fs");
const { join } = require("node:path");
const version = process.env.RELEASE_TAG.slice(1);
const dirs = [
".", "native", "npm/linux-x64-gnu", "npm/linux-x64-musl",
"npm/linux-arm64-gnu", "npm/linux-arm64-musl", "npm/darwin-x64",
"npm/darwin-arm64", "npm/win32-x64-msvc",
];
for (const dir of dirs) {
const pkg = JSON.parse(readFileSync(join(dir, "package.json"), "utf8"));
if (pkg.version !== version) throw new Error(`${pkg.name} is ${pkg.version}, expected ${version}`);
const pkg = JSON.parse(readFileSync("package.json", "utf8"));
if (pkg.name !== "@openclaw/fs-safe" || pkg.version !== version) {
throw new Error(`root package is ${pkg.name}@${pkg.version}, expected @openclaw/fs-safe@${version}`);
}
const nativePkg = JSON.parse(readFileSync("native/package.json", "utf8"));
if (!nativePkg.private || nativePkg.version !== version) {
throw new Error(`private native build workspace must declare ${version}`);
}
const cargo = readFileSync("native/Cargo.toml", "utf8");
if (!cargo.match(new RegExp(`^version = "${version.replaceAll(".", "\\.")}"$`, "m"))) {
throw new Error(`native/Cargo.toml does not declare ${version}`);
}
const loader = readFileSync("native/index.js", "utf8");
if (!loader.includes("require('./package.json').version")) {
throw new Error("native loader must derive its version check from package.json");
}
NODE
package_version="$(node -p "require('./package.json').version")"
[ "${RELEASE_TAG}" = "v${package_version}" ]
Expand Down Expand Up @@ -166,15 +160,15 @@ jobs:

- name: Build native binding
if: matrix.cross == 'none'
run: pnpm --filter @openclaw/fs-safe-native exec napi build --release --platform --target ${{ matrix.target }} --output-dir ../artifacts
run: pnpm --filter @openclaw/fs-safe-native-build exec napi build --release --platform --target ${{ matrix.target }} --output-dir ../artifacts

- name: Build native binding with napi cross toolchain
if: matrix.cross == 'napi'
run: pnpm --filter @openclaw/fs-safe-native exec napi build --release --platform --target ${{ matrix.target }} --use-napi-cross --output-dir ../artifacts
run: pnpm --filter @openclaw/fs-safe-native-build exec napi build --release --platform --target ${{ matrix.target }} --use-napi-cross --output-dir ../artifacts

- name: Build native binding with Zig
if: matrix.cross == 'zig'
run: pnpm --filter @openclaw/fs-safe-native exec napi build --release --platform --target ${{ matrix.target }} --cross-compile --output-dir ../artifacts
run: pnpm --filter @openclaw/fs-safe-native-build exec napi build --release --platform --target ${{ matrix.target }} --cross-compile --output-dir ../artifacts

- name: Upload binding
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
Expand All @@ -185,7 +179,7 @@ jobs:
retention-days: 1

collect:
name: Validate release packages
name: Assemble and validate @openclaw/fs-safe
if: github.ref_protected
needs: prebuild
runs-on: ubuntu-latest
Expand All @@ -210,50 +204,39 @@ jobs:
- name: Install dependencies
run: pnpm install --frozen-lockfile

- name: Download bindings
- name: Build package
run: pnpm build

- name: Download all seven bindings
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
pattern: binding-*
path: artifacts
merge-multiple: true

- name: Assemble platform packages
run: pnpm --filter @openclaw/fs-safe-native exec napi artifacts --output-dir ../artifacts --npm-dir ../npm
- name: Assemble bundled bindings
run: node scripts/assemble-native-binaries.mjs --source artifacts --destination dist/native

- name: Build root package
run: pnpm build

- name: Pack and smoke test every package
- name: Pack and smoke test the single package
run: node scripts/check-release-packages.mjs --output release-artifacts

- name: Upload release packages
- name: Upload release package
uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4
with:
name: release-packages
name: release-package
path: release-artifacts/*
if-no-files-found: error
retention-days: 7

publish-platforms:
name: Publish ${{ matrix.package }}
publish:
name: Publish @openclaw/fs-safe
if: github.ref_protected
needs: collect
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
package:
- "@openclaw/fs-safe-native-linux-x64-gnu"
- "@openclaw/fs-safe-native-linux-x64-musl"
- "@openclaw/fs-safe-native-linux-arm64-gnu"
- "@openclaw/fs-safe-native-linux-arm64-musl"
- "@openclaw/fs-safe-native-darwin-x64"
- "@openclaw/fs-safe-native-darwin-arm64"
- "@openclaw/fs-safe-native-win32-x64-msvc"
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -267,73 +250,19 @@ jobs:
- name: Update npm
run: npm install --global npm@${{ env.NPM_VERSION }}

- name: Download release packages
- name: Download release package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-packages
name: release-package
path: release-artifacts

- name: Publish or verify
run: node scripts/publish-or-verify.mjs --package "${{ matrix.package }}" --artifacts release-artifacts

publish-native:
name: Publish @openclaw/fs-safe-native
if: github.ref_protected
needs: publish-platforms
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: https://registry.npmjs.org
- name: Update npm
run: npm install --global npm@${{ env.NPM_VERSION }}
- name: Download release packages
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-packages
path: release-artifacts
- name: Publish or verify
run: node scripts/publish-or-verify.mjs --package "@openclaw/fs-safe-native" --artifacts release-artifacts

publish-root:
name: Publish @openclaw/fs-safe
if: github.ref_protected
needs: publish-native
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
id-token: write
steps:
- name: Check out
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Set up Node
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version: ${{ env.NODE_VERSION }}
registry-url: https://registry.npmjs.org
- name: Update npm
run: npm install --global npm@${{ env.NPM_VERSION }}
- name: Download release packages
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-packages
path: release-artifacts
- name: Publish or verify
run: node scripts/publish-or-verify.mjs --package "@openclaw/fs-safe" --artifacts release-artifacts

release:
name: Publish GitHub Release
if: github.ref_protected
needs: publish-root
needs: publish
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
Expand All @@ -349,10 +278,10 @@ jobs:
with:
node-version: ${{ env.NODE_VERSION }}

- name: Download release packages
- name: Download release package
uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
name: release-packages
name: release-package
path: release-artifacts

- name: Generate release notes and proof
Expand Down
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ dist/
node_modules
target/
native/*.node
native/index.js
native/index.d.ts
coverage/
release-graph-proof*.json
artifacts*/
release-artifacts*/
*.tsbuildinfo
.clawpatch/
.deepsec/
Expand Down
4 changes: 2 additions & 2 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Changelog

## 0.5.0 - 2026-07-26
## 0.5.0 - Unreleased

### Highlights

Expand All @@ -9,7 +9,7 @@
- Add native fd-relative ZIP and TAR extraction/read support with gzip, zstd, and bzip2 streaming; TypeScript evaluates the shared entry policy before Rust creates any output, and zstd/bzip2 report a typed native-required error when no binding is available.
- Bound PAX, GNU long-name/link, and sparse metadata with one `maxMetaEntryBytes` policy shared by node-tar and the native fixed-header metering reader, including typed failures for oversized or malformed metadata.
- Add `Root.walk()` subtree pruning and partial directory-error reporting for bounded best-effort consumers, plus a shared `maxEntryPathComponents` archive limit that rejects implicit-directory depth attacks before either extraction path creates output.
- Ship the optional `@openclaw/fs-safe-native` helper and seven platform packages from this repository for fd-relative opens, guarded directory creation and hardlinks, atomic no-replace rename, and file identity checks.
- Bundle all seven prebuilt native binaries inside the single `@openclaw/fs-safe` package for fd-relative opens, guarded directory creation and hardlinks, atomic no-replace rename, and file identity checks. This deliberately increases the package size in exchange for deterministic installs with no optional platform packages, downloads, postinstall step, or consumer Rust build; unsupported platforms silently use the guarded JavaScript fallback in `auto` mode.

### Security and Correctness

Expand Down
20 changes: 12 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,10 @@ pnpm add @openclaw/fs-safe

Node 22 or newer. Core root/path/json/temp helpers avoid framework dependencies. Archive helpers use optional `jszip` and `tar` dependencies for ZIP/TAR support; installs that omit optional dependencies can still use every non-archive subpath.

The optional native package supplies fd-relative and atomic no-replace
primitives that Node does not expose directly. Configure its lazy loader before
first use when you need a strict environment policy:
The package bundles prebuilt native bindings for seven supported targets. They
supply fd-relative and atomic no-replace primitives that Node does not expose
directly. Configure the lazy loader before first use when you need a strict
environment policy:

```ts
import { configureFsSafeNative } from "@openclaw/fs-safe";
Expand All @@ -71,10 +72,13 @@ configureFsSafeNative({ mode: "off" }); // guarded JavaScript only
configureFsSafeNative({ mode: "require" }); // fail closed if the binding is unavailable
```

Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. The seven platform
packages are prebuilt; consumers do not need Rust. Without a matching package,
`auto` retains lexical and canonical root checks, no-follow opens, guarded
temp+rename writes, and post-write identity verification. See the [native
Equivalent env var: `FS_SAFE_NATIVE_MODE=auto|off|require`. All seven binaries
ship inside `@openclaw/fs-safe`; there are no platform packages, postinstall
steps, downloads, or consumer Rust builds. This makes the tarball larger than
a per-platform package, but makes installation deterministic. On a platform
without a bundled binary, `auto` silently retains lexical and canonical root
checks, no-follow opens, guarded temp+rename writes, and post-write identity
verification. See the [native
helper policy](docs/native-helper.md) for the exact boundary and deployment
tradeoff, and [native architecture](docs/native.md) for the platform mechanisms
and policy ownership model.
Expand All @@ -85,7 +89,7 @@ and guarded JavaScript results are best-effort. See the [security model](docs/se

## Migrating from the Python helper

Version 0.5 replaces the persistent Python worker with optional prebuilt native
Version 0.5 replaces the persistent Python worker with bundled prebuilt native
bindings. The modes map directly: `configureFsSafePython({ mode: "auto" })`
becomes `configureFsSafeNative({ mode: "auto" })`, and likewise for `off` and
`require`. Replace `FS_SAFE_PYTHON_MODE` with `FS_SAFE_NATIVE_MODE`; remove
Expand Down
24 changes: 7 additions & 17 deletions RELEASE-PREREQS.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,19 +4,9 @@ Complete this checklist in npm before pushing the first `v0.5.0` tag. The releas

## Package setup

Create or confirm these public packages under the `@openclaw` scope:
Exactly one package is published: `@openclaw/fs-safe`. It already exists, is public, and is owned by the OpenClaw npm organization.

- `@openclaw/fs-safe-native-linux-x64-gnu`
- `@openclaw/fs-safe-native-linux-x64-musl`
- `@openclaw/fs-safe-native-linux-arm64-gnu`
- `@openclaw/fs-safe-native-linux-arm64-musl`
- `@openclaw/fs-safe-native-darwin-x64`
- `@openclaw/fs-safe-native-darwin-arm64`
- `@openclaw/fs-safe-native-win32-x64-msvc`
- `@openclaw/fs-safe-native`
- `@openclaw/fs-safe`

For every package above, open npm package settings and configure the same trusted publisher:
Open its npm package settings and configure this trusted publisher:

| npm trusted-publisher field | Required value |
|---|---|
Expand All @@ -26,14 +16,14 @@ For every package above, open npm package settings and configure the same truste
| Workflow filename | `release.yml` |
| Environment | Leave empty; this workflow does not use a GitHub environment. |

Confirm each package is public, the OpenClaw npm organization owns it, and the publishing maintainers have 2FA enabled. Do not add `NPM_TOKEN` or an automation token to GitHub.
Confirm the publishing maintainers have 2FA enabled. Do not add `NPM_TOKEN` or an automation token to GitHub. The former native loader and platform package names were never published, so there is nothing to deprecate or unpublish.

## Release commit and tag

- Replace `0.5.0-dev.0` with `0.5.0` in the root, native loader, Rust crate, all seven platform package manifests, and generated native loader version checks.
- Run `pnpm install` so `pnpm-lock.yaml` matches the stable package versions.
- Confirm `0.5.0` in the root package, private native build workspace, and Rust crate.
- Run `pnpm install` so `pnpm-lock.yaml` matches the stable package version.
- Change `## 0.5.0 - Unreleased` in `CHANGELOG.md` to the release date.
- Run `pnpm check`, `pnpm test:security`, `cargo test --workspace --locked`, `pnpm docs:site`, and `git diff --check` on the release commit.
- Run `pnpm check`, `pnpm test:security`, `cargo test --workspace --locked`, `cargo clippy --workspace --locked -- -D warnings`, `pnpm docs:site`, and `git diff --check` on the release commit.
- Merge the release commit to `main`, then create an annotated, protected `v0.5.0` tag on that exact commit.

The workflow validates the protected annotated tag, `main` ancestry, all nine package versions, package bytes, install/import behavior, and changelog-derived release notes before it publishes anything.
The workflow validates the protected annotated tag, `main` ancestry, package and crate versions, all seven non-empty native binaries, the single tarball's bytes, install/import behavior, native loading and fallback behavior, and changelog-derived release notes before it publishes anything.
Loading
Loading