From edb213b9b4d61733ef5749c8a6303e3e7d522fa2 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:26:00 +0900 Subject: [PATCH 1/7] Add bounded self-human live caregiver bridge --- src/sudachi_life/phase3/human_live.py | 335 ++++++++++++++++++++++++++ 1 file changed, 335 insertions(+) create mode 100644 src/sudachi_life/phase3/human_live.py diff --git a/src/sudachi_life/phase3/human_live.py b/src/sudachi_life/phase3/human_live.py new file mode 100644 index 0000000..d8e02f6 --- /dev/null +++ b/src/sudachi_life/phase3/human_live.py @@ -0,0 +1,335 @@ +from __future__ import annotations + +from dataclasses import dataclass, replace +from datetime import datetime + +from .caregiver import ( + AccountingStatus, + CaregiverAccounting, + CaregiverProposal, + CaregiverRequest, + CaregiverSourceKind, + ProposalAuthority, + proposal_from_text, + validate_proposal, +) +from .human_pilot import ( + PROPOSED_MAX_ATTEMPT_WALL_MS, + PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT, + PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, + PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT, + PROPOSED_MAX_RESPONSE_LATENCY_MS, + PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS, + EthicsReviewStatus, + HumanProposalDraft, + validate_human_proposal_draft, +) + +HUMAN_PILOT_LIVE_BRIDGE_VERSION = "sudachi.phase3.human_caregiver_live_bridge/v1" +SELF_HUMAN_PILOT_ID = "pilot:human-caregiver-v1-self" +LOCAL_STRUCTURED_HUMAN_TRANSPORT = "local_structured_manual" + + +@dataclass(frozen=True, slots=True) +class SelfHumanPilotAuthorization: + protocol_version: str + pilot_id: str + attempt_id: str + caregiver_id: str + source_kind: CaregiverSourceKind + transport: str + project_owner_is_caregiver: bool + self_only: bool + ethics_review_status: EthicsReviewStatus + consent_attested: bool + consent_revocable: bool + raw_chat_transcript_retained: bool + public_raw_payload_default: bool + proposal_payload_retention_days: int + third_party_participants: int + + +@dataclass(frozen=True, slots=True) +class HumanPilotAttemptState: + consultations_used: int = 0 + clarifications_used: int = 0 + caregiver_active_ms_used: int = 0 + attempt_elapsed_ms: int = 0 + live_enabled: bool = True + disabled_at_elapsed_ms: int | None = None + + +@dataclass(frozen=True, slots=True) +class HumanConsultationMeasurement: + latency_ms: int + caregiver_active_ms: int + attempt_elapsed_ms: int + source_timestamp: str + is_clarification: bool = False + + +@dataclass(frozen=True, slots=True) +class HumanBridgeResult: + accepted: bool + errors: tuple[str, ...] + proposal: CaregiverProposal | None + state: HumanPilotAttemptState + + +def _is_pseudonymous_caregiver_id(value: str) -> bool: + prefix = "caregiver:pseudo:" + if not value.startswith(prefix): + return False + token = value[len(prefix) :] + return len(token) == 32 and all(ch in "0123456789abcdef" for ch in token) + + +def _is_offset_timestamp(value: str) -> bool: + if not value: + return False + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError: + return False + return parsed.tzinfo is not None and parsed.utcoffset() is not None + + +def accepted_self_human_pilot_v1_authorization( + *, + attempt_id: str, + caregiver_id: str, +) -> SelfHumanPilotAuthorization: + """Return the owner-approved self-only Pilot v1 live authorization record. + + This function does not perform I/O or contact any external service. The only + live source it authorizes is the project owner supplying a structured draft + locally for the single bound attempt. + """ + return SelfHumanPilotAuthorization( + protocol_version=HUMAN_PILOT_LIVE_BRIDGE_VERSION, + pilot_id=SELF_HUMAN_PILOT_ID, + attempt_id=attempt_id, + caregiver_id=caregiver_id, + source_kind=CaregiverSourceKind.HUMAN, + transport=LOCAL_STRUCTURED_HUMAN_TRANSPORT, + project_owner_is_caregiver=True, + self_only=True, + ethics_review_status=EthicsReviewStatus.NOT_REQUIRED_DECLARED, + consent_attested=True, + consent_revocable=True, + raw_chat_transcript_retained=False, + public_raw_payload_default=False, + proposal_payload_retention_days=PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS, + third_party_participants=0, + ) + + +def validate_self_human_pilot_authorization( + authorization: SelfHumanPilotAuthorization, +) -> tuple[str, ...]: + errors: list[str] = [] + + if authorization.protocol_version != HUMAN_PILOT_LIVE_BRIDGE_VERSION: + errors.append("authorization.protocol_version") + if authorization.pilot_id != SELF_HUMAN_PILOT_ID: + errors.append("authorization.pilot_id") + if not authorization.attempt_id: + errors.append("authorization.attempt_id") + if not _is_pseudonymous_caregiver_id(authorization.caregiver_id): + errors.append("authorization.caregiver_id") + if authorization.source_kind is not CaregiverSourceKind.HUMAN: + errors.append("authorization.source_kind") + if authorization.transport != LOCAL_STRUCTURED_HUMAN_TRANSPORT: + errors.append("authorization.transport") + if not authorization.project_owner_is_caregiver: + errors.append("authorization.owner_is_caregiver") + if not authorization.self_only: + errors.append("authorization.self_only") + if authorization.ethics_review_status is not EthicsReviewStatus.NOT_REQUIRED_DECLARED: + errors.append("authorization.ethics_review_status") + if not authorization.consent_attested: + errors.append("authorization.consent_attested") + if not authorization.consent_revocable: + errors.append("authorization.consent_revocable") + if authorization.raw_chat_transcript_retained: + errors.append("authorization.raw_chat_transcript") + if authorization.public_raw_payload_default: + errors.append("authorization.public_raw_payload") + if authorization.proposal_payload_retention_days != PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS: + errors.append("authorization.retention_days") + if authorization.third_party_participants != 0: + errors.append("authorization.third_party_participants") + + return tuple(errors) + + +def start_self_human_pilot_attempt() -> HumanPilotAttemptState: + return HumanPilotAttemptState() + + +def disable_self_human_bridge( + state: HumanPilotAttemptState, + *, + attempt_elapsed_ms: int, +) -> HumanPilotAttemptState: + if attempt_elapsed_ms < state.attempt_elapsed_ms: + raise ValueError("disablement elapsed time cannot move backwards") + if attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: + raise ValueError("disablement exceeds attempt wall budget") + return replace( + state, + attempt_elapsed_ms=attempt_elapsed_ms, + live_enabled=False, + disabled_at_elapsed_ms=attempt_elapsed_ms, + ) + + +def accept_self_human_proposal( + *, + authorization: SelfHumanPilotAuthorization, + state: HumanPilotAttemptState, + request: CaregiverRequest, + draft: HumanProposalDraft, + measurement: HumanConsultationMeasurement, + allowed_observation_ids: frozenset[str] = frozenset(), + allowed_objective_ids: frozenset[str] = frozenset(), + allowed_action_ids: frozenset[str] = frozenset(), +) -> HumanBridgeResult: + """Convert one validated self-caregiver draft into a proposal-only live proposal. + + The bridge performs no network, subprocess, browser, credential, filesystem, + or external-service access. A caller supplies the already human-attested + structured draft and measured timing values locally. + """ + errors = list(validate_self_human_pilot_authorization(authorization)) + + if not state.live_enabled: + errors.append("bridge.disabled") + if state.disabled_at_elapsed_ms is not None: + errors.append("bridge.disablement_state") + if request.attempt_id != authorization.attempt_id: + errors.append("bridge.attempt_binding") + if draft.caregiver_id != authorization.caregiver_id: + errors.append("bridge.caregiver_binding") + + draft_validation = validate_human_proposal_draft( + request, + draft, + allowed_observation_ids=allowed_observation_ids, + allowed_objective_ids=allowed_objective_ids, + allowed_action_ids=allowed_action_ids, + ) + errors.extend(f"bridge.{error}" for error in draft_validation.errors) + + if measurement.latency_ms < 0: + errors.append("bridge.latency_negative") + elif measurement.latency_ms > PROPOSED_MAX_RESPONSE_LATENCY_MS: + errors.append("bridge.latency_budget") + if measurement.caregiver_active_ms < 0: + errors.append("bridge.active_ms_negative") + if measurement.attempt_elapsed_ms < state.attempt_elapsed_ms: + errors.append("bridge.elapsed_time_regression") + elif measurement.attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: + errors.append("bridge.attempt_wall_budget") + if not _is_offset_timestamp(measurement.source_timestamp): + errors.append("bridge.source_timestamp") + + is_linked_clarification = draft.clarification_of_proposal_id is not None + if measurement.is_clarification != is_linked_clarification: + errors.append("bridge.clarification_binding") + + next_consultations = state.consultations_used + 1 + next_clarifications = state.clarifications_used + int(measurement.is_clarification) + next_active_ms = state.caregiver_active_ms_used + measurement.caregiver_active_ms + + if next_consultations > PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT: + errors.append("bridge.consultation_budget") + if next_clarifications > PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT: + errors.append("bridge.clarification_budget") + if next_active_ms > PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT: + errors.append("bridge.active_time_budget") + + if errors: + return HumanBridgeResult( + accepted=False, + errors=tuple(errors), + proposal=None, + state=state, + ) + + accounting = CaregiverAccounting( + consultation_count=1, + clarification_count=int(measurement.is_clarification), + latency_ms=measurement.latency_ms, + human_active_ms=measurement.caregiver_active_ms, + model_calls=None, + money_minor_units=None, + live_cost_status=AccountingStatus.MEASURED, + absence_reason=None, + ) + proposal = proposal_from_text( + request=request, + source_id=authorization.caregiver_id, + source_kind=CaregiverSourceKind.HUMAN, + kind=draft.kind, + text=draft.text, + accounting=accounting, + source_timestamp=measurement.source_timestamp, + ) + + # The generic source-neutral validator deliberately remains fixture-only. + # A human proposal is acceptable here only if its sole generic-validation + # failure is exactly the expected live-source authorization error and every + # self-pilot gate above has passed. + generic_validation = validate_proposal(request, proposal) + unexpected_generic_errors = tuple( + error + for error in generic_validation.errors + if error != "proposal.live_source_not_authorized" + ) + if "proposal.live_source_not_authorized" not in generic_validation.errors: + unexpected_generic_errors += ("proposal.expected_global_rejection_missing",) + if unexpected_generic_errors: + return HumanBridgeResult( + accepted=False, + errors=tuple(f"bridge.{error}" for error in unexpected_generic_errors), + proposal=None, + state=state, + ) + + if proposal.authority is not ProposalAuthority.PROPOSAL_ONLY: + return HumanBridgeResult( + accepted=False, + errors=("bridge.proposal_authority",), + proposal=None, + state=state, + ) + if proposal.source_kind is not CaregiverSourceKind.HUMAN: + return HumanBridgeResult( + accepted=False, + errors=("bridge.proposal_source_kind",), + proposal=None, + state=state, + ) + if draft_validation.payload_sha256 != proposal.payload_sha256: + return HumanBridgeResult( + accepted=False, + errors=("bridge.payload_digest",), + proposal=None, + state=state, + ) + + next_state = HumanPilotAttemptState( + consultations_used=next_consultations, + clarifications_used=next_clarifications, + caregiver_active_ms_used=next_active_ms, + attempt_elapsed_ms=measurement.attempt_elapsed_ms, + live_enabled=True, + disabled_at_elapsed_ms=None, + ) + return HumanBridgeResult( + accepted=True, + errors=(), + proposal=proposal, + state=next_state, + ) From 05b3b743cd05e3532b02f8c4371c57a662d6af72 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:26:54 +0900 Subject: [PATCH 2/7] Add self-human live bridge regressions --- tests/test_phase3_self_human_live_bridge.py | 328 ++++++++++++++++++++ 1 file changed, 328 insertions(+) create mode 100644 tests/test_phase3_self_human_live_bridge.py diff --git a/tests/test_phase3_self_human_live_bridge.py b/tests/test_phase3_self_human_live_bridge.py new file mode 100644 index 0000000..2c9fa3a --- /dev/null +++ b/tests/test_phase3_self_human_live_bridge.py @@ -0,0 +1,328 @@ +from __future__ import annotations + +from dataclasses import replace + +import pytest + +from sudachi_life.phase3.caregiver import ( + ACTIVE_SOURCE_KINDS, + AccountingStatus, + CaregiverRequest, + CaregiverSourceKind, + ProposalAuthority, + ProposalKind, + validate_proposal, +) +from sudachi_life.phase3.human_live import ( + HUMAN_PILOT_LIVE_BRIDGE_VERSION, + LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_PILOT_ID, + HumanConsultationMeasurement, + HumanPilotAttemptState, + accept_self_human_proposal, + accepted_self_human_pilot_v1_authorization, + disable_self_human_bridge, + start_self_human_pilot_attempt, + validate_self_human_pilot_authorization, +) +from sudachi_life.phase3.human_pilot import ( + PROPOSED_MAX_ATTEMPT_WALL_MS, + PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT, + PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, + PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT, + PROPOSED_MAX_RESPONSE_LATENCY_MS, + CaregiverConfidence, + EthicsReviewStatus, + HumanProposalDraft, +) + +_CAREGIVER_ID = "caregiver:pseudo:0123456789abcdef0123456789abcdef" +_ATTEMPT_ID = "attempt:self-human-pilot-v1-001" + + +def _authorization(): + return accepted_self_human_pilot_v1_authorization( + attempt_id=_ATTEMPT_ID, + caregiver_id=_CAREGIVER_ID, + ) + + +def _request(*, sequence_ordinal: int = 1) -> CaregiverRequest: + return CaregiverRequest( + request_id=f"request:self-human-{sequence_ordinal:03d}", + study_id="study:self-human-pilot-v1", + attempt_id=_ATTEMPT_ID, + episode_id="episode:self-human-pilot-v1-001", + organism_id="sudachi-self-human-v1", + lineage_generation=0, + sequence_ordinal=sequence_ordinal, + allowed_kinds=(ProposalKind.EXPLANATION, ProposalKind.ABSTAIN), + ) + + +def _draft(*, sequence_ordinal: int = 1, **changes: object) -> HumanProposalDraft: + values: dict[str, object] = { + "draft_id": f"draft:self-human-{sequence_ordinal:03d}", + "request_id": f"request:self-human-{sequence_ordinal:03d}", + "caregiver_id": _CAREGIVER_ID, + "sequence_ordinal": sequence_ordinal, + "kind": ProposalKind.EXPLANATION, + "text": "The visible marker supports inspecting before moving.", + "confidence": CaregiverConfidence.MEDIUM, + "observation_ids": ("observation:marker-visible",), + "objective_ids": ("objective:reach-marker",), + "action_ids": ("action:inspect", "action:move"), + } + values.update(changes) + return HumanProposalDraft(**values) # type: ignore[arg-type] + + +def _measurement( + *, + elapsed_ms: int = 30_000, + latency_ms: int = 2_000, + active_ms: int = 5_000, + is_clarification: bool = False, + timestamp: str = "2026-08-30T14:30:00+09:00", +) -> HumanConsultationMeasurement: + return HumanConsultationMeasurement( + latency_ms=latency_ms, + caregiver_active_ms=active_ms, + attempt_elapsed_ms=elapsed_ms, + source_timestamp=timestamp, + is_clarification=is_clarification, + ) + + +def _accept( + *, + authorization=None, + state: HumanPilotAttemptState | None = None, + request: CaregiverRequest | None = None, + draft: HumanProposalDraft | None = None, + measurement: HumanConsultationMeasurement | None = None, +): + return accept_self_human_proposal( + authorization=authorization or _authorization(), + state=state or start_self_human_pilot_attempt(), + request=request or _request(), + draft=draft or _draft(), + measurement=measurement or _measurement(), + allowed_observation_ids=frozenset({"observation:marker-visible"}), + allowed_objective_ids=frozenset({"objective:reach-marker"}), + allowed_action_ids=frozenset({"action:inspect", "action:move"}), + ) + + +def test_authorization_is_exact_self_only_owner_approved_scope() -> None: + authorization = _authorization() + + assert authorization.protocol_version == HUMAN_PILOT_LIVE_BRIDGE_VERSION + assert authorization.pilot_id == SELF_HUMAN_PILOT_ID + assert authorization.transport == LOCAL_STRUCTURED_HUMAN_TRANSPORT + assert authorization.project_owner_is_caregiver is True + assert authorization.self_only is True + assert authorization.ethics_review_status is EthicsReviewStatus.NOT_REQUIRED_DECLARED + assert authorization.consent_attested is True + assert authorization.consent_revocable is True + assert authorization.third_party_participants == 0 + assert validate_self_human_pilot_authorization(authorization) == () + + +def test_live_bridge_accepts_structured_self_human_proposal_only() -> None: + result = _accept() + + assert result.accepted is True + assert result.errors == () + assert result.proposal is not None + proposal = result.proposal + assert proposal.source_kind is CaregiverSourceKind.HUMAN + assert proposal.source_id == _CAREGIVER_ID + assert proposal.authority is ProposalAuthority.PROPOSAL_ONLY + assert proposal.accounting.live_cost_status is AccountingStatus.MEASURED + assert proposal.accounting.human_active_ms == 5_000 + assert proposal.accounting.latency_ms == 2_000 + assert proposal.accounting.model_calls is None + assert proposal.accounting.money_minor_units is None + assert result.state.consultations_used == 1 + assert result.state.caregiver_active_ms_used == 5_000 + + +def test_generic_validator_remains_fixture_only_and_rejects_live_human() -> None: + result = _accept() + assert result.proposal is not None + + generic = validate_proposal(_request(), result.proposal) + + assert generic.valid is False + assert generic.errors == ("proposal.live_source_not_authorized",) + assert ACTIVE_SOURCE_KINDS == frozenset({CaregiverSourceKind.FIXTURE}) + assert CaregiverSourceKind.HUMAN not in ACTIVE_SOURCE_KINDS + + +def test_wrong_attempt_or_caregiver_binding_fails_closed() -> None: + wrong_request = replace(_request(), attempt_id="attempt:other") + wrong_draft = _draft(caregiver_id="caregiver:pseudo:ffffffffffffffffffffffffffffffff") + + attempt_result = _accept(request=wrong_request) + caregiver_result = _accept(draft=wrong_draft) + + assert "bridge.attempt_binding" in attempt_result.errors + assert "bridge.caregiver_binding" in caregiver_result.errors + assert attempt_result.proposal is None + assert caregiver_result.proposal is None + + +def test_relaxed_or_non_self_authorization_fails_closed() -> None: + authorization = _authorization() + relaxed = replace( + authorization, + self_only=False, + project_owner_is_caregiver=False, + consent_attested=False, + ethics_review_status=EthicsReviewStatus.UNRESOLVED, + raw_chat_transcript_retained=True, + public_raw_payload_default=True, + third_party_participants=1, + ) + + errors = validate_self_human_pilot_authorization(relaxed) + result = _accept(authorization=relaxed) + + assert "authorization.self_only" in errors + assert "authorization.owner_is_caregiver" in errors + assert "authorization.consent_attested" in errors + assert "authorization.ethics_review_status" in errors + assert "authorization.raw_chat_transcript" in errors + assert "authorization.public_raw_payload" in errors + assert "authorization.third_party_participants" in errors + assert result.accepted is False + assert result.proposal is None + + +def test_privacy_and_visible_reference_violations_are_rejected() -> None: + draft = _draft( + contains_personal_data=True, + contains_secret=True, + contains_third_party_confidential_data=True, + observation_ids=("observation:hidden",), + ) + result = _accept(draft=draft) + + assert result.accepted is False + assert "bridge.draft.personal_data_forbidden" in result.errors + assert "bridge.draft.secret_forbidden" in result.errors + assert "bridge.draft.third_party_confidential_data_forbidden" in result.errors + assert "bridge.draft.observation_ids.not_allowed" in result.errors + + +def test_latency_active_time_and_wall_budgets_fail_closed() -> None: + latency = _accept( + measurement=_measurement(latency_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 1) + ) + active = _accept( + measurement=_measurement(active_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 1) + ) + wall = _accept( + measurement=_measurement(elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1) + ) + + assert "bridge.latency_budget" in latency.errors + assert "bridge.active_time_budget" in active.errors + assert "bridge.attempt_wall_budget" in wall.errors + + +def test_consultation_budget_is_aggregate_and_fixed() -> None: + state = replace( + start_self_human_pilot_attempt(), + consultations_used=PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT, + caregiver_active_ms_used=10_000, + attempt_elapsed_ms=20_000, + ) + result = _accept(state=state, measurement=_measurement(elapsed_ms=25_000)) + + assert result.accepted is False + assert "bridge.consultation_budget" in result.errors + assert result.state is state + + +def test_clarification_budget_and_linkage_are_enforced() -> None: + unlinked = _accept(measurement=_measurement(is_clarification=True)) + linked_draft = _draft(clarification_of_proposal_id="proposal:" + "0" * 64) + linked = _accept( + draft=linked_draft, + measurement=_measurement(is_clarification=True), + ) + exhausted_state = replace( + start_self_human_pilot_attempt(), + consultations_used=2, + clarifications_used=PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, + attempt_elapsed_ms=10_000, + ) + exhausted = _accept( + state=exhausted_state, + draft=linked_draft, + measurement=_measurement(elapsed_ms=20_000, is_clarification=True), + ) + + assert "bridge.clarification_binding" in unlinked.errors + assert linked.accepted is True + assert linked.state.clarifications_used == 1 + assert "bridge.clarification_budget" in exhausted.errors + + +def test_offset_timestamp_is_required_for_live_provenance() -> None: + naive = _accept(measurement=_measurement(timestamp="2026-08-30T14:30:00")) + malformed = _accept(measurement=_measurement(timestamp="not-a-time")) + + assert "bridge.source_timestamp" in naive.errors + assert "bridge.source_timestamp" in malformed.errors + + +def test_disablement_technically_prevents_post_cutoff_proposals() -> None: + accepted = _accept() + disabled = disable_self_human_bridge(accepted.state, attempt_elapsed_ms=40_000) + after_cutoff = _accept( + state=disabled, + request=_request(sequence_ordinal=2), + draft=_draft(sequence_ordinal=2), + measurement=_measurement(elapsed_ms=50_000), + ) + + assert disabled.live_enabled is False + assert disabled.disabled_at_elapsed_ms == 40_000 + assert after_cutoff.accepted is False + assert "bridge.disabled" in after_cutoff.errors + assert after_cutoff.proposal is None + + +def test_disablement_time_cannot_regress_or_exceed_wall_budget() -> None: + state = replace(start_self_human_pilot_attempt(), attempt_elapsed_ms=20_000) + + with pytest.raises(ValueError): + disable_self_human_bridge(state, attempt_elapsed_ms=19_999) + with pytest.raises(ValueError): + disable_self_human_bridge( + state, + attempt_elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1, + ) + + +def test_elapsed_time_cannot_move_backwards_between_consultations() -> None: + state = replace(start_self_human_pilot_attempt(), attempt_elapsed_ms=30_000) + result = _accept(state=state, measurement=_measurement(elapsed_ms=29_999)) + + assert result.accepted is False + assert "bridge.elapsed_time_regression" in result.errors + + +def test_live_bridge_exposes_no_direct_execution_or_model_authority() -> None: + result = _accept() + assert result.proposal is not None + + proposal = result.proposal + assert not hasattr(proposal, "execute") + assert not hasattr(proposal, "command") + assert not hasattr(proposal, "writer") + assert proposal.accounting.model_calls is None + assert proposal.accounting.money_minor_units is None From 7600c90e286056dca9517b21c8eb1f608228e947 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:27:21 +0900 Subject: [PATCH 3/7] Export self-human live bridge API --- src/sudachi_life/phase3/__init__.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/sudachi_life/phase3/__init__.py b/src/sudachi_life/phase3/__init__.py index 904a896..97e9fc9 100644 --- a/src/sudachi_life/phase3/__init__.py +++ b/src/sudachi_life/phase3/__init__.py @@ -1,4 +1,4 @@ -"""Deterministic, fixture-only Phase 3 evaluation foundation. +"""Deterministic Phase 3 evaluation and bounded caregiver protocol surfaces. This package is intentionally additive and does not write the Phase 1/2 organism body. """ @@ -22,6 +22,20 @@ validate_proposal, ) from .fixtures import build_valid_fixture_episode +from .human_live import ( + HUMAN_PILOT_LIVE_BRIDGE_VERSION, + LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_PILOT_ID, + HumanBridgeResult, + HumanConsultationMeasurement, + HumanPilotAttemptState, + SelfHumanPilotAuthorization, + accept_self_human_proposal, + accepted_self_human_pilot_v1_authorization, + disable_self_human_bridge, + start_self_human_pilot_attempt, + validate_self_human_pilot_authorization, +) from .human_pilot import ( HUMAN_PILOT_PREFLIGHT_VERSION, PROPOSED_MAX_ATTEMPT_WALL_MS, @@ -80,8 +94,10 @@ "ACTIVE_SOURCE_KINDS", "CAREGIVER_PROTOCOL_VERSION", "CONTRACT_VERSION", + "HUMAN_PILOT_LIVE_BRIDGE_VERSION", "HUMAN_PILOT_PREFLIGHT_VERSION", "IMPLEMENTATION_VERSION", + "LOCAL_STRUCTURED_HUMAN_TRANSPORT", "PROPOSED_MAX_ATTEMPT_WALL_MS", "PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT", "PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT", @@ -90,6 +106,7 @@ "PROPOSED_MAX_RESPONSE_LATENCY_MS", "PROPOSED_PILOT_ATTEMPTS", "PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS", + "SELF_HUMAN_PILOT_ID", "AccountingStatus", "Availability", "AttemptState", @@ -108,7 +125,10 @@ "FixtureCaregiverAdapter", "FixtureResponse", "HiddenLaborPolicy", + "HumanBridgeResult", "HumanCaregiverPilotPreflight", + "HumanConsultationMeasurement", + "HumanPilotAttemptState", "HumanPilotBudget", "HumanPilotDataPolicy", "HumanPilotPreflightResult", @@ -121,18 +141,24 @@ "ProposalKind", "ProposalValidation", "ReplayConflict", + "SelfHumanPilotAuthorization", + "accept_self_human_proposal", "accepted_phase3_requirement_ids", + "accepted_self_human_pilot_v1_authorization", "advance_attempt_history", "build_integrated_caregiver_rehearsal", "build_valid_fixture_episode", "classify_availability", + "disable_self_human_bridge", "proposal_data_fields", "proposal_from_text", "proposed_human_caregiver_pilot_v1", "reconcile_immutable_replay", + "start_self_human_pilot_attempt", "validate_episode", "validate_human_pilot_preflight", "validate_human_proposal_draft", "validate_integrated_caregiver_rehearsal", "validate_proposal", + "validate_self_human_pilot_authorization", ] From 5fe7b7760b91f79dbb2cd8798c3ecc91a796b1b4 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:30:38 +0900 Subject: [PATCH 4/7] Harden self-human bridge state and provenance --- src/sudachi_life/phase3/human_live.py | 127 ++++++++++++++++++++++---- 1 file changed, 111 insertions(+), 16 deletions(-) diff --git a/src/sudachi_life/phase3/human_live.py b/src/sudachi_life/phase3/human_live.py index d8e02f6..bd2f9f7 100644 --- a/src/sudachi_life/phase3/human_live.py +++ b/src/sudachi_life/phase3/human_live.py @@ -28,6 +28,8 @@ HUMAN_PILOT_LIVE_BRIDGE_VERSION = "sudachi.phase3.human_caregiver_live_bridge/v1" SELF_HUMAN_PILOT_ID = "pilot:human-caregiver-v1-self" LOCAL_STRUCTURED_HUMAN_TRANSPORT = "local_structured_manual" +SELF_HUMAN_AUTHORIZATION_RECORD = "github:yo4e/sudachi-life/issues/158" +SELF_HUMAN_CONSENT_NOTICE_VERSION = "sudachi.phase3.self_human_consent/v1" @dataclass(frozen=True, slots=True) @@ -38,6 +40,8 @@ class SelfHumanPilotAuthorization: caregiver_id: str source_kind: CaregiverSourceKind transport: str + authorization_record: str + consent_notice_version: str project_owner_is_caregiver: bool self_only: bool ethics_review_status: EthicsReviewStatus @@ -57,6 +61,8 @@ class HumanPilotAttemptState: attempt_elapsed_ms: int = 0 live_enabled: bool = True disabled_at_elapsed_ms: int | None = None + accepted_request_ids: tuple[str, ...] = () + accepted_proposal_ids: tuple[str, ...] = () @dataclass(frozen=True, slots=True) @@ -94,6 +100,48 @@ def _is_offset_timestamp(value: str) -> bool: return parsed.tzinfo is not None and parsed.utcoffset() is not None +def _validate_attempt_state(state: HumanPilotAttemptState) -> tuple[str, ...]: + errors: list[str] = [] + + integer_fields = ( + ("consultations_used", state.consultations_used, PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT), + ("clarifications_used", state.clarifications_used, PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT), + ("caregiver_active_ms_used", state.caregiver_active_ms_used, PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT), + ("attempt_elapsed_ms", state.attempt_elapsed_ms, PROPOSED_MAX_ATTEMPT_WALL_MS), + ) + for name, value, maximum in integer_fields: + if type(value) is not int or value < 0 or value > maximum: + errors.append(f"state.{name}") + + if type(state.live_enabled) is not bool: + errors.append("state.live_enabled") + if state.clarifications_used > state.consultations_used: + errors.append("state.clarifications_exceed_consultations") + if len(state.accepted_request_ids) != state.consultations_used: + errors.append("state.request_count") + if len(state.accepted_proposal_ids) != state.consultations_used: + errors.append("state.proposal_count") + if len(set(state.accepted_request_ids)) != len(state.accepted_request_ids): + errors.append("state.request_ids_duplicate") + if len(set(state.accepted_proposal_ids)) != len(state.accepted_proposal_ids): + errors.append("state.proposal_ids_duplicate") + if any(not value for value in state.accepted_request_ids): + errors.append("state.request_id_empty") + if any(not value.startswith("proposal:") for value in state.accepted_proposal_ids): + errors.append("state.proposal_id_shape") + + if state.live_enabled: + if state.disabled_at_elapsed_ms is not None: + errors.append("state.enabled_with_disablement") + else: + if type(state.disabled_at_elapsed_ms) is not int: + errors.append("state.disabled_without_timestamp") + elif state.disabled_at_elapsed_ms != state.attempt_elapsed_ms: + errors.append("state.disablement_timestamp") + + return tuple(errors) + + def accepted_self_human_pilot_v1_authorization( *, attempt_id: str, @@ -103,7 +151,8 @@ def accepted_self_human_pilot_v1_authorization( This function does not perform I/O or contact any external service. The only live source it authorizes is the project owner supplying a structured draft - locally for the single bound attempt. + locally for the single bound attempt. The approval/ethics/consent decision is + durably bound to GitHub Issue #158. """ return SelfHumanPilotAuthorization( protocol_version=HUMAN_PILOT_LIVE_BRIDGE_VERSION, @@ -112,6 +161,8 @@ def accepted_self_human_pilot_v1_authorization( caregiver_id=caregiver_id, source_kind=CaregiverSourceKind.HUMAN, transport=LOCAL_STRUCTURED_HUMAN_TRANSPORT, + authorization_record=SELF_HUMAN_AUTHORIZATION_RECORD, + consent_notice_version=SELF_HUMAN_CONSENT_NOTICE_VERSION, project_owner_is_caregiver=True, self_only=True, ethics_review_status=EthicsReviewStatus.NOT_REQUIRED_DECLARED, @@ -141,6 +192,10 @@ def validate_self_human_pilot_authorization( errors.append("authorization.source_kind") if authorization.transport != LOCAL_STRUCTURED_HUMAN_TRANSPORT: errors.append("authorization.transport") + if authorization.authorization_record != SELF_HUMAN_AUTHORIZATION_RECORD: + errors.append("authorization.record") + if authorization.consent_notice_version != SELF_HUMAN_CONSENT_NOTICE_VERSION: + errors.append("authorization.consent_notice_version") if not authorization.project_owner_is_caregiver: errors.append("authorization.owner_is_caregiver") if not authorization.self_only: @@ -172,6 +227,13 @@ def disable_self_human_bridge( *, attempt_elapsed_ms: int, ) -> HumanPilotAttemptState: + state_errors = _validate_attempt_state(state) + if state_errors: + raise ValueError(f"invalid attempt state: {state_errors}") + if not state.live_enabled: + raise ValueError("bridge is already disabled") + if type(attempt_elapsed_ms) is not int: + raise TypeError("disablement elapsed time must be int") if attempt_elapsed_ms < state.attempt_elapsed_ms: raise ValueError("disablement elapsed time cannot move backwards") if attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: @@ -202,13 +264,14 @@ def accept_self_human_proposal( structured draft and measured timing values locally. """ errors = list(validate_self_human_pilot_authorization(authorization)) + errors.extend(f"bridge.{error}" for error in _validate_attempt_state(state)) if not state.live_enabled: errors.append("bridge.disabled") - if state.disabled_at_elapsed_ms is not None: - errors.append("bridge.disablement_state") if request.attempt_id != authorization.attempt_id: errors.append("bridge.attempt_binding") + if request.request_id in state.accepted_request_ids: + errors.append("bridge.request_replay") if draft.caregiver_id != authorization.caregiver_id: errors.append("bridge.caregiver_binding") @@ -221,26 +284,54 @@ def accept_self_human_proposal( ) errors.extend(f"bridge.{error}" for error in draft_validation.errors) - if measurement.latency_ms < 0: - errors.append("bridge.latency_negative") - elif measurement.latency_ms > PROPOSED_MAX_RESPONSE_LATENCY_MS: - errors.append("bridge.latency_budget") - if measurement.caregiver_active_ms < 0: - errors.append("bridge.active_ms_negative") - if measurement.attempt_elapsed_ms < state.attempt_elapsed_ms: - errors.append("bridge.elapsed_time_regression") - elif measurement.attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: - errors.append("bridge.attempt_wall_budget") + measurement_ints = ( + ("latency_ms", measurement.latency_ms), + ("caregiver_active_ms", measurement.caregiver_active_ms), + ("attempt_elapsed_ms", measurement.attempt_elapsed_ms), + ) + for name, value in measurement_ints: + if type(value) is not int: + errors.append(f"bridge.{name}_type") + if type(measurement.is_clarification) is not bool: + errors.append("bridge.is_clarification_type") + + elapsed_delta: int | None = None + if type(measurement.attempt_elapsed_ms) is int: + if measurement.attempt_elapsed_ms < state.attempt_elapsed_ms: + errors.append("bridge.elapsed_time_regression") + elif measurement.attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: + errors.append("bridge.attempt_wall_budget") + else: + elapsed_delta = measurement.attempt_elapsed_ms - state.attempt_elapsed_ms + + if type(measurement.latency_ms) is int: + if measurement.latency_ms < 0: + errors.append("bridge.latency_negative") + elif measurement.latency_ms > PROPOSED_MAX_RESPONSE_LATENCY_MS: + errors.append("bridge.latency_budget") + elif elapsed_delta is not None and measurement.latency_ms > elapsed_delta: + errors.append("bridge.latency_exceeds_elapsed") + if type(measurement.caregiver_active_ms) is int: + if measurement.caregiver_active_ms < 0: + errors.append("bridge.active_ms_negative") + elif elapsed_delta is not None and measurement.caregiver_active_ms > elapsed_delta: + errors.append("bridge.active_ms_exceeds_elapsed") if not _is_offset_timestamp(measurement.source_timestamp): errors.append("bridge.source_timestamp") is_linked_clarification = draft.clarification_of_proposal_id is not None - if measurement.is_clarification != is_linked_clarification: + if type(measurement.is_clarification) is bool and measurement.is_clarification != is_linked_clarification: errors.append("bridge.clarification_binding") + if is_linked_clarification and draft.clarification_of_proposal_id not in state.accepted_proposal_ids: + errors.append("bridge.clarification_source") next_consultations = state.consultations_used + 1 - next_clarifications = state.clarifications_used + int(measurement.is_clarification) - next_active_ms = state.caregiver_active_ms_used + measurement.caregiver_active_ms + next_clarifications = state.clarifications_used + int(measurement.is_clarification is True) + next_active_ms = ( + state.caregiver_active_ms_used + measurement.caregiver_active_ms + if type(measurement.caregiver_active_ms) is int + else state.caregiver_active_ms_used + ) if next_consultations > PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT: errors.append("bridge.consultation_budget") @@ -289,6 +380,8 @@ def accept_self_human_proposal( ) if "proposal.live_source_not_authorized" not in generic_validation.errors: unexpected_generic_errors += ("proposal.expected_global_rejection_missing",) + if proposal.proposal_id in state.accepted_proposal_ids: + unexpected_generic_errors += ("proposal.replay",) if unexpected_generic_errors: return HumanBridgeResult( accepted=False, @@ -326,6 +419,8 @@ def accept_self_human_proposal( attempt_elapsed_ms=measurement.attempt_elapsed_ms, live_enabled=True, disabled_at_elapsed_ms=None, + accepted_request_ids=state.accepted_request_ids + (request.request_id,), + accepted_proposal_ids=state.accepted_proposal_ids + (proposal.proposal_id,), ) return HumanBridgeResult( accepted=True, From 57ace28835f0edac2ba42753c1d3fe4091d7be90 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:31:37 +0900 Subject: [PATCH 5/7] Extend live bridge adversarial coverage --- tests/test_phase3_self_human_live_bridge.py | 138 +++++++++++++++++--- 1 file changed, 120 insertions(+), 18 deletions(-) diff --git a/tests/test_phase3_self_human_live_bridge.py b/tests/test_phase3_self_human_live_bridge.py index 2c9fa3a..821c53f 100644 --- a/tests/test_phase3_self_human_live_bridge.py +++ b/tests/test_phase3_self_human_live_bridge.py @@ -16,6 +16,8 @@ from sudachi_life.phase3.human_live import ( HUMAN_PILOT_LIVE_BRIDGE_VERSION, LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_AUTHORIZATION_RECORD, + SELF_HUMAN_CONSENT_NOTICE_VERSION, SELF_HUMAN_PILOT_ID, HumanConsultationMeasurement, HumanPilotAttemptState, @@ -114,12 +116,27 @@ def _accept( ) +def _valid_used_state(*, consultations: int, clarifications: int = 0) -> HumanPilotAttemptState: + request_ids = tuple(f"request:used:{index}" for index in range(consultations)) + proposal_ids = tuple(f"proposal:{index:064x}" for index in range(consultations)) + return HumanPilotAttemptState( + consultations_used=consultations, + clarifications_used=clarifications, + caregiver_active_ms_used=10_000, + attempt_elapsed_ms=20_000, + accepted_request_ids=request_ids, + accepted_proposal_ids=proposal_ids, + ) + + def test_authorization_is_exact_self_only_owner_approved_scope() -> None: authorization = _authorization() assert authorization.protocol_version == HUMAN_PILOT_LIVE_BRIDGE_VERSION assert authorization.pilot_id == SELF_HUMAN_PILOT_ID assert authorization.transport == LOCAL_STRUCTURED_HUMAN_TRANSPORT + assert authorization.authorization_record == SELF_HUMAN_AUTHORIZATION_RECORD + assert authorization.consent_notice_version == SELF_HUMAN_CONSENT_NOTICE_VERSION assert authorization.project_owner_is_caregiver is True assert authorization.self_only is True assert authorization.ethics_review_status is EthicsReviewStatus.NOT_REQUIRED_DECLARED @@ -146,6 +163,8 @@ def test_live_bridge_accepts_structured_self_human_proposal_only() -> None: assert proposal.accounting.money_minor_units is None assert result.state.consultations_used == 1 assert result.state.caregiver_active_ms_used == 5_000 + assert result.state.accepted_request_ids == (_request().request_id,) + assert result.state.accepted_proposal_ids == (proposal.proposal_id,) def test_generic_validator_remains_fixture_only_and_rejects_live_human() -> None: @@ -177,6 +196,8 @@ def test_relaxed_or_non_self_authorization_fails_closed() -> None: authorization = _authorization() relaxed = replace( authorization, + authorization_record="chat-only", + consent_notice_version="unknown", self_only=False, project_owner_is_caregiver=False, consent_attested=False, @@ -189,6 +210,8 @@ def test_relaxed_or_non_self_authorization_fails_closed() -> None: errors = validate_self_human_pilot_authorization(relaxed) result = _accept(authorization=relaxed) + assert "authorization.record" in errors + assert "authorization.consent_notice_version" in errors assert "authorization.self_only" in errors assert "authorization.owner_is_caregiver" in errors assert "authorization.consent_attested" in errors @@ -218,10 +241,16 @@ def test_privacy_and_visible_reference_violations_are_rejected() -> None: def test_latency_active_time_and_wall_budgets_fail_closed() -> None: latency = _accept( - measurement=_measurement(latency_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 1) + measurement=_measurement( + elapsed_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 2, + latency_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 1, + ) ) active = _accept( - measurement=_measurement(active_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 1) + measurement=_measurement( + elapsed_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 2, + active_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 1, + ) ) wall = _accept( measurement=_measurement(elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1) @@ -232,13 +261,16 @@ def test_latency_active_time_and_wall_budgets_fail_closed() -> None: assert "bridge.attempt_wall_budget" in wall.errors +def test_measured_time_must_fit_inside_elapsed_attempt_interval() -> None: + latency = _accept(measurement=_measurement(elapsed_ms=1_000, latency_ms=1_001, active_ms=500)) + active = _accept(measurement=_measurement(elapsed_ms=1_000, latency_ms=500, active_ms=1_001)) + + assert "bridge.latency_exceeds_elapsed" in latency.errors + assert "bridge.active_ms_exceeds_elapsed" in active.errors + + def test_consultation_budget_is_aggregate_and_fixed() -> None: - state = replace( - start_self_human_pilot_attempt(), - consultations_used=PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT, - caregiver_active_ms_used=10_000, - attempt_elapsed_ms=20_000, - ) + state = _valid_used_state(consultations=PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT) result = _accept(state=state, measurement=_measurement(elapsed_ms=25_000)) assert result.accepted is False @@ -246,23 +278,36 @@ def test_consultation_budget_is_aggregate_and_fixed() -> None: assert result.state is state -def test_clarification_budget_and_linkage_are_enforced() -> None: +def test_clarification_must_link_to_prior_accepted_proposal_and_budget() -> None: unlinked = _accept(measurement=_measurement(is_clarification=True)) - linked_draft = _draft(clarification_of_proposal_id="proposal:" + "0" * 64) + first = _accept() + assert first.accepted is True + assert first.proposal is not None + + linked_draft = _draft( + sequence_ordinal=2, + clarification_of_proposal_id=first.proposal.proposal_id, + ) linked = _accept( + state=first.state, + request=_request(sequence_ordinal=2), draft=linked_draft, - measurement=_measurement(is_clarification=True), + measurement=_measurement(elapsed_ms=40_000, is_clarification=True), + ) + + exhausted_state = _valid_used_state( + consultations=2, + clarifications=PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, ) - exhausted_state = replace( - start_self_human_pilot_attempt(), - consultations_used=2, - clarifications_used=PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, - attempt_elapsed_ms=10_000, + exhausted_draft = _draft( + sequence_ordinal=3, + clarification_of_proposal_id=exhausted_state.accepted_proposal_ids[0], ) exhausted = _accept( state=exhausted_state, - draft=linked_draft, - measurement=_measurement(elapsed_ms=20_000, is_clarification=True), + request=_request(sequence_ordinal=3), + draft=exhausted_draft, + measurement=_measurement(elapsed_ms=40_000, is_clarification=True), ) assert "bridge.clarification_binding" in unlinked.errors @@ -271,6 +316,63 @@ def test_clarification_budget_and_linkage_are_enforced() -> None: assert "bridge.clarification_budget" in exhausted.errors +def test_unknown_clarification_source_fails_closed() -> None: + first = _accept() + unknown = _draft( + sequence_ordinal=2, + clarification_of_proposal_id="proposal:" + "f" * 64, + ) + result = _accept( + state=first.state, + request=_request(sequence_ordinal=2), + draft=unknown, + measurement=_measurement(elapsed_ms=40_000, is_clarification=True), + ) + + assert result.accepted is False + assert "bridge.clarification_source" in result.errors + + +def test_request_replay_is_rejected() -> None: + first = _accept() + replay = _accept( + state=first.state, + measurement=_measurement(elapsed_ms=40_000), + ) + + assert replay.accepted is False + assert "bridge.request_replay" in replay.errors + + +def test_tampered_aggregate_state_fails_closed() -> None: + negative = HumanPilotAttemptState(consultations_used=-1) + count_mismatch = HumanPilotAttemptState( + consultations_used=1, + accepted_request_ids=(), + accepted_proposal_ids=(), + ) + + negative_result = _accept(state=negative) + mismatch_result = _accept(state=count_mismatch) + + assert "bridge.state.consultations_used" in negative_result.errors + assert "bridge.state.request_count" in mismatch_result.errors + assert "bridge.state.proposal_count" in mismatch_result.errors + + +def test_measurement_bool_masquerading_as_integer_fails_closed() -> None: + measurement = HumanConsultationMeasurement( + latency_ms=True, # type: ignore[arg-type] + caregiver_active_ms=1, + attempt_elapsed_ms=10, + source_timestamp="2026-08-30T14:30:00+09:00", + ) + result = _accept(measurement=measurement) + + assert result.accepted is False + assert "bridge.latency_ms_type" in result.errors + + def test_offset_timestamp_is_required_for_live_provenance() -> None: naive = _accept(measurement=_measurement(timestamp="2026-08-30T14:30:00")) malformed = _accept(measurement=_measurement(timestamp="not-a-time")) From bb0639796a91a500fe1de9f102b5e837201ee258 Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 14:31:57 +0900 Subject: [PATCH 6/7] Export self-human authorization provenance constants --- src/sudachi_life/phase3/__init__.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/sudachi_life/phase3/__init__.py b/src/sudachi_life/phase3/__init__.py index 97e9fc9..f27c6f9 100644 --- a/src/sudachi_life/phase3/__init__.py +++ b/src/sudachi_life/phase3/__init__.py @@ -25,6 +25,8 @@ from .human_live import ( HUMAN_PILOT_LIVE_BRIDGE_VERSION, LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_AUTHORIZATION_RECORD, + SELF_HUMAN_CONSENT_NOTICE_VERSION, SELF_HUMAN_PILOT_ID, HumanBridgeResult, HumanConsultationMeasurement, @@ -106,6 +108,8 @@ "PROPOSED_MAX_RESPONSE_LATENCY_MS", "PROPOSED_PILOT_ATTEMPTS", "PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS", + "SELF_HUMAN_AUTHORIZATION_RECORD", + "SELF_HUMAN_CONSENT_NOTICE_VERSION", "SELF_HUMAN_PILOT_ID", "AccountingStatus", "Availability", From 74966468a8672b123c3440dbebb6cc008ed08c9f Mon Sep 17 00:00:00 2001 From: Yoshie Yamada <59075346+yo4e@users.noreply.github.com> Date: Sun, 30 Aug 2026 18:55:30 +0900 Subject: [PATCH 7/7] Repair self-human live bridge audit findings --- src/sudachi_life/phase3/__init__.py | 4 + src/sudachi_life/phase3/human_live.py | 421 +++++++++-------- tests/test_phase3_self_human_live_bridge.py | 480 +++++++++++--------- 3 files changed, 501 insertions(+), 404 deletions(-) diff --git a/src/sudachi_life/phase3/__init__.py b/src/sudachi_life/phase3/__init__.py index f27c6f9..f7b4363 100644 --- a/src/sudachi_life/phase3/__init__.py +++ b/src/sudachi_life/phase3/__init__.py @@ -25,7 +25,9 @@ from .human_live import ( HUMAN_PILOT_LIVE_BRIDGE_VERSION, LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_ATTEMPT_ID, SELF_HUMAN_AUTHORIZATION_RECORD, + SELF_HUMAN_CAREGIVER_ID, SELF_HUMAN_CONSENT_NOTICE_VERSION, SELF_HUMAN_PILOT_ID, HumanBridgeResult, @@ -108,7 +110,9 @@ "PROPOSED_MAX_RESPONSE_LATENCY_MS", "PROPOSED_PILOT_ATTEMPTS", "PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS", + "SELF_HUMAN_ATTEMPT_ID", "SELF_HUMAN_AUTHORIZATION_RECORD", + "SELF_HUMAN_CAREGIVER_ID", "SELF_HUMAN_CONSENT_NOTICE_VERSION", "SELF_HUMAN_PILOT_ID", "AccountingStatus", diff --git a/src/sudachi_life/phase3/human_live.py b/src/sudachi_life/phase3/human_live.py index bd2f9f7..29a3d16 100644 --- a/src/sudachi_life/phase3/human_live.py +++ b/src/sudachi_life/phase3/human_live.py @@ -2,6 +2,7 @@ from dataclasses import dataclass, replace from datetime import datetime +from threading import RLock from .caregiver import ( AccountingStatus, @@ -19,6 +20,7 @@ PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT, PROPOSED_MAX_RESPONSE_LATENCY_MS, + PROPOSED_PILOT_ATTEMPTS, PROPOSED_PROPOSAL_PAYLOAD_RETENTION_DAYS, EthicsReviewStatus, HumanProposalDraft, @@ -30,6 +32,8 @@ LOCAL_STRUCTURED_HUMAN_TRANSPORT = "local_structured_manual" SELF_HUMAN_AUTHORIZATION_RECORD = "github:yo4e/sudachi-life/issues/158" SELF_HUMAN_CONSENT_NOTICE_VERSION = "sudachi.phase3.self_human_consent/v1" +SELF_HUMAN_ATTEMPT_ID = "attempt:self-human-pilot-v1-001" +SELF_HUMAN_CAREGIVER_ID = "caregiver:pseudo:0123456789abcdef0123456789abcdef" @dataclass(frozen=True, slots=True) @@ -55,6 +59,8 @@ class SelfHumanPilotAuthorization: @dataclass(frozen=True, slots=True) class HumanPilotAttemptState: + attempt_id: str + generation: int = 0 consultations_used: int = 0 clarifications_used: int = 0 caregiver_active_ms_used: int = 0 @@ -82,12 +88,8 @@ class HumanBridgeResult: state: HumanPilotAttemptState -def _is_pseudonymous_caregiver_id(value: str) -> bool: - prefix = "caregiver:pseudo:" - if not value.startswith(prefix): - return False - token = value[len(prefix) :] - return len(token) == 32 and all(ch in "0123456789abcdef" for ch in token) +_SESSION_LOCK = RLock() +_AUTHORITATIVE_STATE: HumanPilotAttemptState | None = None def _is_offset_timestamp(value: str) -> bool: @@ -102,15 +104,18 @@ def _is_offset_timestamp(value: str) -> bool: def _validate_attempt_state(state: HumanPilotAttemptState) -> tuple[str, ...]: errors: list[str] = [] + if state.attempt_id != SELF_HUMAN_ATTEMPT_ID: + errors.append("state.attempt_id") integer_fields = ( + ("generation", state.generation, None), ("consultations_used", state.consultations_used, PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT), ("clarifications_used", state.clarifications_used, PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT), ("caregiver_active_ms_used", state.caregiver_active_ms_used, PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT), ("attempt_elapsed_ms", state.attempt_elapsed_ms, PROPOSED_MAX_ATTEMPT_WALL_MS), ) for name, value, maximum in integer_fields: - if type(value) is not int or value < 0 or value > maximum: + if type(value) is not int or value < 0 or (maximum is not None and value > maximum): errors.append(f"state.{name}") if type(state.live_enabled) is not bool: @@ -142,23 +147,13 @@ def _validate_attempt_state(state: HumanPilotAttemptState) -> tuple[str, ...]: return tuple(errors) -def accepted_self_human_pilot_v1_authorization( - *, - attempt_id: str, - caregiver_id: str, -) -> SelfHumanPilotAuthorization: - """Return the owner-approved self-only Pilot v1 live authorization record. - - This function does not perform I/O or contact any external service. The only - live source it authorizes is the project owner supplying a structured draft - locally for the single bound attempt. The approval/ethics/consent decision is - durably bound to GitHub Issue #158. - """ +def accepted_self_human_pilot_v1_authorization() -> SelfHumanPilotAuthorization: + """Return the one exact owner-approved self-only Pilot v1 authorization.""" return SelfHumanPilotAuthorization( protocol_version=HUMAN_PILOT_LIVE_BRIDGE_VERSION, pilot_id=SELF_HUMAN_PILOT_ID, - attempt_id=attempt_id, - caregiver_id=caregiver_id, + attempt_id=SELF_HUMAN_ATTEMPT_ID, + caregiver_id=SELF_HUMAN_CAREGIVER_ID, source_kind=CaregiverSourceKind.HUMAN, transport=LOCAL_STRUCTURED_HUMAN_TRANSPORT, authorization_record=SELF_HUMAN_AUTHORIZATION_RECORD, @@ -180,13 +175,15 @@ def validate_self_human_pilot_authorization( ) -> tuple[str, ...]: errors: list[str] = [] + if PROPOSED_PILOT_ATTEMPTS != 1: + errors.append("authorization.pilot_attempt_count") if authorization.protocol_version != HUMAN_PILOT_LIVE_BRIDGE_VERSION: errors.append("authorization.protocol_version") if authorization.pilot_id != SELF_HUMAN_PILOT_ID: errors.append("authorization.pilot_id") - if not authorization.attempt_id: + if authorization.attempt_id != SELF_HUMAN_ATTEMPT_ID: errors.append("authorization.attempt_id") - if not _is_pseudonymous_caregiver_id(authorization.caregiver_id): + if authorization.caregiver_id != SELF_HUMAN_CAREGIVER_ID: errors.append("authorization.caregiver_id") if authorization.source_kind is not CaregiverSourceKind.HUMAN: errors.append("authorization.source_kind") @@ -218,32 +215,62 @@ def validate_self_human_pilot_authorization( return tuple(errors) -def start_self_human_pilot_attempt() -> HumanPilotAttemptState: - return HumanPilotAttemptState() +def start_self_human_pilot_attempt( + *, authorization: SelfHumanPilotAuthorization +) -> HumanPilotAttemptState: + """Start the one process-authoritative Pilot v1 attempt exactly once.""" + authorization_errors = validate_self_human_pilot_authorization(authorization) + if authorization_errors: + raise ValueError(f"invalid authorization: {authorization_errors}") + + global _AUTHORITATIVE_STATE + with _SESSION_LOCK: + if _AUTHORITATIVE_STATE is not None: + raise RuntimeError("self-human Pilot v1 attempt already started in this process") + _AUTHORITATIVE_STATE = HumanPilotAttemptState(attempt_id=SELF_HUMAN_ATTEMPT_ID) + return _AUTHORITATIVE_STATE + + +def _current_state_or(state: HumanPilotAttemptState) -> HumanPilotAttemptState: + return _AUTHORITATIVE_STATE if _AUTHORITATIVE_STATE is not None else state def disable_self_human_bridge( - state: HumanPilotAttemptState, *, + authorization: SelfHumanPilotAuthorization, + state: HumanPilotAttemptState, attempt_elapsed_ms: int, ) -> HumanPilotAttemptState: - state_errors = _validate_attempt_state(state) - if state_errors: - raise ValueError(f"invalid attempt state: {state_errors}") - if not state.live_enabled: - raise ValueError("bridge is already disabled") - if type(attempt_elapsed_ms) is not int: - raise TypeError("disablement elapsed time must be int") - if attempt_elapsed_ms < state.attempt_elapsed_ms: - raise ValueError("disablement elapsed time cannot move backwards") - if attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: - raise ValueError("disablement exceeds attempt wall budget") - return replace( - state, - attempt_elapsed_ms=attempt_elapsed_ms, - live_enabled=False, - disabled_at_elapsed_ms=attempt_elapsed_ms, - ) + authorization_errors = validate_self_human_pilot_authorization(authorization) + if authorization_errors: + raise ValueError(f"invalid authorization: {authorization_errors}") + + global _AUTHORITATIVE_STATE + with _SESSION_LOCK: + if _AUTHORITATIVE_STATE is None: + raise RuntimeError("self-human Pilot v1 attempt has not started") + if state != _AUTHORITATIVE_STATE: + raise ValueError("stale or forked attempt state") + state_errors = _validate_attempt_state(_AUTHORITATIVE_STATE) + if state_errors: + raise ValueError(f"invalid authoritative attempt state: {state_errors}") + if not _AUTHORITATIVE_STATE.live_enabled: + raise ValueError("bridge is already disabled") + if type(attempt_elapsed_ms) is not int: + raise TypeError("disablement elapsed time must be int") + if attempt_elapsed_ms < _AUTHORITATIVE_STATE.attempt_elapsed_ms: + raise ValueError("disablement elapsed time cannot move backwards") + if attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: + raise ValueError("disablement exceeds attempt wall budget") + + _AUTHORITATIVE_STATE = replace( + _AUTHORITATIVE_STATE, + generation=_AUTHORITATIVE_STATE.generation + 1, + attempt_elapsed_ms=attempt_elapsed_ms, + live_enabled=False, + disabled_at_elapsed_ms=attempt_elapsed_ms, + ) + return _AUTHORITATIVE_STATE def accept_self_human_proposal( @@ -257,174 +284,166 @@ def accept_self_human_proposal( allowed_objective_ids: frozenset[str] = frozenset(), allowed_action_ids: frozenset[str] = frozenset(), ) -> HumanBridgeResult: - """Convert one validated self-caregiver draft into a proposal-only live proposal. + """Accept one local structured self-caregiver draft against authoritative state. The bridge performs no network, subprocess, browser, credential, filesystem, - or external-service access. A caller supplies the already human-attested - structured draft and measured timing values locally. + or external-service access. State progression is process-authoritative and + atomic: stale, fresh-reset, or forked snapshots cannot advance the attempt. """ - errors = list(validate_self_human_pilot_authorization(authorization)) - errors.extend(f"bridge.{error}" for error in _validate_attempt_state(state)) - - if not state.live_enabled: - errors.append("bridge.disabled") - if request.attempt_id != authorization.attempt_id: - errors.append("bridge.attempt_binding") - if request.request_id in state.accepted_request_ids: - errors.append("bridge.request_replay") - if draft.caregiver_id != authorization.caregiver_id: - errors.append("bridge.caregiver_binding") - - draft_validation = validate_human_proposal_draft( - request, - draft, - allowed_observation_ids=allowed_observation_ids, - allowed_objective_ids=allowed_objective_ids, - allowed_action_ids=allowed_action_ids, - ) - errors.extend(f"bridge.{error}" for error in draft_validation.errors) - - measurement_ints = ( - ("latency_ms", measurement.latency_ms), - ("caregiver_active_ms", measurement.caregiver_active_ms), - ("attempt_elapsed_ms", measurement.attempt_elapsed_ms), - ) - for name, value in measurement_ints: - if type(value) is not int: - errors.append(f"bridge.{name}_type") - if type(measurement.is_clarification) is not bool: - errors.append("bridge.is_clarification_type") - - elapsed_delta: int | None = None - if type(measurement.attempt_elapsed_ms) is int: - if measurement.attempt_elapsed_ms < state.attempt_elapsed_ms: - errors.append("bridge.elapsed_time_regression") - elif measurement.attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: - errors.append("bridge.attempt_wall_budget") + global _AUTHORITATIVE_STATE + with _SESSION_LOCK: + errors = list(validate_self_human_pilot_authorization(authorization)) + if _AUTHORITATIVE_STATE is None: + errors.append("bridge.attempt_not_started") + authoritative = state else: - elapsed_delta = measurement.attempt_elapsed_ms - state.attempt_elapsed_ms - - if type(measurement.latency_ms) is int: - if measurement.latency_ms < 0: - errors.append("bridge.latency_negative") - elif measurement.latency_ms > PROPOSED_MAX_RESPONSE_LATENCY_MS: - errors.append("bridge.latency_budget") - elif elapsed_delta is not None and measurement.latency_ms > elapsed_delta: - errors.append("bridge.latency_exceeds_elapsed") - if type(measurement.caregiver_active_ms) is int: - if measurement.caregiver_active_ms < 0: - errors.append("bridge.active_ms_negative") - elif elapsed_delta is not None and measurement.caregiver_active_ms > elapsed_delta: - errors.append("bridge.active_ms_exceeds_elapsed") - if not _is_offset_timestamp(measurement.source_timestamp): - errors.append("bridge.source_timestamp") - - is_linked_clarification = draft.clarification_of_proposal_id is not None - if type(measurement.is_clarification) is bool and measurement.is_clarification != is_linked_clarification: - errors.append("bridge.clarification_binding") - if is_linked_clarification and draft.clarification_of_proposal_id not in state.accepted_proposal_ids: - errors.append("bridge.clarification_source") - - next_consultations = state.consultations_used + 1 - next_clarifications = state.clarifications_used + int(measurement.is_clarification is True) - next_active_ms = ( - state.caregiver_active_ms_used + measurement.caregiver_active_ms - if type(measurement.caregiver_active_ms) is int - else state.caregiver_active_ms_used - ) - - if next_consultations > PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT: - errors.append("bridge.consultation_budget") - if next_clarifications > PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT: - errors.append("bridge.clarification_budget") - if next_active_ms > PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT: - errors.append("bridge.active_time_budget") - - if errors: - return HumanBridgeResult( - accepted=False, - errors=tuple(errors), - proposal=None, - state=state, + authoritative = _AUTHORITATIVE_STATE + if state != authoritative: + errors.append("bridge.stale_or_forked_state") + errors.extend(f"bridge.{error}" for error in _validate_attempt_state(authoritative)) + + if not authoritative.live_enabled: + errors.append("bridge.disabled") + if request.attempt_id != authorization.attempt_id: + errors.append("bridge.attempt_binding") + if request.request_id in authoritative.accepted_request_ids: + errors.append("bridge.request_replay") + if draft.caregiver_id != authorization.caregiver_id: + errors.append("bridge.caregiver_binding") + + draft_validation = validate_human_proposal_draft( + request, + draft, + allowed_observation_ids=allowed_observation_ids, + allowed_objective_ids=allowed_objective_ids, + allowed_action_ids=allowed_action_ids, ) + errors.extend(f"bridge.{error}" for error in draft_validation.errors) - accounting = CaregiverAccounting( - consultation_count=1, - clarification_count=int(measurement.is_clarification), - latency_ms=measurement.latency_ms, - human_active_ms=measurement.caregiver_active_ms, - model_calls=None, - money_minor_units=None, - live_cost_status=AccountingStatus.MEASURED, - absence_reason=None, - ) - proposal = proposal_from_text( - request=request, - source_id=authorization.caregiver_id, - source_kind=CaregiverSourceKind.HUMAN, - kind=draft.kind, - text=draft.text, - accounting=accounting, - source_timestamp=measurement.source_timestamp, - ) + measurement_ints = ( + ("latency_ms", measurement.latency_ms), + ("caregiver_active_ms", measurement.caregiver_active_ms), + ("attempt_elapsed_ms", measurement.attempt_elapsed_ms), + ) + for name, value in measurement_ints: + if type(value) is not int: + errors.append(f"bridge.{name}_type") + if type(measurement.is_clarification) is not bool: + errors.append("bridge.is_clarification_type") + + elapsed_delta: int | None = None + if type(measurement.attempt_elapsed_ms) is int: + if measurement.attempt_elapsed_ms < authoritative.attempt_elapsed_ms: + errors.append("bridge.elapsed_time_regression") + elif measurement.attempt_elapsed_ms > PROPOSED_MAX_ATTEMPT_WALL_MS: + errors.append("bridge.attempt_wall_budget") + else: + elapsed_delta = measurement.attempt_elapsed_ms - authoritative.attempt_elapsed_ms + + if type(measurement.latency_ms) is int: + if measurement.latency_ms < 0: + errors.append("bridge.latency_negative") + elif measurement.latency_ms > PROPOSED_MAX_RESPONSE_LATENCY_MS: + errors.append("bridge.latency_budget") + elif elapsed_delta is not None and measurement.latency_ms > elapsed_delta: + errors.append("bridge.latency_exceeds_elapsed") + if type(measurement.caregiver_active_ms) is int: + if measurement.caregiver_active_ms < 0: + errors.append("bridge.active_ms_negative") + elif elapsed_delta is not None and measurement.caregiver_active_ms > elapsed_delta: + errors.append("bridge.active_ms_exceeds_elapsed") + if not _is_offset_timestamp(measurement.source_timestamp): + errors.append("bridge.source_timestamp") + + is_linked_clarification = draft.clarification_of_proposal_id is not None + if type(measurement.is_clarification) is bool and measurement.is_clarification != is_linked_clarification: + errors.append("bridge.clarification_binding") + if is_linked_clarification and draft.clarification_of_proposal_id not in authoritative.accepted_proposal_ids: + errors.append("bridge.clarification_source") + + next_consultations = authoritative.consultations_used + 1 + next_clarifications = authoritative.clarifications_used + int(measurement.is_clarification is True) + next_active_ms = ( + authoritative.caregiver_active_ms_used + measurement.caregiver_active_ms + if type(measurement.caregiver_active_ms) is int + else authoritative.caregiver_active_ms_used + ) - # The generic source-neutral validator deliberately remains fixture-only. - # A human proposal is acceptable here only if its sole generic-validation - # failure is exactly the expected live-source authorization error and every - # self-pilot gate above has passed. - generic_validation = validate_proposal(request, proposal) - unexpected_generic_errors = tuple( - error - for error in generic_validation.errors - if error != "proposal.live_source_not_authorized" - ) - if "proposal.live_source_not_authorized" not in generic_validation.errors: - unexpected_generic_errors += ("proposal.expected_global_rejection_missing",) - if proposal.proposal_id in state.accepted_proposal_ids: - unexpected_generic_errors += ("proposal.replay",) - if unexpected_generic_errors: - return HumanBridgeResult( - accepted=False, - errors=tuple(f"bridge.{error}" for error in unexpected_generic_errors), - proposal=None, - state=state, + if next_consultations > PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT: + errors.append("bridge.consultation_budget") + if next_clarifications > PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT: + errors.append("bridge.clarification_budget") + if next_active_ms > PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT: + errors.append("bridge.active_time_budget") + + if errors: + return HumanBridgeResult( + accepted=False, + errors=tuple(errors), + proposal=None, + state=_current_state_or(state), + ) + + accounting = CaregiverAccounting( + consultation_count=1, + clarification_count=int(measurement.is_clarification), + latency_ms=measurement.latency_ms, + human_active_ms=measurement.caregiver_active_ms, + model_calls=None, + money_minor_units=None, + live_cost_status=AccountingStatus.MEASURED, + absence_reason=None, + ) + proposal = proposal_from_text( + request=request, + source_id=authorization.caregiver_id, + source_kind=CaregiverSourceKind.HUMAN, + kind=draft.kind, + text=draft.text, + accounting=accounting, + source_timestamp=measurement.source_timestamp, ) - if proposal.authority is not ProposalAuthority.PROPOSAL_ONLY: - return HumanBridgeResult( - accepted=False, - errors=("bridge.proposal_authority",), - proposal=None, - state=state, + generic_validation = validate_proposal(request, proposal) + unexpected_generic_errors = tuple( + error + for error in generic_validation.errors + if error != "proposal.live_source_not_authorized" ) - if proposal.source_kind is not CaregiverSourceKind.HUMAN: - return HumanBridgeResult( - accepted=False, - errors=("bridge.proposal_source_kind",), - proposal=None, - state=state, + if "proposal.live_source_not_authorized" not in generic_validation.errors: + unexpected_generic_errors += ("proposal.expected_global_rejection_missing",) + if proposal.proposal_id in authoritative.accepted_proposal_ids: + unexpected_generic_errors += ("proposal.replay",) + if unexpected_generic_errors: + return HumanBridgeResult( + accepted=False, + errors=tuple(f"bridge.{error}" for error in unexpected_generic_errors), + proposal=None, + state=authoritative, + ) + + if proposal.authority is not ProposalAuthority.PROPOSAL_ONLY: + return HumanBridgeResult(False, ("bridge.proposal_authority",), None, authoritative) + if proposal.source_kind is not CaregiverSourceKind.HUMAN: + return HumanBridgeResult(False, ("bridge.proposal_source_kind",), None, authoritative) + if draft_validation.payload_sha256 != proposal.payload_sha256: + return HumanBridgeResult(False, ("bridge.payload_digest",), None, authoritative) + + _AUTHORITATIVE_STATE = HumanPilotAttemptState( + attempt_id=SELF_HUMAN_ATTEMPT_ID, + generation=authoritative.generation + 1, + consultations_used=next_consultations, + clarifications_used=next_clarifications, + caregiver_active_ms_used=next_active_ms, + attempt_elapsed_ms=measurement.attempt_elapsed_ms, + live_enabled=True, + disabled_at_elapsed_ms=None, + accepted_request_ids=authoritative.accepted_request_ids + (request.request_id,), + accepted_proposal_ids=authoritative.accepted_proposal_ids + (proposal.proposal_id,), ) - if draft_validation.payload_sha256 != proposal.payload_sha256: return HumanBridgeResult( - accepted=False, - errors=("bridge.payload_digest",), - proposal=None, - state=state, + accepted=True, + errors=(), + proposal=proposal, + state=_AUTHORITATIVE_STATE, ) - - next_state = HumanPilotAttemptState( - consultations_used=next_consultations, - clarifications_used=next_clarifications, - caregiver_active_ms_used=next_active_ms, - attempt_elapsed_ms=measurement.attempt_elapsed_ms, - live_enabled=True, - disabled_at_elapsed_ms=None, - accepted_request_ids=state.accepted_request_ids + (request.request_id,), - accepted_proposal_ids=state.accepted_proposal_ids + (proposal.proposal_id,), - ) - return HumanBridgeResult( - accepted=True, - errors=(), - proposal=proposal, - state=next_state, - ) diff --git a/tests/test_phase3_self_human_live_bridge.py b/tests/test_phase3_self_human_live_bridge.py index 821c53f..eaabf40 100644 --- a/tests/test_phase3_self_human_live_bridge.py +++ b/tests/test_phase3_self_human_live_bridge.py @@ -4,6 +4,7 @@ import pytest +import sudachi_life.phase3.human_live as live_module from sudachi_life.phase3.caregiver import ( ACTIVE_SOURCE_KINDS, AccountingStatus, @@ -16,7 +17,9 @@ from sudachi_life.phase3.human_live import ( HUMAN_PILOT_LIVE_BRIDGE_VERSION, LOCAL_STRUCTURED_HUMAN_TRANSPORT, + SELF_HUMAN_ATTEMPT_ID, SELF_HUMAN_AUTHORIZATION_RECORD, + SELF_HUMAN_CAREGIVER_ID, SELF_HUMAN_CONSENT_NOTICE_VERSION, SELF_HUMAN_PILOT_ID, HumanConsultationMeasurement, @@ -38,22 +41,25 @@ HumanProposalDraft, ) -_CAREGIVER_ID = "caregiver:pseudo:0123456789abcdef0123456789abcdef" -_ATTEMPT_ID = "attempt:self-human-pilot-v1-001" + +@pytest.fixture(autouse=True) +def _reset_process_authoritative_state(): + with live_module._SESSION_LOCK: + live_module._AUTHORITATIVE_STATE = None + yield + with live_module._SESSION_LOCK: + live_module._AUTHORITATIVE_STATE = None def _authorization(): - return accepted_self_human_pilot_v1_authorization( - attempt_id=_ATTEMPT_ID, - caregiver_id=_CAREGIVER_ID, - ) + return accepted_self_human_pilot_v1_authorization() def _request(*, sequence_ordinal: int = 1) -> CaregiverRequest: return CaregiverRequest( request_id=f"request:self-human-{sequence_ordinal:03d}", study_id="study:self-human-pilot-v1", - attempt_id=_ATTEMPT_ID, + attempt_id=SELF_HUMAN_ATTEMPT_ID, episode_id="episode:self-human-pilot-v1-001", organism_id="sudachi-self-human-v1", lineage_generation=0, @@ -66,7 +72,7 @@ def _draft(*, sequence_ordinal: int = 1, **changes: object) -> HumanProposalDraf values: dict[str, object] = { "draft_id": f"draft:self-human-{sequence_ordinal:03d}", "request_id": f"request:self-human-{sequence_ordinal:03d}", - "caregiver_id": _CAREGIVER_ID, + "caregiver_id": SELF_HUMAN_CAREGIVER_ID, "sequence_ordinal": sequence_ordinal, "kind": ProposalKind.EXPLANATION, "text": "The visible marker supports inspecting before moving.", @@ -96,6 +102,10 @@ def _measurement( ) +def _start(): + return start_self_human_pilot_attempt(authorization=_authorization()) + + def _accept( *, authorization=None, @@ -104,9 +114,10 @@ def _accept( draft: HumanProposalDraft | None = None, measurement: HumanConsultationMeasurement | None = None, ): + actual_state = state if state is not None else _start() return accept_self_human_proposal( authorization=authorization or _authorization(), - state=state or start_self_human_pilot_attempt(), + state=actual_state, request=request or _request(), draft=draft or _draft(), measurement=measurement or _measurement(), @@ -116,24 +127,13 @@ def _accept( ) -def _valid_used_state(*, consultations: int, clarifications: int = 0) -> HumanPilotAttemptState: - request_ids = tuple(f"request:used:{index}" for index in range(consultations)) - proposal_ids = tuple(f"proposal:{index:064x}" for index in range(consultations)) - return HumanPilotAttemptState( - consultations_used=consultations, - clarifications_used=clarifications, - caregiver_active_ms_used=10_000, - attempt_elapsed_ms=20_000, - accepted_request_ids=request_ids, - accepted_proposal_ids=proposal_ids, - ) - - -def test_authorization_is_exact_self_only_owner_approved_scope() -> None: +def test_authorization_is_one_exact_attempt_and_caregiver() -> None: authorization = _authorization() assert authorization.protocol_version == HUMAN_PILOT_LIVE_BRIDGE_VERSION assert authorization.pilot_id == SELF_HUMAN_PILOT_ID + assert authorization.attempt_id == SELF_HUMAN_ATTEMPT_ID + assert authorization.caregiver_id == SELF_HUMAN_CAREGIVER_ID assert authorization.transport == LOCAL_STRUCTURED_HUMAN_TRANSPORT assert authorization.authorization_record == SELF_HUMAN_AUTHORIZATION_RECORD assert authorization.consent_notice_version == SELF_HUMAN_CONSENT_NOTICE_VERSION @@ -146,29 +146,54 @@ def test_authorization_is_exact_self_only_owner_approved_scope() -> None: assert validate_self_human_pilot_authorization(authorization) == () +def test_second_attempt_or_caregiver_cannot_mint_equivalent_authorization() -> None: + authorization = _authorization() + + wrong_attempt = replace(authorization, attempt_id="attempt:self-human-pilot-v1-002") + wrong_caregiver = replace( + authorization, + caregiver_id="caregiver:pseudo:ffffffffffffffffffffffffffffffff", + ) + + assert "authorization.attempt_id" in validate_self_human_pilot_authorization(wrong_attempt) + assert "authorization.caregiver_id" in validate_self_human_pilot_authorization(wrong_caregiver) + + +def test_attempt_start_is_process_authoritative_and_one_shot() -> None: + authorization = _authorization() + first = start_self_human_pilot_attempt(authorization=authorization) + + assert first.attempt_id == SELF_HUMAN_ATTEMPT_ID + assert first.generation == 0 + with pytest.raises(RuntimeError): + start_self_human_pilot_attempt(authorization=authorization) + + def test_live_bridge_accepts_structured_self_human_proposal_only() -> None: - result = _accept() + state = _start() + result = _accept(state=state) assert result.accepted is True assert result.errors == () assert result.proposal is not None proposal = result.proposal assert proposal.source_kind is CaregiverSourceKind.HUMAN - assert proposal.source_id == _CAREGIVER_ID + assert proposal.source_id == SELF_HUMAN_CAREGIVER_ID assert proposal.authority is ProposalAuthority.PROPOSAL_ONLY assert proposal.accounting.live_cost_status is AccountingStatus.MEASURED assert proposal.accounting.human_active_ms == 5_000 assert proposal.accounting.latency_ms == 2_000 assert proposal.accounting.model_calls is None assert proposal.accounting.money_minor_units is None + assert result.state.generation == 1 assert result.state.consultations_used == 1 - assert result.state.caregiver_active_ms_used == 5_000 assert result.state.accepted_request_ids == (_request().request_id,) assert result.state.accepted_proposal_ids == (proposal.proposal_id,) -def test_generic_validator_remains_fixture_only_and_rejects_live_human() -> None: - result = _accept() +def test_generic_validator_remains_fixture_only() -> None: + state = _start() + result = _accept(state=state) assert result.proposal is not None generic = validate_proposal(_request(), result.proposal) @@ -179,162 +204,155 @@ def test_generic_validator_remains_fixture_only_and_rejects_live_human() -> None assert CaregiverSourceKind.HUMAN not in ACTIVE_SOURCE_KINDS -def test_wrong_attempt_or_caregiver_binding_fails_closed() -> None: - wrong_request = replace(_request(), attempt_id="attempt:other") - wrong_draft = _draft(caregiver_id="caregiver:pseudo:ffffffffffffffffffffffffffffffff") +def test_fresh_state_reset_is_rejected_after_progress() -> None: + state0 = _start() + first = _accept(state=state0) + assert first.accepted is True + + forged_fresh = HumanPilotAttemptState(attempt_id=SELF_HUMAN_ATTEMPT_ID) + replay = _accept( + state=forged_fresh, + request=_request(sequence_ordinal=2), + draft=_draft(sequence_ordinal=2), + measurement=_measurement(elapsed_ms=40_000), + ) - attempt_result = _accept(request=wrong_request) - caregiver_result = _accept(draft=wrong_draft) + assert replay.accepted is False + assert "bridge.stale_or_forked_state" in replay.errors + assert replay.state == first.state - assert "bridge.attempt_binding" in attempt_result.errors - assert "bridge.caregiver_binding" in caregiver_result.errors - assert attempt_result.proposal is None - assert caregiver_result.proposal is None +def test_parallel_fork_from_same_prior_state_is_rejected() -> None: + state0 = _start() + first = _accept(state=state0) + assert first.accepted is True -def test_relaxed_or_non_self_authorization_fails_closed() -> None: - authorization = _authorization() - relaxed = replace( - authorization, - authorization_record="chat-only", - consent_notice_version="unknown", - self_only=False, - project_owner_is_caregiver=False, - consent_attested=False, - ethics_review_status=EthicsReviewStatus.UNRESOLVED, - raw_chat_transcript_retained=True, - public_raw_payload_default=True, - third_party_participants=1, + fork = _accept( + state=state0, + request=_request(sequence_ordinal=2), + draft=_draft(sequence_ordinal=2), + measurement=_measurement(elapsed_ms=40_000), ) - errors = validate_self_human_pilot_authorization(relaxed) - result = _accept(authorization=relaxed) - - assert "authorization.record" in errors - assert "authorization.consent_notice_version" in errors - assert "authorization.self_only" in errors - assert "authorization.owner_is_caregiver" in errors - assert "authorization.consent_attested" in errors - assert "authorization.ethics_review_status" in errors - assert "authorization.raw_chat_transcript" in errors - assert "authorization.public_raw_payload" in errors - assert "authorization.third_party_participants" in errors - assert result.accepted is False - assert result.proposal is None + assert fork.accepted is False + assert "bridge.stale_or_forked_state" in fork.errors + assert fork.state == first.state -def test_privacy_and_visible_reference_violations_are_rejected() -> None: - draft = _draft( - contains_personal_data=True, - contains_secret=True, - contains_third_party_confidential_data=True, - observation_ids=("observation:hidden",), +def test_stale_enabled_state_cannot_bypass_disablement() -> None: + state0 = _start() + first = _accept(state=state0) + disabled = disable_self_human_bridge( + authorization=_authorization(), + state=first.state, + attempt_elapsed_ms=40_000, ) - result = _accept(draft=draft) - assert result.accepted is False - assert "bridge.draft.personal_data_forbidden" in result.errors - assert "bridge.draft.secret_forbidden" in result.errors - assert "bridge.draft.third_party_confidential_data_forbidden" in result.errors - assert "bridge.draft.observation_ids.not_allowed" in result.errors - - -def test_latency_active_time_and_wall_budgets_fail_closed() -> None: - latency = _accept( - measurement=_measurement( - elapsed_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 2, - latency_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 1, - ) + stale_after_disable = _accept( + state=first.state, + request=_request(sequence_ordinal=2), + draft=_draft(sequence_ordinal=2), + measurement=_measurement(elapsed_ms=50_000), ) - active = _accept( - measurement=_measurement( - elapsed_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 2, - active_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 1, - ) + + assert disabled.live_enabled is False + assert stale_after_disable.accepted is False + assert "bridge.stale_or_forked_state" in stale_after_disable.errors + assert "bridge.disabled" in stale_after_disable.errors + assert stale_after_disable.state == disabled + + +def test_current_disabled_state_rejects_post_cutoff_submission() -> None: + state0 = _start() + disabled = disable_self_human_bridge( + authorization=_authorization(), + state=state0, + attempt_elapsed_ms=10_000, ) - wall = _accept( - measurement=_measurement(elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1) + result = _accept( + state=disabled, + measurement=_measurement(elapsed_ms=20_000), ) - assert "bridge.latency_budget" in latency.errors - assert "bridge.active_time_budget" in active.errors - assert "bridge.attempt_wall_budget" in wall.errors - - -def test_measured_time_must_fit_inside_elapsed_attempt_interval() -> None: - latency = _accept(measurement=_measurement(elapsed_ms=1_000, latency_ms=1_001, active_ms=500)) - active = _accept(measurement=_measurement(elapsed_ms=1_000, latency_ms=500, active_ms=1_001)) - - assert "bridge.latency_exceeds_elapsed" in latency.errors - assert "bridge.active_ms_exceeds_elapsed" in active.errors - - -def test_consultation_budget_is_aggregate_and_fixed() -> None: - state = _valid_used_state(consultations=PROPOSED_MAX_CONSULTATIONS_PER_ATTEMPT) - result = _accept(state=state, measurement=_measurement(elapsed_ms=25_000)) - assert result.accepted is False - assert "bridge.consultation_budget" in result.errors - assert result.state is state + assert "bridge.disabled" in result.errors + assert result.state == disabled -def test_clarification_must_link_to_prior_accepted_proposal_and_budget() -> None: - unlinked = _accept(measurement=_measurement(is_clarification=True)) - first = _accept() - assert first.accepted is True +def test_clarification_parent_must_be_actually_accepted() -> None: + state0 = _start() + first = _accept(state=state0) assert first.proposal is not None - linked_draft = _draft( + unknown = _draft( sequence_ordinal=2, - clarification_of_proposal_id=first.proposal.proposal_id, + clarification_of_proposal_id="proposal:" + "f" * 64, ) - linked = _accept( + rejected = _accept( state=first.state, request=_request(sequence_ordinal=2), - draft=linked_draft, + draft=unknown, measurement=_measurement(elapsed_ms=40_000, is_clarification=True), ) + assert "bridge.clarification_source" in rejected.errors - exhausted_state = _valid_used_state( - consultations=2, - clarifications=PROPOSED_MAX_CLARIFICATIONS_PER_ATTEMPT, - ) - exhausted_draft = _draft( - sequence_ordinal=3, - clarification_of_proposal_id=exhausted_state.accepted_proposal_ids[0], + linked = _draft( + sequence_ordinal=2, + clarification_of_proposal_id=first.proposal.proposal_id, ) - exhausted = _accept( - state=exhausted_state, - request=_request(sequence_ordinal=3), - draft=exhausted_draft, + accepted = _accept( + state=first.state, + request=_request(sequence_ordinal=2), + draft=linked, measurement=_measurement(elapsed_ms=40_000, is_clarification=True), ) - - assert "bridge.clarification_binding" in unlinked.errors - assert linked.accepted is True - assert linked.state.clarifications_used == 1 - assert "bridge.clarification_budget" in exhausted.errors + assert accepted.accepted is True + assert accepted.state.clarifications_used == 1 -def test_unknown_clarification_source_fails_closed() -> None: - first = _accept() - unknown = _draft( - sequence_ordinal=2, - clarification_of_proposal_id="proposal:" + "f" * 64, +def test_forged_well_shaped_history_is_not_authoritative() -> None: + state0 = _start() + first = _accept(state=state0) + forged = HumanPilotAttemptState( + attempt_id=SELF_HUMAN_ATTEMPT_ID, + generation=1, + consultations_used=1, + caregiver_active_ms_used=5_000, + attempt_elapsed_ms=30_000, + accepted_request_ids=("request:forged",), + accepted_proposal_ids=("proposal:" + "a" * 64,), ) + result = _accept( - state=first.state, + state=forged, request=_request(sequence_ordinal=2), - draft=unknown, + draft=_draft( + sequence_ordinal=2, + clarification_of_proposal_id="proposal:" + "a" * 64, + ), measurement=_measurement(elapsed_ms=40_000, is_clarification=True), ) assert result.accepted is False - assert "bridge.clarification_source" in result.errors + assert "bridge.stale_or_forked_state" in result.errors + assert result.state == first.state -def test_request_replay_is_rejected() -> None: - first = _accept() +def test_rejected_submission_does_not_advance_authoritative_state() -> None: + state0 = _start() + rejected = _accept(state=state0, draft=_draft(contains_secret=True)) + + assert rejected.accepted is False + assert rejected.state == state0 + + valid = _accept(state=state0) + assert valid.accepted is True + assert valid.state.generation == 1 + + +def test_request_replay_is_rejected_on_current_state() -> None: + state0 = _start() + first = _accept(state=state0) replay = _accept( state=first.state, measurement=_measurement(elapsed_ms=40_000), @@ -344,87 +362,143 @@ def test_request_replay_is_rejected() -> None: assert "bridge.request_replay" in replay.errors -def test_tampered_aggregate_state_fails_closed() -> None: - negative = HumanPilotAttemptState(consultations_used=-1) - count_mismatch = HumanPilotAttemptState( - consultations_used=1, - accepted_request_ids=(), - accepted_proposal_ids=(), +def test_budget_and_elapsed_time_controls_fail_closed() -> None: + state0 = _start() + + latency = _accept( + state=state0, + measurement=_measurement( + elapsed_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 2, + latency_ms=PROPOSED_MAX_RESPONSE_LATENCY_MS + 1, + ), + ) + active = _accept( + state=state0, + measurement=_measurement( + elapsed_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 2, + active_ms=PROPOSED_MAX_CAREGIVER_ACTIVE_MS_PER_ATTEMPT + 1, + ), + ) + wall = _accept( + state=state0, + measurement=_measurement(elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1), ) - negative_result = _accept(state=negative) - mismatch_result = _accept(state=count_mismatch) + assert "bridge.latency_budget" in latency.errors + assert "bridge.active_time_budget" in active.errors + assert "bridge.attempt_wall_budget" in wall.errors - assert "bridge.state.consultations_used" in negative_result.errors - assert "bridge.state.request_count" in mismatch_result.errors - assert "bridge.state.proposal_count" in mismatch_result.errors +def test_measured_time_must_fit_elapsed_delta() -> None: + state0 = _start() + latency = _accept( + state=state0, + measurement=_measurement(elapsed_ms=1_000, latency_ms=1_001, active_ms=500), + ) + active = _accept( + state=state0, + measurement=_measurement(elapsed_ms=1_000, latency_ms=500, active_ms=1_001), + ) -def test_measurement_bool_masquerading_as_integer_fails_closed() -> None: - measurement = HumanConsultationMeasurement( - latency_ms=True, # type: ignore[arg-type] - caregiver_active_ms=1, - attempt_elapsed_ms=10, - source_timestamp="2026-08-30T14:30:00+09:00", + assert "bridge.latency_exceeds_elapsed" in latency.errors + assert "bridge.active_ms_exceeds_elapsed" in active.errors + + +def test_privacy_visible_reference_and_caregiver_identity_violations_reject() -> None: + state0 = _start() + draft = _draft( + caregiver_id="caregiver:pseudo:ffffffffffffffffffffffffffffffff", + contains_personal_data=True, + contains_secret=True, + contains_third_party_confidential_data=True, + observation_ids=("observation:hidden",), ) - result = _accept(measurement=measurement) + result = _accept(state=state0, draft=draft) - assert result.accepted is False - assert "bridge.latency_ms_type" in result.errors + assert "bridge.caregiver_binding" in result.errors + assert "bridge.draft.personal_data_forbidden" in result.errors + assert "bridge.draft.secret_forbidden" in result.errors + assert "bridge.draft.third_party_confidential_data_forbidden" in result.errors + assert "bridge.draft.observation_ids.not_allowed" in result.errors -def test_offset_timestamp_is_required_for_live_provenance() -> None: - naive = _accept(measurement=_measurement(timestamp="2026-08-30T14:30:00")) - malformed = _accept(measurement=_measurement(timestamp="not-a-time")) +def test_wrong_attempt_and_relaxed_authorization_reject() -> None: + state0 = _start() + wrong_request = replace(_request(), attempt_id="attempt:other") + wrong_auth = replace( + _authorization(), + self_only=False, + project_owner_is_caregiver=False, + ethics_review_status=EthicsReviewStatus.UNRESOLVED, + third_party_participants=1, + ) - assert "bridge.source_timestamp" in naive.errors - assert "bridge.source_timestamp" in malformed.errors + request_result = _accept(state=state0, request=wrong_request) + authorization_result = _accept(state=state0, authorization=wrong_auth) + assert "bridge.attempt_binding" in request_result.errors + assert "authorization.self_only" in authorization_result.errors + assert "authorization.owner_is_caregiver" in authorization_result.errors + assert "authorization.ethics_review_status" in authorization_result.errors + assert "authorization.third_party_participants" in authorization_result.errors -def test_disablement_technically_prevents_post_cutoff_proposals() -> None: - accepted = _accept() - disabled = disable_self_human_bridge(accepted.state, attempt_elapsed_ms=40_000) - after_cutoff = _accept( - state=disabled, - request=_request(sequence_ordinal=2), - draft=_draft(sequence_ordinal=2), - measurement=_measurement(elapsed_ms=50_000), + +def test_timestamp_and_integer_types_fail_closed() -> None: + state0 = _start() + naive = _accept( + state=state0, + measurement=_measurement(timestamp="2026-08-30T14:30:00"), + ) + bool_latency = _accept( + state=state0, + measurement=HumanConsultationMeasurement( + latency_ms=True, # type: ignore[arg-type] + caregiver_active_ms=1, + attempt_elapsed_ms=10, + source_timestamp="2026-08-30T14:30:00+09:00", + ), ) - assert disabled.live_enabled is False - assert disabled.disabled_at_elapsed_ms == 40_000 - assert after_cutoff.accepted is False - assert "bridge.disabled" in after_cutoff.errors - assert after_cutoff.proposal is None + assert "bridge.source_timestamp" in naive.errors + assert "bridge.latency_ms_type" in bool_latency.errors -def test_disablement_time_cannot_regress_or_exceed_wall_budget() -> None: - state = replace(start_self_human_pilot_attempt(), attempt_elapsed_ms=20_000) +def test_disablement_is_one_way_and_time_bounded() -> None: + state0 = _start() + state1 = _accept(state=state0).state with pytest.raises(ValueError): - disable_self_human_bridge(state, attempt_elapsed_ms=19_999) + disable_self_human_bridge( + authorization=_authorization(), + state=state0, + attempt_elapsed_ms=40_000, + ) with pytest.raises(ValueError): disable_self_human_bridge( - state, - attempt_elapsed_ms=PROPOSED_MAX_ATTEMPT_WALL_MS + 1, + authorization=_authorization(), + state=state1, + attempt_elapsed_ms=state1.attempt_elapsed_ms - 1, ) - -def test_elapsed_time_cannot_move_backwards_between_consultations() -> None: - state = replace(start_self_human_pilot_attempt(), attempt_elapsed_ms=30_000) - result = _accept(state=state, measurement=_measurement(elapsed_ms=29_999)) - - assert result.accepted is False - assert "bridge.elapsed_time_regression" in result.errors + disabled = disable_self_human_bridge( + authorization=_authorization(), + state=state1, + attempt_elapsed_ms=40_000, + ) + with pytest.raises(ValueError): + disable_self_human_bridge( + authorization=_authorization(), + state=disabled, + attempt_elapsed_ms=50_000, + ) -def test_live_bridge_exposes_no_direct_execution_or_model_authority() -> None: - result = _accept() - assert result.proposal is not None +def test_public_surface_contains_no_direct_execution_authority() -> None: + authorization = _authorization() + state = _start() - proposal = result.proposal - assert not hasattr(proposal, "execute") - assert not hasattr(proposal, "command") - assert not hasattr(proposal, "writer") - assert proposal.accounting.model_calls is None - assert proposal.accounting.money_minor_units is None + for obj in (authorization, state): + assert not hasattr(obj, "action") + assert not hasattr(obj, "writer") + assert not hasattr(obj, "execute") + assert not hasattr(obj, "command")