Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
7a73579
feat(core): inject a neutral workspace layout
joshuajbouw Jul 14, 2026
e2d2e12
fix(core): satisfy workspace layout CI gates
joshuajbouw Jul 14, 2026
ddf73ae
fix(core): close workspace layout CI findings
joshuajbouw Jul 14, 2026
0eef532
fix(core): close remaining workspace layout lints
joshuajbouw Jul 14, 2026
8aec624
fix(core): satisfy remaining workspace layout lint gates
joshuajbouw Jul 14, 2026
7fd8b71
fix(cli): satisfy workspace metadata lint
joshuajbouw Jul 14, 2026
2792d20
fix(kernel): secure readiness state directory
joshuajbouw Jul 14, 2026
ba16bd4
fix(cli): validate the selected workspace once
joshuajbouw Jul 14, 2026
a694e0b
fix(core): build workspace fingerprinting with BLAKE3
joshuajbouw Jul 15, 2026
ea4ce11
fix(core): preserve Windows path hashing
joshuajbouw Jul 15, 2026
c5d5a26
Gate CLI daemon clients by workspace
joshuajbouw Jul 15, 2026
7153674
Make workspace recovery scope explicit
joshuajbouw Jul 15, 2026
abdf40a
Harden workspace-aware CLI reconnects
joshuajbouw Jul 15, 2026
17d569e
test(gateway): compose workspace and audit builders
joshuajbouw Jul 15, 2026
8380852
fix(cli): bind init grants to selected workspace
joshuajbouw Jul 15, 2026
25abb00
test(gateway): prove composed builder contracts
joshuajbouw Jul 15, 2026
2207e03
fix: harden workspace state path selection
joshuajbouw Jul 15, 2026
73a8ea0
fix: reject redirected workspace descendants
joshuajbouw Jul 15, 2026
9c28fa9
fix(ci): restore workspace portability checks
joshuajbouw Jul 15, 2026
373912d
refactor(kernel): isolate capsule visibility
joshuajbouw Jul 15, 2026
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
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,17 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked.
duplicate, or SHA-only manifests. Existing v0.9.x installations can still
cross the boundary through the compatibility manifest. External protocol
requirements remain unchanged. Closes #1249.
- **Project runtime state now uses one validated workspace layout.** The CLI
and daemon default to `.astrid`, while distributions can select another safe
relative directory name through `--workspace-state-dir` or
`ASTRID_WORKSPACE_STATE_DIR`. Config, capsule installation and discovery,
kernel boot, gateway source checks, hooks, and WIT garbage collection share
the selected layout and reject persistent symlink/reparse redirection anywhere
in workspace capsule and hook trees before reading them. They never scan both
project roots. CLI uplinks, including long-lived MCP reconnects, and
project-sensitive management reads reject a
daemon booted for a different project or layout; daemon stop remains
available as a recovery operation.

- **Runtime E2E now stages the pinned Unicity AOS monorepo.** The workflow
preserves the AOS Cargo workspace outside the core checkout and supplies
Expand Down
1 change: 1 addition & 0 deletions Cargo.lock

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

118 changes: 106 additions & 12 deletions crates/astrid-capsule-install/src/archive.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,14 @@ use std::path::Path;

use anyhow::{Context, bail};
use astrid_capsule::capsule::CapsuleId;
use astrid_core::dirs::AstridHome;
use astrid_core::dirs::{AstridHome, WorkspaceLayout};

use astrid_core::PrincipalId;

use crate::local::{
InstallOptions, InstallOutput, install_from_local_path_checked_for_principal,
install_from_local_path_for_principal,
ExpectedCapsuleIdentity, InstallOptions, InstallOutput, InstallWorkspace,
install_from_local_path_checked_for_principal_in_workspace,
install_from_local_path_for_principal_in_workspace,
};

/// Unpack `archive_path` (a gzipped tar) into a tempdir, then install
Expand All @@ -35,11 +36,22 @@ pub fn unpack_and_install(
home: &AstridHome,
options: InstallOptions,
) -> anyhow::Result<InstallOutput> {
unpack_and_install_for_principal(
unpack_and_install_with_layout(archive_path, home, options, &WorkspaceLayout::default())
}

/// Unpack and install using an explicit workspace layout.
pub fn unpack_and_install_with_layout(
archive_path: &Path,
home: &AstridHome,
options: InstallOptions,
workspace_layout: &WorkspaceLayout,
) -> anyhow::Result<InstallOutput> {
unpack_and_install_for_principal_with_layout(
archive_path,
home,
options,
&crate::paths::install_principal(),
workspace_layout,
)
}

Expand All @@ -50,7 +62,54 @@ pub fn unpack_and_install_for_principal(
options: InstallOptions,
target_principal: &PrincipalId,
) -> anyhow::Result<InstallOutput> {
unpack_and_install_internal(archive_path, home, options, target_principal, None, None)
unpack_and_install_for_principal_with_layout(
archive_path,
home,
options,
target_principal,
&WorkspaceLayout::default(),
)
}

/// Unpack and install for an explicit principal and workspace layout.
pub fn unpack_and_install_for_principal_with_layout(
archive_path: &Path,
home: &AstridHome,
options: InstallOptions,
target_principal: &PrincipalId,
workspace_layout: &WorkspaceLayout,
) -> anyhow::Result<InstallOutput> {
let workspace_root = std::env::current_dir().ok();
unpack_and_install_for_principal_in_workspace(
archive_path,
home,
options,
target_principal,
workspace_root.as_deref(),
workspace_layout,
)
}

/// Unpack and install with explicit principal and workspace inputs.
pub fn unpack_and_install_for_principal_in_workspace(
archive_path: &Path,
home: &AstridHome,
options: InstallOptions,
target_principal: &PrincipalId,
workspace_root: Option<&Path>,
workspace_layout: &WorkspaceLayout,
) -> anyhow::Result<InstallOutput> {
unpack_and_install_internal(
archive_path,
home,
options,
target_principal,
InstallWorkspace {
root: workspace_root,
layout: workspace_layout,
},
None,
)
}

/// Unpack and install only when the archive manifest identity equals
Expand All @@ -69,23 +128,51 @@ pub fn unpack_and_install_checked_for_principal(
expected: &CapsuleId,
expected_version: Option<&str>,
) -> anyhow::Result<InstallOutput> {
unpack_and_install_internal(
unpack_and_install_checked_for_principal_with_layout(
archive_path,
home,
options,
target_principal,
Some(expected),
expected,
expected_version,
&WorkspaceLayout::default(),
)
}

fn unpack_and_install_internal(
/// Checked archive install for an explicit principal and workspace layout.
pub fn unpack_and_install_checked_for_principal_with_layout(
archive_path: &Path,
home: &AstridHome,
options: InstallOptions,
target_principal: &PrincipalId,
expected: Option<&CapsuleId>,
expected: &CapsuleId,
expected_version: Option<&str>,
workspace_layout: &WorkspaceLayout,
) -> anyhow::Result<InstallOutput> {
let workspace_root = std::env::current_dir().ok();
unpack_and_install_internal(
archive_path,
home,
options,
target_principal,
InstallWorkspace {
root: workspace_root.as_deref(),
layout: workspace_layout,
},
Some(ExpectedCapsuleIdentity {
id: expected,
version: expected_version,
}),
)
}

fn unpack_and_install_internal(
archive_path: &Path,
home: &AstridHome,
options: InstallOptions,
target_principal: &PrincipalId,
workspace: InstallWorkspace<'_>,
expected: Option<ExpectedCapsuleIdentity<'_>>,
) -> anyhow::Result<InstallOutput> {
let tmp_dir = tempfile::tempdir().context("failed to create temp dir for unpacking")?;
let unpack_dir = tmp_dir.path();
Expand Down Expand Up @@ -132,14 +219,21 @@ fn unpack_and_install_internal(
}

match expected {
Some(expected) => install_from_local_path_checked_for_principal(
Some(expected) => install_from_local_path_checked_for_principal_in_workspace(
unpack_dir,
home,
options,
target_principal,
workspace,
expected,
expected_version,
),
None => install_from_local_path_for_principal(unpack_dir, home, options, target_principal),
None => install_from_local_path_for_principal_in_workspace(
unpack_dir,
home,
options,
target_principal,
workspace.root,
workspace.layout,
),
}
}
3 changes: 1 addition & 2 deletions crates/astrid-capsule-install/src/contracts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -300,8 +300,7 @@ fn is_blake3_pin(pin: &str) -> bool {
/// daemon can warn rather than silently skip the baseline. Per-entry errors
/// are logged and skipped so one bad entry can't abort the whole scan.
///
/// Reads only the injected `home` — never the process environment or a
/// workspace `.astrid/` (unlike [`scan_installed_capsules`](crate::scan_installed_capsules)) —
/// Reads only the injected `home`, never process environment or project state,
/// so it is safe on the fail-closed daemon boot path.
fn daemon_fleet_contracts_pin(home: &AstridHome) -> anyhow::Result<Option<String>> {
let principal = crate::paths::install_principal();
Expand Down
23 changes: 18 additions & 5 deletions crates/astrid-capsule-install/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,10 @@ pub mod wasm;
pub mod wit;

pub use archive::{
unpack_and_install, unpack_and_install_checked_for_principal, unpack_and_install_for_principal,
unpack_and_install, unpack_and_install_checked_for_principal,
unpack_and_install_checked_for_principal_with_layout, unpack_and_install_for_principal,
unpack_and_install_for_principal_in_workspace, unpack_and_install_for_principal_with_layout,
unpack_and_install_with_layout,
};
pub use contracts::{
CONTRACTS_WIT_BASENAME, ContractsSkew, canonical_contracts_b3, canonical_contracts_path,
Expand All @@ -83,16 +86,26 @@ pub use contracts::{
pub use copy::copy_capsule_dir;
pub use local::{
InstallOptions, InstallOutput, InstallPhase, install_from_local_path,
install_from_local_path_checked_for_principal, install_from_local_path_for_principal,
install_from_local_path_checked_for_principal,
install_from_local_path_checked_for_principal_with_layout,
install_from_local_path_for_principal, install_from_local_path_for_principal_in_workspace,
install_from_local_path_for_principal_with_layout, install_from_local_path_with_layout,
};
pub use manifest_check::{
ExportConflict, MissingImport, check_export_conflicts, check_export_conflicts_in_workspace,
check_export_conflicts_with_layout, validate_imports, validate_imports_in_workspace,
validate_imports_with_layout,
};
pub use manifest_check::{ExportConflict, MissingImport, check_export_conflicts, validate_imports};
pub use meta::{
CapsuleLocation, CapsuleMeta, InstalledCapsule, read_meta, scan_installed_capsules,
scan_installed_capsules_in_home_for, write_meta,
scan_installed_capsules_in_home_for, scan_installed_capsules_in_home_for_in_workspace,
scan_installed_capsules_in_home_for_with_layout, scan_installed_capsules_in_home_with_layout,
scan_installed_capsules_with_layout, write_meta,
};
pub use paths::{
resolve_env_path, resolve_env_path_for, resolve_target_dir, resolve_target_dir_for,
restore_env_from_backup, restore_env_from_backup_for,
resolve_target_dir_for_in_workspace, resolve_target_dir_for_with_layout,
resolve_target_dir_with_layout, restore_env_from_backup, restore_env_from_backup_for,
};
pub use principal_introspection::materialize_principal_introspection;
pub use wit::{content_address_wit, materialize_wit_mirror};
Expand Down
Loading
Loading