mirror of
https://github.com/Twigpine/zero.git
synced 2026-11-10 13:11:31 -10:00
Update the Go module path, imports, install scripts, docs, workflows, packaging and action defaults from Gitlawb/zero to Twigpine/zero. The npm scope (already @twigpine/zero on main, with its @gitlawb/zero migration notes), LICENSE, action author, the OpenGateway provider and historical CHANGELOG links are unchanged. The X-AIMLAPI-Integration-Repo header keeps Gitlawb/zero: it is what AIMLAPI sees for attribution, so changing it needs a maintainer's yes. docs/INSTALL.md keeps `go install github.com/Gitlawb/zero/...`, because every tagged release so far declares that module path. The Homebrew formula comment names the Twigpine/homebrew-tap tap. Co-authored-by: Claude Opus 5.5 <[email protected]>
124 lines
4.7 KiB
Go
124 lines
4.7 KiB
Go
package tools
|
|
|
|
import (
|
|
"path/filepath"
|
|
"strings"
|
|
"unicode"
|
|
"unicode/utf8"
|
|
|
|
"github.com/Twigpine/zero/internal/redaction"
|
|
udiff "github.com/aymanbagabas/go-udiff"
|
|
)
|
|
|
|
// maxToolPreviewBytes caps the inline diff a write tool appends to its result, so
|
|
// a large generated file can't flood the transcript or balloon the persisted
|
|
// session events. Past this the tool falls back to its summary line alone.
|
|
const maxToolPreviewBytes = 48 * 1024
|
|
|
|
// A structured replacement contains two complete file sides, so its transport
|
|
// budget is intentionally separate from the single rendered-preview budget.
|
|
// Each side may be as large as a normal preview; aggregate producers apply the
|
|
// two-sided result cap in file order.
|
|
const maxToolResultFileDiffBytes = 2 * maxToolPreviewBytes
|
|
|
|
// FileDiff is a human-facing before/after file change. Registry-boundary
|
|
// redaction applies to both sides before any caller receives it.
|
|
type FileDiff struct {
|
|
// Path is the canonical absolute path required by ACP diff content. The
|
|
// separate ChangedFiles result remains workspace-relative for local UI use.
|
|
Path string
|
|
// OldExists and NewExists distinguish an empty file from a missing side of a
|
|
// create/delete/move. Empty strings alone cannot encode that difference.
|
|
OldExists bool
|
|
NewExists bool
|
|
OldText string
|
|
NewText string
|
|
}
|
|
|
|
// boundedFileDiff declines rather than truncating: a truncated side would look
|
|
// like an exact file replacement. Callers keep ChangedFiles as the safe
|
|
// fallback for large, unsafe, or unchanged content. Each complete side gets the
|
|
// same bound as a rendered preview; aggregate result producers apply their own
|
|
// cap. Control bytes are rejected. Ordinary Unicode format/space characters are
|
|
// retained unless removing them reveals a credential shape that the normal
|
|
// redactor could not see in the original text.
|
|
func boundedFileDiff(path, oldText, newText string, oldExists, newExists bool) (FileDiff, bool) {
|
|
if !filepath.IsAbs(path) || (!oldExists && !newExists) ||
|
|
(oldExists == newExists && oldText == newText) ||
|
|
!utf8.ValidString(oldText) || !utf8.ValidString(newText) ||
|
|
unsafeDiffText(oldText) || unsafeDiffText(newText) ||
|
|
len(oldText) > maxToolPreviewBytes || len(newText) > maxToolPreviewBytes {
|
|
return FileDiff{}, false
|
|
}
|
|
return FileDiff{Path: path, OldExists: oldExists, NewExists: newExists, OldText: oldText, NewText: newText}, true
|
|
}
|
|
|
|
func unsafeDiffText(text string) bool {
|
|
if !utf8.ValidString(text) {
|
|
return true
|
|
}
|
|
hasCanonicalizableSeparator := false
|
|
for _, r := range text {
|
|
switch r {
|
|
case '\n', '\r', '\t', ' ':
|
|
continue
|
|
}
|
|
if unicode.IsControl(r) {
|
|
return true
|
|
}
|
|
if isDiffCredentialIgnorable(r) {
|
|
hasCanonicalizableSeparator = true
|
|
}
|
|
}
|
|
if !hasCanonicalizableSeparator {
|
|
return false
|
|
}
|
|
return diffTextRevealsObfuscatedSecret(text)
|
|
}
|
|
|
|
func diffTextRevealsObfuscatedSecret(text string) bool {
|
|
if !utf8.ValidString(text) {
|
|
return false
|
|
}
|
|
canonical := strings.Map(func(r rune) rune {
|
|
switch r {
|
|
case '\n', '\r', '\t', ' ':
|
|
return r
|
|
}
|
|
if isDiffCredentialIgnorable(r) {
|
|
return -1
|
|
}
|
|
return r
|
|
}, text)
|
|
return canonical != text && redaction.RedactString(canonical, redaction.Options{}) != canonical
|
|
}
|
|
|
|
// isDiffCredentialIgnorable mirrors Unicode's Default_Ignorable_Code_Point
|
|
// derived property using the tables exposed by the Go standard library. Cf
|
|
// catches format controls, while the other two tables cover non-Cf default
|
|
// ignorables such as U+034F COMBINING GRAPHEME JOINER and variation selectors.
|
|
// Unicode whitespace is also removable because it can split a credential that
|
|
// the boundary redactor would otherwise recognize. The caller preserves normal
|
|
// ASCII layout whitespace before consulting this helper.
|
|
func isDiffCredentialIgnorable(r rune) bool {
|
|
return unicode.Is(unicode.Cf, r) ||
|
|
unicode.Is(unicode.Other_Default_Ignorable_Code_Point, r) ||
|
|
unicode.Is(unicode.Variation_Selector, r) ||
|
|
unicode.IsSpace(r)
|
|
}
|
|
|
|
// boundedUnifiedDiff returns a unified diff of oldContent -> newContent labelled
|
|
// with path, suitable for the TUI's diff card renderer. A create (oldContent "")
|
|
// yields an all-additions (green) preview; an overwrite/edit yields red/green.
|
|
// Returns "" when there is no change, the rendered diff is unsafe text, or the
|
|
// diff exceeds maxToolPreviewBytes. This applies the same unsafe-text gate as
|
|
// FileDiff to what reaches Display.Preview, without discarding a safe hunk only
|
|
// because an unrelated part of the source file contains an unsafe byte.
|
|
func boundedUnifiedDiff(path, oldContent, newContent string) string {
|
|
diff := udiff.Unified(path, path, oldContent, newContent)
|
|
if diff == "" || !utf8.ValidString(diff) || unsafeDiffText(diff) || len(diff) > maxToolPreviewBytes {
|
|
return ""
|
|
}
|
|
return diff
|
|
}
|