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
3 changes: 3 additions & 0 deletions cmd/claude-notifications/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,9 @@ var (
)

func main() {
if len(os.Args) > 1 && os.Args[1] == "opencode-clock" {
os.Exit(runOpenCodeClock(os.Args[2:], os.Stdout))
}
if len(os.Args) > 1 && os.Args[1] == "gemini-event" {
os.Exit(runGeminiEvent(os.Args[2:], os.Stdin, os.Stdout))
}
Expand Down
10 changes: 10 additions & 0 deletions cmd/claude-notifications/opencode_clock.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package main

import (
"github.com/777genius/agent-notifications/internal/opencodeevent"
"io"
)

func runOpenCodeClock(args []string, output io.Writer) int {
return opencodeevent.WriteClockSnapshot(args, output, opencodeevent.NewSystemSnapshotPort())
}
54 changes: 54 additions & 0 deletions cmd/claude-notifications/opencode_clock_linux_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
//go:build linux

package main

import (
"bytes"
"context"
"encoding/json"
"os"
"os/exec"
"path/filepath"
"syscall"
"testing"
"time"
)

// Regression: dispatch reaching ordinary config/setup can block on a hostile
// config FIFO or create state; a pure isolated CLI snapshot must finish silently.
// The child test calls actual main; it does not inject a substitute clock.
func TestOpenCodeClockIsolatedDispatch(t *testing.T) {
if os.Getenv("AN_CLOCK_TEST_CHILD") == "1" {
os.Args = []string{"claude-notifications", "opencode-clock", "--protocol", "1"}
main()
return
}
root := t.TempDir()
config := filepath.Join(root, "config", "agent-notifications")
if err := os.MkdirAll(config, 0700); err != nil {
t.Fatal(err)
}
if err := syscall.Mkfifo(filepath.Join(config, "config.json"), 0600); err != nil {
t.Fatal(err)
}
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
cmd := exec.CommandContext(ctx, os.Args[0], "-test.run=^TestOpenCodeClockIsolatedDispatch$")
cmd.Dir = root
cmd.Env = []string{"AN_CLOCK_TEST_CHILD=1", "HOME=" + root, "XDG_CONFIG_HOME=" + filepath.Join(root, "config"), "XDG_DATA_HOME=" + filepath.Join(root, "missing-data"), "CLAUDE_PLUGIN_ROOT=" + filepath.Join(root, "missing-plugin"), "AGENT_NOTIFICATIONS_CONFIG=" + filepath.Join(config, "config.json")}
var out, stderr bytes.Buffer
cmd.Stdout = &out
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {
t.Fatal(err, stderr.String(), out.String())
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(out.Bytes(), &fields); err != nil || len(fields) != 8 || string(fields["protocol"]) != "1" || out.Len() > 1024 || stderr.Len() != 0 {
t.Fatal(out.String(), stderr.String(), err)
}
// Only the hostile config fixture may exist afterwards.
entries := 0
if err := filepath.WalkDir(root, func(_ string, _ os.DirEntry, err error) error { entries++; return err }); err != nil || entries != 4 {
t.Fatal("unexpected private state", entries, err)
}
}
16 changes: 13 additions & 3 deletions internal/agentnotify/journal/clock_darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,23 @@ type PlatformClock struct{}
func DefaultClock() Clock { return PlatformClock{} }

func (PlatformClock) Sample() Sample {
boot, sec, _, ok := DarwinBootSample()
if !ok {
return Sample{}
}
return Sample{Boot: boot, Seconds: uint64(sec), Available: true}
}

// DarwinBootSample preserves the existing continuous CLOCK_MONOTONIC_RAW epoch
// and boot-session identity, exposing its integer sec/nsec without new FFI.
func DarwinBootSample() (boot string, sec, nsec int64, ok bool) {
boot, e := unix.Sysctl("kern.bootsessionuuid")
if e != nil || !validText(boot, 256, true) {
return Sample{}
return "", 0, 0, false
}
var ts unix.Timespec
if e = unix.ClockGettime(unix.CLOCK_MONOTONIC_RAW, &ts); e != nil || ts.Sec < 0 {
return Sample{}
return "", 0, 0, false
}
return Sample{Boot: boot, Seconds: uint64(ts.Sec), Available: true}
return boot, ts.Sec, ts.Nsec, true
}
95 changes: 95 additions & 0 deletions internal/agentnotify/journal/clock_diagnostic_windows_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
//go:build windows

package journal

import (
"math"
"runtime"
"strings"
"testing"
"unsafe"

"golang.org/x/sys/windows"
)

// Regression: a wrong class/layout, GUID byte order, precise export or tick
// conversion can refuse or misidentify a valid native boot coordinate. The
// oracle uses native GUID formatting and separate DLL bindings, not product
// layout/formatting/constants. ReturnLength is evidence, never a relaxed gate.
func TestWindowsNativeClockDiagnostic(t *testing.T) {
// PHNT ntexapi.h at 025db3a0ec0fa1b303152d570df492625a021331:
// SystemBootEnvironmentInformation (90), GUID/FIRMWARE_TYPE/BootFlags.
// Private ABI: this observation does not qualify alternate returned lengths.
var info struct {
Identifier windows.GUID
Firmware uint32
_ uint32
Flags uint64
}
const expectedLength = 32
const lengthSentinel uint32 = 0xa5a5a5a5
t.Logf("native ABI size=%d guid_offset=%d guid_size=%d firmware_offset=%d firmware_size=%d flags_offset=%d flags_size=%d expected_length=%d",
unsafe.Sizeof(info), unsafe.Offsetof(info.Identifier), unsafe.Sizeof(info.Identifier),
unsafe.Offsetof(info.Firmware), unsafe.Sizeof(info.Firmware), unsafe.Offsetof(info.Flags), unsafe.Sizeof(info.Flags), expectedLength)
if unsafe.Sizeof(info) != expectedLength || unsafe.Offsetof(info.Identifier) != 0 || unsafe.Sizeof(info.Identifier) != 16 ||
unsafe.Offsetof(info.Firmware) != 16 || unsafe.Sizeof(info.Firmware) != 4 || unsafe.Offsetof(info.Flags) != 24 || unsafe.Sizeof(info.Flags) != 8 {
t.Fatal("diagnostic ABI mismatch")
}
v := windows.RtlGetVersion()
t.Logf("native platform go=%s arch=%s windows_major=%d windows_minor=%d windows_build=%d", runtime.Version(), runtime.GOARCH, v.MajorVersion, v.MinorVersion, v.BuildNumber)
query := windows.NewLazySystemDLL("ntdll.dll").NewProc("NtQuerySystemInformation")
if query.Find() != nil {
t.Fatal("native boot query export unavailable")
}
retLen := lengthSentinel
status, _, _ := query.Call(90, uintptr(unsafe.Pointer(&info)), expectedLength, uintptr(unsafe.Pointer(&retLen)))
statusOK := int32(uint32(status)) >= 0 // NT_SUCCESS; last-error is not NTSTATUS.
nativeGUID := info.Identifier.String() // StringFromGUID2, independent of formatWindowsGUID.
parsed, parseErr := windows.GUIDFromString(nativeGUID)
canonical := strings.ToLower(strings.TrimSuffix(strings.TrimPrefix(nativeGUID, "{"), "}"))
canonicalOK := len(nativeGUID) == 38 && len(canonical) == 36 && parseErr == nil && parsed == info.Identifier
nonzero := info.Identifier != (windows.GUID{})
bootFalse, falseOK := windowsBootIdentity(false)
bootTrue, trueOK := windowsBootIdentity(true)
stable := statusOK && nonzero && canonicalOK && falseOK && bootFalse == canonical
trueMatches := stable && trueOK && bootTrue == canonical
t.Logf("native boot ntstatus=0x%08x status_ok=%t return_length=%d expected_length=%d length_unchanged=%t length_exact=%t guid_nonzero=%t guid_canonical=%t guid_stable=%t boot_false_available=%t boot_true_available=%t boot_true_matches_native=%t",
uint32(status), statusOK, retLen, expectedLength, retLen == lengthSentinel, retLen == expectedLength,
nonzero, canonicalOK, stable, falseOK, trueOK, trueMatches)

// Independent binding to the documented API-set contract, resolved by the OS.
// https://learn.microsoft.com/en-us/uwp/win32-and-com/win32-apis#apis-from-api-ms-win-core-realtime-l1-1-1dll
realtime := windows.NewLazySystemDLL("api-ms-win-core-realtime-l1-1-1.dll")
contractLoadOK := realtime.Load() == nil
precise := realtime.NewProc("QueryInterruptTimePrecise")
contractExportOK := false
if contractLoadOK {
contractExportOK = precise.Find() == nil
}
var before, after uint64
if contractExportOK {
_, _, _ = precise.Call(uintptr(unsafe.Pointer(&before))) // VOID, ignore return/last-error.
}
boot, sec, nsec, sampleOK := WindowsPreciseBootSample()
if contractExportOK {
_, _, _ = precise.Call(uintptr(unsafe.Pointer(&after)))
}
counterNonzero := before != 0 && after != 0
monotone := contractExportOK && counterNonzero && after >= before
conversionOK := sampleOK && sec >= 0 && nsec >= 0 && nsec < 1_000_000_000 && nsec%100 == 0 &&
sec <= (math.MaxInt64-nsec)/1_000_000_000 && before <= math.MaxInt64/100 && after <= math.MaxInt64/100
withinNative := false
if conversionOK && monotone {
ns := sec*1_000_000_000 + nsec
withinNative = uint64(ns) >= before*100 && uint64(ns) <= after*100
}
sampleBootMatches := stable && sampleOK && boot == canonical
t.Logf("native precise contract_load_available=%t contract_export_available=%t counter_nonzero=%t counter_monotone=%t sample_available=%t sample_boot_matches_native=%t conversion_valid=%t sample_within_native_reads=%t",
contractLoadOK, contractExportOK, counterNonzero, monotone, sampleOK, sampleBootMatches, conversionOK, withinNative)
if !statusOK || retLen != expectedLength || !nonzero || !canonicalOK || !stable || !trueMatches {
t.Error("native boot contract unavailable; see bounded predicates")
}
if !contractLoadOK || !contractExportOK || !counterNonzero || !monotone || !sampleBootMatches || !conversionOK || !withinNative {
t.Error("native precise contract unavailable; see bounded predicates")
}
}
48 changes: 38 additions & 10 deletions internal/agentnotify/journal/clock_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,23 +33,48 @@ type bootEnvironmentInfo struct {
}

func WindowsBootSample() (boot string, sec, nsec int64, ok bool) {
boot, ok = windowsBootIdentity(false)
if !ok {
return "", 0, 0, false
}
sec, nsec, ok = windowsInterruptTime()
if !ok {
return "", 0, 0, false
}
return boot, sec, nsec, true
}

func windowsBootIdentity(requireLength bool) (string, bool) {
var info bootEnvironmentInfo
var retLen uint32
if err := windows.NtQuerySystemInformation(windows.SystemBootEnvironmentInformation, unsafe.Pointer(&info), uint32(unsafe.Sizeof(info)), &retLen); err != nil {
return "", 0, 0, false
return "", false
}
if info.BootIdentifier == [16]byte{} {
return "", 0, 0, false
// Preserve the existing journal/notifier path; the new precise export
// additionally requires a complete native structure before using its GUID.
if (requireLength && retLen != uint32(unsafe.Sizeof(info))) || info.BootIdentifier == [16]byte{} {
return "", false
}
boot = formatWindowsGUID(info.BootIdentifier)
boot := formatWindowsGUID(info.BootIdentifier)
if !validText(boot, 256, true) {
return "", false
}
return boot, true
}

// WindowsPreciseBootSample uses the existing interrupt-time epoch; no coarse
// fallback and no accuracy claim derived from its 100ns representation.
func WindowsPreciseBootSample() (boot string, sec, nsec int64, ok bool) {
boot, ok = windowsBootIdentity(true)
if !ok || queryInterruptTimePrecise.Find() != nil {
return "", 0, 0, false
}
sec, nsec, ok = windowsInterruptTime()
if !ok {
var t uint64
_, _, _ = queryInterruptTimePrecise.Call(uintptr(unsafe.Pointer(&t)))
if t == 0 {
return "", 0, 0, false
}
return boot, sec, nsec, true
return boot, int64(t / 10_000_000), int64(t%10_000_000) * 100, true
}

func formatWindowsGUID(g [16]byte) string {
Expand All @@ -59,10 +84,13 @@ func formatWindowsGUID(g [16]byte) string {
return fmt.Sprintf("%08x-%04x-%04x-%02x%02x-%02x%02x%02x%02x%02x%02x", d1, d2, d3, g[8], g[9], g[10], g[11], g[12], g[13], g[14], g[15])
}

// Precise export contract: https://learn.microsoft.com/en-us/uwp/win32-and-com/win32-apis#apis-from-api-ms-win-core-realtime-l1-1-1dll
var (
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
queryInterruptTime = kernel32.NewProc("QueryInterruptTime")
getTickCount64 = kernel32.NewProc("GetTickCount64")
kernel32 = windows.NewLazySystemDLL("kernel32.dll")
realtime = windows.NewLazySystemDLL("api-ms-win-core-realtime-l1-1-1.dll")
queryInterruptTimePrecise = realtime.NewProc("QueryInterruptTimePrecise")
queryInterruptTime = kernel32.NewProc("QueryInterruptTime")
getTickCount64 = kernel32.NewProc("GetTickCount64")
)

func windowsInterruptTime() (sec, nsec int64, ok bool) {
Expand Down
Loading
Loading