-
Notifications
You must be signed in to change notification settings - Fork 11
feat(kiloclaw): derive config endpoint secret status from catalog #1033
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| import { describe, expect, it } from 'vitest'; | ||
| import { buildConfiguredSecrets } from './kiloclaw'; | ||
|
|
||
| describe('buildConfiguredSecrets', () => { | ||
| const envelope = { | ||
| encryptedData: 'x', | ||
| encryptedDEK: 'y', | ||
| algorithm: 'rsa-aes-256-gcm', | ||
| version: 1, | ||
| }; | ||
|
|
||
| it('returns all entries as false when no secrets are configured', () => { | ||
| const result = buildConfiguredSecrets({}); | ||
| expect(result).toEqual({ telegram: false, discord: false, slack: false }); | ||
| }); | ||
|
|
||
| it('marks entry as configured when encryptedSecrets has the env var key', () => { | ||
| const result = buildConfiguredSecrets({ | ||
| encryptedSecrets: { TELEGRAM_BOT_TOKEN: envelope }, | ||
| }); | ||
| expect(result.telegram).toBe(true); | ||
| expect(result.discord).toBe(false); | ||
| expect(result.slack).toBe(false); | ||
| }); | ||
|
|
||
| it('marks multi-field entry as configured only when ALL fields are present', () => { | ||
| const partial = buildConfiguredSecrets({ | ||
| encryptedSecrets: { SLACK_BOT_TOKEN: envelope }, | ||
| }); | ||
| expect(partial.slack).toBe(false); | ||
|
|
||
| const full = buildConfiguredSecrets({ | ||
| encryptedSecrets: { SLACK_BOT_TOKEN: envelope, SLACK_APP_TOKEN: envelope }, | ||
| }); | ||
| expect(full.slack).toBe(true); | ||
| }); | ||
|
|
||
| it('falls back to legacy channels storage when encryptedSecrets is absent', () => { | ||
| const result = buildConfiguredSecrets({ | ||
| channels: { telegramBotToken: envelope, discordBotToken: envelope }, | ||
| }); | ||
| expect(result.telegram).toBe(true); | ||
| expect(result.discord).toBe(true); | ||
| expect(result.slack).toBe(false); | ||
| }); | ||
|
|
||
| it('prefers encryptedSecrets over legacy channels', () => { | ||
| const result = buildConfiguredSecrets({ | ||
| encryptedSecrets: { TELEGRAM_BOT_TOKEN: envelope }, | ||
| channels: { telegramBotToken: envelope, discordBotToken: envelope }, | ||
| }); | ||
| expect(result.telegram).toBe(true); | ||
| expect(result.discord).toBe(true); | ||
| }); | ||
|
|
||
| it('handles legacy channels with all slack fields', () => { | ||
| const result = buildConfiguredSecrets({ | ||
| channels: { slackBotToken: envelope, slackAppToken: envelope }, | ||
| }); | ||
| expect(result.slack).toBe(true); | ||
| }); | ||
|
|
||
| it('does not use legacy channels fallback for non-channel category entries', () => { | ||
| // If a non-channel entry were added, legacy channels storage should not count | ||
| // This tests that CHANNEL_FIELD_KEYS gate is effective — a key not in the | ||
| // channel category won't match even if present in config.channels | ||
| const result = buildConfiguredSecrets({ | ||
| channels: { someNonChannelKey: envelope }, | ||
| }); | ||
| // All current entries are channels, so this just verifies no crash | ||
| expect(result.telegram).toBe(false); | ||
| expect(result.discord).toBe(false); | ||
| expect(result.slack).toBe(false); | ||
| }); | ||
|
|
||
| it('uses entry.id as the result key', () => { | ||
| const result = buildConfiguredSecrets({}); | ||
| const keys = Object.keys(result); | ||
| expect(keys).toContain('telegram'); | ||
| expect(keys).toContain('discord'); | ||
| expect(keys).toContain('slack'); | ||
| expect(keys).toHaveLength(3); | ||
| }); | ||
|
|
||
| it('treats null values as not configured', () => { | ||
| const result = buildConfiguredSecrets({ | ||
| encryptedSecrets: { TELEGRAM_BOT_TOKEN: null as unknown as Record<string, unknown> }, | ||
| }); | ||
| expect(result.telegram).toBe(false); | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,33 +26,6 @@ import { VersionPinCard } from './VersionPinCard'; | |
|
|
||
| type ClawMutations = ReturnType<typeof useKiloClawMutations>; | ||
|
|
||
| /** | ||
| * Maps a catalog entry ID to whether the entry is "configured" based on | ||
| * the channel status from the config endpoint. The config endpoint returns | ||
| * per-field booleans (telegram, discord, slackBot, slackApp) rather than | ||
| * per-entry booleans, so we need this bridge mapping. | ||
| * | ||
| * IMPORTANT: This switch must be updated when new channel entries are added | ||
| * to the secret catalog. Unknown entry IDs silently return false ("Not configured"). | ||
| * The proper fix is to make the config endpoint return per-entry-id status | ||
| * derived from the catalog, eliminating this manual mapping. | ||
| */ | ||
| function isEntryConfigured( | ||
| entryId: string, | ||
| channelStatus: { telegram: boolean; discord: boolean; slackBot: boolean; slackApp: boolean } | ||
| ): boolean { | ||
| switch (entryId) { | ||
| case 'telegram': | ||
| return channelStatus.telegram; | ||
| case 'discord': | ||
| return channelStatus.discord; | ||
| case 'slack': | ||
| return channelStatus.slackBot && channelStatus.slackApp; | ||
| default: | ||
| return false; | ||
| } | ||
| } | ||
|
|
||
| export function SettingsTab({ | ||
| status, | ||
| mutations, | ||
|
|
@@ -116,12 +89,7 @@ export function SettingsTab({ | |
| '2026.2.26' | ||
| ); | ||
|
|
||
| const channelStatus = config?.channels ?? { | ||
| telegram: false, | ||
| discord: false, | ||
| slackBot: false, | ||
| slackApp: false, | ||
| }; | ||
| const configuredSecrets = config?.configuredSecrets ?? {}; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. WARNING: Frontend-first rollout marks every channel as not configured This component now reads only
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Prev acknowledged, this is documented in the Reviewer Notes. Both the worker and Next.js app change in this PR and deploy within minutes of each other. If briefly out of sync, the only effect is channels temporarily showing "Not configured" in the Settings tab. No errors, no data loss, tokens remain functional on the Fly machine. |
||
|
|
||
| function handleSave() { | ||
| if (hasModelSelectionError) { | ||
|
|
@@ -310,7 +278,7 @@ export function SettingsTab({ | |
| <SecretEntrySection | ||
| key={entry.id} | ||
| entry={entry} | ||
| configured={isEntryConfigured(entry.id, channelStatus)} | ||
| configured={configuredSecrets[entry.id] ?? false} | ||
| mutations={mutations} | ||
| onSecretsChanged={onChannelsChanged} | ||
| isDirty={dirtyChannels.has(entry.id)} | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
WARNING: Keep the legacy
channelsfield for a rollout windowcloudand the KiloClaw worker deploy independently (KILOCLAW_API_URLpoints the web app at an external worker). Removingchannelshere means an older web build will seeconfig.channels === undefinedand temporarily render every channel as not configured until the frontend rollout completes. Returning bothconfiguredSecretsand the legacychannelsbooleans for one release avoids that cross-service break.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Acknowledged, this is documented in the Reviewer Notes. Both the worker and Next.js app change in this PR and deploy within minutes of each other. If briefly out of sync, the only effect is channels temporarily showing "Not configured" in the Settings tab. No errors, no data loss, tokens remain functional on the Fly machine.