iop/packages/go/agentworkspace/overlay.go
toki 3c48879c44 feat(agent-runtime): 독립 Agent CLI 런타임 기반을 구현한다
독립 호스트에서 안전한 작업 실행과 복구를 제공하기 위해 런타임 설정, 정책, 상태 저장소, 워크스페이스 격리 및 AgentTask 오케스트레이션을 확장한다.
2026-07-29 13:12:21 +09:00

953 lines
31 KiB
Go

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 &copy
}
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
}