134 lines
4.9 KiB
TypeScript
134 lines
4.9 KiB
TypeScript
import { resetSdkInitState } from '../../bootstrap/state.js'
|
|
import { isRestrictedToPluginOnly } from '../settings/pluginOnlyPolicy.js'
|
|
// Import as module object so spyOn works in tests (direct imports bypass spies)
|
|
import * as settingsModule from '../settings/settings.js'
|
|
import { resetSettingsCache } from '../settings/settingsCache.js'
|
|
import type { HooksSettings } from '../settings/types.js'
|
|
|
|
let initialHooksConfig: HooksSettings | null = null
|
|
|
|
/**
|
|
* Get hooks from allowed sources.
|
|
* If allowManagedHooksOnly is set in policySettings, only managed hooks are returned.
|
|
* If disableAllHooks is set in policySettings, no hooks are returned.
|
|
* If disableAllHooks is set in non-managed settings, only managed hooks are returned
|
|
* (non-managed settings cannot disable managed hooks).
|
|
* Otherwise, returns merged hooks from all sources (backwards compatible).
|
|
*/
|
|
function getHooksFromAllowedSources(): HooksSettings {
|
|
const policySettings = settingsModule.getSettingsForSource('policySettings')
|
|
|
|
// If managed settings disables all hooks, return empty
|
|
if (policySettings?.disableAllHooks === true) {
|
|
return {}
|
|
}
|
|
|
|
// If allowManagedHooksOnly is set in managed settings, only use managed hooks
|
|
if (policySettings?.allowManagedHooksOnly === true) {
|
|
return policySettings.hooks ?? {}
|
|
}
|
|
|
|
// strictPluginOnlyCustomization: block user/project/local settings hooks.
|
|
// Plugin hooks (registered channel, hooks.ts:1391) are NOT affected —
|
|
// they're assembled separately and the managedOnly skip there is keyed
|
|
// on shouldAllowManagedHooksOnly(), not on this policy. Agent frontmatter
|
|
// hooks are gated at REGISTRATION (runAgent.ts:~535) by agent source —
|
|
// plugin/built-in/policySettings agents register normally, user-sourced
|
|
// agents skip registration under ["hooks"]. A blanket execution-time
|
|
// block here would over-kill plugin agents' hooks.
|
|
if (isRestrictedToPluginOnly('hooks')) {
|
|
return policySettings?.hooks ?? {}
|
|
}
|
|
|
|
const mergedSettings = settingsModule.getSettings_DEPRECATED()
|
|
|
|
// If disableAllHooks is set in non-managed settings, only managed hooks still run
|
|
// (non-managed settings cannot override managed hooks)
|
|
if (mergedSettings.disableAllHooks === true) {
|
|
return policySettings?.hooks ?? {}
|
|
}
|
|
|
|
// Otherwise, use all hooks (merged from all sources) - backwards compatible
|
|
return mergedSettings.hooks ?? {}
|
|
}
|
|
|
|
/**
|
|
* Check if only managed hooks should run.
|
|
* This is true when:
|
|
* - policySettings has allowManagedHooksOnly: true, OR
|
|
* - disableAllHooks is set in non-managed settings (non-managed settings
|
|
* cannot disable managed hooks, so they effectively become managed-only)
|
|
*/
|
|
export function shouldAllowManagedHooksOnly(): boolean {
|
|
const policySettings = settingsModule.getSettingsForSource('policySettings')
|
|
if (policySettings?.allowManagedHooksOnly === true) {
|
|
return true
|
|
}
|
|
// If disableAllHooks is set but NOT from managed settings,
|
|
// treat as managed-only (non-managed hooks disabled, managed hooks still run)
|
|
if (
|
|
settingsModule.getSettings_DEPRECATED().disableAllHooks === true &&
|
|
policySettings?.disableAllHooks !== true
|
|
) {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
/**
|
|
* Check if all hooks (including managed) should be disabled.
|
|
* This is only true when managed/policy settings has disableAllHooks: true.
|
|
* When disableAllHooks is set in non-managed settings, managed hooks still run.
|
|
*/
|
|
export function shouldDisableAllHooksIncludingManaged(): boolean {
|
|
return (
|
|
settingsModule.getSettingsForSource('policySettings')?.disableAllHooks ===
|
|
true
|
|
)
|
|
}
|
|
|
|
/**
|
|
* Capture a snapshot of the current hooks configuration
|
|
* This should be called once during application startup
|
|
* Respects the allowManagedHooksOnly setting
|
|
*/
|
|
export function captureHooksConfigSnapshot(): void {
|
|
initialHooksConfig = getHooksFromAllowedSources()
|
|
}
|
|
|
|
/**
|
|
* Update the hooks configuration snapshot
|
|
* This should be called when hooks are modified through the settings
|
|
* Respects the allowManagedHooksOnly setting
|
|
*/
|
|
export function updateHooksConfigSnapshot(): void {
|
|
// Reset the session cache to ensure we read fresh settings from disk.
|
|
// Without this, the snapshot could use stale cached settings when the user
|
|
// edits settings.json externally and then runs /hooks - the session cache
|
|
// may not have been invalidated yet (e.g., if the file watcher's stability
|
|
// threshold hasn't elapsed).
|
|
resetSettingsCache()
|
|
initialHooksConfig = getHooksFromAllowedSources()
|
|
}
|
|
|
|
/**
|
|
* Get the current hooks configuration from snapshot
|
|
* Falls back to settings if no snapshot exists
|
|
* @returns The hooks configuration
|
|
*/
|
|
export function getHooksConfigFromSnapshot(): HooksSettings | null {
|
|
if (initialHooksConfig === null) {
|
|
captureHooksConfigSnapshot()
|
|
}
|
|
return initialHooksConfig
|
|
}
|
|
|
|
/**
|
|
* Reset the hooks configuration snapshot (useful for testing)
|
|
* Also resets SDK init state to prevent test pollution
|
|
*/
|
|
export function resetHooksConfigSnapshot(): void {
|
|
initialHooksConfig = null
|
|
resetSdkInitState()
|
|
}
|