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
5 changes: 5 additions & 0 deletions .changeset/harness-mcp-config-write-safety.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@inkeep/open-knowledge": minor
---

Make MCP server registration non-destructive and format-preserving. `ok init` and the desktop reclaim sweeps now only ever add OpenKnowledge's own entry to a harness config: comments, formatting, key order, value types, and byte-level encoding (BOM, line endings, trailing newline) are preserved. A config that can't be parsed safely — invalid JSON/TOML, a duplicate server block, or an oversized file — is left byte-for-byte untouched and reported as `left unchanged (<reason>)` rather than being reset or rewritten. Codex `config.toml` is edited through a new napi-rs `toml_edit` addon so 64-bit integers and microsecond datetimes are no longer mis-flagged, and symlinked configs are written through to their real target instead of being replaced.
188 changes: 188 additions & 0 deletions .github/workflows/native-config-prebuild.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
name: native-config prebuild

# Cross-platform prebuild matrix for the `@inkeep/open-knowledge-native-config`
# napi-rs addon (the toml_edit parse + format-preserving Codex TOML edit engine).
# Builds one `.node` per target so the published CLI can bundle all of them into
# `packages/cli/dist/native/` (Option 3 — bundle into the CLI tarball; see
# reports/native-addon-npm-distribution/REPORT.md). Standalone by design: it is
# NOT grafted into release.yml so the pure-JS beta-cadence publisher stays
# untouched; the release flow downloads these artifacts before the CLI build.
#
# Mirrors the napi-rs package-template CI build matrix (runner mapping +
# --use-napi-cross for linux-gnu cross builds + cargo-zigbuild for musl). Every
# `uses:` is SHA-pinned with a version comment, matching release.yml.

on:
workflow_dispatch:
push:
paths:
- "packages/native-config/**"
- ".github/workflows/native-config-prebuild.yml"
pull_request:
paths:
- "packages/native-config/**"
- ".github/workflows/native-config-prebuild.yml"

# Per-ref isolation — a new push to the same branch cancels the in-flight run.
# Deliberately NOT the `ok-release-cadence` group (that serializes beta/stable
# cuts); this prebuild is independent of the release pipeline.
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

jobs:
build:
name: build - ${{ matrix.settings.target }}
runs-on: ${{ matrix.settings.host }}
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
settings:
# macos-26 is Apple-Silicon (arm64); it cross-builds the x64 darwin
# target with the same SDK, so both darwin targets share the host.
- host: macos-26
target: aarch64-apple-darwin
napiFlags: ""
- host: macos-26
target: x86_64-apple-darwin
napiFlags: ""
# --use-napi-cross pulls the zig-backed cross toolchain napi-rs ships
# for glibc Linux targets, so an x64 ubuntu host builds both arches.
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
napiFlags: "--use-napi-cross"
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
napiFlags: "--use-napi-cross"
# -x routes the build through cargo-zigbuild for musl (Alpine) targets.
- host: ubuntu-latest
target: x86_64-unknown-linux-musl
napiFlags: "-x"
- host: ubuntu-latest
target: aarch64-unknown-linux-musl
napiFlags: "-x"
- host: windows-latest
target: x86_64-pc-windows-msvc
napiFlags: ""
- host: windows-latest
target: aarch64-pc-windows-msvc
napiFlags: ""
steps:
- name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2

- name: Setup Bun
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2.2.0
with:
bun-version-file: .bun-version

- name: Setup Node
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: "24"

- name: Setup Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable
targets: ${{ matrix.settings.target }}

- name: Cache cargo + target
uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4
with:
path: |
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
packages/native-config/target/
# Hash the manifest + lockfile into the key so a crate bump can't be
# served a stale registry/target tree from an older resolution. The
# restore-keys prefix still allows a partial warm-start (registry +
# unchanged crates) on a manifest change.
key: ${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}-${{ hashFiles('packages/native-config/Cargo.lock', 'packages/native-config/Cargo.toml') }}
restore-keys: |
${{ matrix.settings.target }}-cargo-${{ matrix.settings.host }}-

- name: Setup Zig (musl targets only)
if: contains(matrix.settings.target, 'musl')
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
with:
version: 0.14.1

- name: Install cargo-zigbuild (musl targets only)
if: contains(matrix.settings.target, 'musl')
uses: taiki-e/install-action@bffeee26d4db9be238a4ea78d8826604ebcb594d # v2.82.5
with:
tool: cargo-zigbuild

- name: Install JS dependencies
run: bash scripts/bun-install-ci.sh

- name: Build addon
working-directory: packages/native-config
shell: bash
# Appends the per-target flags to the package's own
# `napi build --platform --release` script.
run: bun run build -- --target ${{ matrix.settings.target }} ${{ matrix.settings.napiFlags }}

- name: Upload binding
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: bindings-${{ matrix.settings.target }}
path: packages/native-config/*.node
if-no-files-found: error

combine:
name: combine + verify all targets present
needs: build
runs-on: ubuntu-latest
timeout-minutes: 10
steps:
- name: Download all bindings
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
with:
path: artifacts

- name: Assert every target produced exactly one binding
shell: bash
run: |
set -euo pipefail
targets=(
aarch64-apple-darwin
x86_64-apple-darwin
x86_64-unknown-linux-gnu
aarch64-unknown-linux-gnu
x86_64-unknown-linux-musl
aarch64-unknown-linux-musl
x86_64-pc-windows-msvc
aarch64-pc-windows-msvc
)
missing=0
for t in "${targets[@]}"; do
dir="artifacts/bindings-$t"
count=$(find "$dir" -name '*.node' 2>/dev/null | wc -l | tr -d ' ')
if [ "$count" -lt 1 ]; then
echo "::error::no .node found for target $t (artifact bindings-$t)"
missing=1
else
echo "ok: $t -> $(find "$dir" -name '*.node')"
fi
done
total=$(find artifacts -name '*.node' | wc -l | tr -d ' ')
echo "total .node files: $total"
if [ "$total" -ne 8 ]; then
echo "::error::expected 8 .node binaries (one per target), found $total"
missing=1
fi
[ "$missing" -eq 0 ]

- name: Upload combined bindings bundle
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: native-config-bindings-all
path: artifacts/**/*.node
if-no-files-found: error
85 changes: 85 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -155,6 +155,91 @@ jobs:
- name: Install
run: bash scripts/bun-install-ci.sh

# The cross-platform native-config `.node` binaries are produced by the
# standalone native-config-prebuild workflow (kept separate so the beta
# cadence stays pure-JS-fast). Stage the latest successful run's bundle
# into packages/native-config/ so the cli `build:native` step copies all
# eight platforms into dist/native/ before publish. The bundle must come
# from a prebuild run whose head commit is an ancestor of the release
# commit (provenance), so the native code we ship was built from source
# actually in this release rather than a later main commit racing the cut.
# Beta is resilient by design: a missing, failed, stale, or incomplete
# bundle warns and ships host-only, with non-host platforms on the
# non-destructive smol-toml fallback, so a beta is never worse than
# host-only and the cadence never blocks. Stable @latest is held higher:
# an incomplete set hard-fails the cut rather than silently shipping
# degraded native fidelity to the default install channel. Binaries are
# ABI-stable (napi6); the latest qualifying run's set is reused until
# native-config changes and a fresh prebuild run completes.
- name: Stage native-config prebuilt binaries
env:
GH_TOKEN: ${{ github.token }}
# Stable @latest cuts arrive as repository_dispatch (publish-stable);
# beta + manual recovery arrive as push/workflow_dispatch. Only stable
# hard-fails on an incomplete native-config set.
IS_STABLE: ${{ github.event_name == 'repository_dispatch' }}
run: |
# Record the staged-binary outcome in the job summary, then either
# warn-and-proceed (beta) or hard-fail (stable). A stable @latest
# publish shipping host-only native binaries is a fidelity regression,
# so it pages rather than ships silently degraded.
summarize() {
echo "Staged native-config binaries: $1" >> "$GITHUB_STEP_SUMMARY"
}
degrade_or_fail() {
# $1: human-readable reason, $2: staged-count label for the summary
summarize "$2 — $1"
if [ "$IS_STABLE" = "true" ]; then
echo "::error::$1; refusing to cut a stable @latest release with an incomplete native-config binary set"
exit 1
fi
echo "::warning::$1; CLI ships the host binary only (other platforms use the smol-toml fallback)"
exit 0
}

# Scope to runs on `main` triggered by `push` only. The prebuild also
# runs on `pull_request` (including from the public mirror's external
# PRs), so without this filter the latest-green run could be an
# unmerged branch whose Rust source is not what shipped to main — a
# supply-chain gap for native code executed on every CLI invocation.
run_id=$(gh run list --workflow=native-config-prebuild.yml --branch main --event push \
--status success --limit 1 --json databaseId --jq '.[0].databaseId // empty' 2>/dev/null || true)
if [ -z "$run_id" ]; then
degrade_or_fail "no successful native-config-prebuild run on main found" "0/8"
fi

# Provenance: the prebuild run's head commit must be an ancestor of the
# release commit, so the binaries staged were built from source that is
# actually in this release. fetch-depth:0 in checkout has full history.
head_sha=$(gh run view "$run_id" --json headSha -q .headSha 2>/dev/null || true)
if [ -z "$head_sha" ]; then
degrade_or_fail "could not resolve head commit of native-config-prebuild run $run_id" "0/8"
fi
if ! git merge-base --is-ancestor "$head_sha" HEAD 2>/dev/null; then
degrade_or_fail "native-config-prebuild run $run_id (commit $head_sha) is not an ancestor of the release commit" "0/8"
fi

if ! gh run download "$run_id" --name native-config-bindings-all --dir /tmp/nc-bindings; then
degrade_or_fail "could not download native-config-bindings-all from run $run_id" "0/8"
fi
find /tmp/nc-bindings -name '*.node' -exec cp -f {} packages/native-config/ \;
count=$(find packages/native-config -maxdepth 1 -name '*.node' | wc -l | tr -d ' ')
echo "staged $count native-config prebuilt binaries from run $run_id (commit $head_sha)"
if [ "$count" -lt 8 ]; then
degrade_or_fail "staged only $count/8 native-config binaries" "$count/8"
fi
summarize "$count/8 from run $run_id (commit $head_sha)"

# `bun run build` builds the native-config addon's host `.node` via
# `napi build` (turbo dep of the cli build), which needs a Rust toolchain.
# Pin it explicitly rather than relying on whatever `ubuntu-latest`
# preinstalls, so an image change can't silently break the whole publish at
# the unguarded cargo invocation. Mirrors native-config-prebuild.yml.
- name: Setup Rust toolchain for the native-config build
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: stable

# `bun changeset publish` invokes `npm publish` per package, which
# in turn fires the cli's `prepublishOnly` (-> `bun run build`). The
# cli's own build chain includes `cp -r ../app/dist dist/public`
Expand Down
Loading
Loading