diff --git a/go.mod b/go.mod index 07c83a8b..042e831c 100644 --- a/go.mod +++ b/go.mod @@ -11,7 +11,7 @@ require ( github.com/asottile/dockerfile v3.1.0+incompatible github.com/blang/semver v3.5.1+incompatible github.com/briandowns/spinner v1.23.2 - github.com/codefly-dev/core v0.2.87 + github.com/codefly-dev/core v0.2.89 github.com/codefly-dev/golor v0.1.3 github.com/codefly-dev/llm v0.1.0 github.com/codefly-dev/sdk-go v0.1.58 diff --git a/go.sum b/go.sum index 7733e17b..3b841962 100644 --- a/go.sum +++ b/go.sum @@ -97,6 +97,8 @@ github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4= github.com/codefly-dev/core v0.2.87 h1:bPRCC5ZmT/JLpEz20ZpI8UKbdpoSRexmf7eTq5nl6lE= github.com/codefly-dev/core v0.2.87/go.mod h1:hNxTk7ZnR5AU8imfvNJcGpoULQ1uzML5M3E4/RffWU4= +github.com/codefly-dev/core v0.2.89 h1:+kA6ACb/4Eit7xk7SLO9E2eTsW23Ti/H3QwO9/80je0= +github.com/codefly-dev/core v0.2.89/go.mod h1:hNxTk7ZnR5AU8imfvNJcGpoULQ1uzML5M3E4/RffWU4= github.com/codefly-dev/golor v0.1.3 h1:xmo+ceyJFRYZdvpWE2fNd0jeaadp/Ibm1BnganiGKOc= github.com/codefly-dev/golor v0.1.3/go.mod h1:sl/u/K1l7J0Pr3xyVZp8fOJYQItKKst1No9JqgzLLoY= github.com/codefly-dev/gortk v0.2.0 h1:7bOlS5valYz2zil+fZctQNcPCYBcPj86abcw9N8h1hQ= diff --git a/pkg/gateway/prepared_mutation.go b/pkg/gateway/prepared_mutation.go index 547f43b0..95321862 100644 --- a/pkg/gateway/prepared_mutation.go +++ b/pkg/gateway/prepared_mutation.go @@ -141,6 +141,7 @@ func (s *Server) PrepareMutation(ctx context.Context, req *gatewayv1.PrepareMuta var fixActions []string var after []byte var previewBeforeHash, previewAfterHash string + var previewBeforeSize, previewAfterSize uint64 if edit != nil { path, err = cleanGatewayPath(edit.GetFile()) if err != nil || path == "" { @@ -168,6 +169,7 @@ func (s *Server) PrepareMutation(ctx context.Context, req *gatewayv1.PrepareMuta after = []byte(preview.GetContent()) strategy, fixActions = preview.GetStrategy(), append([]string(nil), preview.GetFixActions()...) previewBeforeHash, previewAfterHash = preview.GetBeforeSha256(), preview.GetAfterSha256() + previewBeforeSize, previewAfterSize = preview.GetBeforeSizeBytes(), preview.GetAfterSizeBytes() } else { path, err = cleanGatewayPath(symbolPatch.GetFile()) if err != nil || path == "" { @@ -203,6 +205,7 @@ func (s *Server) PrepareMutation(ctx context.Context, req *gatewayv1.PrepareMuta after = []byte(preview.GetContent()) strategy, fixActions = preview.GetStrategy(), append([]string(nil), preview.GetFixActions()...) previewBeforeHash, previewAfterHash = preview.GetBeforeSha256(), preview.GetAfterSha256() + previewBeforeSize, previewAfterSize = preview.GetBeforeSizeBytes(), preview.GetAfterSizeBytes() } current, err := s.fileOps().ReadFile(ctx, path) if err != nil { @@ -210,8 +213,8 @@ func (s *Server) PrepareMutation(ctx context.Context, req *gatewayv1.PrepareMuta } beforeHash := contentSHA256(current) afterHash := contentSHA256(after) - if previewBeforeHash != beforeHash || previewAfterHash != afterHash { - return prepareFailure("language agent preview hashes do not match authoritative project bytes"), nil + if previewBeforeHash != beforeHash || previewAfterHash != afterHash || previewBeforeSize != uint64(len(current)) || previewAfterSize != uint64(len(after)) { + return prepareFailure("language agent preview identities do not match authoritative project bytes"), nil } prepared := &gatewayv1.PreparedMutation{ SchemaVersion: preparedMutationSchemaVersion, @@ -223,6 +226,7 @@ func (s *Server) PrepareMutation(ctx context.Context, req *gatewayv1.PrepareMuta Files: []*gatewayv1.PreparedFileMutation{{ Path: path, Operation: gatewayv1.PreparedFileOperation_PREPARED_FILE_OPERATION_MODIFY, BeforeSha256: beforeHash, AfterSha256: afterHash, + BeforeSizeBytes: uint64(len(current)), AfterSizeBytes: uint64(len(after)), Strategy: strategy, FixActions: fixActions, SymbolId: symbolID, }}, PreparedAt: timestamppb.Now(), ExpiresAt: timestamppb.New(time.Now().UTC().Add(preparedMutationLifetime)), @@ -281,7 +285,7 @@ func (s *Server) ApplyPreparedMutation(ctx context.Context, req *gatewayv1.Apply if err != nil { return applyPreparedFailure(fmt.Sprintf("read prepared target %q: %v", file.GetPath(), err)), nil } - if contentSHA256(current) != file.GetBeforeSha256() { + if contentSHA256(current) != file.GetBeforeSha256() || uint64(len(current)) != file.GetBeforeSizeBytes() { return applyPreparedFailure(fmt.Sprintf("prepared target %q drifted after preparation", file.GetPath())), nil } } @@ -296,7 +300,7 @@ func (s *Server) ApplyPreparedMutation(ctx context.Context, req *gatewayv1.Apply } for _, file := range prepared.GetFiles() { after, ok := afterByPath[file.GetPath()] - if !ok || contentSHA256(after) != file.GetAfterSha256() { + if !ok || contentSHA256(after) != file.GetAfterSha256() || uint64(len(after)) != file.GetAfterSizeBytes() { return applyPreparedFailure(fmt.Sprintf("prepared bytes for %q are unavailable or corrupted", file.GetPath())), nil } response, err := s.proxyExecute(ctx, &codev0.CodeRequest{Operation: &codev0.CodeRequest_WriteFile{WriteFile: &codev0.WriteFileRequest{ @@ -314,6 +318,7 @@ func (s *Server) ApplyPreparedMutation(ctx context.Context, req *gatewayv1.Apply applied = append(applied, &gatewayv1.AppliedFileMutation{ Path: file.GetPath(), Operation: file.GetOperation(), BeforeSha256: file.GetBeforeSha256(), AfterSha256: file.GetAfterSha256(), + BeforeSizeBytes: file.GetBeforeSizeBytes(), AfterSizeBytes: file.GetAfterSizeBytes(), }) } s.deletePreparedMutation(prepared.GetPreparationId(), prepared.GetMutationDigest()) @@ -348,8 +353,8 @@ func (s *Server) storePreparedMutation(prepared *gatewayv1.PreparedMutation, aft storedByteCount := 0 for _, file := range prepared.GetFiles() { content, ok := afterByPath[file.GetPath()] - if !ok || contentSHA256(content) != file.GetAfterSha256() { - return fmt.Errorf("prepared bytes for %q do not match after_sha256", file.GetPath()) + if !ok || contentSHA256(content) != file.GetAfterSha256() || uint64(len(content)) != file.GetAfterSizeBytes() { + return fmt.Errorf("prepared bytes for %q do not match the after identity", file.GetPath()) } if len(content) > maxPreparedMutationBytes || storedByteCount > maxPreparedMutationBytes-len(content) { return fmt.Errorf("prepared mutation exceeds the %d-byte retention limit", maxPreparedMutationBytes) diff --git a/pkg/gateway/prepared_mutation_test.go b/pkg/gateway/prepared_mutation_test.go index d2912b41..c90d90d1 100644 --- a/pkg/gateway/prepared_mutation_test.go +++ b/pkg/gateway/prepared_mutation_test.go @@ -42,6 +42,10 @@ func TestPreparedMutationRequiresPinnedAuthorityAndAppliesSignedPermitOnce(t *te if prepared.GetFiles()[0].ProtoReflect().Descriptor().Fields().ByName("after_content") != nil { t.Fatal("prepared mutation RPC exposes project bytes") } + fileIdentity := prepared.GetFiles()[0] + if fileIdentity.GetBeforeSizeBytes() != uint64(len("package service\n\nfunc Value() int { return 1 }\n")) || fileIdentity.GetAfterSizeBytes() != uint64(len("package service\n\nfunc Value() int { return 2 }\n")) { + t.Fatalf("prepared mutation sizes = before:%d after:%d", fileIdentity.GetBeforeSizeBytes(), fileIdentity.GetAfterSizeBytes()) + } if prepared.GetExpiresAt() == nil || !prepared.GetExpiresAt().AsTime().After(time.Now().UTC()) { t.Fatalf("prepared mutation has no future expiry: %v", prepared.GetExpiresAt()) } @@ -127,6 +131,9 @@ func TestPreparedSymbolPatchRetainsBytesAndRequiresExactSymbolFence(t *testing.T if len(prepared.GetFiles()) != 1 || prepared.GetFiles()[0].GetSymbolId() != "symbol-service-value" { t.Fatalf("prepared symbol resource = %+v", prepared.GetFiles()) } + if prepared.GetFiles()[0].GetBeforeSizeBytes() != uint64(len(before)) || prepared.GetFiles()[0].GetAfterSizeBytes() != uint64(len("package service\n\nfunc Value() int { return 2 }\n")) { + t.Fatalf("prepared symbol sizes = before:%d after:%d", prepared.GetFiles()[0].GetBeforeSizeBytes(), prepared.GetFiles()[0].GetAfterSizeBytes()) + } unchanged, err := os.ReadFile(path) if err != nil || string(unchanged) != before { t.Fatalf("preparation changed source: content=%q err=%v", unchanged, err) @@ -256,6 +263,7 @@ func TestPreparedMutationRetentionRejectsOversizedResults(t *testing.T) { Files: []*gatewayv1.PreparedFileMutation{{ Path: "main.go", Operation: gatewayv1.PreparedFileOperation_PREPARED_FILE_OPERATION_MODIFY, BeforeSha256: contentSHA256([]byte("before")), AfterSha256: contentSHA256(after), + BeforeSizeBytes: uint64(len("before")), AfterSizeBytes: uint64(len(after)), }}, PreparedAt: timestamppb.New(now), ExpiresAt: timestamppb.New(now.Add(preparedMutationLifetime)), } diff --git a/pkg/gateway/server.go b/pkg/gateway/server.go index 3d3671d8..93e53d87 100644 --- a/pkg/gateway/server.go +++ b/pkg/gateway/server.go @@ -942,6 +942,7 @@ func (s *Server) Fix(ctx context.Context, req *gatewayv1.FixRequest) (*gatewayv1 Success: r.GetSuccess(), Content: r.GetContent(), Error: codeFailureMessage(resp), Actions: r.GetActions(), Changed: r.GetChanged(), BeforeSha256: r.GetBeforeSha256(), AfterSha256: r.GetAfterSha256(), Wrote: r.GetWrote(), Output: r.GetOutput(), + BeforeSizeBytes: r.GetBeforeSizeBytes(), AfterSizeBytes: r.GetAfterSizeBytes(), }, nil } @@ -1012,6 +1013,7 @@ func (s *Server) applyEdit( Strategy: result.GetStrategy(), FixActions: result.GetFixActions(), Changed: result.GetChanged(), BeforeSha256: result.GetBeforeSha256(), AfterSha256: result.GetAfterSha256(), Wrote: result.GetWrote(), Output: result.GetOutput(), + BeforeSizeBytes: result.GetBeforeSizeBytes(), AfterSizeBytes: result.GetAfterSizeBytes(), }, nil } @@ -1058,6 +1060,7 @@ func (s *Server) applyEditWithReceipt( Strategy: rawResult.GetStrategy(), FixActions: rawResult.GetFixActions(), Changed: rawResult.GetChanged(), BeforeSha256: rawResult.GetBeforeSha256(), AfterSha256: rawResult.GetAfterSha256(), Wrote: rawResult.GetWrote(), Output: rawResult.GetOutput(), + BeforeSizeBytes: rawResult.GetBeforeSizeBytes(), AfterSizeBytes: rawResult.GetAfterSizeBytes(), } stage := executionv1.ExecutionStage_EXECUTION_STAGE_FAILED statusValue := "failed" @@ -1160,6 +1163,7 @@ func gatewaySymbolPatchResponse(raw *codev0.CodeResponse) *gatewayv1.ApplySymbol BeforeSha256: result.GetBeforeSha256(), AfterSha256: result.GetAfterSha256(), DeclarationSha256: result.GetDeclarationSha256(), Wrote: result.GetWrote(), Output: result.GetOutput(), Failure: failures.Clone(raw.GetFailure()), FailureReason: result.GetFailureReason(), + BeforeSizeBytes: result.GetBeforeSizeBytes(), AfterSizeBytes: result.GetAfterSizeBytes(), } } diff --git a/pkg/gateway/server_test.go b/pkg/gateway/server_test.go index addd9158..6db4e11d 100644 --- a/pkg/gateway/server_test.go +++ b/pkg/gateway/server_test.go @@ -244,6 +244,7 @@ func editingMock(t *testing.T, dir string) *mockCodeClient { return &codev0.ApplyEditResponse{ Success: true, Content: content, Strategy: "exact", Changed: content != string(original), Wrote: !in.GetDryRun(), BeforeSha256: hex.EncodeToString(beforeDigest[:]), AfterSha256: hex.EncodeToString(afterDigest[:]), + BeforeSizeBytes: uint64(len(original)), AfterSizeBytes: uint64(len(content)), }, nil }} } @@ -615,6 +616,7 @@ func TestFix(t *testing.T) { Content: "package main\n\nimport \"fmt\"\n\nfunc main() { fmt.Println(\"hello\") }", Actions: []string{"goimports", "gofmt"}, BeforeSha256: "before", AfterSha256: "after", + BeforeSizeBytes: 12, AfterSizeBytes: 13, }, nil }, } @@ -634,7 +636,7 @@ func TestFix(t *testing.T) { if resp.Actions[0] != "goimports" { t.Errorf("expected first action 'goimports', got %s", resp.Actions[0]) } - if !resp.GetChanged() || resp.GetWrote() || resp.GetBeforeSha256() != "before" || resp.GetAfterSha256() != "after" { + if !resp.GetChanged() || resp.GetWrote() || resp.GetBeforeSha256() != "before" || resp.GetAfterSha256() != "after" || resp.GetBeforeSizeBytes() != 12 || resp.GetAfterSizeBytes() != 13 { t.Fatalf("gateway dropped fix evidence: %+v", resp) } } @@ -657,6 +659,9 @@ func TestApplyEdit(t *testing.T) { if resp.Strategy != "exact" { t.Errorf("expected strategy 'exact', got %s", resp.Strategy) } + if resp.GetBeforeSizeBytes() != uint64(len("package main\n\nold code\n")) || resp.GetAfterSizeBytes() != uint64(len("package main\n\nnew code\n")) { + t.Fatalf("gateway dropped edit sizes: %+v", resp) + } content, err := os.ReadFile(filepath.Join(dir, "main.go")) if err != nil { t.Fatal(err)