Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions docs/CONTROLLED_SESSION_DESIGN.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,9 +60,15 @@ summary: Capability-scoped execution sessions that inherit Reploy's global conta
now durably recorded in the existing live-run state. Reploy monotonically
fills each exact full container ID after Docker creates it, and both IDs are
durable before either process starts. Verified cleanup removes that record;
failed or unverifiable partial-preparation cleanup retains it. The watchdog
and restart reconciliation remain the next ownership phases, and
controlled-session networking remains a later phase.
failed or unverifiable partial-preparation cleanup retains it. Reploy now
derives the immutable watchdog cleanup manifest from the complete recorded
ownership before startup. It names only the exact containers and private
channel, carries the host boot identity,
represents the currently absent lease networks and volumes as empty arrays,
and omits protocol authority. The parent and watchdog will be the same
executable, so this internal manifest adds no independent schema-version
marker. Launching the watchdog and restart reconciliation remain the next
ownership phases, and controlled-session networking remains a later phase.
- Initial runtime: Linux containers under Docker
- Motivating clients: OmegaFlow recording, sandboxed AI agents, security
inspection, and untrusted-code execution
Expand Down
133 changes: 133 additions & 0 deletions internal/deploy/controlled_session_cleanup_manifest.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
package deploy

import (
"bytes"
"encoding/json"
"fmt"
"io"
"path/filepath"

"github.com/omry/reploy/internal/canonical"
)

// ControlledSessionCleanupManifest is the complete immutable resource
// selection a session watchdog may receive. It is derived from durable
// ownership rather than accepting later resource choices.
//
// Networks and volumes are explicit arrays even while controlled sessions do
// not create either resource. Future slices may populate them only after their
// exact identities become part of durable ownership.
Comment on lines +17 to +19
type ControlledSessionCleanupManifest struct {
LiveRunID string `json:"live_run_id"`
BootSession string `json:"boot_session"`
ChannelDirectory string `json:"channel_directory"`
Controller ControlledSessionContainerOwnershipV1 `json:"controller"`
Workload ControlledSessionContainerOwnershipV1 `json:"workload"`
Networks []string `json:"networks"`
Volumes []string `json:"volumes"`
}

// ControlledSessionCleanupManifestFromOwnership creates the watchdog input
// from the exact durable ownership record. The session handle is deliberately
// omitted because cleanup does not need protocol authority.
func ControlledSessionCleanupManifestFromOwnership(ownership ControlledSessionOwnershipV1) (ControlledSessionCleanupManifest, error) {
if err := validateControlledSessionOwnershipV1(ownership); err != nil {
return ControlledSessionCleanupManifest{}, fmt.Errorf("controlled-session cleanup manifest ownership: %w", err)
}
manifest := ControlledSessionCleanupManifest{
LiveRunID: ownership.LiveRunID, BootSession: ownership.BootSession,
ChannelDirectory: ownership.ChannelDirectory,
Controller: ownership.Controller, Workload: ownership.Workload,
Networks: []string{}, Volumes: []string{},
}
if err := ValidateControlledSessionCleanupManifest(manifest); err != nil {
return ControlledSessionCleanupManifest{}, err
}
return manifest, nil
}

func ValidateControlledSessionCleanupManifest(manifest ControlledSessionCleanupManifest) error {
if err := ValidateLiveRunIDV1(manifest.LiveRunID); err != nil {
return fmt.Errorf("controlled-session cleanup manifest live run ID: %w", err)
}
if err := validateBootSessionIDV1(manifest.BootSession); err != nil {
return fmt.Errorf("controlled-session cleanup manifest: %w", err)
}
if !filepath.IsAbs(manifest.ChannelDirectory) || filepath.Clean(manifest.ChannelDirectory) != manifest.ChannelDirectory || !safeRecoveryIdentity(manifest.ChannelDirectory) {
return fmt.Errorf("controlled-session cleanup manifest channel directory must be a clean absolute path")
}
sessionsDirectory := filepath.Dir(manifest.ChannelDirectory)
if filepath.Base(manifest.ChannelDirectory) != manifest.LiveRunID || filepath.Base(sessionsDirectory) != "sessions" ||
filepath.Base(filepath.Dir(sessionsDirectory)) != ".reploy" {
return fmt.Errorf("controlled-session cleanup manifest channel directory must identify the live-run private session directory")
}
if err := validateControlledSessionContainerOwnershipV1(manifest.Controller, "controller"); err != nil {
return fmt.Errorf("controlled-session cleanup manifest controller: %w", err)
}
if err := validateControlledSessionContainerOwnershipV1(manifest.Workload, "workload"); err != nil {
return fmt.Errorf("controlled-session cleanup manifest workload: %w", err)
}
if manifest.Controller.ID == manifest.Workload.ID {
return fmt.Errorf("controlled-session cleanup manifest containers must be different")
}
if manifest.Networks == nil || manifest.Volumes == nil {
return fmt.Errorf("controlled-session cleanup manifest networks and volumes must use arrays")
}
if err := validateControlledSessionCleanupResourceIDs(manifest.Networks, "network"); err != nil {
return err
}
if err := validateControlledSessionCleanupResourceIDs(manifest.Volumes, "volume"); err != nil {
return err
}
return nil
}

func validateControlledSessionCleanupResourceIDs(resources []string, kind string) error {
for index, resource := range resources {
if !safeRecoveryIdentity(resource) {
return fmt.Errorf("controlled-session cleanup manifest %s identity must be nonempty safe text", kind)
}
if index > 0 && resources[index-1] >= resource {
return fmt.Errorf("controlled-session cleanup manifest %s identities must be sorted and unique", kind)
}
}
return nil
}

func EncodeControlledSessionCleanupManifest(manifest ControlledSessionCleanupManifest) ([]byte, error) {
if err := ValidateControlledSessionCleanupManifest(manifest); err != nil {
return nil, err
}
content, err := canonical.Marshal(manifest)
if err != nil {
return nil, fmt.Errorf("encode controlled-session cleanup manifest: %w", err)
}
return content, nil
}

func DecodeControlledSessionCleanupManifest(content []byte) (ControlledSessionCleanupManifest, error) {
decoder := json.NewDecoder(bytes.NewReader(content))
decoder.DisallowUnknownFields()
var manifest ControlledSessionCleanupManifest
if err := decoder.Decode(&manifest); err != nil {
return ControlledSessionCleanupManifest{}, fmt.Errorf("decode controlled-session cleanup manifest: %w", err)
}
var extra any
if err := decoder.Decode(&extra); err != io.EOF {
if err == nil {
return ControlledSessionCleanupManifest{}, fmt.Errorf("controlled-session cleanup manifest contains trailing JSON")
}
return ControlledSessionCleanupManifest{}, fmt.Errorf("decode controlled-session cleanup manifest trailer: %w", err)
}
if err := ValidateControlledSessionCleanupManifest(manifest); err != nil {
return ControlledSessionCleanupManifest{}, err
}
canonicalContent, err := canonical.Marshal(manifest)
if err != nil {
return ControlledSessionCleanupManifest{}, err
}
if !bytes.Equal(content, canonicalContent) {
return ControlledSessionCleanupManifest{}, fmt.Errorf("controlled-session cleanup manifest is not canonical JSON")
}
return manifest, nil
}
86 changes: 86 additions & 0 deletions internal/deploy/controlled_session_cleanup_manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
package deploy

import (
"bytes"
"path/filepath"
"reflect"
"strings"
"testing"
)

func TestControlledSessionCleanupManifestDerivesExactDurableOwnership(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
manifest, err := ControlledSessionCleanupManifestFromOwnership(ownership)
if err != nil {
t.Fatal(err)
}
if manifest.LiveRunID != ownership.LiveRunID || manifest.BootSession != ownership.BootSession ||
manifest.ChannelDirectory != ownership.ChannelDirectory || manifest.Controller != ownership.Controller ||
manifest.Workload != ownership.Workload || len(manifest.Networks) != 0 || len(manifest.Volumes) != 0 {
t.Fatalf("cleanup manifest = %#v", manifest)
}
content, err := EncodeControlledSessionCleanupManifest(manifest)
if err != nil {
t.Fatal(err)
}
if bytes.Contains(content, []byte(ownership.SessionHandle)) || bytes.Contains(content, []byte(`"schema"`)) {
t.Fatalf("cleanup manifest includes unnecessary protocol or version authority: %s", content)
}
decoded, err := DecodeControlledSessionCleanupManifest(content)
if err != nil || !reflect.DeepEqual(decoded, manifest) {
t.Fatalf("decoded manifest = %#v, error=%v", decoded, err)
}
}

func TestControlledSessionCleanupManifestRejectsInvalidDurableOwnership(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
ownership.Workload.ID = ownership.Controller.ID
if _, err := ControlledSessionCleanupManifestFromOwnership(ownership); err == nil || !strings.Contains(err.Error(), "different containers") {
t.Fatalf("invalid durable ownership error = %v", err)
}
}

func TestControlledSessionCleanupManifestRequiresExactChannelAndCanonicalArrays(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
manifest, err := ControlledSessionCleanupManifestFromOwnership(ownership)
if err != nil {
t.Fatal(err)
}
manifest.ChannelDirectory = filepath.VolumeName(ownership.ChannelDirectory) + string(filepath.Separator)
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "private session directory") {
t.Fatalf("broad channel directory error = %v", err)
}
manifest.ChannelDirectory = ownership.ChannelDirectory
manifest.Networks = nil
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "must use arrays") {
t.Fatalf("nil resources error = %v", err)
}
manifest.Networks = []string{"network-b", "network-a"}
if err := ValidateControlledSessionCleanupManifest(manifest); err == nil || !strings.Contains(err.Error(), "sorted and unique") {
t.Fatalf("unordered resources error = %v", err)
}
}

func TestDecodeControlledSessionCleanupManifestRejectsUnknownAndNoncanonicalJSON(t *testing.T) {
ownership := controlledSessionOwnershipFixtureV1(t.TempDir(), "run-0000000000000001", "reploy/env/workload:g-current")
ownership.BootSession = "boot-session"
manifest, err := ControlledSessionCleanupManifestFromOwnership(ownership)
if err != nil {
t.Fatal(err)
}
content, err := EncodeControlledSessionCleanupManifest(manifest)
if err != nil {
t.Fatal(err)
}
unknown := append(append([]byte{}, content[:len(content)-1]...), []byte(`,"extra":true}`)...)
if _, err := DecodeControlledSessionCleanupManifest(unknown); err == nil || !strings.Contains(err.Error(), "unknown field") {
t.Fatalf("unknown field error = %v", err)
}
pretty := append([]byte("\n"), content...)
if _, err := DecodeControlledSessionCleanupManifest(pretty); err == nil || !strings.Contains(err.Error(), "not canonical") {
t.Fatalf("noncanonical error = %v", err)
}
}
2 changes: 1 addition & 1 deletion internal/deploy/live_run_queue_file_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,7 @@ func controlledSessionOwnershipFixtureV1(dir string, runID string, generation st
}
return ControlledSessionOwnershipV1{
LiveRunID: runID, SessionHandle: "session-" + strings.Repeat("a", 64),
ChannelDirectory: filepath.Join(dir, ".reploy", "private", "sessions", runID),
ChannelDirectory: filepath.Join(dir, ".reploy", "sessions", runID),
Controller: container("controller", strings.Repeat("a", 64), "controller", "reploy/env/controller:g-current", "1"),
Workload: container("workload", strings.Repeat("b", 64), "workload", generation, "2"),
}
Expand Down
64 changes: 50 additions & 14 deletions internal/dockerdeploy/controlled_session_supervisor.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ type controlledSessionSupervisorBackendV1 struct {
recordPlannedOwnership func() error
recordControllerOwnership func(string) error
recordControllerRollback func()
recordOwnership func(string, string) error
recordOwnership func(string, string) (deploy.ControlledSessionCleanupManifest, error)
now func() time.Time
}

Expand Down Expand Up @@ -125,6 +125,7 @@ type controlledSessionSupervisorV1 struct {
controllerStarted bool
workloadStarted bool
controllerOwnershipIncomplete bool
cleanupManifest deploy.ControlledSessionCleanupManifest

transportHealthy bool
diagnosticErr error
Expand Down Expand Up @@ -169,13 +170,14 @@ func RunControlledSessionV1(
ownershipRecorded := false
partialPreparationCleanupVerified := false
controllerID := ""
persistOwnership := func(controllerID string, workloadID string) error {
persistOwnership := func(controllerID string, workloadID string) (deploy.ControlledSessionOwnershipV1, error) {
ownership := controlledSessionOwnershipFromPlanV1(plan, controllerID, workloadID)
if _, err := operation.RecordControlledSessionOwnershipV1(ownership); err != nil {
return fmt.Errorf("persist controlled-session ownership: %w", err)
recorded, err := operation.RecordControlledSessionOwnershipV1(ownership)
if err != nil {
return deploy.ControlledSessionOwnershipV1{}, fmt.Errorf("persist controlled-session ownership: %w", err)
}
ownershipRecorded = true
return nil
return recorded, nil
}
result, runErr := runControlledSessionV1(ctx, plan, options, controlledSessionSupervisorBackendV1{
prepareChannel: func(plan ControlledSessionExecutionPlanV1) (controlledSessionChannelRuntimeV1, error) {
Expand All @@ -193,31 +195,38 @@ func RunControlledSessionV1(
},
prepareWorkload: func(ctx context.Context, plan ControlledSessionContainerPlanV1) (controlledSessionWorkloadRuntimeV1, error) {
return prepareDockerWorkloadPTYWithContainerIDV1(ctx, plan, func(workloadID string) error {
return persistOwnership(controllerID, workloadID)
_, err := persistOwnership(controllerID, workloadID)
return err
}, func() {
partialPreparationCleanupVerified = true
})
},
recordPlannedOwnership: func() error {
return persistOwnership("", "")
_, err := persistOwnership("", "")
return err
},
recordControllerOwnership: func(exactControllerID string) error {
controllerID = exactControllerID
err := persistOwnership(exactControllerID, "")
_, err := persistOwnership(exactControllerID, "")
return err
},
recordControllerRollback: func() {
partialPreparationCleanupVerified = true
},
recordOwnership: func(controllerID string, workloadID string) error {
if err := persistOwnership(controllerID, workloadID); err != nil {
return err
recordOwnership: func(controllerID string, workloadID string) (deploy.ControlledSessionCleanupManifest, error) {
recorded, err := persistOwnership(controllerID, workloadID)
if err != nil {
return deploy.ControlledSessionCleanupManifest{}, err
}
manifest, err := deploy.ControlledSessionCleanupManifestFromOwnership(recorded)
if err != nil {
return deploy.ControlledSessionCleanupManifest{}, fmt.Errorf("prepare controlled-session cleanup manifest: %w", err)
}
operationReleaseAttempted = true
if err := operation.Unlock(); err != nil {
return fmt.Errorf("release operation lock before controlled-session startup: %w", err)
return deploy.ControlledSessionCleanupManifest{}, fmt.Errorf("release operation lock before controlled-session startup: %w", err)
}
return nil
return manifest, nil
},
now: time.Now,
})
Expand Down Expand Up @@ -442,9 +451,16 @@ func (supervisor *controlledSessionSupervisorV1) prepare(ctx context.Context) er
return fmt.Errorf("claim controlled-session workload output: %w", err)
}
if supervisor.backend.recordOwnership != nil {
if err := supervisor.backend.recordOwnership(controller.ContainerID(), workload.ContainerID()); err != nil {
manifest, err := supervisor.backend.recordOwnership(controller.ContainerID(), workload.ContainerID())
if err != nil {
return err
}
if err := validateControlledSessionCleanupManifestForRuntimeV1(
manifest, supervisor.plan, controller.ContainerID(), workload.ContainerID(),
); err != nil {
return err
}
supervisor.cleanupManifest = manifest
}
if err := controller.Start(ctx); err != nil {
return fmt.Errorf("start controlled-session controller: %w", err)
Expand Down Expand Up @@ -472,6 +488,26 @@ func (supervisor *controlledSessionSupervisorV1) prepare(ctx context.Context) er
return nil
}

func validateControlledSessionCleanupManifestForRuntimeV1(
manifest deploy.ControlledSessionCleanupManifest,
plan ControlledSessionExecutionPlanV1,
controllerID string,
workloadID string,
) error {
if err := deploy.ValidateControlledSessionCleanupManifest(manifest); err != nil {
return fmt.Errorf("validate controlled-session cleanup manifest: %w", err)
}
expected := controlledSessionOwnershipFromPlanV1(plan, controllerID, workloadID)
if manifest.LiveRunID != expected.LiveRunID || manifest.ChannelDirectory != expected.ChannelDirectory ||
manifest.Controller != expected.Controller || manifest.Workload != expected.Workload {
return fmt.Errorf("controlled-session cleanup manifest does not match the exact prepared resources")
}
if len(manifest.Networks) != 0 || len(manifest.Volumes) != 0 {
return fmt.Errorf("controlled-session cleanup manifest names resources that this runtime does not create")
}
return nil
}

func observeControlledSessionProcessV1(
wait func(context.Context) (controlledsession.ProcessStatusV1, error),
) <-chan controlledSessionProcessResultV1 {
Expand Down
Loading
Loading