Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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.84
github.com/codefly-dev/core v0.2.85
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
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -95,8 +95,8 @@ github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJ
github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/codefly-dev/core v0.2.84 h1:twrXt66ogiKS+icEPImJ1Jh2IcTLMgVrwchvUmyfaso=
github.com/codefly-dev/core v0.2.84/go.mod h1:hNxTk7ZnR5AU8imfvNJcGpoULQ1uzML5M3E4/RffWU4=
github.com/codefly-dev/core v0.2.85 h1:FcOKENGLHW2tbRomMNxbJDMscePS0ARa2Y4+44P3F5k=
github.com/codefly-dev/core v0.2.85/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=
Expand Down
5 changes: 5 additions & 0 deletions pkg/engine/host_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -296,6 +296,11 @@ func TestCodeRetryOnlyReplaysReadOnlyOperations(t *testing.T) {
}) {
t.Fatal("ReadFile should be safe to retry")
}
if !codeRequestRetrySafe(&codev0.CodeRequest{
Operation: &codev0.CodeRequest_GetSourceManifest{GetSourceManifest: &codev0.GetSourceManifestRequest{}},
}) {
t.Fatal("GetSourceManifest should be safe to retry")
}
if codeRequestRetrySafe(&codev0.CodeRequest{
Operation: &codev0.CodeRequest_WriteFile{WriteFile: &codev0.WriteFileRequest{Path: "README.md"}},
}) {
Expand Down
1 change: 1 addition & 0 deletions pkg/engine/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ func codeRequestRetrySafe(request *codev0.CodeRequest) bool {
switch request.GetOperation().(type) {
case *codev0.CodeRequest_ListDependencies,
*codev0.CodeRequest_GetProjectInfo,
*codev0.CodeRequest_GetSourceManifest,
*codev0.CodeRequest_ReadFile,
*codev0.CodeRequest_ListFiles,
*codev0.CodeRequest_Search,
Expand Down
37 changes: 37 additions & 0 deletions pkg/gateway/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -1385,6 +1385,36 @@ func (s *Server) GetSemanticIndex(ctx context.Context, req *gatewayv1.GetSemanti
}, nil
}

// GetSourceManifest invokes the language-neutral rooted Code behavior so every
// workspace has the same body-free artifact inventory regardless of which
// language agents are installed. The Gateway forwards typed identities only;
// it never reads or reconstructs project bytes itself.
func (s *Server) GetSourceManifest(ctx context.Context, req *gatewayv1.GetSourceManifestRequest) (*gatewayv1.GetSourceManifestResponse, error) {
requestedService, revision := "", ""
identityMode := basev0.SourceManifestIdentityMode_SOURCE_MANIFEST_IDENTITY_MODE_NATIVE
if req != nil {
requestedService = req.GetService()
revision = req.GetRevision()
identityMode = req.GetIdentityMode()
}
if err := s.validateService(requestedService); err != nil {
return nil, err
}
response, err := s.sourceExecute(ctx, &codev0.CodeRequest{
Operation: &codev0.CodeRequest_GetSourceManifest{GetSourceManifest: &codev0.GetSourceManifestRequest{Revision: revision, IdentityMode: identityMode}},
})
if err != nil {
return &gatewayv1.GetSourceManifestResponse{Failure: gatewaySourceManifestFailure(err)}, nil
}
value := response.GetGetSourceManifest()
if value == nil {
return &gatewayv1.GetSourceManifestResponse{
Failure: failures.Ensure(response.GetFailure(), basev0.FailureCode_FAILURE_CODE_INTERNAL, "gateway.get-source-manifest", "source behavior returned no manifest"),
}, nil
}
return &gatewayv1.GetSourceManifestResponse{Manifest: value, Failure: failures.Clone(response.GetFailure())}, nil
}

func cloneCodeUnitTarget(target *gatewayv1.CodeUnitTarget) *gatewayv1.CodeUnitTarget {
if target == nil {
return nil
Expand All @@ -1406,6 +1436,13 @@ func gatewaySemanticIndexFailure(err error) *basev0.Failure {
return failures.FromError("gateway.get-semantic-index", err)
}

func gatewaySourceManifestFailure(err error) *basev0.Failure {
if failure, ok := failures.Extract(err); ok {
return failures.Clone(failure)
}
return failures.FromError("gateway.get-source-manifest", err)
}

// DiscoverCodeUnits serves Codefly's language-neutral structural inventory
// directly from the rooted source behavior. Discovery must not first select or
// launch a language plugin: the inventory is what downstream consumers use to
Expand Down
146 changes: 146 additions & 0 deletions pkg/gateway/source_manifest_integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
package gateway

import (
"crypto/sha256"
"encoding/hex"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"

basev0 "github.com/codefly-dev/core/generated/go/codefly/base/v0"
gatewayv1 "github.com/codefly-dev/core/generated/go/mind/gateway/v1"
)

// TestGatewaySourceManifestUsesRootedCodeflySource proves the production
// Gateway returns exact worktree and revision identities without requiring or
// selecting a language agent. It uses a real filesystem and real Git history.
func TestGatewaySourceManifestUsesRootedCodeflySource(t *testing.T) {
root := t.TempDir()
writeGatewaySourceFile(t, root, "README.md", "base\n", 0o644)
writeGatewaySourceFile(t, root, "bin/run", "#!/bin/sh\n", 0o755)
if err := os.Symlink("README.md", filepath.Join(root, "readme-link")); err != nil {
t.Fatalf("create real symlink: %v", err)
}
gitGatewaySource(t, root, "init", "-b", "main")
gitGatewaySource(t, root, "config", "commit.gpgsign", "false")
gitGatewaySource(t, root, "add", ".")
gitGatewaySource(t, root, "commit", "-m", "base")
baseRevision := gitGatewaySource(t, root, "rev-parse", "HEAD")
writeGatewaySourceFile(t, root, "README.md", "worktree\n", 0o644)
writeGatewaySourceFile(t, root, "new.txt", "new\n", 0o644)

server, err := NewServer(Config{WorkDir: root})
if err != nil {
t.Fatal(err)
}
t.Cleanup(func() { _ = server.Close() })

worktree, err := server.GetSourceManifest(t.Context(), &gatewayv1.GetSourceManifestRequest{})
if err != nil {
t.Fatalf("worktree source manifest: %v", err)
}
if worktree.GetFailure() != nil {
t.Fatalf("worktree source manifest failure: %+v", worktree.GetFailure())
}
worktreeEntries := gatewaySourceEntriesByPath(worktree.GetManifest())
if len(worktreeEntries) != 4 {
t.Fatalf("worktree paths = %v, want four", gatewaySourcePaths(worktree.GetManifest()))
}
assertGatewaySHA256Entry(t, worktreeEntries["README.md"], "worktree\n")
assertGatewaySHA256Entry(t, worktreeEntries["new.txt"], "new\n")
if got := worktreeEntries["bin/run"]; got.GetMode() != 0o100755 {
t.Fatalf("worktree executable = %+v", got)
}
if got := worktreeEntries["readme-link"]; got.GetKind() != basev0.SourceEntryKind_SOURCE_ENTRY_KIND_SYMLINK || got.GetMode() != 0o120000 {
t.Fatalf("worktree symlink = %+v", got)
}

revision, err := server.GetSourceManifest(t.Context(), &gatewayv1.GetSourceManifestRequest{Revision: baseRevision})
if err != nil {
t.Fatalf("revision source manifest: %v", err)
}
if revision.GetFailure() != nil {
t.Fatalf("revision source manifest failure: %+v", revision.GetFailure())
}
if revision.GetManifest().GetRevision() != baseRevision {
t.Fatalf("resolved revision = %q, want %q", revision.GetManifest().GetRevision(), baseRevision)
}
revisionEntries := gatewaySourceEntriesByPath(revision.GetManifest())
if len(revisionEntries) != 3 || revisionEntries["new.txt"] != nil {
t.Fatalf("revision paths = %v, want committed source only", gatewaySourcePaths(revision.GetManifest()))
}
if got := revisionEntries["README.md"]; got.GetIdentity().GetAlgorithm() != basev0.SourceIdentityAlgorithm_SOURCE_IDENTITY_ALGORITHM_GIT_BLOB_SHA1 || got.GetIdentity().GetDigest() == worktreeEntries["README.md"].GetIdentity().GetDigest() {
t.Fatalf("revision README identity = %+v", got)
}

contentRevision, err := server.GetSourceManifest(t.Context(), &gatewayv1.GetSourceManifestRequest{
Revision: baseRevision,
IdentityMode: basev0.SourceManifestIdentityMode_SOURCE_MANIFEST_IDENTITY_MODE_CONTENT_SHA256,
})
if err != nil {
t.Fatalf("content-normalized revision source manifest: %v", err)
}
if contentRevision.GetFailure() != nil {
t.Fatalf("content-normalized revision failure: %+v", contentRevision.GetFailure())
}
contentEntries := gatewaySourceEntriesByPath(contentRevision.GetManifest())
assertGatewaySHA256Entry(t, contentEntries["README.md"], "base\n")
if got := contentEntries["README.md"].GetAttributes(); got.GetContentKind() != basev0.SourceContentKind_SOURCE_CONTENT_KIND_TEXT || got.GetSourceRole() != basev0.SourceRole_SOURCE_ROLE_DOCS {
t.Fatalf("content-normalized README attributes = %+v", got)
}
}

func writeGatewaySourceFile(t *testing.T, root, relative, body string, mode os.FileMode) {
t.Helper()
path := filepath.Join(root, filepath.FromSlash(relative))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
t.Fatalf("mkdir %s: %v", relative, err)
}
if err := os.WriteFile(path, []byte(body), mode); err != nil {
t.Fatalf("write %s: %v", relative, err)
}
}

func gitGatewaySource(t *testing.T, root string, args ...string) string {
t.Helper()
command := exec.Command("git", args...)
command.Dir = root
command.Env = append(command.Environ(),
"GIT_AUTHOR_NAME=codefly", "GIT_AUTHOR_EMAIL=test@codefly.dev",
"GIT_COMMITTER_NAME=codefly", "GIT_COMMITTER_EMAIL=test@codefly.dev",
)
output, err := command.CombinedOutput()
if err != nil {
t.Fatalf("git %v: %v\n%s", args, err, output)
}
return strings.TrimSpace(string(output))
}

func gatewaySourceEntriesByPath(value *basev0.SourceManifest) map[string]*basev0.SourceManifestEntry {
entries := make(map[string]*basev0.SourceManifestEntry, len(value.GetEntries()))
for _, entry := range value.GetEntries() {
entries[entry.GetPath()] = entry
}
return entries
}

func gatewaySourcePaths(value *basev0.SourceManifest) []string {
paths := make([]string, 0, len(value.GetEntries()))
for _, entry := range value.GetEntries() {
paths = append(paths, entry.GetPath())
}
return paths
}

func assertGatewaySHA256Entry(t *testing.T, entry *basev0.SourceManifestEntry, body string) {
t.Helper()
if entry == nil {
t.Fatal("source manifest entry is missing")
}
digest := sha256.Sum256([]byte(body))
if entry.GetIdentity().GetAlgorithm() != basev0.SourceIdentityAlgorithm_SOURCE_IDENTITY_ALGORITHM_SHA256 || entry.GetIdentity().GetDigest() != hex.EncodeToString(digest[:]) {
t.Fatalf("source manifest entry = %+v", entry)
}
}
Loading