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
26 changes: 15 additions & 11 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -459,7 +459,7 @@ jobs:

demos:
name: Native demos
needs: [changes, native]
needs: [changes, native-parity-build]
if: needs.changes.outputs.code == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 20
Expand All @@ -476,17 +476,21 @@ jobs:
- run: bun install --frozen-lockfile
- uses: actions/download-artifact@v8
with:
name: algal-debug-linux
name: algal-ci-parity-linux
path: ${{ runner.temp }}
- name: Restore the debug binary built by the native job
run: mkdir -p target/debug && tar -C target/debug -xf "$RUNNER_TEMP/algal-debug.tar" && target/debug/algal --version
- run: ALGAL_MEMORY_NATIVE="$PWD/target/debug/algal" bun test examples/coding-harness/memory-records.test.ts examples/coding-harness/memory.test.ts
- run: ALGAL_MEMORY_NATIVE="$PWD/target/debug/algal" bun test examples/adaptive-inventory/run.test.ts
- run: bun scripts/vm-demo.ts --native ./target/debug/algal
- run: bun scripts/recovery-demo.ts --native ./target/debug/algal
- run: bun scripts/repair-demo.ts --native ./target/debug/algal
- run: bun scripts/coding-recovery-demo.ts --native ./target/debug/algal
- run: bun scripts/process-evidence-demo.ts --native ./target/debug/algal
# SDK children verify their executable hash on every invocation. Reuse
# the existing optimized binary while retaining debug assertions and
# overflow checks, so hashing full debug symbols cannot consume a demo's
# unchanged process deadline.
- name: Restore the ci-parity binary
run: mkdir -p target/ci-parity && tar -C target/ci-parity -xf "$RUNNER_TEMP/algal-ci-parity.tar" && target/ci-parity/algal --version
- run: ALGAL_MEMORY_NATIVE="$PWD/target/ci-parity/algal" bun test examples/coding-harness/memory-records.test.ts examples/coding-harness/memory.test.ts
- run: ALGAL_MEMORY_NATIVE="$PWD/target/ci-parity/algal" bun test examples/adaptive-inventory/run.test.ts
- run: bun scripts/vm-demo.ts --native ./target/ci-parity/algal
- run: bun scripts/recovery-demo.ts --native ./target/ci-parity/algal
- run: bun scripts/repair-demo.ts --native ./target/ci-parity/algal
- run: bun scripts/coding-recovery-demo.ts --native ./target/ci-parity/algal
- run: bun scripts/process-evidence-demo.ts --native ./target/ci-parity/algal

required:
name: Required
Expand Down
39 changes: 4 additions & 35 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ on:
required: true
type: string
publish:
description: Attach verified assets to a draft or prerelease (create a prerelease if absent)
description: Stage and verify complete assets in a draft, then publish an immutable prerelease
required: true
default: false
type: boolean
Expand Down Expand Up @@ -104,6 +104,7 @@ jobs:
- name: Build locked release executable
env:
CARGO_BUILD_TARGET: ${{ matrix.target }}
ALGAL_RELEASE_BUILD_TAG: ${{ inputs.tag }}
run: cargo +1.97.1 build --release --locked -p algal --bin algal
- name: Qualify archive and installer
env:
Expand Down Expand Up @@ -165,6 +166,7 @@ jobs:
- name: Build and qualify the unsigned executable
env:
CARGO_BUILD_TARGET: aarch64-apple-darwin
ALGAL_RELEASE_BUILD_TAG: ${{ inputs.tag }}
run: |
cargo +1.97.1 build --release --locked -p algal --bin algal
python3 scripts/test-native-release.py --binary target/aarch64-apple-darwin/release/algal --commit "$GITHUB_SHA" --rustc-version "$(rustc +1.97.1 --version)"
Expand Down Expand Up @@ -398,37 +400,4 @@ jobs:
notes="$RUNNER_TEMP/release-body.md"
title=$(python3 scripts/release-notes.py title --tag "$RELEASE_TAG")
python3 scripts/release-notes.py render --tag "$RELEASE_TAG" --commit "$RELEASE_SHA" --manifests artifacts --out "$notes"
check_page() {
gh release view "$RELEASE_TAG" --json name,body > "$RUNNER_TEMP/release-page.json"
python3 - "$RUNNER_TEMP/release-page.json" "$RUNNER_TEMP/release-page.md" "$title" <<'PYPAGE'
import json, pathlib, sys
page = json.loads(pathlib.Path(sys.argv[1]).read_text())
if page["name"] != sys.argv[3]:
raise SystemExit("Release title differs from the product name and tag")
pathlib.Path(sys.argv[2]).write_bytes(page["body"].encode())
PYPAGE
python3 scripts/release-notes.py verify --tag "$RELEASE_TAG" --commit "$RELEASE_SHA" --manifests artifacts --body "$RUNNER_TEMP/release-page.md"
}
if gh release view "$RELEASE_TAG" --json isDraft,isPrerelease,body > "$RUNNER_TEMP/release.json"; then
existing=$(python3 - "$RUNNER_TEMP/release.json" <<'PYEXISTING'
import json, sys
r = json.load(open(sys.argv[1]))
assert r["isDraft"] or r["isPrerelease"], "Refusing to mutate a stable release"
print("page" if "<!-- algal.release-page.v1 " in r["body"] else "draft")
PYEXISTING
)
# A page this workflow wrote must still match exactly (a retry);
# a draft without an identity record gets the rendered page.
if [ "$existing" = page ]; then
check_page
else
verify_release_tag
gh release edit "$RELEASE_TAG" --title "$title" --notes-file "$notes"
fi
else
verify_release_tag
gh release create "$RELEASE_TAG" --verify-tag --prerelease --title "$title" --notes-file "$notes"
fi
verify_release_tag
gh release upload "$RELEASE_TAG" artifacts/*
check_page
python3 scripts/publish-native-release.py --artifacts artifacts --notes "$notes" --title "$title"
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,17 @@ independent of these package versions.

## Unreleased

## v0.2.0-vm.13 - 2026-09-30

Verified native installations on macOS and Linux update automatically within
the `vm` preview channel. The Bun runtime keeps its existing update workflow.

- Add `algal update`, with `check`, `status`, `enable`, and `disable` actions and JSON output. Automatic checks run before product work at most once a day; every running command protects its executable until it finishes.
- Compare the full preview tag and source SHA embedded by the official release build. Archive, checksum and release-record downloads must match the immutable GitHub release. macOS retains Developer ID, notarization, hardened-runtime and timestamp checks.
- Preserve hash-keyed `.algal-releases` records through installation and rollback. Re-running the public installer can enroll a verified `vm.11` copy; explicitly selected versions stay pinned.
- Skip automatic updates in CI, offline verification, memory queries and hash-pinned SDK calls. `--no-update` or `HRANESS_NO_UPDATE=1` skips one invocation. Saved opt-outs survive reinstallation.
- Keep the SDK's existing executable hash pin across process startup and check it while the native command holds its update lock. Source and package Bun installs print manual guidance without replacing their runtime.

## v0.2.0-vm.12 - 2026-09-30

macOS releases use one Developer ID identity across upgrades, so system
Expand Down
64 changes: 64 additions & 0 deletions Cargo.lock

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

7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ arm64 are on the [releases page](https://github.com/hraness/algal/releases)
(`curl -fsSL https://algal.computer/install.sh | sh`), and the Bun runtime runs
from this checkout.

From `v0.2.0-vm.13`, verified macOS and Linux installs update automatically
within the `vm` preview channel. `algal update disable` turns automatic updates
off; `algal update` installs a newer verified preview now. Explicit versions,
CI, offline verification and hash-pinned SDK calls stay fixed. The Bun runtime
keeps its existing source or package-manager update workflow. See
[native CLI updates](docs/native-release.md#updates).

## The idea

The bet behind ALGAL is that a computer can accumulate tested ways of acting,
Expand Down
15 changes: 14 additions & 1 deletion cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,8 @@ async function recallSpecExecutor(spec: string, dir: string): Promise<Executor |
const USAGE = `algal: Language and VM for agent programs that wait for approval and resume

usage:
algal update [check|status|enable|disable] [--json]
show this Bun runtime's manual update workflow
algal compile <program.algal> [--out <manifest.json>] [--source-map <map.json>]
[--bundle-out <bundle.json>] [--source-root <dir>]
compile source; bundle its complete local import closure
Expand Down Expand Up @@ -1062,6 +1064,17 @@ async function main(): Promise<number> {
process.stdout.write(help.text);
return help.code;
}
if (argv[0] === "update") {
const tokens = argv.slice(1), actions = tokens.filter(token => token !== "--json");
if (actions.length > 1 || tokens.filter(token => token === "--json").length > 1
|| actions.some(action => !["install", "check", "status", "enable", "disable"].includes(action))) {
usageError("algal update [check|status|enable|disable] [--json]");
}
const instructions = "This Bun runtime uses its original source or package-manager update workflow. Update the checkout or package through that workflow; it stays on Bun.";
if (tokens.includes("--json")) out({schema: "hraness.cli-update.result.v1", product: "algal", status: "unsupported", policy: "disabled", supported: false, automatic: false, current: PACKAGE_VERSION, latest: null, reason: "Source and package Bun installs update through their original workflow.", instructions});
else process.stdout.write(`${instructions}\n`);
return 0;
}
const { cmd, positional, flags } = parseArgs(argv);
const requestedDiagnosticFormat = artifactFlag(flags, "diagnostic-format") ?? (humanErrors(argv) ? "text" : "json");
if (requestedDiagnosticFormat !== "json" && requestedDiagnosticFormat !== "text") {
Expand Down Expand Up @@ -3948,7 +3961,7 @@ function usageError(msg: string): never {
throw new AlgalError("PARSE_FAILED", `usage: ${msg}`);
}

void reportAlgalCliRun(PACKAGE_VERSION);
if (process.argv[2] !== "update") void reportAlgalCliRun(PACKAGE_VERSION);
main()
.then((code) => process.exit(code))
.catch(async (e) => {
Expand Down
5 changes: 5 additions & 0 deletions crates/algal/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@ description = "ALGAL is a programming language and VM for AI agent programs that
workspace = true

[dependencies]
anyhow = "1"
hraness-cli-update = { git = "https://github.com/hraness/cli-update", tag = "v0.1.0", version = "=0.1.0" }
algal-expr = { path = "../algal-expr" }
apple-foundation = { git = "https://github.com/hraness/apple-foundation", tag = "v0.2.0" }
clap = { version = "=4.5.48", features = ["derive"] }
Expand All @@ -24,6 +26,9 @@ serde_json = "=1.0.151"
sha2 = "=0.10.9"
tokio = { version = "=1.47.1", features = ["rt-multi-thread", "macros", "process", "io-util", "io-std", "sync", "time", "signal"] }

[target.'cfg(unix)'.dependencies]
rustix = { version = "1", features = ["fs", "process"] }

[dev-dependencies]
tempfile = "=3.23.0"

Expand Down
25 changes: 25 additions & 0 deletions crates/algal/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ fn set(name: &str, value: &str) {
}

fn main() {
println!("cargo:rerun-if-env-changed=ALGAL_RELEASE_BUILD_TAG");
let manifest = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let root = manifest.parent().unwrap().parent().unwrap();
watch(&root.join(".git"));
Expand Down Expand Up @@ -168,4 +169,28 @@ fn main() {
set("TARGET", &env::var("TARGET").unwrap_or_default());
set("RUSTC", rustc.trim());
set("TAGS", &tags);
if let Ok(tag) = env::var("ALGAL_RELEASE_BUILD_TAG") {
let prefix = format!("v{}-vm.", env::var("CARGO_PKG_VERSION").unwrap());
let suffix = tag
.strip_prefix(&prefix)
.expect("official builds need the full vm release tag");
assert!(
!suffix.is_empty()
&& !suffix.starts_with('0')
&& suffix.bytes().all(|byte| byte.is_ascii_digit())
&& suffix.parse::<u64>().is_ok(),
"release number must be canonical"
);
assert_eq!(
state, "clean",
"official release builds require clean source"
);
assert!(
commit.is_some() && tags.split(',').any(|exact| exact == tag),
"official build tag must name the checked-out commit"
);
set("RELEASE_TAG", &tag);
} else {
set("RELEASE_TAG", "");
}
}
Loading
Loading