Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions notes/participant-role-management.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ relevant_packages:
- vultron/core/models/participant.py
- vultron/wire/as2/vocab/objects/case_participant.py
- vultron/core/use_cases/query/action_rules.py
- vultron/core/predicates/participants.py
- vultron/core/behaviors/case/nodes/on_behalf_guards.py
- vultron/core/use_cases/triggers/case/add_on_behalf_status.py
- test/core/models/test_participant.py
---

Expand Down Expand Up @@ -268,3 +271,24 @@ exist and they are not interchangeable:
Evaluate `RM.CLOSED` terminal rules *before* the `current == new` no-op check.
Ordering them the other way silently permits a transition out of a terminal
state whenever the target happens to equal the current state.

## Assertion Authority and On-Behalf Exceptions (PRM-06)

Participant status is self-declaratory by default (PRM-06-001, ADR-0084). Two
narrow on-behalf exceptions exist:

- **v→V** (`CS_vf.Vf`): a Case Manager or Case Owner MAY assert vendor awareness
on behalf of a notified-but-not-joined vendor (PRM-06-003).
- **d→D** (`CS_d.D`): the same asserting actors MAY assert deployer deployment
under externally-evidenced exceptional circumstances (PRM-06-004).
- **f→F** (`CS_vf.VF`) is always Vendor-only and cannot be asserted on behalf
of another actor (PRM-06-005).

The **Vendor-implies-V invariant** (PRM-06-002): a participant holding
`CVDRole.VENDOR` cannot assert `CS_vf.vf` (vendor-unaware) — a vendor that
has joined a case is by definition aware of it. Enforced by
`vendor_vf_invariant_ok` in `vultron/core/predicates/participants.py`.

Implementation: `SvcAddOnBehalfStatusUseCase` in
`vultron/core/use_cases/triggers/case/add_on_behalf_status.py`; BT guards
in `vultron/core/behaviors/case/nodes/on_behalf_guards.py`.
98 changes: 98 additions & 0 deletions specs/participant-role-management.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -271,3 +271,101 @@ groups:
tags:
- testing
- tooling
- id: PRM-06
title: Assertion Authority and On-Behalf Exceptions
specs:
- id: PRM-06-001
priority: MUST
kind: protocol
statement: >-
Participant status is self-declaratory by default: each participant
asserts its own RM and VFD state without external approval. This is why
they are *participant* status items (ADR-0084).
rationale: >-
The self-declaratory model keeps state authority with the role holder.
Deviations must be narrow, externally evidenced, and explicitly named
(ADR-0084).
adr:
- ADR-0084
tags:
- protocol
lint_suppress:
- missing_story_reference
- id: PRM-06-002
priority: MUST_NOT
kind: protocol
statement: >-
A participant holding ``CVDRole.VENDOR`` MUST NOT assert a VF state of
``CS_vf.vf`` (vendor-unaware). Valid Vendor VF states are
``{CS_vf.Vf, CS_vf.VF}`` (ADR-0084).
rationale: >-
A vendor that has joined a case is by definition aware of it. Allowing
a vendor to assert the vendor-unaware state (``vf``) would create an
impossible protocol record.
verification: >-
``vendor_vf_invariant_ok`` in ``vultron/core/predicates/participants.py``
enforces this predicate; it is called from both
``ValidateTriggerTransitionsNode._check_vf_role`` (trigger path) and
``CreateParticipantStatusNode._check_vf_precondition`` (BT action path).
adr:
- ADR-0084
tags:
- protocol
- id: PRM-06-003
priority: MAY
kind: protocol
statement: >-
A Case Manager or Case Owner MAY assert ``v→V`` (``CS_vf.Vf``) on behalf
of a Vendor-role holder when the notification or invite is evidenced (any
vendor acknowledgement — including ``Read(Invite(Case))`` — suffices).
Scoped to a vendor notified or invited but not yet — or never — a
participant (ADR-0084).
rationale: >-
Closes the vendor-awareness gap (CONCERN-2087): a vendor can be marked
informed even if it never joins the case, so case history is accurate.
verification: >-
``SvcAddOnBehalfStatusUseCase`` implements this path; the asserting
actor's CASE_MANAGER or CASE_OWNER role is verified by
``CheckOnBehalfAuthorizedNode`` before any write occurs.
adr:
- ADR-0084
tags:
- protocol
- id: PRM-06-004
priority: MAY
kind: protocol
statement: >-
A Case Manager or Case Owner MAY assert ``d→D`` (``CS_d.D``) on behalf
of a Deployer-role holder under the same externally-evidenced pattern
(a MAY, expected to be rare; deployment is normally self-reported by the
Deployer) (ADR-0084).
rationale: >-
Exceptional circumstances (e.g. the deployer no longer active) require
a narrow exception path; the on-behalf restriction keeps it from becoming
a general proxy capability.
adr:
- ADR-0084
tags:
- protocol
- id: PRM-06-005
priority: MUST_NOT
kind: protocol
statement: >-
``f→F`` (fix ready, ``CS_vf.VF``) MUST NOT be asserted by any actor
other than the Vendor-role holder. Fix readiness is not externally
knowable; no on-behalf assertion is ever permitted for this transition
(ADR-0084).
rationale: >-
Allowing a non-vendor to assert fix-readiness would let the Case Actor
fabricate protocol-visible fix state, undermining the meaning of the F
event.
verification: >-
``AddOnBehalfStatusTriggerRequest.vf_state_not_fix_ready`` rejects
``CS_vf.VF`` at the request boundary before any BT runs.
``CreateParticipantStatusNode._check_vf_precondition`` and
``ValidateTriggerTransitionsNode._check_vf_role`` both enforce the
VENDOR-only rule on the self-report path.
adr:
- ADR-0084
tags:
- protocol
39 changes: 38 additions & 1 deletion test/core/predicates/test_participants.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,11 @@
)
from vultron.core.models.dimensions import RmDimension
from vultron.core.models.participant_status import ParticipantStatus
from vultron.core.predicates.participants import all_participants_rm_closed
from vultron.core.predicates.participants import (
all_participants_rm_closed,
vendor_vf_invariant_ok,
)
from vultron.core.states.cs import CS_vf
from vultron.core.states.rm import RM
from vultron.enums.roles import CVDRole

Expand Down Expand Up @@ -137,3 +141,36 @@ def test_mixed_roles_including_case_manager_skipped(self):
roles=[CVDRole.COORDINATOR, CVDRole.CASE_MANAGER],
)
assert all_participants_rm_closed([p]) is True


class TestVendorVfInvariantOk:
"""vendor_vf_invariant_ok: VENDOR participant cannot hold CS_vf.vf (ADR-0084, PRM-06-002)."""

def test_none_vf_state_always_ok(self):
assert vendor_vf_invariant_ok([CVDRole.VENDOR], None) is True

def test_vendor_with_vf_fails(self):
assert vendor_vf_invariant_ok([CVDRole.VENDOR], CS_vf.vf) is False

def test_vendor_with_Vf_ok(self):
assert vendor_vf_invariant_ok([CVDRole.VENDOR], CS_vf.Vf) is True

def test_vendor_with_VF_ok(self):
assert vendor_vf_invariant_ok([CVDRole.VENDOR], CS_vf.VF) is True

def test_non_vendor_with_vf_ok(self):
assert vendor_vf_invariant_ok([CVDRole.COORDINATOR], CS_vf.vf) is True

def test_non_vendor_with_Vf_ok(self):
assert vendor_vf_invariant_ok([CVDRole.COORDINATOR], CS_vf.Vf) is True

def test_empty_roles_with_vf_ok(self):
assert vendor_vf_invariant_ok([], CS_vf.vf) is True

def test_vendor_plus_coordinator_with_vf_fails(self):
assert (
vendor_vf_invariant_ok(
[CVDRole.VENDOR, CVDRole.COORDINATOR], CS_vf.vf
)
is False
)
Loading