feat: enhance coding harness, model security and system features

This commit is contained in:
ageerle
2026-09-08 23:33:29 +08:00
parent d4ed2b8672
commit bcff4fd9e3
259 changed files with 15183 additions and 2713 deletions

View File

@@ -0,0 +1,2 @@
**
!Dockerfile

View File

@@ -0,0 +1,38 @@
# syntax=docker/dockerfile:1.7
# Both upstream stages are official images pinned to the linux/amd64 manifest digest observed
# on 2026-08-30. The final scratch stage intentionally drops inherited image metadata such as
# VOLUME, CMD, ENTRYPOINT, healthcheck and environment before adding a minimal allowlist.
FROM node:24-bookworm@sha256:9137a20e25879e0b557227b57e3ee4e9af4bde29eb3db66134cd1723e84f830b AS node
FROM maven:3.9.16-eclipse-temurin-21-noble@sha256:edf045813426842617b1667456ddec0026885146465e890b897655d877ba3386 AS assembly
USER root
COPY --from=node /usr/local/ /usr/local/
RUN apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates git python3 ripgrep \
&& rm -rf /var/lib/apt/lists/* \
&& rm -f /usr/local/bin/pnpm /usr/local/bin/pnpx \
&& corepack enable pnpm \
&& corepack prepare pnpm@10.14.0 --activate \
&& groupadd --gid 65532 harness \
&& useradd --uid 65532 --gid 65532 --no-create-home --home-dir /tmp \
--shell /usr/sbin/nologin harness \
&& java -version \
&& mvn -version \
&& node --version \
&& npm --version \
&& pnpm --version \
&& python3 --version \
&& rg --version \
&& git --version
FROM scratch
COPY --from=assembly / /
ENV PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
JAVA_HOME=/opt/java/openjdk \
MAVEN_HOME=/usr/share/maven \
HOME=/tmp \
LANG=C.UTF-8
USER 65532:65532
WORKDIR /workspace
ENTRYPOINT []
CMD []

28
scripts/sandbox/README.md Normal file
View File

@@ -0,0 +1,28 @@
# Coding harness sandbox image
This directory builds the evaluator-owned Linux toolchain used by `execute_process`.
Runtime code never builds or pulls an image; it accepts only the final local `sha256:<image-id>`.
The build uses official Node and Maven manifests pinned for `linux/amd64`, then copies the
assembled filesystem into a `scratch` stage so inherited volumes, commands, entrypoints,
healthchecks, and environment do not cross the trust boundary. The final image contains Java,
Maven, Node, npm, pnpm 10.14.0, Python 3, ripgrep, and Git, and runs as `65532:65532`.
`build-trusted-sandbox.ps1` verifies the Docker CLI digest and owner/ACL before its first Docker
call. Every build gets a fresh empty CLI config, a minimal process environment, a bounded process
timeout, and a private context containing only digest-approved copies of `Dockerfile` and
`.dockerignore`. Image inspection uses a separate runtime config directory that must stay
completely empty and checks the exact platform, non-root user, working directory, absent implicit
commands/volumes/healthcheck, and an exact five-variable environment allowlist. The script prints
the final image ID, CLI digest, and build-definition digests; deployments must pass the exact image
and CLI values through `CODING_HARNESS_SANDBOX_IMAGE` and
`CODING_HARNESS_DOCKER_EXECUTABLE_SHA256`.
The digest defaults in this repository prevent accidental drift but are not an external trust
anchor: an operator release must approve the script/build-definition digests from an ACL- or
signature-protected evaluator boundary. APT package resolution and the Corepack download are not
yet hermetic, so the approved final image ID—not a rebuilt tag—is the runtime authority.
This image alone does not make the sandbox complete. Runtime create/start/inspect/remove state,
workspace identity, restart cleanup, and live adversarial canaries remain independent release
gates. Dependency caches are deliberately not inherited from the host and are not yet baked in.

View File

@@ -0,0 +1,297 @@
#requires -Version 7.0
[CmdletBinding()]
param(
[string]$DockerExecutable = "C:/Program Files/Docker/Docker/resources/bin/docker.exe",
[string]$ExpectedDockerExecutableSha256 = "83f31577a39b1c9f48c571cc2c31c99d74f74df1ab44003b4022301ed30a9831",
[string]$ExpectedDockerfileSha256 = "72c262c9396e59359e52edddb9963360da4c581298247231f6ab2e369ce43eef",
[string]$ExpectedDockerignoreSha256 = "16150e43ce98c333f93ad82a4991269a89bdd5596c427b4f5066ad266d6f871d",
[string]$BuildDockerConfigDirectory = "C:/Users/wl/.ruoyi-harness/evaluator/docker-build-config",
[string]$RuntimeDockerConfigDirectory = "C:/Users/wl/.ruoyi-harness/evaluator/docker-runtime-empty-config",
[string]$DockerHost = "npipe:////./pipe/docker_engine",
[string]$SourceContextDirectory = $PSScriptRoot,
[string]$ImageTag = "ruoyi-harness-toolchain:local"
)
Set-StrictMode -Version Latest
$ErrorActionPreference = "Stop"
$sourceContext = [System.IO.Path]::GetFullPath($SourceContextDirectory)
if (-not [System.IO.Directory]::Exists($sourceContext) -or
([System.IO.DirectoryInfo]::new($sourceContext).Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
-not [System.IO.File]::Exists((Join-Path $sourceContext "Dockerfile")) -or
-not [System.IO.File]::Exists((Join-Path $sourceContext ".dockerignore"))) {
throw "BUILD_SOURCE_CONTEXT_INVALID"
}
function Assert-NoBroadWriteAcl {
param([Parameter(Mandatory)][string]$LiteralPath)
$trustedWriterSids = [System.Collections.Generic.HashSet[string]]::new(
[System.StringComparer]::OrdinalIgnoreCase)
$null = $trustedWriterSids.Add("S-1-5-18") # SYSTEM
$null = $trustedWriterSids.Add("S-1-5-32-544") # BUILTIN\Administrators
$null = $trustedWriterSids.Add([System.Security.Principal.WindowsIdentity]::GetCurrent().User.Value)
try {
$trustedInstallerSid = ([System.Security.Principal.NTAccount]::new(
"NT SERVICE", "TrustedInstaller")).Translate(
[System.Security.Principal.SecurityIdentifier]).Value
$null = $trustedWriterSids.Add($trustedInstallerSid)
} catch {
throw "TRUSTED_INSTALLER_IDENTITY_UNRESOLVED"
}
$writeMask = [System.Security.AccessControl.FileSystemRights]::WriteData -bor
[System.Security.AccessControl.FileSystemRights]::AppendData -bor
[System.Security.AccessControl.FileSystemRights]::WriteExtendedAttributes -bor
[System.Security.AccessControl.FileSystemRights]::WriteAttributes -bor
[System.Security.AccessControl.FileSystemRights]::DeleteSubdirectoriesAndFiles -bor
[System.Security.AccessControl.FileSystemRights]::Delete -bor
[System.Security.AccessControl.FileSystemRights]::ChangePermissions -bor
[System.Security.AccessControl.FileSystemRights]::TakeOwnership
$target = [System.IO.Path]::GetFullPath($LiteralPath)
$paths = @($target)
$parent = [System.IO.Directory]::GetParent($target)
if ($null -ne $parent) { $paths += $parent.FullName }
foreach ($path in $paths) {
$acl = Get-Acl -LiteralPath $path
$ownerSid = ([System.Security.Principal.NTAccount]$acl.Owner).Translate(
[System.Security.Principal.SecurityIdentifier]).Value
if (-not $trustedWriterSids.Contains($ownerSid)) {
throw "UNTRUSTED_EVALUATOR_ACL_OWNER"
}
foreach ($rule in $acl.Access) {
if ($rule.AccessControlType -ne [System.Security.AccessControl.AccessControlType]::Allow -or
($rule.FileSystemRights -band $writeMask) -eq 0) {
continue
}
try {
$sid = $rule.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
} catch {
throw "UNRESOLVED_EVALUATOR_ACL_IDENTITY"
}
if (-not $trustedWriterSids.Contains($sid)) {
throw "UNTRUSTED_WRITE_EVALUATOR_ACL_DENIED"
}
}
}
}
function Invoke-IsolatedDocker {
param(
[Parameter(Mandatory)][string]$Executable,
[Parameter(Mandatory)][string[]]$ArgumentList,
[Parameter(Mandatory)][string]$TempDirectory,
[ValidateRange(1, 600000)][int]$TimeoutMs = 120000,
[switch]$EchoOutput
)
$startInfo = [System.Diagnostics.ProcessStartInfo]::new()
$startInfo.FileName = $Executable
$startInfo.UseShellExecute = $false
$startInfo.CreateNoWindow = $true
$startInfo.RedirectStandardOutput = $true
$startInfo.RedirectStandardError = $true
foreach ($argument in $ArgumentList) {
$startInfo.ArgumentList.Add($argument)
}
$startInfo.Environment.Clear()
$windowsDirectory = [Environment]::GetFolderPath([Environment+SpecialFolder]::Windows)
$startInfo.Environment["SystemRoot"] = $windowsDirectory
$startInfo.Environment["WINDIR"] = $windowsDirectory
$startInfo.Environment["TEMP"] = $TempDirectory
$startInfo.Environment["TMP"] = $TempDirectory
$process = [System.Diagnostics.Process]::new()
$process.StartInfo = $startInfo
if (-not $process.Start()) {
throw "TRUSTED_DOCKER_PROCESS_START_FAILED"
}
$stdoutTask = $process.StandardOutput.ReadToEndAsync()
$stderrTask = $process.StandardError.ReadToEndAsync()
if (-not $process.WaitForExit($TimeoutMs)) {
try {
$process.Kill($true)
$null = $process.WaitForExit(5000)
} finally {
$process.Dispose()
}
throw "TRUSTED_DOCKER_PROCESS_TIMEOUT"
}
$stdout = $stdoutTask.GetAwaiter().GetResult()
$stderr = $stderrTask.GetAwaiter().GetResult()
if ($EchoOutput) {
if (-not [string]::IsNullOrEmpty($stdout)) { [Console]::Out.Write($stdout) }
if (-not [string]::IsNullOrEmpty($stderr)) { [Console]::Error.Write($stderr) }
}
$result = [pscustomobject]@{
ExitCode = $process.ExitCode
Stdout = $stdout
Stderr = $stderr
}
$process.Dispose()
$result
}
$docker = [System.IO.Path]::GetFullPath($DockerExecutable)
if (-not [System.IO.File]::Exists($docker) -or
-not [System.IO.Path]::GetFileName($docker).Equals("docker.exe", [System.StringComparison]::OrdinalIgnoreCase)) {
throw "TRUSTED_DOCKER_CLI_REQUIRED"
}
$dockerInfo = [System.IO.FileInfo]::new($docker)
if (($dockerInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "TRUSTED_DOCKER_CLI_REPARSE_DENIED"
}
if ($ExpectedDockerExecutableSha256 -notmatch '^[0-9a-fA-F]{64}$') {
throw "TRUSTED_DOCKER_CLI_DIGEST_REQUIRED"
}
$dockerSha256 = (Get-FileHash -LiteralPath $docker -Algorithm SHA256).Hash.ToLowerInvariant()
if (-not $dockerSha256.Equals($ExpectedDockerExecutableSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal)) {
throw "TRUSTED_DOCKER_CLI_DIGEST_MISMATCH"
}
Assert-NoBroadWriteAcl -LiteralPath $docker
$buildConfigRoot = [System.IO.Path]::GetFullPath($BuildDockerConfigDirectory)
$runtimeConfig = [System.IO.Path]::GetFullPath($RuntimeDockerConfigDirectory)
[System.IO.Directory]::CreateDirectory($buildConfigRoot) | Out-Null
[System.IO.Directory]::CreateDirectory($runtimeConfig) | Out-Null
$buildConfigRootInfo = [System.IO.DirectoryInfo]::new($buildConfigRoot)
$runtimeConfigInfo = [System.IO.DirectoryInfo]::new($runtimeConfig)
if (($buildConfigRootInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0) {
throw "CREDENTIAL_FREE_BUILD_DOCKER_CONFIG_REQUIRED"
}
if (($runtimeConfigInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
@($runtimeConfigInfo.EnumerateFileSystemInfos()).Count -ne 0) {
throw "EMPTY_LINK_FREE_RUNTIME_DOCKER_CONFIG_REQUIRED"
}
Assert-NoBroadWriteAcl -LiteralPath $buildConfigRoot
Assert-NoBroadWriteAcl -LiteralPath $runtimeConfig
$buildConfig = [System.IO.Path]::GetFullPath((Join-Path $buildConfigRoot ("session-" + [guid]::NewGuid().ToString("N"))))
if (-not ([System.IO.DirectoryInfo]::new($buildConfig).Parent.FullName).Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "BUILD_DOCKER_CONFIG_BOUNDARY_INVALID"
}
[System.IO.Directory]::CreateDirectory($buildConfig) | Out-Null
$buildConfigInfo = [System.IO.DirectoryInfo]::new($buildConfig)
if (($buildConfigInfo.Attributes -band [System.IO.FileAttributes]::ReparsePoint) -ne 0 -or
@($buildConfigInfo.EnumerateFileSystemInfos()).Count -ne 0) {
throw "EMPTY_LINK_FREE_BUILD_DOCKER_CONFIG_REQUIRED"
}
Assert-NoBroadWriteAcl -LiteralPath $buildConfig
if ($DockerHost -ne "npipe:////./pipe/docker_engine") {
throw "LOCAL_DOCKER_ENGINE_REQUIRED"
}
$processRoot = [System.IO.Path]::GetFullPath((Join-Path $buildConfigRoot ("process-" + [guid]::NewGuid().ToString("N"))))
try {
if (-not ([System.IO.DirectoryInfo]::new($processRoot).Parent.FullName).Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase)) {
throw "BUILD_PROCESS_BOUNDARY_INVALID"
}
$context = Join-Path $processRoot "context"
$processTemp = Join-Path $processRoot "temp"
[System.IO.Directory]::CreateDirectory($context) | Out-Null
[System.IO.Directory]::CreateDirectory($processTemp) | Out-Null
Assert-NoBroadWriteAcl -LiteralPath $processRoot
Assert-NoBroadWriteAcl -LiteralPath $context
Assert-NoBroadWriteAcl -LiteralPath $processTemp
if ($ExpectedDockerfileSha256 -notmatch '^[0-9a-fA-F]{64}$' -or
$ExpectedDockerignoreSha256 -notmatch '^[0-9a-fA-F]{64}$') {
throw "APPROVED_BUILD_CONTEXT_DIGESTS_REQUIRED"
}
$dockerfile = Join-Path $context "Dockerfile"
$dockerignore = Join-Path $context ".dockerignore"
[System.IO.File]::Copy((Join-Path $sourceContext "Dockerfile"), $dockerfile, $false)
[System.IO.File]::Copy((Join-Path $sourceContext ".dockerignore"), $dockerignore, $false)
$dockerfileSha256 = (Get-FileHash -LiteralPath $dockerfile -Algorithm SHA256).Hash.ToLowerInvariant()
$dockerignoreSha256 = (Get-FileHash -LiteralPath $dockerignore -Algorithm SHA256).Hash.ToLowerInvariant()
if (-not $dockerfileSha256.Equals($ExpectedDockerfileSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal) -or
-not $dockerignoreSha256.Equals($ExpectedDockerignoreSha256.ToLowerInvariant(), [System.StringComparison]::Ordinal)) {
throw "BUILD_CONTEXT_DIGEST_MISMATCH"
}
$buildResult = Invoke-IsolatedDocker -Executable $docker -TempDirectory $processTemp -EchoOutput `
-ArgumentList @("--config", $buildConfig, "--host", $DockerHost, "build",
"--platform", "linux/amd64", "--pull", "--tag", $ImageTag,
"--file", $dockerfile, $context)
if ($buildResult.ExitCode -ne 0) {
throw "SANDBOX_IMAGE_BUILD_FAILED"
}
$inspectResult = Invoke-IsolatedDocker -Executable $docker -TempDirectory $processTemp -TimeoutMs 10000 `
-ArgumentList @("--config", $runtimeConfig, "--host", $DockerHost,
"image", "inspect", $ImageTag)
if ($inspectResult.ExitCode -ne 0) {
throw "SANDBOX_IMAGE_INSPECT_FAILED"
}
$inspectJson = $inspectResult.Stdout
$records = @($inspectJson | ConvertFrom-Json -Depth 32)
if ($records.Count -ne 1) {
throw "SANDBOX_IMAGE_IDENTITY_AMBIGUOUS"
}
$image = $records[0]
$volumesProperty = $image.Config.PSObject.Properties['Volumes']
$cmdProperty = $image.Config.PSObject.Properties['Cmd']
$healthcheckProperty = $image.Config.PSObject.Properties['Healthcheck']
$entrypointProperty = $image.Config.PSObject.Properties['Entrypoint']
$volumes = if ($null -eq $volumesProperty) { $null } else { $volumesProperty.Value }
$commandValue = if ($null -eq $cmdProperty) { $null } else { $cmdProperty.Value }
$healthcheck = if ($null -eq $healthcheckProperty) { $null } else { $healthcheckProperty.Value }
$entrypoint = if ($null -eq $entrypointProperty) { $null } else { $entrypointProperty.Value }
if ([string]$image.Id -notmatch '^sha256:[0-9a-f]{64}$' -or
[string]$image.Os -ne "linux" -or [string]$image.Architecture -ne "amd64" -or
[string]$image.Config.User -ne "65532:65532" -or
[string]$image.Config.WorkingDir -ne "/workspace" -or
$null -ne $volumes -or $null -ne $commandValue -or
$null -ne $healthcheck -or $null -ne $entrypoint) {
throw "SANDBOX_IMAGE_METADATA_UNTRUSTED"
}
$approvedEnvironment = @{
PATH = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
JAVA_HOME = "/opt/java/openjdk"
MAVEN_HOME = "/usr/share/maven"
HOME = "/tmp"
LANG = "C.UTF-8"
}
$seenEnvironment = [System.Collections.Generic.HashSet[string]]::new([System.StringComparer]::Ordinal)
foreach ($entry in @($image.Config.Env)) {
$separator = ([string]$entry).IndexOf('=')
if ($separator -le 0) {
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
}
$name = ([string]$entry).Substring(0, $separator)
$value = ([string]$entry).Substring($separator + 1)
if (-not $approvedEnvironment.ContainsKey($name) -or
-not ([string]$approvedEnvironment[$name]).Equals($value, [System.StringComparison]::Ordinal) -or
-not $seenEnvironment.Add($name)) {
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
}
}
if ($seenEnvironment.Count -ne $approvedEnvironment.Count) {
throw "SANDBOX_IMAGE_ENVIRONMENT_UNTRUSTED"
}
[ordered]@{
schemaVersion = 1
status = "BUILT_AND_INSPECTED"
imageId = [string]$image.Id
dockerExecutableSha256 = $dockerSha256
dockerfileSha256 = $dockerfileSha256
dockerignoreSha256 = $dockerignoreSha256
dockerConfigDirectory = $runtimeConfig
dockerHost = $DockerHost
baseManifests = @(
"maven@sha256:edf045813426842617b1667456ddec0026885146465e890b897655d877ba3386"
"node@sha256:9137a20e25879e0b557227b57e3ee4e9af4bde29eb3db66134cd1723e84f830b"
)
} | ConvertTo-Json -Depth 6 -Compress
} finally {
$sessionInfo = [System.IO.DirectoryInfo]::new($buildConfig)
if ([System.IO.Directory]::Exists($buildConfig) -and
$sessionInfo.Parent.FullName.Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
$sessionInfo.Name.StartsWith("session-", [System.StringComparison]::Ordinal)) {
Remove-Item -LiteralPath $buildConfig -Recurse -Force
}
$processInfo = [System.IO.DirectoryInfo]::new($processRoot)
if ([System.IO.Directory]::Exists($processRoot) -and
$processInfo.Parent.FullName.Equals($buildConfigRoot, [System.StringComparison]::OrdinalIgnoreCase) -and
$processInfo.Name.StartsWith("process-", [System.StringComparison]::Ordinal)) {
Remove-Item -LiteralPath $processRoot -Recurse -Force
}
}