derived-artifact-storage-security.test.mjs
576 lines 23.8 KB
Raw
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor ⚠ breaking 8 hours ago
1 /**
2 * Tier 7 — SECURITY (centerpiece): Phase 6 derived-artifact storage layer.
3 *
4 * Covers (§10 Security obligations — all build-blocking):
5 * P6-a/P6-b: NO privacy_max artifact ever written host-readable or under server-held key
6 * P6-j: NO plaintext fallback when encryption unavailable
7 * P6-c: Delegated write fail-closed without owner opt-in
8 * P6-d: NO tier downgrade by a delegate
9 * P6-e: Provenance cannot be forged or omitted
10 * P6-f: NO secret in any artifact, provenance field, log, or error
11 * P6-h: Single-writer no-bypass architecture test (BUILD-BLOCKING)
12 * P6-i: Runtime group imports no writer/encryptor/vault module (extends D5.8)
13 * Global: Convenience never masquerades as privacy_max and vice-versa
14 * Fail-closed posture on every ambiguous input
15 */
16
17 import { describe, it } from 'node:test';
18 import assert from 'node:assert/strict';
19 import { readFileSync, existsSync } from 'node:fs';
20 import { resolve, join } from 'node:path';
21 import { createRequire } from 'node:module';
22 import { fileURLToPath } from 'node:url';
23
24 import {
25 createDerivedArtifactWriter,
26 WRITER_REASONS,
27 } from '../lib/companion-artifact-writer.mjs';
28
29 import {
30 createClientEncryptor,
31 UNAVAILABLE_CLIENT_ENCRYPTOR,
32 ENCRYPTOR_REASONS,
33 } from '../lib/companion-client-encryptor.mjs';
34
35 import {
36 buildConvenienceProvenance,
37 validateProvenance,
38 PROVENANCE_REJECT_REASONS,
39 } from '../lib/companion-provenance-validator.mjs';
40
41 import {
42 TERMINAL_STATES,
43 resolveTier,
44 TIER_RESOLVE_REASONS,
45 } from '../lib/companion-tier-resolver.mjs';
46
47 const __dirname = fileURLToPath(new URL('.', import.meta.url));
48 const REPO_ROOT = resolve(__dirname, '..');
49
50 // ── Helpers ───────────────────────────────────────────────────────────────────
51
52 function buildSecStores() {
53 const written = [];
54 const vectors = [];
55 const insights = [];
56
57 const writeNoteFn = (_vp, notePath, opts) => {
58 written.push({ notePath, frontmatter: { ...opts.frontmatter } });
59 };
60
61 const vs = {
62 upsert: async (points) => { vectors.push(...points); },
63 deleteByPath: async () => {},
64 };
65
66 const mm = {
67 store: (type, data) => {
68 if (type === 'insight') insights.push(data);
69 return { id: 'x', ts: '' };
70 },
71 };
72
73 return { written, vectors, insights, writeNoteFn, vs, mm };
74 }
75
76 function selfCtx(overrides = {}) {
77 return {
78 lane: 'local', containsPrivateData: false, isDelegate: false,
79 delegatedManagedAllowed: false, enrichesDelegatedPartition: false,
80 delegatedEnrichmentAllowed: false,
81 ...overrides,
82 };
83 }
84
85 function baseProv(overrides = {}) {
86 return buildConvenienceProvenance({
87 generatedBy: 'sec-user',
88 source: 'companion',
89 model: 'sec-model',
90 modelVersion: '1.0',
91 lane: 'local',
92 artifactType: 'ai_summary',
93 sourceNotePath: 'sec/note.md',
94 sourceEventId: 'mem_sec_001',
95 ...overrides,
96 });
97 }
98
99 // ── P6-a / P6-b: Privacy_max NEVER host-readable or server-held-key ───────────
100
101 describe('security P6-a/P6-b — privacy_max artifact never host-readable', () => {
102 it('privacy_max + unavailable encryptor → no write occurs (fail closed)', async () => {
103 const { written, writeNoteFn } = buildSecStores();
104 const writer = createDerivedArtifactWriter({
105 writeNoteFn,
106 vaultPath: '/vault',
107 vaultRegistryAvailable: true, // registry present → tier resolves to client_encrypted
108 // No encryptor → UNAVAILABLE_CLIENT_ENCRYPTOR (default)
109 });
110
111 const privProv = { ...baseProv(), privacy_tier: 'privacy_max' };
112 const r = await writer.write({ summary: 'private content' }, privProv, selfCtx());
113
114 assert.equal(r.ok, false);
115 assert.equal(r.reason, WRITER_REASONS.ENCRYPTION_UNAVAILABLE);
116 assert.equal(written.length, 0, 'NOTHING must be written when privacy_max encryption unavailable');
117 });
118
119 it('privacy_max + vault registry absent → tier rejected before encryption check', async () => {
120 const { written, writeNoteFn } = buildSecStores();
121 const writer = createDerivedArtifactWriter({
122 writeNoteFn,
123 vaultPath: '/vault',
124 vaultRegistryAvailable: false, // D6.1.1 — no registry → fail closed
125 });
126
127 const privProv = { ...baseProv(), privacy_tier: 'privacy_max' };
128 const r = await writer.write({ summary: 'private' }, privProv, selfCtx());
129
130 assert.equal(r.ok, false);
131 assert.equal(r.reason, WRITER_REASONS.TIER_UNRESOLVABLE);
132 assert.equal(written.length, 0);
133 });
134
135 it('resolveTier: server-held-key equivalent → host_readable (only convenience may use it)', () => {
136 // Server-held-key = host_readable classification (D6.1.2)
137 // Convenience → host_readable: this is the ONLY class that may use server-held key
138 const r = resolveTier('ai_summary', 'convenience');
139 assert.equal(r.ok, true);
140 assert.equal(r.terminalState, TERMINAL_STATES.HOST_READABLE);
141
142 // Privacy_max NEVER resolves to host_readable
143 const rp = resolveTier('ai_summary', 'privacy_max', { vaultRegistryAvailable: true });
144 assert.equal(rp.terminalState, TERMINAL_STATES.CLIENT_ENCRYPTED);
145 assert.notEqual(rp.terminalState, TERMINAL_STATES.HOST_READABLE, 'privacy_max must never be host_readable');
146 });
147
148 it('privacy_max artifact never has a plaintext ai_summary in frontmatter', async () => {
149 const { written, writeNoteFn } = buildSecStores();
150
151 const workingEncryptor = createClientEncryptor({
152 isAvailable: () => true,
153 encrypt: (bytes) => ({
154 ciphertext: new Uint8Array(bytes.length).fill(0xee),
155 wrappedDekRef: 'dek-sec-001',
156 alg: 'STUB-256',
157 }),
158 });
159
160 const writer = createDerivedArtifactWriter({
161 writeNoteFn,
162 vaultPath: '/vault',
163 vaultRegistryAvailable: true,
164 encryptor: workingEncryptor,
165 });
166
167 const privProv = { ...baseProv(), privacy_tier: 'privacy_max' };
168 await writer.write({ summary: 'very private' }, privProv, selfCtx());
169
170 for (const w of written) {
171 assert.equal(w.frontmatter.ai_summary, undefined,
172 'plaintext ai_summary must NEVER appear in frontmatter at privacy_max');
173 }
174 });
175 });
176
177 // ── P6-j: No plaintext fallback, ever ─────────────────────────────────────────
178
179 describe('security P6-j — no plaintext fallback when encryption unavailable', () => {
180 it('UNAVAILABLE_CLIENT_ENCRYPTOR.encrypt throws, never returns plaintext', () => {
181 assert.throws(
182 () => UNAVAILABLE_CLIENT_ENCRYPTOR.encrypt(new Uint8Array([1, 2, 3]), { scope: 'vault' }),
183 (err) => err.message === ENCRYPTOR_REASONS.UNAVAILABLE,
184 );
185 });
186
187 it('writer never writes plaintext when encrypt() throws', async () => {
188 const { written, writeNoteFn } = buildSecStores();
189 const throwingEncryptor = createClientEncryptor({
190 isAvailable: () => true,
191 encrypt: () => { throw new Error('key locked'); },
192 });
193
194 const writer = createDerivedArtifactWriter({
195 writeNoteFn,
196 vaultPath: '/vault',
197 vaultRegistryAvailable: true,
198 encryptor: throwingEncryptor,
199 });
200
201 const privProv = { ...baseProv(), privacy_tier: 'privacy_max' };
202 const r = await writer.write({ summary: 'sensitive' }, privProv, selfCtx());
203
204 assert.equal(r.ok, false);
205 assert.equal(r.reason, WRITER_REASONS.ENCRYPTION_FAILED);
206 assert.equal(written.length, 0, 'Nothing must be written on encryption failure — no plaintext fallback');
207 });
208
209 it('writer returns ENCRYPTION_UNAVAILABLE before any store call (no partial write)', async () => {
210 let storeCallCount = 0;
211 const countingWriter = (_vp, _np, _opts) => { storeCallCount++; };
212
213 const writer = createDerivedArtifactWriter({
214 writeNoteFn: countingWriter,
215 vaultPath: '/vault',
216 vaultRegistryAvailable: true,
217 });
218
219 const privProv = { ...baseProv(), privacy_tier: 'privacy_max' };
220 await writer.write({ summary: 'secret' }, privProv, selfCtx());
221
222 assert.equal(storeCallCount, 0, 'writeNoteFn must not be called before encryption succeeds');
223 });
224 });
225
226 // ── P6-c: Delegated write fail-closed without owner opt-in ────────────────────
227
228 describe('security P6-c — delegated write fail-closed (D6.3.3)', () => {
229 it('enrichesDelegatedPartition=true always blocked (D6.3.6: tenancy gate absent)', async () => {
230 const { written, writeNoteFn } = buildSecStores();
231 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
232
233 for (const delegatedEnrichmentAllowed of [false, true]) {
234 const r = await writer.write({ summary: 'delegate' }, baseProv(), {
235 lane: 'local', containsPrivateData: false,
236 isDelegate: true, delegatedManagedAllowed: false,
237 enrichesDelegatedPartition: true, delegatedEnrichmentAllowed,
238 });
239 assert.equal(r.ok, false);
240 assert.equal(r.reason, WRITER_REASONS.SELF_PARTITION_ONLY,
241 `enrichesDelegatedPartition=true must always be blocked, delegatedEnrichmentAllowed=${delegatedEnrichmentAllowed}`);
242 }
243 assert.equal(written.length, 0);
244 });
245
246 it('delegated managed-lane write denied without delegatedManagedAllowed', async () => {
247 const { written, writeNoteFn } = buildSecStores();
248 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
249
250 const managedProv = baseProv({
251 source: 'managed',
252 lane: 'direct_provider',
253 artifactType: 'ai_summary',
254 });
255 const r = await writer.write({ summary: 'managed delegate' }, managedProv, {
256 lane: 'direct_provider', containsPrivateData: false,
257 isDelegate: true, delegatedManagedAllowed: false,
258 enrichesDelegatedPartition: false, // self-partition managed
259 });
260 assert.equal(r.ok, false);
261 assert.equal(r.reason, WRITER_REASONS.CONSENT_DENIED);
262 assert.equal(written.length, 0);
263 });
264 });
265
266 // ── P6-d: No tier downgrade ────────────────────────────────────────────────────
267
268 describe('security P6-d — no tier downgrade (D6.3.4)', () => {
269 it('tier is resolved from provenance.privacy_tier (owner tier) — cannot be downgraded', () => {
270 // Tier resolver always uses the provenance.privacy_tier (the owner's tier).
271 // A delegate cannot pass a different tier — the tier is stamped by the writer
272 // from the owner's vault, not from the actor's capability.
273 //
274 // At Phase 6: cross-partition is fully blocked (D6.3.6), so tier downgrade
275 // by a delegate is structurally impossible — the write fails at SELF_PARTITION_ONLY
276 // before tier resolution even runs.
277 //
278 // Verify that the tier resolver never maps privacy_max → host_readable:
279 for (const at of ['ai_summary', 'embedding', 'insight', 'discovery_facet']) {
280 const r = resolveTier(at, 'privacy_max', { vaultRegistryAvailable: true });
281 assert.ok(r.ok);
282 assert.notEqual(r.terminalState, TERMINAL_STATES.HOST_READABLE,
283 `privacy_max must never resolve to host_readable for ${at}`);
284 }
285 });
286
287 it('convenience tier never resolves to client_encrypted (no accidental upgrade)', () => {
288 for (const at of ['ai_summary', 'embedding', 'insight', 'discovery_facet']) {
289 const r = resolveTier(at, 'convenience');
290 assert.equal(r.terminalState, TERMINAL_STATES.HOST_READABLE,
291 `convenience must always resolve to host_readable for ${at}`);
292 }
293 });
294 });
295
296 // ── P6-e: Provenance cannot be forged or omitted ──────────────────────────────
297
298 describe('security P6-e — provenance cannot be forged or omitted (D6.2, D6.6)', () => {
299 it('every missing required field causes rejection', async () => {
300 const { written, writeNoteFn } = buildSecStores();
301 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
302 const base = baseProv();
303
304 const required = [
305 'generated_by', 'source', 'model', 'lane', 'privacy_tier',
306 'source_note_path', 'source_event_id', 'created_at', 'artifact_type', 'schema_version',
307 ];
308
309 for (const field of required) {
310 const p = { ...base };
311 delete p[field];
312 const r = await writer.write({ summary: 'x' }, p, selfCtx());
313 assert.equal(r.ok, false, `Missing ${field} must cause rejection`);
314 assert.equal(written.length, 0, `No write after missing ${field}`);
315 }
316 });
317
318 it('null provenance is always rejected', async () => {
319 const { writeNoteFn } = buildSecStores();
320 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
321 const r = await writer.write({ summary: 'x' }, null, selfCtx());
322 assert.equal(r.ok, false);
323 });
324
325 it('provenance with forged generated_by (empty) is rejected', async () => {
326 const { writeNoteFn } = buildSecStores();
327 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
328 const p = { ...baseProv(), generated_by: '' };
329 const r = await writer.write({ summary: 'x' }, p, selfCtx());
330 assert.equal(r.ok, false);
331 });
332
333 it('provenance sidecar stored in frontmatter matches written provenance exactly', async () => {
334 const { written, writeNoteFn } = buildSecStores();
335 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
336 const p = baseProv();
337 await writer.write({ summary: 'honest summary' }, p, selfCtx());
338
339 const stored = written[0].frontmatter.ai_summary_provenance;
340 assert.equal(stored.generated_by, p.generated_by);
341 assert.equal(stored.model, p.model);
342 assert.equal(stored.lane, p.lane);
343 assert.equal(stored.created_at, p.created_at);
344 });
345 });
346
347 // ── P6-f: No secret in artifact, provenance, log, or error ────────────────────
348
349 describe('security P6-f — no secret in any artifact, provenance, log, or error', () => {
350 it('validateProvenance rejects provenance with token field', () => {
351 const p = { ...baseProv(), access_token: 'bearer-xyz' };
352 const r = validateProvenance(p);
353 assert.equal(r.ok, false);
354 assert.equal(r.reason, PROVENANCE_REJECT_REASONS.SENSITIVE_DATA);
355 });
356
357 it('validateProvenance rejects artifact with api_key', () => {
358 const r = validateProvenance(baseProv(), { summary: 'ok', api_key: 'sk-1234' });
359 assert.equal(r.ok, false);
360 assert.equal(r.reason, PROVENANCE_REJECT_REASONS.SENSITIVE_DATA);
361 });
362
363 it('validateProvenance rejects artifact with nested credential', () => {
364 const r = validateProvenance(baseProv(), { data: { credentials: { password: 'abc' } } });
365 assert.equal(r.ok, false);
366 assert.equal(r.reason, PROVENANCE_REJECT_REASONS.SENSITIVE_DATA);
367 });
368
369 it('writer reason codes do not include key material', async () => {
370 const { writeNoteFn } = buildSecStores();
371 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
372
373 // Test each failure path and ensure the reason code is a fixed string
374 const tests = [
375 // Invalid provenance
376 writer.write({ summary: 'x' }, null, selfCtx()),
377 // Cross-partition
378 writer.write({ summary: 'x' }, baseProv(), { ...selfCtx(), enrichesDelegatedPartition: true }),
379 // Consent denied
380 writer.write({ summary: 'x' }, { ...baseProv(), lane: 'direct_provider', source: 'managed' }, {
381 lane: 'direct_provider', containsPrivateData: false, isDelegate: true,
382 delegatedManagedAllowed: false,
383 }),
384 ];
385
386 const results = await Promise.all(tests);
387 const SECRET_PATTERNS = /key|secret|token|password|credential|bearer/i;
388
389 for (const r of results) {
390 assert.equal(r.ok, false);
391 assert.ok(
392 !SECRET_PATTERNS.test(r.reason),
393 `Reason code must not contain sensitive patterns: ${r.reason}`,
394 );
395 if (r.detail) {
396 assert.ok(
397 !SECRET_PATTERNS.test(r.detail),
398 `Detail must not contain sensitive patterns: ${r.detail}`,
399 );
400 }
401 }
402 });
403
404 it('ENCRYPTOR_REASONS codes do not contain key material (no hex blobs, JWTs, or base64 payloads)', () => {
405 // Reason codes are short fixed identifiers like 'encryptor_unavailable_no_user_held_key'.
406 // The check guards against actual key material (long hex/base64 strings, JWT patterns)
407 // leaking into codes — not against descriptive identifiers that use the word "key".
408 const ACTUAL_SECRET_PATTERNS = /[0-9a-f]{32,}|eyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+|[A-Za-z0-9+/]{40,}={0,2}/;
409 for (const code of Object.values(ENCRYPTOR_REASONS)) {
410 assert.ok(!ACTUAL_SECRET_PATTERNS.test(code), `Encryptor reason must not contain key material: ${code}`);
411 // Also verify it's a reasonable short identifier (not an exfiltrated value)
412 assert.ok(code.length < 80, `Encryptor reason code suspiciously long: ${code}`);
413 }
414 });
415
416 it('provenance sidecar stored in frontmatter contains no secret-bearing fields', async () => {
417 const { written, writeNoteFn } = buildSecStores();
418 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
419 await writer.write({ summary: 'fine' }, baseProv(), selfCtx());
420
421 const stored = written[0].frontmatter.ai_summary_provenance;
422 const secretPattern = /(api[_-]?key|secret|password|token|credential|authorization|bearer|private[_-]?key)/i;
423 for (const key of Object.keys(stored)) {
424 assert.ok(!secretPattern.test(key), `Provenance sidecar must not have sensitive key: ${key}`);
425 }
426 });
427 });
428
429 // ── P6-h: Single-writer no-bypass architecture test (BUILD-BLOCKING) ──────────
430
431 describe('security P6-h — single-writer no-bypass (D6.6.3/D6.6.4) [BUILD-BLOCKING]', () => {
432 it('index-enrich.mjs does not call writeNote directly for ai_summary after migration', () => {
433 const enrichPath = join(REPO_ROOT, 'mcp/tools/index-enrich.mjs');
434 const src = readFileSync(enrichPath, 'utf8');
435
436 // The direct writeNote call for ai_summary frontmatter must be removed (D6.6.2)
437 // Pattern: writeNote(..., { frontmatter: { ai_summary: ... } }) without going through writer
438 const directWritePattern = /writeNote\s*\([^)]*frontmatter\s*:\s*\{[^}]*ai_summary\s*:/;
439 assert.ok(
440 !directWritePattern.test(src),
441 'index-enrich.mjs must NOT contain direct writeNote calls for ai_summary frontmatter. ' +
442 'All writes must go through DerivedArtifactWriter (D6.6.2).',
443 );
444 });
445
446 it('memory-consolidate.mjs does not call mm.store("insight") directly after migration', () => {
447 const consolidatePath = join(REPO_ROOT, 'lib/memory-consolidate.mjs');
448 const src = readFileSync(consolidatePath, 'utf8');
449
450 // Strip comment lines so we only check executable code, not comment references (D6.6.2)
451 const codeLines = src.split('\n')
452 .filter((line) => !line.trim().startsWith('//') && !line.trim().startsWith('*'))
453 .join('\n');
454
455 const directInsightPattern = /mm\.store\s*\(\s*['"]insight['"]/;
456 assert.ok(
457 !directInsightPattern.test(codeLines),
458 'memory-consolidate.mjs must NOT contain direct mm.store("insight", ...) calls in code. ' +
459 'All insight persistence must go through DerivedArtifactWriter (D6.6.2).',
460 );
461 });
462
463 it('companion-artifact-writer.mjs is in the lib/ directory (authority group)', () => {
464 const writerPath = join(REPO_ROOT, 'lib/companion-artifact-writer.mjs');
465 assert.ok(existsSync(writerPath), 'DerivedArtifactWriter must exist in lib/ (authority group)');
466 });
467
468 it('companion-client-encryptor.mjs is in the lib/ directory (authority group)', () => {
469 const encPath = join(REPO_ROOT, 'lib/companion-client-encryptor.mjs');
470 assert.ok(existsSync(encPath), 'ClientEncryptor must exist in lib/ (authority group)');
471 });
472 });
473
474 // ── P6-i: Runtime group imports no writer/encryptor/vault module ──────────────
475
476 describe('security P6-i — runtime group imports no writer/encryptor module (extends D5.8) [BUILD-BLOCKING]', () => {
477 const FORBIDDEN_IMPORTS = [
478 'companion-artifact-writer',
479 'companion-client-encryptor',
480 ];
481
482 const RUNTIME_GROUP_FILES = [
483 'lib/companion-runtime-manager.mjs',
484 'lib/companion-spawn-adapter.mjs',
485 'lib/companion-download-adapter.mjs',
486 'lib/companion-inference-listener.mjs',
487 'lib/companion-resource-probe.mjs',
488 ];
489
490 for (const file of RUNTIME_GROUP_FILES) {
491 for (const forbidden of FORBIDDEN_IMPORTS) {
492 it(`${file} does not import ${forbidden}`, () => {
493 const filePath = join(REPO_ROOT, file);
494 if (!existsSync(filePath)) return; // File may not exist in all branches — skip
495
496 const src = readFileSync(filePath, 'utf8');
497 const importPattern = new RegExp(`import[^'"]*['"].*${forbidden.replace('.', '\\.')}.*['"]`);
498 assert.ok(
499 !importPattern.test(src),
500 `SECURITY VIOLATION: ${file} must NOT import ${forbidden}. ` +
501 `The runtime/inference group must never hold the writer or encryptor capability (D6.6.3, D5.8).`,
502 );
503 });
504 }
505 }
506 });
507
508 // ── Convenience never masquerades as privacy_max / vice-versa ─────────────────
509
510 describe('security — tier purity (no masquerade)', () => {
511 it('convenience write produces host_readable terminal state, never client_encrypted', async () => {
512 const { written, writeNoteFn } = buildSecStores();
513 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
514
515 const r = await writer.write({ summary: 'convenience' }, baseProv(), selfCtx());
516 assert.equal(r.ok, true);
517 assert.equal(r.terminalState, TERMINAL_STATES.HOST_READABLE);
518 // ai_summary is plaintext — that is correct for convenience
519 assert.equal(written[0].frontmatter.ai_summary, 'convenience');
520 assert.equal(written[0].frontmatter.ai_summary_ciphertext, undefined);
521 });
522
523 it('unknown tier input never resolves to convenience (fail-closed to most-restrictive)', () => {
524 // Unknown tier → TIER_RESOLVE_REASONS.UNKNOWN_TIER (not convenience)
525 const r = resolveTier('ai_summary', 'unknown_mystery_tier');
526 assert.equal(r.ok, false);
527 assert.equal(r.reason, TIER_RESOLVE_REASONS.UNKNOWN_TIER);
528 // It must NOT silently fall back to convenience
529 });
530 });
531
532 // ── Global fail-closed posture ────────────────────────────────────────────────
533
534 describe('security — global fail-closed posture', () => {
535 it('writer with every possible bad provenance field combination always returns ok:false', async () => {
536 const { writeNoteFn } = buildSecStores();
537 const writer = createDerivedArtifactWriter({ writeNoteFn, vaultPath: '/vault' });
538
539 const badProvCases = [
540 null, undefined, '', 0, [], {}, { generated_by: 'x' }, { schema_version: 1 },
541 { ...baseProv(), privacy_tier: undefined },
542 { ...baseProv(), artifact_type: 'invalid' },
543 { ...baseProv(), created_at: 'not-a-date' },
544 ];
545
546 for (const prov of badProvCases) {
547 const r = await writer.write({ summary: 'x' }, prov, selfCtx());
548 assert.equal(r.ok, false, `Bad provenance must always fail: ${JSON.stringify(prov)}`);
549 }
550 });
551
552 it('resolveTier returns ok:false for every invalid input combination', () => {
553 const badCombos = [
554 ['', ''],
555 ['ai_summary', ''],
556 ['', 'convenience'],
557 [null, null],
558 ['video', 'convenience'],
559 ['ai_summary', 'SECRET_TIER'],
560 ];
561 for (const [at, pt] of badCombos) {
562 const r = resolveTier(at, pt);
563 assert.equal(r.ok, false, `resolveTier(${at}, ${pt}) must fail`);
564 }
565 });
566
567 it('UNAVAILABLE_CLIENT_ENCRYPTOR.isAvailable never throws, always false', () => {
568 const extremeInputs = [null, undefined, '', 0, {}, [], 'privacy_max', 'vault'];
569 for (let i = 0; i < extremeInputs.length - 1; i++) {
570 assert.doesNotThrow(() => {
571 const result = UNAVAILABLE_CLIENT_ENCRYPTOR.isAvailable(extremeInputs[i], extremeInputs[i + 1]);
572 assert.equal(result, false);
573 });
574 }
575 });
576 });
File History 1 commit
sha256:0d530f9ef27b8b75547d1db7701a74bc77b77aa8f3d7fa3a8672cf2af36e63bb reconcile: import GitHub-direct RBAC/OAuth/companion and ho… Human minor 8 hours ago