/**
 * Tool surface exposed to the skill-proposal forked agent.
 *
 * Wraps the standard read/grep/write/edit tools with proposal-scoped policy
 * checks: reads/greps may inspect proposals, live skills, and the session
 * JSONL read roots; writes/edits are restricted to inside a proposal folder.
 * Session JSONL roots are further restricted to `.jsonl` files and globs to
 * keep the fork from wandering the wider session directory. Adds one custom
 * tool, `copy_into_proposal`, for seeding proposals from a live skill —
 * something the scoped write/edit tools cannot do faithfully for binary or
 * nested assets.
 */
import { Type } from '@sinclair/typebox';
import { lstatSync } from 'node:fs';
import { extname, relative, resolve } from 'node:path';
import { EDIT, GREP, READ, WRITE } from '../../tool-names.js';
import { createEditTool } from '../tools/edit.js';
import { createGrepTool } from '../tools/grep.js';
import { createReadTool } from '../tools/read.js';
import { createWriteTool } from '../tools/write.js';
import { copyFromSkillIntoProposal, findAllowedRoot, getProposalNameForPath, initializeSkillProposalRoots, isPathInside } from './proposal-fs.js';
export const COPY_INTO_PROPOSAL = 'copy_into_proposal';
// The normal write tool already creates proposal folders. The only custom tool
// left is the operation write/edit cannot perform faithfully: copying real files
// and directories, including binary assets, out of live skills.
const copyIntoProposalSchema = Type.Object({
    targetSkillName: Type.String({ description: 'Proposal folder to copy into' }),
    sourceSkillName: Type.String({ description: 'Live skill folder to copy from' }),
    sourceRelativePath: Type.String({
        description: 'File or directory path relative to the live source skill folder'
    }),
    destinationRelativePath: Type.Optional(Type.String({
        description: 'Destination path relative to the target proposal folder'
    }))
});
/** Model-facing relative paths resolve from the agent dir; absolute paths stay absolute. */
function toAbsoluteInputPath(roots, inputPath) {
    return resolve(roots.agentDir, inputPath);
}
/** Find which allowed root (proposals, live skills, or read root) contains the input path. */
function allowedReadRootForPath(roots, readRoots, inputPath) {
    const absolute = toAbsoluteInputPath(roots, inputPath);
    return findAllowedRoot([roots.proposalsRoot, roots.liveSkillsRoot, ...readRoots], absolute);
}
/** True if `root` matches any of the configured session read roots. */
function isSessionReadRoot(readRoots, root) {
    return readRoots.some((readRoot) => resolve(readRoot) === resolve(root));
}
/** True if the path has a `.jsonl` extension. */
function isJsonlPath(path) {
    return extname(path).toLowerCase() === '.jsonl';
}
/** True if the glob restricts matches to `.jsonl` files. */
function isJsonlGlob(glob) {
    if (typeof glob !== 'string')
        return false;
    const trimmed = glob.trim().toLowerCase();
    return !trimmed.startsWith('!') && (trimmed === '*.jsonl' || trimmed.endsWith('/*.jsonl'));
}
/** Throw if the path is outside all allowed read roots. */
function assertAllowedReadPath(roots, readRoots, inputPath) {
    const root = allowedReadRootForPath(roots, readRoots, inputPath);
    if (!root) {
        throw new Error(`Read path is outside allowed skill proposal roots: ${inputPath}`);
    }
}
/** Throw if a read-tool path is outside allowed roots or is a non-JSONL session file. */
function assertAllowedReadFilePath(roots, readRoots, inputPath) {
    const absolute = toAbsoluteInputPath(roots, inputPath);
    const root = allowedReadRootForPath(roots, readRoots, inputPath);
    assertAllowedReadPath(roots, readRoots, inputPath);
    if (root && isSessionReadRoot(readRoots, root) && !isJsonlPath(absolute)) {
        throw new Error(`Session read path must be a JSONL file: ${inputPath}`);
    }
}
/** Throw if a grep target violates path or JSONL-glob restrictions for session reads. */
function assertAllowedGrepPath(roots, readRoots, inputPath, glob) {
    const absolute = toAbsoluteInputPath(roots, inputPath);
    const root = allowedReadRootForPath(roots, readRoots, inputPath);
    assertAllowedReadPath(roots, readRoots, inputPath);
    if (!root || !isSessionReadRoot(readRoots, root))
        return;
    const stats = lstatSync(absolute);
    if (stats.isFile()) {
        if (!isJsonlPath(absolute)) {
            throw new Error(`Session grep file must be JSONL: ${inputPath}`);
        }
        return;
    }
    if (!isJsonlGlob(glob)) {
        throw new Error('Session grep over a directory must use a JSONL-only glob.');
    }
}
/** Throw if a write/edit path is not inside a valid proposal folder. */
function assertProposalWritePath(roots, inputPath) {
    const absolute = toAbsoluteInputPath(roots, inputPath);
    if (!isPathInside(roots.proposalsRoot, absolute)) {
        throw new Error(`Write path is outside proposal root: ${inputPath}`);
    }
    const proposalRelativePath = relative(roots.proposalsRoot, absolute);
    if (!proposalRelativePath || !/[\\/]/.test(proposalRelativePath)) {
        throw new Error(`Write path must be inside a proposal folder: ${inputPath}`);
    }
    // Validates the proposal name segment is well-formed.
    getProposalNameForPath(roots, absolute);
}
/** Attach proposal-specific policy checks to standard file tools. */
function wrapScopedTool(tool, validate) {
    return {
        ...tool,
        execute: async (toolCallId, rawParams, signal, onUpdate, ctx) => {
            validate(rawParams);
            return tool.execute(toolCallId, rawParams, signal, onUpdate, ctx);
        }
    };
}
/** Build the proposal-scoped tool set (read/grep/write/edit + copy_into_proposal) for the forked agent. */
export function createSkillProposalTools(roots, options = {}) {
    initializeSkillProposalRoots(roots);
    const readRoots = options.readRoots ?? [];
    const copyIntoProposalTool = {
        name: COPY_INTO_PROPOSAL,
        label: COPY_INTO_PROPOSAL,
        isReadOnly: false,
        description: 'Copy a file or directory from a live skill folder into a proposal folder. Use this to seed proposals or copy assets, scripts, templates, and binary files.',
        parameters: copyIntoProposalSchema,
        execute: async (_toolCallId, params) => {
            const result = await copyFromSkillIntoProposal(roots, params);
            return {
                content: [{ type: 'text', text: `Copied into proposal: ${result.copiedTo}` }],
                details: result
            };
        }
    };
    const readTool = wrapScopedTool(createReadTool(roots.agentDir), (params) => {
        // Read/grep may inspect proposals and live skills so the fork can compare
        // current behavior and session logs before drafting a proposal. Writes stay proposal-only.
        assertAllowedReadFilePath(roots, readRoots, params.path);
    });
    const baseGrepTool = createGrepTool(roots.agentDir);
    const grepTool = {
        ...baseGrepTool,
        execute: async (toolCallId, rawParams, signal, onUpdate, ctx) => {
            const params = rawParams;
            const scopedParams = {
                ...params,
                path: params.path ?? 'skill-proposals'
            };
            assertAllowedGrepPath(roots, readRoots, scopedParams.path, scopedParams.glob);
            return baseGrepTool.execute(toolCallId, scopedParams, signal, onUpdate, ctx);
        }
    };
    const writeTool = wrapScopedTool(createWriteTool(roots.agentDir), (params) => {
        assertProposalWritePath(roots, params.path);
    });
    const editTool = wrapScopedTool(createEditTool(roots.agentDir), (params) => {
        assertProposalWritePath(roots, params.path);
    });
    return [
        copyIntoProposalTool,
        readTool,
        grepTool,
        writeTool,
        editTool
    ];
}
export const SKILL_PROPOSAL_TOOL_NAMES = [COPY_INTO_PROPOSAL, READ, GREP, WRITE, EDIT];
//# sourceMappingURL=agent-tools.js.map