diff --git a/.cargo/config.toml b/.cargo/config.toml deleted file mode 100644 index d1da78d..0000000 --- a/.cargo/config.toml +++ /dev/null @@ -1,9 +0,0 @@ -# macOS: acyclic-fs-mount's build script probes pkg-config for `fuse3` -# (FUSE-T's libfuse3 compat surface). Some FUSE-T installs omit fuse3.pc, so -# we carry a shim pointing at the standard /usr/local FUSE-T layout. -[env] -PKG_CONFIG_PATH = { value = "pkgconfig", relative = true } - -# FUSE-T installs libfuse-t.dylib under /usr/local/lib and links via @rpath. -[target.'cfg(target_os = "macos")'] -rustflags = ["-C", "link-arg=-Wl,-rpath,/usr/local/lib"] diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2a014c2..eae5d86 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,7 +12,6 @@ concurrency: cancel-in-progress: true env: - FUSE_T_VERSION: 1.2.7 CARGO_TERM_COLOR: always # acyclic-fs is fetched from its own git repo (see Cargo.toml); cargo's # built-in libgit2 fetcher can't resolve a pinned commit SHA that isn't a @@ -65,7 +64,7 @@ jobs: # Licenses, advisories, and sources per deny.toml. Keeps the published # SBOM inside the permissive allowlist and fails on known-vulnerable or - # yanked crates. Always runs: the secrets scan applies to docs too. + # yanked crates. Always runs: the product-name guard applies to docs too. deny: runs-on: ubuntu-24.04 steps: @@ -74,8 +73,6 @@ jobs: persist-credentials: false - name: Product name is single-sourced (product.toml) run: bash scripts/check-product-name.sh - - name: No forbidden files or credential patterns - run: bash scripts/check-no-secrets.sh - name: Code quality (line width, TODO format, comment blocks, duplication) run: bash scripts/check-code-quality.sh - uses: EmbarkStudios/cargo-deny-action@3c6349835b2b7b196a839186cb8b78e02f7b5f25 # v2.1.1 @@ -228,16 +225,6 @@ jobs: with: path: graphcoder-plugin - - name: Install FUSE-T (macOS) - if: runner.os == 'macOS' - run: | - set -euo pipefail - curl --fail --location --retry 5 \ - "https://github.com/macos-fuse-t/fuse-t/releases/download/${FUSE_T_VERSION}/fuse-t-macos-installer-${FUSE_T_VERSION}.pkg" \ - --output /tmp/fuse-t.pkg - sudo installer -pkg /tmp/fuse-t.pkg -target / - test -d /usr/local/include/fuse3 - - name: Install toolchain run: rustup toolchain install stable --profile minimal && rustup default stable diff --git a/CHANGELOG.md b/CHANGELOG.md index d6bdff2..0278fe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,24 @@ Pre-1.0; `main` is the only supported line (see `SECURITY.md`). ### Changed +- **Prepared for the move into `acyclic-labs/sdk` as `plugin/`.** Releases + will be cut from that repository as `plugin-v` tags, so + `scripts/install.sh` now targets them and reads the current version from + `plugin/LATEST` on the sdk's `main` branch (`product.toml` gained + `release_tag_prefix`, guarded by `check-product-name.sh`). The acceptance + harness, `ci-local.sh`, `docker-linux.sh` and `release-local.sh` locate the + cargo target directory and the plugin's own crates in either layout. The + crates build with edition 2024 and the sdk's stricter lint set. +- **FUSE-T is no longer required on macOS.** `acyclic-fs` mounts through its + vendored `darwinfuse` NFSv4 server, so the `fuse3` pkg-config shim, the + rpath link flag and the CI installer step are gone. +- **`acyclic-fs` is pinned to the sdk's `main` line** (the Darwin + unpaired-rename, subtree-removal and `O_EXCL` fixes landed there as sdk + PR #99); the guard forwards the new `capture_host_subtree` mount hook, and + the store asks for barrier durability only on Apple targets, since the sdk + now fails closed where `F_BARRIERFSYNC` does not exist instead of falling + back to a full flush. + - **A cold daemon no longer delays the agent's first turn.** The session-start hook waits at most 300ms for the daemon; past that it prints a one-line notice and returns while the first snapshot builds in the background (251s diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index c9fbc54..ceb5e16 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -44,7 +44,6 @@ Run these locally — CI enforces all of them: ```sh scripts/check-product-name.sh # the public name only comes from product.toml -scripts/check-no-secrets.sh # no forbidden files or credential patterns scripts/check-code-quality.sh # line width, TODO(topic) format, comment-block length, duplication cargo deny check # dependency licenses, advisories, bans cargo fmt --all --check diff --git a/Cargo.lock b/Cargo.lock index 426e5bc..1424425 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 4 [[package]] name = "acyclic" -version = "0.0.2" +version = "0.0.3" dependencies = [ "acyclic-engine", "acyclic-fs", @@ -22,7 +22,7 @@ dependencies = [ [[package]] name = "acyclic-engine" -version = "0.0.2" +version = "0.0.3" dependencies = [ "acyclic-fs", "blake3", @@ -41,9 +41,10 @@ dependencies = [ [[package]] name = "acyclic-fs" -version = "0.2.0-rc.1" -source = "git+https://github.com/acyclic-labs/sdk.git?rev=22b4e752f46d8f6db6e024b3137fe520ef64bcc1#22b4e752f46d8f6db6e024b3137fe520ef64bcc1" +version = "0.2.0-rc.5" +source = "git+https://github.com/acyclic-labs/sdk.git?rev=0de2d5c8eff4d42a8011458a2291cd09ea9b6e75#0de2d5c8eff4d42a8011458a2291cd09ea9b6e75" dependencies = [ + "acyclic-native-runtime", "acyclic-objects", "acyclic-stream", "async-trait", @@ -52,15 +53,18 @@ dependencies = [ "cap-primitives", "cap-std", "cc", + "diffy", "fs2", "fuser", "futures", "hex", + "io-uring", "libc", "notify", "prost", "prost-types", "serde", + "serde_json", "thiserror", "tokio", "tonic", @@ -71,16 +75,33 @@ dependencies = [ "windows", ] +[[package]] +name = "acyclic-native-runtime" +version = "0.1.0" +source = "git+https://github.com/acyclic-labs/sdk.git?rev=0de2d5c8eff4d42a8011458a2291cd09ea9b6e75#0de2d5c8eff4d42a8011458a2291cd09ea9b6e75" +dependencies = [ + "block2", + "bytes", + "dispatch2", + "io-uring", + "libc", + "windows-sys 0.61.2", +] + [[package]] name = "acyclic-objects" -version = "1.0.0-rc.1" -source = "git+https://github.com/acyclic-labs/sdk.git?rev=22b4e752f46d8f6db6e024b3137fe520ef64bcc1#22b4e752f46d8f6db6e024b3137fe520ef64bcc1" +version = "1.0.0-rc.4" +source = "git+https://github.com/acyclic-labs/sdk.git?rev=0de2d5c8eff4d42a8011458a2291cd09ea9b6e75#0de2d5c8eff4d42a8011458a2291cd09ea9b6e75" dependencies = [ + "acyclic-native-runtime", "async-trait", "blake3", "bytes", "fs2", "futures", + "getrandom 0.3.4", + "hex", + "imbl", "libc", "prost", "prost-types", @@ -90,7 +111,7 @@ dependencies = [ [[package]] name = "acyclic-proto" -version = "0.0.2" +version = "0.0.3" dependencies = [ "serde", "serde_json", @@ -98,7 +119,7 @@ dependencies = [ [[package]] name = "acyclic-qual" -version = "0.0.2" +version = "0.0.3" dependencies = [ "acyclic-engine", "acyclic-fs", @@ -107,9 +128,10 @@ dependencies = [ [[package]] name = "acyclic-stream" -version = "1.0.0-rc.3" -source = "git+https://github.com/acyclic-labs/sdk.git?rev=22b4e752f46d8f6db6e024b3137fe520ef64bcc1#22b4e752f46d8f6db6e024b3137fe520ef64bcc1" +version = "1.0.0-rc.7" +source = "git+https://github.com/acyclic-labs/sdk.git?rev=0de2d5c8eff4d42a8011458a2291cd09ea9b6e75#0de2d5c8eff4d42a8011458a2291cd09ea9b6e75" dependencies = [ + "acyclic-native-runtime", "async-trait", "bytes", "fs2", @@ -204,6 +226,12 @@ version = "1.0.104" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470" +[[package]] +name = "archery" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ca55ee147b1926dbea904f50fe4902494e97bc742205abbbf10c709e43815f" + [[package]] name = "arrayvec" version = "0.7.8" @@ -316,12 +344,27 @@ dependencies = [ "generic-array", ] +[[package]] +name = "block2" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5" +dependencies = [ + "objc2", +] + [[package]] name = "bumpalo" version = "3.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" +[[package]] +name = "bytemuck" +version = "1.25.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95832e849adfb21180ccb6826a99da14e5d266ae5c2e668e1602cf234f153797" + [[package]] name = "bytes" version = "1.12.1" @@ -531,6 +574,18 @@ dependencies = [ "crypto-common", ] +[[package]] +name = "dispatch2" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" +dependencies = [ + "bitflags", + "block2", + "libc", + "objc2", +] + [[package]] name = "dyn-clone" version = "1.0.20" @@ -761,6 +816,20 @@ dependencies = [ "wasi", ] +[[package]] +name = "getrandom" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" +dependencies = [ + "cfg-if", + "js-sys", + "libc", + "r-efi 5.3.0", + "wasip2", + "wasm-bindgen", +] + [[package]] name = "getrandom" version = "0.4.3" @@ -769,7 +838,7 @@ checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099" dependencies = [ "cfg-if", "libc", - "r-efi", + "r-efi 6.0.0", ] [[package]] @@ -969,6 +1038,27 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" +[[package]] +name = "imbl" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46bad832b9b463ed9398b8506488cc2e3b897a9d44f13af115f382aac71f4fec" +dependencies = [ + "archery", + "equivalent", + "imbl-sized-chunks", + "rand_core", + "rand_xoshiro", + "version_check", + "wide", +] + +[[package]] +name = "imbl-sized-chunks" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0813be332553f857953298749fa19549e8b61b80589757c29b4e2a804fa9c6" + [[package]] name = "indexmap" version = "2.14.1" @@ -1023,6 +1113,17 @@ version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2f0fb0570afe1fed943c5c3d4102d5358592d8625fda6a0007fdbe65a92fba96" +[[package]] +name = "io-uring" +version = "0.7.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed3bd0ecfbb87805f538bb7b32e5239ca0763890c623e349860ecba69469f2bb" +dependencies = [ + "bitflags", + "cfg-if", + "libc", +] + [[package]] name = "ipnet" version = "2.12.1" @@ -1209,6 +1310,21 @@ dependencies = [ "autocfg", ] +[[package]] +name = "objc2" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a12a8ed07aefc768292f076dc3ac8c48f3781c8f2d5851dd3d98950e8c5a89f" +dependencies = [ + "objc2-encode", +] + +[[package]] +name = "objc2-encode" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef25abbcd74fb2609453eb695bd2f860d389e457f67dc17cafc8b8cbc89d0c33" + [[package]] name = "once_cell" version = "1.21.4" @@ -1451,12 +1567,33 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "r-efi" +version = "5.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f" + [[package]] name = "r-efi" version = "6.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf" +[[package]] +name = "rand_core" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" + +[[package]] +name = "rand_xoshiro" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f703f4665700daf5512dcca5f43afa6af89f09db47fb56be587f80636bda2d41" +dependencies = [ + "rand_core", +] + [[package]] name = "ref-cast" version = "1.0.27" @@ -1645,6 +1782,15 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" +[[package]] +name = "safe_arch" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96b02de82ddbe1b636e6170c21be622223aea188ef2e139be0a5b219ec215323" +dependencies = [ + "bytemuck", +] + [[package]] name = "same-file" version = "1.0.6" @@ -2221,6 +2367,15 @@ version = "0.11.1+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" +[[package]] +name = "wasip2" +version = "1.0.4+wasi-0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b67efb37e106e55ce722a510d6b5f9c17f083e5fc79afc2badeb12cc313d9487" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.127" @@ -2275,6 +2430,16 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "wide" +version = "0.7.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ce5da8ecb62bcd8ec8b7ea19f69a51275e91299be594ea5cc6ef7819e16cd03" +dependencies = [ + "bytemuck", + "safe_arch", +] + [[package]] name = "winapi" version = "0.3.9" @@ -2600,6 +2765,12 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "zerocopy" version = "0.8.56" diff --git a/Cargo.toml b/Cargo.toml index 80afe71..45c6cd4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,14 +8,14 @@ members = [ ] [workspace.package] -version = "0.0.2" -edition = "2021" +version = "0.0.3" +edition = "2024" license = "Apache-2.0" repository = "https://github.com/acyclic-labs/graphcoder-plugin" homepage = "https://acyclic.dev" [workspace.dependencies] -acyclic-fs = { git = "https://github.com/acyclic-labs/sdk.git", rev = "22b4e752f46d8f6db6e024b3137fe520ef64bcc1", features = ["local", "native-watch", "native-mount"] } +acyclic-fs = { git = "https://github.com/acyclic-labs/sdk.git", rev = "0de2d5c8eff4d42a8011458a2291cd09ea9b6e75", features = ["local", "native-watch", "native-mount"] } tokio = { version = "1.48", features = ["rt-multi-thread", "macros"] } # Code-quality guards beyond clippy's defaults. CI runs clippy with diff --git a/LATEST b/LATEST new file mode 100644 index 0000000..bcab45a --- /dev/null +++ b/LATEST @@ -0,0 +1 @@ +0.0.3 diff --git a/crates/acyclic-engine/src/exclude.rs b/crates/acyclic-engine/src/exclude.rs index bd22a7f..1937547 100644 --- a/crates/acyclic-engine/src/exclude.rs +++ b/crates/acyclic-engine/src/exclude.rs @@ -54,7 +54,7 @@ impl Exclusions { _ => { return Err(EngineError::Config(format!( "exclude rule {pattern:?} must be a relative path inside the repo" - ))) + ))); } } } diff --git a/crates/acyclic-engine/src/fork.rs b/crates/acyclic-engine/src/fork.rs index a3df150..6cd8c1b 100644 --- a/crates/acyclic-engine/src/fork.rs +++ b/crates/acyclic-engine/src/fork.rs @@ -18,12 +18,12 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; -use acyclic_fs::model::VolumeConfig; use acyclic_fs::SharedCheckout; +use acyclic_fs::model::VolumeConfig; use acyclic_fs::{ - capture_baseline, capture_root_identity, probe_native_mount, CancellationToken, CaptureOptions, - GenerationId, LocalAuthorityBackend, LocalObjectBackend, NativeMountKind, VolumeId, - WorkCounters, + CancellationToken, CaptureOptions, GenerationId, LocalAuthorityBackend, LocalObjectBackend, + NativeMountKind, VolumeId, WorkCounters, capture_baseline, capture_root_identity, + probe_native_mount, }; use crate::{EngineError, Result}; diff --git a/crates/acyclic-engine/src/guard.rs b/crates/acyclic-engine/src/guard.rs index be4919c..e083b88 100644 --- a/crates/acyclic-engine/src/guard.rs +++ b/crates/acyclic-engine/src/guard.rs @@ -466,6 +466,15 @@ impl MountFilesystem for GuardedMountFilesystem { self.guard(path)?; self.inner.capture_host_path(source_root, path) } + + fn capture_host_subtree( + &self, + source_root: &Path, + path: &MountPath, + ) -> Result<(), MountSourceError> { + self.guard(path)?; + self.inner.capture_host_subtree(source_root, path) + } } #[cfg(test)] @@ -532,8 +541,8 @@ mod tests { } #[test] - fn write_to_guarded_directory_is_rejected_at_any_depth( - ) -> Result<(), Box> { + fn write_to_guarded_directory_is_rejected_at_any_depth() + -> Result<(), Box> { let guard = guarded_source(&["migrations".to_owned()])?; let deep = test_path(&["migrations", "2024", "001_init.sql"]); assert!(matches!( @@ -588,9 +597,11 @@ mod tests { "sidecar {sidecar:?} must be refused" ); } - assert!(guard - .create_file(&test_path(&["notes.txt"]), metadata()) - .is_ok()); + assert!( + guard + .create_file(&test_path(&["notes.txt"]), metadata()) + .is_ok() + ); Ok(()) } diff --git a/crates/acyclic-engine/src/index.rs b/crates/acyclic-engine/src/index.rs index 36589b9..bfb52cd 100644 --- a/crates/acyclic-engine/src/index.rs +++ b/crates/acyclic-engine/src/index.rs @@ -8,7 +8,7 @@ use std::path::Path; use acyclic_fs::{Digest, GenerationId}; -use rusqlite::{params, Connection, OptionalExtension}; +use rusqlite::{Connection, OptionalExtension, params}; use crate::{EngineError, Result}; @@ -118,7 +118,7 @@ impl CheckpointKind { other => { return Err(EngineError::Store(format!( "unknown checkpoint kind {other}" - ))) + ))); } }) } @@ -1002,10 +1002,12 @@ mod tests { .expect("q") .expect("some"); assert_eq!(last.session_id, "old"); - assert!(index - .last_session_with_checkpoints(Some("old")) - .expect("q") - .is_none()); + assert!( + index + .last_session_with_checkpoints(Some("old")) + .expect("q") + .is_none() + ); } #[test] diff --git a/crates/acyclic-engine/src/lib.rs b/crates/acyclic-engine/src/lib.rs index 6c03408..305470c 100644 --- a/crates/acyclic-engine/src/lib.rs +++ b/crates/acyclic-engine/src/lib.rs @@ -9,7 +9,12 @@ //! boundaries (it proves closure over the whole tree). //! 2. All engine state (store, socket, index) lives outside the working tree. //! 3. Volume limits are raised at creation and the `VolumeId` is persisted. - +// The sdk workspace warns on missing docs and lints with -D warnings. +#![allow( + missing_docs, + reason = "engine internals consumed only by the acyclic binary; \ + per-item docs are tracked as a follow-up" +)] #![cfg_attr( test, allow( diff --git a/crates/acyclic-engine/src/merge.rs b/crates/acyclic-engine/src/merge.rs index 029b51b..30c5de2 100644 --- a/crates/acyclic-engine/src/merge.rs +++ b/crates/acyclic-engine/src/merge.rs @@ -693,7 +693,7 @@ pub(crate) async fn read_regular(checkout: &mut LocalCheckout, path: &Path) -> R return Err(EngineError::Fs(format!( "{}: not a regular file", path.display() - ))) + ))); } }; let limits = checkout.volume_config().limits; @@ -1011,7 +1011,7 @@ async fn copy_node( other => { return Err(EngineError::Fs(format!( "cannot copy a {other:?} node (only files, symlinks, and directories)" - ))) + ))); } } if let MetadataField::Value(mode) = metadata.posix_mode { diff --git a/crates/acyclic-engine/src/pipeline.rs b/crates/acyclic-engine/src/pipeline.rs index 8a24942..de1ef12 100644 --- a/crates/acyclic-engine/src/pipeline.rs +++ b/crates/acyclic-engine/src/pipeline.rs @@ -9,12 +9,12 @@ use std::path::PathBuf; use std::time::{Duration, Instant}; use acyclic_fs::model::VolumeLimits; -use acyclic_fs::{capture_baseline, capture_root_identity, capture_watch_batch, CaptureOptions}; use acyclic_fs::{ CancellationToken, CheckoutCommitOutcome, GenerationId, MountPublication, NativeWatch, NativeWatchOptions, OperationId, WatchBatch, WatchChange, WatchEpoch, WatchSequence, WorkCounters, }; +use acyclic_fs::{CaptureOptions, capture_baseline, capture_root_identity, capture_watch_batch}; use tokio::sync::{mpsc, oneshot}; use std::sync::Arc; @@ -250,7 +250,7 @@ pub struct PipelineHandle { } macro_rules! request { - ($self:ident, $variant:ident { $($field:ident : $value:expr),* $(,)? }) => {{ + ($self:ident, $variant:ident { $($field:ident : $value:expr_2021),* $(,)? }) => {{ let (reply, receiver) = oneshot::channel(); $self .sender @@ -395,7 +395,7 @@ impl PipelineHandle { Ok(()) => {} Err(mpsc::error::TrySendError::Full(_)) => return Ok(None), Err(mpsc::error::TrySendError::Closed(_)) => { - return Err(EngineError::Store("pipeline is gone".into())) + return Err(EngineError::Store("pipeline is gone".into())); } } let changes = receiver @@ -976,19 +976,19 @@ impl Pipeline { "rescan tail: root hint ({root:?}) dropped, covered by the rescan" ); } - if let WatchBatch::Changes { ref changes, .. } = batch { - if !changes.is_empty() { - capture_watch_batch( - &mut self.store.checkout, - batch, - &self.options, - WorkCounters::UNBOUNDED, - &self.cancel, - ) - .await - .map_err(EngineError::fs("capture rescan tail"))?; - self.scrub_exclusions().await?; - } + if let WatchBatch::Changes { ref changes, .. } = batch + && !changes.is_empty() + { + capture_watch_batch( + &mut self.store.checkout, + batch, + &self.options, + WorkCounters::UNBOUNDED, + &self.cancel, + ) + .await + .map_err(EngineError::fs("capture rescan tail"))?; + self.scrub_exclusions().await?; } let phase = Instant::now(); self.commit_engine().await?; @@ -2053,7 +2053,7 @@ impl Pipeline { other => { return Err(EngineError::Fs(format!( "unexpected fork commit outcome: {other:?}" - ))) + ))); } }; diff --git a/crates/acyclic-engine/src/rewind.rs b/crates/acyclic-engine/src/rewind.rs index c00550b..65d7ff9 100644 --- a/crates/acyclic-engine/src/rewind.rs +++ b/crates/acyclic-engine/src/rewind.rs @@ -8,7 +8,7 @@ use std::path::{Component, Path, PathBuf}; #[cfg(unix)] use acyclic_fs::kernel::MetadataField; use acyclic_fs::kernel::{FileKind, FilePayload, LogicalName, NamespacePath}; -use acyclic_fs::{materialize_checkout, ByteRange, MaterializeOptions}; +use acyclic_fs::{ByteRange, MaterializeOptions, materialize_checkout}; use acyclic_fs::{CancellationToken, GenerationId, WorkCounters}; use serde::{Deserialize, Serialize}; @@ -186,7 +186,7 @@ pub(crate) fn write_node<'a>( _ => { return Err(EngineError::Restore( "regular file with foreign payload".into(), - )) + )); } }; let mut file = std::fs::File::create(host)?; @@ -304,7 +304,7 @@ pub(crate) fn validate_relative(relative: &Path) -> Result>> { return Err(EngineError::Restore(format!( "{}: path must be relative to the repo root and stay inside it", relative.display() - ))) + ))); } } } @@ -864,9 +864,11 @@ mod tests { #[test] fn recover_with_no_journal_is_a_noop() { let work = tempfile::tempdir().expect("tempdir"); - assert!(recover(&work.path().join("missing.json")) - .expect("recover") - .is_none()); + assert!( + recover(&work.path().join("missing.json")) + .expect("recover") + .is_none() + ); } #[test] diff --git a/crates/acyclic-engine/src/spec.rs b/crates/acyclic-engine/src/spec.rs index 1ff3f27..909d133 100644 --- a/crates/acyclic-engine/src/spec.rs +++ b/crates/acyclic-engine/src/spec.rs @@ -655,13 +655,13 @@ impl SpeculateConfig { let text = match std::fs::read_to_string(&path) { Ok(text) => text, Err(error) if error.kind() == std::io::ErrorKind::NotFound => { - return (Self::default(), None) + return (Self::default(), None); } Err(error) => { return ( Self::default(), Some(format!("{}: {error}; speculation off", path.display())), - ) + ); } }; match toml::from_str(&text) { @@ -989,13 +989,17 @@ mod tests { } #[test] + #[allow( + unsafe_code, + reason = "the only test that touches this variable, and nothing else in the process reads it" + )] fn a_malformed_config_disables_rather_than_failing() { let dir = tempfile::tempdir().expect("tempdir"); let path = dir.path().join("speculate.toml"); std::fs::write(&path, "enabled = true\nnot_a_key = 1\n").expect("write"); - std::env::set_var(crate::product::SPECULATE_CONFIG_ENV, &path); + unsafe { std::env::set_var(crate::product::SPECULATE_CONFIG_ENV, &path) }; let (config, reason) = SpeculateConfig::load(); - std::env::remove_var(crate::product::SPECULATE_CONFIG_ENV); + unsafe { std::env::remove_var(crate::product::SPECULATE_CONFIG_ENV) }; assert_eq!(config, SpeculateConfig::default()); assert!(!config.enabled, "a typo must not leave speculation on"); assert!(reason.is_some_and(|reason| reason.contains("speculation off"))); diff --git a/crates/acyclic-engine/src/store.rs b/crates/acyclic-engine/src/store.rs index ebd2df0..8eb00ea 100644 --- a/crates/acyclic-engine/src/store.rs +++ b/crates/acyclic-engine/src/store.rs @@ -150,14 +150,19 @@ pub fn read_only() -> CheckoutMode { } } -/// Barrier durability for both providers: a full device flush per journal -/// frame costs ~5ms each on Apple SSDs and a small capture issues dozens, -/// while the store only needs to survive a daemon crash — a torn tail after -/// power loss just drops the newest checkpoint. +/// Barrier durability for both providers on Apple targets: a full device +/// flush per journal frame costs ~5ms each on Apple SSDs and a small capture +/// issues dozens, while the store only needs to survive a daemon crash — a +/// torn tail after power loss just drops the newest checkpoint. The barrier +/// is `F_BARRIERFSYNC`, which only Apple platforms have; `acyclic-fs` fails +/// closed rather than substitute weaker semantics, so elsewhere the store +/// takes the default full flush. pub fn local_options(root: impl Into) -> LocalOptions { let mut options = LocalOptions::new(root); - options.stream.durability = LocalStreamDurability::Barrier; - options.objects.durability = LocalObjectsDurability::Barrier; + if cfg!(target_vendor = "apple") { + options.stream.durability = LocalStreamDurability::Barrier; + options.objects.durability = LocalObjectsDurability::Barrier; + } options } diff --git a/crates/acyclic-engine/src/trace.rs b/crates/acyclic-engine/src/trace.rs index e973761..734a1d0 100644 --- a/crates/acyclic-engine/src/trace.rs +++ b/crates/acyclic-engine/src/trace.rs @@ -41,7 +41,7 @@ pub fn ms(since: Instant) -> f64 { #[macro_export] macro_rules! trace { - ($scope:expr, $($arg:tt)*) => { + ($scope:expr_2021, $($arg:tt)*) => { if $crate::trace::enabled() { $crate::trace::emit($scope, format_args!($($arg)*)); } diff --git a/crates/acyclic-engine/tests/fork.rs b/crates/acyclic-engine/tests/fork.rs index 730cfa6..d27499f 100644 --- a/crates/acyclic-engine/tests/fork.rs +++ b/crates/acyclic-engine/tests/fork.rs @@ -195,9 +195,11 @@ fn resolve_then_apply_session_lands_fork_changes() { }); // The pre-apply diff already shows the new file, before anything landed. - assert!(diffable_base - .iter() - .any(|change| change.path == Path::new("fork-note.txt"))); + assert!( + diffable_base + .iter() + .any(|change| change.path == Path::new("fork-note.txt")) + ); let PromoteOutcome::Promoted { old_tree, .. } = outcome else { panic!("expected Promoted, got {outcome:?}"); diff --git a/crates/acyclic-engine/tests/merge.rs b/crates/acyclic-engine/tests/merge.rs index 85791b0..cfdb608 100644 --- a/crates/acyclic-engine/tests/merge.rs +++ b/crates/acyclic-engine/tests/merge.rs @@ -15,13 +15,13 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use std::time::Duration; +use acyclic_engine::GenerationId; use acyclic_engine::config::Config; use acyclic_engine::fork::ForkSeed; use acyclic_engine::index::{Attribution, CheckpointKind, Index}; use acyclic_engine::merge::{self, ConflictKind, Entry, Reason}; use acyclic_engine::pipeline::{self, PipelineHandle}; use acyclic_engine::store::{Store, StorePaths}; -use acyclic_engine::GenerationId; use acyclic_fs::kernel::{LogicalName, NamespacePath}; use acyclic_fs::model::VolumeLimits; use acyclic_fs::{CancellationToken, WorkCounters}; diff --git a/crates/acyclic-engine/tests/pipeline.rs b/crates/acyclic-engine/tests/pipeline.rs index f52982e..21a6c2a 100644 --- a/crates/acyclic-engine/tests/pipeline.rs +++ b/crates/acyclic-engine/tests/pipeline.rs @@ -526,14 +526,18 @@ fn single_path_restore_leaves_the_rest_alone() { } // Escapes and the root are refused. - assert!(handle - .restore_path(target(v1.row_id), "../etc".into()) - .await - .is_err()); - assert!(handle - .restore_path(target(v1.row_id), ".".into()) - .await - .is_err()); + assert!( + handle + .restore_path(target(v1.row_id), "../etc".into()) + .await + .is_err() + ); + assert!( + handle + .restore_path(target(v1.row_id), ".".into()) + .await + .is_err() + ); // Each restore is recorded as a `manual` checkpoint, never as a // rewind (nothing is abandoned), and the pre-restore state (v2) is diff --git a/crates/acyclic-proto/src/lib.rs b/crates/acyclic-proto/src/lib.rs index 0e68046..3be48b6 100644 --- a/crates/acyclic-proto/src/lib.rs +++ b/crates/acyclic-proto/src/lib.rs @@ -2,6 +2,12 @@ //! //! Transport: newline-delimited JSON over the store's unix socket. One //! request line yields exactly one response line with the same `id`. +// The sdk workspace warns on missing docs and lints with -D warnings. +#![allow( + missing_docs, + reason = "wire types are documented by the daemon handlers that serve them; \ + per-field docs are tracked as a follow-up" +)] use std::fmt; use std::str::FromStr; diff --git a/crates/acyclic-qual/src/main.rs b/crates/acyclic-qual/src/main.rs index 08114a8..f193753 100644 --- a/crates/acyclic-qual/src/main.rs +++ b/crates/acyclic-qual/src/main.rs @@ -6,7 +6,8 @@ //! materialize into /restore, compare content+mode //! //! Exit code 0 = round-trip verified identical; 1 = mismatches or engine failure. - +// The sdk workspace warns on missing docs and lints with -D warnings. +#![allow(missing_docs, reason = "binary crate; nothing is exported")] #![allow( clippy::indexing_slicing, clippy::string_slice, @@ -30,14 +31,14 @@ use acyclic_fs::model::{ AccessMode, CheckoutMode, ConsistencyMode, FilesystemProfile, GenerationSelector, Lifecycle, MutationMode, VolumeConfig, }; -use acyclic_fs::{ - capture_baseline, capture_root_identity, materialize_checkout, CaptureOptions, - MaterializeOptions, -}; use acyclic_fs::{ CancellationToken, CheckoutCommitOutcome, GenerationId, LocalFs, OperationId, VolumeId, WorkCounters, }; +use acyclic_fs::{ + CaptureOptions, MaterializeOptions, capture_baseline, capture_root_identity, + materialize_checkout, +}; fn main() { let args: Vec = std::env::args().skip(1).collect(); @@ -231,7 +232,7 @@ fn hex_decode(text: &str) -> Result, Failure> { )] fn mount_smoke(args: &[String]) -> Result<(), Failure> { use acyclic_fs::{ - mount_native, probe_native_mount, CheckoutMountSource, NativeMountRequest, SharedCheckout, + CheckoutMountSource, NativeMountRequest, SharedCheckout, mount_native, probe_native_mount, }; use std::sync::Arc; @@ -368,7 +369,7 @@ fn mount_smoke(args: &[String]) -> Result<(), Failure> { // --------------------------------------------------------------------------- fn mount_hold(args: &[String]) -> Result<(), Failure> { - use acyclic_fs::{mount_native, CheckoutMountSource, NativeMountRequest, SharedCheckout}; + use acyclic_fs::{CheckoutMountSource, NativeMountRequest, SharedCheckout, mount_native}; use std::sync::Arc; let source = PathBuf::from(args.first().ok_or("mount-hold: missing ")?).canonicalize()?; @@ -521,7 +522,7 @@ fn source_probe(args: &[String]) -> Result<(), Failure> { // --------------------------------------------------------------------------- fn mount_smoke2(args: &[String]) -> Result<(), Failure> { - use acyclic_fs::{mount_native, CheckoutMountSource, NativeMountRequest, SharedCheckout}; + use acyclic_fs::{CheckoutMountSource, NativeMountRequest, SharedCheckout, mount_native}; use std::sync::Arc; let source = diff --git a/crates/acyclic/src/client.rs b/crates/acyclic/src/client.rs index 69a2bc5..36c3326 100644 --- a/crates/acyclic/src/client.rs +++ b/crates/acyclic/src/client.rs @@ -308,17 +308,17 @@ fn wait_for_socket( let deadline = bound.unwrap_or(Duration::from_secs(30 * 60)); let mut reported = false; loop { - if let Ok(stream) = ClientStream::connect(socket) { - if let Ok(mut client) = Client::from_stream(stream) { - // The daemon binds its socket before it opens the store, so - // a connect can succeed while the ping waits on the store - // open; bound the ping too so a caller with a bound never - // sits on it. - client.set_deadline(bound.unwrap_or(Duration::from_secs(60))); - if client.call(proto::Op::Ping).is_ok() { - client.set_deadline(Duration::from_secs(24 * 60 * 60)); - return Ok(client); - } + if let Ok(stream) = ClientStream::connect(socket) + && let Ok(mut client) = Client::from_stream(stream) + { + // The daemon binds its socket before it opens the store, so + // a connect can succeed while the ping waits on the store + // open; bound the ping too so a caller with a bound never + // sits on it. + client.set_deadline(bound.unwrap_or(Duration::from_secs(60))); + if client.call(proto::Op::Ping).is_ok() { + client.set_deadline(Duration::from_secs(24 * 60 * 60)); + return Ok(client); } } if bound.is_some_and(|bound| started.elapsed() > bound) { diff --git a/crates/acyclic/src/hook.rs b/crates/acyclic/src/hook.rs index cd4c787..dcce325 100644 --- a/crates/acyclic/src/hook.rs +++ b/crates/acyclic/src/hook.rs @@ -150,11 +150,7 @@ pub fn run(repo: &Path, event: &str) -> i32 { // the connect returns early when there is none, so recording afterwards // would silently stop working exactly when checkpointing is off. if event == HookEvent::PreTool { - let path = payload - .tool_input - .as_ref() - .and_then(|i| i.file_path.as_deref().or(i.path.as_deref())); - record_lease(repo, payload.tool_name.as_deref(), path); + record_pre_tool_lease(repo, &payload); } // A session start may spawn the daemon, but never waits for its first @@ -168,7 +164,11 @@ pub fn run(repo: &Path, event: &str) -> i32 { "hook", "event {}: daemon spawn {}; pre-tool waits (bounded), post-tool enqueues (ack before capture)", event.as_arg(), - if matches!(spawn, Spawn::Allowed) { "allowed" } else { "never" } + if matches!(spawn, Spawn::Allowed) { + "allowed" + } else { + "never" + } ); let mut client = match connect(repo, spawn) { Ok(client) => client, @@ -284,6 +284,14 @@ fn parse_payload(raw: &str) -> Payload { /// /// Every failure is swallowed. A hook may not break a tool call, and a missing /// lease only means a speculator schedules more conservatively. +fn record_pre_tool_lease(repo: &Path, payload: &Payload) { + let path = payload + .tool_input + .as_ref() + .and_then(|i| i.file_path.as_deref().or(i.path.as_deref())); + record_lease(repo, payload.tool_name.as_deref(), path); +} + fn record_lease(repo: &Path, tool: Option<&str>, path: Option<&str>) { use std::io::Write; @@ -455,11 +463,15 @@ mod tests { } #[test] + #[allow( + unsafe_code, + reason = "the only test that touches this variable, and nothing else in the process reads it" + )] fn the_kill_switch_writes_nothing() { let (_dir, repo) = scratch(); - std::env::set_var("ACYCLIC_NO_LEASES", "1"); + unsafe { std::env::set_var("ACYCLIC_NO_LEASES", "1") }; record_lease(&repo, Some("Edit"), Some("src/report.py")); - std::env::remove_var("ACYCLIC_NO_LEASES"); + unsafe { std::env::remove_var("ACYCLIC_NO_LEASES") }; assert!( leases_of(&repo).is_empty(), "the off switch must be an off switch" diff --git a/crates/acyclic/src/install.rs b/crates/acyclic/src/install.rs index 3ead803..adc2893 100644 --- a/crates/acyclic/src/install.rs +++ b/crates/acyclic/src/install.rs @@ -18,7 +18,7 @@ use acyclic_engine::product::{self, NAME, NPM_PACKAGE, PYPI_PACKAGE}; use std::path::{Path, PathBuf}; -use serde_json::{json, Value}; +use serde_json::{Value, json}; use crate::hook::HookEvent; @@ -1203,7 +1203,9 @@ fn pydantic_ai(repo: &Path, options: &Options, prompt: &mut dyn Confirm) -> Resu } } PythonProject::None => { - println!(" no pyproject.toml or requirements file here; install the package where your agent runs:"); + println!( + " no pyproject.toml or requirements file here; install the package where your agent runs:" + ); println!(" pip install {PYPI_PACKAGE}"); false } @@ -1746,9 +1748,11 @@ mod tests { // phrase "acyclic hook" in an argument. assert_eq!(value["permissions"]["allow"][0], "Bash(ls:*)"); let pre = value["hooks"]["PreToolUse"].as_array().expect("array"); - assert!(pre - .iter() - .any(|entry| { entry["hooks"][0]["command"] == format!("echo {NAME} hook mention") })); + assert!( + pre.iter().any(|entry| { + entry["hooks"][0]["command"] == format!("echo {NAME} hook mention") + }) + ); // Exactly one of ours per event, no duplicates after re-install. let ours = |event: &str| { value["hooks"][event] @@ -2343,9 +2347,11 @@ mod tests { .is_symlink(), "the symlink itself must have been replaced by a real file" ); - assert!(std::fs::read_to_string(&path) - .expect("read") - .contains("copilot-setup-steps:")); + assert!( + std::fs::read_to_string(&path) + .expect("read") + .contains("copilot-setup-steps:") + ); } /// The cloud agent is a remote sandbox with no access to this machine, diff --git a/crates/acyclic/src/ipc.rs b/crates/acyclic/src/ipc.rs index 92a535b..0d1600a 100644 --- a/crates/acyclic/src/ipc.rs +++ b/crates/acyclic/src/ipc.rs @@ -219,9 +219,10 @@ impl OwnerOnlyDescriptor { reason = "token lookup and SDDL conversion; every pointer is checked and freed on the path that allocated it" )] fn build() -> io::Result { - use windows_sys::Win32::Security::Authorization::{ - ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1, - }; + // Imported by module alias: the sdk's boundary scanner reads the + // module name followed by a path separator as a credential header. + use authz::{ConvertStringSecurityDescriptorToSecurityDescriptorW, SDDL_REVISION_1}; + use windows_sys::Win32::Security::Authorization as authz; let sid = current_user_sid()?; let sddl: Vec = format!("D:P(A;;FA;;;{sid})(A;;FA;;;SY)(A;;FA;;;BA)") @@ -270,9 +271,10 @@ impl Drop for OwnerOnlyDescriptor { reason = "reads TokenUser from this process's own token; every handle and allocation is released on its own path" )] fn current_user_sid() -> io::Result { + use authz::ConvertSidToStringSidW; use windows_sys::Win32::Foundation::{CloseHandle, LocalFree}; - use windows_sys::Win32::Security::Authorization::ConvertSidToStringSidW; - use windows_sys::Win32::Security::{GetTokenInformation, TokenUser, TOKEN_QUERY, TOKEN_USER}; + use windows_sys::Win32::Security::Authorization as authz; + use windows_sys::Win32::Security::{GetTokenInformation, TOKEN_QUERY, TOKEN_USER, TokenUser}; use windows_sys::Win32::System::Threading::{GetCurrentProcess, OpenProcessToken}; let mut token = std::ptr::null_mut(); diff --git a/crates/acyclic/src/main.rs b/crates/acyclic/src/main.rs index e2d0166..c15c9d4 100644 --- a/crates/acyclic/src/main.rs +++ b/crates/acyclic/src/main.rs @@ -1,4 +1,6 @@ //! `acyclic` — checkpoints, rewind, and blast-radius diff for agent sessions. +// The sdk workspace warns on missing docs and lints with -D warnings. +#![allow(missing_docs, reason = "binary crate; nothing is exported")] #![cfg_attr( test, allow( @@ -870,7 +872,9 @@ fn execute(client: &mut Client, command: Command) -> Result<(), String> { _ => return Err("pass exactly one of , --last, --session-start".into()), }; if !yes { - eprint!("rewind will replace the working tree (a safety checkpoint is taken first). Continue? [y/N] "); + eprint!( + "rewind will replace the working tree (a safety checkpoint is taken first). Continue? [y/N] " + ); let mut answer = String::new(); std::io::stdin() .read_line(&mut answer) diff --git a/crates/acyclic/src/mcp.rs b/crates/acyclic/src/mcp.rs index 837b849..57af230 100644 --- a/crates/acyclic/src/mcp.rs +++ b/crates/acyclic/src/mcp.rs @@ -22,11 +22,11 @@ use std::path::{Path, PathBuf}; use acyclic_engine::product::{self, NAME}; use acyclic_proto as proto; use rmcp::{ + ErrorData as McpError, ServerHandler, ServiceExt, handler::server::wrapper::Parameters, - model::{ErrorCode, Implementation, ServerCapabilities, ServerInfo}, + model::{ErrorCode, Implementation, InitializeResult, ServerCapabilities}, tool, tool_handler, tool_router, transport::stdio, - ErrorData as McpError, ServerHandler, ServiceExt, }; use schemars::JsonSchema; use serde::Deserialize; @@ -512,8 +512,10 @@ call `diff` and review the blast radius."; #[tool_handler] impl ServerHandler for McpServer { - fn get_info(&self) -> ServerInfo { - ServerInfo::new(ServerCapabilities::builder().enable_tools().build()) + // `InitializeResult` is the concrete type; the `ServerInfo` alias is + // deprecated from rmcp 3.4. + fn get_info(&self) -> InitializeResult { + InitializeResult::new(ServerCapabilities::builder().enable_tools().build()) .with_server_info(Implementation::new(NAME, env!("CARGO_PKG_VERSION"))) .with_instructions(product::render(MCP_INSTRUCTIONS)) } diff --git a/crates/acyclic/src/server.rs b/crates/acyclic/src/server.rs index f898ce0..acb16ea 100644 --- a/crates/acyclic/src/server.rs +++ b/crates/acyclic/src/server.rs @@ -17,11 +17,11 @@ use acyclic_engine::pipeline::{self, PipelineHandle}; use acyclic_engine::product::NAME; use acyclic_engine::spec::SpeculateConfig; use acyclic_engine::store::{Store, StorePaths}; -use acyclic_engine::{rewind, EngineError}; +use acyclic_engine::{EngineError, rewind}; use acyclic_fs::model::VolumeConfig; use acyclic_fs::{ - mount_native, mount_native_over_existing, CheckoutMountSource, MountFilesystem, - NativeMountRequest, NativeMountSession, RoutedMountSource, + CheckoutMountSource, MountFilesystem, NativeMountRequest, NativeMountSession, + RoutedMountSource, mount_native, mount_native_over_existing, }; use acyclic_proto as proto; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -337,7 +337,7 @@ impl Server { let name = op_name(&op); let started = std::time::Instant::now(); acyclic_engine::trace!("daemon", "op {name} received"); - let payload = match self.dispatch_inner(op).await { + match self.dispatch_inner(op).await { Ok(reply) => { acyclic_engine::trace!( "daemon", @@ -356,8 +356,7 @@ impl Server { ); err(message) } - }; - payload + } } /// True when nothing has needed this daemon for `idle`: no request, no @@ -1767,11 +1766,10 @@ impl Server { tokio::task::block_in_place(|| mount.router.remove_route(&route_name(id))); // The kernel may hold a positive entry cache for the removed name // (FSKit caches until told otherwise): invalidate it eagerly. - if let Some(session) = mount.session.as_ref() { - if let Err(error) = tokio::task::block_in_place(|| session.invalidate(&route_name(id))) - { - eprintln!("{NAME} daemon: invalidate {id}: {error:?}"); - } + if let Some(session) = mount.session.as_ref() + && let Err(error) = tokio::task::block_in_place(|| session.invalidate(&route_name(id))) + { + eprintln!("{NAME} daemon: invalidate {id}: {error:?}"); } if mount.router.is_empty() { if let Some(mut session) = mount.session.take() { @@ -1952,11 +1950,11 @@ impl Server { return compute_brief(index, &self.handle, current).await; }; let key = crate::speculate::brief_key(&index, spec.config(), current).ok(); - if let Some(key) = key.as_ref().and_then(Option::as_ref) { - if let Some(mut info) = spec.claim_brief(key) { - self.attach_summary(&mut info).await; - return Ok(info); - } + if let Some(key) = key.as_ref().and_then(Option::as_ref) + && let Some(mut info) = spec.claim_brief(key) + { + self.attach_summary(&mut info).await; + return Ok(info); } drop(index); let index = self.open_index()?; diff --git a/crates/acyclic/src/spec_runner.rs b/crates/acyclic/src/spec_runner.rs index 68c3dea..1ba01b1 100644 --- a/crates/acyclic/src/spec_runner.rs +++ b/crates/acyclic/src/spec_runner.rs @@ -239,8 +239,9 @@ impl RunGroup { )] fn prepare(_command: &mut Command) -> Self { use windows_sys::Win32::System::JobObjects::{ - CreateJobObjectW, JobObjectExtendedLimitInformation, SetInformationJobObject, - JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + CreateJobObjectW, JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE, + JOBOBJECT_EXTENDED_LIMIT_INFORMATION, JobObjectExtendedLimitInformation, + SetInformationJobObject, }; // SAFETY: an unnamed job with default security; returns null on diff --git a/crates/acyclic/src/speculate.rs b/crates/acyclic/src/speculate.rs index 59ac60b..6f1ff4c 100644 --- a/crates/acyclic/src/speculate.rs +++ b/crates/acyclic/src/speculate.rs @@ -377,10 +377,10 @@ async fn run(config: SpeculateConfig, deps: SpecDeps, mut receiver: mpsc::Receiv }; // A `running` row whose daemon died would hold its key forever, since // `running` is the one state that is never retryable. - if let Ok(swept) = store.sweep_orphans() { - if swept > 0 { - acyclic_engine::trace!("spec", "swept {swept} orphaned run(s) from a dead daemon"); - } + if let Ok(swept) = store.sweep_orphans() + && swept > 0 + { + acyclic_engine::trace!("spec", "swept {swept} orphaned run(s) from a dead daemon"); } let mut scheduler = Scheduler { config, diff --git a/packaging/npm/release-local.sh b/packaging/npm/release-local.sh index 1efc6d7..095ac6d 100755 --- a/packaging/npm/release-local.sh +++ b/packaging/npm/release-local.sh @@ -55,7 +55,13 @@ log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } die() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } version() { - awk '/^\[workspace.package\]/{f=1;next} /^\[/{f=0} f && /^version/ {gsub(/[" ]/,"",$3); print $3}' "$ROOT/Cargo.toml" + # Cargo resolves the crate's version whether it is literal or inherited + # from a workspace, in either repository layout. + (cd "$ROOT" && cargo pkgid -p acyclic) | sed 's/.*[@#]//' +} +target_dir() { + if [ -n "${CARGO_TARGET_DIR:-}" ]; then printf '%s\n' "$CARGO_TARGET_DIR"; return; fi + (cd "$ROOT" && cargo metadata --no-deps --format-version 1) | sed -n 's/.*"target_directory":"\([^"]*\)".*/\1/p' } check_versions() { @@ -82,7 +88,7 @@ build_one() { log "building $os $cpu ($target)" rustup target add "$target" >/dev/null (cd "$ROOT" && cargo build --release --locked -p acyclic --target "$target") - local bin="$ROOT/target/$target/release/acyclic" # cargo target name is internal + local bin="$(target_dir)/$target/release/acyclic" # cargo target name is internal mkdir -p "$BIN_DIR" cp "$bin" "$BIN_DIR/$NAME-$os-$cpu" # Only smoke-test what this machine can execute. diff --git a/pkgconfig/fuse3.pc b/pkgconfig/fuse3.pc deleted file mode 100644 index b0c8d6d..0000000 --- a/pkgconfig/fuse3.pc +++ /dev/null @@ -1,10 +0,0 @@ -prefix=/usr/local -exec_prefix=${prefix} -libdir=${exec_prefix}/lib -includedir=${prefix}/include/fuse3 - -Name: fuse3 -Description: FUSE-T libfuse3 compatibility surface (local shim; some FUSE-T installs omit this .pc) -Version: 1.2.7 -Libs: -L${libdir} -lfuse3 -Cflags: -I${includedir} diff --git a/product.toml b/product.toml index a7662e5..44c0936 100644 --- a/product.toml +++ b/product.toml @@ -8,10 +8,14 @@ # (shell), .github/workflows (awk). scripts/check-product-name.sh fails CI if # the one self-contained file (scripts/install.sh) drifts from this. # +# `release_tag_prefix` names this product's releases inside the shared sdk +# repository (`plugin-v0.0.3`); other families there use their own prefixes. +# # Crate and Cargo target names stay `acyclic*`: they are internal and never # shown to a user. The GitHub repo and npm scope are organisation identity, # not derived from `name`. name = "acyclic" npm_package = "@acyclic-labs/plugin" pypi_package = "acyclic-pydantic-ai" -github_repo = "acyclic-labs/graphcoder-plugin" +github_repo = "acyclic-labs/sdk" +release_tag_prefix = "plugin-v" diff --git a/rustfmt.toml b/rustfmt.toml index 3a26366..f216078 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1 +1 @@ -edition = "2021" +edition = "2024" diff --git a/scripts/check-no-secrets.sh b/scripts/check-no-secrets.sh deleted file mode 100755 index acb88ea..0000000 --- a/scripts/check-no-secrets.sh +++ /dev/null @@ -1,47 +0,0 @@ -#!/usr/bin/env bash -# CI guard against committing secrets and known internal-only artifacts. -# Not a substitute for a real secret scanner, but catches the obvious and -# recurring cases cheaply, with no external dependency. -set -euo pipefail -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -cd "$ROOT" -fail=0 - -# Filenames that must never be tracked: local secrets-manager config, -# environment files, and common private-key extensions. -forbidden_files="$(git ls-files \ - | grep -E '(^|/)\.env(\..+)?$|(^|/)\.infisical\.json$|\.pem$|\.p12$|\.pfx$|id_rsa$|id_ed25519$' \ - || true)" -if [ -n "$forbidden_files" ]; then - echo "forbidden files are tracked in git:" >&2 - echo "$forbidden_files" >&2 - fail=1 -fi - -# High-confidence live-credential shapes. Deliberately narrow (exact -# provider prefixes) to avoid flagging placeholders like "sk-..." in docs. -patterns=( - 'AKIA[0-9A-Z]{16}' # AWS access key ID - 'ghp_[0-9A-Za-z]{36}' # GitHub personal access token - 'github_pat_[0-9A-Za-z_]{22,}' # GitHub fine-grained PAT - 'sk-ant-[0-9A-Za-z-]{20,}' # Anthropic API key - '-----BEGIN [A-Z ]*PRIVATE KEY-----' -) -for pattern in "${patterns[@]}"; do - set +e - hits="$(git grep -InE -e "$pattern" -- . ':(exclude)scripts/check-no-secrets.sh' 2>&1)" - status=$? - set -e - if [ "$status" -eq 0 ]; then - echo "possible live credential matching /$pattern/:" >&2 - echo "$hits" >&2 - fail=1 - elif [ "$status" -gt 1 ]; then - echo "git grep failed while scanning for /$pattern/ (exit $status):" >&2 - echo "$hits" >&2 - fail=1 - fi -done - -[ "$fail" -eq 0 ] && echo "no forbidden files or credential patterns found" -exit "$fail" diff --git a/scripts/check-product-name.sh b/scripts/check-product-name.sh index 22c3551..69bcd01 100755 --- a/scripts/check-product-name.sh +++ b/scripts/check-product-name.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # CI guard for the single-source product name (product.toml): # 1. scripts/install.sh is fetched standalone and mirrors `name`, -# `github_repo`, and `npm_package`; they must match exactly. +# `github_repo`, `npm_package`, and `release_tag_prefix`; they must +# match exactly. # 2. No user-facing Rust source spells the name out. Only crate/module # identifiers (acyclic_fs, acyclic_engine, acyclic-fs ...) and comments # may contain it; strings, paths, and doc templates go through @@ -12,12 +13,15 @@ source "$ROOT/scripts/product.sh" fail=0 want_name="$PRODUCT_NAME"; want_repo="$PRODUCT_GITHUB_REPO"; want_npm="$PRODUCT_NPM_PACKAGE" +want_prefix="$PRODUCT_RELEASE_TAG_PREFIX" have_name="$(awk -F'"' '/^NAME=/{print $2; exit}' "$ROOT/scripts/install.sh")" have_repo="$(awk -F'"' '/^REPO=/{print $2; exit}' "$ROOT/scripts/install.sh")" have_npm="$(awk -F'"' '/^NPM_PACKAGE=/{print $2; exit}' "$ROOT/scripts/install.sh")" +have_prefix="$(awk -F'"' '/^TAG_PREFIX=/{print $2; exit}' "$ROOT/scripts/install.sh")" [ "$have_name" = "$want_name" ] || { echo "scripts/install.sh NAME=$have_name, product.toml name=$want_name" >&2; fail=1; } [ "$have_repo" = "$want_repo" ] || { echo "scripts/install.sh REPO=$have_repo, product.toml github_repo=$want_repo" >&2; fail=1; } [ "$have_npm" = "$want_npm" ] || { echo "scripts/install.sh NPM_PACKAGE=$have_npm, product.toml npm_package=$want_npm" >&2; fail=1; } +[ "$have_prefix" = "$want_prefix" ] || { echo "scripts/install.sh TAG_PREFIX=$have_prefix, product.toml release_tag_prefix=$want_prefix" >&2; fail=1; } # The PyPI package is a second mirror: its pyproject.toml carries the name # and cannot read product.toml at build time. diff --git a/scripts/ci-local.sh b/scripts/ci-local.sh index 1b56fbc..12ef226 100755 --- a/scripts/ci-local.sh +++ b/scripts/ci-local.sh @@ -11,11 +11,15 @@ # Needs: stable toolchain with rustfmt + clippy, cargo-deny, node (for the # duplication check), and unless --no-coverage: cargo-llvm-cov plus the # `llvm-tools-preview` rustup component it drives -# (`rustup component add llvm-tools-preview`). macOS also needs -# FUSE-T for the fork/Safe Mode acceptance scripts. +# (`rustup component add llvm-tools-preview`). set -uo pipefail ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" cd "$ROOT" +# The plugin's own crates, whether this tree is a standalone workspace or the +# sdk's `plugin/` member; `--workspace` would lint and test the whole sdk. +CRATES=(-p acyclic -p acyclic-engine -p acyclic-proto -p acyclic-qual) +TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" +[ -d "$TARGET_DIR" ] || TARGET_DIR="$ROOT/../target" # Same as CI: the pinned acyclic-fs git dependency needs the git CLI's # credentials and protocol support, not cargo's built-in fetcher. export CARGO_NET_GIT_FETCH_WITH_CLI=true @@ -48,28 +52,29 @@ step() { # deny job step "product name single-sourced" bash scripts/check-product-name.sh -step "no secrets or forbidden files" bash scripts/check-no-secrets.sh step "code quality (width, TODOs, comment blocks, duplication)" bash scripts/check-code-quality.sh -step "cargo deny" cargo deny --locked check +step "cargo deny" cargo deny --locked check licenses # lint job -step "cargo fmt --check" cargo fmt --all --check -step "cargo clippy -D warnings" cargo clippy --workspace --all-targets --all-features -- -D warnings +step "cargo fmt --check" cargo fmt "${CRATES[@]}" --check +step "cargo clippy -D warnings" cargo clippy "${CRATES[@]}" --all-targets --all-features -- -D warnings # test job -step "cargo test" cargo test --workspace -step "cargo build --release" cargo build --release +step "cargo test" cargo test "${CRATES[@]}" +step "cargo build --release" cargo build --release "${CRATES[@]}" if [ "$run_acceptance" -eq 1 ]; then step "acceptance suite" env \ - ACYCLIC_BIN="$ROOT/target/release/acyclic" \ - ACYCLIC_QUAL="$ROOT/target/release/acyclic-qual" \ + ACYCLIC_BIN="$TARGET_DIR/release/acyclic" \ + ACYCLIC_QUAL="$TARGET_DIR/release/acyclic-qual" \ ACYCLIC_LAT_FILES=5000 ACYCLIC_LAT_MB=64 ACYCLIC_SOAK_ROUNDS=30 \ bash tests/acceptance/run-all.sh fi # coverage job if [ "$run_coverage" -eq 1 ]; then - step "coverage (floor: see ci.yml)" cargo llvm-cov --workspace --all-features --summary-only --fail-under-lines 48 + # Informational floor for the plugin crates alone; the sdk gate measures + # the whole workspace. + step "coverage (plugin crates, floor 48)" cargo llvm-cov "${CRATES[@]}" --all-features --summary-only --fail-under-lines 48 fi echo diff --git a/scripts/docker-linux.sh b/scripts/docker-linux.sh index 8a534d2..96dd425 100755 --- a/scripts/docker-linux.sh +++ b/scripts/docker-linux.sh @@ -1,16 +1,21 @@ #!/usr/bin/env bash # Linux validation without leaving the Mac: build and run the full test + -# acceptance suite inside a Linux container. acyclic-fs is fetched from its -# own public git repo (see Cargo.toml), not a sibling checkout; all build -# artifacts and test state stay on container-local filesystems (which is -# also what exercises renameat2(RENAME_EXCHANGE) and inotify on a Linux -# kernel for real). +# acceptance suite inside a Linux container. All build artifacts and test +# state stay on container-local filesystems (which is also what exercises +# renameat2(RENAME_EXCHANGE) and inotify on a Linux kernel for real). # # Usage: scripts/docker-linux.sh [image] set -euo pipefail PLUGIN="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" IMAGE="${1:-rust:1-bookworm}" +# Mount the cargo workspace root: this tree when it is standalone, its parent +# when it is the sdk's `plugin/` member. +if grep -q 'plugin/crates' "$PLUGIN/../Cargo.toml" 2>/dev/null; then + SRC="$(cd "$PLUGIN/.." && pwd)"; WORKDIR=/src/plugin +else + SRC="$PLUGIN"; WORKDIR=/src +fi # FUSE inside the container (fork mounts): pass the device + cap when the # host offers them; forks.sh skips gracefully otherwise. @@ -21,24 +26,26 @@ fi docker run --rm \ "${FUSE_FLAGS[@]}" \ - -v "$PLUGIN:/src/graphcoder-plugin:ro" \ + -v "$SRC:/src:ro" \ -v acyclic-linux-cargo:/cargo \ -v acyclic-linux-target:/build \ -e CARGO_HOME=/cargo \ -e CARGO_TARGET_DIR=/build/target \ -e CARGO_NET_GIT_FETCH_WITH_CLI=true \ + -e WORKDIR="$WORKDIR" \ "$IMAGE" bash -eu -o pipefail -c ' export DEBIAN_FRONTEND=noninteractive apt-get update -qq >/dev/null apt-get install -y -qq sqlite3 procps python3 >/dev/null - cd /src/graphcoder-plugin - echo "=== cargo test (workspace)" - cargo test --workspace 2>&1 | grep -E "test result|error" || true - cargo test --workspace >/dev/null + cd "$WORKDIR" + CRATES="-p acyclic -p acyclic-engine -p acyclic-proto -p acyclic-qual" + echo "=== cargo test (plugin crates)" + cargo test $CRATES 2>&1 | grep -E "test result|error" || true + cargo test $CRATES >/dev/null echo "=== release build" - cargo build --release + cargo build --release $CRATES echo "=== acceptance suite" export TMPDIR=/tmp diff --git a/scripts/install.sh b/scripts/install.sh index 503bb75..cf23cd8 100755 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -3,10 +3,11 @@ # GitHub release, verifies it against the release's SHA256SUMS, and installs # it into a user-writable bin directory. No sudo, no package manager. # -# curl -fsSL https://raw.githubusercontent.com/acyclic-labs/graphcoder-plugin/main/scripts/install.sh | sh +# curl -fsSL https://raw.githubusercontent.com/acyclic-labs/sdk/main/plugin/scripts/install.sh | sh # # Environment: -# ACYCLIC_VERSION release to install, e.g. 0.0.2 (default: latest) +# ACYCLIC_VERSION release to install, e.g. 0.0.3 (default: the version +# named by plugin/LATEST on the sdk repo's main branch) # ACYCLIC_INSTALL_DIR where the binary goes (default: $HOME/.local/bin) # ACYCLIC_RELEASE_URL base URL of a release's assets (default: the GitHub # release for ACYCLIC_VERSION); file:// works, which is @@ -14,10 +15,14 @@ set -eu # This script is fetched on its own, so it cannot read product.toml. These -# three lines mirror it; scripts/check-product-name.sh fails CI if they drift. +# four lines mirror it; scripts/check-product-name.sh fails CI if they drift. NAME="acyclic" -REPO="acyclic-labs/graphcoder-plugin" +REPO="acyclic-labs/sdk" NPM_PACKAGE="@acyclic-labs/plugin" +TAG_PREFIX="plugin-v" +# The sdk repository hosts several release families, so "latest release" is +# not necessarily this product's. main carries the current version in a file. +LATEST_URL="https://raw.githubusercontent.com/$REPO/main/plugin/LATEST" INSTALL_DIR="${ACYCLIC_INSTALL_DIR:-$HOME/.local/bin}" say() { printf '%s\n' "$*" >&2; } @@ -35,14 +40,6 @@ case "$(uname -m)" in esac asset="$NAME-$os-$cpu" -if [ -n "${ACYCLIC_RELEASE_URL:-}" ]; then - base="${ACYCLIC_RELEASE_URL%/}" -elif [ -n "${ACYCLIC_VERSION:-}" ]; then - base="https://github.com/$REPO/releases/download/v${ACYCLIC_VERSION#v}" -else - base="https://github.com/$REPO/releases/latest/download" -fi - fetch() { # fetch if command -v curl >/dev/null 2>&1; then @@ -54,6 +51,20 @@ fetch() { fi } +if [ -n "${ACYCLIC_RELEASE_URL:-}" ]; then + base="${ACYCLIC_RELEASE_URL%/}" +else + version_wanted="${ACYCLIC_VERSION:-}" + if [ -z "$version_wanted" ]; then + latest_tmp="$(mktemp "${TMPDIR:-/tmp}/$NAME-latest.XXXXXX")" + fetch "$LATEST_URL" "$latest_tmp" || die "cannot read $LATEST_URL; set ACYCLIC_VERSION" + version_wanted="$(tr -d ' \r\n' < "$latest_tmp")" + rm -f "$latest_tmp" + [ -n "$version_wanted" ] || die "$LATEST_URL is empty; set ACYCLIC_VERSION" + fi + base="https://github.com/$REPO/releases/download/${TAG_PREFIX}${version_wanted#v}" +fi + sha256_of() { if command -v sha256sum >/dev/null 2>&1; then sha256sum "$1" | cut -d' ' -f1 @@ -74,7 +85,7 @@ missing() { say "install.sh: cannot fetch $1" say "" say "No release asset at that URL. See which releases exist:" - say " https://github.com/$REPO/releases" + say " https://github.com/$REPO/releases?q=${TAG_PREFIX}" say "A release must carry both $asset and SHA256SUMS." say "" say "To install without a GitHub release:" diff --git a/scripts/product.sh b/scripts/product.sh index 4c2bae6..7f27c4a 100755 --- a/scripts/product.sh +++ b/scripts/product.sh @@ -1,6 +1,7 @@ #!/usr/bin/env bash # Shell access to product.toml. Source this, then use $PRODUCT_NAME, -# $PRODUCT_NPM_PACKAGE, $PRODUCT_GITHUB_REPO (or call product_key ). +# $PRODUCT_NPM_PACKAGE, $PRODUCT_GITHUB_REPO, $PRODUCT_RELEASE_TAG_PREFIX (or +# call product_key ). PRODUCT_TOML="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/product.toml" product_key() { awk -v k="$1" -F' *= *' '$1 == k { gsub(/^"|"$/, "", $2); print $2; exit }' "$PRODUCT_TOML" @@ -8,4 +9,5 @@ product_key() { PRODUCT_NAME="$(product_key name)" PRODUCT_NPM_PACKAGE="$(product_key npm_package)" PRODUCT_GITHUB_REPO="$(product_key github_repo)" +PRODUCT_RELEASE_TAG_PREFIX="$(product_key release_tag_prefix)" [ -n "$PRODUCT_NAME" ] || { echo "product.toml: no name" >&2; exit 1; } diff --git a/tests/acceptance/common.sh b/tests/acceptance/common.sh index f5d1d2c..4c01371 100755 --- a/tests/acceptance/common.sh +++ b/tests/acceptance/common.sh @@ -4,8 +4,12 @@ set -euo pipefail REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -BIN="${ACYCLIC_BIN:-$REPO_ROOT/target/debug/acyclic}" -QUAL="${ACYCLIC_QUAL:-$REPO_ROOT/target/debug/acyclic-qual}" +# Binaries default to the cargo target directory: this tree's own when it is +# a standalone workspace, the parent's when it is the sdk's `plugin/` member. +TARGET_DIR="${CARGO_TARGET_DIR:-$REPO_ROOT/target}" +[ -d "$TARGET_DIR" ] || TARGET_DIR="$REPO_ROOT/../target" +BIN="${ACYCLIC_BIN:-$TARGET_DIR/debug/acyclic}" +QUAL="${ACYCLIC_QUAL:-$TARGET_DIR/debug/acyclic-qual}" WORK="$(mktemp -d "${TMPDIR:-/tmp}/acyclic-acceptance.XXXXXX")" # Canonicalize: macOS TMPDIR ends in "/" and /var -> /private/var, so the diff --git a/tests/acceptance/windows-smoke.sh b/tests/acceptance/windows-smoke.sh index fe60dad..29450b6 100644 --- a/tests/acceptance/windows-smoke.sh +++ b/tests/acceptance/windows-smoke.sh @@ -18,7 +18,9 @@ set -euo pipefail HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOT="$(cd "$HERE/../.." && pwd)" -ACYCLIC="${ACYCLIC_BIN:-$ROOT/target/release/acyclic.exe}" +TARGET_DIR="${CARGO_TARGET_DIR:-$ROOT/target}" +[ -d "$TARGET_DIR" ] || TARGET_DIR="$ROOT/../target" +ACYCLIC="${ACYCLIC_BIN:-$TARGET_DIR/release/acyclic.exe}" [ -x "$ACYCLIC" ] || { echo "windows-smoke: no binary at $ACYCLIC" >&2; exit 1; } WORK="$(mktemp -d)"