fix(schedule): validate exact time-context marker shapes
This commit is contained in:
@@ -226,18 +226,26 @@ function isTimeContextReading(event: SessionEvent): boolean {
|
||||
|| source.plugin !== 'time-context'
|
||||
|| Object.keys(source).length !== 4
|
||||
|| source.form !== 'snapshot') return false
|
||||
const [block] = event.data.content
|
||||
const blockValue: unknown = event.data.content[0]
|
||||
const block = typeof blockValue === 'object' && blockValue !== null
|
||||
? blockValue as Record<string, unknown>
|
||||
: undefined
|
||||
const sections: unknown = source.sections
|
||||
const section: unknown = Array.isArray(sections) ? sections[0] : undefined
|
||||
const sectionValue: unknown = Array.isArray(sections) ? sections[0] : undefined
|
||||
const section = typeof sectionValue === 'object' && sectionValue !== null
|
||||
? sectionValue as Record<string, unknown>
|
||||
: undefined
|
||||
return event.data.content.length === 1
|
||||
&& block?.type === 'text'
|
||||
&& block !== undefined
|
||||
&& Object.keys(block).length === 2
|
||||
&& block.type === 'text'
|
||||
&& typeof block.text === 'string'
|
||||
&& Array.isArray(sections)
|
||||
&& sections.length === 1
|
||||
&& typeof section === 'object'
|
||||
&& section !== null
|
||||
&& 'name' in section
|
||||
&& section !== undefined
|
||||
&& Object.keys(section).length === 2
|
||||
&& section.name === 'time-context'
|
||||
&& 'text' in section
|
||||
&& typeof section.text === 'string'
|
||||
&& section.text === block.text
|
||||
}
|
||||
|
||||
|
||||
@@ -359,6 +359,65 @@ describe('Schedule tool protocol', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('does not let an array-like snapshot marker authorize an implicit local at', async () => {
|
||||
const test = await harness(true, 'Asia/Shanghai')
|
||||
test.agent.session.append('turn/start', { turn: 1 })
|
||||
test.agent.session.append('step/start', { turn: 1, step: 1 })
|
||||
test.agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'request' }],
|
||||
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
|
||||
}), { surfaceOp: 'append' })
|
||||
const text = 'time context'
|
||||
test.agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text }],
|
||||
source: {
|
||||
kind: 'plugin',
|
||||
plugin: 'time-context',
|
||||
form: 'snapshot',
|
||||
sections: { 0: { name: 'time-context', text }, length: 1 },
|
||||
} as never,
|
||||
}), { surfaceOp: 'append' })
|
||||
|
||||
expect(value(await execute(test, 'schedule_create', {
|
||||
prompt: 'malformed marker', at: { date: '2026-08-06', time: '09:00:00' },
|
||||
}))).toMatchObject({
|
||||
code: 'timezone_confirmation_required',
|
||||
sessionTimeZone: 'Asia/Shanghai',
|
||||
clientTimeZones: [],
|
||||
})
|
||||
})
|
||||
|
||||
it.each([
|
||||
['a non-object text block', 7, [{ name: 'time-context', text: 'time context' }]],
|
||||
['matched non-string text', { type: 'text', text: 7 }, [{ name: 'time-context', text: 7 }]],
|
||||
['extra text-block field', { type: 'text', text: 'time context', extra: true }, [{ name: 'time-context', text: 'time context' }]],
|
||||
['non-string section text', { type: 'text', text: 'time context' }, [{ name: 'time-context', text: 7 }]],
|
||||
['extra section field', { type: 'text', text: 'time context' }, [{ name: 'time-context', text: 'time context', extra: true }]],
|
||||
] as const)(
|
||||
'does not let snapshot provenance with %s authorize an implicit local at',
|
||||
async (_name, block, sections) => {
|
||||
const test = await harness(true, 'Asia/Shanghai')
|
||||
test.agent.session.append('turn/start', { turn: 1 })
|
||||
test.agent.session.append('step/start', { turn: 1, step: 1 })
|
||||
test.agent.session.append('user/message', createUserMessage({
|
||||
content: [{ type: 'text', text: 'request' }],
|
||||
source: { kind: 'user', clientTimeZone: 'Asia/Shanghai' } as never,
|
||||
}), { surfaceOp: 'append' })
|
||||
test.agent.session.append('user/message', createUserMessage({
|
||||
content: [block as never],
|
||||
source: { kind: 'plugin', plugin: 'time-context', form: 'snapshot', sections } as never,
|
||||
}), { surfaceOp: 'append' })
|
||||
|
||||
expect(value(await execute(test, 'schedule_create', {
|
||||
prompt: 'malformed marker', at: { date: '2026-08-06', time: '09:00:00' },
|
||||
}))).toMatchObject({
|
||||
code: 'timezone_confirmation_required',
|
||||
sessionTimeZone: 'Asia/Shanghai',
|
||||
clientTimeZones: [],
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
it.each(['step/end', 'turn/end'] as const)(
|
||||
'fails closed after the current %s boundary',
|
||||
async (boundary) => {
|
||||
|
||||
Reference in New Issue
Block a user