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
8 changes: 8 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -1,6 +1,14 @@
* text=auto eol=lf
*.raimodel binary

# THIRD-PARTY-NOTICES.md is generated by `cargo about` from `about.hbs`, and CI
# regenerates it and diffs. That gate only holds if the committed bytes are the
# bytes a Linux checkout produces, so the file must be LF like everything else
# — which `* text=auto eol=lf` above already guarantees. It is called out here
# only to record that it needs no exception: an earlier `-text` entry was added
# in the belief that upstream license texts contributed CRLF lines, and
# regenerating from an LF template produces a file with none.

# Windows batch files must reach cmd.exe with CRLF: with LF-only line endings
# cmd mis-parses block structure (a nested `exit /b N` silently returns 0).
*.cmd text eol=crlf
Expand Down
89 changes: 87 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,63 @@ jobs:
- name: Verify publishable crate archives
run: cargo package -p classeve-rai-infer -p classeve-rai-compress --locked

# Licenses, bans and sources over the whole graph. Nothing previously
# checked what a dependency's license actually permitted, which matters
# because a release archive is a binary distribution of every crate below
# us. Advisories are deliberately not listed here: `cargo audit` above
# owns those and reads `.cargo/audit.toml`, and duplicating the check
# would mean a second RustSec parser and a second ignore list.
- name: License, ban and source policy
run: |
cargo install cargo-deny --version 0.18.2 --locked
cargo deny --all-features check licenses bans sources

# The attribution that ships with the binaries has to describe the
# binaries that ship. Regenerating and diffing is what makes a dependency
# bump change the notices in the same commit, instead of six months later
# when somebody notices.
- name: Third-party notices are current
run: |
cargo install cargo-about --version 0.7.1 --locked
# cargo-about asks api.clearlydefined.io for the license text of any
# crate whose own files do not settle it. When that request fails it
# warns and carries on, and the file it then writes groups a dozen
# crates under different license texts and drops whole sections — a
# real difference, produced by a network blip rather than by the
# dependency graph. Diffing that against the committed copy would fail
# this job with a message telling the author to regenerate a file that
# was already correct. The request is flaky enough to have failed on
# three of four attempts from one host while `curl` to the same
# endpoint returned 200, so retry it here rather than spend a human on
# it, and only give up — with the real reason — if every attempt fails.
#
# `--offline` would remove the dependency on that service outright and
# is deliberately not used: its own documentation says it falls back to
# a default license file, "missing eg. copyright information", and
# reproducing that information is the entire point of this file.
for attempt in 1 2 3 4 5; do
cargo about generate --manifest-path rai-infer/Cargo.toml \
-c about.toml about.hbs -o /tmp/notices.md 2> /tmp/notices.warn
grep -q 'failed to request license information' /tmp/notices.warn || break
echo "attempt $attempt: the clearly-defined lookup failed, retrying"
sleep $((attempt * 5))
done
cat /tmp/notices.warn

if grep -q 'failed to request license information' /tmp/notices.warn; then
echo "::error::cargo-about could not reach api.clearlydefined.io on any of"
echo "::error::five attempts, so the regenerated notices are incomplete and"
echo "::error::this comparison is void. This is a network failure, not a"
echo "::error::stale THIRD-PARTY-NOTICES.md."
exit 1
fi

if ! diff -u THIRD-PARTY-NOTICES.md /tmp/notices.md; then
echo "::error::THIRD-PARTY-NOTICES.md is stale — regenerate it with:"
echo "::error:: cargo about generate --manifest-path rai-infer/Cargo.toml -c about.toml about.hbs -o THIRD-PARTY-NOTICES.md"
exit 1
fi

container:
name: Container build and MCP smoke
runs-on: ubuntu-24.04
Expand All @@ -160,8 +217,36 @@ jobs:
with:
persist-credentials: false

- name: Build pinned image
run: docker build --tag rai-audit:local .
# Two images, and the default one is asserted to be the product. A bare
# `docker build .` used to produce the memory service, so the artifact a
# user gets by following the obvious command was not what this repository
# ships. The CLI smoke below is what keeps that from drifting back.
- name: Build the default image (must be the inference CLI)
run: docker build --tag rai-cli:local .

- name: Smoke-test the default image
run: |
set -euo pipefail
# `rai --help` names every product subcommand. If the default stage
# ever points at the server again, this fails immediately.
docker run --rm rai-cli:local rai --help | tee /tmp/rai-cli-help.txt
for sub in convert run serve models perplexity; do
grep -qE "^\s*$sub\b" /tmp/rai-cli-help.txt || { echo "::error::default image does not offer 'rai $sub' — it is not the inference CLI"; exit 1; }
done
# The product image must carry no TLS or HTTP stack. Same assertion
# the release workflow makes about the archive binaries.
cid=$(docker create rai-cli:local)
docker cp "$cid:/usr/local/bin/rai" /tmp/rai-bin
docker rm "$cid" >/dev/null
for marker in rustls webpki BoringSSL reqwest; do
if grep -qaiF "$marker" /tmp/rai-bin; then
echo "::error::the rai binary contains '$marker' — the engine must carry no TLS or HTTP stack"
exit 1
fi
done

- name: Build the non-product memory-service image
run: docker build --target server --tag rai-audit:local .

# This used to assert only that the initialize response contained
# "rai-server" — i.e. that the binary starts and can complete a
Expand Down
93 changes: 79 additions & 14 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,12 @@ jobs:
cat > notes.md <<'NOTES'
Prebuilt binaries for x86-64 Linux and Windows, and for macOS on both
Intel and Apple Silicon: the
`rai` inference CLI (`convert`, `run`, `serve`, `models`) and
`rai-server`, plus the deprecated `rai-convert`, `rai-generate`, and
`rai-chat` wrappers.
`rai` inference CLI (`convert`, `run`, `serve`, `models`), plus the
deprecated `rai-convert`, `rai-generate`, and `rai-chat` wrappers.

Every binary in these archives is the inference engine. Nothing in
them opens a network connection to anything but your own loopback
interface.

**Verify your download before running it.** Fetch `SHA256SUMS` from
this release, put it next to the archive, and run:
Expand Down Expand Up @@ -318,14 +321,19 @@ jobs:
exit 1 ;;
esac

# Same command the CI release-build step runs, plus --locked. Only the
# two crates that own shipped binaries are built.
# Same command the CI release-build step runs, plus --locked. RAI is
# `rai-infer` plus `rai-compress`, and `rai-infer` owns every shipped
# binary, so it is the only package the release compiles. The
# memory/reasoning crates (rai-server, rai-core, rem-nra) share this
# workspace and are not the product: building them here is what used to
# put an OpenAI-calling binary — and its reqwest/rustls/ring dependency
# tree — inside an archive whose whole claim is that nothing leaves the
# machine.
- name: Build release binaries
shell: bash
run: |
cargo build --locked --release \
--package classeve-rai-infer \
--package classeve-rai-server
--package classeve-rai-infer

# A compile is not an execution: a green build has shipped a stale or
# broken exe before. Every binary the archive will carry answers
Expand All @@ -339,7 +347,7 @@ jobs:
set -euo pipefail
ext=""
if [ "${RUNNER_OS}" = "Windows" ]; then ext=".exe"; fi
for bin in rai rai-server rai-convert rai-generate rai-chat; do
for bin in rai rai-convert rai-generate rai-chat; do
out="$("./target/release/${bin}${ext}" --version)"
printf '%s: %s\n' "$bin" "$out"
# Anchored to the end: a substring match would let 0.2.10 pass a
Expand Down Expand Up @@ -369,7 +377,7 @@ jobs:
# a broken release, not a smaller archive — fail loudly. `rai` is the
# CLI the documentation and the launchers use; the three legacy names
# are deprecated wrappers kept so existing scripts keep working.
for bin in rai rai-server rai-convert rai-generate rai-chat; do
for bin in rai rai-convert rai-generate rai-chat; do
src="target/release/${bin}${ext}"
if [ ! -f "$src" ]; then
echo "::error::expected release binary $src was not produced"
Expand All @@ -380,7 +388,7 @@ jobs:

# INSTALL.md is referenced from RUNNING.txt below, and the launchers
# are useless if they are not next to the binary they start.
cp LICENSE NOTICE README.md INSTALL.md "$stage/"
cp LICENSE NOTICE THIRD-PARTY-NOTICES.md README.md INSTALL.md "$stage/"
cp -r launchers "$stage/"

# `cp` preserves the checkout's mode, and tar preserves it again, so a
Expand All @@ -404,7 +412,6 @@ jobs:
rai run <model.raimodel> generate text
rai serve <model.raimodel> local chat UI (:8090)
rai models [dir] what is on this machine
rai-server local REST + MCP memory service
launchers/ double-click scripts that start the chat UI

rai-convert, rai-generate and rai-chat are deprecated wrappers over
Expand Down Expand Up @@ -440,9 +447,10 @@ jobs:
runs, on scalar code, slower.

NO GPU IS REQUIRED, AND NOTHING PHONES HOME
rai serve and rai-server listen on loopback only. rai-server sends
data off the machine only if you configure an external embedding
provider yourself.
Every binary here is the inference engine. rai serve binds
127.0.0.1 and refuses any request whose Host is not loopback.
Nothing in this archive contains an HTTP client, a TLS stack, or
the address of any server but your own.

MORE
INSTALL.md, README.md, and https://github.com/Classevelabs/rai
Expand All @@ -454,6 +462,63 @@ jobs:
ls -l "$stage"
cat "$stage/RUNNING.txt"

# The product boundary, enforced instead of documented.
#
# RAI is the inference engine. The memory/reasoning crates (rai-server,
# rai-core, rem-nra) share this workspace, are `publish = false`, and are
# not the product — yet the archive shipped `rai-server` for four
# releases because the boundary lived only in prose. Prose does not fail
# a build.
#
# This asserts the staged executable set is exactly the allowlist, and
# that nothing in it links a TLS stack or an HTTP client. The second half
# matters independently: it is what lets RUNNING.txt say "nothing phones
# home" with no conditional attached, and it would fail if a future
# dependency edit pulled reqwest back onto the inference path.
- name: Assert the archive carries only the product
shell: bash
env:
VERSION: ${{ needs.prepare.outputs.version }}
TARGET: ${{ matrix.target }}
run: |
set -euo pipefail
stage="dist/rai-${VERSION}-${TARGET}"
ext=""
if [ "${RUNNER_OS}" = "Windows" ]; then ext=".exe"; fi

# `sed` rather than `-exec basename {} "$ext"`: BSD basename on the
# macOS runners does not handle an empty suffix the way GNU does, and
# this gate has to behave identically on all four targets. LC_ALL=C
# so the comparison does not depend on the runner's collation either.
expected="rai rai-chat rai-convert rai-generate"
actual=$(find "$stage" -maxdepth 1 -type f \
! -name '*.md' ! -name '*.txt' ! -name 'LICENSE' ! -name 'NOTICE' \
| sed 's#.*/##; s#\.exe$##' \
| LC_ALL=C sort | tr '\n' ' ' | sed 's/ $//')

if [ "$actual" != "$expected" ]; then
echo "::error::archive binaries are '$actual', expected '$expected'"
echo "::error::a binary outside the product allowlist reached the archive"
exit 1
fi
echo "archive binaries: $actual"

# No shipped binary may carry a TLS implementation or an HTTP client.
# Both reach this workspace only through rai-core's OpenAI embedder,
# so a hit here means the inference engine grew a network dependency.
for bin in $expected; do
path="$stage/${bin}${ext}"
# `grep -a` rather than `strings`: grep is present on all three
# runner images, and binutils is not guaranteed on the Windows one.
for marker in rustls webpki BoringSSL reqwest hyper.rs; do
if grep -qaiF "$marker" "$path"; then
echo "::error::${bin} contains '$marker' — the engine must carry no TLS or HTTP stack"
exit 1
fi
done
done
echo "no TLS or HTTP stack in any shipped binary"

- name: Create tar.gz
if: runner.os != 'Windows'
shell: bash
Expand Down
Loading
Loading