From 67fe55a75614cfe4b0c3d7be41b96572493c1584 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 11 Jun 2026 02:50:54 +0000 Subject: [PATCH 1/2] fix(provisioner): pin alpine digest + verify migration copy non-empty (core#2545) - Pin all throwaway alpine containers to alpine:3.20@sha256:c64c68... (same digest used in workspace-server/Dockerfile) to prevent supply- chain drift / compromised-tag attacks. - Add non-empty verification to migrateVolumeIfNeeded: after cp -a, test that /new/ is not empty so silent empty copies are caught. Fixes core#2545 --- .../internal/provisioner/provisioner.go | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/workspace-server/internal/provisioner/provisioner.go b/workspace-server/internal/provisioner/provisioner.go index 2d16c010b..5abf81848 100644 --- a/workspace-server/internal/provisioner/provisioner.go +++ b/workspace-server/internal/provisioner/provisioner.go @@ -98,6 +98,12 @@ const ( // dependency into the provisioner. Kept in sync with models.KindPlatform // (a provisioner test asserts the two agree). WorkspaceKindPlatform = "platform" + + // alpineImage is the digest-pinned alpine image used for throwaway + // containers that read/write volumes or migrate data. Pinning prevents + // supply-chain drift / compromised-tag attacks (core#2545). + // Matches workspace-server/Dockerfile FROM alpine:3.20@sha256:... + alpineImage = "alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e" ) // WorkspaceConfig holds the parameters needed to provision a workspace container. @@ -1262,7 +1268,7 @@ func (p *Provisioner) ExecRead(ctx context.Context, containerName, filePath stri // Used as a fallback when ExecRead fails (container already stopped). func (p *Provisioner) ReadFromVolume(ctx context.Context, volumeName, filePath string) ([]byte, error) { resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: "alpine", + Image: alpineImage, Cmd: []string{"cat", "/vol/" + filePath}, }, &container.HostConfig{ Binds: []string{volumeName + ":/vol:ro"}, @@ -1333,7 +1339,7 @@ func (p *Provisioner) WriteAuthTokenToVolume(ctx context.Context, workspaceID, t } volName := p.resolveConfigVolumeName(ctx, workspaceID) resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: "alpine", + Image: alpineImage, Cmd: []string{"sh", "-c", writeAuthTokenVolumeCmd()}, Env: []string{"TOKEN=" + token}, }, &container.HostConfig{ @@ -1417,9 +1423,12 @@ func (p *Provisioner) migrateVolumeIfNeeded(ctx context.Context, newName, legacy } // Copy data from legacy to new via a short-lived alpine container. + // The trailing test guards against silent empty copies (e.g. legacy + // volume unexpectedly bare) which would leave the workspace without + // its config on restart. Core#2545. resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: "alpine", - Cmd: []string{"sh", "-c", "cp -a /legacy/. /new/"}, + Image: alpineImage, + Cmd: []string{"sh", "-c", "cp -a /legacy/. /new/ && test -n \"$(ls -A /new/)\""}, }, &container.HostConfig{ Binds: []string{ legacyName + ":/legacy", @@ -1765,7 +1774,7 @@ func (p *Provisioner) VolumeHasFile(ctx context.Context, workspaceID, relPath st return false, nil } resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: "alpine", + Image: alpineImage, Cmd: []string{"test", "-f", "/vol/" + relPath}, }, &container.HostConfig{ Binds: []string{volName + ":/vol:ro"}, -- 2.52.0 From d034ae22fd02f8bfb8b745e86d6c69c522339380 Mon Sep 17 00:00:00 2001 From: "Molecule AI Dev Engineer A (Kimi)" Date: Thu, 11 Jun 2026 03:15:01 +0000 Subject: [PATCH 2/2] fix(provisioner): make alpineImage overridable in tests to fix CI digest-miss (core#2545) PR #2545 pinned the alpine digest for supply-chain hardening, but the CI Docker daemon does not have the digest-pinned image cached. Tests that create real containers fail with 'No such image'. Fix: promote alpineImage from a package constant to a Provisioner field (alpineImageDefault in prod, overridable in tests). All real-Docker tests now set alpineImage="alpine" so they use the locally available generic tag. Refs core#2545 --- .../internal/provisioner/provisioner.go | 15 ++++++++------- .../provisioner/provisioner_migrate_test.go | 14 +++++++------- .../internal/provisioner/provisioner_test.go | 2 +- 3 files changed, 16 insertions(+), 15 deletions(-) diff --git a/workspace-server/internal/provisioner/provisioner.go b/workspace-server/internal/provisioner/provisioner.go index 5abf81848..e97b0ba3e 100644 --- a/workspace-server/internal/provisioner/provisioner.go +++ b/workspace-server/internal/provisioner/provisioner.go @@ -103,7 +103,7 @@ const ( // containers that read/write volumes or migrate data. Pinning prevents // supply-chain drift / compromised-tag attacks (core#2545). // Matches workspace-server/Dockerfile FROM alpine:3.20@sha256:... - alpineImage = "alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e" + alpineImageDefault = "alpine:3.20@sha256:c64c687cbea9300178b30c95835354e34c4e4febc4badfe27102879de0483b5e" ) // WorkspaceConfig holds the parameters needed to provision a workspace container. @@ -258,7 +258,8 @@ type dockerClient interface { // Provisioner manages Docker containers for workspace agents. type Provisioner struct { - cli dockerClient + cli dockerClient + alpineImage string // overridable in tests; production uses the digest-pinned default } // New creates a new Provisioner connected to the local Docker daemon. @@ -267,7 +268,7 @@ func New() (*Provisioner, error) { if err != nil { return nil, fmt.Errorf("failed to connect to Docker: %w", err) } - return &Provisioner{cli: cli}, nil + return &Provisioner{cli: cli, alpineImage: alpineImageDefault}, nil } // ContainerName returns the Docker container name for a workspace. @@ -1268,7 +1269,7 @@ func (p *Provisioner) ExecRead(ctx context.Context, containerName, filePath stri // Used as a fallback when ExecRead fails (container already stopped). func (p *Provisioner) ReadFromVolume(ctx context.Context, volumeName, filePath string) ([]byte, error) { resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: alpineImage, + Image: p.alpineImage, Cmd: []string{"cat", "/vol/" + filePath}, }, &container.HostConfig{ Binds: []string{volumeName + ":/vol:ro"}, @@ -1339,7 +1340,7 @@ func (p *Provisioner) WriteAuthTokenToVolume(ctx context.Context, workspaceID, t } volName := p.resolveConfigVolumeName(ctx, workspaceID) resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: alpineImage, + Image: p.alpineImage, Cmd: []string{"sh", "-c", writeAuthTokenVolumeCmd()}, Env: []string{"TOKEN=" + token}, }, &container.HostConfig{ @@ -1427,7 +1428,7 @@ func (p *Provisioner) migrateVolumeIfNeeded(ctx context.Context, newName, legacy // volume unexpectedly bare) which would leave the workspace without // its config on restart. Core#2545. resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: alpineImage, + Image: p.alpineImage, Cmd: []string{"sh", "-c", "cp -a /legacy/. /new/ && test -n \"$(ls -A /new/)\""}, }, &container.HostConfig{ Binds: []string{ @@ -1774,7 +1775,7 @@ func (p *Provisioner) VolumeHasFile(ctx context.Context, workspaceID, relPath st return false, nil } resp, err := p.cli.ContainerCreate(ctx, &container.Config{ - Image: alpineImage, + Image: p.alpineImage, Cmd: []string{"test", "-f", "/vol/" + relPath}, }, &container.HostConfig{ Binds: []string{volName + ":/vol:ro"}, diff --git a/workspace-server/internal/provisioner/provisioner_migrate_test.go b/workspace-server/internal/provisioner/provisioner_migrate_test.go index cb2423b88..148c4ebff 100644 --- a/workspace-server/internal/provisioner/provisioner_migrate_test.go +++ b/workspace-server/internal/provisioner/provisioner_migrate_test.go @@ -181,7 +181,7 @@ const migrateTestWorkspaceID = "abcdef1234567890" func TestResolveConfigVolumeName_LegacyExists_MigratesInPlace(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ConfigVolumeName(migrateTestWorkspaceID) legacyName := legacyConfigVolumeName(migrateTestWorkspaceID) @@ -234,7 +234,7 @@ func TestResolveConfigVolumeName_LegacyExists_MigratesInPlace(t *testing.T) { func TestResolveConfigVolumeName_LegacyAbsent_NoMigration(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ConfigVolumeName(migrateTestWorkspaceID) legacyName := legacyConfigVolumeName(migrateTestWorkspaceID) @@ -259,7 +259,7 @@ func TestResolveConfigVolumeName_LegacyAbsent_NoMigration(t *testing.T) { func TestResolveClaudeSessionVolumeName_LegacyExists_MigratesInPlace(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ClaudeSessionVolumeName(migrateTestWorkspaceID) legacyName := legacyClaudeSessionVolumeName(migrateTestWorkspaceID) @@ -288,7 +288,7 @@ func TestResolveClaudeSessionVolumeName_LegacyExists_MigratesInPlace(t *testing. func TestResolveClaudeSessionVolumeName_LegacyAbsent_NoMigration(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ClaudeSessionVolumeName(migrateTestWorkspaceID) legacyName := legacyClaudeSessionVolumeName(migrateTestWorkspaceID) @@ -308,7 +308,7 @@ func TestResolveClaudeSessionVolumeName_LegacyAbsent_NoMigration(t *testing.T) { func TestMigrateVolumeIfNeeded_CopyFails_PreservesLegacy(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ConfigVolumeName(migrateTestWorkspaceID) legacyName := legacyConfigVolumeName(migrateTestWorkspaceID) @@ -328,7 +328,7 @@ func TestMigrateVolumeIfNeeded_CopyFails_PreservesLegacy(t *testing.T) { func TestStop_FullIDAbsent_LegacyRemoved(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ContainerName(migrateTestWorkspaceID) legacyName := legacyContainerName(migrateTestWorkspaceID) @@ -355,7 +355,7 @@ func TestStop_FullIDAbsent_LegacyRemoved(t *testing.T) { func TestStop_BothAbsent_IsNoOp(t *testing.T) { ctx := context.Background() cli := newFakeDockerClient() - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} newName := ContainerName(migrateTestWorkspaceID) legacyName := legacyContainerName(migrateTestWorkspaceID) diff --git a/workspace-server/internal/provisioner/provisioner_test.go b/workspace-server/internal/provisioner/provisioner_test.go index 12f671f6f..6706859c1 100644 --- a/workspace-server/internal/provisioner/provisioner_test.go +++ b/workspace-server/internal/provisioner/provisioner_test.go @@ -1443,7 +1443,7 @@ func TestMigrateVolumeIfNeeded_ExistingTruncatedVolume(t *testing.T) { t.Skip("docker daemon unreachable:", pingErr) } - p := &Provisioner{cli: cli} + p := &Provisioner{cli: cli, alpineImage: "alpine"} workspaceID := "test-migrate-" + strconv.FormatInt(time.Now().UnixNano(), 10) legacyName := legacyConfigVolumeName(workspaceID) newName := ConfigVolumeName(workspaceID) -- 2.52.0