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
99 changes: 98 additions & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,88 @@ on:
permissions:
contents: write

env:
CARGO_TERM_COLOR: always

jobs:
# ── Build binaries for each platform ──────────────────────────────
build:
name: Build (${{ matrix.target }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-apple-darwin
os: macos-latest
archive: tar.gz
- target: aarch64-apple-darwin
os: macos-latest
archive: tar.gz
- target: x86_64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
- target: aarch64-unknown-linux-gnu
os: ubuntu-latest
archive: tar.gz
steps:
- uses: actions/checkout@v4

- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@stable
with:
toolchain: "1.94"
targets: ${{ matrix.target }}

- name: Install cross-compilation tools (Linux ARM)
if: matrix.target == 'aarch64-unknown-linux-gnu'
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu

- uses: Swatinem/rust-cache@v2
with:
key: ${{ matrix.target }}

- name: Build release binaries
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
run: cargo build --release --target ${{ matrix.target }} -p astrid

- name: Package binaries
run: |
VERSION="${GITHUB_REF_NAME#v}"
DIR="astrid-${VERSION}-${{ matrix.target }}"
mkdir "$DIR"
cp "target/${{ matrix.target }}/release/astrid" "$DIR/"
cp "target/${{ matrix.target }}/release/astrid-daemon" "$DIR/"
cp LICENSE* README.md "$DIR/" 2>/dev/null || true
tar czf "${DIR}.tar.gz" "$DIR"
echo "ASSET=${DIR}.tar.gz" >> "$GITHUB_ENV"

- name: Upload artifact
uses: actions/upload-artifact@v4
with:
name: binary-${{ matrix.target }}
path: ${{ env.ASSET }}

# ── Create the GitHub release with all binaries ───────────────────
github-release:
name: GitHub Release
needs: build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Download all binary artifacts
uses: actions/download-artifact@v4
with:
path: artifacts
pattern: binary-*
merge-multiple: true

- name: Extract changelog for this version
id: changelog
run: |
Expand Down Expand Up @@ -45,7 +118,6 @@ jobs:
ASTRINAUTS=""
while IFS= read -r name; do
[ -z "$name" ] && continue
# Try to resolve GitHub username from commit email
EMAIL=$(git log --format='%aE' --author="$name" -1)
USERNAME=$(gh api "search/users?q=$EMAIL+in:email" --jq '.items[0].login // empty' 2>/dev/null || true)
if [ -n "$USERNAME" ]; then
Expand All @@ -60,12 +132,37 @@ jobs:
echo "EOF"
} >> "$GITHUB_OUTPUT"

- name: Generate checksums
run: |
cd artifacts
sha256sum *.tar.gz > SHA256SUMS.txt
cat SHA256SUMS.txt

- name: Create release
uses: softprops/action-gh-release@v2
with:
files: |
artifacts/*.tar.gz
artifacts/SHA256SUMS.txt
body: |
${{ steps.changelog.outputs.body }}

## Install

**From source (requires Rust 1.94+):**
```
cargo install astrid
```

**Pre-built binaries:**
Download the archive for your platform, extract, and add to PATH:
```
tar xzf astrid-*-$(uname -m)-*.tar.gz
sudo mv astrid-*/astrid astrid-*/astrid-daemon /usr/local/bin/
```

Then run `astrid init` to set up capsules.

---

**With many thanks from the following Astrinauts** 🚀
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked.

### Added

- `cargo install astrid` installs both `astrid` (CLI) and `astrid-daemon` binaries from a single crate. The CLI crate now includes the daemon as a second `[[bin]]` entry point.
- Standard WIT interface installation during `astrid init` — fetches 9 WIT files (llm, session, spark, context, prompt, tool, hook, registry, types) from the canonical WIT repo and installs to `~/.astrid/home/{principal}/wit/` for capsule and LLM access via `home://wit/`
- Short-circuit interceptor chain — interceptors return `Continue`, `Final`, or `Deny` to control the middleware chain. A guard at priority 10 can veto an event before the core handler at priority 100 ever sees it. Wire format: discriminant byte (0x00/0x01/0x02) + payload, backward compatible with existing capsules.
- Export conflict detection on `capsule install` — detects when a new capsule exports interfaces already provided by an installed capsule, prompts user to replace. Nix-aligned approach: conflicts derived from exports data, no name-based `supersedes` field needed.
Expand Down
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ tar = "0.4"
teloxide = { version = "0.13", default-features = false, features = ["macros", "rustls", "ctrlc_handler"] }
tempfile = "3"
thiserror = "2.0"
tokio = { version = "1.49", features = ["sync", "macros", "time", "rt", "rt-multi-thread", "net", "io-util"] }
tokio = { version = "1.49", features = ["sync", "macros", "time", "rt", "rt-multi-thread", "net", "io-util", "signal"] }
tokio-util = "0.7"
toml = "0.8"
tracing = "0.1"
Expand Down
5 changes: 5 additions & 0 deletions crates/astrid-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ description = "Command-line interface for Astrid secure agent runtime"
name = "astrid"
path = "src/main.rs"

[[bin]]
name = "astrid-daemon"
path = "src/daemon.rs"

[dependencies]
anyhow = { workspace = true }
arboard = { workspace = true }
Expand All @@ -21,6 +25,7 @@ astrid-core = { workspace = true }
blake3 = { workspace = true }
astrid-events = { workspace = true }
astrid-types = { workspace = true }
astrid-daemon = { path = "../astrid-daemon" }
astrid-storage = { workspace = true, features = ["keychain"] }
astrid-telemetry = { workspace = true, features = ["config"] }
chrono = { workspace = true }
Expand Down
10 changes: 10 additions & 0 deletions crates/astrid-cli/src/daemon.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
//! Bundled daemon binary — installed alongside `astrid` via `cargo install astrid`.
//!
//! Delegates to the shared `astrid_daemon::run()` library function. This is
//! identical to the standalone `astrid-daemon` binary but co-installed with
//! the CLI so `find_companion_binary("astrid-daemon")` always finds it.

#[tokio::main]
async fn main() -> anyhow::Result<()> {
astrid_daemon::run().await
}
5 changes: 4 additions & 1 deletion crates/astrid-daemon/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ repository.workspace = true
rust-version.workspace = true
description = "Astrid daemon - the background kernel process for the Astrid secure agent runtime"

[lib]
path = "src/lib.rs"

[[bin]]
name = "astrid-daemon"
path = "src/main.rs"
Expand All @@ -20,7 +23,7 @@ astrid-kernel = { workspace = true }
astrid-telemetry = { workspace = true, features = ["config"] }
clap = { workspace = true }
colored = { workspace = true }
tokio = { workspace = true, features = ["signal"] }
tokio = { workspace = true }
tracing = { workspace = true }
uuid = { workspace = true }

Expand Down
173 changes: 173 additions & 0 deletions crates/astrid-daemon/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
//! Astrid Daemon — shared library for the background kernel process.
//!
//! This crate provides the daemon entry point as a library function so it can
//! be reused by both the standalone `astrid-daemon` binary and the `astrid`
//! CLI binary (which ships both via `cargo install astrid`).

#![deny(unsafe_code)]
#![deny(missing_docs)]
#![deny(clippy::all)]
#![deny(unreachable_pub)]
#![deny(clippy::unwrap_used)]

use anyhow::{Context, Result};
use clap::Parser;

/// Astrid Daemon - Background kernel process
#[derive(Parser)]
#[command(name = "astrid-daemon")]
#[command(author, version, about)]
pub struct Args {
/// The session ID to bind the daemon to
#[arg(short, long, default_value = "00000000-0000-0000-0000-000000000000")]
pub session: String,

/// Workspace root directory
#[arg(short, long)]
pub workspace: Option<std::path::PathBuf>,

/// Enable ephemeral mode (auto-shutdown on idle timeout after last client disconnects)
#[arg(long)]
pub ephemeral: bool,

/// Enable verbose logging
#[arg(short, long)]
pub verbose: bool,
}

fn init_logging(verbose: bool) {
let workspace_root = std::env::current_dir().ok();
let unified_cfg = astrid_config::Config::load(workspace_root.as_deref())
.ok()
.map(|r| r.config);

let log_config = if let Some(cfg) = &unified_cfg {
let mut lc = astrid_telemetry::log_config_from(cfg);
if verbose {
"debug".clone_into(&mut lc.level);
}
if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
}
lc
} else {
let level = if verbose { "debug" } else { "info" };
let mut lc = astrid_telemetry::LogConfig::new(level)
.with_format(astrid_telemetry::LogFormat::Compact);
if let Ok(home) = astrid_core::dirs::AstridHome::resolve() {
lc.target = astrid_telemetry::LogTarget::File(home.log_dir());
}
lc
};

if let Err(e) = astrid_telemetry::setup_logging(&log_config) {
eprintln!("Failed to initialize logging: {e}");
}
}

/// Run the Astrid daemon with the given arguments.
///
/// This is the shared entry point used by both the standalone `astrid-daemon`
/// binary and the `astrid` CLI's bundled daemon binary.
///
/// # Errors
///
/// Returns an error if the kernel fails to boot, the CLI proxy capsule is
/// missing, or the readiness file cannot be written.
pub async fn run() -> Result<()> {
let args = Args::parse();

init_logging(args.verbose);

let session_id = astrid_core::SessionId::from_uuid(
uuid::Uuid::parse_str(&args.session)
.map_err(|e| anyhow::anyhow!("Invalid UUID format: {e}"))?,
);

let ws = args.workspace.unwrap_or_else(|| {
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from("."))
});

let kernel = astrid_kernel::Kernel::new(session_id.clone(), ws)
.await
.map_err(|e| anyhow::anyhow!("Failed to boot Kernel: {e}"))?;

// In ephemeral mode, shut down immediately when the last client disconnects.
if args.ephemeral {
kernel.set_ephemeral(true);
}

// Load all capsules (auto-discovery)
kernel.load_all_capsules().await;

// Verify the CLI proxy capsule loaded. Without it, the daemon
// has no accept loop and CLI connections will always time out.
{
let reg = kernel.capsules.read().await;
let has_cli_proxy = reg
.list()
.iter()
.any(|id| id.as_str() == "astrid-capsule-cli");
if !has_cli_proxy {
tracing::error!(
"CLI proxy capsule (astrid-capsule-cli) not found - \
daemon cannot accept CLI connections"
);
anyhow::bail!(
"CLI proxy capsule (astrid-capsule-cli) not found. \
Install it with: astrid capsule install @unicity-astrid/capsule-cli"
);
}
}

// Signal readiness AFTER all capsules are loaded and accepting
// connections. The CLI polls for this file to avoid connecting
// before the handshake accept loop is running.
astrid_kernel::socket::write_readiness_file().map_err(|e| {
anyhow::anyhow!(
"Failed to write readiness file \
(daemon is useless without it): {e}"
)
})?;

tracing::info!(
session = %session_id.0,
ephemeral = args.ephemeral,
"Kernel booted successfully"
);

// Wait for a termination signal or API shutdown request.
let mut shutdown_rx = kernel.shutdown_tx.subscribe();

#[cfg(unix)]
{
let mut sigterm = tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate())
.context("failed to register SIGTERM handler")?;
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("Received SIGINT, shutting down");
}
_ = sigterm.recv() => {
tracing::info!("Received SIGTERM, shutting down");
}
_ = shutdown_rx.wait_for(|v| *v) => {
tracing::info!("Received API shutdown request, shutting down");
}
}
}
#[cfg(not(unix))]
{
tokio::select! {
_ = tokio::signal::ctrl_c() => {
tracing::info!("Received SIGINT, shutting down");
}
_ = shutdown_rx.wait_for(|v| *v) => {
tracing::info!("Received API shutdown request, shutting down");
}
}
}

kernel.shutdown(Some("signal".to_string())).await;

Ok(())
}
Loading
Loading