feat(realism): canary cultivator on the realism contract

Stage 7 — final stage of the realism migration. Canary plants are
now scheduled by the same realism planner that handles inert content,
keeping the orchestrator as the single decision point and avoiding
duplicate diurnal / persona / rate-limit logic in the canary
subsystem.

New surface:

- decnet/canary/cultivator.py: cultivate(plan, repo) builds a
  CanaryContext, calls the right generator (canary_aws_creds ->
  aws_creds, canary_mysql_dump -> mysql_dump, …), persists the
  canary_tokens row before plant so the canary worker can attribute
  callbacks even on plant-time previews. Resolves canary placements
  to credible operator paths (~/.aws/credentials, ~/.ssh/id_rsa,
  /var/backups/db_backup.sql).
- realism/planner.py adds 8 canary content_classes uniformly weighted
  inside a 3% probability gate. Hard-capped: each tick at most one
  canary; create branch falls through to inert otherwise.
- scheduler.pick_file dispatches canary content_class to the
  cultivator; FileAction grows an optional content_bytes field so
  binary canary artifacts (DOCX/PDF/honeydoc) survive the wire
  intact instead of being utf-8 round-tripped.
- SSHDriver._run_file uses content_bytes when set, falls back to
  encoding the str content otherwise.

Stealth (per feedback_stealth.md): cultivator does not introduce
any DECNET literal; the underlying generators are already
stealth-clean and the test suite asserts the contract holds.

Tests cover round-tripping every canary class through the cultivator,
verifying placement-path conventions, persona-login normalisation
("John Smith" -> /home/johnsmith/.aws/credentials), and the
no-DECNET-leak invariant.
This commit is contained in:
2026-04-27 16:47:59 -04:00
parent 4e436da569
commit a07fb3fe08
6 changed files with 392 additions and 10 deletions

View File

@@ -80,16 +80,29 @@ def test_pick_distributes_across_user_and_system_classes() -> None:
assert system_classes, f"no system-class plans in 80 trials: {seen}"
def test_pick_never_returns_canary_class_in_stage3() -> None:
def test_canary_picks_are_rare() -> None:
"""Stage 7: canary content_classes ARE picked, but bounded.
The documented rate is ~3% of file picks per
decnet/realism/planner.py:_CANARY_PROBABILITY. We trial a large
sample and assert the rate stays under a generous ceiling so a
typo bumping the constant to 30% explodes here loudly.
"""
deckies = [_decky()]
for seed in range(40):
canary_count = 0
create_count = 0
for seed in range(500):
plan = pick(deckies, _NOW, rand=random.Random(seed))
if plan is None:
continue
assert not plan.content_class.is_canary(), (
"canary class slipped into the realism planner; cultivator "
"lands in stage 7"
)
create_count += 1
if plan.content_class.is_canary():
canary_count += 1
# 3% target with a 6% upper bound — sampling noise on 500 trials
# is comfortably below this for the documented rate.
rate = canary_count / max(1, create_count)
assert rate <= 0.06, f"canary rate {rate:.2%} exceeds 6% ceiling"
assert canary_count > 0, "expected at least one canary across 500 seeds"
def test_pick_persists_persona_window_in_notes() -> None: