Files
Agent-Native/adapters/pitransport/resultv1.go
T
weichenandCursor e65c6ba604 Align execution errors with the shared catalog and add optional logs.
Contract codes now use the agent.* values, and runtime can emit structured logs without changing results when no logger is configured.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-29 14:24:11 +00:00

181 lines
4.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package pitransport
import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"path/filepath"
"strings"
)
const (
ResultV1SchemaVersion = 1
MaxResultMembers = 32
MaxResultMemberBytes = 2 << 20
MaxResultBodyBytes = 8 << 20
RunnerProtocolLegacy = "legacy"
RunnerProtocolTypedV1 = "typed-v1"
)
var ErrTypedResult = fmt.Errorf("pitransport: typed result_v1 rejected")
// 对外错误码与 Error-Code-System 的 agent 域一致。
// CANCELLED 并入 agent.canceled,MISSING_TARGET 并入 agent.missing_deliverable。
var allowedErrorCodes = map[string]struct{}{
"agent.prompt_empty": {},
"agent.unauthorized": {},
"agent.timeout": {},
"agent.canceled": {},
"agent.transient": {},
"agent.missing_deliverable": {},
"agent.run_failed": {},
}
var retryableErrorCodes = map[string]struct{}{
"agent.timeout": {},
"agent.canceled": {},
"agent.transient": {},
}
type ResultV1Member struct {
Kind string `json:"kind"`
Path string `json:"path"`
Digest string `json:"digest"`
Size int `json:"size"`
}
type ResultV1 struct {
SchemaVersion int `json:"schema_version"`
RunID string `json:"run_id"`
Outcome string `json:"outcome"`
Retryable bool `json:"retryable"`
ErrorCode string `json:"error_code,omitempty"`
Members []ResultV1Member `json:"members"`
ContentDigest string `json:"content_digest"`
}
func SafeRelPath(p string) bool {
p = strings.TrimSpace(p)
if p == "" || filepath.IsAbs(p) {
return false
}
clean := filepath.ToSlash(filepath.Clean(p))
if clean == "." || clean == ".." || strings.HasPrefix(clean, "../") || strings.Contains(p, "..") {
return false
}
return true
}
func DigestBytes(b []byte) string {
sum := sha256.Sum256(b)
return hex.EncodeToString(sum[:])
}
func (r ResultV1) MembersDigest() string {
type row struct {
Kind string `json:"kind"`
Path string `json:"path"`
Digest string `json:"digest"`
Size int `json:"size"`
}
members := r.Members
if members == nil {
members = []ResultV1Member{}
}
rows := make([]row, 0, len(members))
for _, m := range members {
rows = append(rows, row{m.Kind, m.Path, m.Digest, m.Size})
}
raw, _ := json.Marshal(rows)
return DigestBytes(raw)
}
func (r ResultV1) Validate(expectedRunID string, patches map[string][]byte) error {
if r.SchemaVersion != ResultV1SchemaVersion {
return fmt.Errorf("%w: schema", ErrTypedResult)
}
if strings.TrimSpace(r.RunID) == "" {
return fmt.Errorf("%w: run_id", ErrTypedResult)
}
if expectedRunID != "" && r.RunID != expectedRunID {
return fmt.Errorf("%w: run mismatch", ErrTypedResult)
}
if r.Outcome != "success" && r.Outcome != "error" {
return fmt.Errorf("%w: outcome", ErrTypedResult)
}
if r.Outcome == "success" && strings.TrimSpace(r.ErrorCode) != "" {
return fmt.Errorf("%w: error_code", ErrTypedResult)
}
if r.Outcome == "error" {
if _, ok := allowedErrorCodes[r.ErrorCode]; !ok {
return fmt.Errorf("%w: error_code", ErrTypedResult)
}
}
if len(r.Members) > MaxResultMembers {
return fmt.Errorf("%w: members", ErrTypedResult)
}
total := 0
seen := map[string]struct{}{}
for _, m := range r.Members {
if m.Kind != "file" || !SafeRelPath(m.Path) {
return fmt.Errorf("%w: member path", ErrTypedResult)
}
if _, dup := seen[m.Path]; dup {
return fmt.Errorf("%w: duplicate member", ErrTypedResult)
}
seen[m.Path] = struct{}{}
if m.Size < 0 || m.Size > MaxResultMemberBytes {
return fmt.Errorf("%w: member size", ErrTypedResult)
}
total += m.Size
body, ok := patches[m.Path]
if !ok {
return fmt.Errorf("%w: member missing body", ErrTypedResult)
}
if len(body) != m.Size || DigestBytes(body) != m.Digest {
return fmt.Errorf("%w: digest", ErrTypedResult)
}
}
if total > MaxResultBodyBytes {
return fmt.Errorf("%w: body", ErrTypedResult)
}
if r.ContentDigest != r.MembersDigest() {
return fmt.Errorf("%w: content_digest", ErrTypedResult)
}
return nil
}
func TypedRetryable(r ResultV1) bool {
if r.Outcome != "error" || !r.Retryable {
return false
}
_, ok := retryableErrorCodes[r.ErrorCode]
return ok
}
func BuildResultV1(runID, outcome string, retryable bool, errorCode string, patches []struct {
RelPath string
Content []byte
}) ResultV1 {
members := make([]ResultV1Member, 0, len(patches))
for _, p := range patches {
if !SafeRelPath(p.RelPath) {
continue
}
members = append(members, ResultV1Member{
Kind: "file", Path: p.RelPath, Digest: DigestBytes(p.Content), Size: len(p.Content),
})
}
r := ResultV1{
SchemaVersion: ResultV1SchemaVersion,
RunID: runID,
Outcome: outcome,
Retryable: retryable,
ErrorCode: errorCode,
Members: members,
}
r.ContentDigest = r.MembersDigest()
return r
}