Skip to content
Merged
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
51 changes: 47 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ case before making changes to workspace resources.
Hosts must forward both values and runtimes must handle them before enabling
an implementation that depends on workspace ownership. These fields describe
provisioning intent; they do not authorize resource replacement. Duplicate
creation and explicit Delete remain separate operations.
creation is rejected unless a runtime implements the explicit API 1.3
authorization described below. Explicit Delete remains a separate operation.

### Reuse validation in API 1.2

Expand All @@ -99,6 +100,38 @@ Hosts skip this optional RPC when it is not advertised. The conformance suite
checks successful reuse and cancellation without resource mutation; backend
suites must cover their own incompatible contracts.

### Explicit recreation in API 1.3

A host that implements recreate-on-run sets `InfoRequest.supports_recreate_on_run`.
A supporting runtime may then advertise `RECREATE_MODE_ON_RUN` with API minor 3
or newer. Without that opt-in (including an empty request from an older host),
it must advertise `RECREATE_MODE_DELETE`. Runtimes without this capability keep
their existing DELETE or STOP behavior; unknown request fields are ignored by
older providers. `ValidateInfo` rejects ON_RUN below API 1.3 and still rejects
unknown recreate modes.

ON_RUN means the host leaves the existing resource intact before RunImage.
Only an explicitly authorized recreation supplies `RunImageRequest.allow_recreate=true`.
False or omitted never authorizes replacement; an otherwise-valid duplicate
returns AlreadyExists without mutating the existing workspace. Even when true,
the runtime must validate the complete effective configuration before destroying
the old resource. This does not promise rollback if later materialization or
launch fails. Fresh creation does not require permission to replace anything.

Info opt-in advertises host capability; it is not replacement permission. The
SDK forwards requests unchanged and does not automatically negotiate or downgrade
capabilities. Implement the response selection in the runtime's existing Info
method. RunImage authorization is per request, never dependent on a previous
Info call or process-local session state: hosts may launch a fresh runtime
process for each operation. Host orchestration sends the permission bit only
for explicit recreation with negotiated ON_RUN. Legacy modes retain their
existing host-managed recreation path.

This preserves old-host/new-provider interoperability through DELETE, and
new-host/old-provider interoperability through the older provider's existing
mode. It cannot retrofit validation-before-deletion into an old host's legacy
recreation path.

## Development

```sh
Expand Down Expand Up @@ -155,7 +188,11 @@ Logs support. Select mount profiles with `--mount-types all|none|bind|volume|tmp
or a comma-separated subset. Unsupported mounts fail before workspace creation.
Exec and Logs require a running workspace. Start and Stop are idempotent for
existing resources; Delete is idempotent even for absent resources. Duplicate
RunImage returns AlreadyExists. Lifecycle failures include RuntimeError details.
RunImage returns AlreadyExists unless the optional `--recreate-on-run` profile
is enabled and that creation request explicitly sets `allow_recreate=true`.
This profile advertises ON_RUN only when discovery opts in; discovery alone
never authorizes replacement. The default profile retains STOP behavior.
Lifecycle failures include RuntimeError details.
Output frames are limited to 32 KiB and preserve binary bytes.

The SDK tests exercise lifecycle state across real plugin restarts, fault modes,
Expand All @@ -179,7 +216,9 @@ conformance.Run(t, conformance.Options{

`Connect` must create a fresh client per subtest and register bounded process and
connection cleanup with `t.Cleanup`. `Workspace` must provide an image and a
unique workspace ID that is initially absent. Configure the image's entrypoint
unique workspace ID that is initially absent, with `allow_recreate=false`.
The suite sets authorization explicitly only for negotiated replacement tests.
Configure the image's entrypoint
so its finite log output matches `LogData` if the runtime advertises Logs.
`Command` receives the scenario and workspace ID; return exact argv with
`tty=false`. The suite does not choose a shell or construct commands for you.
Expand All @@ -190,6 +229,9 @@ resources, malformed Exec frames, binary preservation, separate output channels,
12 MiB of simultaneous stdin/stdout/stderr, consumer backpressure, early exit,
nonzero and signal exits, cancellation, deadlines, and finite merged Logs.
Logs and provisioning preflight checks follow the advertised capabilities.
When ON_RUN is negotiated, running and stopped workspace checks require
unauthorized duplicates and invalid authorized requests to preserve the full
Find response, followed by a valid explicitly authorized replacement.
Streaming comparisons use bounded buffers and require a single terminal exit
frame after all output. Data frames must be nonempty and follow the SDK's
recommended 32 KiB bound. Send goroutines are canceled and joined on failure.
Expand Down Expand Up @@ -219,7 +261,8 @@ The fake executable's `conformance` mode uses scenario names as argv[0], with
accepts exactly `/bin/sh`, `-c`, `printf 'shell stdout\n'`; the fake simulates
that output rather than executing a host shell. `error <CATEGORY>` injects a
structured error, and `crash` emits readiness then exits after one input frame.
CI runs the full reusable suite against this executable on Linux, macOS, and
CI runs the full reusable suite against both the default and `--recreate-on-run`
profiles of this executable on Linux, macOS, and
Windows, along with fault, mount-profile, crash/restart, and process-reap tests.

Host launcher trust, startup cancellation, descendant process ownership,
Expand Down
6 changes: 6 additions & 0 deletions cmd/devsy-fake-runtime/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ func main() {
"private state directory (required; one active plugin owner)",
)
flag.StringVar(&config.Mode, "mode", fake.Normal, "fixture scenario; see SDK README")
flag.BoolVar(
&config.RecreateOnRun,
"recreate-on-run",
false,
"enable negotiated recreation fixture",
)
delay := flag.Duration(
"handshake-delay",
time.Minute,
Expand Down
13 changes: 12 additions & 1 deletion conformance/fake/fake.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ type Config struct {
Mode string
// MountTypes defaults to all when nil; an empty slice advertises no mounts.
MountTypes []runtimev1.MountType
// RecreateOnRun enables negotiated, explicitly authorized replacement.
RecreateOnRun bool
}

// Driver implements the protocol without launching a real container or command.
Expand Down Expand Up @@ -83,7 +85,10 @@ func New(config Config) (*Driver, error) {
}

// Info reports supported capabilities or the selected discovery failure.
func (d *Driver) Info(context.Context, *runtimev1.InfoRequest) (*runtimev1.InfoResponse, error) {
func (d *Driver) Info(
_ context.Context,
req *runtimev1.InfoRequest,
) (*runtimev1.InfoResponse, error) {
if d.config.Mode == CrashAfterHandshake {
os.Exit(23)
}
Expand All @@ -96,6 +101,12 @@ func (d *Driver) Info(context.Context, *runtimev1.InfoRequest) (*runtimev1.InfoR
ProvisioningPreflight: true, Logs: true, ReusePreflight: true,
},
}
if d.config.RecreateOnRun {
info.Capabilities.RecreateMode = runtimev1.RecreateMode_RECREATE_MODE_DELETE
if req.GetSupportsRecreateOnRun() {
info.Capabilities.RecreateMode = runtimev1.RecreateMode_RECREATE_MODE_ON_RUN
}
}
if d.config.Mode == IncompatibleVersion {
info.ApiMajor++
}
Expand Down
11 changes: 7 additions & 4 deletions conformance/fake/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"os/exec"
"path/filepath"
"runtime"
"strconv"
"testing"
"time"

Expand Down Expand Up @@ -61,10 +62,11 @@ func launchWithTimeout(t *testing.T, dir, mode string, timeout time.Duration) *h
}

type launchOptions struct {
directory string
mode string
timeout time.Duration
mounts string
directory string
mode string
timeout time.Duration
mounts string
recreateOnRun bool
}

func launchProcess(t *testing.T, options launchOptions) *hplugin.Client {
Expand All @@ -83,6 +85,7 @@ func launchProcess(t *testing.T, options launchOptions) *hplugin.Client {
options.mode,
"--mount-types",
options.mounts,
"--recreate-on-run="+strconv.FormatBool(options.recreateOnRun),
),
AllowedProtocols: []hplugin.Protocol{hplugin.ProtocolGRPC},
StartTimeout: options.timeout,
Expand Down
49 changes: 27 additions & 22 deletions conformance/fake/lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,40 +39,22 @@ func (d *Driver) Find(
return &runtimev1.FindResponse{Found: container != nil, Container: container}, nil
}

// RunImage creates a running workspace and rejects duplicate creation.
// RunImage creates a workspace or explicitly replaces one in the ON_RUN profile.
// The fixture validates its supported configuration before writing any state.
func (d *Driver) RunImage(
ctx context.Context,
req *runtimev1.RunImageRequest,
) (*runtimev1.RunImageResponse, error) {
d.mu.Lock()
defer d.mu.Unlock()
if err := validateWorkspace(ctx, req.GetWorkspaceId()); err != nil {
return nil, err
}
if req.GetImage() == "" {
return nil, runtimeError(
codes.InvalidArgument,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_INVALID_ARGUMENT,
"image is required",
false,
)
}
if d.config.Mode == FailRun {
return nil, runtimeError(
codes.Internal,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_RUNTIME_FAILURE,
"fake image creation failed",
false,
)
}
if err := d.validateMounts(req); err != nil {
if err := d.validateRunImage(ctx, req); err != nil {
return nil, err
}
container, err := d.load(req.GetWorkspaceId())
if err != nil {
return nil, storageError()
}
if container != nil {
if container != nil && (!d.config.RecreateOnRun || !req.GetAllowRecreate()) {
return nil, runtimeError(
codes.AlreadyExists,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_ALREADY_EXISTS,
Expand All @@ -87,6 +69,29 @@ func (d *Driver) RunImage(
return &runtimev1.RunImageResponse{}, nil
}

func (d *Driver) validateRunImage(ctx context.Context, req *runtimev1.RunImageRequest) error {
if err := validateWorkspace(ctx, req.GetWorkspaceId()); err != nil {
return err
}
if req.GetImage() == "" {
return runtimeError(
codes.InvalidArgument,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_INVALID_ARGUMENT,
"image is required",
false,
)
}
if d.config.Mode == FailRun {
return runtimeError(
codes.Internal,
runtimev1.RuntimeErrorCode_RUNTIME_ERROR_CODE_RUNTIME_FAILURE,
"fake image creation failed",
false,
)
}
return d.validateMounts(req)
}

// Start normalizes an already-running workspace to success.
func (d *Driver) Start(
ctx context.Context,
Expand Down
Loading
Loading