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:
@@ -21,6 +21,10 @@ const logger = pino({
|
||||
transport: { target: 'pino-pretty', options: { colorize: true } }
|
||||
});
|
||||
|
||||
// Sentinel markers for robust output parsing (must match agent-runner)
|
||||
const OUTPUT_START_MARKER = '---NANOCLAW_OUTPUT_START---';
|
||||
const OUTPUT_END_MARKER = '---NANOCLAW_OUTPUT_END---';
|
||||
|
||||
function getHomeDir(): string {
|
||||
const home = process.env.HOME || os.homedir();
|
||||
if (!home) {
|
||||
@@ -321,9 +325,19 @@ export async function runContainerAgent(
|
||||
}
|
||||
|
||||
try {
|
||||
// Last non-empty line is the JSON output
|
||||
const lines = stdout.trim().split('\n');
|
||||
const jsonLine = lines[lines.length - 1];
|
||||
// Extract JSON between sentinel markers for robust parsing
|
||||
const startIdx = stdout.indexOf(OUTPUT_START_MARKER);
|
||||
const endIdx = stdout.indexOf(OUTPUT_END_MARKER);
|
||||
|
||||
let jsonLine: string;
|
||||
if (startIdx !== -1 && endIdx !== -1 && endIdx > startIdx) {
|
||||
jsonLine = stdout.slice(startIdx + OUTPUT_START_MARKER.length, endIdx).trim();
|
||||
} else {
|
||||
// Fallback: last non-empty line (backwards compatibility)
|
||||
const lines = stdout.trim().split('\n');
|
||||
jsonLine = lines[lines.length - 1];
|
||||
}
|
||||
|
||||
const output: ContainerOutput = JSON.parse(jsonLine);
|
||||
|
||||
logger.info({
|
||||
|
||||
37
src/index.ts
37
src/index.ts
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user