diff --git a/THIRD_PARTY_LICENSES.md b/THIRD_PARTY_LICENSES.md index 6190cd4076..b5fef46da7 100644 --- a/THIRD_PARTY_LICENSES.md +++ b/THIRD_PARTY_LICENSES.md @@ -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 | @@ -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 | diff --git a/go.mod b/go.mod index 60d2f53716..5cb904f858 100644 --- a/go.mod +++ b/go.mod @@ -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 diff --git a/go.sum b/go.sum index 8974774707..678ef9b1dd 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/pkg/driver/external/internal/testfixture/main.go b/pkg/driver/external/internal/testfixture/main.go index aa8ce9cda1..c020889662 100644 --- a/pkg/driver/external/internal/testfixture/main.go +++ b/pkg/driver/external/internal/testfixture/main.go @@ -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}) @@ -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 diff --git a/pkg/driver/external/run_image.go b/pkg/driver/external/run_image.go index d9657fdf79..73ddcdc733 100644 --- a/pkg/driver/external/run_image.go +++ b/pkg/driver/external/run_image.go @@ -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), diff --git a/pkg/driver/external/run_image_test.go b/pkg/driver/external/run_image_test.go index 2ff4ca2900..134e340058 100644 --- a/pkg/driver/external/run_image_test.go +++ b/pkg/driver/external/run_image_test.go @@ -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"}, @@ -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{ diff --git a/pkg/driver/external/workspace_identity_test.go b/pkg/driver/external/workspace_identity_test.go new file mode 100644 index 0000000000..1f9af2c05d --- /dev/null +++ b/pkg/driver/external/workspace_identity_test.go @@ -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"]) + }) + } +} diff --git a/sites/docs-devsy-sh/content/docs/developing-providers/runtime-protocol.mdx b/sites/docs-devsy-sh/content/docs/developing-providers/runtime-protocol.mdx index f8c1b46c28..2075055581 100644 --- a/sites/docs-devsy-sh/content/docs/developing-providers/runtime-protocol.mdx +++ b/sites/docs-devsy-sh/content/docs/developing-providers/runtime-protocol.mdx @@ -7,7 +7,7 @@ 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. -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 @@ -15,6 +15,8 @@ Info, Preflight, ProvisioningPreflight, Find, TargetArchitecture, RunImage, Star 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.