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
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,11 @@ Changelog tracking starts with 0.2.0. Prior versions were not tracked.

### Added

- **Operators can enforce a distro for `astrid init`.**
`ASTRID_ENFORCED_DISTRO` supplies the distro source and rejects CLI attempts
to override it. Standalone `astrid init` requires an explicit `--distro`;
Astrid Runtime never chooses a product distro. Closes #1253.

- **Passive content-addressed capability-registry primitives.** Astrid now has
exact capability IDs, typed content-bound references, immutable registered
definitions, deterministic BLAKE3 semantic digests and canonical registry
Expand Down
50 changes: 7 additions & 43 deletions crates/astrid-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,8 @@ pub(crate) enum Commands {

/// Initialize a workspace and install a distro
Init {
/// Required distro to install (name, @org/repo, path to Distro.toml, or .shuttle)
/// Distro source to install. Required unless an embedding launcher sets
/// `ASTRID_ENFORCED_DISTRO` (`@owner/repo`, URL, local Distro.toml, or .shuttle).
#[arg(long)]
distro: Option<String>,
/// Non-interactive: accept all defaults.
Expand Down Expand Up @@ -512,7 +513,7 @@ pub(crate) enum SessionCommands {
pub(crate) enum DistroCommands {
/// Apply a distro to the active or specified agent.
Apply {
/// Distro identifier (name, `@org/repo`, path, or .shuttle).
/// Distro source (`@owner/repo`, URL, local Distro.toml, or .shuttle).
name: Option<String>,
/// Target agent (defaults to active context).
#[arg(short, long)]
Expand Down Expand Up @@ -561,6 +562,10 @@ pub(crate) enum DistroCommands {
},
}

#[cfg(test)]
#[path = "cli_distro_tests.rs"]
mod distro_tests;

#[cfg(test)]
mod tests {
use super::{CapsuleCommands, Cli, Commands, InviteCommand};
Expand Down Expand Up @@ -684,47 +689,6 @@ mod tests {
));
}

#[test]
fn grant_capsules_parsing_stays_open_for_embedding_composition() {
let cli = Cli::try_parse_from(["astrid", "init", "--grant-capsules"])
.expect("parsing stays open so an embedding layer can resolve the distro source");

assert!(matches!(
cli.command,
Some(Commands::Init {
distro: None,
grant_capsules: true,
..
})
));
}

#[test]
fn init_parses_target_separately_from_operator() {
let cli = Cli::try_parse_from([
"astrid",
"--principal",
"operator-1",
"init",
"--distro",
"./Distro.toml",
"--target-principal",
"agent-1",
"--grant-capsules",
])
.expect("operator and target principal should parse independently");

assert_eq!(cli.principal.as_deref(), Some("operator-1"));
assert!(matches!(
cli.command,
Some(Commands::Init {
target_principal: Some(ref target),
grant_capsules: true,
..
}) if target == "agent-1"
));
}

#[test]
fn global_format_does_not_collide_with_nested_format_enum() {
let cli = Cli::try_parse_from(["astrid", "keypair", "pubkey", "e2e-cli-key"])
Expand Down
101 changes: 101 additions & 0 deletions crates/astrid-cli/src/cli_distro_tests.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
use clap::{CommandFactory, Parser};

use super::{Cli, Commands};

#[test]
fn grant_capsules_allows_an_operator_enforced_distro() {
let cli = Cli::try_parse_from(["astrid", "init", "--grant-capsules"])
.expect("the operator may supply the distro outside the CLI argument surface");

assert!(matches!(
cli.command,
Some(Commands::Init {
distro: None,
grant_capsules: true,
..
})
));
}

#[test]
fn distro_help_lists_only_supported_sources_and_the_launcher_exception() {
let mut command = Cli::command();
let init = command.find_subcommand_mut("init").expect("init command");
let init_distro_help = init
.get_arguments()
.find(|argument| argument.get_id() == "distro")
.and_then(|argument| argument.get_help())
.map(ToString::to_string);

let mut command = Cli::command();
let apply = command
.find_subcommand_mut("distro")
.expect("distro command")
.find_subcommand_mut("apply")
.expect("distro apply command");
let apply_name_help = apply
.get_arguments()
.find(|argument| argument.get_id() == "name")
.and_then(|argument| argument.get_help())
.map(ToString::to_string);

assert_eq!(
init_distro_help.as_deref(),
Some(
"Distro source to install. Required unless an embedding launcher sets \
`ASTRID_ENFORCED_DISTRO` (`@owner/repo`, URL, local Distro.toml, or .shuttle)"
)
);
assert_eq!(
apply_name_help.as_deref(),
Some("Distro source (`@owner/repo`, URL, local Distro.toml, or .shuttle)")
);
}

#[test]
fn init_parses_target_separately_from_operator() {
let cli = Cli::try_parse_from([
"astrid",
"--principal",
"operator-1",
"init",
"--distro",
"./Distro.toml",
"--target-principal",
"agent-1",
"--grant-capsules",
])
.expect("operator and target principal should parse independently");

assert_eq!(cli.principal.as_deref(), Some("operator-1"));
assert!(matches!(
cli.command,
Some(Commands::Init {
target_principal: Some(ref target),
grant_capsules: true,
..
}) if target == "agent-1"
));
}

#[test]
fn global_options_before_init_preserve_the_requested_distro() {
let cli = Cli::try_parse_from([
"astrid",
"--principal",
"operator-1",
"init",
"--distro",
"@example/other",
])
.expect("global options before init should parse");

assert_eq!(cli.principal.as_deref(), Some("operator-1"));
assert!(matches!(
cli.command,
Some(Commands::Init {
distro: Some(ref distro),
..
}) if distro == "@example/other"
));
}
Loading
Loading