Compare commits

..
Author SHA1 Message Date
anandh8x daabe11164 fix(providers): send stable session headers to OpenCode 2026-09-15 18:02:59 +05:30
2 changed files with 0 additions and 198 deletions

No files matched your search

-42
View File
@@ -246,62 +246,20 @@ func RedactResult(result Result) Result {
return redacted
}
// redactMetadata redacts EVERY text field of the metadata, not the subset a
// reader happens to think is user-facing. `zero search --json` prints this
// straight to stdout, and the fields carrying the most free text are the ones
// least likely to be on a hand-kept list: SpecUserComment and SpecRejectReason
// are review text the user typed, SpecDraftReasoning is the model's own prose,
// and Goal.Objective and Goal.StatusReason are whatever the goal was phrased
// as. A field added to sessions.Metadata later is covered by
// TestRedactMetadataCoversEveryTextField, which walks the struct rather than
// repeating this list, so the two cannot drift apart silently.
func redactMetadata(session sessions.Metadata, options redaction.Options) sessions.Metadata {
session.SessionID = redaction.RedactString(session.SessionID, options)
session.SessionKind = sessions.SessionKind(redaction.RedactString(string(session.SessionKind), options))
session.Title = redaction.RedactString(session.Title, options)
session.Cwd = redaction.RedactString(session.Cwd, options)
session.ModelID = redaction.RedactString(session.ModelID, options)
session.Provider = redaction.RedactString(session.Provider, options)
session.Tag = redaction.RedactString(session.Tag, options)
session.ParentSessionID = redaction.RedactString(session.ParentSessionID, options)
session.RootSessionID = redaction.RedactString(session.RootSessionID, options)
session.AgentName = redaction.RedactString(session.AgentName, options)
session.TaskID = redaction.RedactString(session.TaskID, options)
session.ForkedFromEventID = redaction.RedactString(session.ForkedFromEventID, options)
session.SpawnedFromEventID = redaction.RedactString(session.SpawnedFromEventID, options)
session.SpecID = redaction.RedactString(session.SpecID, options)
session.SpecFilePath = redaction.RedactString(session.SpecFilePath, options)
session.SpecStatus = sessions.SpecStatus(redaction.RedactString(string(session.SpecStatus), options))
session.SpecDraftModelID = redaction.RedactString(session.SpecDraftModelID, options)
session.SpecDraftReasoning = redaction.RedactString(session.SpecDraftReasoning, options)
session.SpecUserComment = redaction.RedactString(session.SpecUserComment, options)
session.SpecRejectReason = redaction.RedactString(session.SpecRejectReason, options)
session.SpecSourceSessionID = redaction.RedactString(session.SpecSourceSessionID, options)
session.SpecImplSessionID = redaction.RedactString(session.SpecImplSessionID, options)
session.CreatedAt = redaction.RedactString(session.CreatedAt, options)
session.UpdatedAt = redaction.RedactString(session.UpdatedAt, options)
session.LastEventType = sessions.EventType(redaction.RedactString(string(session.LastEventType), options))
session.Goal = redactGoal(session.Goal, options)
return session
}
// redactGoal returns a redacted COPY. Metadata is passed by value, but Goal is
// a pointer inside it, so redacting through it would rewrite the goal the
// caller still holds: a search would quietly edit the live session record it
// was only supposed to report on.
func redactGoal(goal *sessions.Goal, options redaction.Options) *sessions.Goal {
if goal == nil {
return nil
}
redacted := *goal
redacted.Objective = redaction.RedactString(redacted.Objective, options)
redacted.Status = sessions.GoalStatus(redaction.RedactString(string(redacted.Status), options))
redacted.StatusReason = redaction.RedactString(redacted.StatusReason, options)
redacted.CreatedAt = redaction.RedactString(redacted.CreatedAt, options)
redacted.UpdatedAt = redaction.RedactString(redacted.UpdatedAt, options)
return &redacted
}
func redactEventSummary(event EventSummary, options redaction.Options) EventSummary {
event.ID = redaction.RedactString(event.ID, options)
event.Type = sessions.EventType(redaction.RedactString(string(event.Type), options))
-156
View File
@@ -1,15 +1,12 @@
package search
import (
"fmt"
"os"
"path/filepath"
"reflect"
"strings"
"testing"
"time"
"github.com/Gitlawb/zero/internal/redaction"
"github.com/Gitlawb/zero/internal/sessions"
)
@@ -110,156 +107,3 @@ func fixedSearchClock(value string) func() time.Time {
}
return func() time.Time { return parsed }
}
// EVERY TEXT FIELD, NOT A HAND-KEPT SUBSET.
//
// `zero search --json` prints Hit.Session straight to stdout, so a field
// redactMetadata forgets is a field that reaches the terminal with whatever was
// in it. The list in redactMetadata is written out by hand; this walks the
// struct instead, so a string field added to sessions.Metadata (or to the Goal
// hanging off it) fails here on the day it lands rather than the day someone
// notices it in output. It named twenty uncovered fields when it was written,
// including the review text the user types and the model's own draft reasoning.
func TestRedactMetadataCoversEveryTextField(t *testing.T) {
const secret = "sk-ant-api03-aaaaaaaaaaaaaaaaaaaaaaaa0123456789ABCD"
// The premise: the secret is one RedactString already recognizes, so a field
// that still carries it was not passed through the redactor at all.
if redaction.RedactString(secret, redaction.Options{}) == secret {
t.Fatalf("SETUP INVALID: %q is not a shape RedactString redacts, so every leg below passes vacuously", secret)
}
var session sessions.Metadata
filled := fillMetadataText(t, reflect.ValueOf(&session).Elem(), secret, 0)
var leaked []string
var checked int
walkMetadataText(t, reflect.ValueOf(redactMetadata(session, redaction.Options{})), "", 0, func(name, value string) {
checked++
if strings.Contains(value, secret) {
leaked = append(leaked, name)
}
})
// Filling and walking are separate traversals of the same shape, so a field
// one of them understands and the other does not would otherwise show up as
// a quiet pass rather than a failure.
if checked != filled {
t.Fatalf("SETUP INVALID: filled %d text fields and walked %d, so the two traversals disagree about the shape of Metadata", filled, checked)
}
if filled == 0 {
t.Fatal("SETUP INVALID: no text fields were populated, so this proves nothing")
}
if len(leaked) > 0 {
t.Errorf("redactMetadata left the secret in %d of %d text fields: %s",
len(leaked), checked, strings.Join(leaked, ", "))
}
}
// fillMetadataText writes text into every text-bearing field reachable from
// value, allocating nil pointers and one-element slices on the way, and returns
// how many strings it set.
//
// Recursive on purpose. The first version of this walked only the direct string
// fields of one struct and hand-allocated the Goal next to it, which left the
// guard with the same blind spot it exists to close: a nested struct added to
// sessions.Metadata later would arrive empty, the walk would find nothing in it
// and the test would pass while the new field went unredacted. Raised by
// CodeRabbit on the PR.
//
// It fails rather than skips on a kind it does not understand, because a field
// this cannot populate is a field the guard is not guarding.
func fillMetadataText(t *testing.T, value reflect.Value, text string, depth int) int {
t.Helper()
if depth > 6 {
t.Fatalf("fillMetadataText recursed past depth %d, which suggests a cycle in the type", depth)
}
if !value.CanSet() {
return 0
}
switch value.Kind() {
case reflect.String:
value.SetString(text)
return 1
case reflect.Pointer:
if value.IsNil() {
value.Set(reflect.New(value.Type().Elem()))
}
return fillMetadataText(t, value.Elem(), text, depth+1)
case reflect.Struct:
filled := 0
for index := 0; index < value.NumField(); index++ {
if !value.Type().Field(index).IsExported() {
continue
}
filled += fillMetadataText(t, value.Field(index), text, depth+1)
}
return filled
case reflect.Slice:
value.Set(reflect.MakeSlice(value.Type(), 1, 1))
return fillMetadataText(t, value.Index(0), text, depth+1)
case reflect.Bool, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64,
reflect.Float32, reflect.Float64:
return 0
default:
t.Fatalf("fillMetadataText does not know how to populate %s (kind %s); teach it and walkMetadataText together, or this guard stops covering that field",
value.Type(), value.Kind())
return 0
}
}
// walkMetadataText visits every text-bearing field reachable from value, in the
// same shape fillMetadataText populates. The two are deliberately symmetric and
// the caller compares their counts.
func walkMetadataText(t *testing.T, value reflect.Value, prefix string, depth int, visit func(name, value string)) {
t.Helper()
if depth > 6 {
t.Fatalf("walkMetadataText recursed past depth %d, which suggests a cycle in the type", depth)
}
switch value.Kind() {
case reflect.String:
visit(prefix, value.String())
case reflect.Pointer:
if !value.IsNil() {
walkMetadataText(t, value.Elem(), prefix, depth+1, visit)
}
case reflect.Struct:
for index := 0; index < value.NumField(); index++ {
field := value.Type().Field(index)
if !field.IsExported() {
continue
}
name := field.Name
if prefix != "" {
name = prefix + "." + name
}
walkMetadataText(t, value.Field(index), name, depth+1, visit)
}
case reflect.Slice:
for index := 0; index < value.Len(); index++ {
walkMetadataText(t, value.Index(index), fmt.Sprintf("%s[%d]", prefix, index), depth+1, visit)
}
}
}
// Metadata is passed by value, but Goal is a pointer inside it. Redacting
// through that pointer would edit the session the caller still holds, so a
// search would quietly rewrite the live record it was asked to report on.
func TestRedactMetadataDoesNotRewriteTheCallersGoal(t *testing.T) {
const secret = "sk-ant-api03-aaaaaaaaaaaaaaaaaaaaaaaa0123456789ABCD"
goal := &sessions.Goal{Objective: "ship " + secret, StatusReason: "blocked on " + secret}
session := sessions.Metadata{SessionID: "s", Goal: goal}
redacted := redactMetadata(session, redaction.Options{})
if goal.Objective != "ship "+secret || goal.StatusReason != "blocked on "+secret {
t.Errorf("redactMetadata rewrote the caller's goal: %+v", *goal)
}
if redacted.Goal == goal {
t.Error("redacted metadata still points at the caller's goal")
}
if strings.Contains(redacted.Goal.Objective, secret) || strings.Contains(redacted.Goal.StatusReason, secret) {
t.Errorf("goal text was not redacted: %+v", *redacted.Goal)
}
}