Remote /v1/run now emits additive result_v1; Go validates run_id, digest, and paths before materializing, and only accepts missing typed payloads with an explicit legacy handshake. Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.4 KiB
Go
136 lines
4.4 KiB
Go
package agent
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestDecodeRemoteRun_TypedSuccess(t *testing.T) {
|
|
body := []byte("hello")
|
|
rv := BuildResultV1("ws1", "success", false, "", []struct {
|
|
RelPath string
|
|
Content []byte
|
|
}{{"output.md", body}})
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "run_id": "ws1", "runner_protocol": "typed-v1",
|
|
"capabilities": map[string]any{"typed_result": true},
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": "hello"}},
|
|
"result_v1": rv,
|
|
})
|
|
got, err := DecodeRemoteRun(raw, "ws1", true)
|
|
if err != nil || !got.OK || got.Legacy {
|
|
t.Fatalf("typed success: err=%v ok=%v legacy=%v", err, got != nil && got.OK, got != nil && got.Legacy)
|
|
}
|
|
if string(got.Incoming["output.md"]) != "hello" {
|
|
t.Fatal("patch body")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_TypedPresentInvalidNoFallback(t *testing.T) {
|
|
rv := BuildResultV1("ws1", "success", false, "", []struct {
|
|
RelPath string
|
|
Content []byte
|
|
}{{"output.md", []byte("hello")}})
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "run_id": "ws1", "runner_protocol": "typed-v1",
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": "TAMPER"}},
|
|
"result_v1": rv,
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("tampered typed body must fail closed")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_RunMismatch(t *testing.T) {
|
|
rv := BuildResultV1("other-run", "success", false, "", []struct {
|
|
RelPath string
|
|
Content []byte
|
|
}{{"output.md", []byte("hello")}})
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "run_id": "other-run",
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": "hello"}},
|
|
"result_v1": rv,
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("foreign run_id must fail closed")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_HandshakeDowngrade(t *testing.T) {
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "run_id": "ws1",
|
|
"capabilities": map[string]any{"typed_result": true},
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": "hello"}},
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("typed capability without result_v1 is a downgrade attack")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_MissingTypedWithoutLegacyHandshake(t *testing.T) {
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "patches": []map[string]string{{"rel_path": "output.md", "content": "hello"}},
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("implicit legacy must fail after P4")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_LegacyHandshakeStillWorks(t *testing.T) {
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "runner_protocol": "legacy",
|
|
"fallback_reason": "Connection error.",
|
|
"retryable": true,
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": "hello"}},
|
|
})
|
|
got, err := DecodeRemoteRun(raw, "ws1", true)
|
|
if err != nil || !got.OK || !got.Legacy {
|
|
t.Fatalf("legacy handshake: err=%v got=%+v", err, got)
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_LegacyDoesNotLeakPaths(t *testing.T) {
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": false, "runner_protocol": "legacy",
|
|
"fallback_reason": "failed /tmp/secret/token=abc",
|
|
"patches": []any{},
|
|
})
|
|
got, err := DecodeRemoteRun(raw, "ws1", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got.OK {
|
|
t.Fatal("want failure")
|
|
}
|
|
if strings.Contains(got.Reason, "/tmp") || strings.Contains(got.Reason, "token") {
|
|
t.Fatalf("leaked reason %q", got.Reason)
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_PathEscapeMember(t *testing.T) {
|
|
rv := ResultV1{
|
|
SchemaVersion: 1, RunID: "ws1", Outcome: "success",
|
|
Members: []ResultV1Member{{Kind: "file", Path: "../etc/passwd", Digest: DigestBytes([]byte("x")), Size: 1}},
|
|
}
|
|
rv.ContentDigest = rv.MembersDigest()
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "result_v1": rv,
|
|
"patches": []map[string]string{{"rel_path": "../etc/passwd", "content": "x"}},
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("escaped member path must fail closed")
|
|
}
|
|
}
|
|
|
|
func TestDecodeRemoteRun_OversizedMember(t *testing.T) {
|
|
big := strings.Repeat("a", MaxResultMemberBytes+1)
|
|
raw, _ := json.Marshal(map[string]any{
|
|
"ok": true, "runner_protocol": "legacy",
|
|
"patches": []map[string]string{{"rel_path": "output.md", "content": big}},
|
|
})
|
|
if _, err := DecodeRemoteRun(raw, "ws1", true); err == nil {
|
|
t.Fatal("oversized member must fail closed")
|
|
}
|
|
}
|