package com.easyagents.agent.runtime.tool.operate;

import org.apache.commons.compress.archivers.tar.TarArchiveEntry;
import org.apache.commons.compress.archivers.tar.TarArchiveInputStream;
import org.apache.commons.compress.archivers.tar.TarArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.UnixStat;
import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
import org.apache.commons.compress.archivers.zip.ZipFile;
import org.apache.commons.compress.compressors.gzip.GzipCompressorInputStream;
import org.apache.commons.compress.compressors.gzip.GzipCompressorOutputStream;
import org.apache.commons.compress.compressors.gzip.GzipParameters;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.FilterOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.channels.SeekableByteChannel;
import java.nio.charset.CharacterCodingException;
import java.nio.charset.CodingErrorAction;
import java.nio.charset.StandardCharsets;
import java.nio.file.AtomicMoveNotSupportedException;
import java.nio.file.Files;
import java.nio.file.LinkOption;
import java.nio.file.OpenOption;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
import java.nio.file.attribute.BasicFileAttributes;
import java.time.Instant;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import java.util.regex.Pattern;
import java.util.stream.Stream;
import java.util.zip.CRC32;

/**
 * 使用纯 Java 流完成的工作区安全归档命令执行器。
 *
 * <p>所有归档输入先复制到工作区外、同文件系统的临时目录。解包会先完整扫描条目，再写入
 * staging，最后以逐文件原子移动和失败补偿提交，系统归档程序不会参与执行。
 */
final class SafeArchiveCommandExecutor {

    private static final Logger logger = LoggerFactory.getLogger(SafeArchiveCommandExecutor.class);
    static final Set<String> COMMANDS = Set.of("gzip", "gunzip", "zip", "unzip", "tar");

    private static final Pattern WINDOWS_ABSOLUTE = Pattern.compile("^[A-Za-z]:/.*");
    private static final int MAX_PATH_LENGTH = 512;
    private static final int MAX_PATH_DEPTH = 64;
    private static final int COPY_BUFFER_SIZE = 16 * 1024;

    private final WorkspacePathGuard pathGuard;
    private final WorkspaceQuotaGuard quotaGuard;
    private final int maxOutputBytes;
    private final ArchiveCommitObserver commitObserver;

    /**
     * 创建归档命令执行器。
     *
     * @param pathGuard 工作区路径保护器
     * @param quotaGuard 工作区配额保护器
     * @param maxOutputBytes 命令文本输出最大字节数
     */
    SafeArchiveCommandExecutor(WorkspacePathGuard pathGuard,
                               WorkspaceQuotaGuard quotaGuard,
                               int maxOutputBytes) {
        this(pathGuard, quotaGuard, maxOutputBytes, ArchiveCommitObserver.noop());
    }

    /**
     * 创建带提交观察器的归档执行器，供事务补偿测试使用。
     *
     * @param pathGuard 工作区路径保护器
     * @param quotaGuard 工作区配额保护器
     * @param maxOutputBytes 命令文本输出最大字节数
     * @param commitObserver 提交观察器
     */
    SafeArchiveCommandExecutor(WorkspacePathGuard pathGuard,
                               WorkspaceQuotaGuard quotaGuard,
                               int maxOutputBytes,
                               ArchiveCommitObserver commitObserver) {
        this.pathGuard = pathGuard;
        this.quotaGuard = quotaGuard;
        this.maxOutputBytes = maxOutputBytes;
        this.commitObserver = commitObserver == null ? ArchiveCommitObserver.noop() : commitObserver;
    }

    /**
     * 执行已经完成 Shell 安全分词的归档命令。
     *
     * @param arguments 命令及参数
     * @param deadlineNanos 单调时钟截止时间
     * @return 有界文本结果
     */
    ArchiveExecutionResult execute(List<String> arguments, long deadlineNanos) {
        if (arguments == null || arguments.isEmpty() || !COMMANDS.contains(arguments.get(0))) {
            throw denied("Unsupported archive command.");
        }
        try (Stage stage = Stage.create(pathGuard.root())) {
            return switch (arguments.get(0)) {
                case "gzip" -> gzip(arguments, false, stage, deadlineNanos);
                case "gunzip" -> gzip(arguments, true, stage, deadlineNanos);
                case "zip" -> zip(arguments, stage, deadlineNanos);
                case "unzip" -> unzip(arguments, stage, deadlineNanos);
                case "tar" -> tar(arguments, stage, deadlineNanos);
                default -> throw denied("Unsupported archive command.");
            };
        } catch (WorkspaceToolException error) {
            throw error;
        } catch (IOException error) {
            throw new WorkspaceToolException("ARCHIVE_IO_FAILED",
                    "Archive operation failed.", true, error);
        }
    }

    private ArchiveExecutionResult gzip(List<String> arguments,
                                         boolean decompress,
                                         Stage stage,
                                         long deadlineNanos) throws IOException {
        ParsedFileList parsed = parseFileList(arguments, Set.of("-k"));
        boolean keep = parsed.options().contains("-k");
        if (parsed.files().isEmpty()) {
            throw denied(arguments.get(0) + " requires at least one workspace file.");
        }
        List<OutputPlan> outputs = new ArrayList<>();
        List<DeletionPlan> deletions = new ArrayList<>();
        Map<Path, Long> quotaPlan = new LinkedHashMap<>();
        OutputCollector output = new OutputCollector(maxOutputBytes);
        long expandedTotal = 0;
        for (String file : parsed.files()) {
            checkDeadline(deadlineNanos);
            Path input = pathGuard.resolveExistingFile(file);
            String targetName = decompress ? gunzipTarget(file) : file + ".gz";
            Path target = newOutputTarget(targetName);
            Snapshot snapshot = snapshotFile(input, stage, deadlineNanos);
            Path stagedOutput = stage.newFile("gzip-output-");
            if (decompress) {
                long written;
                try (InputStream raw = Files.newInputStream(snapshot.path(), StandardOpenOption.READ);
                     GzipCompressorInputStream gzip = new GzipCompressorInputStream(raw);
                     OutputStream targetOutput = limitedFileOutput(
                             stagedOutput, quotaGuard.maxArchiveSingleFileSize())) {
                    written = transfer(gzip, targetOutput,
                            quotaGuard.maxArchiveSingleFileSize(), deadlineNanos, null);
                } catch (IOException error) {
                    throw invalidArchive("GZIP input is invalid.", error);
                }
                expandedTotal = addExpanded(expandedTotal, written);
            } else {
                GzipParameters parameters = new GzipParameters();
                parameters.setModificationInstant(Instant.EPOCH);
                try (OutputStream raw = limitedFileOutput(
                        stagedOutput, quotaGuard.maxArchiveSingleFileSize());
                     GzipCompressorOutputStream gzip = new GzipCompressorOutputStream(raw, parameters);
                     InputStream source = Files.newInputStream(snapshot.path(), StandardOpenOption.READ)) {
                    transfer(source, gzip, snapshot.size(), deadlineNanos, null);
                }
                expandedTotal = addExpanded(expandedTotal, snapshot.size());
            }
            forceFile(stagedOutput);
            long outputSize = Files.size(stagedOutput);
            outputs.add(new OutputPlan(target, stagedOutput));
            quotaPlan.put(target, outputSize);
            if (!keep) {
                deletions.add(new DeletionPlan(input, snapshot.fingerprint()));
                quotaPlan.put(input, -1L);
            }
            output.append(pathGuard.display(target)).append('\n');
        }
        quotaGuard.validateBatch(quotaPlan);
        commit(stage, outputs, List.of(), deletions, deadlineNanos);
        return output.result();
    }

    private ArchiveExecutionResult zip(List<String> arguments,
                                       Stage stage,
                                       long deadlineNanos) throws IOException {
        ParsedZip parsed = parseZip(arguments);
        Path target = newOutputTarget(parsed.archive());
        List<ArchiveSource> sources = snapshotSources(
                parsed.inputs(), pathGuard.root(), parsed.recursive(), stage, deadlineNanos);
        Path stagedArchive = stage.newFile("zip-output-");
        try (OutputStream raw = limitedFileOutput(stagedArchive, quotaGuard.maxArchiveSingleFileSize());
             ZipArchiveOutputStream zip = new ZipArchiveOutputStream(raw)) {
            zip.setEncoding(StandardCharsets.UTF_8.name());
            zip.setUseLanguageEncodingFlag(true);
            for (ArchiveSource source : sources) {
                checkDeadline(deadlineNanos);
                String name = source.directory() ? source.name() + "/" : source.name();
                ZipArchiveEntry entry = new ZipArchiveEntry(name);
                entry.setTime(0L);
                entry.setUnixMode(source.directory() ? UnixStat.DEFAULT_DIR_PERM : UnixStat.DEFAULT_FILE_PERM);
                zip.putArchiveEntry(entry);
                if (!source.directory()) {
                    try (InputStream input = Files.newInputStream(source.snapshot(), StandardOpenOption.READ)) {
                        transfer(input, zip, source.size(), deadlineNanos, null);
                    }
                }
                zip.closeArchiveEntry();
            }
            zip.finish();
        }
        forceFile(stagedArchive);
        quotaGuard.validateWrite(target, Files.size(stagedArchive));
        commit(stage, List.of(new OutputPlan(target, stagedArchive)), List.of(), List.of(), deadlineNanos);
        return textResult(pathGuard.display(target) + "\n");
    }

    private ArchiveExecutionResult unzip(List<String> arguments,
                                         Stage stage,
                                         long deadlineNanos) throws IOException {
        ParsedExtract parsed = parseUnzip(arguments);
        Path archive = pathGuard.resolveExistingFile(parsed.archive());
        Snapshot snapshot = snapshotFile(archive, stage, deadlineNanos);
        enforceZipCentralDirectoryCount(snapshot.path());
        Path destination = resolveDestination(parsed.destination());
        List<EntryPlan> plans = scanZip(snapshot.path(), destination, deadlineNanos);
        return extractZip(snapshot.path(), plans, stage, deadlineNanos);
    }

    private ArchiveExecutionResult tar(List<String> arguments,
                                       Stage stage,
                                       long deadlineNanos) throws IOException {
        ParsedTar parsed = parseTar(arguments);
        if (parsed.mode().create()) {
            Path target = newOutputTarget(parsed.archive());
            Path base = parsed.destination() == null
                    ? pathGuard.root() : pathGuard.resolveExistingDirectory(parsed.destination());
            List<ArchiveSource> sources = snapshotSources(
                    parsed.inputs(), base, true, stage, deadlineNanos);
            Path stagedArchive = stage.newFile("tar-output-");
            writeTar(stagedArchive, sources, parsed.mode().gzip(), deadlineNanos);
            quotaGuard.validateWrite(target, Files.size(stagedArchive));
            commit(stage, List.of(new OutputPlan(target, stagedArchive)), List.of(), List.of(), deadlineNanos);
            return textResult(pathGuard.display(target) + "\n");
        }
        Path archive = pathGuard.resolveExistingFile(parsed.archive());
        Snapshot snapshot = snapshotFile(archive, stage, deadlineNanos);
        Path destination = parsed.destination() == null
                ? pathGuard.root() : resolveDestination(parsed.destination());
        List<EntryPlan> plans = scanTar(
                snapshot.path(), destination, parsed.mode().gzip(), parsed.mode().list(), deadlineNanos);
        if (parsed.mode().list()) {
            OutputCollector output = new OutputCollector(maxOutputBytes);
            for (EntryPlan plan : plans) {
                output.append(plan.name()).append(plan.directory() ? "/\n" : "\n");
            }
            return output.result();
        }
        return extractTar(snapshot.path(), plans, parsed.mode().gzip(), stage, deadlineNanos);
    }

    private List<ArchiveSource> snapshotSources(List<String> inputs,
                                                Path base,
                                                boolean recursive,
                                                Stage stage,
                                                long deadlineNanos) throws IOException {
        if (inputs.isEmpty()) {
            throw denied("Archive creation requires at least one input.");
        }
        EntryIndex index = new EntryIndex();
        List<ArchiveSource> sources = new ArrayList<>();
        long total = 0;
        for (String inputValue : inputs) {
            checkDeadline(deadlineNanos);
            Path input = resolveInput(base, inputValue);
            String rootName = normalizeEntryName(inputValue);
            if (Files.isDirectory(input, LinkOption.NOFOLLOW_LINKS)) {
                if (!recursive) {
                    throw denied("Directory inputs require the recursive option.");
                }
                try (Stream<Path> paths = Files.walk(input)) {
                    for (Path source : (Iterable<Path>) paths::iterator) {
                        checkDeadline(deadlineNanos);
                        String relative = input.equals(source) ? ""
                                : input.relativize(source).toString().replace(source.getFileSystem().getSeparator(), "/");
                        String name = relative.isEmpty() ? rootName : rootName + "/" + relative;
                        if (Files.isSymbolicLink(source)) {
                            throw entryDenied("Archive input contains a symbolic link.");
                        }
                        if (Files.isDirectory(source, LinkOption.NOFOLLOW_LINKS)) {
                            index.add(name, true);
                            sources.add(new ArchiveSource(name, null, true, 0));
                        } else if (Files.isRegularFile(source, LinkOption.NOFOLLOW_LINKS)) {
                            Path guarded = pathGuard.resolveExistingFile(pathGuard.display(source));
                            Snapshot snapshot = snapshotFile(guarded, stage, deadlineNanos);
                            total = addExpanded(total, snapshot.size());
                            index.add(name, false);
                            sources.add(new ArchiveSource(name, snapshot.path(), false, snapshot.size()));
                        } else {
                            throw entryDenied("Archive input contains a non-regular entry.");
                        }
                        ensureEntryCount(sources.size());
                    }
                }
            } else {
                Path guarded = pathGuard.resolveExistingFile(pathGuard.display(input));
                Snapshot snapshot = snapshotFile(guarded, stage, deadlineNanos);
                total = addExpanded(total, snapshot.size());
                index.add(rootName, false);
                sources.add(new ArchiveSource(rootName, snapshot.path(), false, snapshot.size()));
                ensureEntryCount(sources.size());
            }
        }
        return sources;
    }

    private Path resolveInput(Path base, String inputValue) {
        String normalized = normalizeEntryName(inputValue);
        String baseName = pathGuard.display(base);
        String combined = ".".equals(baseName) ? normalized : baseName + "/" + normalized;
        return pathGuard.resolveExistingEntry(combined);
    }

    private Snapshot snapshotFile(Path source, Stage stage, long deadlineNanos) throws IOException {
        pathGuard.revalidate(source);
        BasicFileAttributes before = Files.readAttributes(
                source, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
        if (!before.isRegularFile()) {
            throw entryDenied("Archive input is not a regular file.");
        }
        if (before.size() > quotaGuard.maxArchiveSingleFileSize()) {
            throw limitExceeded("Archive input exceeds the single-file limit.");
        }
        Fingerprint fingerprint = Fingerprint.from(before);
        Path snapshot = stage.newFile("archive-input-");
        Set<OpenOption> inputOptions = Set.of(StandardOpenOption.READ, LinkOption.NOFOLLOW_LINKS);
        try (SeekableByteChannel input = Files.newByteChannel(source, inputOptions);
             InputStream inputStream = java.nio.channels.Channels.newInputStream(input);
             OutputStream output = limitedFileOutput(snapshot, quotaGuard.maxArchiveSingleFileSize())) {
            transfer(inputStream, output, before.size(), deadlineNanos, null);
        }
        forceFile(snapshot);
        pathGuard.revalidate(source);
        BasicFileAttributes after = Files.readAttributes(
                source, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
        if (!fingerprint.matches(after) || Files.size(snapshot) != before.size()) {
            throw new WorkspaceToolException("ARCHIVE_INPUT_CHANGED",
                    "Archive input changed while it was being copied.", true);
        }
        return new Snapshot(snapshot, before.size(), fingerprint);
    }

    private void writeTar(Path target,
                          List<ArchiveSource> sources,
                          boolean gzip,
                          long deadlineNanos) throws IOException {
        try (OutputStream limited = limitedFileOutput(target, quotaGuard.maxArchiveSingleFileSize());
             OutputStream compressed = gzip ? newGzipOutput(limited) : limited;
             TarArchiveOutputStream tar = new TarArchiveOutputStream(compressed, StandardCharsets.UTF_8.name())) {
            tar.setLongFileMode(TarArchiveOutputStream.LONGFILE_POSIX);
            tar.setBigNumberMode(TarArchiveOutputStream.BIGNUMBER_ERROR);
            for (ArchiveSource source : sources) {
                checkDeadline(deadlineNanos);
                TarArchiveEntry entry = new TarArchiveEntry(
                        source.directory() ? source.name() + "/" : source.name());
                entry.setMode(source.directory() ? TarArchiveEntry.DEFAULT_DIR_MODE : TarArchiveEntry.DEFAULT_FILE_MODE);
                entry.setModTime(0L);
                entry.setSize(source.directory() ? 0 : source.size());
                tar.putArchiveEntry(entry);
                if (!source.directory()) {
                    try (InputStream input = Files.newInputStream(source.snapshot(), StandardOpenOption.READ)) {
                        transfer(input, tar, source.size(), deadlineNanos, null);
                    }
                }
                tar.closeArchiveEntry();
            }
            tar.finish();
        }
        forceFile(target);
    }

    private List<EntryPlan> scanZip(Path archive,
                                    Path destination,
                                    long deadlineNanos) {
        List<EntryPlan> plans = new ArrayList<>();
        EntryIndex index = new EntryIndex();
        long total = 0;
        try (ZipFile zip = new ZipFile(archive)) {
            Enumeration<ZipArchiveEntry> entries = zip.getEntriesInPhysicalOrder();
            while (entries.hasMoreElements()) {
                checkDeadline(deadlineNanos);
                ZipArchiveEntry entry = entries.nextElement();
                String name = strictZipEntryName(entry);
                boolean directory = entry.isDirectory();
                validateZipType(zip, entry);
                index.add(name, directory);
                ensureEntryCount(totalEntryCounter(plans));
                long size = directory ? 0 : entry.getSize();
                long compressed = directory ? 0 : entry.getCompressedSize();
                if (size < 0 || compressed < 0 || (!directory && entry.getCrc() < 0)) {
                    throw invalidArchive("ZIP entry metadata is incomplete.", null);
                }
                if (!directory) {
                    ensureSingleSize(size);
                    total = addExpanded(total, size);
                }
                plans.add(entryPlan(destination, name, directory, size, entry.getCrc()));
            }
        } catch (WorkspaceToolException error) {
            throw error;
        } catch (IOException error) {
            throw invalidArchive("ZIP archive is invalid.", error);
        }
        return plans;
    }

    private ArchiveExecutionResult extractZip(Path archive,
                                              List<EntryPlan> plans,
                                              Stage stage,
                                              long deadlineNanos) throws IOException {
        List<OutputPlan> outputs = new ArrayList<>();
        List<Path> directories = new ArrayList<>();
        Map<Path, Long> quotaPlan = quotaExtractionPlan(plans);
        OutputCollector result = new OutputCollector(maxOutputBytes);
        try (ZipFile zip = new ZipFile(archive)) {
            for (EntryPlan plan : plans) {
                checkDeadline(deadlineNanos);
                if (plan.directory()) {
                    directories.add(plan.target());
                    result.append(plan.name()).append("/\n");
                    continue;
                }
                ZipArchiveEntry entry = zip.getEntry(plan.name());
                if (entry == null) {
                    throw invalidArchive("ZIP entry disappeared between validation and extraction.", null);
                }
                Path staged = stage.newFile("zip-entry-");
                CRC32 crc = new CRC32();
                try (InputStream input = zip.getInputStream(entry);
                     OutputStream output = limitedFileOutput(staged, quotaGuard.maxArchiveSingleFileSize())) {
                    long written = transfer(input, output, plan.size(), deadlineNanos, crc);
                    if (written != plan.size() || crc.getValue() != plan.crc()) {
                        throw invalidArchive("ZIP entry size or CRC does not match metadata.", null);
                    }
                }
                forceFile(staged);
                outputs.add(new OutputPlan(plan.target(), staged));
                result.append(plan.name()).append('\n');
            }
        } catch (WorkspaceToolException error) {
            throw error;
        } catch (IOException error) {
            throw invalidArchive("ZIP archive data is invalid.", error);
        }
        quotaGuard.validateBatch(quotaPlan);
        commit(stage, outputs, directories, List.of(), deadlineNanos);
        return result.result();
    }

    private List<EntryPlan> scanTar(Path archive,
                                    Path destination,
                                    boolean gzip,
                                    boolean listOnly,
                                    long deadlineNanos) {
        List<EntryPlan> plans = new ArrayList<>();
        EntryIndex index = new EntryIndex();
        long total = 0;
        try (TarArchiveInputStream tar = openTar(archive, gzip)) {
            TarArchiveEntry entry;
            while ((entry = tar.getNextTarEntry()) != null) {
                checkDeadline(deadlineNanos);
                validateTarType(entry);
                String name = normalizeEntryName(entry.getName());
                boolean directory = entry.isDirectory();
                index.add(name, directory);
                ensureEntryCount(plans.size() + 1);
                long size = directory ? 0 : entry.getSize();
                if (!directory) {
                    ensureSingleSize(size);
                    total = addExpanded(total, size);
                }
                plans.add(listOnly
                        ? new EntryPlan(name, null, directory, size, -1)
                        : entryPlan(destination, name, directory, size, -1));
            }
        } catch (WorkspaceToolException error) {
            throw error;
        } catch (IOException error) {
            throw invalidArchive("TAR archive is invalid.", error);
        }
        return plans;
    }

    private ArchiveExecutionResult extractTar(Path archive,
                                              List<EntryPlan> plans,
                                              boolean gzip,
                                              Stage stage,
                                              long deadlineNanos) throws IOException {
        List<OutputPlan> outputs = new ArrayList<>();
        List<Path> directories = new ArrayList<>();
        Map<Path, Long> quotaPlan = quotaExtractionPlan(plans);
        OutputCollector result = new OutputCollector(maxOutputBytes);
        int index = 0;
        try (TarArchiveInputStream tar = openTar(archive, gzip)) {
            TarArchiveEntry entry;
            while ((entry = tar.getNextTarEntry()) != null) {
                checkDeadline(deadlineNanos);
                EntryPlan plan = plans.get(index++);
                if (plan.directory()) {
                    directories.add(plan.target());
                    result.append(plan.name()).append("/\n");
                    continue;
                }
                Path staged = stage.newFile("tar-entry-");
                try (OutputStream output = limitedFileOutput(staged, quotaGuard.maxArchiveSingleFileSize())) {
                    long written = transfer(tar, output, plan.size(), deadlineNanos, null);
                    if (written != plan.size()) {
                        throw invalidArchive("TAR entry size does not match its header.", null);
                    }
                }
                forceFile(staged);
                outputs.add(new OutputPlan(plan.target(), staged));
                result.append(plan.name()).append('\n');
            }
        } catch (WorkspaceToolException error) {
            throw error;
        } catch (IOException | IndexOutOfBoundsException error) {
            throw invalidArchive("TAR archive data is invalid.", error);
        }
        quotaGuard.validateBatch(quotaPlan);
        commit(stage, outputs, directories, List.of(), deadlineNanos);
        return result.result();
    }

    private Map<Path, Long> quotaExtractionPlan(List<EntryPlan> plans) {
        Map<Path, Long> quotaPlan = new LinkedHashMap<>();
        for (EntryPlan plan : plans) {
            if (plan.directory()) {
                if (!Files.exists(plan.target(), LinkOption.NOFOLLOW_LINKS)) {
                    quotaPlan.put(plan.target(), 0L);
                }
            } else {
                quotaPlan.put(plan.target(), plan.size());
            }
        }
        return quotaPlan;
    }

    private EntryPlan entryPlan(Path destination,
                                String name,
                                boolean directory,
                                long size,
                                long crc) {
        String destinationName = pathGuard.display(destination);
        String combined = ".".equals(destinationName) ? name : destinationName + "/" + name;
        Path target = directory ? pathGuard.resolveCommandPath(combined) : pathGuard.resolveForWrite(combined);
        if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
            if (directory && Files.isDirectory(target, LinkOption.NOFOLLOW_LINKS)) {
                return new EntryPlan(name, target, true, 0, crc);
            }
            throw new WorkspaceToolException("ARCHIVE_TARGET_CONFLICT",
                    "Archive output target already exists.", false);
        }
        return new EntryPlan(name, target, directory, size, crc);
    }

    private void commit(Stage stage,
                        List<OutputPlan> outputs,
                        List<Path> directories,
                        List<DeletionPlan> deletions,
                        long deadlineNanos) {
        List<Path> createdDirectories = new ArrayList<>();
        List<Path> committedOutputs = new ArrayList<>();
        Map<Path, Path> deletionBackups = new LinkedHashMap<>();
        try {
            checkDeadline(deadlineNanos);
            for (OutputPlan output : outputs) {
                pathGuard.revalidate(output.target());
                if (Files.exists(output.target(), LinkOption.NOFOLLOW_LINKS)) {
                    throw new WorkspaceToolException("ARCHIVE_TARGET_CONFLICT",
                            "Archive output target already exists.", false);
                }
            }
            for (DeletionPlan deletion : deletions) {
                verifyFingerprint(deletion.target(), deletion.fingerprint());
            }
            for (Path directory : directories.stream().sorted(Comparator.comparingInt(Path::getNameCount)).toList()) {
                ensureDirectory(directory, createdDirectories);
            }
            for (OutputPlan output : outputs) {
                ensureDirectory(output.target().getParent(), createdDirectories);
            }
            for (DeletionPlan deletion : deletions) {
                Path backup = stage.newFile("archive-backup-");
                Files.deleteIfExists(backup);
                atomicMove(deletion.target(), backup);
                deletionBackups.put(deletion.target(), backup);
            }
            for (OutputPlan output : outputs) {
                checkDeadline(deadlineNanos);
                atomicMove(output.staged(), output.target());
                committedOutputs.add(output.target());
                commitObserver.afterOutputCommitted(committedOutputs.size());
                forceDirectory(output.target().getParent());
            }
        } catch (Exception error) {
            Exception rollbackError = rollback(committedOutputs, deletionBackups, createdDirectories);
            if (rollbackError != null) {
                error.addSuppressed(rollbackError);
                throw new WorkspaceToolException("ARCHIVE_ROLLBACK_FAILED",
                        "Archive commit and rollback failed; workspace requires inspection.", false, error);
            }
            if (error instanceof WorkspaceToolException typed) {
                throw typed;
            }
            throw new WorkspaceToolException("ARCHIVE_COMMIT_FAILED",
                    "Archive commit failed and changes were rolled back.", true, error);
        }
    }

    private Exception rollback(List<Path> committedOutputs,
                               Map<Path, Path> deletionBackups,
                               List<Path> createdDirectories) {
        Exception failure = null;
        for (int index = committedOutputs.size() - 1; index >= 0; index--) {
            try {
                Files.deleteIfExists(committedOutputs.get(index));
            } catch (IOException error) {
                failure = appendFailure(failure, error);
            }
        }
        List<Map.Entry<Path, Path>> backups = new ArrayList<>(deletionBackups.entrySet());
        for (int index = backups.size() - 1; index >= 0; index--) {
            try {
                atomicMove(backups.get(index).getValue(), backups.get(index).getKey());
            } catch (IOException error) {
                failure = appendFailure(failure, error);
            }
        }
        for (int index = createdDirectories.size() - 1; index >= 0; index--) {
            try {
                Files.deleteIfExists(createdDirectories.get(index));
            } catch (IOException error) {
                failure = appendFailure(failure, error);
            }
        }
        return failure;
    }

    private Exception appendFailure(Exception current, Exception next) {
        if (current == null) {
            return next;
        }
        current.addSuppressed(next);
        return current;
    }

    private void ensureDirectory(Path directory, List<Path> created) throws IOException {
        if (directory == null || directory.equals(pathGuard.root())) {
            return;
        }
        Path current = pathGuard.root();
        for (Path segment : pathGuard.root().relativize(directory)) {
            current = current.resolve(segment);
            if (Files.exists(current, LinkOption.NOFOLLOW_LINKS)) {
                if (!Files.isDirectory(current, LinkOption.NOFOLLOW_LINKS) || Files.isSymbolicLink(current)) {
                    throw new WorkspaceToolException("ARCHIVE_TARGET_CONFLICT",
                            "Archive output parent conflicts with an existing entry.", false);
                }
            } else {
                Files.createDirectory(current);
                created.add(current);
            }
        }
    }

    private void verifyFingerprint(Path target, Fingerprint expected) throws IOException {
        pathGuard.revalidate(target);
        BasicFileAttributes actual = Files.readAttributes(
                target, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS);
        if (!expected.matches(actual)) {
            throw new WorkspaceToolException("ARCHIVE_INPUT_CHANGED",
                    "Archive input changed before commit.", true);
        }
    }

    private TarArchiveInputStream openTar(Path archive, boolean gzip) throws IOException {
        InputStream raw = Files.newInputStream(archive, StandardOpenOption.READ);
        try {
            InputStream input = gzip ? new GzipCompressorInputStream(raw) : raw;
            return new TarArchiveInputStream(input, StandardCharsets.UTF_8.name());
        } catch (IOException error) {
            raw.close();
            throw error;
        }
    }

    private OutputStream newGzipOutput(OutputStream output) throws IOException {
        GzipParameters parameters = new GzipParameters();
        parameters.setModificationInstant(Instant.EPOCH);
        return new GzipCompressorOutputStream(output, parameters);
    }

    private void validateZipType(ZipFile zip, ZipArchiveEntry entry) {
        if (!zip.canReadEntryData(entry)) {
            throw entryDenied("Encrypted or unsupported ZIP entries are not allowed.");
        }
        int mode = entry.getUnixMode();
        int type = mode & UnixStat.FILE_TYPE_FLAG;
        if (entry.isUnixSymlink() || (type != 0 && type != UnixStat.FILE_FLAG && type != UnixStat.DIR_FLAG)) {
            throw entryDenied("ZIP links, devices, and other special entries are not allowed.");
        }
    }

    private void validateTarType(TarArchiveEntry entry) {
        if (!entry.isCheckSumOK()) {
            throw invalidArchive("TAR entry checksum is invalid.", null);
        }
        if (entry.isSymbolicLink() || entry.isLink() || entry.isBlockDevice() || entry.isCharacterDevice()
                || entry.isFIFO() || entry.isSparse() || (!entry.isFile() && !entry.isDirectory())) {
            throw entryDenied("TAR links, devices, FIFO, sparse, and special entries are not allowed.");
        }
    }

    private String strictZipEntryName(ZipArchiveEntry entry) {
        byte[] raw = entry.getRawName();
        if (raw == null) {
            throw invalidArchive("ZIP entry name bytes are missing.", null);
        }
        try {
            String decoded = StandardCharsets.UTF_8.newDecoder()
                    .onMalformedInput(CodingErrorAction.REPORT)
                    .onUnmappableCharacter(CodingErrorAction.REPORT)
                    .decode(ByteBuffer.wrap(raw)).toString();
            if (!decoded.equals(entry.getName())) {
                throw invalidArchive("ZIP entry name is not unambiguous UTF-8.", null);
            }
            return normalizeEntryName(decoded);
        } catch (CharacterCodingException error) {
            throw invalidArchive("ZIP entry name is not valid UTF-8.", error);
        }
    }

    private String normalizeEntryName(String rawName) {
        if (rawName == null || rawName.isBlank() || rawName.indexOf('\0') >= 0) {
            throw entryDenied("Archive entry path is empty or contains NUL.");
        }
        if (rawName.indexOf('\\') >= 0) {
            throw entryDenied("Archive entry path must use forward slashes.");
        }
        String value = rawName;
        while (value.startsWith("./")) {
            value = value.substring(2);
        }
        while (value.endsWith("/")) {
            value = value.substring(0, value.length() - 1);
        }
        if (value.isBlank() || value.startsWith("/") || value.startsWith("~")
                || WINDOWS_ABSOLUTE.matcher(value).matches()) {
            throw entryDenied("Archive entry path must be workspace-relative.");
        }
        String[] segments = value.split("/", -1);
        if (segments.length > MAX_PATH_DEPTH || value.length() > MAX_PATH_LENGTH) {
            throw entryDenied("Archive entry path exceeds the safety limit.");
        }
        for (String segment : segments) {
            if (segment.isEmpty() || ".".equals(segment) || "..".equals(segment)) {
                throw entryDenied("Archive entry path contains an unsafe segment.");
            }
        }
        return String.join("/", segments);
    }

    private void enforceZipCentralDirectoryCount(Path archive) throws IOException {
        long size = Files.size(archive);
        int tailSize = (int) Math.min(size, 65_557L);
        byte[] tail = new byte[tailSize];
        try (FileChannel channel = FileChannel.open(archive, StandardOpenOption.READ)) {
            channel.position(size - tailSize);
            ByteBuffer buffer = ByteBuffer.wrap(tail);
            while (buffer.hasRemaining() && channel.read(buffer) >= 0) {
                // 读取 ZIP 末尾固定有界窗口。
            }
        }
        for (int index = tail.length - 22; index >= 0; index--) {
            if (littleEndianInt(tail, index) != 0x06054b50) {
                continue;
            }
            int commentLength = littleEndianShort(tail, index + 20);
            if (index + 22 + commentLength != tail.length) {
                continue;
            }
            int entries = littleEndianShort(tail, index + 10);
            if (entries == 0xffff || entries > quotaGuard.maxArchiveEntries()) {
                throw limitExceeded("ZIP contains too many entries.");
            }
            return;
        }
        throw invalidArchive("ZIP end-of-central-directory record is missing.", null);
    }

    private int littleEndianInt(byte[] value, int offset) {
        if (offset < 0 || offset + 4 > value.length) {
            return -1;
        }
        return (value[offset] & 0xff) | ((value[offset + 1] & 0xff) << 8)
                | ((value[offset + 2] & 0xff) << 16) | ((value[offset + 3] & 0xff) << 24);
    }

    private int littleEndianShort(byte[] value, int offset) {
        return (value[offset] & 0xff) | ((value[offset + 1] & 0xff) << 8);
    }

    private ParsedFileList parseFileList(List<String> arguments, Set<String> allowedOptions) {
        Set<String> options = new LinkedHashSet<>();
        List<String> files = new ArrayList<>();
        boolean endOfOptions = false;
        for (int index = 1; index < arguments.size(); index++) {
            String argument = arguments.get(index);
            if (!endOfOptions && "--".equals(argument)) {
                endOfOptions = true;
            } else if (!endOfOptions && argument.startsWith("-")) {
                if (!allowedOptions.contains(argument) || !options.add(argument)) {
                    throw denied("Archive command option is not allowed.");
                }
            } else {
                files.add(argument);
            }
        }
        return new ParsedFileList(options, files);
    }

    private ParsedZip parseZip(List<String> arguments) {
        boolean quiet = false;
        boolean recursive = false;
        List<String> operands = new ArrayList<>();
        boolean endOfOptions = false;
        for (int index = 1; index < arguments.size(); index++) {
            String argument = arguments.get(index);
            if (!endOfOptions && "--".equals(argument)) {
                endOfOptions = true;
            } else if (!endOfOptions && argument.startsWith("-")) {
                for (int flag = 1; flag < argument.length(); flag++) {
                    if (argument.charAt(flag) == 'q') {
                        quiet = true;
                    } else if (argument.charAt(flag) == 'r') {
                        recursive = true;
                    } else {
                        throw denied("zip option is not allowed.");
                    }
                }
            } else {
                operands.add(argument);
            }
        }
        if (operands.size() < 2) {
            throw denied("zip requires an archive path and at least one input.");
        }
        return new ParsedZip(quiet, recursive, operands.get(0), operands.subList(1, operands.size()));
    }

    private ParsedExtract parseUnzip(List<String> arguments) {
        boolean quiet = false;
        String archive = null;
        String destination = ".";
        for (int index = 1; index < arguments.size(); index++) {
            String argument = arguments.get(index);
            if ("-q".equals(argument) && archive == null && !quiet) {
                quiet = true;
            } else if (archive == null && !argument.startsWith("-")) {
                archive = argument;
            } else if ("-d".equals(argument) && index + 1 < arguments.size() && ".".equals(destination)) {
                destination = arguments.get(++index);
            } else {
                throw denied("unzip option or operand is not allowed.");
            }
        }
        if (archive == null) {
            throw denied("unzip requires one archive path.");
        }
        return new ParsedExtract(quiet, archive, destination);
    }

    private ParsedTar parseTar(List<String> arguments) {
        if (arguments.size() < 3) {
            throw denied("tar requires a fixed mode and archive path.");
        }
        TarMode mode = TarMode.parse(arguments.get(1));
        String archive = arguments.get(2);
        String destination = null;
        List<String> inputs = new ArrayList<>();
        for (int index = 3; index < arguments.size(); index++) {
            String argument = arguments.get(index);
            if ("-C".equals(argument) && destination == null && index + 1 < arguments.size()) {
                destination = arguments.get(++index);
            } else if (argument.startsWith("-")) {
                throw denied("tar option is not allowed.");
            } else {
                inputs.add(argument);
            }
        }
        if (mode.create() && inputs.isEmpty()) {
            throw denied("tar creation requires at least one input.");
        }
        if (!mode.create() && !inputs.isEmpty()) {
            throw denied("tar extraction and listing do not accept member filters.");
        }
        if (mode.list() && destination != null) {
            throw denied("tar listing does not accept -C.");
        }
        return new ParsedTar(mode, archive, destination, inputs);
    }

    private Path newOutputTarget(String value) {
        Path target = pathGuard.resolveForWrite(value);
        if (Files.exists(target, LinkOption.NOFOLLOW_LINKS)) {
            throw new WorkspaceToolException("ARCHIVE_TARGET_CONFLICT",
                    "Archive output target already exists.", false);
        }
        return target;
    }

    private Path resolveDestination(String value) {
        Path destination = pathGuard.resolveCommandPath(value == null ? "." : value);
        if (Files.exists(destination, LinkOption.NOFOLLOW_LINKS)
                && !Files.isDirectory(destination, LinkOption.NOFOLLOW_LINKS)) {
            throw new WorkspaceToolException("ARCHIVE_TARGET_CONFLICT",
                    "Archive destination is not a directory.", false);
        }
        return destination;
    }

    private String gunzipTarget(String value) {
        if (value == null || !value.toLowerCase(Locale.ROOT).endsWith(".gz") || value.length() <= 3) {
            throw denied("gunzip input must end with .gz.");
        }
        return value.substring(0, value.length() - 3);
    }

    private long transfer(InputStream input,
                          OutputStream output,
                          long limit,
                          long deadlineNanos,
                          CRC32 crc) throws IOException {
        byte[] buffer = new byte[COPY_BUFFER_SIZE];
        long total = 0;
        int read;
        while ((read = input.read(buffer)) >= 0) {
            checkDeadline(deadlineNanos);
            if (read == 0) {
                continue;
            }
            if (total > limit - read) {
                throw limitExceeded("Archive content exceeds the configured size limit.");
            }
            output.write(buffer, 0, read);
            if (crc != null) {
                crc.update(buffer, 0, read);
            }
            total += read;
        }
        return total;
    }

    private OutputStream limitedFileOutput(Path path, long limit) throws IOException {
        return new LimitedOutputStream(Files.newOutputStream(
                path, StandardOpenOption.WRITE, StandardOpenOption.TRUNCATE_EXISTING), limit);
    }

    private void ensureEntryCount(int count) {
        if (count > quotaGuard.maxArchiveEntries()) {
            throw limitExceeded("Archive contains too many entries.");
        }
    }

    private int totalEntryCounter(List<EntryPlan> plans) {
        return plans.size() + 1;
    }

    private void ensureSingleSize(long size) {
        if (size < 0 || size > quotaGuard.maxArchiveSingleFileSize()) {
            throw limitExceeded("Archive entry exceeds the single-file limit.");
        }
    }

    private long addExpanded(long current, long size) {
        ensureSingleSize(size);
        long total;
        try {
            total = Math.addExact(current, size);
        } catch (ArithmeticException error) {
            throw limitExceeded("Archive expanded size overflowed.");
        }
        if (total > quotaGuard.maxArchiveTotalSize()) {
            throw limitExceeded("Archive exceeds the expanded total-size limit.");
        }
        return total;
    }

    private void checkDeadline(long deadlineNanos) {
        if (Thread.currentThread().isInterrupted() || System.nanoTime() > deadlineNanos) {
            throw new WorkspaceToolException("SHELL_TIMEOUT",
                    "Archive command exceeded its timeout.", true);
        }
    }

    private void forceFile(Path path) throws IOException {
        try (FileChannel channel = FileChannel.open(path, StandardOpenOption.WRITE)) {
            channel.force(true);
        }
    }

    private void forceDirectory(Path directory) {
        if (directory == null) {
            return;
        }
        try (FileChannel channel = FileChannel.open(directory, StandardOpenOption.READ)) {
            channel.force(true);
        } catch (IOException | UnsupportedOperationException ignored) {
            // 某些文件系统不支持目录 fsync；文件已完成 force 和原子 rename。
        }
    }

    private void atomicMove(Path source, Path target) throws IOException {
        try {
            Files.move(source, target, StandardCopyOption.ATOMIC_MOVE);
        } catch (AtomicMoveNotSupportedException error) {
            throw new WorkspaceToolException("ARCHIVE_ATOMIC_MOVE_UNSUPPORTED",
                    "Workspace filesystem does not support atomic archive commits.", false, error);
        }
    }

    private ArchiveExecutionResult textResult(String value) {
        OutputCollector output = new OutputCollector(maxOutputBytes);
        output.append(value);
        return output.result();
    }

    private WorkspaceToolException denied(String message) {
        return new WorkspaceToolException("SHELL_COMMAND_DENIED", message, false);
    }

    private WorkspaceToolException entryDenied(String message) {
        return new WorkspaceToolException("ARCHIVE_ENTRY_DENIED", message, false);
    }

    private WorkspaceToolException limitExceeded(String message) {
        return new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED", message, false);
    }

    private WorkspaceToolException invalidArchive(String message, Throwable cause) {
        return cause == null
                ? new WorkspaceToolException("ARCHIVE_INVALID", message, false)
                : new WorkspaceToolException("ARCHIVE_INVALID", message, false, cause);
    }

    /**
     * 有界归档命令结果。
     *
     * @param output 文本输出
     * @param truncated 是否截断
     */
    record ArchiveExecutionResult(String output, boolean truncated) {
    }

    /**
     * 归档提交观察器。
     */
    @FunctionalInterface
    interface ArchiveCommitObserver {

        /**
         * 在一个输出完成原子移动后回调。
         *
         * @param committedCount 已提交输出数
         */
        void afterOutputCommitted(int committedCount);

        /**
         * 获取无操作观察器。
         *
         * @return 无操作观察器
         */
        static ArchiveCommitObserver noop() {
            return committedCount -> {
            };
        }
    }

    private record ParsedFileList(Set<String> options, List<String> files) {
    }

    private record ParsedZip(boolean quiet, boolean recursive, String archive, List<String> inputs) {
    }

    private record ParsedExtract(boolean quiet, String archive, String destination) {
    }

    private record ParsedTar(TarMode mode, String archive, String destination, List<String> inputs) {
    }

    private record ArchiveSource(String name, Path snapshot, boolean directory, long size) {
    }

    private record EntryPlan(String name, Path target, boolean directory, long size, long crc) {
    }

    private record OutputPlan(Path target, Path staged) {
    }

    private record DeletionPlan(Path target, Fingerprint fingerprint) {
    }

    private record Snapshot(Path path, long size, Fingerprint fingerprint) {
    }

    private record Fingerprint(Object fileKey, long size, long modifiedMillis) {

        private static Fingerprint from(BasicFileAttributes attributes) {
            return new Fingerprint(attributes.fileKey(), attributes.size(),
                    attributes.lastModifiedTime().toMillis());
        }

        private boolean matches(BasicFileAttributes attributes) {
            return attributes.isRegularFile() && size == attributes.size()
                    && modifiedMillis == attributes.lastModifiedTime().toMillis()
                    && (fileKey == null || fileKey.equals(attributes.fileKey()));
        }
    }

    private enum TarMode {
        CREATE(false, true, false),
        CREATE_GZIP(true, true, false),
        EXTRACT(false, false, false),
        EXTRACT_GZIP(true, false, false),
        LIST(false, false, true),
        LIST_GZIP(true, false, true);

        private final boolean gzip;
        private final boolean create;
        private final boolean list;

        TarMode(boolean gzip, boolean create, boolean list) {
            this.gzip = gzip;
            this.create = create;
            this.list = list;
        }

        private static TarMode parse(String value) {
            return switch (value) {
                case "-cf" -> CREATE;
                case "-czf" -> CREATE_GZIP;
                case "-xf" -> EXTRACT;
                case "-xzf" -> EXTRACT_GZIP;
                case "-tf" -> LIST;
                case "-tzf" -> LIST_GZIP;
                default -> throw new WorkspaceToolException("SHELL_COMMAND_DENIED",
                        "tar mode is not allowed.", false);
            };
        }

        private boolean gzip() {
            return gzip;
        }

        private boolean create() {
            return create;
        }

        private boolean list() {
            return list;
        }
    }

    private static final class EntryIndex {

        private final Set<String> exact = new HashSet<>();
        private final Set<String> files = new HashSet<>();
        private final Map<String, String> firstDescendant = new HashMap<>();

        private void add(String rawName, boolean directory) {
            String name = rawName;
            if (!exact.add(name)) {
                throw new WorkspaceToolException("ARCHIVE_DUPLICATE_ENTRY",
                        "Archive contains a duplicate entry.", false);
            }
            String ancestor = name;
            int separator = ancestor.indexOf('/');
            while (separator >= 0) {
                String prefix = ancestor.substring(0, separator);
                if (files.contains(prefix)) {
                    throw new WorkspaceToolException("ARCHIVE_ENTRY_DENIED",
                            "Archive contains a file/directory hierarchy conflict.", false);
                }
                firstDescendant.putIfAbsent(prefix, name);
                separator = ancestor.indexOf('/', separator + 1);
            }
            if (!directory && firstDescendant.containsKey(name)) {
                throw new WorkspaceToolException("ARCHIVE_ENTRY_DENIED",
                        "Archive contains a file/directory hierarchy conflict.", false);
            }
            if (!directory) {
                files.add(name);
            }
        }
    }

    private static final class LimitedOutputStream extends FilterOutputStream {

        private final long limit;
        private long count;

        private LimitedOutputStream(OutputStream output, long limit) {
            super(output);
            this.limit = limit;
        }

        @Override
        public void write(int value) throws IOException {
            ensureCapacity(1);
            out.write(value);
            count++;
        }

        @Override
        public void write(byte[] value, int offset, int length) throws IOException {
            ensureCapacity(length);
            out.write(value, offset, length);
            count += length;
        }

        private void ensureCapacity(int additional) {
            if (additional < 0 || count > limit - additional) {
                throw new WorkspaceToolException("WORKSPACE_QUOTA_EXCEEDED",
                        "Archive output exceeds the single-file limit.", false);
            }
        }
    }

    private static final class OutputCollector {

        private final int limit;
        private final StringBuilder value = new StringBuilder();
        private int bytes;
        private boolean truncated;

        private OutputCollector(int limit) {
            this.limit = limit;
        }

        private OutputCollector append(char character) {
            return append(String.valueOf(character));
        }

        private OutputCollector append(String text) {
            if (truncated || text == null || text.isEmpty()) {
                return this;
            }
            for (int index = 0; index < text.length(); ) {
                int codePoint = text.codePointAt(index);
                String current = new String(Character.toChars(codePoint));
                int encoded = current.getBytes(StandardCharsets.UTF_8).length;
                if (bytes > limit - encoded) {
                    truncated = true;
                    break;
                }
                value.append(current);
                bytes += encoded;
                index += Character.charCount(codePoint);
            }
            return this;
        }

        private ArchiveExecutionResult result() {
            return new ArchiveExecutionResult(value.toString(), truncated);
        }
    }

    private static final class Stage implements AutoCloseable {

        private final Path root;

        private Stage(Path root) {
            this.root = root;
        }

        private static Stage create(Path workspaceRoot) throws IOException {
            Path parent = workspaceRoot.getParent();
            if (parent == null) {
                throw new WorkspaceToolException("WORKSPACE_CONFIG_INVALID",
                        "Workspace root must have a parent directory for archive staging.", false);
            }
            return new Stage(Files.createTempDirectory(parent, ".easyagents-archive-stage-"));
        }

        private Path newFile(String prefix) throws IOException {
            return Files.createTempFile(root, prefix + UUID.randomUUID(), ".tmp");
        }

        @Override
        public void close() {
            try (Stream<Path> paths = Files.walk(root)) {
                for (Path path : paths.sorted(Comparator.reverseOrder()).toList()) {
                    Files.deleteIfExists(path);
                }
            } catch (IOException error) {
                // 调用结果已确定；完整堆栈仅记录到服务端，不向模型泄露宿主路径。
                logger.error("Failed to clean safe archive staging directory", error);
            }
        }
    }
}
