Skip to content

Commit 824ae2d

Browse files
committed
fix: address 6 more CodeRabbit findings from third review round
- agent_engine: type PersonalityTrimPayload identifier fields (agent_id, agent_name, task_id) as NotBlankStr per CLAUDE.md identifier convention. Pydantic validators don't fire inside a TypedDict at runtime, but the alias documents intent and keeps the notifier contract consistent with the rest of the codebase. - web/CLAUDE.md: resolve the Tooltip inconsistency -- the 'Creating New Components' and 'Base UI Adoption Decisions' sections no longer mention Tooltip as preferred Base UI, since the adoption table does not list it as adopted. Contributors are directed to reach for existing primitives first and add a row to the table if a real Tooltip requirement appears. - stores/agents: null-guard WsEvent.payload before casting to Record<string, unknown>. The TypeScript 'as' cast is compile-time only -- a malformed broker sending null would throw on the next property access. The guard drops the event and logs the drop. - stores/agents: sanitize attacker-controlled values in log.warn calls -- 'agent' (URL route param) in fetchAgentDetail, 'status' in the unknown-status branch of the agent.status_changed handler, and 'event_type' in the new payload-null guard. Matches the 'sanitizeForLog attacker-controlled fields' rule in web/CLAUDE.md. - dialog.tsx: reuse the shared Button component inside DialogCloseButton via Base UI's render prop, instead of duplicating the icon-button styles. Keeps tokens, focus states, and hover semantics centralized. - dialog.tsx: DialogDescription was text-muted (already fixed in prior commit, retained here). - useGlobalNotifications.test: resolve the 'agents' binding by channel name via bindings.find(b => b.channel === 'agents') instead of by index, so adding unrelated subscriptions upstream cannot silently break the suite.
1 parent bdbc073 commit 824ae2d

5 files changed

Lines changed: 52 additions & 21 deletions

File tree

src/synthorg/engine/agent_engine.py

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from synthorg.budget.currency import DEFAULT_CURRENCY
1414
from synthorg.budget.errors import BudgetExhaustedError, QuotaExhaustedError
1515
from synthorg.budget.quota import DegradationAction
16+
from synthorg.core.types import NotBlankStr # noqa: TC001
1617
from synthorg.engine._security_factory import (
1718
make_security_interceptor,
1819
registry_with_approval_tool,
@@ -158,14 +159,19 @@ class PersonalityTrimPayload(TypedDict):
158159
"""Structured payload forwarded to :data:`PersonalityTrimNotifier` callbacks.
159160
160161
All keys are always present when the engine invokes the notifier from
161-
:meth:`AgentEngine._prepare_context`. ``trim_tier`` is one of ``1``, ``2``,
162-
or ``3`` (enforced upstream by
162+
:meth:`AgentEngine._prepare_context`. Identifier fields (``agent_id``,
163+
``agent_name``, ``task_id``) are typed as :data:`NotBlankStr` to match
164+
the project-wide identifier convention documented in CLAUDE.md -- the
165+
Pydantic constraint is not enforced inside a ``TypedDict`` at runtime,
166+
but the alias communicates the contract to readers and keeps this
167+
surface consistent with the rest of the codebase. ``trim_tier`` is
168+
one of ``1``, ``2``, or ``3`` (enforced upstream by
163169
:class:`synthorg.engine.prompt_helpers.PersonalityTrimInfo`).
164170
"""
165171

166-
agent_id: str
167-
agent_name: str
168-
task_id: str
172+
agent_id: NotBlankStr
173+
agent_name: NotBlankStr
174+
task_id: NotBlankStr
169175
before_tokens: int
170176
after_tokens: int
171177
max_tokens: int

web/CLAUDE.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,7 @@ When a new shared component is needed (not covered by the inventory above):
111111
3. Export props as a TypeScript interface
112112
4. Use design tokens exclusively -- no hardcoded colors, fonts, or spacing
113113
5. Import `cn` from `@/lib/utils` for conditional class merging
114-
6. **For primitives backed by Base UI** (Dialog, Popover, Menu, Tabs, Tooltip, etc. -- see the Adoption Decisions table below for the canonical list; `Select`, `Toast`, `Drawer`, `Meter`, `Combobox` are intentionally **not** adopted):
114+
6. **For primitives backed by Base UI** (Dialog, AlertDialog, Popover, Menu, Tabs -- see the Adoption Decisions table below for the canonical list; `Select`, `Toast`, `Drawer`, `Meter`, `Combobox`, `Tooltip` are intentionally **not** adopted):
115115
- Import from the specific subpath: `import { Dialog } from '@base-ui/react/dialog'`
116116
- Use the component's `render` prop for polymorphism: `<Dialog.Trigger render={<Button>Open</Button>} />`. Never spread props manually.
117117
- For Dialog/AlertDialog/Popover: compose with `Portal` + `Backdrop` + `Popup`. Popover and Menu additionally require a `Positioner` wrapper that owns `side` / `align` / `sideOffset`.
@@ -155,7 +155,7 @@ The dashboard uses Base UI as its primitive layer via shadcn/ui. Base UI ships s
155155
| `Select` | **Not adopted** | `SelectField` is a native `<select>` -- we intentionally keep the native mobile picker for iOS/Android UX. Replacing with a custom dropdown would lose that. |
156156
| `Combobox`, `Autocomplete` | **Not adopted (for now)** | No current typeahead call sites in the dashboard that would benefit. Re-evaluate when filterable selects become a feature requirement. |
157157

158-
When adding new dashboard primitives, prefer Base UI components for accessibility (Dialog, Menu, Popover, Tabs, Tooltip, etc.) and keep the existing custom components (`SelectField`, `Drawer`, `Toast`, `ProgressGauge`, animations) where they are -- see the Adoption Decisions table above for the canonical rationale.
158+
When adding new dashboard primitives, prefer Base UI components for accessibility (Dialog, AlertDialog, Popover, Tabs, Menu) and keep the existing custom components (`SelectField`, `Drawer`, `Toast`, `ProgressGauge`, animations) where they are -- see the Adoption Decisions table above for the canonical rationale. Tooltip is not yet adopted; reach for an existing primitive first and add a row to the table above if a real Tooltip requirement appears.
159159

160160
## Post-Training Reference (TypeScript 6 & Storybook 10)
161161

web/src/__tests__/hooks/useGlobalNotifications.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,8 +41,12 @@ describe('useGlobalNotifications', () => {
4141
const { bindings } = options as {
4242
bindings: Array<{ channel: string; handler: (event: WsEvent) => void }>
4343
}
44+
// Resolve by channel name rather than index so adding unrelated
45+
// subscriptions upstream cannot silently break this test.
46+
const agentsBinding = bindings.find((b) => b.channel === 'agents')
47+
expect(agentsBinding).toBeDefined()
4448

45-
bindings[0]!.handler({
49+
agentsBinding!.handler({
4650
event_type: 'agent.status_changed',
4751
channel: 'agents',
4852
timestamp: '2026-04-05T10:00:00Z',
@@ -59,8 +63,12 @@ describe('useGlobalNotifications', () => {
5963
const { bindings } = options as {
6064
bindings: Array<{ channel: string; handler: (event: WsEvent) => void }>
6165
}
66+
// Resolve by channel name rather than index so adding unrelated
67+
// subscriptions upstream cannot silently break this test.
68+
const agentsBinding = bindings.find((b) => b.channel === 'agents')
69+
expect(agentsBinding).toBeDefined()
6270

63-
bindings[0]!.handler({
71+
agentsBinding!.handler({
6472
event_type: 'personality.trimmed',
6573
channel: 'agents',
6674
timestamp: '2026-04-05T10:00:00Z',

web/src/components/ui/dialog.tsx

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Dialog as BaseDialog } from '@base-ui/react/dialog'
22
import { X } from 'lucide-react'
33
import { cn } from '@/lib/utils'
4+
import { Button } from './button'
45

56
export interface DialogProps {
67
open: boolean
@@ -90,15 +91,16 @@ export interface DialogCloseButtonProps {
9091
export function DialogCloseButton({ className }: DialogCloseButtonProps) {
9192
return (
9293
<BaseDialog.Close
93-
aria-label="Close"
94-
className={cn(
95-
'rounded-md p-1 text-muted-foreground transition-colors',
96-
'hover:bg-card-hover hover:text-foreground',
97-
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent',
98-
className,
99-
)}
100-
>
101-
<X className="size-4" />
102-
</BaseDialog.Close>
94+
render={
95+
<Button
96+
variant="ghost"
97+
size="icon"
98+
aria-label="Close"
99+
className={className}
100+
>
101+
<X className="size-4" />
102+
</Button>
103+
}
104+
/>
103105
)
104106
}

web/src/stores/agents.ts

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { listAgents, getAgent, getAgentPerformance, getAgentActivity, getAgentHi
33
import { listTasks } from '@/api/endpoints/tasks'
44
import { useToastStore } from '@/stores/toast'
55
import { getErrorMessage } from '@/utils/errors'
6+
import { sanitizeForLog } from '@/utils/logging'
67
import { createLogger } from '@/lib/logger'
78
import type {
89
AgentActivityEvent,
@@ -161,7 +162,9 @@ export const useAgentsStore = create<AgentsState>()((set, get) => ({
161162
})
162163
} catch (err) {
163164
if (_detailRequestName !== name) return
164-
log.warn('Failed to load agent detail', { agent: name }, err)
165+
// `name` originates from a URL segment / router param and is therefore
166+
// attacker-controlled; sanitize before embedding in the structured log.
167+
log.warn('Failed to load agent detail', { agent: sanitizeForLog(name) }, err)
165168
set({ detailLoading: false, detailError: getErrorMessage(err) })
166169
}
167170
},
@@ -226,6 +229,16 @@ export const useAgentsStore = create<AgentsState>()((set, get) => ({
226229
},
227230

228231
updateFromWsEvent: (event) => {
232+
// Runtime null-guard: `event.payload` is typed as `Record<string, unknown>`
233+
// on the wire, but a malformed broker could still send `null` or a
234+
// non-object. The `as` cast below would not catch that, so we filter
235+
// here once and treat every invalid envelope as a dropped event.
236+
if (typeof event.payload !== 'object' || event.payload === null) {
237+
log.warn('WS event dropped: payload is not an object', {
238+
event_type: sanitizeForLog(event.event_type),
239+
})
240+
return
241+
}
229242
if (event.event_type === 'agent.status_changed') {
230243
const payload = event.payload as Record<string, unknown>
231244
const agentId = payload.agent_id
@@ -238,8 +251,10 @@ export const useAgentsStore = create<AgentsState>()((set, get) => ({
238251
return
239252
}
240253
if (!VALID_RUNTIME_STATUSES.has(status)) {
254+
// `status` arrives from an untrusted WebSocket payload, so sanitize
255+
// before embedding in the structured log.
241256
log.warn('agent.status_changed received unknown status', {
242-
status,
257+
status: sanitizeForLog(status),
243258
knownStatuses: [...VALID_RUNTIME_STATUSES],
244259
})
245260
return

0 commit comments

Comments
 (0)