mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
* fix(agent): abort on image-rejection 400s instead of retrying in a loop
* feat(imageinput): add ReadClipboardImage for OS clipboard image extraction
* feat(tui): clipboard image paste — detect and attach screenshots from OS clipboard
* fix(imageinput): use temp file for Windows clipboard image (PowerShell stdout can't emit raw binary)
* test: handle both image-present and image-absent clipboard states
* fix(vision): add kimi-k2 and qwen-vl to vision-capable name heuristic
* fix(tui): render image errors as red error rows instead of grey system notes
* feat(vision): check models.dev InputModalities for vision support instead of only name heuristics
The vision gate now checks three sources:
1. Curated model registry (catalog authority + name heuristic)
2. Discovered model list from models.dev (InputModalities contains 'image')
3. Name heuristic fallback for unknown models
This means custom/ollama/cloud models (kimi-k2, qwen-vl, etc.) are correctly
detected as vision-capable when models.dev reports them with image input
modality — no more manual name matching needed.
* chore: gofmt clipboard_test.go
* fix(imageinput): remove shell injection in the Linux clipboard reader
The Linux clipboard reader built shell commands by concatenating a
clipboard-derived MIME type into `sh -c`:
exec.Command("sh", "-c", "wl-paste --type "+t+" 2>/dev/null")
exec.Command("sh", "-c", "xclip ... -t "+t+" -o 2>/dev/null")
`t` comes from `wl-paste --list-types` / `xclip TARGETS` and is only filtered by
`HasPrefix(t, "image/")`, so a hostile clipboard offerer could register a target
like `image/png; rm -rf ~` — it passes the prefix check and then executes
through the shell. A real (if low-likelihood) local code-exec vector.
Drop the shell entirely: every helper now runs via exec.Command(prog, args...)
with the MIME type passed as a discrete argument (matching the existing no-shell
pattern in pdf.go). Stderr is discarded by not assigning it. Factored into
runClipboardStdout + imageMIMETypes helpers.
Test: imageMIMETypes filters to image/* lines and returns even an
injection-shaped type verbatim (safe now that it never reaches a shell).
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
* test(tools): de-flake TestWriteStdinInterruptTerminatesSession
The test had two Windows-CI flakes that reddened unrelated PRs:
1. It relied on write_stdin's 1000ms yield window being long enough for the
async SIGKILL + reap to land before asserting the session was no longer
running. On a loaded runner the reap exceeded that window, so write_stdin
reported "still running" and the assertion failed. Fix: after terminating,
wait on session.done deterministically (30s safety timeout that fails loudly
if the kill genuinely hangs); the common case returns the instant the
process exits.
2. The SIGKILL'd child had the test's t.TempDir() as its cwd, and Windows may
not release the directory handle the instant the process is reaped — so
t.TempDir()'s immediate RemoveAll on cleanup failed with "being used by
another process". Fix: resilientTempDir retries RemoveAll for up to 5s
(best-effort; a leaked temp dir never fails the test).
Verified 20x locally; full internal/tools suite green.
Co-Authored-By: Claude Opus 4.8 (1M context) <[email protected]>
---------
Co-authored-by: Claude Opus 4.8 (1M context) <[email protected]>
49 lines
1.6 KiB
Go
49 lines
1.6 KiB
Go
package imageinput
|
|
|
|
import (
|
|
"reflect"
|
|
"testing"
|
|
)
|
|
|
|
func TestImageMIMETypesFiltersAndPreservesRaw(t *testing.T) {
|
|
// Only image/* lines are kept, trimmed. Crucially, a hostile/odd type is
|
|
// returned VERBATIM (not sanitized) — safety comes from never passing it
|
|
// through a shell, so the filter must not silently mangle a real type.
|
|
list := []byte("text/plain\nimage/png\n image/jpeg \nTARGETS\n" +
|
|
"image/png; rm -rf ~\n")
|
|
got := imageMIMETypes(list)
|
|
want := []string{"image/png", "image/jpeg", "image/png; rm -rf ~"}
|
|
if !reflect.DeepEqual(got, want) {
|
|
t.Fatalf("imageMIMETypes = %#v, want %#v", got, want)
|
|
}
|
|
}
|
|
|
|
func TestImageMIMETypesEmpty(t *testing.T) {
|
|
if got := imageMIMETypes([]byte("text/plain\nTARGETS\n")); got != nil {
|
|
t.Fatalf("expected no image types, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestReadClipboardImage(t *testing.T) {
|
|
// ReadClipboardImage either returns nil (no image) or valid image bytes.
|
|
// On CI there's no image → nil. On a dev machine with a screenshot copied,
|
|
// it returns real bytes. Both paths are valid — the test verifies whichever
|
|
// one the clipboard produces.
|
|
data, mediaType, err := ReadClipboardImage()
|
|
if err != nil {
|
|
t.Fatalf("unexpected error: %v", err)
|
|
}
|
|
if data == nil {
|
|
// No image in clipboard — valid, mediaType must be empty.
|
|
if mediaType != "" {
|
|
t.Fatalf("expected empty media type when no image, got %q", mediaType)
|
|
}
|
|
return
|
|
}
|
|
// Image found — mediaType must be a supported type.
|
|
validTypes := map[string]bool{"image/png": true, "image/jpeg": true, "image/gif": true, "image/webp": true}
|
|
if !validTypes[mediaType] {
|
|
t.Errorf("mediaType = %q, want one of png/jpeg/gif/webp", mediaType)
|
|
}
|
|
}
|