Skip to content
Merged
Show file tree
Hide file tree
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
25 changes: 21 additions & 4 deletions docs/crosscheck-slack.md
Original file line number Diff line number Diff line change
Expand Up @@ -149,10 +149,27 @@ The listener logs only its nonsecret key ID.

## Credentials and app permissions

The Slack app must have Socket Mode enabled.
Its app-level token needs `connections:write`.
Its bot token needs `app_mentions:read`, `chat:write`, `channels:history`, and `reactions:write`.
Subscribe the app to `app_mention`, install it to the workspace, and invite it only to approved channels.
The repository-owned Slack app manifest is in `ops/slack-crosscheck`.
It enables Socket Mode, subscribes only to `app_mention`, and requests only `app_mentions:read`, `chat:write`, and `reactions:write` for the bot.
Create or update the app through the official Slack CLI from that directory:

```sh
cd ops/slack-crosscheck
$HOME/.slack/bin/slack manifest validate
$HOME/.slack/bin/slack app install --team <team-id> --environment deployed
```

The app-level token is created in the app's Basic Information settings with only `connections:write`.
No Request URL is configured.
Install the app to the workspace and invite it only to the exact channels in `channel_allowlist`.

Store credentials without putting values in shell history by running each command and entering the value only at the Keychain prompt:

```sh
security add-generic-password -U -a "$USER" -s firstmate-crosscheck-slack-app -w
security add-generic-password -U -a "$USER" -s firstmate-crosscheck-slack-bot -w
security add-generic-password -U -a "$USER" -s firstmate-crosscheck-github-read -w
```

The GitHub credential must be a read-only GitHub App installation token or fine-grained credential limited to the repositories in `repo_allowlist`.
It needs pull request metadata and repository contents read access.
Expand Down
4 changes: 4 additions & 0 deletions ops/slack-crosscheck/.slack/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
/apps.json
/apps.dev.json
/cache/
/config.json
5 changes: 5 additions & 0 deletions ops/slack-crosscheck/.slack/hooks.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"hooks": {
"get-manifest": "./get-manifest.sh"
}
}
5 changes: 5 additions & 0 deletions ops/slack-crosscheck/get-manifest.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#!/usr/bin/env sh
set -eu

SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd)
exec /bin/cat "$SCRIPT_DIR/manifest.json"
35 changes: 35 additions & 0 deletions ops/slack-crosscheck/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"_metadata": {
"major_version": 1
},
"display_information": {
"name": "Crosscheck",
"description": "Internal exact-head pull request reviews"
},
"features": {
"bot_user": {
"display_name": "Crosscheck",
"always_online": false
}
},
"oauth_config": {
"scopes": {
"bot": [
"app_mentions:read",
"chat:write",
"reactions:write"
]
}
},
"settings": {
"event_subscriptions": {
"bot_events": [
"app_mention"
]
},
"org_deploy_enabled": false,
"socket_mode_enabled": true,
"is_hosted": false,
"token_rotation_enabled": false
}
}
28 changes: 28 additions & 0 deletions tests/fm-crosscheck-slack.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -583,6 +583,33 @@ json.dump(value, open(sys.argv[2], "w"))' "$CONFIG_MAIN" "$bad"
pass "selftest validates config shape and refuses malformed configs by name"
}

test_slack_app_manifest_is_minimal_and_socket_only() {
app_dir="$ROOT/ops/slack-crosscheck"
output=$("$app_dir/get-manifest.sh" --source="$app_dir" 2>&1) \
|| fail "Slack manifest hook failed: $output"
APP_MANIFEST_JSON="$output" "$PYTHON" - <<'PYTEST' \
|| fail "Slack app manifest contract failed"
import json
import os

manifest = json.loads(os.environ["APP_MANIFEST_JSON"])
assert manifest["settings"] == {
"event_subscriptions": {"bot_events": ["app_mention"]},
"org_deploy_enabled": False,
"socket_mode_enabled": True,
"is_hosted": False,
"token_rotation_enabled": False,
}
assert manifest["oauth_config"]["scopes"]["bot"] == [
"app_mentions:read",
"chat:write",
"reactions:write",
]
assert "request_url" not in json.dumps(manifest).lower()
PYTEST
pass "Slack app manifest is outbound-only and least-privileged"
}

test_missing_token_env_refuses_start() {
output=$(perl -e 'alarm 60; exec @ARGV' -- \
env -u FM_TEST_SLACK_APP_TOKEN -u FM_TEST_SLACK_BOT_TOKEN -u FM_TEST_GITHUB_READ_TOKEN \
Expand Down Expand Up @@ -1389,6 +1416,7 @@ test_tokens_never_reach_logs_or_ledgers() {
UNITS=(
test_pr_link_extraction
test_selftest_validates_config_shape
test_slack_app_manifest_is_minimal_and_socket_only
test_missing_token_env_refuses_start
test_attestation_cli_derives_and_signs_author_identity
test_mention_without_link_gets_usage_reply
Expand Down
Loading