mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
fix(config): disable Slack unless configured
Require an explicit server.integrations.slack table before Slack reports enabled or starts from vault tokens.
This commit is contained in:
parent
160f587a1d
commit
e952bb4c7f
7 changed files with 107 additions and 10 deletions
|
|
@ -444,9 +444,9 @@ GitHub App mode stores these secrets in the vault. `fabro install` writes them a
|
|||
|
||||
### Slack integration (optional)
|
||||
|
||||
Slack credentials are server-level secrets. They enable one Slack connection that is shared by human interview prompts and run lifecycle notifications. `server.integrations.slack.default_channel` is optional and is used only as the default destination for interview prompts; lifecycle notifications use `[run.notifications.<name>.slack].channel` in run or workflow configuration.
|
||||
Slack credentials are server-level secrets. Add `[server.integrations.slack]` to enable one Slack connection that is shared by human interview prompts and run lifecycle notifications. `server.integrations.slack.default_channel` is optional and is used only as the default destination for interview prompts; lifecycle notifications use `[run.notifications.<name>.slack].channel` in run or workflow configuration.
|
||||
|
||||
Fabro resolves these from the vault only. If both are present, startup logs `Slack integration enabled` and then the Slack Socket Mode connection status. If either is missing or empty, startup logs `Slack integration disabled; missing credentials` with the missing variable names.
|
||||
Fabro resolves these from the vault only. When `[server.integrations.slack]` is present and both credentials are present, startup logs `Slack integration enabled` and then the Slack Socket Mode connection status. If the Slack config table is absent or `enabled = false`, startup logs `Slack integration disabled by server configuration`. If the table is present but either credential is missing or empty, startup logs `Slack integration disabled; missing credentials` with the missing variable names.
|
||||
|
||||
```bash
|
||||
fabro secret set FABRO_SLACK_BOT_TOKEN xoxb-...
|
||||
|
|
|
|||
|
|
@ -94,7 +94,7 @@ If the pending session disappears before an answer is submitted, the waiting que
|
|||
|
||||
Fabro's [Slack integration](/integrations/slack) uses the web interviewer under the hood. When a human gate fires, the pending question is rendered as a Slack message with interactive buttons. When a user clicks a button, the Slack event handler calls `submit_answer()` on the web interviewer, unblocking the workflow.
|
||||
|
||||
Slack interview prompts require Slack server credentials and a destination channel. Most servers should configure `server.integrations.slack.default_channel`; lifecycle notifications use their own per-route channels instead.
|
||||
Slack interview prompts require Slack server credentials, an enabled `[server.integrations.slack]` table, and a destination channel. Most servers should configure `server.integrations.slack.default_channel`; lifecycle notifications use their own per-route channels instead.
|
||||
|
||||
### Auto-approve
|
||||
|
||||
|
|
|
|||
|
|
@ -109,10 +109,11 @@ The server runtime does not read Slack tokens from process env or `server.env`.
|
|||
|
||||
Restart the server after changing Slack credentials so the Socket Mode connection is recreated with the new tokens.
|
||||
|
||||
Optionally, set a default interview channel in your [server configuration](/administration/server-configuration):
|
||||
Enable Slack in your [server configuration](/administration/server-configuration). You can leave `default_channel` out if you only use per-route lifecycle notification channels:
|
||||
|
||||
```toml title="settings.toml"
|
||||
[server.integrations.slack]
|
||||
enabled = true
|
||||
default_channel = "#fabro-reviews"
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -330,7 +330,10 @@ fn resolve_integrations(layer: Option<&ServerIntegrationsLayer>) -> ServerIntegr
|
|||
enabled: slack.enabled.unwrap_or(true),
|
||||
default_channel: slack.default_channel.clone(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
.unwrap_or(SlackIntegrationSettings {
|
||||
enabled: false,
|
||||
default_channel: None,
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ fn resolves_server_defaults_from_empty_settings() {
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_server_integrations_are_slack_only_for_chat() {
|
||||
fn resolved_server_integrations_disable_slack_when_config_is_absent() {
|
||||
let settings = resolve_server(&empty_settings_with_auth_methods());
|
||||
|
||||
let integrations =
|
||||
|
|
@ -128,13 +128,27 @@ fn resolved_server_integrations_are_slack_only_for_chat() {
|
|||
"webhooks": null,
|
||||
},
|
||||
"slack": {
|
||||
"enabled": true,
|
||||
"enabled": false,
|
||||
"default_channel": null,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolved_server_integrations_enable_slack_when_config_is_present() {
|
||||
let settings = resolve_server(&parse(
|
||||
r"
|
||||
_version = 1
|
||||
|
||||
[server.integrations.slack]
|
||||
",
|
||||
));
|
||||
|
||||
assert!(settings.integrations.slack.enabled);
|
||||
assert!(settings.integrations.slack.default_channel.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn server_sandbox_defaults_all_providers_enabled() {
|
||||
let settings = ServerSettingsBuilder::from_toml(
|
||||
|
|
|
|||
|
|
@ -1929,6 +1929,18 @@ fn server_secrets_resolve_bootstrap_process_env_before_server_env() {
|
|||
fn slack_app_state_with_secret_sources(
|
||||
vault_entries: &[(&str, &str, SecretType)],
|
||||
server_secret_env: HashMap<String, String>,
|
||||
) -> Arc<AppState> {
|
||||
slack_app_state_with_settings_and_secret_sources(
|
||||
default_test_server_settings(),
|
||||
vault_entries,
|
||||
server_secret_env,
|
||||
)
|
||||
}
|
||||
|
||||
fn slack_app_state_with_settings_and_secret_sources(
|
||||
settings: ServerSettings,
|
||||
vault_entries: &[(&str, &str, SecretType)],
|
||||
server_secret_env: HashMap<String, String>,
|
||||
) -> Arc<AppState> {
|
||||
let (store, artifact_store) = test_store_bundle();
|
||||
let vault_path = test_secret_store_path();
|
||||
|
|
@ -1939,7 +1951,7 @@ fn slack_app_state_with_secret_sources(
|
|||
}
|
||||
build_app_state(AppStateConfig {
|
||||
resolved_settings: resolved_runtime_settings_for_tests(
|
||||
default_test_server_settings(),
|
||||
settings,
|
||||
RunLayer::default(),
|
||||
LlmCatalogSettings::default(),
|
||||
),
|
||||
|
|
@ -1965,7 +1977,7 @@ fn slack_app_state_with_secret_sources(
|
|||
}
|
||||
|
||||
#[test]
|
||||
fn slack_service_is_enabled_by_vault_tokens() {
|
||||
fn slack_service_ignores_vault_tokens_when_config_is_absent() {
|
||||
let state = slack_app_state_with_secret_sources(
|
||||
&[
|
||||
(
|
||||
|
|
@ -1982,10 +1994,42 @@ fn slack_service_is_enabled_by_vault_tokens() {
|
|||
HashMap::new(),
|
||||
);
|
||||
|
||||
assert!(state.slack_service.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn slack_service_is_enabled_by_config_and_vault_tokens() {
|
||||
let state = slack_app_state_with_settings_and_secret_sources(
|
||||
server_settings_from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[server.auth]
|
||||
methods = ["dev-token"]
|
||||
|
||||
[server.integrations.slack]
|
||||
enabled = true
|
||||
"#,
|
||||
),
|
||||
&[
|
||||
(
|
||||
EnvVars::FABRO_SLACK_BOT_TOKEN,
|
||||
"xoxb-test",
|
||||
SecretType::Token,
|
||||
),
|
||||
(
|
||||
EnvVars::FABRO_SLACK_APP_TOKEN,
|
||||
"xapp-test",
|
||||
SecretType::Token,
|
||||
),
|
||||
],
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
let service = state
|
||||
.slack_service
|
||||
.as_ref()
|
||||
.expect("slack service should be enabled by vault tokens");
|
||||
.expect("slack service should be enabled by config and vault tokens");
|
||||
let connection = service.connection_status();
|
||||
assert_eq!(connection.kind, IntegrationConnectionKind::SocketMode);
|
||||
assert_eq!(connection.status, IntegrationConnectionState::Connecting);
|
||||
|
|
|
|||
|
|
@ -145,6 +145,41 @@ async fn get_system_info_returns_runtime_fields() {
|
|||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_system_integrations_reports_slack_disabled_when_config_is_absent() {
|
||||
let settings = settings_from_toml(
|
||||
r"
|
||||
_version = 1
|
||||
",
|
||||
);
|
||||
let app = fabro_server::test_support::build_test_router(
|
||||
fabro_server::test_support::TestAppStateBuilder::new()
|
||||
.runtime_settings(settings.server_settings, settings.manifest_run_defaults)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let request = Request::builder()
|
||||
.method("GET")
|
||||
.uri(api("/system/integrations"))
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
let response = app.oneshot(request).await.unwrap();
|
||||
|
||||
let body = response_json(response, StatusCode::OK, "GET /api/v1/system/integrations").await;
|
||||
let slack = body["data"]
|
||||
.as_array()
|
||||
.expect("integration response should include data")
|
||||
.iter()
|
||||
.find(|integration| integration["provider"] == "slack")
|
||||
.expect("slack integration should be present");
|
||||
|
||||
assert_eq!(slack["enabled"], false);
|
||||
assert_eq!(slack["configured"], false);
|
||||
assert_eq!(slack["status"], "disabled");
|
||||
assert_eq!(slack["missing_credentials"], serde_json::json!([]));
|
||||
assert!(slack["connection"].is_null());
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn get_system_integrations_reports_slack_missing_credentials_when_allowed() {
|
||||
let settings = settings_from_toml(
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue