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 }