checkpoint: multi-repo Gitea ingestion, per-project repos, rotation anchor_sha fix
Pre-existing uncommitted work found at session start: rotation now emits anchor_sha on TaskReleased (previously silently dropped by store.Append validation), multi-repo Gitea provider support, per-project git worktree roots, and associated test coverage. Committing as a checkpoint before starting remediation work tracked in AUDIT.md.
This commit is contained in:
@@ -17,7 +17,11 @@ var ErrConflict = errors.New("task version conflict")
|
||||
var ErrNotFound = errors.New("task not found")
|
||||
var ErrInvalid = errors.New("invalid event")
|
||||
|
||||
const CurrentEventSchema = 1
|
||||
// CurrentEventSchema is 2: schema 2 requires every event to declare its
|
||||
// authorizing Surface (see ValidateEvent), enforced at the store append
|
||||
// boundary. Schema 1 events already on disk replay unchanged — tolerant
|
||||
// reader, not upcast (spec open question #2).
|
||||
const CurrentEventSchema = 2
|
||||
|
||||
type TaskState string
|
||||
|
||||
@@ -63,6 +67,12 @@ type Event struct {
|
||||
Version int `json:"version"`
|
||||
At time.Time `json:"at"`
|
||||
Payload json.RawMessage `json:"payload"`
|
||||
// Surface identifies the bus capability the emitter is authorized under
|
||||
// (see internal/authz). It is required on every event so authorization is
|
||||
// enforced once, at the store append boundary, regardless of whether the
|
||||
// emitter reached the store over HTTP, from the router, from a harness
|
||||
// adapter, or from a provider.
|
||||
Surface string `json:"surface"`
|
||||
}
|
||||
|
||||
func Hash(v []byte) string { h := sha256.Sum256(v); return hex.EncodeToString(h[:]) }
|
||||
@@ -82,6 +92,9 @@ func ValidateEvent(e Event) error {
|
||||
if e.SchemaVersion > CurrentEventSchema || e.Type == "" || e.TaskID == "" || len(e.Payload) == 0 || len(e.Payload) > 64*1024 {
|
||||
return ErrInvalid
|
||||
}
|
||||
if e.SchemaVersion >= 2 && strings.TrimSpace(e.Surface) == "" {
|
||||
return fmt.Errorf("%w: surface required", ErrInvalid)
|
||||
}
|
||||
allowed := map[string]bool{"TaskCreated": true, "TaskLeased": true, "TaskReleased": true, "TaskCompleted": true, "TaskFailed": true, "TaskBlocked": true, "ApprovalRequested": true, "ApprovalGranted": true, "ApprovalDenied": true, "TaskAmended": true, "QuotaReported": true, "StandupAdvisory": true}
|
||||
if !allowed[e.Type] {
|
||||
return fmt.Errorf("%w: unknown type %q", ErrInvalid, e.Type)
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package domain
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// eventTypesUnderTest is the full lifecycle vocabulary the spec (§9 item 5)
|
||||
// requires validators for; a validator that panics or accepts garbage for any
|
||||
// of these on adversarial input is a defect regardless of whether real
|
||||
// producers happen to send well-formed payloads.
|
||||
var eventTypesUnderTest = []string{
|
||||
"TaskCreated", "TaskLeased", "TaskReleased", "TaskCompleted", "TaskFailed",
|
||||
"TaskBlocked", "ApprovalRequested", "ApprovalGranted", "ApprovalDenied",
|
||||
"TaskAmended", "QuotaReported", "StandupAdvisory",
|
||||
}
|
||||
|
||||
// FuzzValidatePayload feeds arbitrary JSON object shapes at every known event
|
||||
// type's validator and requires it to either return a typed ErrInvalid or
|
||||
// accept — never panic. The seed corpus below exercises adjacent-to-valid and
|
||||
// wildly-malformed shapes (wrong types, huge strings, nested structures,
|
||||
// nulls, NaN-adjacent floats via JSON) for each type.
|
||||
func FuzzValidatePayload(f *testing.F) {
|
||||
seeds := []string{
|
||||
`{}`,
|
||||
`null`,
|
||||
`{"source":"jsonl","external_id":"1","project":"p"}`,
|
||||
`{"source":123,"external_id":null,"project":[]}`,
|
||||
`{"harness_id":"h1","ttl":60,"expected_version":1}`,
|
||||
`{"harness_id":"h1","until_ns":1e300,"expected_version":1.5}`,
|
||||
`{"handoff_ref":"` + fakeHash() + `","anchor_sha":"` + fakeSHA() + `"}`,
|
||||
`{"handoff_ref":123,"anchor_sha":true}`,
|
||||
`{"report_ref":"` + fakeHash() + `","receipt":{"harness_id":"h1","consumed":1}}`,
|
||||
`{"report_ref":"","receipt":{}}`,
|
||||
`{"reason":"x"}`,
|
||||
`{"reason":123}`,
|
||||
`{"blocker":"x","handoff_ref":"` + fakeHash() + `"}`,
|
||||
`{"blocker":""}`,
|
||||
`{"amendment":"x"}`,
|
||||
`{"subject_ref":"x","options":["a","b"]}`,
|
||||
`{"subject_ref":123,"options":null}`,
|
||||
`{"harness_id":"h1","consumed":90.5}`,
|
||||
`{"harness_id":"h1","consumed":-1}`,
|
||||
`{"harness_id":"h1","consumed":"a lot"}`,
|
||||
`{"items":["standup line"]}`,
|
||||
`{"items":null}`,
|
||||
`{"a":{"b":{"c":{"d":[1,2,3,{"e":"f"}]}}}}`,
|
||||
`{"x":` + hugeString() + `}`,
|
||||
}
|
||||
for _, s := range seeds {
|
||||
f.Add(s)
|
||||
}
|
||||
|
||||
f.Fuzz(func(t *testing.T, raw string) {
|
||||
var p map[string]any
|
||||
if err := json.Unmarshal([]byte(raw), &p); err != nil {
|
||||
return // not a JSON object; ValidateEvent itself rejects non-objects before reaching ValidatePayload
|
||||
}
|
||||
for _, typ := range eventTypesUnderTest {
|
||||
func() {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("ValidatePayload(%q, %s) panicked: %v", typ, raw, r)
|
||||
}
|
||||
}()
|
||||
err := ValidatePayload(typ, p)
|
||||
if err != nil && err != ErrInvalid {
|
||||
// must still be a typed validation error, wrapping ErrInvalid
|
||||
if !isInvalid(err) {
|
||||
t.Fatalf("ValidatePayload(%q, %s) returned non-typed error: %v", typ, raw, err)
|
||||
}
|
||||
}
|
||||
}()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzValidateEvent exercises the full envelope path (schema version, surface
|
||||
// requirement, type allow-list, payload size/parseability) with arbitrary
|
||||
// type names, surfaces, and payload bytes, proving no combination panics.
|
||||
func FuzzValidateEvent(f *testing.F) {
|
||||
f.Add("TaskCreated", "system", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
|
||||
f.Add("", "", 0, []byte(``))
|
||||
f.Add("Bogus", "system", 2, []byte(`{}`))
|
||||
f.Add("TaskCreated", "", 2, []byte(`{"source":"jsonl","external_id":"1","project":"p"}`))
|
||||
f.Add("TaskCreated", "system", 1, []byte(`not json`))
|
||||
f.Add("QuotaReported", "system", 2, []byte(`null`))
|
||||
f.Add("TaskCompleted", "system", 99, []byte(`{"report_ref":"x","receipt":{}}`))
|
||||
|
||||
f.Fuzz(func(t *testing.T, typ, surface string, schema int, payload []byte) {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
t.Fatalf("ValidateEvent panicked: type=%q surface=%q schema=%d payload=%q: %v", typ, surface, schema, payload, r)
|
||||
}
|
||||
}()
|
||||
e := Event{
|
||||
SchemaVersion: schema,
|
||||
Type: typ,
|
||||
TaskID: "t1",
|
||||
Payload: payload,
|
||||
Surface: surface,
|
||||
}
|
||||
_ = ValidateEvent(e)
|
||||
})
|
||||
}
|
||||
|
||||
func isInvalid(err error) bool {
|
||||
for e := err; e != nil; {
|
||||
if e == ErrInvalid {
|
||||
return true
|
||||
}
|
||||
u, ok := e.(interface{ Unwrap() error })
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
e = u.Unwrap()
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func fakeHash() string {
|
||||
b := make([]byte, 32)
|
||||
for i := range b {
|
||||
b[i] = byte(i)
|
||||
}
|
||||
s := ""
|
||||
for _, c := range b {
|
||||
s += string("0123456789abcdef"[c>>4]) + string("0123456789abcdef"[c&0xf])
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func fakeSHA() string {
|
||||
s := ""
|
||||
for i := 0; i < 40; i++ {
|
||||
s += "a"
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func hugeString() string {
|
||||
b, _ := json.Marshal(make([]byte, 0))
|
||||
_ = b
|
||||
s := `"`
|
||||
for i := 0; i < 5000; i++ {
|
||||
s += "x"
|
||||
}
|
||||
return s + `"`
|
||||
}
|
||||
Reference in New Issue
Block a user