iop/apps/node/internal/workspace/path.go
toki dc9a9a8c59 feat(agent): 단일 요청 Agent 실행 경계를 구현한다
승인된 execution preset을 Edge 조정 경계와 Node workspace/tool 실행 경계로 연결해 단일 요청 수명주기와 관측 계약을 일관되게 처리한다.
2026-08-07 07:03:55 +09:00

113 lines
3 KiB
Go

package workspace
import (
"errors"
"io/fs"
"os"
"path"
"strings"
)
var (
errInvalidPath = errors.New("workspace path is invalid")
errReservedPath = errors.New("workspace path is reserved")
errUnsafePath = errors.New("workspace path is unsafe")
errNotFound = errors.New("workspace path not found")
)
type targetIdentity struct {
exists bool
device uint64
inode uint64
mode fs.FileMode
}
// userPath accepts a canonical relative path only. .iop is private runtime
// state: no caller-facing operation can name it or a child beneath it.
func userPath(value string) (string, error) {
if value == "" || strings.Contains(value, "\\") || path.IsAbs(value) || path.Clean(value) != value {
return "", errInvalidPath
}
if value == "." {
return value, nil
}
if strings.HasPrefix(value, "../") || value == ".." {
return "", errInvalidPath
}
first := strings.Split(value, "/")[0]
if first == ".iop" {
return "", errReservedPath
}
return value, nil
}
// internalPath is deliberately unexported. It is available only to future
// request-owned runtime artifacts and cannot name sibling request namespaces.
func (r *Request) internalPath(value string) (string, error) {
if value == "" || path.IsAbs(value) || path.Clean(value) != value || value == "." || strings.HasPrefix(value, "../") || value == ".." {
return "", errInvalidPath
}
prefix := r.internalPrefix + "/"
if value != r.internalPrefix && !strings.HasPrefix(value, prefix) {
return "", errReservedPath
}
return value, nil
}
func checkedExisting(entry *catalogEntry, name string, wantDirectory bool, allowSymlinkTarget bool) (fs.FileInfo, error) {
if name != "." {
parts := strings.Split(name, "/")
for index := range parts {
partial := strings.Join(parts[:index+1], "/")
info, err := entry.root.Lstat(partial)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, errNotFound
}
return nil, errUnsafePath
}
if info.Mode()&os.ModeSymlink != 0 && (!allowSymlinkTarget || index != len(parts)-1) {
return nil, errUnsafePath
}
if index != len(parts)-1 && !info.IsDir() {
return nil, errUnsafePath
}
if info.Mode()&os.ModeSymlink == 0 {
if err := sameFilesystem(entry, partial); err != nil {
return nil, err
}
}
}
}
info, err := entry.root.Lstat(name)
if err != nil {
if errors.Is(err, os.ErrNotExist) {
return nil, errNotFound
}
return nil, errUnsafePath
}
if info.Mode()&os.ModeSymlink != 0 && !allowSymlinkTarget {
return nil, errUnsafePath
}
if !allowSymlinkTarget || info.Mode()&os.ModeSymlink == 0 {
if err := sameFilesystem(entry, name); err != nil {
return nil, err
}
}
if wantDirectory && !info.IsDir() {
return nil, errUnsafePath
}
return info, nil
}
func sameFilesystem(entry *catalogEntry, name string) error {
info, err := entry.root.Stat(name)
if err != nil {
return errUnsafePath
}
device, _, ok := fileIdentity(info)
if !ok || device != entry.device {
return errUnsafePath
}
return nil
}