model-runtime-lane-security.test.mjs
235 lines 9.2 KB
Raw
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor ⚠ breaking 1 day ago
1 /**
2 * Tier 7 — SECURITY: model-runtime-lane adversarial properties
3 *
4 * Tests the security invariants from the Phase 1 seam contract:
5 * 1. orgPrivacyMode never routes to managed — org privacy cannot be bypassed.
6 * 2. Delegate without owner opt-in is denied before consent (policy beats consent).
7 * 3. Private data never reaches managed without an explicit consentId — even if every
8 * other parameter is maximally permissive.
9 * 4. Unknown/malformed lane values in enforceConsentPolicy never grant managed access.
10 * 5. Fail-closed: no capability or preference set (empty objects) never selects a
11 * non-disabled, metered lane.
12 * 6. Unknown extra fields on inputs cannot escalate privileges.
13 * 7. A forged 'delegatedManagedAllowed' on an injected capabilities object does NOT
14 * bypass the policy gate (policy parameters are separate from capabilities).
15 *
16 * Reference: docs/COMPANION-APP-PHASE-1-ADAPTER-SEAM.md §5 (threat model table)
17 * docs/COMPANION-APP-MODEL-ROUTING-AND-ENRICHMENT-ARCHITECTURE.md §8.7
18 */
19 import { describe, it } from 'node:test';
20 import assert from 'node:assert/strict';
21 import {
22 selectLane,
23 isManagedLane,
24 enforceConsentPolicy,
25 } from '../lib/model-runtime-lane.mjs';
26
27 describe('Security — orgPrivacyMode cannot be bypassed to reach managed lane', () => {
28 it('all capability combinations with orgPrivacyMode=true: managed never selected', () => {
29 const boolValues = [true, false];
30 for (const managed of boolValues)
31 for (const inBrowser of boolValues)
32 for (const companion of boolValues) {
33 const lane = selectLane(
34 { managedKeyAvailable: managed, inBrowserAvailable: inBrowser, companionAvailable: companion },
35 { orgPrivacyMode: true },
36 );
37 assert.notEqual(
38 lane, 'direct_provider',
39 `orgPrivacyMode=true still selected direct_provider (caps: managed=${String(managed)}, inBrowser=${String(inBrowser)}, companion=${String(companion)})`,
40 );
41 }
42 });
43
44 it('orgPrivacyMode=true with unknown extra preference fields: still no managed', () => {
45 const lane = selectLane(
46 { managedKeyAvailable: true },
47 { orgPrivacyMode: true, unknownOverride: false },
48 );
49 assert.notEqual(lane, 'direct_provider');
50 });
51 });
52
53 describe('Security — policy denial beats consent (evaluation order)', () => {
54 it('delegate without opt-in: lane_policy_denied even when consentId is present', () => {
55 const d = enforceConsentPolicy({
56 lane: 'direct_provider',
57 containsPrivateData: true,
58 consentId: 'attacker-supplied-consent-id',
59 isDelegate: true,
60 delegatedManagedAllowed: false,
61 });
62 assert.equal(d, 'lane_policy_denied', 'policy denial should precede consent check');
63 });
64
65 it('delegate without opt-in, non-private data, valid consentId: still denied', () => {
66 const d = enforceConsentPolicy({
67 lane: 'direct_provider',
68 containsPrivateData: false,
69 consentId: 'cid-valid',
70 isDelegate: true,
71 delegatedManagedAllowed: false,
72 });
73 assert.equal(d, 'lane_policy_denied');
74 });
75 });
76
77 describe('Security — private data never reaches managed without explicit consent', () => {
78 it('managed lane + private data + no consentId: always cloud_consent_required', () => {
79 // Verify across all combinations of isDelegate + delegatedManagedAllowed
80 // where policy doesn't already deny.
81 const allowed = [
82 { isDelegate: false, delegatedManagedAllowed: false },
83 { isDelegate: false, delegatedManagedAllowed: true },
84 { isDelegate: true, delegatedManagedAllowed: true },
85 ];
86 for (const { isDelegate, delegatedManagedAllowed } of allowed) {
87 const d = enforceConsentPolicy({
88 lane: 'direct_provider',
89 containsPrivateData: true,
90 consentId: undefined,
91 isDelegate,
92 delegatedManagedAllowed,
93 });
94 assert.equal(
95 d, 'cloud_consent_required',
96 `private data without consent should be blocked (isDelegate=${String(isDelegate)}, delegatedManagedAllowed=${String(delegatedManagedAllowed)})`,
97 );
98 }
99 });
100
101 it('empty string consentId is treated as missing (not a valid consent token)', () => {
102 const d = enforceConsentPolicy({
103 lane: 'direct_provider',
104 containsPrivateData: true,
105 consentId: '',
106 isDelegate: false,
107 delegatedManagedAllowed: false,
108 });
109 // '' is falsy — treated as no consentId.
110 assert.equal(d, 'cloud_consent_required');
111 });
112 });
113
114 describe('Security — unknown or malformed lane values in enforceConsentPolicy', () => {
115 it('unknown lane string is NOT treated as managed (not in RUNTIME_LANES)', () => {
116 const d = enforceConsentPolicy({
117 lane: 'unknown_future_lane',
118 containsPrivateData: true,
119 consentId: undefined,
120 isDelegate: false,
121 delegatedManagedAllowed: false,
122 });
123 // isManagedLane('unknown_future_lane') = false → allow (not a managed lane).
124 assert.equal(d, 'allow');
125 // Verify the managed boundary is NOT crossed.
126 assert.equal(isManagedLane('unknown_future_lane'), false);
127 });
128
129 it('empty string lane is not managed', () => {
130 assert.equal(isManagedLane(''), false);
131 const d = enforceConsentPolicy({ lane: '', containsPrivateData: true, consentId: undefined, isDelegate: false, delegatedManagedAllowed: false });
132 assert.equal(d, 'allow');
133 });
134 });
135
136 describe('Security — fail-closed: empty caps/prefs never select a metered lane', () => {
137 it('no capabilities → disabled (no metered lane selected)', () => {
138 const lane = selectLane({}, {});
139 assert.equal(lane, 'disabled');
140 assert.equal(isManagedLane(lane), false);
141 });
142
143 it('unknown extra fields on capabilities cannot introduce a metered lane', () => {
144 const lane = selectLane({ magicManagedAccess: true }, {});
145 // managedKeyAvailable is not set → disabled.
146 assert.equal(lane, 'disabled');
147 assert.equal(isManagedLane(lane), false);
148 });
149 });
150
151 describe('Security — injected delegatedManagedAllowed field on capabilities object', () => {
152 it('delegatedManagedAllowed on capabilities does not bypass policy (wrong param location)', () => {
153 // Attacker puts delegatedManagedAllowed in capabilities instead of preferences.
154 // The policy gate reads it from the explicit params object, not from capabilities.
155 const lane = selectLane({ managedKeyAvailable: true }, {});
156 const d = enforceConsentPolicy({
157 lane,
158 containsPrivateData: false,
159 consentId: undefined,
160 isDelegate: true,
161 delegatedManagedAllowed: false, // correct source — attacker cannot override via capabilities
162 });
163 assert.equal(d, 'lane_policy_denied');
164 });
165 });
166
167 describe('Security — D1.3(2) delegated companion enrichment cannot silently proceed', () => {
168 // This is the gate §12 canonical defect: "a member's companion silently enriching an
169 // owner's notes". The default-OFF gate must hold across every off-owner-infra lane.
170 it('delegate enrichment of owner partition is denied by default on local and openrouter', () => {
171 for (const lane of ['local', 'openrouter']) {
172 const d = enforceConsentPolicy({
173 lane,
174 containsPrivateData: true,
175 consentId: 'attacker-consent', // a consentId must NOT unlock a policy-gated lane
176 isDelegate: true,
177 delegatedManagedAllowed: true, // even the managed opt-in must not leak across
178 enrichesDelegatedPartition: true,
179 delegatedEnrichmentAllowed: false,
180 });
181 assert.equal(d, 'lane_policy_denied', `lane ${lane} leaked delegated enrichment`);
182 }
183 });
184
185 it('a consentId cannot override the delegated-enrichment policy denial', () => {
186 const d = enforceConsentPolicy({
187 lane: 'local',
188 containsPrivateData: false,
189 consentId: 'cid-supplied',
190 isDelegate: true,
191 delegatedManagedAllowed: false,
192 enrichesDelegatedPartition: true,
193 delegatedEnrichmentAllowed: false,
194 });
195 assert.equal(d, 'lane_policy_denied');
196 });
197
198 it('fail-closed: omitting delegatedEnrichmentAllowed denies (default OFF)', () => {
199 const d = enforceConsentPolicy({
200 lane: 'local',
201 containsPrivateData: true,
202 consentId: undefined,
203 isDelegate: true,
204 delegatedManagedAllowed: false,
205 enrichesDelegatedPartition: true,
206 // delegatedEnrichmentAllowed intentionally omitted
207 });
208 assert.equal(d, 'lane_policy_denied');
209 });
210 });
211
212 describe('Security — no secrets or private data in return values', () => {
213 it('selectLane returns only a lane string — no input data echoed back', () => {
214 const sensitiveCapabilities = {
215 inBrowserAvailable: false,
216 _privateKey: 'secret-key-value',
217 managedKeyAvailable: true,
218 };
219 const lane = selectLane(sensitiveCapabilities, {});
220 // The return value is one of the canonical strings, never a reflection of inputs.
221 assert.equal(typeof lane, 'string');
222 assert.ok(!lane.includes('secret'));
223 });
224
225 it('enforceConsentPolicy return value contains no consentId content', () => {
226 const d = enforceConsentPolicy({
227 lane: 'direct_provider',
228 containsPrivateData: true,
229 consentId: 'user-private-consent-token-xyz',
230 isDelegate: false,
231 delegatedManagedAllowed: false,
232 });
233 assert.ok(!d.includes('user-private-consent-token-xyz'));
234 });
235 });
File History 2 commits
sha256:65ccb454656ea5acdea0a10e559b78bcde1eb6ff753ecc2911bc99d1c3d7cadd feat(calendar): enforce agent context tiers in retrieval AP… Human minor 1 day ago
sha256:9103f98c89257ed2b01c237cea895dabb3e85ea337dccb1161c175e4422355b6 docs: accept Calendar Events v0 spec with Phase 0 security … Human 1 day ago