model-runtime-lane-security.test.mjs
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