calendar-agent-retrieval-security.test.mjs
128 lines 5.9 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Tier 7 — SECURITY: tier enforcement, policy caps, injection handling, route contract.
3 *
4 * Calendar summaries/descriptions are untrusted prompt content. These tests prove
5 * that disabled calendars and policy caps fail closed, that titles never leak below
6 * tier 2, and that the Hub route is auth-gated and never exposes connector secrets.
7 * @see lib/calendar/agent-retrieval.mjs, hub/server.mjs
8 */
9 import { describe, it, beforeEach, afterEach } from 'node:test';
10 import assert from 'node:assert/strict';
11 import fs from 'node:fs';
12 import path from 'node:path';
13 import { fileURLToPath } from 'node:url';
14 import { importIcsIntoVault } from '../lib/calendar/event-store.mjs';
15 import { patchSourceCalendar } from '../lib/calendar/source-calendar-patch.mjs';
16 import { retrieveAgentCalendarContext } from '../lib/calendar/agent-retrieval.mjs';
17
18 const __dirname = path.dirname(fileURLToPath(import.meta.url));
19 const repoRoot = path.dirname(__dirname);
20 const tmpRoot = path.join(__dirname, 'fixtures', 'tmp-calendar-agent-security');
21 const fixtureDir = path.join(__dirname, 'fixtures', 'calendar');
22 const injectionIcs = fs.readFileSync(path.join(fixtureDir, 'injection-summary.ics'), 'utf8');
23
24 const RANGE = { from: '2026-06-01', to: '2026-06-30' };
25
26 function readRepoFile(relativePath) {
27 return fs.readFileSync(path.join(repoRoot, relativePath), 'utf8');
28 }
29
30 describe('Security — agent tier enforcement', () => {
31 const dataDir = path.join(tmpRoot, 'data');
32 const vaultId = 'default';
33 /** @type {string} */
34 let calendarId;
35
36 beforeEach(() => {
37 fs.rmSync(tmpRoot, { recursive: true, force: true });
38 fs.mkdirSync(dataDir, { recursive: true });
39 delete process.env.KNOWTATION_CALENDAR_AGENT_TIER_MAX_CAP;
40 calendarId = importIcsIntoVault(dataDir, vaultId, { icsText: injectionIcs, displayName: 'Injected' }).source_calendar_id;
41 });
42
43 afterEach(() => {
44 fs.rmSync(tmpRoot, { recursive: true, force: true });
45 delete process.env.KNOWTATION_CALENDAR_AGENT_TIER_MAX_CAP;
46 });
47
48 it('fails closed: agents-disabled calendar yields nothing even at requested tier 2', () => {
49 const result = retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 2 });
50 assert.equal(result.items.length, 0);
51 assert.equal(result.source_calendars.length, 0);
52 });
53
54 it('never exposes an injection summary below tier 2', () => {
55 patchSourceCalendar(dataDir, vaultId, calendarId, { enabled_for_agents: true, agent_context_tier_max: 2 });
56 const tier1 = retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 1 });
57 assert.ok(tier1.items.length > 0);
58 for (const item of tier1.items) {
59 assert.ok(!('summary' in item), 'tier 1 must never carry the summary');
60 assert.ok(!('description' in item));
61 assert.ok(!('location' in item));
62 }
63 });
64
65 it('org policy cap clamps a tier-2 request to tier 1 (minor/classroom policy)', () => {
66 process.env.KNOWTATION_CALENDAR_AGENT_TIER_MAX_CAP = '1';
67 patchSourceCalendar(dataDir, vaultId, calendarId, { enabled_for_agents: true, agent_context_tier_max: 1 });
68 const result = retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 2 });
69 assert.equal(result.effective_tier, 1);
70 assert.equal(result.policy_agent_context_tier_max_cap, 1);
71 assert.ok(result.items.every((i) => i.agent_tier === 1 && !('summary' in i)));
72 });
73
74 it('org policy cap 0 blocks all calendar fields regardless of per-calendar settings', () => {
75 process.env.KNOWTATION_CALENDAR_AGENT_TIER_MAX_CAP = '0';
76 patchSourceCalendar(dataDir, vaultId, calendarId, { enabled_for_agents: true });
77 const result = retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 2 });
78 assert.equal(result.effective_tier, 0);
79 assert.equal(result.items.length, 0);
80 });
81
82 it('rejects requests above the v0 retrieval ceiling (tier 3+)', () => {
83 assert.throws(
84 () => retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 3 }),
85 /ceiling is 2/,
86 );
87 });
88
89 it('at tier 2 the untrusted summary is returned verbatim as data, not interpreted', () => {
90 patchSourceCalendar(dataDir, vaultId, calendarId, { enabled_for_agents: true, agent_context_tier_max: 2 });
91 const result = retrieveAgentCalendarContext(dataDir, vaultId, { ...RANGE, agentContextTier: 2 });
92 assert.equal(result.items.length, 1);
93 assert.equal(typeof result.items[0].summary, 'string');
94 assert.match(result.items[0].summary, /Ignore prior instructions/);
95 });
96 });
97
98 describe('Security — Hub agent-context route contract', () => {
99 it('registers the route behind viewer+ role and delegates to the enforcement lib only', () => {
100 const src = readRepoFile('hub/server.mjs');
101 assert.match(src, /app\.get\('\/api\/v1\/calendar\/agent-context', requireRole\('viewer', 'editor', 'admin', 'evaluator'\)/);
102 assert.match(src, /retrieveAgentCalendarContext\(/);
103 assert.match(src, /from '\.\.\/lib\/calendar\/agent-retrieval\.mjs'/);
104 });
105
106 it('the agent-context route never references OAuth or provider APIs', () => {
107 const src = readRepoFile('hub/server.mjs');
108 const start = src.indexOf("app.get('/api/v1/calendar/agent-context'");
109 const end = src.indexOf('// GET /api/v1/calendar/source-calendars', start);
110 assert.notEqual(start, -1);
111 assert.notEqual(end, -1);
112 const route = src.slice(start, end);
113 assert.doesNotMatch(route, /oauth|google|microsoft|parseIcsToEvents/i);
114 });
115
116 it('retrieval reads events independently of the display toggle (displayOnly false)', () => {
117 const lib = readRepoFile('lib/calendar/agent-retrieval.mjs');
118 assert.match(lib, /displayOnly:\s*false/);
119 assert.match(lib, /redactEventForAgentTier/);
120 });
121
122 it('OpenAPI documents the agent-context schema', () => {
123 const api = readRepoFile('docs/openapi.yaml');
124 assert.match(api, /\/calendar\/agent-context:/);
125 assert.match(api, /knowtation\.calendar_agent_context\/v0/);
126 assert.match(api, /CalendarAgentContextEventItem/);
127 });
128 });
File History 1 commit
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago