From 8657e87e99a960cf5f1826c5f5581da88c4b4888 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 23:38:25 -0400 Subject: [PATCH 1/2] feat(history): verify explicit weekly calendar occurrences --- docs/calendar-signed-history.md | 57 ++++++++++ docs/demos/calendar-history.log | 9 ++ docs/demos/calendar-history.timing | 4 + scripts/demo-calendar-history.py | 29 ++++++ shuttle/calendar_history.py | 130 +++++++++++++++++++++++ tests/test_calendar_history.py | 162 +++++++++++++++++++++++++++++ 6 files changed, 391 insertions(+) create mode 100644 docs/calendar-signed-history.md create mode 100644 docs/demos/calendar-history.log create mode 100644 docs/demos/calendar-history.timing create mode 100644 scripts/demo-calendar-history.py create mode 100644 shuttle/calendar_history.py create mode 100644 tests/test_calendar_history.py diff --git a/docs/calendar-signed-history.md b/docs/calendar-signed-history.md new file mode 100644 index 0000000..5ab8d81 --- /dev/null +++ b/docs/calendar-signed-history.md @@ -0,0 +1,57 @@ +# Opt-in calendar signed history + +`shuttle.calendar_history.CalendarScheduledHistory` is separate from the fixed +`ScheduledHistory` path. It adds no CLI, timer, polling, dispatch, private-key +access, graph mutation, profile or runtime activation. The fixed reader and its +9l1 contract are unchanged. + +Pin immutable scope, public key/registration/identity graph, weekday (Monday=0), +HH:MM local time, IANA zone, activation boundary, signed completion budget and +maximum start lateness. Supply current declaration eligibility through the same +bounded reader contract. The caller must establish declaration coverage and the +natural origin of its successful control; signatures cannot establish those. + +Construct `CalendarOccurrence(scheduled_at, next_deadline, zone)` from the +admitted Chaski WeeklyProjection seam: decode the exact `event_id` run slot, +use `next_checks[job]`, and retain the pinned projection timezone. The current +projection JSON does not itself expose a flat occurrence object. `scheduled_at` is the integer UTC slot; +`next_deadline` is the next **occurrence**, not the completion deadline. Chaski +selects occurrences. This reader checks each supplied occurrence against pinned +local weekday/time and verifies the next local-week date, without deriving a +604800-second cadence. Both sides of a DST overlap and nonexistent local times +refuse. An occurrence whose following date is ambiguous also refuses. + +Call `measure(now=utc_integer, occurrence=target, positive=prior_occurrence)`. +A stale target at or beyond next occurrence plus completion budget is UNKNOWN. +Before the target completion budget expires, it returns NOT_DUE. At the exact +budget boundary it reads only the pinned identity graph and explicit monthly +run graph for two deterministic run identities. A successful signed control is +required before a missing target can be MISSED. SUCCEEDED/FAILED/HELD/LATE remain +separate; malformed, partial, duplicated, truncated or unavailable evidence is +UNKNOWN. Exceptions report only their class, never remote text. + +Target and control must cover the same month, including their start and +completion windows. October26 to November2 crosses months and is UNKNOWN, +even though the UTC occurrence arithmetic is valid. Refresh the retained +control each month. Never broaden a query or substitute another graph after a +zero. The reader reuses existing public registration/signature/event verification +through composition; it never calls the fixed reader's cadence-based measure. +A private compatibility interval used for verifier construction is not evidence +of calendar coverage and is never used for slot selection. + +The seam is explicit input, not authenticated producer provenance. Runtime +admission must bind the actual Chaski projection, zone database, declaration, +config and scope. Query and local clocks are caller responsibilities. There is +no ingestion timestamp: newly published on-time signed completion can resolve +MISSED but cannot reconstruct when publication arrived. This is the latest +supplied deadline window, not an audit of every historic slot. + +## Demonstration + +Run `python scripts/demo-calendar-history.py`. It uses temporary fixture keys +and explicitly mocked graph transport. Watch missing target become MISSED only +with a valid signed control, then SUCCEEDED after signed publication; damaged +control becomes UNKNOWN. The October26 occurrence has a next UTC difference of +608400 seconds across DST. This proves source behavior, not installation, +natural firing or alert delivery. The recording and timing live in +`docs/demos/calendar-history.log` and `.timing`; download and use `scriptreplay`. diff --git a/docs/demos/calendar-history.log b/docs/demos/calendar-history.log new file mode 100644 index 0000000..2ed022f --- /dev/null +++ b/docs/demos/calendar-history.log @@ -0,0 +1,9 @@ +Script started on 2026-10-20 23:37:12-04:00 [COMMAND="python3 scripts/demo-calendar-history.py" ] +Explicit occurrence: CalendarOccurrence(scheduled_at=1793020620, next_deadline=1793629020, zone='America/New_York') +UTC difference: 608400 +Signed control, missing target: MISSED +Signed target: SUCCEEDED +Empty history: UNKNOWN +Temporary keys + mocked transport only; not natural/runtime acceptance. + +Script done on 2026-10-20 23:37:12-04:00 [COMMAND_EXIT_CODE="0"] diff --git a/docs/demos/calendar-history.timing b/docs/demos/calendar-history.timing new file mode 100644 index 0000000..97e8e7e --- /dev/null +++ b/docs/demos/calendar-history.timing @@ -0,0 +1,4 @@ +0.078410 141 +0.001954 40 +0.007222 26 +0.000189 97 diff --git a/scripts/demo-calendar-history.py b/scripts/demo-calendar-history.py new file mode 100644 index 0000000..df505a5 --- /dev/null +++ b/scripts/demo-calendar-history.py @@ -0,0 +1,29 @@ +#!/usr/bin/env python3 +"""Real temporary fixture signatures, mocked graph; no runtime action.""" +from pathlib import Path +import sys +import warnings + +sys.path.insert(0, str(Path(__file__).resolve().parents[1])) +from tests.test_calendar_history import CalendarHistoryTests + + +def main(): + warnings.simplefilter('ignore', ResourceWarning) + fixture = CalendarHistoryTests() + fixture.setUp() + try: + print('Explicit occurrence:', fixture.target) + print('UTC difference:', fixture.target.next_deadline-fixture.target.scheduled_at) + print('Signed control, missing target:', fixture.measure()['verdict']) + fixture.publish(fixture.slot) + print('Signed target:', fixture.measure()['verdict']) + fixture.f.facts.clear() + print('Empty history:', fixture.measure()['verdict']) + print('Temporary keys + mocked transport only; not natural/runtime acceptance.') + finally: + fixture.doCleanups() + + +if __name__ == '__main__': + main() diff --git a/shuttle/calendar_history.py b/shuttle/calendar_history.py new file mode 100644 index 0000000..097b1e7 --- /dev/null +++ b/shuttle/calendar_history.py @@ -0,0 +1,130 @@ +"""Opt-in calendar signed history; no timer, private key or mutation. + +Chaski owns occurrence selection. This reader validates explicit occurrence +coverage against pinned weekly policy, then reuses fixed-reader signature +verification without calling its fixed-cadence measure method. +""" +from dataclasses import dataclass +from datetime import datetime, timedelta, timezone +import re +from zoneinfo import ZoneInfo + +from . import export, windows +from .scheduled import ScheduledRefused, _rows +from .scheduled_history import ScheduledHistory + + +@dataclass(frozen=True) +class CalendarOccurrence: + scheduled_at: int + next_deadline: int + zone: str + + +class CalendarScheduledHistory: + """Explicit Chaski WeeklyProjection seam; next_deadline is next occurrence. + + deadline_s is the separate signed completion budget. Supplied occurrences + are not authenticated by this class; the caller must retain their admitted + Chaski/declaration provenance and the control's natural origin. + """ + def __init__(self, binding, *, weekday, local_time, zone, effective_from_s, + public_key, registration, identity_graph, deadline_s, + max_lateness_s, eligible, query=None): + if (type(weekday) is not int or not 0 <= weekday <= 6 or + type(local_time) is not str or + not re.fullmatch(r'(?:[01][0-9]|2[0-3]):[0-5][0-9]', local_time) or + type(zone) is not str or not 0 < len(zone) <= 255): + raise ScheduledRefused('pinned weekday/HH:MM/IANA zone required') + self.zone = ZoneInfo(zone) + self.zone_name, self.weekday = zone, weekday + self.hour, self.minute = map(int, local_time.split(':')) + self.effective = effective_from_s + # Only reuse exact public-key/registration/event verifiers. The fixed + # cadence below is NEVER used to derive or validate calendar coverage. + self.verifier = ScheduledHistory(binding, public_key=public_key, + registration=registration, identity_graph=identity_graph, + anchor_s=effective_from_s, interval_s=60, effective_from_s=effective_from_s, + deadline_s=deadline_s, max_lateness_s=max_lateness_s, + eligible=eligible, query=query) + + def _utc(self, day): + local = datetime(day.year, day.month, day.day, self.hour, self.minute) + candidates = set() + for fold in (0, 1): + utc = local.replace(tzinfo=self.zone, fold=fold).astimezone(timezone.utc) + if utc.astimezone(self.zone).replace(tzinfo=None) == local: + candidates.add(int(utc.timestamp())) + if len(candidates) != 1: + raise ScheduledRefused('gap or overlap in calendar occurrence') + return candidates.pop() + + def _occurrence(self, value): + if (type(value) is not CalendarOccurrence or value.zone != self.zone_name or + any(type(s) is not int or not 0 <= s <= 253402214400 + for s in (value.scheduled_at, value.next_deadline)) or + value.scheduled_at < self.effective): + raise ScheduledRefused('bounded pinned-zone occurrence required') + day = datetime.fromtimestamp(value.scheduled_at, self.zone).date() + if (day.weekday() != self.weekday or self._utc(day) != value.scheduled_at or + self._utc(day + timedelta(days=7)) != value.next_deadline): + raise ScheduledRefused('canonical occurrence and next local week required') + return value.scheduled_at + + def measure(self, *, now, occurrence, positive): + """Latest deadline-passed explicit occurrence, never fixed-week inference.""" + slot = None + v = self.verifier + try: + if type(now) is not int or not 0 <= now <= 253402214400: + raise ScheduledRefused('bounded integer UTC clock required') + slot = self._occurrence(occurrence) + control = self._occurrence(positive) + if (export.entity_ns(), windows.window_ns()) != (v.namespace, v.window_namespace): + raise ScheduledRefused('namespace changed during measurement') + if now < slot: + raise ScheduledRefused('future occurrence does not cover current time') + if control >= slot: + raise ScheduledRefused('prior natural signed positive control required') + if now >= occurrence.next_deadline + v.deadline: + raise ScheduledRefused('stale occurrence does not cover current deadline') + month = v._month(slot) + # Cover the entire signed start/completion windows, not merely the + # scheduled instants. Never broaden into another graph on a zero. + if any(v._month(s + offset) != month + for s in (control, slot) for offset in (0, v.lateness, v.deadline)): + raise ScheduledRefused('same-month control and completion coverage required') + if now < slot + v.deadline: + return {'verdict': 'NOT_DUE', 'slot': None} + if any(v.eligible(dict(v.binding), s) is not True for s in (control, slot)): + raise ScheduledRefused('current declaration eligibility unavailable') + a = export.AEGIS + registration = _rows(v.query( + f'SELECT ?verifier ?pk ?workflow (isIRI(?workflow) AS ?scopeIRI) WHERE {{ ' + f'<{v.registration}> a ?type ; <{a}verifier> ?verifier ; ' + f'<{a}publicKey> ?pk ; <{a}attests> ?workflow . ' + f'FILTER(?type = <{a}VerifierRegistration>) }} LIMIT 8', + graph=v.identity_graph), 8) + if registration != [{'verifier': v.binding['agent'], 'pk': v.public_key, + 'workflow': v.binding['workflow'], 'scopeIRI': True}] or \ + registration[0].get('scopeIRI') is not True: + raise ScheduledRefused('exact public registration unavailable') + runs = [v._run(s) for s in (control, slot)] + rows = _rows(v.query( + 'SELECT (STR(?event) AS ?ev) (STR(?predicate) AS ?p) ' + '(STR(?raw) AS ?value) (isIRI(?raw) AS ?iri) ' + '(STR(DATATYPE(?raw)) AS ?datatype) WHERE { VALUES ?run { ' + + ' '.join('<'+export.run_iri(run)+'>' for run in runs) + ' } ' + + f'?event <{a}inRun> ?run ; ?predicate ?raw }} LIMIT 64', + graph=windows.window_iri(month)), 64) + grouped = [[r for r in rows if r.get('ev') in + (export.event_iri(run, 2), export.event_iri(run, 3))] for run in runs] + if sum(map(len, grouped)) != len(rows): + raise ScheduledRefused('unexpected event outside exact coverage') + if v._outcome(grouped[0], control, now) != 'SUCCEEDED': + raise ScheduledRefused('successful signed positive control unavailable') + return {'verdict': v._outcome(grouped[1], slot, now), 'slot': slot, + 'positive_slot': control, 'graph': windows.window_iri(month), + 'next_deadline': occurrence.next_deadline, 'zone': self.zone_name} + except Exception as exc: + return {'verdict': 'UNKNOWN', 'slot': slot, 'reason': type(exc).__name__} diff --git a/tests/test_calendar_history.py b/tests/test_calendar_history.py new file mode 100644 index 0000000..1526540 --- /dev/null +++ b/tests/test_calendar_history.py @@ -0,0 +1,162 @@ +"""Real temporary signatures; remote transport explicitly mocked.""" +from dataclasses import asdict, replace +from datetime import datetime +import re +import unittest +from unittest import mock + +from shuttle import export, signing, windows +from shuttle.calendar_history import CalendarOccurrence, CalendarScheduledHistory +from tests import test_scheduled as fixtures + + +def at(value): + return int(datetime.fromisoformat(value).timestamp()) + + +class CalendarHistoryTests(unittest.TestCase): + def setUp(self): + self.f = fixtures.ScheduledPortTests() + self.f.query = self.query + self.f.setUp() + self.addCleanup(self.f.doCleanups) + self.truncated, self.extra, self.failure = False, [], None + self.control = at('2026-10-19T13:17:00+00:00') + self.slot = at('2026-10-26T13:17:00+00:00') + self.next = at('2026-11-02T14:17:00+00:00') + self.reader = CalendarScheduledHistory(asdict(self.f.scope), weekday=0, + local_time='09:17', zone='America/New_York', effective_from_s=self.control, + public_key=self.f.pk, registration='urn:example:registration', + identity_graph='urn:example:identity', deadline_s=20, max_lateness_s=10, + eligible=lambda *_: self.f.eligible, query=self.query) + self.positive = CalendarOccurrence(self.control, self.slot, 'America/New_York') + self.target = CalendarOccurrence(self.slot, self.next, 'America/New_York') + self.publish(self.control) + + def publish(self, slot, outcome='succeeded', offset=2): + f = self.f + f.slot, f.run = slot, self.reader.verifier._run(slot) + f.now = slot + 1 + f.port.root = f.base/('run-'+str(slot)+'-'+outcome+'-'+str(offset)) + f.start() + f.now = slot + offset - 1 + result = f.finish(outcome, 1 if outcome == 'failed' else 0) + f.port.publish(f.run, f.scope, result) + + def query(self, text, graph): + f = self.f + f.queries.append((text, graph)) + if self.failure: + raise self.failure + if '?verifier' in text: + self.assertEqual(graph, 'urn:example:identity') + rows = [dict(f.registration)] + else: + self.assertEqual(graph, windows.window_iri('2026-10')) + requested = set(re.findall(r'<(urn:shuttle:run:[^>]+)>', text)) + events = {r['ev'] for r in f.facts.values() + if r['p'] == export.AEGIS+'inRun' and r['value'] in requested} + rows = [dict(r) for r in f.facts.values() if r['ev'] in events] + self.extra + return {'rows': rows, 'count': len(rows), 'truncated': self.truncated} + + def measure(self, **changes): + args = dict(now=self.slot+20, occurrence=self.target, positive=self.positive) + args.update(changes) + posts = len(self.f.posts) + with mock.patch.object(signing, 'load_or_generate', side_effect=AssertionError('private key')), \ + mock.patch.object(signing, 'load_existing', side_effect=AssertionError('private key')): + result = self.reader.measure(**args) + self.assertEqual(len(self.f.posts), posts) + return result + + def test_paired_missing_and_signed_target(self): + self.assertEqual(self.measure()['verdict'], 'MISSED') + self.publish(self.slot) + result = self.measure() + self.assertEqual(result['verdict'], 'SUCCEEDED') + self.assertEqual(result['next_deadline']-result['slot'], 608400) + + def test_failed_held_late_and_running(self): + for outcome, offset, expected in [('failed', 2, 'FAILED'), + ('skipped-by-hold', 2, 'HELD'), ('succeeded', 21, 'LATE')]: + with self.subTest(outcome=outcome): + self.publish(self.slot, outcome, offset) + self.assertEqual(self.measure(now=self.slot+22)['verdict'], expected) + end = export.event_iri(self.reader.verifier._run(self.slot), 3) + self.f.facts = {k:v for k,v in self.f.facts.items() if v['ev'] != end} + self.assertEqual(self.measure()['verdict'], 'MISSED') + + def test_empty_partial_tampered_duplicate_truncated_and_transport(self): + original = dict(self.f.facts) + for key in original: + with self.subTest(key=key): + self.f.facts = dict(original) + del self.f.facts[key] + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.f.facts = dict(original) + key = next(k for k in original if k[1] == export.AEGIS+'signature') + self.f.facts[key] = dict(original[key], value='0'*128) + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.f.facts = original + self.extra = [dict(next(iter(original.values())))] + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.extra, self.truncated = [], True + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.truncated, self.failure = False, TimeoutError('secret must not leak') + self.assertNotIn('secret', str(self.measure())) + self.failure = None + self.f.facts = {} + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + + def test_eligibility_registration_and_namespace(self): + for value in (False, None, 1, 'true'): + self.f.eligible = value + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.f.eligible = True + self.f.registration['scopeIRI'] = 1 + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + self.f.registration['scopeIRI'] = True + with mock.patch.object(export, 'entity_ns', return_value='urn:changed:'): + self.assertEqual(self.measure()['verdict'], 'UNKNOWN') + + def test_invalid_or_stale_occurrences_refuse_without_queries(self): + count = len(self.f.queries) + for target in (replace(self.target, zone='UTC'), replace(self.target, scheduled_at=True), + replace(self.target, next_deadline=self.slot+604800), + replace(self.target, scheduled_at=self.slot+1)): + self.assertEqual(self.measure(occurrence=target)['verdict'], 'UNKNOWN') + self.assertEqual(self.measure(now=self.next+20)['verdict'], 'UNKNOWN') + self.assertEqual(self.measure(positive=self.target)['verdict'], 'UNKNOWN') + self.assertEqual(self.measure(now=True)['verdict'], 'UNKNOWN') + self.assertEqual(self.measure(now=self.slot-1)['verdict'], 'UNKNOWN') + self.assertEqual(len(self.f.queries), count) + self.assertEqual(self.measure(now=self.slot+19)['verdict'], 'NOT_DUE') + self.assertEqual(len(self.f.queries), count) + + def test_cross_month_and_completion_window_unknown(self): + count = len(self.f.queries) + nov = CalendarOccurrence(self.next, at('2026-11-09T14:17:00+00:00'), self.target.zone) + self.assertEqual(self.measure(now=self.next+20, occurrence=nov, positive=self.target)['verdict'], 'UNKNOWN') + self.reader.verifier.deadline = 604800 + self.assertEqual(self.measure(now=self.slot+604800)['verdict'], 'UNKNOWN') + self.assertEqual(len(self.f.queries), count) + + def test_dst_gap_both_fold_and_spring(self): + # Validate explicit source occurrences independently of graph month coverage. + self.reader.effective = 0 + spring = CalendarOccurrence(at('2026-03-02T14:17:00+00:00'), + at('2026-03-09T13:17:00+00:00'), self.target.zone) + self.assertEqual(self.reader._occurrence(spring), spring.scheduled_at) + self.assertEqual(spring.next_deadline-spring.scheduled_at, 601200) + self.reader.weekday, self.reader.hour, self.reader.minute = 6, 1, 30 + for stamp in ('2026-11-01T05:30:00+00:00', '2026-11-01T06:30:00+00:00'): + with self.assertRaises(Exception): + self.reader._occurrence(CalendarOccurrence(at(stamp), at('2026-11-08T06:30:00+00:00'), self.target.zone)) + self.reader.hour = 2 + for stamp in ('2026-03-08T06:30:00+00:00', '2026-03-08T07:30:00+00:00'): + with self.assertRaises(Exception): + self.reader._occurrence(CalendarOccurrence(at(stamp), at('2026-03-15T06:30:00+00:00'), self.target.zone)) + + +if __name__ == '__main__': + unittest.main() From e55c801066fc3b0d1a139f447e657b93a8926680 Mon Sep 17 00:00:00 2001 From: Steve Brown Date: Tue, 20 Oct 2026 23:39:59 -0400 Subject: [PATCH 2/2] test(history): forbid fixed cadence measurement in calendar path --- tests/test_calendar_history.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_calendar_history.py b/tests/test_calendar_history.py index 1526540..2d5ff86 100644 --- a/tests/test_calendar_history.py +++ b/tests/test_calendar_history.py @@ -63,7 +63,8 @@ def measure(self, **changes): args = dict(now=self.slot+20, occurrence=self.target, positive=self.positive) args.update(changes) posts = len(self.f.posts) - with mock.patch.object(signing, 'load_or_generate', side_effect=AssertionError('private key')), \ + with mock.patch.object(self.reader.verifier, 'measure', side_effect=AssertionError('fixed cadence')), \ + mock.patch.object(signing, 'load_or_generate', side_effect=AssertionError('private key')), \ mock.patch.object(signing, 'load_existing', side_effect=AssertionError('private key')): result = self.reader.measure(**args) self.assertEqual(len(self.f.posts), posts)