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
4 changes: 2 additions & 2 deletions THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,7 +125,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`.
| [github.com/devsy-org/agentapi](https://github.com/devsy-org/agentapi) | `v1.0.1` | MPL-2.0 |
| [github.com/devsy-org/api](https://github.com/devsy-org/api) | `v1.1.0` | MPL-2.0 |
| [github.com/devsy-org/apiserver](https://github.com/devsy-org/apiserver) | `v1.5.4` | Apache-2.0 |
| [github.com/devsy-org/devsy-runtime-sdk](https://github.com/devsy-org/devsy-runtime-sdk) | `v1.2.0` | MPL-2.0 |
| [github.com/devsy-org/devsy-runtime-sdk](https://github.com/devsy-org/devsy-runtime-sdk) | `v1.4.0` | MPL-2.0 |
| [github.com/devsy-org/ssh](https://github.com/devsy-org/ssh) | `v1.2.9` | BSD-3-Clause |
| [github.com/distribution/reference](https://github.com/distribution/reference) | `v0.6.0` | Apache-2.0 |
| [github.com/docker/cli](https://github.com/docker/cli) | `v29.8.0+incompatible` | Apache-2.0 |
Expand Down Expand Up @@ -379,7 +379,7 @@ To regenerate this file after changing dependencies, run `task cli:licenses`.
| [k8s.io/metrics](https://github.com/kubernetes/metrics) | `v0.37.0` | Apache-2.0 |
| [k8s.io/streaming](https://github.com/kubernetes/streaming) | `v0.37.1` | Apache-2.0 |
| [k8s.io/utils](https://github.com/kubernetes/utils) | `v0.0.0-20260707023825-cf1189d6abe3` | Apache-2.0 |
| [mvdan.cc/sh/v3](https://mvdan.cc/sh/v3) | `v3.14.0` | BSD-3-Clause |
| [mvdan.cc/sh/v3](https://mvdan.cc/sh/v3) | `v3.14.1` | BSD-3-Clause |
| [sigs.k8s.io/apiserver-network-proxy/konnectivity-client](https://sigs.k8s.io/apiserver-network-proxy/konnectivity-client) | `v0.36.0` | Apache-2.0 |
| [sigs.k8s.io/controller-runtime](https://sigs.k8s.io/controller-runtime) | `v0.25.0` | Apache-2.0 |
| [sigs.k8s.io/json](https://sigs.k8s.io/json) | `v0.0.0-20250730193827-2d320260d730` | Apache-2.0 |
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ require (
github.com/devsy-org/agentapi v1.0.1
github.com/devsy-org/api v1.1.0
github.com/devsy-org/apiserver v1.5.4
github.com/devsy-org/devsy-runtime-sdk v1.2.0
github.com/devsy-org/devsy-runtime-sdk v1.4.0
github.com/devsy-org/ssh v1.2.9
github.com/distribution/reference v0.6.0
github.com/docker/cli v29.8.0+incompatible
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -416,8 +416,8 @@ github.com/devsy-org/api v1.1.0 h1:l7T9k7RVwatwN4lxeDTF3iN6EYmfGZgR3ZTJMDGha1M=
github.com/devsy-org/api v1.1.0/go.mod h1:mAZklKdnywJYiXDReBLte/H+3m69z6G7RHB3n1lI53Q=
github.com/devsy-org/apiserver v1.5.4 h1:/bEPrSRSlfii2QHxc9qAiXlQawNJfIaPLraR3bYKHxg=
github.com/devsy-org/apiserver v1.5.4/go.mod h1:sDFCTjCN13wAHhno4KX0Z756gCI8ttW/c7NwTzo16K0=
github.com/devsy-org/devsy-runtime-sdk v1.2.0 h1:yQ6yJq00ZOcdn0+IjYygK7Byk9qLBuZgSD1AIiA/FRo=
github.com/devsy-org/devsy-runtime-sdk v1.2.0/go.mod h1:MiBP/fiY83DAS0TueEiJZSHYZ1vq85UR4KJTMfHIM+E=
github.com/devsy-org/devsy-runtime-sdk v1.4.0 h1:dJqPJrKxJVmgWT9r1vnZ08rqZTxshyazj9eM+fZG8k4=
github.com/devsy-org/devsy-runtime-sdk v1.4.0/go.mod h1:MiBP/fiY83DAS0TueEiJZSHYZ1vq85UR4KJTMfHIM+E=
github.com/devsy-org/ssh v1.2.9 h1:KHqX1xAplGFanm0FMSAojtiC9nV/UFxUeP/5jU5quak=
github.com/devsy-org/ssh v1.2.9/go.mod h1:Uff10+cSSDZk3bG07u5D9+eQ8GMGqsgE70WCUJWcHv4=
github.com/devsy-org/tailscale v1.102.2 h1:9SB6htvO+HmG8alal8WGCshHapfHR7dUFRSMYiFIzIM=
Expand Down
9 changes: 8 additions & 1 deletion pkg/driver/external/internal/testfixture/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ func serve(mode, state string, delay time.Duration) {
}
fixtureMode := mode
if fixtureMode == "blocked" || fixtureMode == "environment" || fixtureMode == "block-info" ||
fixtureMode == "env-error" || strings.HasPrefix(fixtureMode, "stream-") {
fixtureMode == "env-error" || fixtureMode == "workspace-identity" ||
strings.HasPrefix(fixtureMode, "stream-") {
fixtureMode = fake.Normal
}
runtime, err := fake.New(fake.Config{StateDir: state, Mode: fixtureMode})
Expand Down Expand Up @@ -122,6 +123,12 @@ func (f *fixture) RunImage(
ctx context.Context,
request *runtimev1.RunImageRequest,
) (*runtimev1.RunImageResponse, error) {
if f.mode == "workspace-identity" {
request.Labels = append(request.Labels,
"fixture.remote-user="+request.GetRemoteUser(),
"fixture.dockerless="+strconv.FormatBool(request.GetDockerless()),
)
}
if f.mode == "stream-workspace-secret" {
if err := f.saveEnvironment(request); err != nil {
return nil, err
Expand Down
2 changes: 2 additions & 0 deletions pkg/driver/external/run_image.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,8 @@ func runImageRequest(
Image: options.Image,
ImageBuiltLocally: options.ImageBuilt,
User: options.User,
RemoteUser: options.RemoteUser,
Dockerless: options.Dockerless,
Entrypoint: options.Entrypoint,
Args: slices.Clone(options.Cmd),
Environment: maps.Clone(options.Env),
Expand Down
9 changes: 6 additions & 3 deletions pkg/driver/external/run_image_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,9 @@ func (s *ImageRunSuite) TestResolvedIntentAndOwnership() {
UID: "host-uid",
Image: imageRunImage,
ImageBuilt: true,
User: "root",
RemoteUser: "developer",
User: fixtureProcessUser,
RemoteUser: fixtureRemoteUser,
Dockerless: true,
Entrypoint: imageRunEntrypoint,
Cmd: []string{"", "literal $arg"},
Env: map[string]string{"TOKEN": "secret"},
Expand Down Expand Up @@ -83,7 +84,9 @@ func (s *ImageRunSuite) TestResolvedIntentAndOwnership() {
WorkspaceId: fixtureWorkspace,
Image: imageRunImage,
ImageBuiltLocally: true,
User: "root",
User: fixtureProcessUser,
RemoteUser: fixtureRemoteUser,
Dockerless: true,
Entrypoint: imageRunEntrypoint,
Args: []string{"", "literal $arg"},
Environment: map[string]string{
Expand Down
48 changes: 48 additions & 0 deletions pkg/driver/external/workspace_identity_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package external

import (
"context"
"strconv"
"time"

"github.com/devsy-org/devsy/pkg/driver"
)

const (
fixtureProcessUser = "root"
fixtureRemoteUser = "developer"
)

func (s *HostSuite) TestWorkspaceIdentityThroughRuntimeProcess() {
for _, tc := range []struct {
name string
user string
remoteUser string
dockerless bool
}{
{"dockerless developer", fixtureProcessUser, fixtureRemoteUser, true},
{"prebuilt developer", fixtureProcessUser, fixtureRemoteUser, false},
{"numeric developer", fixtureProcessUser, "1000:1001", true},
{"empty developer identity", fixtureProcessUser, "", false},
{"unset identities", "", "", false},
} {
s.Run(tc.name, func() {
host := s.host("workspace-identity")
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
s.Require().NoError(host.RunImageDevContainer(ctx, &driver.RunImageDevContainerParams{
WorkspaceID: fixtureWorkspace,
Options: &driver.RunOptions{
Image: fixtureImage, User: tc.user,
RemoteUser: tc.remoteUser, Dockerless: tc.dockerless,
},
}))
found, err := host.FindDevContainer(ctx, fixtureWorkspace)
s.Require().NoError(err)
s.Require().NotNil(found)
s.Equal(tc.user, found.Config.User)
s.Equal(tc.remoteUser, found.Config.Labels["fixture.remote-user"])
s.Equal(strconv.FormatBool(tc.dockerless), found.Config.Labels["fixture.dockerless"])
})
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,16 @@ description: Contract for external runtime driver authors using the Devsy Runtim
Providers can select `agent.driver: external` using the [external runtime configuration](./driver). The SDK and Devsy host integration are available; backend extraction and parity testing remain in development.
</Callout>

The canonical [protobuf schema](https://github.com/devsy-org/devsy-runtime-sdk/blob/main/proto/devsy/runtime/v1/runtime.proto) is maintained in the [Devsy Runtime SDK](https://github.com/devsy-org/devsy-runtime-sdk). The module path is `github.com/devsy-org/devsy-runtime-sdk`; the logical plugin name is `devsy-runtime`. HashiCorp application protocol 1 and Info API major 1/minor 0 are separate version checks. Same-major newer minor versions are accepted. Unknown mount/recreate enum values are rejected because they control host behavior.
The canonical [protobuf schema](https://github.com/devsy-org/devsy-runtime-sdk/blob/main/proto/devsy/runtime/v1/runtime.proto) is maintained in the [Devsy Runtime SDK](https://github.com/devsy-org/devsy-runtime-sdk). The module path is `github.com/devsy-org/devsy-runtime-sdk`; the logical plugin name is `devsy-runtime`. HashiCorp application protocol 1 and Info API major 1/minor 1 are separate version checks. Same-major newer minor versions are accepted. Unknown mount/recreate enum values are rejected because they control host behavior.

## Runtime boundary

Info, Preflight, ProvisioningPreflight, Find, TargetArchitecture, RunImage, Start, Stop, Delete, Exec, and Logs are the v1 RPC surface. Runtime state persists in the backend across plugin processes. Plugins do not own image build/tag/push, registry credentials, Compose, IDE configuration, snapshots, provider machine lifecycle, or updates.

RunImage receives resolved intent. Its empty response acknowledges completion; Find queries state. `image_built_locally` is an image-origin hint, not permission to build. Optional privileged/init flags distinguish absent from explicit false. Environment and mounts may contain secrets and must not appear in diagnostic logs.

`remote_user` carries the developer identity used for workspace ownership, separately from the container process `user`. The host forwards both values without substituting one for the other. Runtimes use `remote_user` when set, otherwise `user`, otherwise `root` for workspace ownership. `dockerless` indicates that the host will build the developer filesystem after the image starts, so that identity may not yet exist in the image. A runtime that resolves mount ownership from image contents must validate this case before changing workspace resources. These fields describe provisioning intent and do not authorize replacement of an existing workspace.

Runtime name, driver name/version, and capabilities are required in Info. Runtime version may be empty when a backend cannot report it without expensive setup. An empty mount list means no supported mount types. ProvisioningPreflight may be a no-op when its capability is false. Logs may return Unimplemented when its capability is false. Reprovision means RunImage can update an existing workspace with complete resolved intent; it does not imply that an empty request is safe. The Devsy host does not enable in-place reprovisioning; workspace changes follow the negotiated stop/delete recreate policy.

TargetArchitecture returns canonical `amd64` or `arm64`. A runtime may require an existing workspace to answer; hosts must not require pre-start architecture discovery from such runtimes.
Expand Down
Loading