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
54 changes: 53 additions & 1 deletion src/utils/statusNoticeDefinitions.safety.test.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { afterEach, beforeEach, describe, expect, mock, test } from 'bun:test'
import type { StatusNoticeContext } from './statusNoticeDefinitions.js'
import { getActiveNotices } from './statusNoticeDefinitions.js'
import {
getActiveNotices,
statusNoticeDefinitions,
} from './statusNoticeDefinitions.js'
import { renderToString } from './staticRender.js'

// Regression coverage for issue #244 — the two safety-related status notices
// that warn 3P users when they are running without the AI classifier or with
Expand All @@ -21,6 +25,15 @@ function activeIds(ctx: StatusNoticeContext): string[] {
return getActiveNotices(ctx).map(n => n.id)
}

async function renderNoticePlainText(
id: string,
ctx: StatusNoticeContext,
): Promise<string> {
const notice = statusNoticeDefinitions.find(n => n.id === id)
expect(notice).toBeDefined()
return renderToString(notice!.render(ctx), 80)
}

const SAVED_ARGV = process.argv
const SAVED_API_KEY = process.env.ANTHROPIC_API_KEY

Expand Down Expand Up @@ -139,3 +152,42 @@ describe('dangerously-skip-permissions sandbox notice (#244 finding 2)', () => {
)
})
})

describe('safety notice rendering', () => {
test('separates warning icons from the notice text', async () => {
const ctx = buildContext({
permissionMode: 'bypassPermissions',
mainLoopModel: 'llama3.1',
})

const thirdPartyNotice = await renderNoticePlainText(
'third-party-permissive-mode',
ctx,
)
const dangerouslySkipNotice = await renderNoticePlainText(
'dangerously-skip-permissions-no-sandbox',
ctx,
)

expect(thirdPartyNotice).toContain('⚠ bypassPermissions')
expect(thirdPartyNotice).not.toContain('⚠bypassPermissions')
expect(dangerouslySkipNotice).toContain(
'⚠ --dangerously-skip-permissions',
)
expect(dangerouslySkipNotice).not.toContain(
'⚠--dangerously-skip-permissions',
)
expect(
thirdPartyNotice
.split('\n')
.slice(1)
.every(line => line.startsWith(' ')),
).toBe(true)
expect(
dangerouslySkipNotice
.split('\n')
.slice(1)
.every(line => line.startsWith(' ')),
).toBe(true)
})
})
68 changes: 41 additions & 27 deletions src/utils/statusNoticeDefinitions.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,23 @@ export type StatusNoticeDefinition = {
render: (context: StatusNoticeContext) => React.ReactNode;
};

function WarningNoticeRow({
children,
marginTop,
}: {
children: React.ReactNode;
marginTop?: number;
}): React.ReactNode {
return <Box flexDirection="row" marginTop={marginTop}>
<Box marginRight={1}>
<Text color="warning">{figures.warning}</Text>
</Box>
<Box flexDirection="column" flexShrink={1}>
{children}
</Box>
</Box>;
}

// Individual notice definitions
const largeMemoryFilesNotice: StatusNoticeDefinition = {
id: 'large-memory-files',
Expand All @@ -45,15 +62,14 @@ const largeMemoryFilesNotice: StatusNoticeDefinition = {
return <>
{largeMemoryFiles.map(file => {
const displayPath = file.path.startsWith(getCwd()) ? relative(getCwd(), file.path) : file.path;
return <Box key={file.path} flexDirection="row">
<Text color="warning">{figures.warning}</Text>
return <WarningNoticeRow key={file.path}>
<Text color="warning">
Large <Text bold>{displayPath}</Text> will impact performance (
{formatNumber(file.content.length)} chars &gt;{' '}
{formatNumber(MAX_MEMORY_CHARACTER_COUNT)})
<Text dimColor> · /memory to edit</Text>
</Text>
</Box>;
</WarningNoticeRow>;
})}
</>;
}
Expand All @@ -67,14 +83,13 @@ const claudeAiSubscriberExternalTokenNotice: StatusNoticeDefinition = {
},
render: () => {
const authTokenInfo = getAuthTokenSource();
return <Box flexDirection="row" marginTop={1}>
<Text color="warning">{figures.warning}</Text>
return <WarningNoticeRow marginTop={1}>
<Text color="warning">
Auth conflict: Using {authTokenInfo.source} instead of Claude account
subscription token. Either unset {authTokenInfo.source}, or run
`claude /logout`.
</Text>
</Box>;
</WarningNoticeRow>;
}
};
const apiKeyConflictNotice: StatusNoticeDefinition = {
Expand All @@ -94,13 +109,12 @@ const apiKeyConflictNotice: StatusNoticeDefinition = {
} = getAnthropicApiKeyWithSource({
skipRetrievingKeyFromApiKeyHelper: true
});
return <Box flexDirection="row" marginTop={1}>
<Text color="warning">{figures.warning}</Text>
return <WarningNoticeRow marginTop={1}>
<Text color="warning">
Auth conflict: Using {apiKeySource} instead of Anthropic Console key.
Either unset {apiKeySource}, or run `openclaude /logout`.
</Text>
</Box>;
</WarningNoticeRow>;
}
};
const bothAuthMethodsNotice: StatusNoticeDefinition = {
Expand All @@ -123,13 +137,12 @@ const bothAuthMethodsNotice: StatusNoticeDefinition = {
});
const authTokenInfo = getAuthTokenSource();
return <Box flexDirection="column" marginTop={1}>
<Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
<WarningNoticeRow>
<Text color="warning">
Auth conflict: Both a token ({authTokenInfo.source}) and an API key
({apiKeySource}) are set. This may lead to unexpected behavior.
</Text>
</Box>
</WarningNoticeRow>
<Box flexDirection="column" marginLeft={3}>
<Text color="warning">
· Trying to use{' '}
Expand All @@ -154,15 +167,14 @@ const largeAgentDescriptionsNotice: StatusNoticeDefinition = {
},
render: context => {
const totalTokens = getAgentDescriptionsTotalTokens(context.agentDefinitions);
return <Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
return <WarningNoticeRow>
<Text color="warning">
Large cumulative agent descriptions will impact performance (~
{formatNumber(totalTokens)} tokens &gt;{' '}
{formatNumber(AGENT_DESCRIPTIONS_THRESHOLD)})
<Text dimColor> · /agents to manage</Text>
</Text>
</Box>;
</WarningNoticeRow>;
}
};
const jetbrainsPluginNotice: StatusNoticeDefinition = {
Expand Down Expand Up @@ -224,14 +236,15 @@ const thirdPartyPermissiveModeNotice: StatusNoticeDefinition = {
},
render: ctx => {
const mode = ctx.permissionMode;
return <Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
return <WarningNoticeRow>
<Text color="warning">
<Text bold>{mode}</Text> mode is active on a third-party provider —
tool calls run without the AI safety classifier.
<Text dimColor> Inspect tool calls manually, especially when working with untrusted code.</Text>
<Text bold>{mode}</Text> mode is active on a third-party provider.
</Text>
</Box>;
<Text dimColor>
Tool calls run without the AI safety classifier. Inspect tool calls manually,
especially when working with untrusted code.
</Text>
</WarningNoticeRow>;
}
};
// `--dangerously-skip-permissions` (a.k.a. bypassPermissions) auto-approves
Expand All @@ -250,14 +263,15 @@ const dangerouslySkipPermissionsNotice: StatusNoticeDefinition = {
isActive: ctx =>
hasDangerouslySkipPermissionsArg() ||
ctx.permissionMode === 'bypassPermissions',
render: () => <Box flexDirection="row">
<Text color="warning">{figures.warning}</Text>
render: () => <WarningNoticeRow>
<Text color="warning">
<Text bold>--dangerously-skip-permissions</Text> bypasses every tool
consent check.
<Text dimColor> Only use inside a sandbox with no internet access. Restart without the flag to re-enable prompts.</Text>
<Text bold>--dangerously-skip-permissions</Text> is active.
</Text>
<Text dimColor>
Every tool consent check is bypassed. Only use inside a sandbox with no internet access.
Restart without the flag to re-enable prompts.
</Text>
</Box>
</WarningNoticeRow>
};

// All notice definitions
Expand Down