194 lines
5.5 KiB
Go
194 lines
5.5 KiB
Go
package agentguard
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
const maxGitPointerBytes = 64 * 1024
|
|
|
|
type gitMetadataError struct {
|
|
code BlockerCode
|
|
message string
|
|
}
|
|
|
|
func (e *gitMetadataError) Error() string {
|
|
return e.message
|
|
}
|
|
|
|
func discoverGitMetadata(taskRoot string, mode IsolationMode) ([]canonicalPath, *gitMetadataError) {
|
|
dotGit := filepath.Join(taskRoot, ".git")
|
|
info, err := os.Lstat(dotGit)
|
|
if err != nil {
|
|
if os.IsNotExist(err) && mode == IsolationModeOverlay {
|
|
return nil, nil
|
|
}
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeWorkspaceNotCanonical,
|
|
message: "task isolation is missing the Git metadata required by its mode",
|
|
}
|
|
}
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "task .git entry cannot be a symbolic link",
|
|
}
|
|
}
|
|
|
|
if info.IsDir() {
|
|
if mode == IsolationModeWorktree {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree isolation requires a .git pointer file",
|
|
}
|
|
}
|
|
metadata, canonicalErr := canonicalDirectory(dotGit, false)
|
|
if canonicalErr != nil || !containsPath(taskRoot, metadata.path) {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "full-clone Git metadata must remain inside the task root",
|
|
}
|
|
}
|
|
return []canonicalPath{metadata}, nil
|
|
}
|
|
if mode == IsolationModeClone {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "full-clone isolation requires an internal .git directory",
|
|
}
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "task .git entry has an unsupported file type",
|
|
}
|
|
}
|
|
|
|
return parseGitPointer(taskRoot, dotGit)
|
|
}
|
|
|
|
func discoverEffectiveGitMetadata(taskRoot, workingDir string) ([]canonicalPath, *gitMetadataError) {
|
|
curr := workingDir
|
|
for curr != taskRoot && containsPath(taskRoot, curr) {
|
|
dotGit := filepath.Join(curr, ".git")
|
|
info, err := os.Lstat(dotGit)
|
|
if err == nil {
|
|
if info.Mode()&os.ModeSymlink != 0 {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "task .git entry cannot be a symbolic link",
|
|
}
|
|
}
|
|
if info.IsDir() {
|
|
metadata, canonicalErr := canonicalDirectory(dotGit, false)
|
|
if canonicalErr != nil {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "nested Git metadata is unavailable",
|
|
}
|
|
}
|
|
return []canonicalPath{metadata}, nil
|
|
}
|
|
if !info.Mode().IsRegular() {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "task .git entry has an unsupported file type",
|
|
}
|
|
}
|
|
return parseGitPointer(curr, dotGit)
|
|
}
|
|
if !os.IsNotExist(err) {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "nested Git metadata is inaccessible",
|
|
}
|
|
}
|
|
parent := filepath.Dir(curr)
|
|
if parent == curr {
|
|
break
|
|
}
|
|
curr = parent
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func parseGitPointer(parentDir, dotGit string) ([]canonicalPath, *gitMetadataError) {
|
|
gitDirValue, readErr := readPointerFile(dotGit, "gitdir:")
|
|
if readErr != nil {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree .git pointer is invalid",
|
|
}
|
|
}
|
|
gitDir, canonicalErr := canonicalDirectory(resolvePointer(parentDir, gitDirValue), false)
|
|
if canonicalErr != nil {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree Git directory is unavailable",
|
|
}
|
|
}
|
|
metadata := []canonicalPath{gitDir}
|
|
|
|
commonPath := filepath.Join(gitDir.path, "commondir")
|
|
if commonInfo, statErr := os.Lstat(commonPath); statErr == nil {
|
|
if !commonInfo.Mode().IsRegular() || commonInfo.Mode()&os.ModeSymlink != 0 {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree common-dir pointer is invalid",
|
|
}
|
|
}
|
|
commonValue, pointerErr := readPointerFile(commonPath, "")
|
|
if pointerErr != nil {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree common-dir pointer is invalid",
|
|
}
|
|
}
|
|
commonDir, commonErr := canonicalDirectory(resolvePointer(gitDir.path, commonValue), false)
|
|
if commonErr != nil {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree common Git directory is unavailable",
|
|
}
|
|
}
|
|
if commonDir.path != gitDir.path {
|
|
metadata = append(metadata, commonDir)
|
|
}
|
|
} else if !os.IsNotExist(statErr) {
|
|
return nil, &gitMetadataError{
|
|
code: BlockerCodeVCSMetadataNotAllowed,
|
|
message: "worktree common-dir metadata is inaccessible",
|
|
}
|
|
}
|
|
return metadata, nil
|
|
}
|
|
|
|
func readPointerFile(path, prefix string) (string, error) {
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if len(content) > maxGitPointerBytes {
|
|
return "", fmt.Errorf("pointer file is too large")
|
|
}
|
|
value := strings.TrimSpace(string(content))
|
|
if prefix != "" {
|
|
if !strings.HasPrefix(strings.ToLower(value), prefix) {
|
|
return "", fmt.Errorf("missing %s prefix", prefix)
|
|
}
|
|
value = strings.TrimSpace(value[len(prefix):])
|
|
}
|
|
if value == "" || containsParentReference(value) && filepath.IsAbs(value) {
|
|
return "", fmt.Errorf("empty or invalid pointer")
|
|
}
|
|
return value, nil
|
|
}
|
|
|
|
func resolvePointer(parent, value string) string {
|
|
if filepath.IsAbs(value) {
|
|
return filepath.Clean(value)
|
|
}
|
|
return filepath.Clean(filepath.Join(parent, value))
|
|
}
|