From 7eb374e512ff619b67b0279c8258ec6b9cc8dfc1 Mon Sep 17 00:00:00 2001 From: Vesper Date: Thu, 20 Aug 2026 05:43:11 +0000 Subject: [PATCH 1/2] =?UTF-8?q?feat(cli):=20madp=20canary=20=E2=80=94=20on?= =?UTF-8?q?e=20turn=20through=20the=20real=20acceptance=20path=20(issue=20?= =?UTF-8?q?#5=20proposal=204)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Operationalizes the README live-smoke/canary advice as a standard command: builds a tiny two-actor/one-turn protocol in a fresh local Git repo, launches the turn through the normal runner, and validates with the production gate (--require-git --require-runner-completion). Binaries default to the shipped fakes (MADP_FAKE_BIN override); hermes-cli can probe a real installed CLI via --command-name with explicit --expected-provider/--expected-model (a canary never guesses identity). Scratch runtime state lives in a .canary-scratch sibling outside the dialogue repo so the clean-tree gate still passes; canaries are always local-only (--no-push accepted for explicitness). Canary: fakes of all three transports exit 0; a tampered published turn flips validation to non-zero with no new commit. --- README.md | 2 +- src/multi_agent_dialogue/canary.py | 296 +++++++++++++++++++++++++++++ src/multi_agent_dialogue/cli.py | 35 +++- tests/test_cli.py | 86 +++++++++ tests/test_provenance_recovery.py | 3 +- 5 files changed, 419 insertions(+), 3 deletions(-) create mode 100644 src/multi_agent_dialogue/canary.py diff --git a/README.md b/README.md index 09d546a..055d2a0 100644 --- a/README.md +++ b/README.md @@ -77,7 +77,7 @@ madp owner-decide DIR --decision DECISION.md Every accepted turn also records the engine-probed adapter CLI version (`--version` output, stored verbatim up to 500 chars with SHA-256 over the full untruncated output) as `cli_version` in the turn's evidence record, since real adapters depend on the exact installed CLI versions. The probe runs under the actor's own settings env and is informational only: a failed probe is recorded, never fatal to the turn. -Real adapters depend on the exact installed CLI versions. Run the fake demo first, then perform a harmless live smoke and bounded canary on your own machine before relying on a real transport. +Real adapters depend on the exact installed CLI versions. Run the fake demo first, then the standard canary — `madp canary --adapter hermes-cli --dialogue DIR` runs one turn through the real acceptance path in a fresh local dialogue and validates it with the production gate (shipped fakes by default; `--command-name` plus explicit `--expected-provider`/`--expected-model` probes a real installed CLI). Canaries are always local-only. ## Why trust it? diff --git a/src/multi_agent_dialogue/canary.py b/src/multi_agent_dialogue/canary.py new file mode 100644 index 0000000..fcbd09b --- /dev/null +++ b/src/multi_agent_dialogue/canary.py @@ -0,0 +1,296 @@ +"""The standard canary: one turn through the REAL acceptance path. + +`madp canary` builds a tiny local protocol (two actors, one scheduled +turn) inside a fresh Git repository, launches the single turn through +the normal runner, and validates the result with the production gate +(``--require-git --require-runner-completion``). It operationalizes the +README's "harmless live smoke and bounded canary" advice: a pass means +the engine, the adapter, the Git transaction model, and the validation +gate all work end to end on this machine. + +Default binaries are the contract-faithful fakes shipped under +``examples/fakes/bin`` (override with ``MADP_FAKE_BIN``). The hermes-cli +adapter additionally accepts a real binary via ``--command-name`` plus +explicit ``--expected-provider``/``--expected-model`` — the canary never +guesses an identity. + +Everything is local-only: the engine has no push capability, and the +scratch runtime state (actor homes, registries, tmux dirs) stays in a +sibling directory ``.canary-scratch/`` OUTSIDE the dialogue's +Git repository, so the production gate's clean-tree check still passes. +""" + +from __future__ import annotations + +import json +import os +import subprocess +from pathlib import Path + +from . import config, engine, runner + +__all__ = ["CanaryError", "run_canary"] + +CANARY_TRANSPORTS = ("command", "hermes-cli", "fable-session") + +# Identities the shipped fakes produce by default. A canary against a +# real CLI must name its expectations explicitly instead. +_FAKE_IDENTITIES = { + "command": ("madp-canary-provider", "madp-canary-model"), + "hermes-cli": ("nousresearch", "hermes-4-405b"), + "fable-session": ("anthropic", "claude-fable-5"), +} + + +class CanaryError(RuntimeError): + """The canary could not run or the acceptance path failed.""" + + +def _fake_bin() -> Path: + override = os.environ.get("MADP_FAKE_BIN", "").strip() + candidates = [] + if override: + candidates.append(Path(override)) + # Source checkout: /examples/fakes/bin relative to this package. + candidates.append( + Path(__file__).resolve().parents[2] / "examples" / "fakes" / "bin" + ) + for candidate in candidates: + if (candidate / "fake-worker").is_file(): + return candidate + raise CanaryError( + "cannot find the shipped fake executables (looked for " + + ", ".join(str(c) for c in candidates) + + "); set MADP_FAKE_BIN to examples/fakes/bin" + ) + + +def _git(dirpath: Path, *args: str) -> subprocess.CompletedProcess: + result = subprocess.run( + ["git", "-C", str(dirpath), *args], + capture_output=True, + text=True, + check=False, + ) + if result.returncode != 0: + raise CanaryError( + f"git {' '.join(args)} failed: " + f"{result.stderr.strip() or result.stdout.strip()}" + ) + return result + + +def _ensure_git_repo(dirpath: Path) -> bool: + """git init + a repo-local canary identity when none resolves. + + Returns True when a fallback identity was installed (reported in the + canary output; the engine itself never touches Git config). + """ + _git(dirpath, "init", "--quiet") + probe = subprocess.run( + ["git", "-C", str(dirpath), "var", "GIT_AUTHOR_IDENT"], + capture_output=True, + text=True, + check=False, + ) + if probe.returncode == 0 and probe.stdout.strip(): + return False + # No global identity resolves: install a repo-local canary identity + # so the engine's commits can land on identity-less machines. + _git(dirpath, "config", "user.name", "MADP Canary") + _git(dirpath, "config", "user.email", "madp-canary@example.invalid") + return True + + +def _settings( + transport: str, + *, + fakes: Path, + scratch: Path, + command_name: str | None, + expected: tuple[str, str], +) -> dict: + provider, model = expected + if transport == "command": + if command_name is not None: + raise CanaryError( + "the command transport proves identity through its " + "identity_verifier_argv; overriding the worker binary is " + "not a meaningful canary — use the shipped fakes" + ) + worker = str(fakes / "fake-worker") + verifier = str(fakes / "fake-verifier") + return { + "argv": [ + worker, "--task", "{task_file}", + "--turn-output", "{turn_file}", + "--round", "{round_id}", "--actor", "{actor_id}", + ], + "identity_verifier_argv": [ + verifier, "--turn", "{turn_file}", + "--round", "{round_id}", "--actor", "{actor_id}", + ], + "env": {"FAKE_PROVIDER": provider, "FAKE_MODEL": model}, + } + if transport == "hermes-cli": + return { + "command_name": command_name or str(fakes / "fake-hermes"), + "hermes_home": str(scratch / "hermes-home"), + } + if transport == "fable-session": + if command_name is not None: + raise CanaryError( + "fable-session canaries run against the shipped fake " + "(a real launch needs a host-local profile/registry); " + "run the live example for real-CLI smoke instead" + ) + registry = scratch / "registry.toml" + registry.write_text( + "[project.canary]\n" + f'repo = "{scratch.parent}"\n' + f'profile = "{fakes.parent / "fable-profile.toml"}"\n' + f'model = "{model}"\n' + 'effort = "high"\n' + 'fallback = "stop"\n' + 'permission_mode = "auto"\n' + 'tmux_prefix = "madpcanary-"\n', + encoding="utf-8", + ) + return { + "command_name": str(fakes / "fake-fable-session"), + "project": "canary", + "registry": str(registry), + "state_dir": str(scratch / "fable-state"), + "tmux_prefix": "madpcanary-", + "tmux_command_name": str(fakes / "fake-tmux"), + "env": {"FAKE_TMUX_DIR": str(scratch / "tmux-sessions")}, + } + raise CanaryError( + f"unknown canary transport {transport!r}; " + f"choose one of {list(CANARY_TRANSPORTS)}" + ) + + +def run_canary( + dialogue_dir: Path, + transport: str, + *, + command_name: str | None = None, + expected_provider: str | None = None, + expected_model: str | None = None, +) -> dict: + """Run one turn through the real acceptance path; return the report.""" + dialogue_dir = Path(dialogue_dir) + if transport not in CANARY_TRANSPORTS: + raise CanaryError( + f"unknown canary transport {transport!r}; " + f"choose one of {list(CANARY_TRANSPORTS)}" + ) + if command_name is not None and ( + expected_provider is None or expected_model is None + ): + raise CanaryError( + "--command-name names a real CLI; pass --expected-provider and " + "--expected-model explicitly — a canary never guesses identity" + ) + if dialogue_dir.exists() and any(dialogue_dir.iterdir()): + raise CanaryError( + f"canary dialogue path {dialogue_dir} is not empty; " + "a canary always starts from a fresh directory" + ) + dialogue_dir.mkdir(parents=True, exist_ok=True) + identity_fallback = _ensure_git_repo(dialogue_dir) + # Scratch lives OUTSIDE the dialogue's Git repository: untracked + # runtime state inside the repo would trip the production gate's + # clean-tree check. + scratch = dialogue_dir.parent / (dialogue_dir.name + ".canary-scratch") + scratch.mkdir() + + expected = ( + expected_provider or _FAKE_IDENTITIES[transport][0], + expected_model or _FAKE_IDENTITIES[transport][1], + ) + settings = _settings( + transport, + fakes=_fake_bin(), + scratch=scratch, + command_name=command_name, + expected=expected, + ) + challenger = dict(settings) + if transport == "hermes-cli": + challenger["hermes_home"] = str(scratch / "hermes-home-challenger") + raw = { + "protocol_id": f"madp-canary-{transport}", + "version": 1, + "owner": "canary-owner", + "source_sha": "0" * 40, + "evidence_roots": [], + "actors": [ + { + "actor_id": "canary-proposer", + "role": "proposer", + "transport": transport, + "expected_provider": expected[0], + "expected_model": expected[1], + "settings": settings, + }, + { + # Never scheduled: the two-actor minimum is a definition + # rule, not a second turn. + "actor_id": "canary-challenger", + "role": "challenger", + "transport": transport, + "expected_provider": expected[0], + "expected_model": expected[1], + "settings": challenger, + }, + ], + "schedule": [ + { + "round_id": "C01", + "actor_id": "canary-proposer", + "purpose": "exercise the real acceptance path end to end", + "artifact_kind": "proposal", + } + ], + "final_round_id": "C01", + } + definition = config.parse_definition(raw) + try: + dialogue = engine.init_dialogue(definition, dialogue_dir) + except (config.ConfigError, engine.ProtocolError) as exc: + raise CanaryError(f"canary init failed: {exc}") from exc + try: + runner.launch(dialogue, "canary-proposer") + except Exception as exc: + raise CanaryError( + f"canary turn REJECTED by the real acceptance path: {exc}" + ) from exc + report = dialogue.validate( + require_git=True, require_runner_completion=True + ) + record = dialogue.state()["completed_turns"][0] + evidence_file = dialogue_dir / record["evidence_file"] + turn_evidence = json.loads(evidence_file.read_text(encoding="utf-8")) + ok = bool(report["ok"]) + return { + "ok": ok, + "adapter": transport, + "dialogue": str(dialogue_dir), + "local_only": True, + "identity_fallback_installed": identity_fallback, + "turn": { + "round_id": record["round_id"], + "actor_id": record["actor_id"], + "completed_via": record.get("completed_via"), + "evidence_file": record["evidence_file"], + }, + "cli_version": turn_evidence.get("cli_version"), + "validation_ok": ok, + "validation": {k: report[k] for k in ("errors", "warnings") if k in report}, + "note": ( + "local-only: the engine never pushes; scratch runtime state " + "stays in the .canary-scratch sibling outside the Git repo" + ), + } diff --git a/src/multi_agent_dialogue/cli.py b/src/multi_agent_dialogue/cli.py index d2fe6d4..85c7f1c 100644 --- a/src/multi_agent_dialogue/cli.py +++ b/src/multi_agent_dialogue/cli.py @@ -11,7 +11,7 @@ import sys from pathlib import Path -from . import adapters, artifacts, config, engine, evidence, runner +from . import adapters, artifacts, canary, config, engine, evidence, runner def _emit(payload: dict) -> None: @@ -142,6 +142,18 @@ def cmd_owner_decide(args: argparse.Namespace) -> int: return 0 +def cmd_canary(args: argparse.Namespace) -> int: + report = canary.run_canary( + args.dialogue, + args.adapter, + command_name=args.command_name, + expected_provider=args.expected_provider, + expected_model=args.expected_model, + ) + _emit(report) + return 0 if report["ok"] else 1 + + def build_parser() -> argparse.ArgumentParser: parser = argparse.ArgumentParser( prog="madp", @@ -203,6 +215,26 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--decision", required=True, type=Path) p.set_defaults(func=cmd_owner_decide) + p = sub.add_parser( + "canary", + help="run one turn through the real acceptance path in a fresh local " + "dialogue and validate it with the production gate", + ) + p.add_argument("--adapter", required=True, choices=canary.CANARY_TRANSPORTS, + help="transport to exercise; binaries default to the " + "shipped fakes (MADP_FAKE_BIN overrides their location)") + p.add_argument("--dialogue", required=True, type=Path, + help="fresh directory for the canary dialogue (must be empty)") + p.add_argument("--command-name", default=None, + help="hermes-cli only: probe a REAL installed CLI instead of " + "the fake; requires --expected-provider/--expected-model") + p.add_argument("--expected-provider", default=None) + p.add_argument("--expected-model", default=None) + p.add_argument("--no-push", action="store_true", + help="accepted for explicitness: canaries are always " + "local-only (the engine has no push capability)") + p.set_defaults(func=cmd_canary) + return parser @@ -212,6 +244,7 @@ def main(argv: list[str] | None = None) -> int: try: return args.func(args) except ( + canary.CanaryError, config.ConfigError, engine.ProtocolError, evidence.EvidenceError, diff --git a/tests/test_cli.py b/tests/test_cli.py index ce1797b..9647df7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -253,5 +253,91 @@ def test_owner_decide_flow_and_post_final_stop(self) -> None: self.assertNotEqual(again.returncode, 0) +class CanaryTests(unittest.TestCase): + """`madp canary` exercises the real acceptance path end to end.""" + + def setUp(self) -> None: + self._tmp = tempfile.TemporaryDirectory() + self.addCleanup(self._tmp.cleanup) + self.base = Path(self._tmp.name) + + def run_canary(self, adapter: str, *extra: str, + dirname: str = "canary") -> subprocess.CompletedProcess: + return run_cli( + "canary", "--adapter", adapter, + "--dialogue", str(self.base / dirname), *extra, + ) + + def test_canary_command_adapter_passes(self) -> None: + result = self.run_canary("command", "--no-push") + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["ok"]) + self.assertTrue(payload["validation_ok"]) + self.assertEqual(payload["turn"]["completed_via"], "runner-launch") + self.assertTrue(payload["local_only"]) + cli = payload["cli_version"] + self.assertEqual(cli["exit_status"], 0) + self.assertIn("fake-worker", cli["output"]) + + def test_canary_hermes_adapter_passes(self) -> None: + result = self.run_canary("hermes-cli") + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["ok"]) + self.assertIn("fake-hermes", payload["cli_version"]["output"]) + + def test_canary_fable_adapter_passes(self) -> None: + result = self.run_canary("fable-session") + self.assertEqual(result.returncode, 0, result.stderr) + payload = json.loads(result.stdout) + self.assertTrue(payload["ok"]) + self.assertIn("fake-fable-session", payload["cli_version"]["output"]) + + def test_sabotaged_turn_fails_validation_afterwards(self) -> None: + # The canary dialogue is a real Git-backed dialogue: tampering + # with the published turn must flip the production gate to + # non-zero, with no new commit and nothing pushed anywhere. + result = self.run_canary("command") + self.assertEqual(result.returncode, 0, result.stderr) + dialogue = self.base / "canary" + turn_files = list((dialogue / "turns").glob("*.md")) + self.assertEqual(len(turn_files), 1) + before = subprocess.run( + ["git", "-C", str(dialogue), "rev-list", "--count", "HEAD"], + capture_output=True, text=True, check=True, + ).stdout.strip() + with turn_files[0].open("a", encoding="utf-8") as handle: + handle.write("\nsabotage\n") + validation = run_cli( + "validate", str(dialogue), + "--require-git", "--require-runner-completion", + ) + self.assertEqual(validation.returncode, 1) + after = subprocess.run( + ["git", "-C", str(dialogue), "rev-list", "--count", "HEAD"], + capture_output=True, text=True, check=True, + ).stdout.strip() + self.assertEqual(before, after, "validation never commits") + + def test_non_empty_dialogue_dir_is_rejected(self) -> None: + target = self.base / "occupied" + target.mkdir() + (target / "stale.txt").write_text("x", encoding="utf-8") + result = self.run_cli_dir(target) + self.assertNotEqual(result.returncode, 0) + self.assertIn("not empty", result.stderr) + + def run_cli_dir(self, path: Path) -> subprocess.CompletedProcess: + return run_cli("canary", "--adapter", "command", "--dialogue", str(path)) + + def test_real_binary_override_requires_explicit_identity(self) -> None: + result = self.run_canary( + "hermes-cli", "--command-name", "/bin/true" + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("never guesses identity", result.stderr) + + if __name__ == "__main__": unittest.main() diff --git a/tests/test_provenance_recovery.py b/tests/test_provenance_recovery.py index 1532e2a..69010ec 100644 --- a/tests/test_provenance_recovery.py +++ b/tests/test_provenance_recovery.py @@ -158,7 +158,8 @@ def test_top_level_help_has_exactly_the_public_commands(self) -> None: self.assertEqual(result.returncode, 0, result.stderr) self.assertEqual( subcommands(result.stdout), - {"init", "status", "next", "run", "validate", "owner-decide"}, + {"init", "status", "next", "run", "validate", "owner-decide", + "canary"}, "top-level madp must expose exactly the public path " "(claim/prepare/complete are recovery-only)", ) From a1641c020ddfdf02d7dc2d00f69ee85d864ec1b7 Mon Sep 17 00:00:00 2001 From: Vesper Date: Thu, 20 Aug 2026 06:05:39 +0000 Subject: [PATCH 2/2] fix(canary): fail-closed argument/scratch guards per review - stale .canary-scratch and file-as-dialogue paths raise CanaryError with clear messages instead of raw tracebacks; - --expected-provider/--expected-model without --command-name are rejected instead of silently ignored (and now carry help text); - the missing-fakes error states plainly that fakes ship with the source repo, not the installed package, and how MADP_FAKE_BIN helps; - test helper placement tidied; guards covered by new tests. --- src/multi_agent_dialogue/canary.py | 32 +++++++++++++++++++++++++----- src/multi_agent_dialogue/cli.py | 8 ++++++-- tests/test_cli.py | 24 ++++++++++++++++++++-- 3 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/multi_agent_dialogue/canary.py b/src/multi_agent_dialogue/canary.py index fcbd09b..c55075a 100644 --- a/src/multi_agent_dialogue/canary.py +++ b/src/multi_agent_dialogue/canary.py @@ -61,7 +61,10 @@ def _fake_bin() -> Path: raise CanaryError( "cannot find the shipped fake executables (looked for " + ", ".join(str(c) for c in candidates) - + "); set MADP_FAKE_BIN to examples/fakes/bin" + + "); the fakes ship with the source repository under " + "examples/fakes/bin — they are NOT part of the installed " + "package, so an installed madp needs MADP_FAKE_BIN pointed at a " + "source checkout's examples/fakes/bin" ) @@ -193,17 +196,36 @@ def run_canary( "--command-name names a real CLI; pass --expected-provider and " "--expected-model explicitly — a canary never guesses identity" ) - if dialogue_dir.exists() and any(dialogue_dir.iterdir()): + if command_name is None and ( + expected_provider is not None or expected_model is not None + ): raise CanaryError( - f"canary dialogue path {dialogue_dir} is not empty; " - "a canary always starts from a fresh directory" + "--expected-provider/--expected-model are only meaningful " + "with --command-name; without it the shipped fake's identity " + "is used and these flags would be silently ignored" + ) + if dialogue_dir.exists(): + if not dialogue_dir.is_dir(): + raise CanaryError( + f"canary dialogue path {dialogue_dir} exists and is not " + "a directory" + ) + if any(dialogue_dir.iterdir()): + raise CanaryError( + f"canary dialogue path {dialogue_dir} is not empty; " + "a canary always starts from a fresh directory" + ) + scratch = dialogue_dir.parent / (dialogue_dir.name + ".canary-scratch") + if scratch.exists(): + raise CanaryError( + f"canary scratch {scratch} already exists from a prior run; " + "remove it first — a canary never reuses stale runtime state" ) dialogue_dir.mkdir(parents=True, exist_ok=True) identity_fallback = _ensure_git_repo(dialogue_dir) # Scratch lives OUTSIDE the dialogue's Git repository: untracked # runtime state inside the repo would trip the production gate's # clean-tree check. - scratch = dialogue_dir.parent / (dialogue_dir.name + ".canary-scratch") scratch.mkdir() expected = ( diff --git a/src/multi_agent_dialogue/cli.py b/src/multi_agent_dialogue/cli.py index 85c7f1c..a6aed6a 100644 --- a/src/multi_agent_dialogue/cli.py +++ b/src/multi_agent_dialogue/cli.py @@ -228,8 +228,12 @@ def build_parser() -> argparse.ArgumentParser: p.add_argument("--command-name", default=None, help="hermes-cli only: probe a REAL installed CLI instead of " "the fake; requires --expected-provider/--expected-model") - p.add_argument("--expected-provider", default=None) - p.add_argument("--expected-model", default=None) + p.add_argument("--expected-provider", default=None, + help="identity the real CLI must prove; required with " + "--command-name (hermes-cli), meaningless without it") + p.add_argument("--expected-model", default=None, + help="identity the real CLI must prove; required with " + "--command-name (hermes-cli), meaningless without it") p.add_argument("--no-push", action="store_true", help="accepted for explicitness: canaries are always " "local-only (the engine has no push capability)") diff --git a/tests/test_cli.py b/tests/test_cli.py index 9647df7..bff9cee 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -268,6 +268,9 @@ def run_canary(self, adapter: str, *extra: str, "--dialogue", str(self.base / dirname), *extra, ) + def run_cli_dir(self, path: Path) -> subprocess.CompletedProcess: + return run_cli("canary", "--adapter", "command", "--dialogue", str(path)) + def test_canary_command_adapter_passes(self) -> None: result = self.run_canary("command", "--no-push") self.assertEqual(result.returncode, 0, result.stderr) @@ -328,8 +331,25 @@ def test_non_empty_dialogue_dir_is_rejected(self) -> None: self.assertNotEqual(result.returncode, 0) self.assertIn("not empty", result.stderr) - def run_cli_dir(self, path: Path) -> subprocess.CompletedProcess: - return run_cli("canary", "--adapter", "command", "--dialogue", str(path)) + def test_dialogue_path_that_is_a_file_is_rejected(self) -> None: + target = self.base / "a-file" + target.write_text("x", encoding="utf-8") + result = self.run_cli_dir(target) + self.assertNotEqual(result.returncode, 0) + self.assertIn("not a directory", result.stderr) + + def test_stale_scratch_dir_is_rejected(self) -> None: + (self.base / "canary.canary-scratch").mkdir() + result = self.run_canary("command") + self.assertNotEqual(result.returncode, 0) + self.assertIn("already exists", result.stderr) + + def test_expected_identity_flags_require_command_name(self) -> None: + result = self.run_canary( + "hermes-cli", "--expected-model", "some-model" + ) + self.assertNotEqual(result.returncode, 0) + self.assertIn("only meaningful", result.stderr) def test_real_binary_override_requires_explicit_identity(self) -> None: result = self.run_canary(