Skip to content
Merged
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
Original file line number Diff line number Diff line change
Expand Up @@ -772,7 +772,7 @@ impl ExtensionManagementPort {
count,
},
);
if extension_search_has_installed_external_channel_result(response.payload.as_ref()) {
if extension_search_has_setup_needed_external_channel_result(response.payload.as_ref()) {
response.message = Some(
"Search found external channel results whose personal setup is incomplete. For an explicit connect, pair, authenticate, or account-access request, call builtin.extension_install for the matching extension id so the manifest-declared connection flow can continue. For routine, trigger, or notification delivery, prefer the configured outbound delivery target when one is available."
.to_string(),
Expand Down Expand Up @@ -3045,11 +3045,7 @@ fn extension_search_has_ready_result(payload: Option<&LifecycleProductPayload>)
matches!(
extension.installation_phase,
Some(LifecyclePublicState::Active)
) && !extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
&& extension.summary.credential_requirements.is_empty()
) && extension.summary.credential_requirements.is_empty()
&& extension.summary.onboarding.is_none()
})
}
Expand All @@ -3071,20 +3067,18 @@ fn extension_search_has_inactive_installed_result(
})
}

fn extension_search_has_installed_external_channel_result(
fn extension_search_has_setup_needed_external_channel_result(
payload: Option<&LifecycleProductPayload>,
) -> bool {
let Some(LifecycleProductPayload::ExtensionSearch { extensions, .. }) = payload else {
return false;
};
extensions.iter().any(|extension| {
matches!(
extension.installation_phase,
Some(LifecyclePublicState::SetupNeeded | LifecyclePublicState::Active)
) && extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
extension.installation_phase == Some(LifecyclePublicState::SetupNeeded)
&& extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
})
}
Comment on lines +3070 to 3083

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve the credential-requirements guard.

This predicate now classifies every SetupNeeded channel as personal connection setup, including results with unresolved credential_requirements. Restore the empty-requirements check so credential-gated extensions do not receive the channel-specific builtin.extension_install guidance.

Proposed fix
     extensions.iter().any(|extension| {
         extension.installation_phase == Some(LifecyclePublicState::SetupNeeded)
+            && extension.summary.credential_requirements.is_empty()
             && extension
                 .summary
                 .surface_kinds

Based on the supplied lifecycle guidance contract, this branch must remain limited to setup-needed channels without package credential requirements.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
fn extension_search_has_setup_needed_external_channel_result(
payload: Option<&LifecycleProductPayload>,
) -> bool {
let Some(LifecycleProductPayload::ExtensionSearch { extensions, .. }) = payload else {
return false;
};
extensions.iter().any(|extension| {
matches!(
extension.installation_phase,
Some(LifecyclePublicState::SetupNeeded | LifecyclePublicState::Active)
) && extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
extension.installation_phase == Some(LifecyclePublicState::SetupNeeded)
&& extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
})
}
fn extension_search_has_setup_needed_external_channel_result(
payload: Option<&LifecycleProductPayload>,
) -> bool {
let Some(LifecycleProductPayload::ExtensionSearch { extensions, .. }) = payload else {
return false;
};
extensions.iter().any(|extension| {
extension.installation_phase == Some(LifecyclePublicState::SetupNeeded)
&& extension.summary.credential_requirements.is_empty()
&& extension
.summary
.surface_kinds
.contains(&CapabilitySurfaceKind::Channel)
})
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/ironclaw_reborn_composition/src/extension_host/extension_lifecycle.rs`
around lines 3070 - 3083, Update
extension_search_has_setup_needed_external_channel_result so it only matches
SetupNeeded channel extensions whose credential_requirements are empty; preserve
the existing false result for non-ExtensionSearch payloads and leave
credential-gated extensions out of the channel-specific guidance.


Expand Down Expand Up @@ -3390,7 +3384,7 @@ mod tests {
}

#[test]
fn installed_external_channel_search_result_gets_activation_guidance() {
fn setup_needed_external_channel_search_result_gets_activation_guidance() {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This only tests the classifier, not the search branch that emits the model-visible setup/install guidance. Please add a facade-level SetupNeeded external-channel search regression asserting the incomplete-setup and builtin.extension_install message.

let payload = LifecycleProductPayload::ExtensionSearch {
extensions: vec![LifecycleSearchExtensionSummary {
summary: LifecycleExtensionSummary {
Expand Down Expand Up @@ -3418,7 +3412,7 @@ mod tests {
count: 1,
};

assert!(extension_search_has_installed_external_channel_result(
assert!(extension_search_has_setup_needed_external_channel_result(
Some(&payload)
));
assert!(!extension_search_has_ready_result(Some(&payload)));
Expand Down Expand Up @@ -4847,11 +4841,11 @@ supports_threads = true
}

#[tokio::test]
async fn extension_search_distinguishes_external_channel_connect_from_delivery() {
// Generic external-channel search guidance. Uses a neutral `example_bot`
// fixture rather than the real Slack bot: under model B `slack_bot` is
// hidden from search, so a Slack-named fixture would be filtered out and
// this generic guidance would go untested.
async fn active_external_channel_search_result_gets_ready_guidance() {
// Uses a neutral `example_bot` fixture rather than the real Slack bot:
// under model B `slack_bot` is hidden from search, so a Slack-named
// fixture would be filtered out and this generic guidance would go
// untested.
let (_dir, _storage_root, facade, _active_registry, _installation_store) =
extension_lifecycle_fixture_with_catalog_and_service(
AvailableExtensionCatalog::from_packages(vec![fixture_external_channel_package(
Expand Down Expand Up @@ -4883,16 +4877,13 @@ supports_threads = true

let message = search.message.as_deref().expect("search guidance");
assert!(
message.contains("external channel")
&& message.contains("explicit connect")
&& message.contains("builtin.extension_install")
&& message.contains("outbound delivery target")
&& message.contains("personal setup is incomplete"),
"active external channel search should distinguish connect requests from delivery, got: {message}"
message.contains("Treat those results as ready"),
"active external channel search must use ready guidance, got: {message}"
);
assert!(
!message.contains("Treat those results as ready"),
"active external channels must not use ready-extension guidance: {message}"
!message.contains("setup is incomplete")
&& !message.contains("builtin.extension_install"),
"active external channel search must not emit setup guidance: {message}"
);
let Some(LifecycleProductPayload::ExtensionSearch { extensions, .. }) =
search.payload.as_ref()
Expand Down
Loading