Hash deliverables through Capability.ReadArtifact, resolve backends on the server, and refuse workspace removal until frozen members match FAV receipts. Co-authored-by: Cursor <cursoragent@cursor.com>
112 lines
2.6 KiB
Go
112 lines
2.6 KiB
Go
package workspace
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
// OpaqueID turns a stored work dir or already-opaque id into a WorkspaceRef.ID.
|
|
// It never returns a physical path.
|
|
func OpaqueID(parts ...string) string {
|
|
for _, p := range parts {
|
|
p = strings.TrimSpace(p)
|
|
if p == "" {
|
|
continue
|
|
}
|
|
slash := filepath.ToSlash(p)
|
|
if i := strings.LastIndex(slash, "/workspaces/"); i >= 0 {
|
|
rest := slash[i+len("/workspaces/"):]
|
|
if rest == "" {
|
|
continue
|
|
}
|
|
return strings.Split(rest, "/")[0]
|
|
}
|
|
if !strings.Contains(slash, "/") {
|
|
return p
|
|
}
|
|
return filepath.Base(filepath.Clean(p))
|
|
}
|
|
return ""
|
|
}
|
|
|
|
type RetentionReceipt struct {
|
|
LogicalID string
|
|
Digest string
|
|
StorageIdentity string
|
|
Verified bool
|
|
}
|
|
|
|
type FrozenManifest struct {
|
|
SHA256 string
|
|
StorageIdentity string
|
|
WorkspaceID string
|
|
Members []FrozenMember
|
|
}
|
|
|
|
type FrozenMember struct {
|
|
LogicalID string
|
|
Digest string
|
|
Kind string
|
|
}
|
|
|
|
func memberNeedsRetention(kind string) bool {
|
|
switch kind {
|
|
case "document", "assistant_turn", "run_manifest", "attachment":
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}
|
|
|
|
var ErrCleanupBlocked = fmt.Errorf("workspace: cleanup blocked until verified receipts match the frozen manifest")
|
|
|
|
func AssertCleanupAllowed(frozen FrozenManifest, receipts []RetentionReceipt) error {
|
|
if frozen.SHA256 == "" || frozen.StorageIdentity == "" {
|
|
return ErrCleanupBlocked
|
|
}
|
|
byLogical := map[string]RetentionReceipt{}
|
|
for _, r := range receipts {
|
|
byLogical[r.LogicalID] = r
|
|
}
|
|
for _, mem := range frozen.Members {
|
|
if !memberNeedsRetention(mem.Kind) {
|
|
continue
|
|
}
|
|
r, ok := byLogical[mem.LogicalID]
|
|
if !ok || !r.Verified {
|
|
return ErrCleanupBlocked
|
|
}
|
|
if r.Digest != mem.Digest || r.StorageIdentity != frozen.StorageIdentity {
|
|
return ErrCleanupBlocked
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Cleanup removes the workspace directory only after verified receipts match the frozen manifest.
|
|
func (c *Capability) Cleanup(ctx context.Context, ref WorkspaceRef, frozen FrozenManifest, receipts []RetentionReceipt) error {
|
|
if err := AssertCleanupAllowed(frozen, receipts); err != nil {
|
|
return err
|
|
}
|
|
if err := c.guard(ctx, ref, false); err != nil {
|
|
return err
|
|
}
|
|
root := filepath.Join(c.dataRoot, "workspaces", ref.ID)
|
|
abs, err := filepath.Abs(root)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
dataRoot, err := filepath.Abs(c.dataRoot)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
rel, err := filepath.Rel(dataRoot, abs)
|
|
if err != nil || rel == ".." || strings.HasPrefix(rel, ".."+string(os.PathSeparator)) {
|
|
return ErrPathEscape
|
|
}
|
|
return os.RemoveAll(abs)
|
|
}
|