add template
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
function readStdin() {
|
||||
return fs.readFileSync(0, 'utf8');
|
||||
}
|
||||
|
||||
function isInside(parent, target) {
|
||||
const relative = path.relative(parent, target);
|
||||
return relative === '' || (!relative.startsWith('..') && !path.isAbsolute(relative));
|
||||
}
|
||||
|
||||
function stripQuotes(value) {
|
||||
return value.replace(/^["']|["']$/g, '');
|
||||
}
|
||||
|
||||
function collectPathStrings(value, key = '', output = []) {
|
||||
if (value == null) {
|
||||
return output;
|
||||
}
|
||||
|
||||
if (typeof value === 'string') {
|
||||
if (/path|file/i.test(key)) {
|
||||
output.push(value);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) {
|
||||
value.forEach((item) => collectPathStrings(item, key, output));
|
||||
return output;
|
||||
}
|
||||
|
||||
if (typeof value === 'object') {
|
||||
Object.entries(value).forEach(([childKey, childValue]) => {
|
||||
collectPathStrings(childValue, childKey, output);
|
||||
});
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
function resolveCandidate(candidate, rootDir) {
|
||||
const cleaned = stripQuotes(String(candidate).trim());
|
||||
if (!cleaned || /^(https?:|file:|data:)/i.test(cleaned)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const resolved = path.isAbsolute(cleaned)
|
||||
? path.resolve(cleaned)
|
||||
: path.resolve(rootDir, cleaned);
|
||||
|
||||
return resolved;
|
||||
}
|
||||
|
||||
function commandTouchesRaw(command, rawDir) {
|
||||
if (!command || typeof command !== 'string') {
|
||||
return false;
|
||||
}
|
||||
|
||||
const normalizedRaw = rawDir.toLowerCase().replace(/\//g, path.sep);
|
||||
const lower = command.toLowerCase();
|
||||
const mentionsRaw =
|
||||
lower.includes(normalizedRaw.toLowerCase()) ||
|
||||
/(^|[\s"'`])raw([\\/]|$)/i.test(command);
|
||||
|
||||
if (!mentionsRaw) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return /(remove-item|move-item|copy-item|set-content|add-content|out-file|new-item|\brm\b|\bdel\b|\berase\b|\bmv\b|\bmove\b|\bcp\b|\bcopy\b|>>|(?<!\d)>(?!\d)|sed\s+-i|perl\s+-pi|\btouch\b)/i.test(command);
|
||||
}
|
||||
|
||||
function deny(reason) {
|
||||
process.stdout.write(JSON.stringify({ decision: 'deny', reason }));
|
||||
}
|
||||
|
||||
try {
|
||||
const input = JSON.parse(readStdin() || '{}');
|
||||
const projectDir = process.env.GEMINI_PROJECT_DIR || input.cwd || process.cwd();
|
||||
const rawDir = path.resolve(projectDir, 'raw');
|
||||
const toolName = input.tool_name || '';
|
||||
const toolInput = input.tool_input || {};
|
||||
|
||||
if (toolName === 'run_shell_command') {
|
||||
const command = toolInput.command || toolInput.cmd || '';
|
||||
if (commandTouchesRaw(command, rawDir)) {
|
||||
deny('raw/는 불변 원본이다. raw/를 수정하지 말고, 필요하면 wiki/ 또는 output/에 결과를 작성하라.');
|
||||
process.exit(0);
|
||||
}
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
const candidates = collectPathStrings(toolInput);
|
||||
const targets = candidates
|
||||
.map((candidate) => resolveCandidate(candidate, projectDir))
|
||||
.filter(Boolean);
|
||||
|
||||
const rawTargets = targets.filter((target) => isInside(rawDir, target));
|
||||
if (rawTargets.length > 0) {
|
||||
deny('raw/는 불변 원본이다. 해당 경로를 수정하지 말고, 요약·해석·구조화는 wiki/에서 수행하라.');
|
||||
}
|
||||
} catch (error) {
|
||||
process.stderr.write(String(error));
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user