Sandbox-Runner mit bubblewrap (#654) - noch ungeprueft unter Linux
Jede Codeausfuehrung ausserhalb des Hauptprozesses: eigene Namensraeume (Mount, PID, Netz nur loopback, User ohne verschachtelte), nur der Arbeitsbereich unter /work beschreibbar, scripts/ darin schreibgeschuetzt, keine Umgebung und keine Deskriptoren des Hauptprozesses, alle Capabilities weg, no_new_privs, seccomp-Sperrliste (eigene BPF, x86_64 und arm64), Grenzen fuer Zeit, Speicher, Prozesse, Dateigroesse und Ausgabe. Unter Windows oder ohne bwrap: keine Ausfuehrung, mit Grund im Protokoll. Erster Nutzer: ffmpeg im SocialMediaManager (fremde Mediendateien). yt-dlp braucht Netz und folgt mit dem Egress-Proxy (#655). Die Ausbruchstests (SandboxEscapeTests) laufen nur unter Linux mit bwrap. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
8c5eca18f5
commit
257ba87cf5
@@ -8,7 +8,7 @@ using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Tools.SocialMediaManager;
|
||||
|
||||
public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider, ClawdDotNet.Core.Sandbox.ISandboxUser
|
||||
{
|
||||
public string Name => "SocialMediaManager";
|
||||
|
||||
@@ -523,7 +523,7 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
logger.LogInformation("Starting transcription for {File} ({Size:F1} MB) via OpenRouter ({Model}), ffmpeg={HasFfmpeg}",
|
||||
Path.GetFileName(audioFile), fileSize / 1024.0 / 1024.0, model, hasFfmpeg);
|
||||
|
||||
var transcript = await TranscribeAudioAsync(audioFile, apiKey, model, hasFfmpeg, ffmpegPath, ytDlpPath, logger, ct);
|
||||
var transcript = await TranscribeAudioAsync(audioFile, apiKey, model, hasFfmpeg, ffmpegPath, ytDlpPath, logger, ct, workspacePath);
|
||||
|
||||
// 4. Save transcript
|
||||
var transcriptFile = Path.Combine(targetDir, $"{videoId}.txt");
|
||||
@@ -574,8 +574,13 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
|
||||
private const long MaxChunkBytes = 24 * 1024 * 1024; // 24 MB (Whisper-Limit: 25 MB, Puffer)
|
||||
|
||||
/// <summary>#654: Where ffmpeg runs — set by the instance at start.</summary>
|
||||
public ClawdDotNet.Core.Sandbox.ISandboxRunner? Sandbox { get; set; }
|
||||
|
||||
private int _warnedUnsandboxed;
|
||||
|
||||
private async Task<string> TranscribeAudioAsync(string filePath, string apiKey, string model,
|
||||
bool hasFfmpeg, string? ffmpegPath, string ytDlpPath, ILogger logger, CancellationToken ct)
|
||||
bool hasFfmpeg, string? ffmpegPath, string ytDlpPath, ILogger logger, CancellationToken ct, string? workspacePath = null)
|
||||
{
|
||||
var fileInfo = new FileInfo(filePath);
|
||||
if (!fileInfo.Exists)
|
||||
@@ -596,8 +601,45 @@ public sealed class SocialMediaManagerTool : IAgentTool, IToolJobProvider
|
||||
{
|
||||
List<string> chunks;
|
||||
|
||||
if (hasFfmpeg)
|
||||
if (hasFfmpeg && workspacePath is not null && Sandbox is { IsAvailable: true } sandbox
|
||||
&& ClawdDotNet.Core.Sandbox.SandboxPaths.InSandbox(workspacePath, filePath) is { } inFile
|
||||
&& ClawdDotNet.Core.Sandbox.SandboxPaths.InSandbox(workspacePath, chunkDir) is { } inChunks)
|
||||
{
|
||||
// #654: The media comes from the internet — ffmpeg parses it in the sandbox,
|
||||
// seeing nothing but the workspace, without network.
|
||||
var ffmpegExe = !string.IsNullOrEmpty(ffmpegPath) ? ffmpegPath : "ffmpeg";
|
||||
var result = await sandbox.RunAsync(new ClawdDotNet.Core.Sandbox.SandboxRequest
|
||||
{
|
||||
Executable = ffmpegExe,
|
||||
Arguments = ["-hide_banner", "-loglevel", "error", "-i", inFile, "-f", "segment", "-segment_time", "1200",
|
||||
"-c:a", "libmp3lame", "-q:a", "5", inChunks + "/chunk_%03d.mp3"],
|
||||
WorkspacePath = workspacePath,
|
||||
// A program outside /usr is made visible where it lies — read-only.
|
||||
ReadOnlyPaths = Path.IsPathRooted(ffmpegExe) && !ffmpegExe.StartsWith("/usr/", StringComparison.Ordinal)
|
||||
? [Path.GetDirectoryName(ffmpegExe)!]
|
||||
: [],
|
||||
Limits = new ClawdDotNet.Core.Sandbox.SandboxLimits
|
||||
{
|
||||
Timeout = TimeSpan.FromMinutes(30),
|
||||
MaxFileBytes = 200L * 1024 * 1024,
|
||||
MaxOutputBytes = 256 * 1024
|
||||
}
|
||||
}, ct);
|
||||
if (!result.Succeeded)
|
||||
logger.LogWarning("ffmpeg in the sandbox ended with {Code}{Timeout}: {Error}",
|
||||
result.ExitCode, result.TimedOut ? " (timeout)" : "", result.StandardError.Trim());
|
||||
|
||||
chunks = Directory.GetFiles(chunkDir, "chunk_*.mp3").OrderBy(f => f).ToList();
|
||||
}
|
||||
else if (hasFfmpeg)
|
||||
{
|
||||
// #654: Without a sandbox (Windows, or no bwrap) as before — said once. yt-dlp,
|
||||
// which needs the network, still runs this way everywhere until #655 brings the
|
||||
// sandbox a network of its own.
|
||||
if (OperatingSystem.IsLinux() && Interlocked.Exchange(ref _warnedUnsandboxed, 1) == 0)
|
||||
logger.LogWarning("ffmpeg runs without a sandbox: {Reason}",
|
||||
Sandbox?.UnavailableReason ?? "no sandbox was handed to the tool");
|
||||
|
||||
// ffmpeg: 20-minute segments as mp3 (under 25 MB Whisper limit at q:a 5)
|
||||
var chunkPattern = Path.Combine(chunkDir, "chunk_%03d.mp3");
|
||||
var ffmpegExe = !string.IsNullOrEmpty(ffmpegPath) ? ffmpegPath : "ffmpeg";
|
||||
|
||||
Reference in New Issue
Block a user