package agentworkspace import ( "context" "encoding/json" "errors" "fmt" "io" "io/fs" "os" "path/filepath" "slices" "sort" "strings" "sync" "iop/packages/go/agentconfig" "iop/packages/go/agentguard" "iop/packages/go/agenttask" ) const ( overlayRecordSchemaVersion uint32 = 2 defaultSnapshotRetries = 3 ) // RetentionState is persisted before the backend exposes a prepared overlay. // Later change-set and cleanup tasks may advance this state without guessing // whether a task layer is still required. type RetentionState string const ( RetentionStateActive RetentionState = "active" ) // OverlayLocator contains the task-owned filesystem locations needed by // change-set validation and restart recovery. type OverlayLocator struct { SnapshotRoot string `json:"snapshot_root"` SnapshotRecord string `json:"snapshot_record"` LayerRoot string `json:"layer_root"` ViewRoot string `json:"view_root"` TempRoot string `json:"temp_root"` CacheRoot string `json:"cache_root"` GitMetadata string `json:"git_metadata,omitempty"` OverlayRecord string `json:"overlay_record"` } // OverlayRetentionPolicy is the immutable project policy captured when the // task layer is prepared. type OverlayRetentionPolicy struct { CompletedDays int `json:"completed_days"` BlockedDays int `json:"blocked_days"` MaxProjectLogRecords int `json:"max_project_log_records"` } // OverlayRecord is the durable, identity-bound record for one prepared task // layer. It deliberately stores no credentials or provider output. type OverlayRecord struct { SchemaVersion uint32 `json:"schema_version"` IsolationID string `json:"isolation_id"` Revision string `json:"revision"` IdempotencyKey string `json:"idempotency_key"` ProjectID string `json:"project_id"` WorkspaceID string `json:"workspace_id"` WorkUnitID string `json:"work_unit_id"` AttemptID string `json:"attempt_id"` CanonicalRoot string `json:"canonical_root"` ConfigRevision string `json:"config_revision"` GrantRevision string `json:"grant_revision"` ProfileRevision string `json:"profile_revision"` SnapshotRevision string `json:"snapshot_revision"` ConfinementRevision string `json:"confinement_revision"` Mode agentguard.IsolationMode `json:"mode"` Locator OverlayLocator `json:"locator"` Retention RetentionState `json:"retention"` RetentionPolicy OverlayRetentionPolicy `json:"retention_policy"` } // ResolvedInputs binds host configuration to one strict isolation request. type ResolvedInputs struct { Grant agentguard.WorkspaceGrant Profile agentguard.ProviderProfile } // InputResolver supplies the immutable workspace grant and selected provider // capability snapshot. The backend never invents either input. type InputResolver interface { Resolve(context.Context, agenttask.IsolationRequest) (ResolvedInputs, error) } // InputResolverFunc adapts a function to InputResolver. type InputResolverFunc func( context.Context, agenttask.IsolationRequest, ) (ResolvedInputs, error) func (function InputResolverFunc) Resolve( ctx context.Context, request agenttask.IsolationRequest, ) (ResolvedInputs, error) { return function(ctx, request) } // BackendConfig defines the device-local root for immutable snapshots, // task-owned layers, and task-local temp/cache paths. type BackendConfig struct { LocalRoot string MaxSnapshotRetries int Retention agentconfig.RetentionPolicy } // Backend implements agenttask.IsolationBackend with a materialized overlay: // every task gets a private writable copy of one immutable base snapshot. // This provides overlay semantics without mutating the canonical workspace or // requiring a privileged overlayfs mount. type Backend struct { root string snapshots string tasks string retries int retention agentconfig.RetentionPolicy resolver InputResolver lockMu sync.Mutex rootLocks map[string]*sync.Mutex } var _ agenttask.IsolationBackend = (*Backend)(nil) // NewBackend validates and creates the device-local runtime root. func NewBackend(config BackendConfig, resolver InputResolver) (*Backend, error) { if resolver == nil { return nil, errors.New("agentworkspace: input resolver is required") } if config.Retention.CompletedDays < 0 || config.Retention.BlockedDays < 0 || config.Retention.MaxProjectLogRecords < 0 { return nil, errors.New("agentworkspace: retention values must be non-negative") } if strings.TrimSpace(config.LocalRoot) == "" || !filepath.IsAbs(config.LocalRoot) || filepath.Clean(config.LocalRoot) != config.LocalRoot { return nil, errors.New("agentworkspace: local root must be an absolute clean path") } if err := os.MkdirAll(config.LocalRoot, 0o700); err != nil { return nil, fmt.Errorf("agentworkspace: create local root: %w", err) } canonical, err := filepath.EvalSymlinks(config.LocalRoot) if err != nil { return nil, fmt.Errorf("agentworkspace: canonicalize local root: %w", err) } if filepath.Clean(canonical) != config.LocalRoot { return nil, errors.New("agentworkspace: local root must already be canonical") } retries := config.MaxSnapshotRetries if retries <= 0 { retries = defaultSnapshotRetries } backend := &Backend{ root: canonical, snapshots: filepath.Join(canonical, "snapshots"), tasks: filepath.Join(canonical, "tasks"), retries: retries, retention: config.Retention, resolver: resolver, rootLocks: make(map[string]*sync.Mutex), } for _, directory := range []string{backend.snapshots, backend.tasks} { if err := os.MkdirAll(directory, 0o700); err != nil { return nil, fmt.Errorf("agentworkspace: create backend directory: %w", err) } } return backend, nil } // Prepare captures or reuses the exact canonical base and materializes one // idempotent task-owned layer. Only overlay mode is implemented here; callers // must select an explicit worktree or clone backend for Git-native tasks. func (backend *Backend) Prepare( ctx context.Context, request agenttask.IsolationRequest, ) (agenttask.PreparedIsolation, error) { if err := ctx.Err(); err != nil { return agenttask.PreparedIsolation{}, err } if request.Work.Unit.IsolationMode != agentguard.IsolationModeOverlay { return agenttask.PreparedIsolation{}, fmt.Errorf( "agentworkspace: isolation mode %q requires an explicit fallback backend", request.Work.Unit.IsolationMode, ) } if strings.TrimSpace(request.IdempotencyKey) == "" { return agenttask.PreparedIsolation{}, errors.New( "agentworkspace: isolation idempotency key is required", ) } inputs, err := backend.resolver.Resolve(ctx, request) if err != nil { return agenttask.PreparedIsolation{}, fmt.Errorf( "agentworkspace: resolve immutable isolation inputs: %w", err, ) } baseRoot, err := backend.validateInputs(request, inputs) if err != nil { return agenttask.PreparedIsolation{}, err } if _, err := platformConfinementRevision(); err != nil { return agenttask.PreparedIsolation{}, err } rootLock := backend.workspaceLock(baseRoot) rootLock.Lock() defer rootLock.Unlock() record, exists, err := backend.loadExistingTaskOverlay(request, inputs, baseRoot) if err != nil { return agenttask.PreparedIsolation{}, err } if exists { return backend.preparedIsolation(baseRoot, inputs, record) } snapshot, snapshotRoot, err := backend.ensureSnapshot( ctx, request, inputs, baseRoot, ) if err != nil { return agenttask.PreparedIsolation{}, err } record, err = backend.ensureTaskOverlay(ctx, request, inputs, snapshot, snapshotRoot) if err != nil { return agenttask.PreparedIsolation{}, err } return backend.preparedIsolation(baseRoot, inputs, record) } func (backend *Backend) preparedIsolation( baseRoot string, inputs ResolvedInputs, record OverlayRecord, ) (agenttask.PreparedIsolation, error) { proof, err := newConfinementProof(confinementBinding(backend.root, record)) if err != nil { return agenttask.PreparedIsolation{}, err } descriptor := &agentguard.IsolationDescriptor{ ID: record.IsolationID, Revision: record.Revision, Mode: record.Mode, BaseRoot: baseRoot, TaskRoot: filepath.Dir(record.Locator.OverlayRecord), WorkingDir: record.Locator.ViewRoot, WritableRoots: []string{record.Locator.ViewRoot, record.Locator.TempRoot, record.Locator.CacheRoot}, PinnedBaseRevision: record.SnapshotRevision, ConfinementRevision: record.ConfinementRevision, } return agenttask.PreparedIsolation{ Grant: cloneGrant(inputs.Grant), Descriptor: descriptor, Profile: inputs.Profile, Confinement: proof, }, nil } // LoadRecord returns a defensive copy of the durable overlay record identified // by a descriptor. Corrupt or mismatched state is an error, never an empty // replacement record. func (backend *Backend) LoadRecord(descriptor agentguard.IsolationDescriptor) (OverlayRecord, error) { taskName := strings.TrimPrefix(descriptor.ID, "overlay:") if taskName == "" || strings.ContainsAny(taskName, `/\`) { return OverlayRecord{}, errors.New("agentworkspace: invalid overlay identity") } recordPath := filepath.Join(backend.tasks, taskName, "overlay.json") record, err := readOverlayRecord(recordPath) if err != nil { return OverlayRecord{}, err } if err := backend.validateRecordLayout(record, filepath.Join(backend.tasks, taskName)); err != nil { return OverlayRecord{}, err } if err := backend.validateRecordRevision(record); err != nil { return OverlayRecord{}, err } if record.IsolationID != descriptor.ID || record.Revision != descriptor.Revision || record.SnapshotRevision != descriptor.PinnedBaseRevision || record.ConfinementRevision != descriptor.ConfinementRevision || record.Mode != descriptor.Mode || record.CanonicalRoot != descriptor.BaseRoot || filepath.Dir(record.Locator.OverlayRecord) != descriptor.TaskRoot || record.Locator.ViewRoot != descriptor.WorkingDir || !slices.Equal( []string{ record.Locator.ViewRoot, record.Locator.TempRoot, record.Locator.CacheRoot, }, descriptor.WritableRoots, ) { return OverlayRecord{}, errors.New("agentworkspace: overlay descriptor identity mismatch") } return cloneOverlayRecord(record), nil } func (backend *Backend) validateInputs( request agenttask.IsolationRequest, inputs ResolvedInputs, ) (string, error) { if request.Project.Intent == nil { return "", errors.New("agentworkspace: project has no immutable start intent") } grant := inputs.Grant if grant.ProjectID != string(request.Project.ProjectID) || grant.WorkspaceID != string(request.Project.WorkspaceID) || grant.Revision != string(request.Project.Intent.GrantRevision) { return "", errors.New("agentworkspace: workspace grant identity or revision mismatch") } profile := inputs.Profile if profile.ProviderID != request.Target.ProviderID || profile.ModelID != request.Target.ModelID || profile.ProfileID != request.Target.ProfileID || profile.Revision != request.Target.ProfileRevision { return "", errors.New("agentworkspace: provider profile identity or revision mismatch") } if request.Project.Intent.ConfigRevision == "" || request.Target.ConfigRevision != request.Project.Intent.ConfigRevision { return "", errors.New("agentworkspace: configuration revision mismatch") } if !profile.Unattended || !profile.ApprovalBypass || !profile.WritableRootConfinement { return "", errors.New( "agentworkspace: selected profile cannot demonstrate unattended writable-root confinement", ) } if len(grant.VCSMetadataRoots) != 0 { return "", errors.New( "agentworkspace: overlay mode cannot use shared external Git metadata allowances", ) } if strings.TrimSpace(grant.Root) == "" || !filepath.IsAbs(grant.Root) || filepath.Clean(grant.Root) != grant.Root { return "", errors.New("agentworkspace: workspace grant root must be absolute and clean") } canonical, err := filepath.EvalSymlinks(grant.Root) if err != nil { return "", fmt.Errorf("agentworkspace: canonicalize workspace root: %w", err) } canonical = filepath.Clean(canonical) if canonical != grant.Root { return "", errors.New("agentworkspace: workspace grant root must already be canonical") } info, err := os.Stat(canonical) if err != nil || !info.IsDir() { return "", errors.New("agentworkspace: workspace grant root must be an existing directory") } if pathContains(canonical, backend.root) || pathContains(backend.root, canonical) { return "", errors.New( "agentworkspace: local runtime root and canonical workspace must not overlap", ) } return canonical, nil } func (backend *Backend) loadExistingTaskOverlay( request agenttask.IsolationRequest, inputs ResolvedInputs, baseRoot string, ) (OverlayRecord, bool, error) { taskName := taskNameFor(request.IdempotencyKey) taskRoot := filepath.Join(backend.tasks, taskName) if _, err := os.Stat(taskRoot); err != nil { if os.IsNotExist(err) { return OverlayRecord{}, false, nil } return OverlayRecord{}, false, err } record, err := readOverlayRecord(filepath.Join(taskRoot, "overlay.json")) if err != nil { return OverlayRecord{}, false, err } if err := backend.validateRecordLayout(record, taskRoot); err != nil { return OverlayRecord{}, false, err } snapshot, err := readWorkspaceSnapshot(record.Locator.SnapshotRecord) if err != nil { return OverlayRecord{}, false, fmt.Errorf( "agentworkspace: read retained base snapshot: %w", err, ) } if err := backend.validateExistingRecord( record, request, inputs, baseRoot, snapshot, ); err != nil { return OverlayRecord{}, false, err } return record, true, nil } func (backend *Backend) ensureSnapshot( ctx context.Context, request agenttask.IsolationRequest, inputs ResolvedInputs, baseRoot string, ) (WorkspaceSnapshot, string, error) { configRevision := string(request.Project.Intent.ConfigRevision) grantRevision := inputs.Grant.Revision var lastErr error for attempt := 0; attempt < backend.retries; attempt++ { if err := ctx.Err(); err != nil { return WorkspaceSnapshot{}, "", err } temporary, err := os.MkdirTemp(backend.snapshots, ".capturing-") if err != nil { return WorkspaceSnapshot{}, "", fmt.Errorf( "agentworkspace: create snapshot staging directory: %w", err, ) } treeRoot := filepath.Join(temporary, "tree") snapshot, captureErr := captureWorkspaceSnapshot( ctx, baseRoot, treeRoot, configRevision, grantRevision, ) if captureErr == nil { captureErr = captureGitMetadata(ctx, baseRoot, filepath.Join(temporary, "git")) } var confirmation WorkspaceSnapshot if captureErr == nil { confirmation, captureErr = captureWorkspaceSnapshot( ctx, baseRoot, "", configRevision, grantRevision, ) if captureErr == nil && confirmation.Revision != snapshot.Revision { captureErr = errors.New("canonical workspace changed during snapshot capture") } } if captureErr != nil { _ = os.RemoveAll(temporary) lastErr = captureErr continue } if err := writeJSONFile(filepath.Join(temporary, "snapshot.json"), snapshot, 0o400); err != nil { _ = os.RemoveAll(temporary) return WorkspaceSnapshot{}, "", err } name := strings.TrimPrefix(snapshot.Revision, "sha256:") finalRoot := filepath.Join(backend.snapshots, name) if _, err := os.Stat(finalRoot); err == nil { _ = os.RemoveAll(temporary) existing, readErr := readWorkspaceSnapshot(filepath.Join(finalRoot, "snapshot.json")) if readErr != nil || existing.Revision != snapshot.Revision { return WorkspaceSnapshot{}, "", errors.New( "agentworkspace: existing snapshot record is corrupt", ) } return existing, finalRoot, nil } else if !os.IsNotExist(err) { _ = os.RemoveAll(temporary) return WorkspaceSnapshot{}, "", err } if err := os.Rename(temporary, finalRoot); err != nil { _ = os.RemoveAll(temporary) if _, statErr := os.Stat(finalRoot); statErr == nil { existing, readErr := readWorkspaceSnapshot(filepath.Join(finalRoot, "snapshot.json")) if readErr == nil && existing.Revision == snapshot.Revision { return existing, finalRoot, nil } } return WorkspaceSnapshot{}, "", fmt.Errorf( "agentworkspace: install immutable snapshot: %w", err, ) } return snapshot, finalRoot, nil } return WorkspaceSnapshot{}, "", fmt.Errorf( "agentworkspace: could not capture a stable workspace snapshot after %d attempts: %w", backend.retries, lastErr, ) } func (backend *Backend) ensureTaskOverlay( ctx context.Context, request agenttask.IsolationRequest, inputs ResolvedInputs, snapshot WorkspaceSnapshot, snapshotRoot string, ) (OverlayRecord, error) { taskName := taskNameFor(request.IdempotencyKey) isolationID := "overlay:" + taskName taskRoot := filepath.Join(backend.tasks, taskName) recordPath := filepath.Join(taskRoot, "overlay.json") if _, err := os.Stat(taskRoot); err == nil { record, readErr := readOverlayRecord(recordPath) if readErr != nil { return OverlayRecord{}, readErr } if err := backend.validateRecordLayout(record, taskRoot); err != nil { return OverlayRecord{}, err } if err := backend.validateExistingRecord( record, request, inputs, inputs.Grant.Root, snapshot, ); err != nil { return OverlayRecord{}, err } return record, nil } else if !os.IsNotExist(err) { return OverlayRecord{}, err } temporary, err := os.MkdirTemp(backend.tasks, ".preparing-") if err != nil { return OverlayRecord{}, fmt.Errorf("agentworkspace: create task staging root: %w", err) } cleanup := true defer func() { if cleanup { _ = os.RemoveAll(temporary) } }() viewRoot := filepath.Join(temporary, "view") if err := materializeSnapshot(ctx, snapshot, snapshotRoot, viewRoot); err != nil { return OverlayRecord{}, err } for _, directory := range []string{ filepath.Join(temporary, "temp"), filepath.Join(temporary, "cache"), } { if err := os.MkdirAll(directory, 0o700); err != nil { return OverlayRecord{}, fmt.Errorf("agentworkspace: create task runtime root: %w", err) } } finalLocator := OverlayLocator{ SnapshotRoot: snapshotRoot, SnapshotRecord: filepath.Join(snapshotRoot, "snapshot.json"), LayerRoot: filepath.Join(taskRoot, "view"), ViewRoot: filepath.Join(taskRoot, "view"), TempRoot: filepath.Join(taskRoot, "temp"), CacheRoot: filepath.Join(taskRoot, "cache"), OverlayRecord: recordPath, } if _, err := os.Stat(filepath.Join(snapshotRoot, "git")); err == nil { finalLocator.GitMetadata = filepath.Join(finalLocator.ViewRoot, ".git") } else if !os.IsNotExist(err) { return OverlayRecord{}, err } record := OverlayRecord{ SchemaVersion: overlayRecordSchemaVersion, IsolationID: isolationID, IdempotencyKey: request.IdempotencyKey, ProjectID: string(request.Project.ProjectID), WorkspaceID: string(request.Project.WorkspaceID), WorkUnitID: string(request.Work.Unit.ID), AttemptID: string(request.Work.AttemptID), CanonicalRoot: inputs.Grant.Root, ConfigRevision: string(request.Project.Intent.ConfigRevision), GrantRevision: inputs.Grant.Revision, ProfileRevision: inputs.Profile.Revision, SnapshotRevision: snapshot.Revision, Mode: agentguard.IsolationModeOverlay, Locator: finalLocator, Retention: RetentionStateActive, RetentionPolicy: OverlayRetentionPolicy{ CompletedDays: backend.retention.CompletedDays, BlockedDays: backend.retention.BlockedDays, MaxProjectLogRecords: backend.retention.MaxProjectLogRecords, }, } record.Revision = overlayRevision(record) confinementRevision, err := resolveConfinementRevision( confinementBinding(backend.root, record), ) if err != nil { return OverlayRecord{}, err } record.ConfinementRevision = confinementRevision if err := writeJSONFile(filepath.Join(temporary, "overlay.json"), record, 0o400); err != nil { return OverlayRecord{}, err } if err := os.Rename(temporary, taskRoot); err != nil { if _, statErr := os.Stat(taskRoot); statErr == nil { existing, readErr := readOverlayRecord(recordPath) if readErr == nil { validateErr := backend.validateRecordLayout(existing, taskRoot) if validateErr == nil { validateErr = backend.validateExistingRecord( existing, request, inputs, inputs.Grant.Root, snapshot, ) } if validateErr == nil { cleanup = true return existing, nil } } } return OverlayRecord{}, fmt.Errorf("agentworkspace: install task overlay: %w", err) } cleanup = false return record, nil } func (backend *Backend) validateExistingRecord( record OverlayRecord, request agenttask.IsolationRequest, inputs ResolvedInputs, baseRoot string, snapshot WorkspaceSnapshot, ) error { if err := backend.validateRecordRevision(record); err != nil { return errors.New("agentworkspace: existing overlay record checksum is invalid") } configRevision := string(request.Project.Intent.ConfigRevision) if record.IdempotencyKey != request.IdempotencyKey || record.ProjectID != string(request.Project.ProjectID) || record.WorkspaceID != string(request.Project.WorkspaceID) || record.WorkUnitID != string(request.Work.Unit.ID) || record.AttemptID != string(request.Work.AttemptID) || record.CanonicalRoot != baseRoot || record.ConfigRevision != configRevision || record.GrantRevision != inputs.Grant.Revision || record.ProfileRevision != inputs.Profile.Revision || record.SnapshotRevision != snapshot.Revision || record.Mode != request.Work.Unit.IsolationMode { return errors.New("agentworkspace: existing overlay identity does not match the request") } if snapshot.CanonicalRoot != baseRoot || snapshot.ConfigRevision != configRevision || snapshot.GrantRevision != inputs.Grant.Revision || snapshot.Revision != record.SnapshotRevision { return errors.New("agentworkspace: retained snapshot identity does not match the request") } for _, directory := range []string{ record.Locator.SnapshotRoot, filepath.Join(record.Locator.SnapshotRoot, "tree"), record.Locator.LayerRoot, record.Locator.ViewRoot, record.Locator.TempRoot, record.Locator.CacheRoot, } { info, err := os.Stat(directory) if err != nil || !info.IsDir() { return errors.New("agentworkspace: existing overlay locator is unavailable") } } if record.Locator.GitMetadata != "" { info, err := os.Stat(record.Locator.GitMetadata) if err != nil || !info.IsDir() { return errors.New("agentworkspace: isolated Git metadata is unavailable") } } return nil } func (backend *Backend) validateRecordLayout(record OverlayRecord, taskRoot string) error { snapshotName := strings.TrimPrefix(record.SnapshotRevision, "sha256:") expectedSnapshotRoot := filepath.Join(backend.snapshots, snapshotName) expected := OverlayLocator{ SnapshotRoot: expectedSnapshotRoot, SnapshotRecord: filepath.Join(expectedSnapshotRoot, "snapshot.json"), LayerRoot: filepath.Join(taskRoot, "view"), ViewRoot: filepath.Join(taskRoot, "view"), TempRoot: filepath.Join(taskRoot, "temp"), CacheRoot: filepath.Join(taskRoot, "cache"), OverlayRecord: filepath.Join(taskRoot, "overlay.json"), } if record.Locator.GitMetadata != "" { expected.GitMetadata = filepath.Join(expected.ViewRoot, ".git") } if record.IsolationID != "overlay:"+filepath.Base(taskRoot) || record.Locator != expected { return errors.New("agentworkspace: overlay record contains an invalid locator layout") } return nil } func taskNameFor(idempotencyKey string) string { return strings.TrimPrefix(digestText(idempotencyKey), "sha256:") } func materializeSnapshot( ctx context.Context, snapshot WorkspaceSnapshot, snapshotRoot string, viewRoot string, ) error { if err := os.MkdirAll(viewRoot, 0o700); err != nil { return fmt.Errorf("agentworkspace: create task view: %w", err) } directories := make([]SnapshotEntry, 0) for _, entry := range snapshot.Entries { if err := ctx.Err(); err != nil { return err } destination := filepath.Join(viewRoot, filepath.FromSlash(entry.Path)) source := filepath.Join(snapshotRoot, "tree", filepath.FromSlash(entry.Path)) switch entry.Kind { case SnapshotEntryDirectory: if err := os.MkdirAll(destination, 0o700); err != nil { return err } directories = append(directories, entry) case SnapshotEntryRegular: if _, err := digestAndCopyRegular(source, destination); err != nil { return fmt.Errorf("agentworkspace: materialize %q: %w", entry.Path, err) } if err := os.Chmod(destination, os.FileMode(entry.Mode).Perm()); err != nil { return err } case SnapshotEntrySymlink: if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil { return err } if err := os.Symlink(entry.SymlinkTarget, destination); err != nil { return err } case SnapshotEntryMissing: continue default: return fmt.Errorf("agentworkspace: unsupported snapshot entry kind %q", entry.Kind) } } sort.Slice(directories, func(left, right int) bool { return strings.Count(directories[left].Path, "/") > strings.Count(directories[right].Path, "/") }) for _, directory := range directories { if err := os.Chmod( filepath.Join(viewRoot, filepath.FromSlash(directory.Path)), os.FileMode(directory.Mode).Perm(), ); err != nil { return err } } gitSource := filepath.Join(snapshotRoot, "git") if _, err := os.Stat(gitSource); err == nil { if err := copyDirectory(ctx, gitSource, filepath.Join(viewRoot, ".git")); err != nil { return fmt.Errorf("agentworkspace: materialize isolated Git metadata: %w", err) } } else if !os.IsNotExist(err) { return err } return nil } func captureGitMetadata(ctx context.Context, baseRoot, destination string) error { dotGit := filepath.Join(baseRoot, ".git") info, err := os.Lstat(dotGit) if err != nil { if os.IsNotExist(err) { return nil } return fmt.Errorf("agentworkspace: inspect Git metadata: %w", err) } if !info.IsDir() || info.Mode()&os.ModeSymlink != 0 { return errors.New( "agentworkspace: overlay mode requires internal Git metadata; use a worktree or clone fallback", ) } if content, readErr := os.ReadFile(filepath.Join(dotGit, "objects", "info", "alternates")); readErr == nil && strings.TrimSpace(string(content)) != "" { return errors.New( "agentworkspace: overlay mode cannot retain external Git object alternates", ) } else if readErr != nil && !os.IsNotExist(readErr) { return fmt.Errorf("agentworkspace: inspect Git object alternates: %w", readErr) } return copyDirectory(ctx, dotGit, destination) } func copyDirectory(ctx context.Context, sourceRoot, destinationRoot string) error { return filepath.WalkDir(sourceRoot, func(path string, entry fs.DirEntry, walkErr error) error { if walkErr != nil { return walkErr } if err := ctx.Err(); err != nil { return err } relative, err := filepath.Rel(sourceRoot, path) if err != nil { return err } destination := destinationRoot if relative != "." { destination = filepath.Join(destinationRoot, relative) } info, err := os.Lstat(path) if err != nil { return err } if strings.HasSuffix(info.Name(), ".lock") { return fmt.Errorf("transient lock file %q prevents a stable metadata snapshot", relative) } switch { case info.IsDir(): return os.MkdirAll(destination, 0o700) case info.Mode().IsRegular(): _, err := digestAndCopyRegular(path, destination) if err != nil { return err } return os.Chmod(destination, info.Mode().Perm()) case info.Mode()&os.ModeSymlink != 0: target, err := os.Readlink(path) if err != nil { return err } targetPath := filepath.Clean(filepath.Join(filepath.Dir(path), target)) resolved, resolveErr := filepath.EvalSymlinks(targetPath) if filepath.IsAbs(target) || resolveErr != nil || !pathContains(sourceRoot, resolved) { return fmt.Errorf("Git metadata symlink %q escapes its isolated root", relative) } if err := os.MkdirAll(filepath.Dir(destination), 0o700); err != nil { return err } return os.Symlink(target, destination) default: return fmt.Errorf("unsupported Git metadata object %q", relative) } }) } func overlayRevision(record OverlayRecord) string { copy := record copy.Revision = "" copy.ConfinementRevision = "" encoded, _ := json.Marshal(copy) return digestBytes(encoded) } func (backend *Backend) validateRecordRevision(record OverlayRecord) error { if record.SchemaVersion != overlayRecordSchemaVersion || record.Revision != overlayRevision(record) { return errors.New("agentworkspace: overlay record is corrupt") } expected, err := resolveConfinementRevision( confinementBinding(backend.root, record), ) if err != nil { return err } if record.ConfinementRevision != expected { return errors.New("agentworkspace: overlay confinement identity is corrupt") } return nil } func readOverlayRecord(path string) (OverlayRecord, error) { var record OverlayRecord if err := readJSONFile(path, &record); err != nil { return OverlayRecord{}, fmt.Errorf("agentworkspace: read overlay record: %w", err) } if record.SchemaVersion != overlayRecordSchemaVersion || record.Revision != overlayRevision(record) { return OverlayRecord{}, errors.New("agentworkspace: overlay record is corrupt") } return record, nil } func readWorkspaceSnapshot(path string) (WorkspaceSnapshot, error) { var snapshot WorkspaceSnapshot if err := readJSONFile(path, &snapshot); err != nil { return WorkspaceSnapshot{}, err } if snapshot.SchemaVersion != snapshotSchemaVersion || snapshot.Revision != snapshotRevision(snapshot) { return WorkspaceSnapshot{}, errors.New("agentworkspace: snapshot record is corrupt") } return snapshot, nil } func writeJSONFile(path string, value any, mode fs.FileMode) error { encoded, err := json.MarshalIndent(value, "", " ") if err != nil { return fmt.Errorf("agentworkspace: encode durable record: %w", err) } encoded = append(encoded, '\n') file, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_EXCL, mode) if err != nil { return fmt.Errorf("agentworkspace: create durable record: %w", err) } if _, err := file.Write(encoded); err != nil { _ = file.Close() return err } if err := file.Sync(); err != nil { _ = file.Close() return err } return file.Close() } func readJSONFile(path string, destination any) error { file, err := os.Open(path) if err != nil { return err } defer file.Close() decoder := json.NewDecoder(io.LimitReader(file, 16<<20)) decoder.DisallowUnknownFields() if err := decoder.Decode(destination); err != nil { return err } var trailing any if err := decoder.Decode(&trailing); !errors.Is(err, io.EOF) { return errors.New("durable record must contain exactly one JSON value") } return nil } func cloneGrant(grant agentguard.WorkspaceGrant) *agentguard.WorkspaceGrant { copy := grant copy.VCSMetadataRoots = append([]string(nil), grant.VCSMetadataRoots...) return © } func cloneOverlayRecord(record OverlayRecord) OverlayRecord { return record } func (backend *Backend) workspaceLock(root string) *sync.Mutex { backend.lockMu.Lock() defer backend.lockMu.Unlock() lock := backend.rootLocks[root] if lock == nil { lock = &sync.Mutex{} backend.rootLocks[root] = lock } return lock }