use clap::builder::styling::{Style, Styles};
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
const STYLES: Styles = Styles::plain().header(Style::new().bold());
#[derive(Parser, Debug)]
#[command(name = "nono")]
#[command(author, version, about, long_about = None)]
#[command(styles = STYLES, next_help_heading = "OPTIONS")]
#[command(subcommand_help_heading = "")]
#[command(help_template = "\
{about-with-newline}
\x1b[1mUSAGE\x1b[0m
nono <command> [flags]
\x1b[1mGETTING STARTED\x1b[0m
setup Set up nono on this system
\x1b[1mCORE USAGE\x1b[0m
run Run a command inside the sandbox
shell Start an interactive shell inside the sandbox
wrap Apply sandbox and exec into command (nono disappears)
\x1b[1mEXPLORATION & DEBUGGING\x1b[0m
learn [deprecated] Use `nono run` to learn from sandbox denials
why Check why a path or network operation would be allowed or denied
\x1b[1mSESSION MANAGEMENT\x1b[0m
ps List running or detached sandbox sessions
stop Stop a running sandbox session
detach Detach from an interactive runtime session
attach Attach to a detached runtime session
logs View runtime session event logs
inspect Show detailed runtime session state
session Manage runtime session storage
rollback Manage rollback sessions (browse, restore, cleanup)
audit View audit trail of sandboxed commands
trust Manage file trust and attestation
\x1b[1mPACKS\x1b[0m
pull Install a signed nono pack from the registry
remove Remove an installed nono pack
update Update installed nono packs
outdated Show which installed packs have newer versions available
pin Pin a pack to its current version
unpin Unpin a pack to re-include it in updates
search Search the registry for nono packs
list List installed nono packs
\x1b[1mPOLICY & PROFILES\x1b[0m
policy [deprecated] Use 'nono profile' instead
profile Create, inspect, and compare nono profiles
\x1b[1mSHELL\x1b[0m
completion Generate shell completion scripts
\x1b[1mOPTIONS\x1b[0m
{options}
\x1b[1mLEARN MORE\x1b[0m
Use `nono <command> --help` for more information about a command.
Read the docs at https://nono.sh/docs
")]
pub struct Cli {
#[arg(long, short = 's', global = true, help_heading = "OPTIONS")]
pub silent: bool,
#[arg(
long,
global = true,
env = "NONO_THEME",
value_name = "THEME",
help_heading = "OPTIONS"
)]
pub theme: Option<String>,
#[arg(
long,
global = true,
env = "NONO_LOG_FILE",
value_name = "PATH",
help_heading = "OPTIONS"
)]
pub log_file: Option<PathBuf>,
#[command(subcommand)]
pub command: Commands,
}
#[derive(Subcommand, Debug)]
pub enum Commands {
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono setup [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono setup --profiles # Full setup with profile generation
nono setup --check-only # Verify installation and sandbox support
nono setup --profiles --shell-integration # Setup with shell integration help
nono setup -v --profiles # Verbose setup
")]
Setup(SetupArgs),
#[command(trailing_var_arg = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono run [flags] <program>...
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono run --allow . claude # Read/write current dir, run claude
nono run --profile always-further/claude claude # Use a profile
nono run --profile always-further/claude --allow-domain api.openai.com claude
# Restrict outbound access to listed domains
nono run --read ./src --write ./output cargo build
# Separate read/write permissions
nono run --allow . --block-net cargo build # Block network access
nono run --allow . --env-credential openai_api_key,anthropic_api_key -- claude
# Load secrets from system keystore
")]
Run(Box<RunArgs>),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono shell [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono shell --allow . # Shell with read/write to current dir
nono shell --profile always-further/claude # Use a named profile
nono shell --allow . --shell /bin/zsh # Override shell binary
")]
Shell(Box<ShellArgs>),
#[command(trailing_var_arg = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono wrap [flags] <program>...
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono wrap --allow . -- cargo build # Sandbox and exec into cargo build
nono wrap --profile rust-dev -- cargo test # Use a named profile
")]
Wrap(Box<WrapArgs>),
#[command(trailing_var_arg = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono learn [flags] <program>...
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mDEPRECATED\x1b[0m
Use `nono run --profile <name> -- <command>` instead. `nono run` keeps the
command sandboxed, reports denials, and offers to save profile updates.
\x1b[1mEXAMPLES\x1b[0m
nono run --profile my-profile -- my-app # Preferred learning workflow
nono learn --profile my-profile -- my-app # Deprecated compatibility path
nono learn --json -- node server.js # Output as JSON for profile
nono learn --timeout 30 -- my-app # Limit trace duration
\x1b[1mPLATFORM NOTES\x1b[0m
Linux Uses strace (install with: apt install strace)
macOS Prefer: nono run --profile <name> -- <command>
Legacy unsandboxed fs_usage/nettop tracing: nono learn --trace -- <command>
")]
Learn(Box<LearnArgs>),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono why [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono why --path ~/.ssh --op read # Check if ~/.ssh is readable
nono why --path ./src --op write --allow . # Check with capability context
nono why --json --path ~/.aws --op read # JSON output for agents
nono why --host api.openai.com --port 443 # Query network access
nono why --self --path /tmp --op write # Inside sandbox, query own capabilities
")]
Why(Box<WhyArgs>),
#[command(subcommand_help_heading = "COMMANDS", disable_help_subcommand = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono rollback <command>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono rollback list # List rollback sessions
nono rollback show <id> --diff # Show changes with diff
nono rollback restore <id> # Restore files from a session
nono rollback restore <id> --dry-run # Preview what would change
nono rollback verify <id> # Verify session integrity
nono rollback cleanup --dry-run # Preview cleanup
")]
Rollback(RollbackArgs),
#[command(subcommand_help_heading = "COMMANDS", disable_help_subcommand = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono audit <command>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono audit list # List all sessions
nono audit list --today # List sessions from today
nono audit list --command claude # Filter by command
nono audit show <id> --json # Export as JSON
")]
Audit(AuditArgs),
#[command(subcommand_help_heading = "COMMANDS", disable_help_subcommand = true)]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono trust <command>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono trust sign SKILLS.md # Sign with default keystore key
nono trust sign SKILLS.md --key my-key # Sign with a specific key ID
nono trust sign-policy # Sign project trust policy
nono trust sign-policy --user # Sign user-level trust policy
nono trust verify SKILLS.md # Verify a file
nono trust verify --all # Verify all files matching policy
nono trust list # List files and verification status
nono trust keygen # Generate a new signing key pair
")]
Trust(TrustArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono ps [flags]
{all-args}
{after-help}")]
#[command(after_help = "EXAMPLES:
# Show running sessions
nono ps
# Show all sessions (including exited)
nono ps --all
# JSON output
nono ps --json
")]
Ps(PsArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono stop [flags] <session>
{all-args}
{after-help}")]
#[command(after_help = "EXAMPLES:
# Stop a session by ID (prefix match)
nono stop a3f7c2
# Force stop (SIGKILL)
nono stop --force a3f7c2
")]
Stop(StopArgs),
#[command(
help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono detach <session>
{all-args}
{after-help}",
alias = "pause",
after_help = "EXAMPLES:
# Detach by session ID
nono detach a3f7c2
# Detach by name
nono detach calm-gate
IN-BAND DETACH:
By default, press Ctrl-] then d to detach without opening a second terminal.
This can be changed in ~/.config/nono/config.toml:
[ui]
detach_sequence = \"ctrl-] d\"
"
)]
Detach(DetachArgs),
#[command(
help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono attach <session>
{all-args}
{after-help}",
alias = "resume",
after_help = "EXAMPLES:
# Attach by session ID
nono attach a3f7c2
# Attach by name
nono attach calm-gate
IN-BAND DETACH:
By default, press Ctrl-] then d to detach from the session.
This can be changed in ~/.config/nono/config.toml:
[ui]
detach_sequence = \"ctrl-] d\"
"
)]
Attach(AttachArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono logs [flags] <session>
{all-args}
{after-help}")]
#[command(after_help = "EXAMPLES:
# View recent events
nono logs a3f7c2
# Follow events in real-time
nono logs -f a3f7c2
# Show last 20 events
nono logs --tail 20 a3f7c2
# JSON output
nono logs --json a3f7c2
")]
Logs(LogsArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono inspect [flags] <session>
{all-args}
{after-help}")]
#[command(after_help = "EXAMPLES:
# Inspect a session
nono inspect a3f7c2
# Include event log
nono inspect --events a3f7c2
# JSON output
nono inspect --json a3f7c2
")]
Inspect(InspectArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono prune [flags]
{all-args}
{after-help}")]
#[command(after_help = "EXAMPLES:
# Preview what would be cleaned
nono prune --dry-run
# Remove sessions older than 7 days
nono prune --older-than 7
# Keep only 10 most recent sessions
nono prune --keep 10
")]
#[command(hide = true)]
Prune(PruneArgs),
#[command(subcommand_help_heading = "COMMANDS")]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono session <command>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono session cleanup --dry-run # Preview old runtime sessions
nono session cleanup --older-than 7 # Remove sessions older than 7 days
nono session cleanup --keep 10 # Keep only 10 recent sessions
")]
Session(SessionArgs),
#[command(subcommand_help_heading = "COMMANDS")]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono policy <command>
\x1b[1mNOTE\x1b[0m
These commands are deprecated. Use the corresponding 'nono profile'
form; every invocation of 'nono policy <sub>' prints a deprecation
warning to stderr.
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono policy groups # deprecated -> use 'nono profile groups'
nono policy profiles # deprecated -> use 'nono profile list'
nono policy show <name> # deprecated -> use 'nono profile show <name>'
nono policy diff a b # deprecated -> use 'nono profile diff a b'
nono policy validate <f> # deprecated -> use 'nono profile validate <f>'
")]
Policy(crate::deprecated_policy::PolicyArgs),
#[command(subcommand_help_heading = "COMMANDS")]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono profile <command>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono profile init my-agent # Create a new profile with defaults
nono profile init my-agent --extends default --groups deny_credentials
# Extend an existing profile
nono profile init my-agent --full # Generate a full skeleton
nono profile list # List all profiles (built-in and user)
nono profile show claude-code # Show a fully resolved profile
nono profile diff default claude-code # Compare two profiles
nono profile validate my-agent # Validate a profile by name
nono profile validate ~/my-profile.json # Validate a profile file
nono profile validate --draft my-profile # Validate a profile draft
nono profile promote my-profile # Review and apply a profile draft
nono profile groups # List all policy groups
nono profile groups deny_credentials # Show details for a specific group
nono profile schema # Print JSON Schema for editor validation
nono profile guide # Print profile authoring guide
")]
Profile(ProfileCmdArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono pull <namespace>/<name>[@<version>] [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono pull always-further/claude
nono pull always-further/claude@1.2.0 --registry http://localhost:3000
nono pull always-further/claude --init
")]
Pull(PullArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono remove <namespace>/<name>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono remove always-further/claude
")]
Remove(RemoveArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono update [<namespace>/<name>] [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono update
nono update always-further/claude
nono update --dry-run
nono update --force # also update pinned packs
")]
Update(UpdateArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono search <query> [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono search claude
nono search sandbox --json
")]
Search(SearchArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono list --installed [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono list --installed
nono list --installed --json
")]
List(ListArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono pin <namespace>/<name>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono pin always-further/claude
")]
Pin(PinArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono unpin <namespace>/<name>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono unpin always-further/claude
")]
Unpin(UnpinArgs),
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono outdated [flags]
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono outdated
nono outdated --json
")]
Outdated(OutdatedArgs),
#[command(name = "completion")]
#[command(help_template = "\
{about}
\x1b[1mUSAGE\x1b[0m
nono completion <shell>
{all-args}
{after-help}")]
#[command(after_help = "\x1b[1mEXAMPLES\x1b[0m
nono completion bash >> ~/.bashrc
nono completion zsh > ~/.zfunc/_nono
nono completion fish > ~/.config/fish/completions/nono.fish
nono completion powershell >> $PROFILE
")]
Completions(CompletionsArgs),
#[command(hide = true)]
OpenUrlHelper(OpenUrlHelperArgs),
#[command(hide = true)]
PackUpdateHintHelper(PackUpdateHintHelperArgs),
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct PullArgs {
pub package_ref: String,
#[arg(
long,
env = "NONO_REGISTRY",
value_name = "URL",
help_heading = "OPTIONS"
)]
pub registry: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub force: bool,
#[arg(long, help_heading = "OPTIONS")]
pub init: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RemoveArgs {
pub package_ref: String,
#[arg(long, default_value_t = false)]
pub force: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct UpdateArgs {
pub package_ref: Option<String>,
#[arg(
long,
env = "NONO_REGISTRY",
value_name = "URL",
help_heading = "OPTIONS"
)]
pub registry: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub dry_run: bool,
#[arg(long, help_heading = "OPTIONS")]
pub force: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct SearchArgs {
pub query: String,
#[arg(
long,
env = "NONO_REGISTRY",
value_name = "URL",
help_heading = "OPTIONS"
)]
pub registry: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct ListArgs {
#[arg(long, help_heading = "OPTIONS")]
pub installed: bool,
#[arg(long, help_heading = "OPTIONS")]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct PinArgs {
pub package_ref: String,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct UnpinArgs {
pub package_ref: String,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct OutdatedArgs {
#[arg(
long,
env = "NONO_REGISTRY",
value_name = "URL",
help_heading = "OPTIONS"
)]
pub registry: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug, Clone)]
pub struct OpenUrlHelperArgs {
pub url: String,
}
#[derive(Parser, Debug, Clone)]
pub struct PackUpdateHintHelperArgs {
pub packs: Vec<String>,
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum CompletionShell {
Bash,
Zsh,
Fish,
#[value(name = "powershell")]
PowerShell,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct CompletionsArgs {
pub shell: CompletionShell,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct ProfileCmdArgs {
#[command(subcommand)]
pub command: ProfileCommands,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Subcommand, Debug)]
pub enum ProfileCommands {
Init(ProfileInitArgs),
List(ProfileListArgs),
Show(ProfileShowArgs),
Diff(ProfileDiffArgs),
Validate(ProfileValidateArgs),
Promote(ProfilePromoteArgs),
Groups(ProfileGroupsArgs),
Schema(ProfileSchemaArgs),
Guide(ProfileGuideArgs),
}
#[derive(Parser, Debug)]
pub struct ProfileInitArgs {
pub name: String,
#[arg(long)]
pub extends: Option<String>,
#[arg(long, value_delimiter = ',')]
pub groups: Vec<String>,
#[arg(long)]
pub description: Option<String>,
#[arg(long)]
pub full: bool,
#[arg(long, short)]
pub output: Option<PathBuf>,
#[arg(long)]
pub force: bool,
}
#[derive(Parser, Debug)]
pub struct ProfileSchemaArgs {
#[arg(long, short)]
pub output: Option<PathBuf>,
}
#[derive(Parser, Debug)]
pub struct ProfileGuideArgs {}
#[derive(Parser, Debug)]
pub struct ProfileListArgs {
#[arg(long)]
pub json: bool,
}
#[derive(Parser, Debug)]
pub struct ProfileShowArgs {
pub profile: String,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub raw: bool,
#[arg(long, value_enum, value_name = "FORMAT")]
pub format: Option<ProfileShowFormat>,
}
#[derive(clap::ValueEnum, Clone, Debug)]
pub enum ProfileShowFormat {
Profile,
Manifest,
}
#[derive(Parser, Debug)]
pub struct ProfileDiffArgs {
pub profile1: String,
pub profile2: String,
#[arg(long)]
pub json: bool,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct ProfileValidateArgs {
pub file: PathBuf,
#[arg(long)]
pub draft: bool,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub strict: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct ProfilePromoteArgs {
pub name: String,
#[arg(long)]
pub diff: bool,
#[arg(long)]
pub yes: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
pub struct ProfileGroupsArgs {
pub name: Option<String>,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub all_platforms: bool,
}
#[derive(Parser, Debug, Clone, Default)]
pub struct SandboxArgs {
#[arg(
long,
short = 'a',
value_name = "DIR",
env = "NONO_ALLOW",
value_delimiter = ',',
help_heading = "FILESYSTEM"
)]
pub allow: Vec<PathBuf>,
#[arg(long, short = 'r', value_name = "DIR", help_heading = "FILESYSTEM")]
pub read: Vec<PathBuf>,
#[arg(long, short = 'w', value_name = "DIR", help_heading = "FILESYSTEM")]
pub write: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub allow_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub read_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub write_file: Vec<PathBuf>,
#[arg(long, value_name = "SOCKET", help_heading = "FILESYSTEM")]
pub allow_unix_socket: Vec<PathBuf>,
#[arg(long, value_name = "SOCKET", help_heading = "FILESYSTEM")]
pub allow_unix_socket_bind: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_dir: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_dir_bind: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_subtree: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_subtree_bind: Vec<PathBuf>,
#[arg(
long = "bypass-protection",
alias = "override-deny",
value_name = "PATH",
help_heading = "FILESYSTEM"
)]
pub bypass_protection: Vec<PathBuf>,
#[arg(
long = "suppress-save-prompt",
alias = "ignore-denied",
value_name = "PATH",
help_heading = "FILESYSTEM"
)]
pub suppress_save_prompt: Vec<PathBuf>,
#[arg(long, help_heading = "FILESYSTEM")]
pub allow_cwd: bool,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub workdir: Option<PathBuf>,
#[arg(
long = "block-net",
alias = "net-block",
conflicts_with = "allow_net",
env = "NONO_BLOCK_NET",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
help_heading = "NETWORK"
)]
pub block_net: bool,
#[arg(
long = "allow-net",
alias = "net-allow",
env = "NONO_ALLOW_NET",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
conflicts_with_all = [
"block_net",
"network_profile",
"allow_proxy",
"proxy_credential",
"external_proxy",
"external_proxy_bypass",
"proxy_port"
],
hide = true,
help_heading = "NETWORK"
)]
pub allow_net: bool,
#[arg(
long,
value_name = "PROFILE",
env = "NONO_NETWORK_PROFILE",
help_heading = "NETWORK"
)]
pub network_profile: Option<String>,
#[arg(
long = "allow-domain",
alias = "allow-proxy",
alias = "proxy-allow",
env = "NONO_ALLOW_DOMAIN",
value_name = "DOMAIN_OR_URL",
help_heading = "NETWORK"
)]
pub allow_proxy: Vec<String>,
#[arg(
long = "listen-port",
alias = "allow-bind",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_bind: Vec<u16>,
#[arg(
long = "open-port",
alias = "allow-port",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_port: Vec<u16>,
#[arg(
long = "allow-connect-port",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_connect_port: Vec<u16>,
#[arg(
long = "upstream-proxy",
alias = "external-proxy",
value_name = "HOST:PORT",
env = "NONO_UPSTREAM_PROXY",
help_heading = "NETWORK"
)]
pub external_proxy: Option<String>,
#[arg(
long = "upstream-bypass",
alias = "external-proxy-bypass",
value_name = "DOMAIN",
env = "NONO_UPSTREAM_BYPASS",
value_delimiter = ',',
help_heading = "NETWORK"
)]
pub external_proxy_bypass: Vec<String>,
#[arg(long, value_name = "PORT", help_heading = "NETWORK")]
pub proxy_port: Option<u16>,
#[cfg(target_os = "macos")]
#[arg(
long,
env = "NONO_TRUST_PROXY_CA",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
help_heading = "NETWORK"
)]
pub trust_proxy_ca: bool,
#[arg(
long,
value_name = "DAYS",
env = "NONO_PROXY_CA_VALIDITY",
value_parser = clap::value_parser!(u32).range(1..=365),
help_heading = "NETWORK"
)]
pub proxy_ca_validity: Option<u32>,
#[arg(
long = "credential",
alias = "proxy-credential",
env = "NONO_CREDENTIAL",
value_name = "SERVICE",
help_heading = "CREDENTIALS"
)]
pub proxy_credential: Vec<String>,
#[arg(
long = "allow-endpoint",
value_name = "SERVICE:METHOD:PATH",
help_heading = "CREDENTIALS"
)]
pub allow_endpoint: Vec<String>,
#[arg(
long,
value_name = "CREDENTIALS",
env = "NONO_ENV_CREDENTIAL",
help_heading = "CREDENTIALS"
)]
pub env_credential: Option<String>,
#[arg(
long,
value_names = ["CREDENTIAL_REF", "ENV_VAR"],
num_args = 2,
action = clap::ArgAction::Append,
help_heading = "CREDENTIALS"
)]
pub env_credential_map: Vec<String>,
#[arg(long, value_name = "CMD", help_heading = "COMMANDS")]
pub allow_command: Vec<String>,
#[arg(long, value_name = "CMD", help_heading = "COMMANDS")]
pub block_command: Vec<String>,
#[arg(
long,
short = 'p',
value_name = "NAME_OR_PATH",
env = "NONO_PROFILE",
help_heading = "OPTIONS"
)]
pub profile: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub allow_launch_services: bool,
#[arg(long, help_heading = "OPTIONS")]
pub allow_gpu: bool,
#[arg(
long,
short = 'c',
value_name = "FILE",
conflicts_with_all = &[
"allow", "read", "write", "allow_file", "read_file", "write_file",
"allow_unix_socket", "allow_unix_socket_bind",
"allow_unix_socket_dir", "allow_unix_socket_dir_bind",
"allow_unix_socket_subtree", "allow_unix_socket_subtree_bind",
"profile", "bypass_protection", "suppress_save_prompt", "allow_cwd",
"block_net", "allow_net", "network_profile", "allow_proxy",
"allow_bind", "allow_port", "allow_connect_port", "external_proxy", "proxy_port",
"proxy_credential", "allow_endpoint", "env_credential", "env_credential_map",
"allow_command", "block_command", "allow_launch_services", "allow_gpu",
],
help_heading = "OPTIONS"
)]
pub config: Option<PathBuf>,
#[arg(long, short = 'v', action = clap::ArgAction::Count, help_heading = "OPTIONS")]
pub verbose: u8,
#[arg(long, help_heading = "OPTIONS")]
pub dry_run: bool,
}
impl SandboxArgs {
pub fn has_proxy_flags(&self) -> bool {
self.network_profile.is_some()
|| !self.allow_proxy.is_empty()
|| !self.proxy_credential.is_empty()
|| self.external_proxy.is_some()
}
}
#[derive(Parser, Debug, Clone, Default)]
pub struct WrapSandboxArgs {
#[arg(
long,
short = 'a',
value_name = "DIR",
env = "NONO_ALLOW",
value_delimiter = ',',
help_heading = "FILESYSTEM"
)]
pub allow: Vec<PathBuf>,
#[arg(long, short = 'r', value_name = "DIR", help_heading = "FILESYSTEM")]
pub read: Vec<PathBuf>,
#[arg(long, short = 'w', value_name = "DIR", help_heading = "FILESYSTEM")]
pub write: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub allow_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub read_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "FILESYSTEM")]
pub write_file: Vec<PathBuf>,
#[arg(long, value_name = "SOCKET", help_heading = "FILESYSTEM")]
pub allow_unix_socket: Vec<PathBuf>,
#[arg(long, value_name = "SOCKET", help_heading = "FILESYSTEM")]
pub allow_unix_socket_bind: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_dir: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_dir_bind: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_subtree: Vec<PathBuf>,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub allow_unix_socket_subtree_bind: Vec<PathBuf>,
#[arg(
long = "bypass-protection",
alias = "override-deny",
value_name = "PATH",
help_heading = "FILESYSTEM"
)]
pub bypass_protection: Vec<PathBuf>,
#[arg(
long = "suppress-save-prompt",
alias = "ignore-denied",
value_name = "PATH",
help_heading = "FILESYSTEM"
)]
pub suppress_save_prompt: Vec<PathBuf>,
#[arg(long, help_heading = "FILESYSTEM")]
pub allow_cwd: bool,
#[arg(long, value_name = "DIR", help_heading = "FILESYSTEM")]
pub workdir: Option<PathBuf>,
#[arg(
long = "block-net",
alias = "net-block",
env = "NONO_BLOCK_NET",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
help_heading = "NETWORK"
)]
pub block_net: bool,
#[arg(
long = "listen-port",
alias = "allow-bind",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_bind: Vec<u16>,
#[arg(
long = "open-port",
alias = "allow-port",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_port: Vec<u16>,
#[arg(
long = "allow-connect-port",
value_name = "PORT",
help_heading = "NETWORK"
)]
pub allow_connect_port: Vec<u16>,
#[arg(
long,
value_name = "CREDENTIALS",
env = "NONO_ENV_CREDENTIAL",
help_heading = "CREDENTIALS"
)]
pub env_credential: Option<String>,
#[arg(
long,
value_names = ["CREDENTIAL_REF", "ENV_VAR"],
num_args = 2,
action = clap::ArgAction::Append,
help_heading = "CREDENTIALS"
)]
pub env_credential_map: Vec<String>,
#[arg(long, value_name = "CMD", help_heading = "COMMANDS")]
pub allow_command: Vec<String>,
#[arg(long, value_name = "CMD", help_heading = "COMMANDS")]
pub block_command: Vec<String>,
#[arg(
long,
short = 'p',
value_name = "NAME_OR_PATH",
env = "NONO_PROFILE",
help_heading = "OPTIONS"
)]
pub profile: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub allow_launch_services: bool,
#[arg(long, help_heading = "OPTIONS")]
pub allow_gpu: bool,
#[arg(
long,
short = 'c',
value_name = "FILE",
conflicts_with_all = &[
"allow", "read", "write", "allow_file", "read_file", "write_file",
"allow_unix_socket", "allow_unix_socket_bind",
"allow_unix_socket_dir", "allow_unix_socket_dir_bind",
"allow_unix_socket_subtree", "allow_unix_socket_subtree_bind",
"profile", "bypass_protection", "suppress_save_prompt", "allow_cwd",
"block_net", "allow_bind", "allow_port", "allow_connect_port",
"env_credential", "env_credential_map",
"allow_command", "block_command", "allow_launch_services", "allow_gpu",
],
help_heading = "OPTIONS"
)]
pub config: Option<PathBuf>,
#[arg(long, short = 'v', action = clap::ArgAction::Count, help_heading = "OPTIONS")]
pub verbose: u8,
#[arg(long, help_heading = "OPTIONS")]
pub dry_run: bool,
}
impl From<WrapSandboxArgs> for SandboxArgs {
fn from(args: WrapSandboxArgs) -> Self {
Self {
allow: args.allow,
read: args.read,
write: args.write,
allow_file: args.allow_file,
read_file: args.read_file,
write_file: args.write_file,
allow_unix_socket: args.allow_unix_socket,
allow_unix_socket_bind: args.allow_unix_socket_bind,
allow_unix_socket_dir: args.allow_unix_socket_dir,
allow_unix_socket_dir_bind: args.allow_unix_socket_dir_bind,
allow_unix_socket_subtree: args.allow_unix_socket_subtree,
allow_unix_socket_subtree_bind: args.allow_unix_socket_subtree_bind,
bypass_protection: args.bypass_protection,
suppress_save_prompt: args.suppress_save_prompt,
allow_cwd: args.allow_cwd,
workdir: args.workdir,
block_net: args.block_net,
allow_net: false,
network_profile: None,
allow_proxy: Vec::new(),
allow_bind: args.allow_bind,
allow_port: args.allow_port,
allow_connect_port: args.allow_connect_port,
external_proxy: None,
external_proxy_bypass: Vec::new(),
proxy_port: None,
#[cfg(target_os = "macos")]
trust_proxy_ca: false,
proxy_ca_validity: None,
proxy_credential: Vec::new(),
allow_endpoint: Vec::new(),
env_credential: args.env_credential,
env_credential_map: args.env_credential_map,
allow_command: args.allow_command,
block_command: args.block_command,
profile: args.profile,
allow_launch_services: args.allow_launch_services,
allow_gpu: args.allow_gpu,
config: args.config,
verbose: args.verbose,
dry_run: args.dry_run,
}
}
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RunArgs {
#[command(flatten)]
pub sandbox: SandboxArgs,
#[arg(long, help_heading = "OPTIONS")]
pub detached: bool,
#[arg(
long = "detach-timeout",
value_name = "SECS",
requires = "detached",
env = "NONO_DETACH_STARTUP_TIMEOUT",
help_heading = "OPTIONS"
)]
pub detach_timeout_secs: Option<u64>,
#[arg(long, conflicts_with = "no_rollback", help_heading = "ROLLBACK")]
pub rollback: bool,
#[arg(long, help_heading = "ROLLBACK")]
pub no_rollback_prompt: bool,
#[arg(long, conflicts_with = "rollback", help_heading = "ROLLBACK")]
pub no_rollback: bool,
#[arg(long, value_name = "PATTERN", help_heading = "ROLLBACK")]
pub rollback_exclude: Vec<String>,
#[arg(long, value_name = "DIR_NAME", help_heading = "ROLLBACK")]
pub rollback_include: Vec<String>,
#[arg(long, conflicts_with = "rollback_include", help_heading = "ROLLBACK")]
pub rollback_all: bool,
#[arg(long, value_name = "DIR_NAME", help_heading = "OPTIONS")]
pub skip_dir: Vec<String>,
#[arg(
long,
value_name = "PATH",
requires = "rollback",
help_heading = "ROLLBACK"
)]
pub rollback_dest: Option<std::path::PathBuf>,
#[arg(long, help_heading = "OPTIONS")]
pub no_diagnostics: bool,
#[arg(
long = "startup-timeout",
value_name = "SECS",
env = "NONO_STARTUP_TIMEOUT",
help_heading = "OPTIONS"
)]
pub startup_timeout_secs: Option<u64>,
#[arg(
long,
conflicts_with_all = ["audit_integrity", "no_audit_integrity", "rollback"],
help_heading = "OPTIONS"
)]
pub no_audit: bool,
#[arg(long, conflicts_with_all = ["audit_integrity", "rollback"], help_heading = "OPTIONS")]
pub no_audit_integrity: bool,
#[arg(long, help_heading = "OPTIONS")]
pub audit_integrity: bool,
#[arg(
long,
value_name = "SECRET_REF",
conflicts_with_all = ["no_audit", "no_audit_integrity"],
help_heading = "OPTIONS"
)]
pub audit_sign_key: Option<String>,
#[arg(
long,
env = "NONO_TRUST_OVERRIDE",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
help_heading = "OPTIONS"
)]
pub trust_override: bool,
#[arg(long, value_name = "NAME", help_heading = "OPTIONS")]
pub name: Option<String>,
#[arg(
long,
env = "NONO_CAPABILITY_ELEVATION",
value_parser = clap::builder::BoolishValueParser::new(),
action = clap::ArgAction::SetTrue,
help_heading = "OPTIONS"
)]
pub capability_elevation: bool,
#[arg(hide = true)]
pub command: Vec<String>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct ShellArgs {
#[command(flatten)]
pub sandbox: SandboxArgs,
#[arg(long, value_name = "SHELL", help_heading = "OPTIONS")]
pub shell: Option<PathBuf>,
#[arg(long, value_name = "NAME", help_heading = "OPTIONS")]
pub name: Option<String>,
#[arg(
long = "startup-timeout",
value_name = "SECS",
env = "NONO_STARTUP_TIMEOUT",
help_heading = "OPTIONS"
)]
pub startup_timeout_secs: Option<u64>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct WrapArgs {
#[command(flatten)]
pub sandbox: WrapSandboxArgs,
#[arg(long, help_heading = "OPTIONS")]
pub no_diagnostics: bool,
#[arg(required = true, hide = true)]
pub command: Vec<String>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct SetupArgs {
#[arg(long, help_heading = "OPTIONS")]
pub check_only: bool,
#[arg(long, help_heading = "OPTIONS")]
pub profiles: bool,
#[arg(long, help_heading = "OPTIONS")]
pub shell_integration: bool,
#[arg(short, long, action = clap::ArgAction::Count, help_heading = "OPTIONS")]
pub verbose: u8,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct WhyArgs {
#[arg(long, help_heading = "QUERY")]
pub path: Option<PathBuf>,
#[arg(long, value_enum, help_heading = "QUERY")]
pub op: Option<WhyOp>,
#[arg(long, help_heading = "QUERY")]
pub host: Option<String>,
#[arg(long, value_enum, value_name = "SCOPE", help_heading = "QUERY")]
pub scope: Option<WhyScope>,
#[arg(long, default_value = "443", help_heading = "QUERY")]
pub port: u16,
#[arg(long, help_heading = "OPTIONS")]
pub json: bool,
#[arg(long = "self", help_heading = "OPTIONS")]
pub self_query: bool,
#[arg(long, short = 'a', value_name = "DIR", help_heading = "CONTEXT")]
pub allow: Vec<PathBuf>,
#[arg(long, short = 'r', value_name = "DIR", help_heading = "CONTEXT")]
pub read: Vec<PathBuf>,
#[arg(long, short = 'w', value_name = "DIR", help_heading = "CONTEXT")]
pub write: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "CONTEXT")]
pub allow_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "CONTEXT")]
pub read_file: Vec<PathBuf>,
#[arg(long, value_name = "FILE", help_heading = "CONTEXT")]
pub write_file: Vec<PathBuf>,
#[arg(long = "block-net", alias = "net-block", help_heading = "CONTEXT")]
pub block_net: bool,
#[arg(long, short = 'p', value_name = "NAME", help_heading = "CONTEXT")]
pub profile: Option<String>,
#[arg(long, value_name = "DIR", help_heading = "CONTEXT")]
pub workdir: Option<PathBuf>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct LearnArgs {
#[arg(long, short = 'p', value_name = "NAME", help_heading = "OPTIONS")]
pub profile: Option<String>,
#[arg(long, help_heading = "OPTIONS")]
pub json: bool,
#[arg(long, value_name = "SECS", help_heading = "OPTIONS")]
pub timeout: Option<u64>,
#[arg(long, help_heading = "OPTIONS")]
pub all: bool,
#[arg(long, help_heading = "OPTIONS")]
pub no_rdns: bool,
#[arg(long, help_heading = "OPTIONS")]
pub trace: bool,
#[arg(long, short = 'v', action = clap::ArgAction::Count, help_heading = "OPTIONS")]
pub verbose: u8,
#[arg(required = true, hide = true)]
pub command: Vec<String>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Clone, Debug, ValueEnum)]
pub enum WhyOp {
Read,
Write,
#[value(name = "readwrite")]
ReadWrite,
}
#[derive(Clone, Debug, ValueEnum)]
pub enum WhyScope {
Signal,
#[value(name = "abstract-unix-socket")]
AbstractUnixSocket,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackArgs {
#[command(subcommand)]
pub command: RollbackCommands,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Subcommand, Debug)]
pub enum RollbackCommands {
List(RollbackListArgs),
Show(RollbackShowArgs),
Restore(RollbackRestoreArgs),
Verify(RollbackVerifyArgs),
Cleanup(RollbackCleanupArgs),
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackListArgs {
#[arg(long, value_name = "N")]
pub recent: Option<usize>,
#[arg(long, value_name = "PATH")]
pub path: Option<PathBuf>,
#[arg(long)]
pub all: bool,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackShowArgs {
pub session_id: String,
#[arg(long)]
pub diff: bool,
#[arg(long)]
pub side_by_side: bool,
#[arg(long)]
pub full: bool,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackRestoreArgs {
pub session_id: String,
#[arg(long)]
pub snapshot: Option<u32>,
#[arg(long)]
pub dry_run: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackVerifyArgs {
pub session_id: String,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct RollbackCleanupArgs {
#[arg(long, value_name = "N")]
pub keep: Option<usize>,
#[arg(long, value_name = "DAYS")]
pub older_than: Option<u64>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub all: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct AuditArgs {
#[command(subcommand)]
pub command: AuditCommands,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Subcommand, Debug)]
pub enum AuditCommands {
List(AuditListArgs),
Show(AuditShowArgs),
Verify(AuditVerifyArgs),
Cleanup(AuditCleanupArgs),
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct AuditListArgs {
#[arg(long)]
pub today: bool,
#[arg(long, value_name = "DATE")]
pub since: Option<String>,
#[arg(long, value_name = "DATE")]
pub until: Option<String>,
#[arg(long, value_name = "CMD")]
pub command: Option<String>,
#[arg(long, value_name = "PATH")]
pub path: Option<PathBuf>,
#[arg(long, value_name = "N")]
pub recent: Option<usize>,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct AuditShowArgs {
pub session_id: String,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct AuditVerifyArgs {
pub session_id: String,
#[arg(long, value_name = "FILE")]
pub public_key_file: Option<PathBuf>,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct AuditCleanupArgs {
#[arg(long, value_name = "N")]
pub keep: Option<usize>,
#[arg(long, value_name = "DAYS")]
pub older_than: Option<u64>,
#[arg(long)]
pub dry_run: bool,
#[arg(long)]
pub all: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct SessionArgs {
#[command(subcommand)]
pub command: SessionCommands,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Subcommand, Debug)]
pub enum SessionCommands {
Cleanup(PruneArgs),
}
#[derive(Parser, Debug)]
pub struct PsArgs {
#[arg(long)]
pub json: bool,
#[arg(long)]
pub all: bool,
}
#[derive(Parser, Debug)]
pub struct StopArgs {
pub session: String,
#[arg(long)]
pub force: bool,
#[arg(long, default_value = "10")]
pub timeout: u64,
}
#[derive(Parser, Debug)]
pub struct DetachArgs {
pub session: String,
}
#[derive(Parser, Debug)]
pub struct AttachArgs {
pub session: String,
}
#[derive(Parser, Debug)]
pub struct LogsArgs {
pub session: String,
#[arg(long, short = 'f')]
pub follow: bool,
#[arg(long, value_name = "N")]
pub tail: Option<usize>,
#[arg(long)]
pub json: bool,
}
#[derive(Parser, Debug)]
pub struct InspectArgs {
pub session: String,
#[arg(long)]
pub json: bool,
#[arg(long)]
pub events: bool,
#[arg(long)]
pub changes: bool,
}
#[derive(Parser, Debug)]
pub struct PruneArgs {
#[arg(long)]
pub dry_run: bool,
#[arg(long, value_name = "DAYS")]
pub older_than: Option<u64>,
#[arg(long, value_name = "N")]
pub keep: Option<usize>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustArgs {
#[command(subcommand)]
pub command: TrustCommands,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Subcommand, Debug)]
pub enum TrustCommands {
Init(TrustInitArgs),
Sign(TrustSignArgs),
SignPolicy(TrustSignPolicyArgs),
Verify(TrustVerifyArgs),
List(TrustListArgs),
Keygen(TrustKeygenArgs),
ExportKey(TrustExportKeyArgs),
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustSignArgs {
#[arg(required_unless_present = "all")]
pub files: Vec<PathBuf>,
#[arg(long)]
pub all: bool,
#[arg(long, value_name = "KEY_ID", conflicts_with_all = ["keyless", "keyref"])]
pub key: Option<String>,
#[arg(long, value_name = "URI", conflicts_with_all = ["key", "keyless"])]
pub keyref: Option<String>,
#[arg(long, conflicts_with = "keyref")]
pub keyless: bool,
#[arg(long)]
pub multi_subject: bool,
#[arg(long, value_name = "FILE")]
pub policy: Option<PathBuf>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustSignPolicyArgs {
#[arg(conflicts_with = "user")]
pub file: Option<PathBuf>,
#[arg(long, value_name = "KEY_ID", conflicts_with = "keyref")]
pub key: Option<String>,
#[arg(long, value_name = "URI", conflicts_with = "key")]
pub keyref: Option<String>,
#[arg(long)]
pub user: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustVerifyArgs {
#[arg(required_unless_present = "all")]
pub files: Vec<PathBuf>,
#[arg(long)]
pub all: bool,
#[arg(long, value_name = "FILE")]
pub policy: Option<PathBuf>,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustInitArgs {
#[arg(long, value_name = "PATTERN", num_args = 1..)]
pub include: Vec<String>,
#[arg(long, value_name = "KEY_ID", conflicts_with = "keyref")]
pub key: Option<String>,
#[arg(long, value_name = "URI", conflicts_with = "key")]
pub keyref: Option<String>,
#[arg(long)]
pub user: bool,
#[arg(long)]
pub force: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustListArgs {
#[arg(long, value_name = "FILE")]
pub policy: Option<PathBuf>,
#[arg(long)]
pub json: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustKeygenArgs {
#[arg(
long,
value_name = "NAME",
default_value = "default",
conflicts_with = "keyref"
)]
pub id: String,
#[arg(long, value_name = "URI", conflicts_with = "id")]
pub keyref: Option<String>,
#[arg(long)]
pub force: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[derive(Parser, Debug)]
#[command(disable_help_flag = true)]
pub struct TrustExportKeyArgs {
#[arg(
long,
value_name = "NAME",
default_value = "default",
conflicts_with = "keyref"
)]
pub id: String,
#[arg(long, value_name = "URI", conflicts_with = "id")]
pub keyref: Option<String>,
#[arg(long)]
pub pem: bool,
#[arg(long, short = 'h', action = clap::ArgAction::Help, help_heading = "OPTIONS")]
pub help: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use clap::CommandFactory;
#[test]
fn test_run_basic() {
let cli = Cli::parse_from(["nono", "run", "--allow", ".", "echo", "hello"]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow.len(), 1);
assert_eq!(args.command, vec!["echo", "hello"]);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_run_with_separator() {
let cli = Cli::parse_from(["nono", "run", "--allow", ".", "--", "echo", "hello"]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow.len(), 1);
assert_eq!(args.command, vec!["echo", "hello"]);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_run_multiple_paths() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
"./src",
"--allow",
"./docs",
"--read",
"/usr/share",
"ls",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow.len(), 2);
assert_eq!(args.sandbox.read.len(), 1);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_wrap_basic() {
let cli = Cli::parse_from(["nono", "wrap", "--allow", ".", "--", "cargo", "build"]);
match cli.command {
Commands::Wrap(args) => {
assert_eq!(args.command, vec!["cargo", "build"]);
assert_eq!(args.sandbox.allow.len(), 1);
assert!(!args.no_diagnostics);
}
_ => panic!("Expected Wrap command"),
}
}
#[test]
fn test_wrap_supports_direct_network_flags_only() {
let cli = Cli::parse_from([
"nono",
"wrap",
"--block-net",
"--listen-port",
"3000",
"--open-port",
"5432",
"--allow",
".",
"--",
"cargo",
"build",
]);
match cli.command {
Commands::Wrap(args) => {
assert!(args.sandbox.block_net);
assert_eq!(args.sandbox.allow_bind, vec![3000]);
assert_eq!(args.sandbox.allow_port, vec![5432]);
}
_ => panic!("Expected Wrap command"),
}
}
#[test]
fn test_wrap_rejects_proxy_flags_at_parse_time() {
let result = Cli::try_parse_from([
"nono",
"wrap",
"--allow-domain",
"api.openai.com",
"--",
"echo",
]);
assert!(
result.is_err(),
"wrap should not accept proxy filtering flags"
);
}
#[test]
fn test_wrap_help_hides_proxy_flags() {
let mut cmd = Cli::command();
let wrap = cmd
.find_subcommand_mut("wrap")
.expect("wrap subcommand should exist");
let mut buf = Vec::new();
wrap.write_long_help(&mut buf)
.expect("failed to write wrap help");
let help = String::from_utf8(buf).expect("help is not utf-8");
assert!(help.contains("--block-net"));
assert!(help.contains("--listen-port"));
assert!(help.contains("--open-port"));
assert!(!help.contains("--allow-domain"));
assert!(!help.contains("--credential"));
assert!(!help.contains("--network-profile"));
assert!(!help.contains("--upstream-proxy"));
assert!(!help.contains("--upstream-bypass"));
assert!(!help.contains("--proxy-port"));
assert!(!help.contains("--allow-net"));
}
#[test]
fn test_shell_basic() {
let cli = Cli::parse_from(["nono", "shell", "--allow", "."]);
match cli.command {
Commands::Shell(args) => {
assert_eq!(args.sandbox.allow.len(), 1);
assert!(args.shell.is_none());
}
_ => panic!("Expected Shell command"),
}
}
#[test]
fn test_rollback_list() {
let cli = Cli::parse_from(["nono", "rollback", "list"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::List(list_args) => {
assert!(list_args.recent.is_none());
assert!(list_args.path.is_none());
assert!(!list_args.json);
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_list_recent_json() {
let cli = Cli::parse_from(["nono", "rollback", "list", "--recent", "5", "--json"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::List(list_args) => {
assert_eq!(list_args.recent, Some(5));
assert!(list_args.json);
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_show() {
let cli = Cli::parse_from(["nono", "rollback", "show", "20260214-143022-12345"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Show(show_args) => {
assert_eq!(show_args.session_id, "20260214-143022-12345");
assert!(!show_args.json);
}
_ => panic!("Expected Show subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_restore_defaults() {
let cli = Cli::parse_from(["nono", "rollback", "restore", "20260214-143022-12345"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Restore(restore_args) => {
assert_eq!(restore_args.session_id, "20260214-143022-12345");
assert_eq!(restore_args.snapshot, None); assert!(!restore_args.dry_run);
}
_ => panic!("Expected Restore subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_restore_with_options() {
let cli = Cli::parse_from([
"nono",
"rollback",
"restore",
"20260214-143022-12345",
"--snapshot",
"3",
"--dry-run",
]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Restore(restore_args) => {
assert_eq!(restore_args.snapshot, Some(3));
assert!(restore_args.dry_run);
}
_ => panic!("Expected Restore subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_audit_list() {
let cli = Cli::parse_from(["nono", "audit", "list", "--today"]);
match cli.command {
Commands::Audit(args) => match args.command {
AuditCommands::List(list_args) => {
assert!(list_args.today);
assert!(!list_args.json);
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_audit_show() {
let cli = Cli::parse_from(["nono", "audit", "show", "20260214-143022-12345", "--json"]);
match cli.command {
Commands::Audit(args) => match args.command {
AuditCommands::Show(show_args) => {
assert_eq!(show_args.session_id, "20260214-143022-12345");
assert!(show_args.json);
}
_ => panic!("Expected Show subcommand"),
},
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_audit_verify() {
let cli = Cli::parse_from(["nono", "audit", "verify", "20260214-143022-12345", "--json"]);
match cli.command {
Commands::Audit(args) => match args.command {
AuditCommands::Verify(verify_args) => {
assert_eq!(verify_args.session_id, "20260214-143022-12345");
assert!(verify_args.json);
}
_ => panic!("Expected Verify subcommand"),
},
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_audit_cleanup() {
let cli = Cli::parse_from(["nono", "audit", "cleanup", "--keep", "5", "--dry-run"]);
match cli.command {
Commands::Audit(args) => match args.command {
AuditCommands::Cleanup(cleanup_args) => {
assert_eq!(cleanup_args.keep, Some(5));
assert!(cleanup_args.dry_run);
assert!(!cleanup_args.all);
}
_ => panic!("Expected Cleanup subcommand"),
},
_ => panic!("Expected Audit command"),
}
}
#[test]
fn test_session_cleanup() {
let cli = Cli::parse_from(["nono", "session", "cleanup", "--older-than", "7"]);
match cli.command {
Commands::Session(args) => match args.command {
SessionCommands::Cleanup(cleanup_args) => {
assert_eq!(cleanup_args.older_than, Some(7));
assert!(!cleanup_args.dry_run);
}
},
_ => panic!("Expected Session command"),
}
}
#[test]
fn test_prune_still_parses_as_hidden_compat_command() {
let cli = Cli::parse_from(["nono", "prune", "--dry-run"]);
match cli.command {
Commands::Prune(args) => assert!(args.dry_run),
_ => panic!("Expected hidden Prune command"),
}
}
#[test]
fn test_rollback_verify() {
let cli = Cli::parse_from(["nono", "rollback", "verify", "20260214-143022-12345"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Verify(verify_args) => {
assert_eq!(verify_args.session_id, "20260214-143022-12345");
}
_ => panic!("Expected Verify subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_cleanup_defaults() {
let cli = Cli::parse_from(["nono", "rollback", "cleanup"]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Cleanup(cleanup_args) => {
assert!(cleanup_args.keep.is_none());
assert!(cleanup_args.older_than.is_none());
assert!(!cleanup_args.dry_run);
assert!(!cleanup_args.all);
}
_ => panic!("Expected Cleanup subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_rollback_cleanup_with_options() {
let cli = Cli::parse_from([
"nono",
"rollback",
"cleanup",
"--keep",
"5",
"--older-than",
"30",
"--dry-run",
]);
match cli.command {
Commands::Rollback(args) => match args.command {
RollbackCommands::Cleanup(cleanup_args) => {
assert_eq!(cleanup_args.keep, Some(5));
assert_eq!(cleanup_args.older_than, Some(30));
assert!(cleanup_args.dry_run);
assert!(!cleanup_args.all);
}
_ => panic!("Expected Cleanup subcommand"),
},
_ => panic!("Expected Rollback command"),
}
}
#[test]
fn test_trust_init_defaults() {
let cli = Cli::parse_from(["nono", "trust", "init"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Init(init_args) => {
assert!(!init_args.force);
assert!(init_args.key.is_none());
assert!(init_args.include.is_empty());
}
_ => panic!("Expected Init subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_init_with_includes() {
let cli = Cli::parse_from([
"nono",
"trust",
"init",
"--include",
"*.md",
"*.py",
"--force",
]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Init(init_args) => {
assert!(init_args.force);
assert_eq!(init_args.include, vec!["*.md", "*.py"]);
}
_ => panic!("Expected Init subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_sign() {
let cli = Cli::parse_from(["nono", "trust", "sign", "SKILLS.md"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Sign(sign_args) => {
assert_eq!(sign_args.files, vec![PathBuf::from("SKILLS.md")]);
assert!(!sign_args.all);
assert!(sign_args.key.is_none());
}
_ => panic!("Expected Sign subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_sign_with_key() {
let cli = Cli::parse_from(["nono", "trust", "sign", "SKILLS.md", "--key", "my-key"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Sign(sign_args) => {
assert_eq!(sign_args.key, Some("my-key".to_string()));
}
_ => panic!("Expected Sign subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_sign_all() {
let cli = Cli::parse_from(["nono", "trust", "sign", "--all"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Sign(sign_args) => {
assert!(sign_args.all);
assert!(sign_args.files.is_empty());
assert!(!sign_args.multi_subject);
}
_ => panic!("Expected Sign subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_sign_multi_subject() {
let cli = Cli::parse_from(["nono", "trust", "sign", "--all", "--multi-subject"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Sign(sign_args) => {
assert!(sign_args.all);
assert!(sign_args.multi_subject);
}
_ => panic!("Expected Sign subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_verify() {
let cli = Cli::parse_from(["nono", "trust", "verify", "SKILLS.md"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Verify(verify_args) => {
assert_eq!(verify_args.files, vec![PathBuf::from("SKILLS.md")]);
assert!(!verify_args.all);
}
_ => panic!("Expected Verify subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_list() {
let cli = Cli::parse_from(["nono", "trust", "list"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::List(list_args) => {
assert!(!list_args.json);
}
_ => panic!("Expected List subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_keygen() {
let cli = Cli::parse_from(["nono", "trust", "keygen"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Keygen(keygen_args) => {
assert_eq!(keygen_args.id, "default");
assert!(!keygen_args.force);
}
_ => panic!("Expected Keygen subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_keygen_with_id() {
let cli = Cli::parse_from(["nono", "trust", "keygen", "--id", "my-key", "--force"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::Keygen(keygen_args) => {
assert_eq!(keygen_args.id, "my-key");
assert!(keygen_args.force);
}
_ => panic!("Expected Keygen subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_export_key_defaults() {
let cli = Cli::parse_from(["nono", "trust", "export-key"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::ExportKey(export_args) => {
assert_eq!(export_args.id, "default");
assert!(!export_args.pem);
}
_ => panic!("Expected ExportKey subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_trust_export_key_with_options() {
let cli = Cli::parse_from(["nono", "trust", "export-key", "--id", "my-key", "--pem"]);
match cli.command {
Commands::Trust(args) => match args.command {
TrustCommands::ExportKey(export_args) => {
assert_eq!(export_args.id, "my-key");
assert!(export_args.pem);
}
_ => panic!("Expected ExportKey subcommand"),
},
_ => panic!("Expected Trust command"),
}
}
#[test]
fn test_rollback_flags_with_no_rollback() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--no-rollback",
"--rollback-exclude",
"target",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert!(args.no_rollback);
assert_eq!(args.rollback_exclude, vec!["target"]);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_no_audit_integrity_flag_parses() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--no-audit-integrity",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert!(args.no_audit_integrity);
assert!(!args.audit_integrity);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_rollback_all_conflicts_with_include() {
let result = Cli::try_parse_from([
"nono",
"run",
"--allow",
".",
"--rollback-all",
"--rollback-include",
"target",
"echo",
"hello",
]);
assert!(
result.is_err(),
"--rollback-all and --rollback-include should conflict"
);
}
#[test]
fn test_allow_net_parsing() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--allow-net",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert!(args.sandbox.allow_net);
assert!(!args.sandbox.block_net);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_allow_net_conflicts_with_block_net() {
let result = Cli::try_parse_from([
"nono",
"run",
"--allow",
".",
"--allow-net",
"--block-net",
"echo",
]);
assert!(
result.is_err(),
"--allow-net and --block-net should conflict"
);
}
#[test]
fn test_allow_net_conflicts_with_network_profile() {
let result = Cli::try_parse_from([
"nono",
"run",
"--allow",
".",
"--allow-net",
"--network-profile",
"developer",
"echo",
]);
assert!(
result.is_err(),
"--allow-net and --network-profile should conflict"
);
}
#[test]
fn test_allow_net_conflicts_with_allow_domain() {
let result = Cli::try_parse_from([
"nono",
"run",
"--allow",
".",
"--allow-net",
"--allow-domain",
"api.openai.com",
"echo",
]);
assert!(
result.is_err(),
"--allow-net and --allow-domain should conflict"
);
}
#[test]
fn test_network_flag_aliases_still_parse() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--allow-domain",
"api.openai.com",
"--credential",
"openai",
"--listen-port",
"3000",
"--open-port",
"5432",
"--upstream-proxy",
"squid.corp:3128",
"--upstream-bypass",
"internal.corp",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow_proxy, vec!["api.openai.com"]);
assert_eq!(args.sandbox.proxy_credential, vec!["openai"]);
assert_eq!(args.sandbox.allow_bind, vec![3000]);
assert_eq!(args.sandbox.allow_port, vec![5432]);
assert_eq!(
args.sandbox.external_proxy.as_deref(),
Some("squid.corp:3128")
);
assert_eq!(args.sandbox.external_proxy_bypass, vec!["internal.corp"]);
}
_ => panic!("Expected Run command"),
}
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--net-allow",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert!(args.sandbox.allow_net);
}
_ => panic!("Expected Run command"),
}
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--proxy-allow",
"api.openai.com",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow_proxy, vec!["api.openai.com"]);
}
_ => panic!("Expected Run command"),
}
let cli = Cli::parse_from(["nono", "why", "--host", "example.com", "--net-block"]);
match cli.command {
Commands::Why(args) => {
assert!(args.block_net);
}
_ => panic!("Expected Why command"),
}
let cli = Cli::parse_from(["nono", "why", "--scope", "abstract-unix-socket"]);
match cli.command {
Commands::Why(args) => {
assert!(matches!(args.scope, Some(WhyScope::AbstractUnixSocket)));
}
_ => panic!("Expected Why command"),
}
}
#[test]
fn test_unix_socket_subtree_flags_parse() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow-unix-socket-subtree",
"/tmp/nx",
"--allow-unix-socket-subtree-bind",
"/tmp/nx-bind",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(
args.sandbox.allow_unix_socket_subtree,
vec![PathBuf::from("/tmp/nx")]
);
assert_eq!(
args.sandbox.allow_unix_socket_subtree_bind,
vec![PathBuf::from("/tmp/nx-bind")]
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_allow_endpoint_flag_parses() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--credential",
"github",
"--allow-endpoint",
"github:GET:/repos/*/issues",
"--allow-endpoint",
"github:POST:/repos/*/issues/*/comments",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow_endpoint.len(), 2);
assert_eq!(args.sandbox.allow_endpoint[0], "github:GET:/repos/*/issues");
assert_eq!(
args.sandbox.allow_endpoint[1],
"github:POST:/repos/*/issues/*/comments"
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_bypass_protection_single() {
let cli = Cli::parse_from([
"nono",
"run",
"--bypass-protection",
"/tmp/test",
"--allow",
"/tmp/test",
"echo",
"hello",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.bypass_protection.len(), 1);
assert_eq!(
args.sandbox.bypass_protection[0],
PathBuf::from("/tmp/test")
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_bypass_protection_multiple() {
let cli = Cli::parse_from([
"nono",
"run",
"--bypass-protection",
"/tmp/a",
"--bypass-protection",
"/tmp/b",
"--allow",
".",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.bypass_protection.len(), 2);
assert_eq!(args.sandbox.bypass_protection[0], PathBuf::from("/tmp/a"));
assert_eq!(args.sandbox.bypass_protection[1], PathBuf::from("/tmp/b"));
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_override_deny_alias_populates_bypass_protection() {
let cli = Cli::parse_from([
"nono",
"run",
"--override-deny",
"/tmp/test",
"--allow",
"/tmp/test",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.bypass_protection.len(), 1);
assert_eq!(
args.sandbox.bypass_protection[0],
PathBuf::from("/tmp/test")
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_suppress_save_prompt_multiple() {
let cli = Cli::parse_from([
"nono",
"run",
"--suppress-save-prompt",
"/tmp/a",
"--suppress-save-prompt",
"/tmp/b",
"--allow",
".",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.suppress_save_prompt.len(), 2);
assert_eq!(
args.sandbox.suppress_save_prompt[0],
PathBuf::from("/tmp/a")
);
assert_eq!(
args.sandbox.suppress_save_prompt[1],
PathBuf::from("/tmp/b")
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_ignore_denied_alias_maps_to_suppress_save_prompt() {
let cli = Cli::parse_from([
"nono",
"run",
"--ignore-denied",
"/tmp/a",
"--allow",
".",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(
args.sandbox.suppress_save_prompt,
vec![PathBuf::from("/tmp/a")]
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_env_credential_map_repeatable_parses_pairs() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--env-credential-map",
"op://vault/item/field",
"OPENAI_API_KEY",
"--env-credential-map",
"apple-password://github.com/user=name",
"GITHUB_PASSWORD",
"echo",
"ok",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(
args.sandbox.env_credential_map,
vec![
"op://vault/item/field".to_string(),
"OPENAI_API_KEY".to_string(),
"apple-password://github.com/user=name".to_string(),
"GITHUB_PASSWORD".to_string()
]
);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_capability_elevation_flag_sets_true() {
let cli = Cli::parse_from([
"nono",
"run",
"--allow",
".",
"--capability-elevation",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert!(args.capability_elevation);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_trust_override_flag_sets_true() {
let cli = Cli::parse_from(["nono", "run", "--allow", ".", "--trust-override", "echo"]);
match cli.command {
Commands::Run(args) => {
assert!(args.trust_override);
}
_ => panic!("Expected Run command"),
}
}
#[cfg(target_os = "macos")]
#[test]
fn test_trust_proxy_ca_flag_sets_true() {
let cli = Cli::parse_from(["nono", "run", "--allow", ".", "--trust-proxy-ca", "echo"]);
match cli.command {
Commands::Run(args) => {
assert!(args.sandbox.trust_proxy_ca);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_allow_port_parsing() {
let cli = Cli::parse_from([
"nono",
"run",
"--open-port",
"3000",
"--open-port",
"5000",
"--allow",
".",
"echo",
]);
match cli.command {
Commands::Run(args) => {
assert_eq!(args.sandbox.allow_port, vec![3000, 5000]);
}
_ => panic!("Expected Run command"),
}
}
#[test]
fn test_profile_init_basic() {
let cli = Cli::parse_from(["nono", "profile", "init", "my-agent"]);
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::Init(init) => {
assert_eq!(init.name, "my-agent");
assert!(init.extends.is_none());
assert!(init.groups.is_empty());
assert!(init.description.is_none());
assert!(!init.full);
assert!(init.output.is_none());
assert!(!init.force);
}
_ => panic!("Expected Init subcommand"),
},
_ => panic!("Expected Profile command"),
}
}
#[test]
fn test_profile_init_all_flags() {
let cli = Cli::parse_from([
"nono",
"profile",
"init",
"my-agent",
"--extends",
"default",
"--groups",
"deny_credentials,node_runtime",
"--description",
"My agent profile",
"--full",
"--output",
"/tmp/out.json",
"--force",
]);
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::Init(init) => {
assert_eq!(init.name, "my-agent");
assert_eq!(init.extends, Some("default".to_string()));
assert_eq!(init.groups, vec!["deny_credentials", "node_runtime"]);
assert_eq!(init.description, Some("My agent profile".to_string()));
assert!(init.full);
assert_eq!(init.output, Some(std::path::PathBuf::from("/tmp/out.json")));
assert!(init.force);
}
_ => panic!("Expected Init subcommand"),
},
_ => panic!("Expected Profile command"),
}
}
#[test]
fn test_profile_schema_default() {
let cli = Cli::parse_from(["nono", "profile", "schema"]);
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::Schema(schema) => {
assert!(schema.output.is_none());
}
_ => panic!("Expected Schema subcommand"),
},
_ => panic!("Expected Profile command"),
}
}
#[test]
fn test_profile_schema_with_output() {
let cli = Cli::parse_from(["nono", "profile", "schema", "-o", "/tmp/schema.json"]);
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::Schema(schema) => {
assert_eq!(
schema.output,
Some(std::path::PathBuf::from("/tmp/schema.json"))
);
}
_ => panic!("Expected Schema subcommand"),
},
_ => panic!("Expected Profile command"),
}
}
#[test]
fn test_profile_guide() {
let cli = Cli::parse_from(["nono", "profile", "guide"]);
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::Guide(_) => {}
_ => panic!("Expected Guide subcommand"),
},
_ => panic!("Expected Profile command"),
}
}
#[test]
fn test_profile_init_missing_name() {
let result = Cli::try_parse_from(["nono", "profile", "init"]);
assert!(result.is_err(), "init without name should fail");
}
#[test]
fn test_profile_no_subcommand() {
let result = Cli::try_parse_from(["nono", "profile"]);
assert!(result.is_err(), "profile without subcommand should fail");
}
#[test]
fn test_profile_list_parses() {
let cli = Cli::try_parse_from(["nono", "profile", "list", "--json"])
.expect("profile list --json must parse");
match cli.command {
Commands::Profile(args) => match args.command {
ProfileCommands::List(a) => assert!(a.json, "--json flag not set"),
_ => panic!("expected ProfileCommands::List"),
},
_ => panic!("expected Commands::Profile"),
}
}
#[test]
fn test_profile_show_parses_with_format_manifest() {
let cli =
Cli::try_parse_from(["nono", "profile", "show", "default", "--format", "manifest"])
.expect("profile show --format manifest must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Show(a) = args.command
{
assert_eq!(a.profile, "default");
assert!(matches!(a.format, Some(ProfileShowFormat::Manifest)));
return;
}
panic!("expected Commands::Profile(Show(..))");
}
#[test]
fn test_profile_show_parses_with_json_and_raw() {
let cli = Cli::try_parse_from(["nono", "profile", "show", "default", "--json", "--raw"])
.expect("profile show --json --raw must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Show(a) = args.command
{
assert!(a.json);
assert!(a.raw);
return;
}
panic!("expected Commands::Profile(Show(..))");
}
#[test]
fn test_profile_groups_parses() {
let cli = Cli::try_parse_from(["nono", "profile", "groups", "--json", "--all-platforms"])
.expect("profile groups --json --all-platforms must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Groups(a) = args.command
{
assert!(a.json);
assert!(a.all_platforms);
return;
}
panic!("expected Commands::Profile(Groups(..))");
}
#[test]
fn test_profile_groups_with_name() {
let cli = Cli::try_parse_from(["nono", "profile", "groups", "deny_credentials"])
.expect("profile groups <name> must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Groups(a) = args.command
{
assert_eq!(a.name.as_deref(), Some("deny_credentials"));
return;
}
panic!("expected Commands::Profile(Groups(..))");
}
#[test]
fn test_profile_diff_parses() {
let cli = Cli::try_parse_from(["nono", "profile", "diff", "a", "b"])
.expect("profile diff must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Diff(a) = args.command
{
assert_eq!(a.profile1, "a");
assert_eq!(a.profile2, "b");
return;
}
panic!("expected Commands::Profile(Diff(..))");
}
#[test]
fn test_profile_validate_parses() {
let cli = Cli::try_parse_from(["nono", "profile", "validate", "/tmp/p.json"])
.expect("profile validate must parse");
if let Commands::Profile(args) = cli.command
&& let ProfileCommands::Validate(a) = args.command
{
assert_eq!(a.file.to_string_lossy(), "/tmp/p.json");
return;
}
panic!("expected Commands::Profile(Validate(..))");
}
const ALL_SUBCOMMANDS: &[&str] = &[
"setup",
"run",
"shell",
"wrap",
"learn",
"why",
"ps",
"stop",
"detach",
"attach",
"logs",
"inspect",
"session",
"rollback",
"audit",
"trust",
"policy",
"profile",
"pull",
"remove",
"update",
"outdated",
"pin",
"unpin",
"search",
"list",
"completion",
];
#[test]
fn test_root_help_lists_all_commands() {
let cmd = Cli::command();
let mut buf = Vec::new();
cmd.clone()
.write_help(&mut buf)
.expect("failed to write help");
let help = String::from_utf8(buf).expect("help is not utf-8");
for name in ALL_SUBCOMMANDS {
assert!(
help.contains(&format!(" {name}")),
"Root --help is missing subcommand `{name}`. \
Update the help_template on the Cli struct.",
);
}
for sub in cmd.get_subcommands() {
let name = sub.get_name().to_string();
if name == "help" || sub.is_hide_set() {
continue; }
assert!(
ALL_SUBCOMMANDS.contains(&name.as_str()),
"Commands enum has variant `{name}` not listed in ALL_SUBCOMMANDS. \
Add it to the constant and to the root help_template.",
);
}
}
#[test]
fn test_root_help_shows_all_flags() {
let cmd = Cli::command();
let mut buf = Vec::new();
cmd.clone()
.write_help(&mut buf)
.expect("failed to write help");
let help = String::from_utf8(buf).expect("help is not utf-8");
for arg in cmd.get_arguments() {
if arg.is_hide_set() {
continue;
}
if let Some(long) = arg.get_long() {
assert!(
help.contains(&format!("--{long}")),
"Root --help is missing flag `--{long}`. \
Add `help_heading = \"OPTIONS\"` to its #[arg] attribute.",
);
}
}
}
#[test]
fn test_subcommand_help_structure() {
let root = Cli::command();
for sub in root.get_subcommands() {
let name = sub.get_name().to_string();
if name == "help" || sub.is_hide_set() {
continue;
}
let mut buf = Vec::new();
sub.clone()
.write_help(&mut buf)
.expect("failed to write help");
let help = String::from_utf8(buf).expect("help is not utf-8");
assert!(
help.contains("USAGE"),
"`nono {name} --help` is missing a USAGE section",
);
assert!(
help.contains("EXAMPLES"),
"`nono {name} --help` is missing an EXAMPLES section",
);
assert!(
help.contains(&format!("nono {name}")),
"`nono {name} --help` USAGE line doesn't mention `nono {name}`",
);
let known_flags: Vec<String> = sub
.get_arguments()
.filter_map(|a: &clap::Arg| a.get_long().map(|l| l.to_string()))
.collect();
let known_sub_flags: Vec<String> = sub
.get_subcommands()
.flat_map(|s: &clap::Command| s.get_arguments())
.filter_map(|a: &clap::Arg| a.get_long().map(|l| l.to_string()))
.collect();
if let Some(examples_start) = help.find("EXAMPLES") {
let examples = &help[examples_start..];
for token in examples.split_whitespace() {
if let Some(flag) = token.strip_prefix("--") {
let flag =
flag.trim_end_matches(|c: char| !c.is_ascii_alphanumeric() && c != '-');
if flag.is_empty() || flag == "help" {
continue;
}
let valid = known_flags.iter().any(|f| f == flag)
|| known_sub_flags.iter().any(|f| f == flag);
assert!(
valid,
"`nono {name} --help` EXAMPLES references --{flag} \
which is not a known flag on this subcommand",
);
}
}
}
}
}
#[test]
fn test_log_file_flag() {
let cli = Cli::parse_from([
"nono",
"--log-file",
"/tmp/nono.log",
"run",
"--allow",
".",
"echo",
"hi",
]);
assert_eq!(cli.log_file, Some(PathBuf::from("/tmp/nono.log")));
}
#[test]
fn test_log_file_flag_absent() {
let cli = Cli::parse_from(["nono", "run", "--allow", ".", "echo", "hi"]);
assert!(cli.log_file.is_none());
}
}