init
This commit is contained in:
@@ -0,0 +1,147 @@
|
||||
package runtime
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"browser.local/run/config"
|
||||
"browser.local/run/protocol"
|
||||
)
|
||||
|
||||
type workspaceSeedFile struct {
|
||||
Path string `json:"path"`
|
||||
Content string `json:"content"`
|
||||
Encoding string `json:"encoding,omitempty"`
|
||||
Mode int `json:"mode,omitempty"`
|
||||
}
|
||||
|
||||
// MaterializeWorkspaceSeed writes platform-packaged plugin assets into the
|
||||
// scoped run workspace. The seed contains plugin-owned files only; run treats
|
||||
// them as opaque generic lifecycle assets.
|
||||
func MaterializeWorkspaceSeed(cfg config.Config) error {
|
||||
startedAt := time.Now()
|
||||
encoded := strings.TrimSpace(cfg.WorkspaceSeed)
|
||||
if encoded == "" {
|
||||
log.Printf("RUN phase=workspace_seed status=skipped reason=empty workspace=%s", safeOptional(cfg.WorkspaceRoot))
|
||||
return nil
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed status=decoding workspace=%s encodedBytes=%d componentKey=%s", safeOptional(cfg.WorkspaceRoot), len(encoded), safeOptional(cfg.ComponentKey))
|
||||
payload, err := base64.StdEncoding.DecodeString(encoded)
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=workspace_seed status=decode_failed workspace=%s error=%s", safeOptional(cfg.WorkspaceRoot), RedactText(err.Error()))
|
||||
return fmt.Errorf("decode workspace seed: %w", err)
|
||||
}
|
||||
var files []workspaceSeedFile
|
||||
if err := json.Unmarshal(payload, &files); err != nil {
|
||||
log.Printf("RUN phase=workspace_seed status=manifest_failed workspace=%s payloadBytes=%d error=%s", safeOptional(cfg.WorkspaceRoot), len(payload), RedactText(err.Error()))
|
||||
return fmt.Errorf("decode workspace seed manifest: %w", err)
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed status=decoded workspace=%s payloadBytes=%d files=%d", safeOptional(cfg.WorkspaceRoot), len(payload), len(files))
|
||||
if len(files) == 0 {
|
||||
log.Printf("RUN phase=workspace_seed status=skipped reason=no_files workspace=%s durationMs=%d", safeOptional(cfg.WorkspaceRoot), time.Since(startedAt).Milliseconds())
|
||||
return nil
|
||||
}
|
||||
if strings.TrimSpace(cfg.ServerInstanceID) == "" {
|
||||
log.Printf("RUN phase=workspace_seed status=failed reason=missing_server workspace=%s", safeOptional(cfg.WorkspaceRoot))
|
||||
return fmt.Errorf("workspace seed requires a server instance id")
|
||||
}
|
||||
scope, err := seededWorkspaceScope(cfg)
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=workspace_seed status=scope_failed workspace=%s server=%s componentKey=%s error=%s", safeOptional(cfg.WorkspaceRoot), safeOptional(cfg.ServerInstanceID), safeOptional(cfg.ComponentKey), RedactText(err.Error()))
|
||||
return err
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed status=scope_ready workspace=%s scope=%s files=%d", safeOptional(cfg.WorkspaceRoot), safeOptional(scope), len(files))
|
||||
totalBytes := 0
|
||||
for index, file := range files {
|
||||
written, err := writeWorkspaceSeedFile(scope, file, index+1, len(files))
|
||||
if err != nil {
|
||||
log.Printf("RUN phase=workspace_seed.file status=failed index=%d total=%d path=%s error=%s", index+1, len(files), safeOptional(file.Path), RedactText(err.Error()))
|
||||
return err
|
||||
}
|
||||
totalBytes += written
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed status=complete workspace=%s scope=%s files=%d bytes=%d durationMs=%d", safeOptional(cfg.WorkspaceRoot), safeOptional(scope), len(files), totalBytes, time.Since(startedAt).Milliseconds())
|
||||
return nil
|
||||
}
|
||||
|
||||
func seededWorkspaceScope(cfg config.Config) (string, error) {
|
||||
if strings.TrimSpace(cfg.ComponentKey) != "" {
|
||||
return NewWorkspaceResolver(cfg.WorkspaceRoot).Scope(cfg.ServerInstanceID, cfg.ComponentKey)
|
||||
}
|
||||
return scopedServerWorkspace(cfg.WorkspaceRoot, cfg.ServerInstanceID)
|
||||
}
|
||||
|
||||
func writeWorkspaceSeedFile(scope string, file workspaceSeedFile, index int, total int) (int, error) {
|
||||
target, err := workspaceSeedTarget(scope, file.Path)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
mode := os.FileMode(file.Mode)
|
||||
if mode == 0 {
|
||||
mode = 0o600
|
||||
}
|
||||
if mode&0o777 != mode || mode&0o022 != 0 {
|
||||
return 0, fmt.Errorf("workspace seed file mode is unsafe")
|
||||
}
|
||||
body, err := workspaceSeedFileContent(file)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed.file status=writing index=%d total=%d path=%s target=%s bytes=%d mode=%#o", index, total, safeOptional(file.Path), safeOptional(target), len(body), mode)
|
||||
if err := ensureDirectory(filepath.Dir(target)); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed.file status=directory_ready index=%d total=%d dir=%s", index, total, safeOptional(filepath.Dir(target)))
|
||||
if err := os.WriteFile(target, body, mode); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
log.Printf("RUN phase=workspace_seed.file status=written index=%d total=%d path=%s target=%s bytes=%d mode=%#o", index, total, safeOptional(file.Path), safeOptional(target), len(body), mode)
|
||||
return len(body), nil
|
||||
}
|
||||
|
||||
func workspaceSeedFileContent(file workspaceSeedFile) ([]byte, error) {
|
||||
switch strings.TrimSpace(file.Encoding) {
|
||||
case "":
|
||||
return []byte(file.Content), nil
|
||||
case "base64":
|
||||
body, err := base64.StdEncoding.DecodeString(strings.TrimSpace(file.Content))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("workspace seed file content is not valid base64")
|
||||
}
|
||||
return body, nil
|
||||
default:
|
||||
return nil, fmt.Errorf("workspace seed file encoding is unsupported")
|
||||
}
|
||||
}
|
||||
|
||||
func workspaceSeedTarget(scope string, key string) (string, error) {
|
||||
if strings.TrimSpace(scope) == "" {
|
||||
return "", fmt.Errorf("workspace scope is invalid")
|
||||
}
|
||||
if !protocol.ValidLogicalFileKey(key) || filepath.IsAbs(key) || strings.Contains(key, `\`) {
|
||||
return "", fmt.Errorf("workspace seed path is unsafe")
|
||||
}
|
||||
cleanScope, err := filepath.Abs(scope)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
parts := strings.Split(filepath.ToSlash(key), "/")
|
||||
current := cleanScope
|
||||
for _, part := range parts {
|
||||
if part == "" || part == "." || part == ".." {
|
||||
return "", fmt.Errorf("workspace seed path contains unsafe component")
|
||||
}
|
||||
current = filepath.Join(current, part)
|
||||
}
|
||||
rel, err := filepath.Rel(cleanScope, current)
|
||||
if err != nil || rel == "." || strings.HasPrefix(rel, "..") || filepath.IsAbs(rel) {
|
||||
return "", fmt.Errorf("workspace seed path escapes scope")
|
||||
}
|
||||
return current, nil
|
||||
}
|
||||
Reference in New Issue
Block a user