Skip to content

Commit 9764daa

Browse files
authored
fix(slack): route install target lookup to backend fastapi (#222)
1 parent edd0880 commit 9764daa

5 files changed

Lines changed: 147 additions & 47 deletions

File tree

integrations/slack-gateway/.env.example

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ SPRITZ_SLACK_BOT_SCOPES=app_mentions:read,channels:history,chat:write,im:history
1717
SPRITZ_SLACK_PRESET_ID=zeno
1818

1919
SPRITZ_SLACK_BACKEND_BASE_URL=https://backend.example.com
20+
SPRITZ_SLACK_BACKEND_FASTAPI_BASE_URL=https://backend-fastapi.example.com
2021
SPRITZ_SLACK_BACKEND_INTERNAL_TOKEN=fill-me
2122

2223
SPRITZ_SLACK_SPRITZ_BASE_URL=https://spritz.example.com

integrations/slack-gateway/backend_client.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -225,7 +225,7 @@ func (g *slackGateway) listInstallTargets(ctx context.Context, installation *sla
225225
"requestId": strings.TrimSpace(requestID),
226226
}
227227
var payload backendInstallTargetListResponse
228-
if err := g.postBackendJSON(ctx, "/internal/v2/spritz/channel-install-targets/list", body, &payload); err != nil {
228+
if err := g.postBackendFastAPIJSON(ctx, "/internal/v2/spritz/channel-install-targets/list", body, &payload); err != nil {
229229
return nil, err
230230
}
231231
if payload.Status != "resolved" {
@@ -328,6 +328,14 @@ func (g *slackGateway) postBackendJSON(ctx context.Context, path string, body an
328328
return g.postJSON(ctx, g.cfg.BackendBaseURL+path, g.cfg.BackendInternalToken, body, target)
329329
}
330330

331+
func (g *slackGateway) postBackendFastAPIJSON(ctx context.Context, path string, body any, target any) error {
332+
baseURL := g.cfg.BackendFastAPIBaseURL
333+
if strings.TrimSpace(baseURL) == "" {
334+
baseURL = g.cfg.BackendBaseURL
335+
}
336+
return g.postJSON(ctx, baseURL+path, g.cfg.BackendInternalToken, body, target)
337+
}
338+
331339
func (g *slackGateway) postSpritzJSON(ctx context.Context, method, path, bearer string, body any, target any, query map[string]string) error {
332340
endpoint := g.cfg.SpritzBaseURL + path
333341
if len(query) > 0 {

integrations/slack-gateway/backend_client_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,68 @@ import (
1111
"time"
1212
)
1313

14+
func TestListInstallTargetsUsesBackendFastAPIBaseURLWhenConfigured(t *testing.T) {
15+
backendHits := 0
16+
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
17+
backendHits++
18+
t.Fatalf("unexpected backend base request to %s", r.URL.Path)
19+
}))
20+
defer backend.Close()
21+
22+
fastapiHits := 0
23+
fastapi := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
24+
if r.URL.Path != "/internal/v2/spritz/channel-install-targets/list" {
25+
t.Fatalf("unexpected fastapi path %s", r.URL.Path)
26+
}
27+
fastapiHits++
28+
writeJSON(w, http.StatusOK, map[string]any{
29+
"status": "resolved",
30+
"targets": []map[string]any{
31+
{
32+
"id": "ag_workspace",
33+
"profile": map[string]any{
34+
"name": "Workspace Helper",
35+
},
36+
"presetInputs": map[string]any{
37+
"agentId": "ag_workspace",
38+
},
39+
},
40+
},
41+
})
42+
}))
43+
defer fastapi.Close()
44+
45+
gateway := newSlackGateway(config{
46+
BackendBaseURL: backend.URL,
47+
BackendFastAPIBaseURL: fastapi.URL,
48+
BackendInternalToken: "backend-internal-token",
49+
PresetID: "zeno",
50+
PrincipalID: "shared-slack-gateway",
51+
HTTPTimeout: 5 * time.Second,
52+
}, slog.New(slog.NewTextHandler(io.Discard, nil)))
53+
54+
targets, err := gateway.listInstallTargets(
55+
context.Background(),
56+
&slackInstallation{
57+
TeamID: "T_workspace_1",
58+
InstallingUserID: "U_installer",
59+
},
60+
"req_install_1",
61+
)
62+
if err != nil {
63+
t.Fatalf("list install targets: %v", err)
64+
}
65+
if backendHits != 0 {
66+
t.Fatalf("expected backend base URL to be unused, got %d hits", backendHits)
67+
}
68+
if fastapiHits != 1 {
69+
t.Fatalf("expected fastapi base URL to be hit once, got %d", fastapiHits)
70+
}
71+
if len(targets) != 1 || targets[0].ID != "ag_workspace" {
72+
t.Fatalf("unexpected install targets: %#v", targets)
73+
}
74+
}
75+
1476
func TestUpsertChannelConversationOmitsImplicitDefaultCWD(t *testing.T) {
1577
var upsertPayload map[string]any
1678
spritz := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

integrations/slack-gateway/config.go

Lines changed: 51 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,58 @@ import (
99
)
1010

1111
type config struct {
12-
Addr string
13-
PublicURL string
14-
SlackClientID string
15-
SlackClientSecret string
16-
SlackSigningSecret string
17-
OAuthStateSecret string
18-
SlackAPIBaseURL string
19-
SlackBotScopes []string
20-
PresetID string
21-
BackendBaseURL string
22-
BackendInternalToken string
23-
SpritzBaseURL string
24-
SpritzServiceToken string
25-
PrincipalID string
26-
HTTPTimeout time.Duration
27-
DedupeTTL time.Duration
28-
ProcessingTimeout time.Duration
29-
SessionRetryInterval time.Duration
30-
StatusMessageDelay time.Duration
31-
RecoveryTimeout time.Duration
32-
PromptRetryInitial time.Duration
33-
PromptRetryMax time.Duration
34-
PromptRetryTimeout time.Duration
12+
Addr string
13+
PublicURL string
14+
SlackClientID string
15+
SlackClientSecret string
16+
SlackSigningSecret string
17+
OAuthStateSecret string
18+
SlackAPIBaseURL string
19+
SlackBotScopes []string
20+
PresetID string
21+
BackendBaseURL string
22+
BackendFastAPIBaseURL string
23+
BackendInternalToken string
24+
SpritzBaseURL string
25+
SpritzServiceToken string
26+
PrincipalID string
27+
HTTPTimeout time.Duration
28+
DedupeTTL time.Duration
29+
ProcessingTimeout time.Duration
30+
SessionRetryInterval time.Duration
31+
StatusMessageDelay time.Duration
32+
RecoveryTimeout time.Duration
33+
PromptRetryInitial time.Duration
34+
PromptRetryMax time.Duration
35+
PromptRetryTimeout time.Duration
3536
}
3637

3738
func loadConfig() (config, error) {
3839
cfg := config{
39-
Addr: envOrDefault("SPRITZ_SLACK_GATEWAY_ADDR", ":8080"),
40-
PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_GATEWAY_PUBLIC_URL")), "/"),
41-
SlackClientID: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_CLIENT_ID")),
42-
SlackClientSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_CLIENT_SECRET")),
43-
SlackSigningSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SIGNING_SECRET")),
44-
OAuthStateSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_OAUTH_STATE_SECRET")),
45-
SlackAPIBaseURL: strings.TrimRight(envOrDefault("SPRITZ_SLACK_API_BASE_URL", "https://slack.com/api"), "/"),
46-
SlackBotScopes: splitCSV(envOrDefault("SPRITZ_SLACK_BOT_SCOPES", "app_mentions:read,channels:history,chat:write,im:history,mpim:history")),
47-
PresetID: strings.TrimSpace(envOrDefault("SPRITZ_SLACK_PRESET_ID", defaultSlackPresetID)),
48-
BackendBaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_BACKEND_BASE_URL")), "/"),
49-
BackendInternalToken: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_BACKEND_INTERNAL_TOKEN")),
50-
SpritzBaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SPRITZ_BASE_URL")), "/"),
51-
SpritzServiceToken: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SPRITZ_SERVICE_TOKEN")),
52-
PrincipalID: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_PRINCIPAL_ID")),
53-
HTTPTimeout: parseDurationEnv("SPRITZ_SLACK_HTTP_TIMEOUT", 15*time.Second),
54-
DedupeTTL: parseDurationEnv("SPRITZ_SLACK_DEDUPE_TTL", 10*time.Minute),
55-
ProcessingTimeout: parseDurationEnv("SPRITZ_SLACK_PROCESSING_TIMEOUT", 120*time.Second),
56-
SessionRetryInterval: parseDurationEnv("SPRITZ_SLACK_SESSION_RETRY_INTERVAL", time.Second),
57-
StatusMessageDelay: parseDurationEnv("SPRITZ_SLACK_STATUS_MESSAGE_DELAY", 5*time.Second),
58-
RecoveryTimeout: parseDurationEnv("SPRITZ_SLACK_RECOVERY_TIMEOUT", 120*time.Second),
59-
PromptRetryInitial: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_INITIAL", 250*time.Millisecond),
60-
PromptRetryMax: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_MAX", 2*time.Second),
61-
PromptRetryTimeout: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_TIMEOUT", 8*time.Second),
40+
Addr: envOrDefault("SPRITZ_SLACK_GATEWAY_ADDR", ":8080"),
41+
PublicURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_GATEWAY_PUBLIC_URL")), "/"),
42+
SlackClientID: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_CLIENT_ID")),
43+
SlackClientSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_CLIENT_SECRET")),
44+
SlackSigningSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SIGNING_SECRET")),
45+
OAuthStateSecret: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_OAUTH_STATE_SECRET")),
46+
SlackAPIBaseURL: strings.TrimRight(envOrDefault("SPRITZ_SLACK_API_BASE_URL", "https://slack.com/api"), "/"),
47+
SlackBotScopes: splitCSV(envOrDefault("SPRITZ_SLACK_BOT_SCOPES", "app_mentions:read,channels:history,chat:write,im:history,mpim:history")),
48+
PresetID: strings.TrimSpace(envOrDefault("SPRITZ_SLACK_PRESET_ID", defaultSlackPresetID)),
49+
BackendBaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_BACKEND_BASE_URL")), "/"),
50+
BackendFastAPIBaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_BACKEND_FASTAPI_BASE_URL")), "/"),
51+
BackendInternalToken: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_BACKEND_INTERNAL_TOKEN")),
52+
SpritzBaseURL: strings.TrimRight(strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SPRITZ_BASE_URL")), "/"),
53+
SpritzServiceToken: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_SPRITZ_SERVICE_TOKEN")),
54+
PrincipalID: strings.TrimSpace(os.Getenv("SPRITZ_SLACK_PRINCIPAL_ID")),
55+
HTTPTimeout: parseDurationEnv("SPRITZ_SLACK_HTTP_TIMEOUT", 15*time.Second),
56+
DedupeTTL: parseDurationEnv("SPRITZ_SLACK_DEDUPE_TTL", 10*time.Minute),
57+
ProcessingTimeout: parseDurationEnv("SPRITZ_SLACK_PROCESSING_TIMEOUT", 120*time.Second),
58+
SessionRetryInterval: parseDurationEnv("SPRITZ_SLACK_SESSION_RETRY_INTERVAL", time.Second),
59+
StatusMessageDelay: parseDurationEnv("SPRITZ_SLACK_STATUS_MESSAGE_DELAY", 5*time.Second),
60+
RecoveryTimeout: parseDurationEnv("SPRITZ_SLACK_RECOVERY_TIMEOUT", 120*time.Second),
61+
PromptRetryInitial: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_INITIAL", 250*time.Millisecond),
62+
PromptRetryMax: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_MAX", 2*time.Second),
63+
PromptRetryTimeout: parseDurationEnv("SPRITZ_SLACK_PROMPT_RETRY_TIMEOUT", 8*time.Second),
6264
}
6365

6466
if cfg.PublicURL == "" {
@@ -86,6 +88,9 @@ func loadConfig() (config, error) {
8688
if cfg.BackendBaseURL == "" {
8789
return config{}, fmt.Errorf("SPRITZ_SLACK_BACKEND_BASE_URL is required")
8890
}
91+
if cfg.BackendFastAPIBaseURL == "" {
92+
cfg.BackendFastAPIBaseURL = cfg.BackendBaseURL
93+
}
8994
if cfg.BackendInternalToken == "" {
9095
return config{}, fmt.Errorf("SPRITZ_SLACK_BACKEND_INTERNAL_TOKEN is required")
9196
}

integrations/slack-gateway/gateway_test.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6117,6 +6117,30 @@ func TestLoadConfigIncludesMPIMHistoryByDefault(t *testing.T) {
61176117
}
61186118
}
61196119

6120+
func TestLoadConfigDefaultsBackendFastAPIBaseURLToBackendBaseURL(t *testing.T) {
6121+
t.Setenv("SPRITZ_SLACK_GATEWAY_PUBLIC_URL", "https://gateway.example.test")
6122+
t.Setenv("SPRITZ_SLACK_CLIENT_ID", "client-id")
6123+
t.Setenv("SPRITZ_SLACK_CLIENT_SECRET", "client-secret")
6124+
t.Setenv("SPRITZ_SLACK_SIGNING_SECRET", "signing-secret")
6125+
t.Setenv("SPRITZ_SLACK_OAUTH_STATE_SECRET", "oauth-state-secret")
6126+
t.Setenv("SPRITZ_SLACK_BACKEND_BASE_URL", "https://backend.example.test")
6127+
t.Setenv("SPRITZ_SLACK_BACKEND_INTERNAL_TOKEN", "backend-internal-token")
6128+
t.Setenv("SPRITZ_SLACK_SPRITZ_BASE_URL", "https://spritz.example.test")
6129+
t.Setenv("SPRITZ_SLACK_SPRITZ_SERVICE_TOKEN", "spritz-service-token")
6130+
t.Setenv("SPRITZ_SLACK_PRINCIPAL_ID", "shared-slack-gateway")
6131+
6132+
cfg, err := loadConfig()
6133+
if err != nil {
6134+
t.Fatalf("loadConfig failed: %v", err)
6135+
}
6136+
if cfg.BackendFastAPIBaseURL != "https://backend.example.test" {
6137+
t.Fatalf(
6138+
"expected backend fastapi base url fallback to match backend base url, got %q",
6139+
cfg.BackendFastAPIBaseURL,
6140+
)
6141+
}
6142+
}
6143+
61206144
func TestSpritzWebSocketURLPreservesBasePath(t *testing.T) {
61216145
gateway := newSlackGateway(
61226146
config{SpritzBaseURL: "https://spritz.example.test/prefix"},

0 commit comments

Comments
 (0)