"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.setProjectRoot = setProjectRoot;
exports.getProjectRoot = getProjectRoot;
exports.setAllowedExternalPaths = setAllowedExternalPaths;
exports.getAllowedExternalPaths = getAllowedExternalPaths;
exports.getUploadsDir = getUploadsDir;
exports.getAgentOutputDir = getAgentOutputDir;
exports.maybeRedirectAgentOutput = maybeRedirectAgentOutput;
exports.clearFileWriteRedirectCache = clearFileWriteRedirectCache;
exports.getFileWriteRedirects = getFileWriteRedirects;
exports.applyFileWriteRedirect = applyFileWriteRedirect;
exports.previewFileWriteRedirect = previewFileWriteRedirect;
exports.drainUploadsFallbacks = drainUploadsFallbacks;
exports.resolveProjectPath = resolveProjectPath;
exports.resolveProjectReadPath = resolveProjectReadPath;
const fs = __importStar(require("fs"));
const os = __importStar(require("os"));
const path = __importStar(require("path"));
const logger_1 = require("../core/logger");
// ─── Project root override ──────────────────────────────────────────
// When the server detects workspace isolation (HARNESS_PROJECT_DIR or
// harness-repo redirect), it calls setProjectRoot() so that all path
// resolution honours the workspace instead of the launch-time cwd.
let _projectRoot = null;
/** Override the project root used by all path resolution functions. */
function setProjectRoot(dir) {
    _projectRoot = path.resolve(dir);
}
/** Return the effective project root (explicit override or process.cwd()). */
function getProjectRoot() {
    return _projectRoot ?? process.cwd();
}
const DEFAULT_UPLOADS_DIRNAME = path.join('.harness', 'uploads');
// ─── Allowed external paths ─────────────────────────────────────────
// Directories outside the project root that file_read and list_files
// are permitted to access. Configured via Settings → Allowed External Paths.
let allowedExternalPaths = [];
function setAllowedExternalPaths(paths) {
    allowedExternalPaths = paths
        .map((p) => path.resolve(p.trim()))
        .filter((p) => {
        const segments = p.split(path.sep).filter(Boolean);
        // Require at least 2 path segments to prevent near-root paths
        // e.g. reject C:\, C:\x but allow C:\Users\..., /home/user/...
        return segments.length >= 2;
    });
}
function getAllowedExternalPaths() {
    return [...allowedExternalPaths];
}
/**
 * Resolve the uploads directory. Resolution order:
 *   1. `HARNESS_UPLOADS_DIR` (explicit override; absolute or project-relative)
 *   2. `HARNESS_GLOBAL_UPLOADS=1` → `~/.harness/uploads` (cycle 18)
 *      Useful when the daemon serves multiple workspaces and uploads
 *      should not get scattered into whichever cwd happened to start it.
 *   3. `<cwd>/.harness/uploads` (legacy default)
 *
 * The directory is not auto-created here; the upload route does that on demand.
 */
function getUploadsDir() {
    const override = process.env.HARNESS_UPLOADS_DIR?.trim();
    if (override) {
        return path.isAbsolute(override) ? override : path.resolve(getProjectRoot(), override);
    }
    if (process.env.HARNESS_GLOBAL_UPLOADS === '1') {
        return path.join(os.homedir(), '.harness', 'uploads');
    }
    return path.join(getProjectRoot(), DEFAULT_UPLOADS_DIRNAME);
}
const DEFAULT_AGENT_OUTPUT_DIRNAME = 'agent-outputs';
/**
 * Resolve the agent-outputs directory. Honors HARNESS_AGENT_OUTPUT_DIR
 * (absolute or project-relative) and falls back to <project>/agent-outputs.
 * Used by file_write to corral bare-filename writes (e.g. analysis.md,
 * run-all-analysis.js) into a single directory so the repo root does not
 * become a dumping ground for model-generated files.
 */
function getAgentOutputDir() {
    const override = process.env.HARNESS_AGENT_OUTPUT_DIR?.trim();
    if (override) {
        return path.isAbsolute(override) ? override : path.resolve(getProjectRoot(), override);
    }
    return path.join(getProjectRoot(), DEFAULT_AGENT_OUTPUT_DIRNAME);
}
/**
 * Decide whether a file_write target should be redirected to the
 * agent-outputs directory. Returns the redirected absolute path, or null
 * when the original path should be used as-is. Redirects only when:
 *   - the path is a bare filename (no directory component), AND
 *   - no file already exists at that bare name in the project root.
 *
 * This preserves intentional edits to existing files (package.json,
 * tsconfig.json) and intentional writes to subdirectories
 * (src/foo.ts, scripts/bar.js), while corralling new scratch files.
 */
function maybeRedirectAgentOutput(rawPath) {
    const trimmed = String(rawPath ?? '').trim();
    if (!trimmed || path.isAbsolute(trimmed))
        return null;
    const basename = path.basename(trimmed);
    if (!basename)
        return null;
    // Never redirect edits to existing project files.
    const directTarget = path.resolve(getProjectRoot(), trimmed);
    if (fs.existsSync(directTarget))
        return null;
    const explicitOverride = Boolean(process.env.HARNESS_AGENT_OUTPUT_DIR?.trim());
    const outputRoot = getAgentOutputDir();
    const dir = path.dirname(trimmed);
    // Two scenarios where the agent-outputs redirect should fire:
    //   1. Bare filename — classic scratch (notes.md, run.js).
    //   2. Path that already starts with agent-outputs/ — agent or user
    //      explicitly opted in by naming the directory.
    // Any other path with a directory component (src/foo.ts,
    // fine-tuning/python/trainer.py, scripts/x.js) is a deliberate write
    // to a specific subtree and MUST be honored as-is. Previous behavior
    // when HARNESS_AGENT_OUTPUT_DIR was set silently routed even those
    // intentional subdir writes to the override directory, which broke
    // multi-file projects (e.g. fine-tuning-studio landed in AgentFiles
    // instead of the user-named subdir of the repo).
    const isBareFilename = (dir === '.' || dir === '');
    const optedIntoAgentOutputs = (() => {
        const segments = trimmed.replace(/\\/g, '/').split('/').filter(Boolean);
        return segments[0]?.toLowerCase() === DEFAULT_AGENT_OUTPUT_DIRNAME;
    })();
    if (!isBareFilename && !optedIntoAgentOutputs)
        return null;
    if (explicitOverride) {
        // When the override is set we still honour the bare-filename / opt-in
        // semantics above, but we redirect to the configured directory rather
        // than the project-local default.
        const tail = optedIntoAgentOutputs
            ? trimmed.replace(/\\/g, '/').split('/').slice(1).join('/') || basename
            : basename;
        const redirected = path.resolve(outputRoot, tail);
        return isInsideOrEqualPath(redirected, outputRoot) ? redirected : null;
    }
    return path.join(outputRoot, basename);
}
function isInsideOrEqualPath(child, parent) {
    const rel = path.relative(parent, child);
    return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
}
const REDIRECTS_FILE = path.join('.harness', 'file-write-redirects.json');
let cachedRedirects = null;
let cachedRedirectsSource = 'none';
/**
 * Force a reload of the redirect rules on the next access. Call this from
 * the UI POST handler so changes take effect immediately without a restart.
 */
function clearFileWriteRedirectCache() {
    cachedRedirects = null;
    cachedRedirectsSource = 'none';
}
/**
 * Returns the active redirect rules and their source (env/file/none).
 * Cached so the per-write hot path avoids JSON.parse on every call.
 */
function getFileWriteRedirects() {
    if (cachedRedirects !== null)
        return { rules: cachedRedirects, source: cachedRedirectsSource };
    // Prefer env var (handy for one-off CI overrides).
    const fromEnv = process.env.HARNESS_FILE_WRITE_REDIRECTS?.trim();
    if (fromEnv) {
        const parsed = parseRedirectRules(fromEnv);
        if (parsed) {
            cachedRedirects = parsed;
            cachedRedirectsSource = 'env';
            return { rules: parsed, source: 'env' };
        }
    }
    // Fall back to the JSON file managed by the UI Settings panel.
    try {
        const raw = fs.readFileSync(path.resolve(getProjectRoot(), REDIRECTS_FILE), 'utf-8');
        const parsed = parseRedirectRules(raw);
        if (parsed) {
            cachedRedirects = parsed;
            cachedRedirectsSource = 'file';
            return { rules: parsed, source: 'file' };
        }
    }
    catch {
        // Missing file is the common case — user has not configured any rules.
    }
    cachedRedirects = [];
    cachedRedirectsSource = 'none';
    return { rules: [], source: 'none' };
}
function parseRedirectRules(raw) {
    try {
        const parsed = JSON.parse(raw);
        if (!Array.isArray(parsed))
            return null;
        const rules = [];
        for (const entry of parsed) {
            if (!entry || typeof entry !== 'object')
                continue;
            const match = typeof entry.match === 'string' ? entry.match.trim() : '';
            const redirect = typeof entry.redirect === 'string' ? entry.redirect.trim() : '';
            if (!match || !redirect)
                continue;
            rules.push({ match, redirect });
        }
        return rules;
    }
    catch {
        return null;
    }
}
/**
 * Convert a glob-style pattern (`*`, `**`) to a case-insensitive RegExp.
 * Handles backslash-vs-forward-slash by normalizing to forward slashes
 * before matching, so Windows paths and pattern authors agree.
 */
function compileMatchPattern(pattern) {
    const normalized = pattern.replace(/\\/g, '/');
    let regex = '';
    for (let i = 0; i < normalized.length; i++) {
        const ch = normalized[i];
        if (ch === '*') {
            if (normalized[i + 1] === '*') {
                regex += '.*';
                i++;
            }
            else {
                regex += '[^/]*';
            }
        }
        else if (/[.+^$|(){}\[\]\\]/.test(ch)) {
            regex += '\\' + ch;
        }
        else {
            regex += ch;
        }
    }
    return new RegExp('^' + regex + '$', 'i');
}
/**
 * If any redirect rule matches the supplied path, return the absolute
 * destination path (preserving the original basename). Otherwise null.
 *
 * Matching tries both the relative path and the bare basename so simple
 * patterns like `lottery-*` work without forcing the user to write
 * `**\/lottery-*` for every variant.
 */
function applyFileWriteRedirect(rawPath) {
    const { rules } = getFileWriteRedirects();
    if (rules.length === 0)
        return null;
    return matchRedirectRules(rawPath, rules);
}
/**
 * Pure (no env / no file IO) variant used by the UI preview endpoint.
 * Lets the user see which rule (if any) would match a typed path,
 * BEFORE saving the rules. Returns the matched rule + destination, or
 * null when no rule matches.
 */
function previewFileWriteRedirect(rawPath, rules) {
    if (!Array.isArray(rules) || rules.length === 0)
        return null;
    const normalizedPath = rawPath.replace(/\\/g, '/');
    const basename = path.basename(rawPath);
    for (const rule of rules) {
        if (!rule || !rule.match || !rule.redirect)
            continue;
        const re = compileMatchPattern(rule.match);
        if (re.test(normalizedPath) || re.test(basename)) {
            const targetDir = path.isAbsolute(rule.redirect)
                ? rule.redirect
                : path.resolve(getProjectRoot(), rule.redirect);
            // Safety: relative paths that resolve outside the workspace are traversal
            // attacks (e.g. redirect: '../../../system32'). Block them. Absolute paths
            // are explicit user config and are allowed unconditionally.
            if (!path.isAbsolute(rule.redirect)) {
                const safeTargetDir = isInsideOrEqualPath(targetDir, getProjectRoot()) ||
                    allowedExternalPaths.some((p) => isInsideOrEqualPath(targetDir, p));
                if (!safeTargetDir)
                    continue;
            }
            return { rule, destination: path.join(targetDir, basename) };
        }
    }
    return null;
}
/**
 * Internal helper used by both applyFileWriteRedirect (active rules) and
 * the preview endpoint. Returns the destination path or null.
 */
function matchRedirectRules(rawPath, rules) {
    const normalizedPath = rawPath.replace(/\\/g, '/');
    const basename = path.basename(rawPath);
    for (const rule of rules) {
        const re = compileMatchPattern(rule.match);
        if (re.test(normalizedPath) || re.test(basename)) {
            const targetDir = path.isAbsolute(rule.redirect)
                ? rule.redirect
                : path.resolve(getProjectRoot(), rule.redirect);
            // Safety: relative paths that resolve outside the workspace are traversal
            // attacks (e.g. redirect: '../../../system32'). Block them. Absolute paths
            // are explicit user config and are allowed unconditionally.
            if (!path.isAbsolute(rule.redirect)) {
                const safeTargetDir = isInsideOrEqualPath(targetDir, getProjectRoot()) ||
                    allowedExternalPaths.some((p) => isInsideOrEqualPath(targetDir, p));
                if (!safeTargetDir)
                    return null;
            }
            return path.join(targetDir, basename);
        }
    }
    return null;
}
const fallbackBuffer = [];
const MAX_FALLBACK_BUFFER = 50;
/**
 * Return and clear the buffered uploads-fallback records. The web layer drains
 * this after each tool result so it can surface a trace event when a model
 * passed a bare filename and the resolver had to rewrite it to .harness/uploads.
 */
function drainUploadsFallbacks() {
    if (fallbackBuffer.length === 0)
        return [];
    const snapshot = fallbackBuffer.splice(0, fallbackBuffer.length);
    return snapshot;
}
function recordFallback(requested, resolved) {
    fallbackBuffer.push({ requested, resolved, at: new Date().toISOString() });
    if (fallbackBuffer.length > MAX_FALLBACK_BUFFER) {
        fallbackBuffer.splice(0, fallbackBuffer.length - MAX_FALLBACK_BUFFER);
    }
}
/**
 * Resolve a tool path against the project root. Returns the absolute path
 * when it is inside the project directory, or null when it would escape.
 */
function resolveProjectPath(value) {
    const raw = String(value ?? '');
    const root = getProjectRoot();
    const resolved = path.resolve(root, raw);
    const relative = path.relative(root, resolved);
    if (!relative.startsWith('..') && !path.isAbsolute(relative))
        return resolved;
    // Check allowed external paths
    for (const allowed of allowedExternalPaths) {
        if (isInside(resolved, allowed) || resolved === allowed)
            return resolved;
    }
    return null;
}
/**
 * Like {@link resolveProjectPath} but, when the caller passes a bare filename
 * that is missing at the cwd-resolved location, falls back to the matching
 * file in the uploads directory if one exists. Lets read tools accept
 * attached files by name even when the model omits the upload directory.
 * Also accepts absolute or relative paths that resolve inside the configured
 * uploads directory even when it lives outside the project root (set via
 * HARNESS_UPLOADS_DIR).
 */
function resolveProjectReadPath(value) {
    const raw = String(value ?? '');
    const uploadsDir = getUploadsDir();
    // Accept paths that already resolve inside the uploads directory, even when
    // it sits outside the project root (HARNESS_UPLOADS_DIR override).
    const directResolved = path.resolve(raw);
    if (isInside(directResolved, uploadsDir) && fs.existsSync(directResolved)) {
        return directResolved;
    }
    const resolved = resolveProjectPath(value);
    if (!resolved)
        return null;
    if (fs.existsSync(resolved))
        return resolved;
    const dir = path.dirname(raw);
    if (dir !== '.' && dir !== '')
        return resolved;
    const base = path.basename(raw);
    if (!base)
        return resolved;
    const candidate = path.join(uploadsDir, base);
    if (fs.existsSync(candidate)) {
        logger_1.logger.warn('PathResolution', 'Bare filename rewritten to uploads directory', { requested: raw, resolved: candidate });
        recordFallback(raw, candidate);
        return candidate;
    }
    return resolved;
}
function isInside(child, parent) {
    const rel = path.relative(parent, child);
    return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
}
//# sourceMappingURL=pathResolution.js.map