Skip to content
35 changes: 15 additions & 20 deletions crates/codegen/ainxt-pager-bin/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -87,33 +87,28 @@ fn resolve_agent_profile_path(path: &std::path::Path) -> std::path::PathBuf {
}
/// Print startup information for the serve command.
///
/// The secret itself is never printed to the terminal (which can be
/// No part of the secret is ever printed to the terminal (which can be
/// captured by session recorders, terminal multiplexer scrollback, or
/// screen-sharing); only a short prefix is shown so an operator who already
/// knows the secret can sanity-check they're looking at the right value.
fn print_serve_startup_info(bind_addr: SocketAddr, secret: &str) {
let masked = mask_secret_for_display(secret);
/// screen-sharing). Even a short prefix materially reduces the search space
/// of an auto-generated secret, so only its *source* is reported. The
/// function deliberately does not take the secret as a parameter so that no
/// secret-derived data can reach this output: it only receives whether the
/// user supplied the value (`user_supplied`) and prints a fixed label.
fn print_serve_startup_info(bind_addr: SocketAddr, user_supplied: bool) {
let origin_label = if user_supplied {
"provided via --secret / AINXT_AGENT_SECRET (not shown)"
} else {
"auto-generated (not shown; pass --secret / AINXT_AGENT_SECRET to choose one)"
};
eprintln!();
eprintln!(" ainxt agent server starting...");
eprintln!();
eprintln!(" Address: {}:{}", bind_addr.ip(), bind_addr.port());
eprintln!(" Secret: {masked} (see --secret / AINXT_AGENT_SECRET to recover the full value)");
eprintln!(" Secret: {origin_label}");
eprintln!();
eprintln!(" WebSocket URL: ws://{bind_addr}/ws?server-key=<secret>");
eprintln!();
}

/// Render a secret as a short, non-reversible display form: the first 4
/// characters followed by an ellipsis, or fully masked when too short for a
/// partial prefix to be safe.
fn mask_secret_for_display(secret: &str) -> String {
if secret.chars().count() <= 4 {
"****".to_string()
} else {
let prefix: String = secret.chars().take(4).collect();
format!("{prefix}…")
}
}
/// Entrypoint tag for `ainxt -p`; keys the quiet stderr default in `init_tracing_simple`.
const HEADLESS_ENTRYPOINT: &str = "headless";
/// Initialize simple tracing for non-TUI agent modes.
Expand Down Expand Up @@ -1338,9 +1333,9 @@ async fn run_agent_command(
let secret = a.get_secret();
let server_config = ainxt_shell::agent::ServerConfig {
bind_addr: a.bind,
secret: secret.clone(),
secret,
};
print_serve_startup_info(a.bind, &secret);
print_serve_startup_info(a.bind, a.secret.is_some());
ainxt_shell::agent::run_agent_server(server_config, agent_config).await
}
Some(AgentCmd::Leader(a)) => {
Expand Down
190 changes: 190 additions & 0 deletions crates/codegen/ainxt-pager/src/app/actions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2961,3 +2961,193 @@ mod tests {
);
}
}

/// Test-only view of effects for assertion / panic messages: each `Effect`
/// is reduced to its variant name, never its payload. Payloads can carry
/// session ids, account and billing data, so failing tests report *which*
/// effects were emitted (e.g. `["PersistSetting", "SwitchModel"]`)
/// without writing their contents to the test log.
#[cfg(test)]
pub(crate) trait EffectKinds {
type Kinds: std::fmt::Debug;
fn effect_kinds(&self) -> Self::Kinds;
}

#[cfg(test)]
impl EffectKinds for Effect {
type Kinds = &'static str;
fn effect_kinds(&self) -> &'static str {
match self {
Effect::CreateSession { .. } => "CreateSession",
Effect::SetWorkingDir { .. } => "SetWorkingDir",
Effect::CreateWorktreeSession { .. } => "CreateWorktreeSession",
Effect::LoadSession { .. } => "LoadSession",
Effect::ScanForeignSessions { .. } => "ScanForeignSessions",
Effect::CanonicalizeForeignResumeCwd { .. } => "CanonicalizeForeignResumeCwd",
Effect::DetectForeignResumeHint { .. } => "DetectForeignResumeHint",
Effect::FetchSessionList { .. } => "FetchSessionList",
Effect::DebounceSessionSearch { .. } => "DebounceSessionSearch",
Effect::FetchRoster { .. } => "FetchRoster",
Effect::FetchDashboardSessions { .. } => "FetchDashboardSessions",
Effect::LoadCardDetail { .. } => "LoadCardDetail",
Effect::RestoreAndLoadSession { .. } => "RestoreAndLoadSession",
Effect::SendPrompt { .. } => "SendPrompt",
Effect::SendBashCommand { .. } => "SendBashCommand",
Effect::CancelTurn { .. } => "CancelTurn",
Effect::Compact { .. } => "Compact",
Effect::KillBgTask { .. } => "KillBgTask",
Effect::KillSubagent { .. } => "KillSubagent",
Effect::DeleteScheduledTask { .. } => "DeleteScheduledTask",
Effect::DemoteToBackground { .. } => "DemoteToBackground",
Effect::SwitchModel { .. } => "SwitchModel",
Effect::FetchChangelog { .. } => "FetchChangelog",
Effect::PersistAnnouncementsHidden { .. } => "PersistAnnouncementsHidden",
Effect::PersistMemoryFullscreen { .. } => "PersistMemoryFullscreen",
Effect::PersistProjectPickerDisabled { .. } => "PersistProjectPickerDisabled",
Effect::PersistDashboard { .. } => "PersistDashboard",
Effect::PersistWorktreeMode { .. } => "PersistWorktreeMode",
Effect::PersistPreferredModel { .. } => "PersistPreferredModel",
Effect::PersistPermissionMode { .. } => "PersistPermissionMode",
Effect::PersistSetting { .. } => "PersistSetting",
Effect::SaveModelOverride { .. } => "SaveModelOverride",
Effect::DeleteModelOverride { .. } => "DeleteModelOverride",
Effect::SendPromptBlocks { .. } => "SendPromptBlocks",
Effect::SendPromptNow { .. } => "SendPromptNow",
Effect::TogglePlanMode { .. } => "TogglePlanMode",
Effect::QueueRemove { .. } => "QueueRemove",
Effect::QueueReorder { .. } => "QueueReorder",
Effect::QueueClear { .. } => "QueueClear",
Effect::QueueEdit { .. } => "QueueEdit",
Effect::QueueInterject { .. } => "QueueInterject",
Effect::SetSessionMode { .. } => "SetSessionMode",
Effect::SetModeThenPrompt { .. } => "SetModeThenPrompt",
Effect::FetchPromptHistory { .. } => "FetchPromptHistory",
Effect::FetchSessionAgentName { .. } => "FetchSessionAgentName",
Effect::Authenticate { .. } => "Authenticate",
Effect::PollAuthUrl { .. } => "PollAuthUrl",
Effect::SubmitAuthCode { .. } => "SubmitAuthCode",
Effect::FetchMcpsList { .. } => "FetchMcpsList",
Effect::McpAuthTrigger { .. } => "McpAuthTrigger",
Effect::FetchHooksList { .. } => "FetchHooksList",
Effect::FetchPluginsList { .. } => "FetchPluginsList",
Effect::HooksAction { .. } => "HooksAction",
Effect::PluginsAction { .. } => "PluginsAction",
Effect::FetchMarketplaceList { .. } => "FetchMarketplaceList",
Effect::CheckMarketplaceUpdates { .. } => "CheckMarketplaceUpdates",
Effect::FetchPluginCtaCatalog { .. } => "FetchPluginCtaCatalog",
Effect::FetchSkillsList { .. } => "FetchSkillsList",
Effect::ToggleSkill { .. } => "ToggleSkill",
Effect::MarketplaceAction { .. } => "MarketplaceAction",
Effect::InstallPluginFromCta { .. } => "InstallPluginFromCta",
Effect::ReloadPluginsForCta { .. } => "ReloadPluginsForCta",
Effect::FetchPluginCtaMcps { .. } => "FetchPluginCtaMcps",
Effect::RetryPluginCtaMcps { .. } => "RetryPluginCtaMcps",
Effect::DismissCtaInstalled { .. } => "DismissCtaInstalled",
Effect::UpsertMcpServer { .. } => "UpsertMcpServer",
Effect::DeleteMcpServer { .. } => "DeleteMcpServer",
Effect::ToggleMcpServer { .. } => "ToggleMcpServer",
Effect::ToggleMcpTool { .. } => "ToggleMcpTool",
Effect::ShareSession { .. } => "ShareSession",
Effect::ShowSessionInfo { .. } => "ShowSessionInfo",
Effect::ShowContextInfo { .. } => "ShowContextInfo",
Effect::ShowSessionUsage { .. } => "ShowSessionUsage",
Effect::FetchBundleStatus { .. } => "FetchBundleStatus",
Effect::FetchCatalogEntry { .. } => "FetchCatalogEntry",
Effect::SendFeedback { .. } => "SendFeedback",
Effect::SaveMemoryNote { .. } => "SaveMemoryNote",
Effect::RewriteMemoryNote { .. } => "RewriteMemoryNote",
Effect::RefreshAvailableCommands { .. } => "RefreshAvailableCommands",
Effect::SendBtw { .. } => "SendBtw",
Effect::GitStatus { .. } => "GitStatus",
Effect::GitDiff { .. } => "GitDiff",
Effect::GitStage { .. } => "GitStage",
Effect::GitUnstage { .. } => "GitUnstage",
Effect::GitCommit { .. } => "GitCommit",
Effect::SuggestCommitMessage { .. } => "SuggestCommitMessage",
Effect::SendRecap { .. } => "SendRecap",
Effect::SendInterject { .. } => "SendInterject",
Effect::Logout { .. } => "Logout",
Effect::CheckSubscription { .. } => "CheckSubscription",
Effect::CreditLimitRecheck { .. } => "CreditLimitRecheck",
Effect::SchedulePaywallCheck { .. } => "SchedulePaywallCheck",
Effect::ScheduleGateVerifyTimeout { .. } => "ScheduleGateVerifyTimeout",
Effect::SwitchAccount { .. } => "SwitchAccount",
Effect::ScheduleClearAuthCopied { .. } => "ScheduleClearAuthCopied",
Effect::RegisterActiveSession { .. } => "RegisterActiveSession",
Effect::UnregisterActiveSession { .. } => "UnregisterActiveSession",
Effect::Quit { .. } => "Quit",
Effect::SetCodingDataSharing { .. } => "SetCodingDataSharing",
Effect::RenameSession { .. } => "RenameSession",
Effect::DeleteSession { .. } => "DeleteSession",
Effect::DeepSearchSessions { .. } => "DeepSearchSessions",
Effect::ForkSession { .. } => "ForkSession",
Effect::HydrateSessionTitleFromDisk { .. } => "HydrateSessionTitleFromDisk",
Effect::FetchRewindPoints { .. } => "FetchRewindPoints",
Effect::RewindPreview { .. } => "RewindPreview",
Effect::RewindExecute { .. } => "RewindExecute",
Effect::FetchBilling { .. } => "FetchBilling",
Effect::FetchAppBilling { .. } => "FetchAppBilling",
Effect::RefreshGate { .. } => "RefreshGate",
Effect::DebounceSuggestions { .. } => "DebounceSuggestions",
Effect::DebouncePluginCta { .. } => "DebouncePluginCta",
Effect::FetchShellSuggestions { .. } => "FetchShellSuggestions",
Effect::FetchPromptSuggestion { .. } => "FetchPromptSuggestion",
Effect::ProbeClipboardAttachment { .. } => "ProbeClipboardAttachment",
Effect::PreparePromptImagePreview { .. } => "PreparePromptImagePreview",
}
}
}

#[cfg(test)]
impl EffectKinds for [Effect] {
type Kinds = Vec<&'static str>;
fn effect_kinds(&self) -> Vec<&'static str> {
let mut kinds = Vec::new();
for effect in self {
kinds.push(effect.effect_kinds());
}
kinds
}
}

#[cfg(test)]
impl EffectKinds for Vec<Effect> {
type Kinds = Vec<&'static str>;
fn effect_kinds(&self) -> Vec<&'static str> {
self.as_slice().effect_kinds()
}
}

#[cfg(test)]
impl<T: EffectKinds> EffectKinds for Option<T> {
type Kinds = Option<T::Kinds>;
fn effect_kinds(&self) -> Self::Kinds {
match self {
Some(inner) => Some(inner.effect_kinds()),
None => None,
}
}
}

#[cfg(test)]
impl<T: EffectKinds + ?Sized> EffectKinds for &T {
type Kinds = T::Kinds;
fn effect_kinds(&self) -> T::Kinds {
(**self).effect_kinds()
}
}

#[cfg(test)]
impl<T: EffectKinds + ?Sized> EffectKinds for &mut T {
type Kinds = T::Kinds;
fn effect_kinds(&self) -> T::Kinds {
(**self).effect_kinds()
}
}

/// Variant name(s) of an `Effect`, a slice / `Vec` of them, or an `Option`
/// of either. See [`EffectKinds`].
#[cfg(test)]
pub(crate) fn effect_kinds<T: EffectKinds + ?Sized>(value: &T) -> T::Kinds {
value.effect_kinds()
}
6 changes: 3 additions & 3 deletions crates/codegen/ainxt-pager/src/app/dispatch/session/fork.rs
Original file line number Diff line number Diff line change
Expand Up @@ -439,17 +439,17 @@ fn build_fork_placeholder(
/// Called in `TaskResult::SessionLoaded` (not at dispatch time) because
/// the child's session id is not known until the backend responds.
pub(in crate::app::dispatch) fn build_child_fork_marker(
session_id: &str,
child_sid: &str,
parent_sid: &str,
worktree: bool,
switch_hint: Option<&str>,
) -> String {
let header = if let Some(cmd) = switch_hint {
format!(
"Session {session_id} (forked from {parent_sid}) \u{2014} use {cmd} to switch between sessions",
"Session {child_sid} (forked from {parent_sid}) \u{2014} use {cmd} to switch between sessions",
)
} else {
format!("Session {session_id} (forked from {parent_sid})")
format!("Session {child_sid} (forked from {parent_sid})")
};
if worktree {
header
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -460,7 +460,8 @@ fn auth_complete_retries_stashed_prompt_from_dashboard() {
effects
.iter()
.any(|effect| matches!(effect, Effect::SendPromptBlocks { .. })),
"stash must be retried even when login returns to the dashboard, got: {effects:?}"
"stash must be retried even when login returns to the dashboard, got: {effects:?}",
effects = crate::app::actions::effect_kinds(&effects),
);
}

Expand Down Expand Up @@ -1345,7 +1346,8 @@ fn dashboard_second_stash_does_not_overwrite_first() {
});
assert!(
reply_sent,
"the stashed peek reply must reach the peeked agent with the image; effects = {effects:?}"
"the stashed peek reply must reach the peeked agent with the image; effects = {effects:?}",
effects = crate::app::actions::effect_kinds(&effects),
);
}

Expand Down Expand Up @@ -3821,6 +3823,7 @@ fn dashboard_rename_end_to_end_top_level_row() {
if *agent_id == id && title == "My renamed session"
)),
"commit must emit a RenameSession effect, got {effects:?}",
effects = crate::app::actions::effect_kinds(&effects),
);
assert_eq!(
app.agents.get(&id).and_then(|a| a.display_name.clone()),
Expand Down
Loading
Loading