From b33c1043e0556a69961f9333efe2ba79b650fd47 Mon Sep 17 00:00:00 2001 From: Dongkeun Lee Date: Thu, 27 Aug 2026 01:26:33 -0400 Subject: [PATCH 1/2] ops(crosscheck): add reproducible Slack app manifest --- docs/crosscheck-slack.md | 25 +++++++++++++++--- ops/slack-crosscheck/.slack/hooks.json | 5 ++++ ops/slack-crosscheck/get-manifest.sh | 5 ++++ ops/slack-crosscheck/manifest.json | 35 ++++++++++++++++++++++++++ tests/fm-crosscheck-slack.test.sh | 28 +++++++++++++++++++++ 5 files changed, 94 insertions(+), 4 deletions(-) create mode 100644 ops/slack-crosscheck/.slack/hooks.json create mode 100755 ops/slack-crosscheck/get-manifest.sh create mode 100644 ops/slack-crosscheck/manifest.json diff --git a/docs/crosscheck-slack.md b/docs/crosscheck-slack.md index 561aa263766..ec205778afc 100644 --- a/docs/crosscheck-slack.md +++ b/docs/crosscheck-slack.md @@ -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 --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. diff --git a/ops/slack-crosscheck/.slack/hooks.json b/ops/slack-crosscheck/.slack/hooks.json new file mode 100644 index 00000000000..f8a9277f288 --- /dev/null +++ b/ops/slack-crosscheck/.slack/hooks.json @@ -0,0 +1,5 @@ +{ + "hooks": { + "get-manifest": "./get-manifest.sh" + } +} diff --git a/ops/slack-crosscheck/get-manifest.sh b/ops/slack-crosscheck/get-manifest.sh new file mode 100755 index 00000000000..e4f8a534cc4 --- /dev/null +++ b/ops/slack-crosscheck/get-manifest.sh @@ -0,0 +1,5 @@ +#!/usr/bin/env sh +set -eu + +SCRIPT_DIR=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd) +exec /bin/cat "$SCRIPT_DIR/manifest.json" diff --git a/ops/slack-crosscheck/manifest.json b/ops/slack-crosscheck/manifest.json new file mode 100644 index 00000000000..b49ccb48ab0 --- /dev/null +++ b/ops/slack-crosscheck/manifest.json @@ -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 + } +} diff --git a/tests/fm-crosscheck-slack.test.sh b/tests/fm-crosscheck-slack.test.sh index 563783b87c2..9aabd5b53af 100755 --- a/tests/fm-crosscheck-slack.test.sh +++ b/tests/fm-crosscheck-slack.test.sh @@ -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 \ @@ -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 From aae573c1634d96d70b8b59ed4f7be412b38ac4d7 Mon Sep 17 00:00:00 2001 From: Dongkeun Lee Date: Thu, 27 Aug 2026 01:31:27 -0400 Subject: [PATCH 2/2] chore(crosscheck): ignore Slack CLI app state --- ops/slack-crosscheck/.slack/.gitignore | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 ops/slack-crosscheck/.slack/.gitignore diff --git a/ops/slack-crosscheck/.slack/.gitignore b/ops/slack-crosscheck/.slack/.gitignore new file mode 100644 index 00000000000..c25ad253bce --- /dev/null +++ b/ops/slack-crosscheck/.slack/.gitignore @@ -0,0 +1,4 @@ +/apps.json +/apps.dev.json +/cache/ +/config.json