fix(image-analysis): harden runtime and provisioning

This commit is contained in:
Tam Nhu Tran committed 2026-04-02 18:36:19 -04:00
1 parent 6d1c895721
commit 77b488f8b1
21 files changed
+811 -169

No files matched your search

+16 -5
View File
@@ -9,6 +9,7 @@ const DEFAULT_MODEL = 'gemini-2.5-flash';
const DEFAULT_TIMEOUT_SEC = 60;
const MAX_FILE_SIZE_MB = 10;
const MAX_FILE_SIZE_BYTES = MAX_FILE_SIZE_MB * 1024 * 1024;
const MAX_PROMPT_TEMPLATE_BYTES = 32 * 1024;
const SCREENSHOT_NAME_REGEX =
/(screen[-_ ]?shot|screen[-_ ]?capture|screencap|snapshot|snip|clip|capture)/i;
const TEMPLATE_FILE_NAMES = {
@@ -103,6 +104,10 @@ function selectPromptTemplate(filePath, requestedTemplate) {
function readPromptFile(filePath) {
try {
const stats = fs.statSync(filePath);
if (stats.size > MAX_PROMPT_TEMPLATE_BYTES) {
return null;
}
const content = fs.readFileSync(filePath, 'utf8').trim();
return content.length > 0 ? content : null;
} catch {
@@ -189,10 +194,6 @@ function getRuntimeBaseUrl() {
return normalizedBaseUrl;
}
if (normalizedPath.length > 0 && normalizedPath !== '/') {
return normalizedBaseUrl;
}
parsed.pathname = runtimePath;
return parsed.toString().replace(/\/+$/, '');
} catch {
@@ -217,6 +218,11 @@ function getApiKey() {
return process.env.CCS_CLIPROXY_API_KEY || process.env.ANTHROPIC_AUTH_TOKEN || 'ccs-internal-managed';
}
function shouldAllowSelfSigned() {
const value = `${process.env.CCS_IMAGE_ANALYSIS_RUNTIME_ALLOW_SELF_SIGNED || ''}`.trim().toLowerCase();
return value === '1' || value === 'true' || value === 'yes';
}
function getTimeoutMs(timeoutMs) {
if (typeof timeoutMs === 'number' && timeoutMs > 0) {
return timeoutMs;
@@ -301,6 +307,7 @@ function analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs) {
return new Promise((resolve, reject) => {
const endpoint = new URL(getRuntimeEndpoint());
const transport = endpoint.protocol === 'https:' ? https : http;
const apiKey = getApiKey();
const requestBody = JSON.stringify({
model,
max_tokens: 4096,
@@ -325,9 +332,13 @@ function analyzeViaCliProxy(base64Data, mediaType, model, prompt, timeoutMs) {
headers: {
'Content-Type': 'application/json',
'Content-Length': Buffer.byteLength(requestBody),
'x-api-key': getApiKey(),
'x-api-key': apiKey,
Authorization: `Bearer ${apiKey}`,
},
timeout: timeoutMs,
...(endpoint.protocol === 'https:' && shouldAllowSelfSigned()
? { rejectUnauthorized: false }
: {}),
},
(res) => {
let data = '';
+121 -18
View File
@@ -55,11 +55,11 @@ function getTools() {
inputSchema: {
type: 'object',
properties: {
filePath: {
type: 'string',
description:
'Absolute or workspace-relative path to a local image or PDF file to analyze.',
},
filePath: {
type: 'string',
description:
'Workspace-relative path, or an absolute path inside the current workspace, to a local image or PDF file to analyze.',
},
focus: {
type: 'string',
description:
@@ -128,6 +128,20 @@ function normalizeTemplate(value) {
return TEMPLATE_NAMES.includes(normalized) ? normalized : undefined;
}
function normalizePathForComparison(value) {
return process.platform === 'win32' ? value.toLowerCase() : value;
}
function isPathWithinWorkspace(workspaceRoot, candidatePath) {
const relativePath = path.relative(workspaceRoot, candidatePath);
return (
relativePath === '' ||
(!relativePath.startsWith('..') &&
!relativePath.startsWith(`..${path.sep}`) &&
!path.isAbsolute(relativePath))
);
}
function resolveFilePath(toolArgs) {
if (!toolArgs || typeof toolArgs !== 'object') {
return '';
@@ -143,6 +157,60 @@ function resolveFilePath(toolArgs) {
return '';
}
function resolveWorkspaceFilePath(toolArgs) {
const requestedPath = resolveFilePath(toolArgs);
if (!requestedPath) {
return { filePath: '', error: null };
}
const workspaceRoot = (() => {
try {
return fs.realpathSync(process.cwd());
} catch {
return path.resolve(process.cwd());
}
})();
const absolutePath = path.resolve(process.cwd(), requestedPath);
const comparisonPath = (() => {
if (fs.existsSync(absolutePath)) {
return fs.realpathSync(absolutePath);
}
const suffixSegments = [];
let currentPath = absolutePath;
while (!fs.existsSync(currentPath)) {
const parentPath = path.dirname(currentPath);
if (parentPath === currentPath) {
break;
}
suffixSegments.unshift(path.basename(currentPath));
currentPath = parentPath;
}
const resolvedExistingPath = fs.existsSync(currentPath)
? fs.realpathSync(currentPath)
: path.resolve(currentPath);
return path.join(resolvedExistingPath, ...suffixSegments);
})();
if (
!isPathWithinWorkspace(
normalizePathForComparison(workspaceRoot),
normalizePathForComparison(comparisonPath)
)
) {
return {
filePath: '',
error: 'ImageAnalysis only allows files inside the current workspace.',
};
}
return {
filePath: absolutePath,
error: null,
};
}
function resolveFocus(toolArgs) {
return typeof toolArgs.focus === 'string' && toolArgs.focus.trim().length > 0
? toolArgs.focus.trim()
@@ -213,9 +281,13 @@ async function handleToolCall(message) {
return;
}
const filePath = resolveFilePath(toolArgs);
const { filePath, error: filePathError } = resolveWorkspaceFilePath(toolArgs);
if (!filePath) {
writeError(id, -32602, `Tool "${TOOL_NAME}" requires a non-empty filePath.`);
writeError(
id,
-32602,
filePathError || `Tool "${TOOL_NAME}" requires a non-empty filePath.`
);
return;
}
@@ -296,22 +368,53 @@ let inputBuffer = Buffer.alloc(0);
function processIncomingBuffer() {
while (true) {
const newlineIndex = inputBuffer.indexOf(0x0a);
if (newlineIndex === -1) {
return;
}
let body;
const startsWithLegacyHeaders = inputBuffer
.slice(0, Math.min(inputBuffer.length, 32))
.toString('utf8')
.toLowerCase()
.startsWith('content-length:');
const lineBuffer = inputBuffer.subarray(0, newlineIndex);
inputBuffer = inputBuffer.subarray(newlineIndex + 1);
const line = lineBuffer.toString('utf8').replace(/\r$/, '').trim();
if (!line) {
continue;
if (startsWithLegacyHeaders) {
const headerEnd = inputBuffer.indexOf('\r\n\r\n');
if (headerEnd === -1) {
return;
}
const headerText = inputBuffer.slice(0, headerEnd).toString('utf8');
const contentLengthMatch = headerText.match(/content-length:\s*(\d+)/i);
if (!contentLengthMatch) {
inputBuffer = Buffer.alloc(0);
return;
}
const contentLength = Number.parseInt(contentLengthMatch[1], 10);
const messageEnd = headerEnd + 4 + contentLength;
if (inputBuffer.length < messageEnd) {
return;
}
body = inputBuffer.slice(headerEnd + 4, messageEnd).toString('utf8');
inputBuffer = inputBuffer.slice(messageEnd);
} else {
const newlineIndex = inputBuffer.indexOf('\n');
if (newlineIndex === -1) {
return;
}
body = inputBuffer.slice(0, newlineIndex).toString('utf8').replace(/\r$/, '').trim();
inputBuffer = inputBuffer.slice(newlineIndex + 1);
if (!body) {
continue;
}
}
try {
const message = JSON.parse(line);
const message = JSON.parse(body);
Promise.resolve(handleMessage(message)).catch((error) => {
if (process.env.CCS_DEBUG) {
if (message && message.id !== undefined) {
writeError(message.id, -32603, (error && error.message) || 'Internal error');
} else if (process.env.CCS_DEBUG) {
console.error(`[ccs-image-analysis] ${error instanceof Error ? error.stack : error}`);
}
});