Apply fixes from closed PRs: sentinel markers, JID lookup, schedule validation

- PR #10: Add sentinel markers for robust JSON parsing between container
  and host. Fallback to last-line parsing for backwards compatibility.

- PR #5: Look up target JID from registeredGroups instead of trusting
  IPC payload, fixing cross-group scheduled tasks getting wrong chat_jid.

- PR #8: Add lightweight schedule validation in container MCP that
  returns errors to agents (cron syntax, positive interval, valid ISO
  timestamp). Also defensive validation on host side.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
gavrielc
2026-02-01 20:49:57 +02:00
parent ade9f2d323
commit 6745a1c54b
6 changed files with 468 additions and 13 deletions

View File

@@ -247,18 +247,21 @@ async function processTaskIpc(
switch (data.type) {
case 'schedule_task':
if (data.prompt && data.schedule_type && data.schedule_value && data.groupFolder && data.chatJid) {
if (data.prompt && data.schedule_type && data.schedule_value && data.groupFolder) {
// Authorization: non-main groups can only schedule for themselves
const targetGroup = data.groupFolder;
if (!isMain && targetGroup !== sourceGroup) {
logger.warn({ sourceGroup, targetGroup, chatJid: data.chatJid }, 'Unauthorized schedule_task attempt blocked');
logger.warn({ sourceGroup, targetGroup }, 'Unauthorized schedule_task attempt blocked');
break;
}
// Authorization: verify the chatJid belongs to the target group
const chatGroup = registeredGroups[data.chatJid];
if (!isMain && (!chatGroup || chatGroup.folder !== targetGroup)) {
logger.warn({ sourceGroup, targetGroup, chatJid: data.chatJid }, 'Unauthorized schedule_task chatJid blocked');
// Resolve the correct JID for the target group (don't trust IPC payload)
const targetJid = Object.entries(registeredGroups).find(
([, group]) => group.folder === targetGroup
)?.[0];
if (!targetJid) {
logger.warn({ targetGroup }, 'Cannot schedule task: target group not registered');
break;
}
@@ -266,20 +269,34 @@ async function processTaskIpc(
let nextRun: string | null = null;
if (scheduleType === 'cron') {
const interval = CronExpressionParser.parse(data.schedule_value);
nextRun = interval.next().toISOString();
try {
const interval = CronExpressionParser.parse(data.schedule_value);
nextRun = interval.next().toISOString();
} catch {
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid cron expression');
break;
}
} else if (scheduleType === 'interval') {
const ms = parseInt(data.schedule_value, 10);
if (isNaN(ms) || ms <= 0) {
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid interval');
break;
}
nextRun = new Date(Date.now() + ms).toISOString();
} else if (scheduleType === 'once') {
nextRun = data.schedule_value; // ISO timestamp
const scheduled = new Date(data.schedule_value);
if (isNaN(scheduled.getTime())) {
logger.warn({ scheduleValue: data.schedule_value }, 'Invalid timestamp');
break;
}
nextRun = scheduled.toISOString();
}
const taskId = `task-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
createTask({
id: taskId,
group_folder: targetGroup,
chat_jid: data.chatJid,
chat_jid: targetJid,
prompt: data.prompt,
schedule_type: scheduleType,
schedule_value: data.schedule_value,