A canary is temporary: bound activation with a fail-closed lease
`LINEAGE_CANARY_SPORTS=mlb` was parsed once at module load and frozen, so an enabled canary stayed writable for the whole process lifetime. On 2026-08-29 it was left set and the 22:00Z, 01:00Z and 03:00Z scheduled snapshots each wrote lineage overnight with nobody watching. That history happened to be correct. Lineage is append-only evidence, so a defective canary would have written irreversible WRONG evidence exactly as quietly. Correct history was luck, not a safety property. NEW CONTRACT: LINEAGE_CANARY_SPORTS=mlb@2026-08-30T18:00:00Z Absolute UTC instant only -- no duration, no local timezone. A relative "4h" would silently restart on every redeploy, which is the defect being removed. LEGACY `mlb` NO LONGER ACTIVATES ANYTHING. It is INVALID_MISSING_EXPIRY. Leaving it working would have left the defect in place behind a nicer-looking alternative, so this is the load-bearing half of the repair. EXPIRY IS EVALUATED AT THE WRITE GATE, not at startup. `isEnabled(sport, now)` re-reads the clock on every call and `lineageCanaryEnabled` threads it through, so a lease turns itself off with no operator, no restart, no Redis and no network. A design where an expired canary keeps writing until someone restarts is the same failure in a different hat. MAX_LEASE is FOUR HOURS, and the bound is proven rather than chosen. MLB ticks are [14,19,22,1,3] UTC; exhaustively over a minute grid across UTC date boundaries, the shortest span enclosing THREE consecutive ticks is 22:00 -> 01:00 -> 03:00 = five hours. Four hours encloses at most two, with an hour of margin. (An earlier note claimed six hours admitted two. It admits three; that claim was false and the test now pins the arithmetic.) The bound is a function of the scheduler, so a test reads the hours from sportCadence and fails if a cadence change invalidates the proof. Everything fails closed: absent, empty, bare sport, comma list, two leases, duplicate @, non-leasable sport, missing timezone, numeric offset, malformed, over-long, already-expired, expiry-equals-now, and an unreadable clock. Configured-but-EXPIRED stays distinguishable from never-configured so automatic containment is auditable. No Redis, no Supabase, no counter, no network in the lease path -- an unreachable dependency must never decide whether lineage may write. Lineage algorithms, cache-date, participant and retention identity untouched. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01CQJeAG8vcDoL5zkiaJyVb8
This commit is contained in:
@@ -113,54 +113,86 @@ describe('LINEAGE CONFIG — one parser', () => {
|
||||
const snap = require('../../src/services/snapshotService');
|
||||
// eslint-disable-next-line global-require
|
||||
const cfg = require('../../src/services/lineageCanaryConfig');
|
||||
const now = new Date('2026-08-30T12:00:00Z');
|
||||
for (const sp of ['mlb', 'wnba', 'nba', 'soccer']) {
|
||||
expect(snap.lineageCanaryEnabled(sp)).toBe(cfg.isEnabled(sp));
|
||||
expect(snap.lineageCanaryEnabled(sp, now)).toBe(cfg.isEnabled(sp, now));
|
||||
}
|
||||
expect(snap.LINEAGE_CANARY_SPORTS).toBe(cfg.SPORTS);
|
||||
});
|
||||
|
||||
test('unset reports disabled + [] + DEFAULT', () => {
|
||||
test('unset reports OFF + [] + DEFAULT', () => {
|
||||
const c = loadConfig(undefined);
|
||||
expect(c.state()).toEqual({ enabled: false, sports: [], configuration_source: 'DEFAULT' });
|
||||
const st = c.state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.enabled).toBe(false);
|
||||
expect(st.sports).toEqual([]);
|
||||
expect(st.configuration_source).toBe('DEFAULT');
|
||||
expect(st.lease_state).toBe('OFF');
|
||||
expect(st.configured).toBe(false);
|
||||
});
|
||||
|
||||
test('mlb reports enabled + ["mlb"] + ENVIRONMENT', () => {
|
||||
test('LEGACY plain `mlb` no longer activates anything', () => {
|
||||
// This is the safety repair, expressed as a test. The bare sport form used
|
||||
// to mean "write forever until somebody remembers"; it is now refused.
|
||||
const c = loadConfig('mlb');
|
||||
expect(c.state()).toEqual({ enabled: true, sports: ['mlb'], configuration_source: 'ENVIRONMENT' });
|
||||
const st = c.state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.enabled).toBe(false);
|
||||
expect(st.effective_enabled).toBe(false);
|
||||
expect(st.sports).toEqual([]);
|
||||
expect(st.lease_state).toBe('INVALID');
|
||||
expect(st.invalid_reason).toBe('INVALID_MISSING_EXPIRY');
|
||||
expect(c.isEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
|
||||
});
|
||||
|
||||
test('a bounded lease reports ACTIVE + ["mlb"] + ENVIRONMENT', () => {
|
||||
const c = loadConfig('mlb@2026-08-30T13:00:00Z');
|
||||
const st = c.state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.enabled).toBe(true);
|
||||
expect(st.sports).toEqual(['mlb']);
|
||||
expect(st.configuration_source).toBe('ENVIRONMENT');
|
||||
expect(st.lease_state).toBe('ACTIVE');
|
||||
expect(st.configured_expires_at).toBe('2026-08-30T13:00:00.000Z');
|
||||
});
|
||||
|
||||
test('an EXPLICIT empty is ENVIRONMENT, not DEFAULT', () => {
|
||||
// An operator deliberately blanking the value reads differently from never
|
||||
// having set it, even though both are dark.
|
||||
const c = loadConfig('');
|
||||
expect(c.state()).toEqual({ enabled: false, sports: [], configuration_source: 'ENVIRONMENT' });
|
||||
const st = c.state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.enabled).toBe(false);
|
||||
expect(st.sports).toEqual([]);
|
||||
expect(st.configuration_source).toBe('ENVIRONMENT');
|
||||
expect(st.lease_state).toBe('OFF');
|
||||
});
|
||||
|
||||
test('multiple sports normalise deterministically — sorted, deduped, trimmed, lowercased', () => {
|
||||
expect(loadConfig('MLB, mlb ,wnba').state().sports).toEqual(['mlb', 'wnba']);
|
||||
expect(loadConfig('wnba,mlb').state().sports).toEqual(['mlb', 'wnba']);
|
||||
expect(loadConfig(' MLB ').state().sports).toEqual(['mlb']);
|
||||
expect(loadConfig(',,mlb,,').state().sports).toEqual(['mlb']);
|
||||
test('a comma list can no longer widen the canary', () => {
|
||||
for (const v of ['MLB, mlb ,wnba', 'wnba,mlb', 'mlb@2026-08-30T13:00:00Z,nba@2026-08-30T13:00:00Z']) {
|
||||
const st = loadConfig(v).state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.effective_enabled).toBe(false);
|
||||
expect(st.sports).toEqual([]);
|
||||
expect(st.lease_state).toBe('INVALID');
|
||||
}
|
||||
});
|
||||
|
||||
test('an unsupported value behaves exactly as the gate behaves', () => {
|
||||
const c = loadConfig('cricket');
|
||||
expect(c.state().sports).toEqual(['cricket']);
|
||||
expect(c.isEnabled('cricket')).toBe(true); // the flag is scope, not validation
|
||||
expect(c.isEnabled('mlb')).toBe(false);
|
||||
test('a sport outside the leasable set cannot be leased', () => {
|
||||
const c = loadConfig('cricket@2026-08-30T13:00:00Z');
|
||||
const st = c.state(new Date('2026-08-30T12:00:00Z'));
|
||||
expect(st.lease_state).toBe('INVALID');
|
||||
expect(st.invalid_reason).toBe('INVALID_SPORT_NOT_LEASABLE');
|
||||
expect(c.isEnabled('cricket', new Date('2026-08-30T12:00:00Z'))).toBe(false);
|
||||
expect(c.isEnabled('mlb', new Date('2026-08-30T12:00:00Z'))).toBe(false);
|
||||
});
|
||||
|
||||
test('the source-code default cannot hide an environment override', () => {
|
||||
// The whole point: if Coolify sets mlb, the probe must say mlb.
|
||||
expect(loadConfig('mlb').state().enabled).toBe(true);
|
||||
expect(loadConfig(undefined).state().enabled).toBe(false);
|
||||
const now = new Date('2026-08-30T12:00:00Z');
|
||||
expect(loadConfig('mlb@2026-08-30T13:00:00Z').state(now).enabled).toBe(true);
|
||||
expect(loadConfig(undefined).state(now).enabled).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SECURITY — no raw config, no weakened access', () => {
|
||||
test('the raw environment value is never returned', () => {
|
||||
const c = loadConfig('MLB, wnba ');
|
||||
const payload = JSON.stringify(c.state());
|
||||
const payload = JSON.stringify(c.state(new Date('2026-08-30T12:00:00Z')));
|
||||
expect(payload).not.toContain('MLB, wnba');
|
||||
expect(payload).not.toContain(' ');
|
||||
});
|
||||
@@ -195,18 +227,23 @@ describe('READ ONLY — the probe observes and nothing else', () => {
|
||||
|
||||
test('it cannot enable lineage — it only reads the frozen state', () => {
|
||||
const c = loadConfig(undefined);
|
||||
const before = JSON.stringify(c.state());
|
||||
c.state(); c.state();
|
||||
expect(JSON.stringify(c.state())).toBe(before);
|
||||
const fixed = new Date('2026-08-30T12:00:00Z');
|
||||
const before = JSON.stringify(c.state(fixed));
|
||||
c.state(fixed); c.state(fixed);
|
||||
expect(JSON.stringify(c.state(fixed))).toBe(before);
|
||||
// No setter exists.
|
||||
expect(Object.keys(c).filter((k) => /^(set|enable|disable|update)/i.test(k))).toHaveLength(0);
|
||||
});
|
||||
|
||||
test('the reported state is a copy — a caller cannot mutate the gate', () => {
|
||||
const c = loadConfig('mlb');
|
||||
const s1 = c.state();
|
||||
const c = loadConfig('mlb@2026-08-30T13:00:00Z');
|
||||
const now = new Date('2026-08-30T12:00:00Z');
|
||||
const s1 = c.state(now);
|
||||
s1.sports.push('wnba');
|
||||
expect(c.state().sports).toEqual(['mlb']);
|
||||
expect(c.isEnabled('wnba')).toBe(false);
|
||||
s1.effective_active_sports.push('nba');
|
||||
expect(c.state(now).sports).toEqual(['mlb']);
|
||||
expect(c.state(now).effective_active_sports).toEqual(['mlb']);
|
||||
expect(c.isEnabled('wnba', now)).toBe(false);
|
||||
expect(c.isEnabled('nba', now)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user