fix(hooks): initialize flashbang when its DLL cache is unavailable
This commit is contained in:
1 parent
3586245408
commit
5c66f79a76
3 files changed
+46
-16
No files matched your search
+1
-1
@@ -13,7 +13,7 @@ The adapters register these events:
|
|||||||
|
|
||||||
The main agent alone maintains canonical task state. Hooks never write or summarize it, print its contents, or infer completed verification. They use the hook event's `cwd`, falling back to the process working directory when no workspace is supplied. Malformed event data does not fall back to another project. An absent state file is a successful no-op. Legacy state under the installed configuration root is not automatically adopted; confirm its task before migrating it. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
|
The main agent alone maintains canonical task state. Hooks never write or summarize it, print its contents, or infer completed verification. They use the hook event's `cwd`, falling back to the process working directory when no workspace is supplied. Malformed event data does not fall back to another project. An absent state file is a successful no-op. Legacy state under the installed configuration root is not automatically adopted; confirm its task before migrating it. The scripts do not grant permissions, bypass approvals, or replace client permission systems.
|
||||||
|
|
||||||
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable. Registered hooks are best effort: errors do not fail a Claude session.
|
When enabled during installation or update, Flashbang runs on `Stop`, after the main agent finishes responding. The default flash lasts about 500 milliseconds. The PowerShell command omits `-WindowStyle Hidden`, which can hide or minimize the shared terminal. The PowerShell hook creates a low-opacity click-through overlay without activating another window when its native GUI APIs are available; unsupported runtimes exit safely. Its native DLL cache is optional: a missing `LOCALAPPDATA` environment variable or an unusable cache falls back to in-process compilation instead of suppressing the notification. The Bash hook uses Python's `tkinter`, uses `xrandr` for multi-monitor bounds when available, and falls back to `notify-send` when the overlay is unavailable. Registered hooks are best effort: errors do not fail a Claude session.
|
||||||
|
|
||||||
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are not an automatic requirement after each change.
|
`Validate-CommandSafety`/`validate-command-safety.sh` and `Invoke-PostChangeVerification`/`invoke-post-change-verification.sh` are unregistered utilities. The safety utilities check command text for selected dangerous patterns; the PowerShell variant also checks some absolute paths. Neither is a complete security boundary. The verification utilities explicitly run the repository build when invoked; they are not an automatic requirement after each change.
|
||||||
|
|
||||||
|
|||||||
@@ -207,15 +207,45 @@ class SessionStateHookTests(unittest.TestCase):
|
|||||||
with self.subTest(shell=shell):
|
with self.subTest(shell=shell):
|
||||||
environment = dict(os.environ)
|
environment = dict(os.environ)
|
||||||
if shell == 'powershell':
|
if shell == 'powershell':
|
||||||
environment['LOCALAPPDATA'] = ''
|
environment['FLASHBANG_TEST_SCRIPT'] = str(ROOT / 'shared/hooks/flashbang.ps1')
|
||||||
command = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-File',
|
command = [executable, '-NoProfile', '-ExecutionPolicy', 'Bypass', '-Command',
|
||||||
str(ROOT / 'shared/hooks/flashbang.ps1')]
|
"function Add-Type { throw 'GUI runtime unavailable' }; & $env:FLASHBANG_TEST_SCRIPT"]
|
||||||
else:
|
else:
|
||||||
environment['PATH'] = ''
|
environment['PATH'] = ''
|
||||||
command = [executable, str(ROOT / 'shared/hooks/flashbang.sh')]
|
command = [executable, str(ROOT / 'shared/hooks/flashbang.sh')]
|
||||||
result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=20)
|
result = subprocess.run(command, env=environment, capture_output=True, text=True, timeout=20)
|
||||||
self.assertEqual(result.returncode, 0, result.stderr)
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
|
||||||
|
@unittest.skipUnless(os.name == 'nt', 'Windows GUI runtime required')
|
||||||
|
def test_flashbang_cache_is_optional(self):
|
||||||
|
executable = shutil.which('powershell') or shutil.which('pwsh')
|
||||||
|
if not executable:
|
||||||
|
self.skipTest('PowerShell unavailable')
|
||||||
|
# Exercise native initialization, stopping at the UI boundary without flashing the screen.
|
||||||
|
probe = """
|
||||||
|
function New-Object {
|
||||||
|
param([string]$TypeName)
|
||||||
|
if ($TypeName -eq 'System.Windows.Forms.Form' -and ('Flashbang.Native' -as [type])) {
|
||||||
|
[Console]::WriteLine('OVERLAY_READY')
|
||||||
|
}
|
||||||
|
throw 'Stop at UI boundary'
|
||||||
|
}
|
||||||
|
& $env:FLASHBANG_TEST_SCRIPT
|
||||||
|
"""
|
||||||
|
with tempfile.TemporaryDirectory(prefix='.flashbang-test-', dir=ROOT) as directory:
|
||||||
|
blocked = Path(directory) / 'not-a-directory'
|
||||||
|
blocked.write_text('keep')
|
||||||
|
for cache in ('', str(blocked), directory):
|
||||||
|
with self.subTest(cache=cache):
|
||||||
|
environment = dict(os.environ, LOCALAPPDATA=cache,
|
||||||
|
FLASHBANG_TEST_SCRIPT=str(ROOT / 'shared/hooks/flashbang.ps1'))
|
||||||
|
result = subprocess.run([executable, '-NoProfile', '-ExecutionPolicy', 'Bypass',
|
||||||
|
'-Command', probe], env=environment, capture_output=True,
|
||||||
|
text=True, timeout=20)
|
||||||
|
self.assertEqual(result.returncode, 0, result.stderr)
|
||||||
|
self.assertIn('OVERLAY_READY', result.stdout, 'Flashbang exited before creating the overlay')
|
||||||
|
self.assertEqual(blocked.read_text(), 'keep')
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
unittest.main()
|
unittest.main()
|
||||||
+12
-12
@@ -66,27 +66,27 @@ $nativeSource = @'
|
|||||||
if (-not ('Flashbang.Native' -as [type])) {
|
if (-not ('Flashbang.Native' -as [type])) {
|
||||||
# Keyed by host runtime: an assembly emitted by Windows PowerShell (.NET
|
# Keyed by host runtime: an assembly emitted by Windows PowerShell (.NET
|
||||||
# Framework) is not guaranteed to load under pwsh (.NET), and vice versa.
|
# Framework) is not guaranteed to load under pwsh (.NET), and vice versa.
|
||||||
$cacheDir = Join-Path $env:LOCALAPPDATA 'claude-flashbang'
|
try {
|
||||||
$runtimeTag = '{0}{1}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion.Major
|
$cacheDir = Join-Path $env:LOCALAPPDATA 'claude-flashbang'
|
||||||
$cacheDll = Join-Path $cacheDir "Flashbang.Native.$runtimeTag.dll"
|
$runtimeTag = '{0}{1}' -f $PSVersionTable.PSEdition, $PSVersionTable.PSVersion.Major
|
||||||
$loaded = $false
|
$cacheDll = Join-Path $cacheDir "Flashbang.Native.$runtimeTag.dll"
|
||||||
|
$loaded = $false
|
||||||
|
|
||||||
if (Test-Path -LiteralPath $cacheDll) {
|
if (Test-Path -LiteralPath $cacheDll) {
|
||||||
try { Add-Type -Path $cacheDll; $loaded = $true } catch { $loaded = $false }
|
try { Add-Type -Path $cacheDll; $loaded = $true } catch { $loaded = $false }
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $loaded) {
|
if (-not $loaded) {
|
||||||
try {
|
|
||||||
if (-not (Test-Path -LiteralPath $cacheDir)) {
|
if (-not (Test-Path -LiteralPath $cacheDir)) {
|
||||||
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
New-Item -ItemType Directory -Path $cacheDir -Force | Out-Null
|
||||||
}
|
}
|
||||||
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource `
|
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource `
|
||||||
-OutputAssembly $cacheDll
|
-OutputAssembly $cacheDll
|
||||||
Add-Type -Path $cacheDll
|
Add-Type -Path $cacheDll
|
||||||
} catch {
|
|
||||||
# Cache unavailable (locked file, no write access) - compile in-process.
|
|
||||||
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource
|
|
||||||
}
|
}
|
||||||
|
} catch {
|
||||||
|
# Missing profile environment or unusable cache must not suppress the flash.
|
||||||
|
Add-Type -Namespace 'Flashbang' -Name 'Native' -MemberDefinition $nativeSource
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user