feat(reborn): add Slack personal (user-token) tool#5177
Conversation
Port the bot-token-free Slack personal tool (originally nearai/ironclaw PR #4941) to the Reborn extension architecture. Lets IronClaw act as the user via a Slack user token (xoxp-) to read full message history, DMs, group DMs, and search — things a bot token cannot do. - WASM tool source at tools-src/slack_user/ (sandboxed-tool WIT), with context-dispatched capabilities: search_messages, list_conversations, get_conversation_history, get_user_info, send_message. - reborn.extension_manifest.v2 manifest at crates/ironclaw_first_party_extensions/assets/slack_user/ with a manual-token credential: product_auth_account provider "slack_personal" with the default ManualToken setup (mirrors the github PAT tool), bearer-injected to slack.com — no OAuth provider required. Two deliberate choices: * product_auth_account (not secret_handle) so the WebUI manual-token auth gate can save the pasted token: the gate/save path keys on a provider, and a provider-less secret_handle credential made the gate fail with "Could not save the token". * provider "slack_personal" (not "slack") so the user-token credential is a separate account from the bot Slack extension (id "slack") and its IRONCLAW_REBORN_SLACK_BOT_TOKEN config — the personal tool never picks up the bot token. Per-capability input schemas + prompts mirror the google-sheets tool. - Registered in ironclaw_reborn_composition (available_extensions catalog + factory first-party trust policy), gated behind slack-v2-host-beta. Builds clean with RUSTFLAGS=-D warnings; composition catalog/trust tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
This PR was not deployed automatically as @sergeiest does not have access to the Railway project. In order to get automatic PR deploys, please add @sergeiest to your workspace on Railway. |
📝 WalkthroughSummary by CodeRabbit
WalkthroughAdds a new ChangesSlack Personal (User-Token) Extension
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 3 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (3 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Code Review
This pull request introduces a new Slack personal (user-token) WASM tool (slack_user) to allow IronClaw to act as a user in Slack, implementing capabilities such as searching messages, listing conversations, reading history, getting user info, and sending messages. The feedback suggests refactoring the reuse of gsuite_allowed_effects() for the Slack package to avoid domain boundary violations, stripping query parameters from the logged endpoint in api.rs to prevent leaking sensitive data, and clamping the limit parameter in list_conversations and get_conversation_history for defensive input validation.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| "/system/extensions/slack_user/manifest.toml".to_string(), | ||
| Some(slack_user_manifest_digest()), | ||
| HostTrustAssignment::first_party(), | ||
| gsuite_allowed_effects(), |
There was a problem hiding this comment.
Reusing gsuite_allowed_effects() for the slack_user first-party package is a domain boundary violation, as Slack is not part of the GSuite domain. While using a centralized helper is preferred to maintain drift-resistance and avoid duplicating domain knowledge locally, in a future refactoring we should rename this helper to a more generic name (e.g., wasm_tool_allowed_effects()) or split it to maintain proper domain boundaries.
References
- Prefer routing through centralized helper functions that define domain boundaries or strip transient fields (e.g.,
credential_owner_scope()) to maintain drift-resistance and avoid duplicating domain knowledge locally, even if it introduces minor performance overhead (like cloning) on non-hot paths.
| host::log( | ||
| host::LogLevel::Debug, | ||
| &format!("Slack API: {} {}", method, endpoint), | ||
| ); |
There was a problem hiding this comment.
Logging the full endpoint string can leak sensitive user-supplied query parameters (such as search queries or message contents) into the debug logs. To ensure only sanitized identifiers are logged and prevent sensitive data exposure, we should strip the query parameters from the endpoint before logging.
| host::log( | |
| host::LogLevel::Debug, | |
| &format!("Slack API: {} {}", method, endpoint), | |
| ); | |
| let log_endpoint = endpoint.split('?').next().unwrap_or(endpoint); | |
| host::log( | |
| host::LogLevel::Debug, | |
| &format!("Slack API: {} {}", method, log_endpoint), | |
| ); |
References
- Do not use warn! or info! logging in REPL/TUI-reachable code as they can corrupt the REPL/TUI. Use debug! logging instead, and ensure only sanitized identifiers are logged.
| pub fn list_conversations(types: &str, limit: u32) -> Result<ListConversationsResult, String> { | ||
| let url = format!( | ||
| "conversations.list?types={}&limit={}&exclude_archived=true", | ||
| url_encode(types), | ||
| limit | ||
| ); |
There was a problem hiding this comment.
To enforce defensive programming and ensure robust handling of invalid inputs, we should clamp the limit parameter to a valid range (e.g., 1 to 1000) before constructing the API URL. This prevents potential API errors or unexpected behavior if schema validation is bypassed.
| pub fn list_conversations(types: &str, limit: u32) -> Result<ListConversationsResult, String> { | |
| let url = format!( | |
| "conversations.list?types={}&limit={}&exclude_archived=true", | |
| url_encode(types), | |
| limit | |
| ); | |
| pub fn list_conversations(types: &str, limit: u32) -> Result<ListConversationsResult, String> { | |
| let limit = limit.clamp(1, 1000); | |
| let url = format!( | |
| "conversations.list?types={}&limit={}&exclude_archived=true", | |
| url_encode(types), | |
| limit | |
| ); |
References
- When implementing methods that poll for batches of data (e.g.,
poll_inputs), ensure that the caller-provided limit is clamped or bounded by a predefined maximum value (e.g.,MAX_HOST_INPUT_POLL_LIMIT) before being passed to underlying host queues or services. This prevents performance degradation from excessively large batch requests.
| pub fn get_conversation_history( | ||
| channel: &str, | ||
| limit: u32, | ||
| latest: Option<&str>, | ||
| oldest: Option<&str>, | ||
| ) -> Result<ConversationHistoryResult, String> { | ||
| let mut url = format!( | ||
| "conversations.history?channel={}&limit={}", | ||
| url_encode(channel), | ||
| limit | ||
| ); |
There was a problem hiding this comment.
To enforce defensive programming and ensure robust handling of invalid inputs, we should clamp the limit parameter to a valid range (e.g., 1 to 1000) before constructing the API URL. This prevents potential API errors or unexpected behavior if schema validation is bypassed.
pub fn get_conversation_history(
channel: &str,
limit: u32,
latest: Option<&str>,
oldest: Option<&str>,
) -> Result<ConversationHistoryResult, String> {
let limit = limit.clamp(1, 1000);
let mut url = format!(
"conversations.history?channel={}&limit={}",
url_encode(channel),
limit
);References
- When implementing methods that poll for batches of data (e.g.,
poll_inputs), ensure that the caller-provided limit is clamped or bounded by a predefined maximum value (e.g.,MAX_HOST_INPUT_POLL_LIMIT) before being passed to underlying host queues or services. This prevents performance degradation from excessively large batch requests.
There was a problem hiding this comment.
Actionable comments posted: 3
🤖 Prompt for all review comments with 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.
Inline comments:
In
`@crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/raw_output.v1.json`:
- Around line 5-6: The slack_user raw_output schema is too permissive because
the top-level object allows any properties, so validation cannot detect
malformed tool outputs. Update the raw_output.v1.json contract to use explicit
shapes instead of a free-form object by defining strict per-operation schemas
and combining them with a discriminated oneOf union, and ensure the Slack user
output schema rejects unexpected or partial payloads.
In `@crates/ironclaw_reborn_composition/src/factory.rs`:
- Line 3042: The Slack package is using the wrong ceiling helper,
`gsuite_allowed_effects()`, which ties Slack permissions to an unrelated G-Suite
helper; update the Slack manifest wiring in `factory.rs` to use a dedicated
`slack_user_allowed_effects()` instead. Add or reuse the Slack-specific helper
so it matches the manifest’s declared Slack effects, preserving current
capabilities like `external_write` without depending on future changes to the
G-Suite helper.
In `@tools-src/slack_user/src/types.rs`:
- Around line 31-33: The remaining free-form CSV field in
list_conversations.types should be replaced with a typed, validated value to
match the schema-bounded approach already used for search_messages.sort. Update
the types definition in slack_user/src/types.rs by introducing an enum or
newtype list for the allowed conversation kinds, then wire it through the
list_conversations input model so deserialization validates the values instead
of accepting arbitrary strings. Keep the JSON schema in
slack_user/list_conversations.input.v1.json aligned with the Rust type so both
sides enforce the same allowed set.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro Plus
Run ID: 4fabd8e5-38ea-48a0-a4a2-a625dbf87393
⛔ Files ignored due to path filters (1)
crates/ironclaw_first_party_extensions/assets/slack_user/wasm/slack_user_tool.wasmis excluded by!**/*.wasm,!**/*.wasm
📒 Files selected for processing (20)
crates/ironclaw_first_party_extensions/assets/slack_user/manifest.tomlcrates/ironclaw_first_party_extensions/assets/slack_user/prompts/slack_user/get_conversation_history.mdcrates/ironclaw_first_party_extensions/assets/slack_user/prompts/slack_user/get_user_info.mdcrates/ironclaw_first_party_extensions/assets/slack_user/prompts/slack_user/list_conversations.mdcrates/ironclaw_first_party_extensions/assets/slack_user/prompts/slack_user/search_messages.mdcrates/ironclaw_first_party_extensions/assets/slack_user/prompts/slack_user/send_message.mdcrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/get_conversation_history.input.v1.jsoncrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/get_user_info.input.v1.jsoncrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.jsoncrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/raw_output.v1.jsoncrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/search_messages.input.v1.jsoncrates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/send_message.input.v1.jsoncrates/ironclaw_reborn_composition/src/available_extensions.rscrates/ironclaw_reborn_composition/src/factory.rstools-src/slack_user/Cargo.tomltools-src/slack_user/README.mdtools-src/slack_user/slack_user-tool.capabilities.jsontools-src/slack_user/src/api.rstools-src/slack_user/src/lib.rstools-src/slack_user/src/types.rs
| "type": "object", | ||
| "additionalProperties": true |
There was a problem hiding this comment.
🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift
raw_output is effectively untyped, so output validation cannot catch shape regressions.
Line 5–6 accepts any object. Since this schema is registered by the host as the tool output contract, malformed/partial payloads can pass validation and break downstream assumptions. Please tighten this to explicit output shapes (e.g., per-operation schemas or a strict oneOf union).
🤖 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_first_party_extensions/assets/slack_user/schemas/slack_user/raw_output.v1.json`
around lines 5 - 6, The slack_user raw_output schema is too permissive because
the top-level object allows any properties, so validation cannot detect
malformed tool outputs. Update the raw_output.v1.json contract to use explicit
shapes instead of a free-form object by defining strict per-operation schemas
and combining them with a discriminated oneOf union, and ensure the Slack user
output schema rejects unexpected or partial payloads.
| "/system/extensions/slack_user/manifest.toml".to_string(), | ||
| Some(slack_user_manifest_digest()), | ||
| HostTrustAssignment::first_party(), | ||
| gsuite_allowed_effects(), |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value
gsuite_allowed_effects() is the wrong-named ceiling for a Slack package.
It currently happens to cover the union of slack_user effects (incl. external_write for send_message), so it's functionally correct today. But coupling Slack's effect ceiling to a G-Suite helper is fragile: any future narrowing of gsuite_allowed_effects() would silently drop a Slack capability. Prefer a dedicated slack_user_allowed_effects() matching the manifest's declared effects.
🤖 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/factory.rs` at line 3042, The Slack
package is using the wrong ceiling helper, `gsuite_allowed_effects()`, which
ties Slack permissions to an unrelated G-Suite helper; update the Slack manifest
wiring in `factory.rs` to use a dedicated `slack_user_allowed_effects()`
instead. Add or reuse the Slack-specific helper so it matches the manifest’s
declared Slack effects, preserving current capabilities like `external_write`
without depending on future changes to the G-Suite helper.
| /// Sort by `score` (relevance, default) or `timestamp` (recency). | ||
| #[serde(default)] | ||
| sort: Option<String>, |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🔵 Trivial
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify all stringly-typed bounded fields and their downstream usage before migration.
rg -n --type rust 'sort:\s*Option<String>|types:\s*String' tools-src/slack_user/src/types.rs
rg -n --type rust 'SearchMessages|ListConversations|as_deref\(' tools-src/slack_user/src/lib.rs tools-src/slack_user/src/api.rs
rg -n '"sort"|"types"' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/*.jsonRepository: nearai/ironclaw
Length of output: 1866
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/types.rs | cat -n
printf '\n--- schema: search_messages ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/search_messages.input.v1.json | cat -n
printf '\n--- schema: list_conversations ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.json | cat -nRepository: nearai/ironclaw
Length of output: 9208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/types.rs | cat -n
printf '\n--- schema: search_messages ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/search_messages.input.v1.json | cat -n
printf '\n--- schema: list_conversations ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.json | cat -nRepository: nearai/ironclaw
Length of output: 9208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/types.rs | cat -n
printf '\n--- schema: search_messages ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/search_messages.input.v1.json | cat -n
printf '\n--- schema: list_conversations ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.json | cat -nRepository: nearai/ironclaw
Length of output: 9208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/types.rs | cat -n
printf '\n--- schema: search_messages ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/search_messages.input.v1.json | cat -n
printf '\n--- schema: list_conversations ---\n'
sed -n '1,120p' crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.json | cat -nRepository: nearai/ironclaw
Length of output: 9208
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/api.rs | cat -nRepository: nearai/ironclaw
Length of output: 8885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/api.rs | cat -nRepository: nearai/ironclaw
Length of output: 8885
🏁 Script executed:
#!/bin/bash
set -euo pipefail
sed -n '1,220p' tools-src/slack_user/src/api.rs | cat -nRepository: nearai/ironclaw
Length of output: 8885
Make list_conversations.types a typed value
search_messages.sort is already schema-bounded; the remaining gap is tools-src/slack_user/src/types.rs:39-42 / crates/ironclaw_first_party_extensions/assets/slack_user/schemas/slack_user/list_conversations.input.v1.json:7, where types is still a free-form CSV string. Model the allowed conversation kinds as a validated enum/newtype list and keep the schema aligned.
🤖 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 `@tools-src/slack_user/src/types.rs` around lines 31 - 33, The remaining
free-form CSV field in list_conversations.types should be replaced with a typed,
validated value to match the schema-bounded approach already used for
search_messages.sort. Update the types definition in slack_user/src/types.rs by
introducing an enum or newtype list for the allowed conversation kinds, then
wire it through the list_conversations input model so deserialization validates
the values instead of accepting arbitrary strings. Keep the JSON schema in
slack_user/list_conversations.input.v1.json aligned with the Rust type so both
sides enforce the same allowed set.
Source: Coding guidelines
What
Ports the bot-token-free Slack personal tool (originally #4941, old architecture) to the Reborn extension architecture. It lets IronClaw act as the user via a Slack user token (
xoxp-) — read full message history, DMs, group DMs, and search — things a bot token fundamentally cannot do.Distinct from the bot Slack channel: it has its own credential, so the personal token never collides with
slack_bot_token.Capabilities
slack_user.{search_messages, list_conversations, get_conversation_history, get_user_info, send_message}How it's built (mirrors the google-sheets / github WASM tools)
tools-src/slack_user/—sandboxed-toolWIT, context-dispatched (action derived fromcapability_id, action-less per-capability params).reborn.extension_manifest.v2atcrates/ironclaw_first_party_extensions/assets/slack_user/(manifest + embeddedwasm/+ per-capability input schemas + prompt docs).ironclaw_reborn_composition:available_extensions.rscatalog +factory.rsfirst-party trust policy. Gated behindslack-v2-host-beta, like the bot Slack extension.Credential model (two deliberate choices)
product_auth_accountwith defaultManualTokensetup (mirrors the github PAT tool), notsecret_handle. The WebUI manual-token auth-gate save path keys on a provider; a provider-lesssecret_handlecredential makes the gate fail with "Could not save the token."slack_personal(notslack) so the user-token credential is a separate account from the bot Slack extension (idslack) and itsIRONCLAW_REBORN_SLACK_BOT_TOKENconfig — the personal tool never picks up the bot token. Bearer-injected toslack.com; no OAuth provider required.Auth / setup
Manual token: create a private Slack app, add User Token Scopes (
search:read,channels:history,groups:history,im:history,mpim:history,*:read,users:read, optionalchat:write), install, and paste the resultingxoxp-user token into the extension's auth gate.Testing
cargo component build --target wasm32-wasip2→ clean component; tool clippy/fmt clean.RUSTFLAGS=-D warnings cargo build -p ironclaw_reborn_cli --features webui-v2-beta,slack-v2-host-beta→ clean.ironclaw_reborn_compositioncatalog + trust-policy tests pass (manifest parses, registers, digest pinned).serve: the auth gate saves thexoxp-token andsearch_messagesauthorizes as the user.🤖 Generated with Claude Code