Skip to content
Open
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
21 changes: 15 additions & 6 deletions frontend/src/stores/product-assistant.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,19 @@ const MAX_DEBUG_LOG_ENTRIES = 100 // maximum number of debug log entries to keep
const TOOL_POLICIES = ['allow', 'ask', 'deny']
const isToolPolicy = (p) => TOOL_POLICIES.includes(p)
const TOOL_CLASSES = ['read', 'write', 'destructive']
// Fail-safe default when a class has no configured default: read allows, the rest ask.
const fallbackForToolClass = (cls) => (cls === 'read' ? 'allow' : 'ask')
// Flow-building changes only touch the editor and nothing runs until the user deploys,
// so every class starts allowed there. Platform actions act on the account straight
// away: read allows, the rest ask.
const fallbackForToolClass = (cls, group = TOOL_GROUPS.FLOW_BUILDING) => {
if (group === TOOL_GROUPS.FLOW_BUILDING || cls === 'read') return 'allow'
return 'ask'
}
// The class defaults a team starts with before the user changes anything.
const defaultToolDefaults = () => ({ read: 'allow', write: 'ask', destructive: 'ask' })
const defaultToolDefaults = (group = TOOL_GROUPS.FLOW_BUILDING) => ({
read: fallbackForToolClass('read', group),
write: fallbackForToolClass('write', group),
destructive: fallbackForToolClass('destructive', group)
})
// The team whose saved permissions are in effect. Permissions are per team, so every
// read/write of defaults or preferences is scoped by this id.
const currentTeamId = () => useContextStore().team?.id || null
Expand Down Expand Up @@ -336,7 +345,7 @@ export const useProductAssistantStore = defineStore('product-assistant', {
const migrated = saved.destructive === undefined && legacyDelete !== undefined
? { ...saved, destructive: legacyDelete }
: saved
return { ...defaultToolDefaults(), ...migrated }
return { ...defaultToolDefaults(group), ...migrated }
},
/** The current team's saved per-tool preferences ({ [key]: policy }). */
teamToolPreferences: (state) => {
Expand All @@ -346,7 +355,7 @@ export const useProductAssistantStore = defineStore('product-assistant', {
defaultForToolClass () {
return (cls, group = TOOL_GROUPS.FLOW_BUILDING) => {
const d = this.teamGroupDefaults(group)[cls]
return isToolPolicy(d) ? d : fallbackForToolClass(cls)
return isToolPolicy(d) ? d : fallbackForToolClass(cls, group)
}
},
/** This chat session's grant for a tool key, or null if none ('allow'|'deny'). */
Expand Down Expand Up @@ -719,7 +728,7 @@ export const useProductAssistantStore = defineStore('product-assistant', {
const teamId = currentTeamId()
if (!teamId) return
const teamDefaults = this.toolDefaultsByTeam[teamId] || {}
const groupDefaults = { ...defaultToolDefaults(), ...(teamDefaults[group] || {}), [cls]: policy }
const groupDefaults = { ...defaultToolDefaults(group), ...(teamDefaults[group] || {}), [cls]: policy }
this.toolDefaultsByTeam = {
...this.toolDefaultsByTeam,
[teamId]: { ...teamDefaults, [group]: groupDefaults }
Expand Down
36 changes: 25 additions & 11 deletions test/unit/frontend/stores/product-assistant.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -508,9 +508,23 @@ describe('product-assistant store', () => {
})

describe('teamGroupDefaults / defaultForToolClass', () => {
it('returns the fail-safe defaults when the team has none saved', () => {
it('allows every class in flow-building when the team has none saved', () => {
const store = useProductAssistantStore()
expect(store.teamGroupDefaults(TOOL_GROUPS.FLOW_BUILDING)).toEqual({ read: 'allow', write: 'ask', destructive: 'ask' })
expect(store.teamGroupDefaults(TOOL_GROUPS.FLOW_BUILDING)).toEqual({ read: 'allow', write: 'allow', destructive: 'allow' })
})

it('keeps asking for platform writes and destructive actions by default', () => {
const store = useProductAssistantStore()
expect(store.teamGroupDefaults(TOOL_GROUPS.PLATFORM)).toEqual({ read: 'allow', write: 'ask', destructive: 'ask' })
expect(store.defaultForToolClass('write', TOOL_GROUPS.PLATFORM)).toBe('ask')
expect(store.defaultForToolClass('destructive', TOOL_GROUPS.PLATFORM)).toBe('ask')
})

it('keeps a class default the user saved as ask', () => {
const store = useProductAssistantStore()
store.setToolClassDefault(TOOL_GROUPS.FLOW_BUILDING, 'write', 'ask')
expect(store.defaultForToolClass('write')).toBe('ask')
expect(store.defaultForToolClass('destructive')).toBe('allow')
})

it('defaultForToolClass reflects a saved class default', () => {
Expand All @@ -522,15 +536,15 @@ describe('product-assistant store', () => {
it('defaultForToolClass falls back per class when nothing valid is stored', () => {
const store = useProductAssistantStore()
expect(store.defaultForToolClass('read')).toBe('allow')
expect(store.defaultForToolClass('write')).toBe('ask')
expect(store.defaultForToolClass('destructive')).toBe('ask')
expect(store.defaultForToolClass('write')).toBe('allow')
expect(store.defaultForToolClass('destructive')).toBe('allow')
})

it('migrates a pre-rename "delete" default saved before the destructive rename', () => {
const store = useProductAssistantStore()
store.toolDefaultsByTeam = { [TEAM]: { [TOOL_GROUPS.FLOW_BUILDING]: { delete: 'allow' } } }
expect(store.teamGroupDefaults(TOOL_GROUPS.FLOW_BUILDING)).toEqual({ read: 'allow', write: 'ask', destructive: 'allow' })
expect(store.defaultForToolClass('destructive')).toBe('allow')
store.toolDefaultsByTeam = { [TEAM]: { [TOOL_GROUPS.FLOW_BUILDING]: { delete: 'ask' } } }
expect(store.teamGroupDefaults(TOOL_GROUPS.FLOW_BUILDING)).toEqual({ read: 'allow', write: 'allow', destructive: 'ask' })
expect(store.defaultForToolClass('destructive')).toBe('ask')
})

it('prefers an already-migrated "destructive" default over a stale "delete" one', () => {
Expand Down Expand Up @@ -572,13 +586,13 @@ describe('product-assistant store', () => {
{ key: 'write-flow', toolClass: 'write' },
{ key: 'destructive-flow', toolClass: 'destructive' }
])
store.setSessionToolOverride('write-flow', 'allow')
store.setSessionToolOverride('write-flow', 'deny')
const resolved = store.resolvedToolPermissions
expect(resolved.defaults).toEqual({ read: 'allow', write: 'ask', destructive: 'ask' })
expect(resolved.defaults).toEqual({ read: 'allow', write: 'allow', destructive: 'allow' })
expect(resolved.tools).toEqual({
'read-flow': 'allow',
'write-flow': 'allow', // session grant folded in
'destructive-flow': 'ask'
'write-flow': 'deny', // session grant folded in
'destructive-flow': 'allow'
})
})
})
Expand Down
Loading