Skip to content

Commit df808d4

Browse files
committed
fix: tighten truth boundaries across web and pet flows
- require canonical readback for task and context writeback paths - distinguish accepted versus completed group space operations - make pet review and profile refresh complete on real assistive outputs
1 parent fe436f5 commit df808d4

23 files changed

Lines changed: 1410 additions & 274 deletions

src/cccc/daemon/context/context_ops.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@
4343
from ...kernel.actors import get_effective_role, list_actors
4444
from ...kernel.group import load_group
4545
from ...kernel.query_projections import get_actor_list_projection
46+
from ...kernel.pet_actor import PET_ACTOR_ID
4647
from ...kernel.ledger import append_event
4748
from ...kernel.prompt_files import (
4849
HELP_FILENAME,
@@ -77,6 +78,43 @@ def _error(code: str, message: str, *, details: Optional[Dict[str, Any]] = None)
7778
return DaemonResponse(ok=False, error=DaemonError(code=code, message=message, details=(details or {})))
7879

7980

81+
_PET_FORBIDDEN_CONTEXT_OPS = {
82+
"coordination.brief.update",
83+
"coordination.note.add",
84+
"task.create",
85+
"task.update",
86+
"task.move",
87+
"task.restore",
88+
"task.delete",
89+
"meta.merge",
90+
"role_notes.set",
91+
"agent_state.clear",
92+
}
93+
94+
95+
def _check_pet_context_permission(by: str, op_name: str, *, target_actor_id: Optional[str] = None) -> Optional[str]:
96+
if str(by or "").strip() != PET_ACTOR_ID:
97+
return None
98+
if op_name in _PET_FORBIDDEN_CONTEXT_OPS:
99+
return f"Permission denied: pet cannot run {op_name}"
100+
if op_name == "agent_state.update":
101+
target = str(target_actor_id or "").strip()
102+
if target and target != PET_ACTOR_ID:
103+
return f"Permission denied: pet can only update its own agent_state ({PET_ACTOR_ID})"
104+
return None
105+
106+
107+
def _validate_pet_agent_state_update(raw: Dict[str, Any], *, actor_id: str) -> Optional[str]:
108+
target = str(actor_id or "").strip()
109+
if target != PET_ACTOR_ID:
110+
return f"Permission denied: pet can only update its own agent_state ({PET_ACTOR_ID})"
111+
allowed = {"op", "actor_id", "agent_id", "user_model", "user_profile"}
112+
extra = sorted(key for key in raw.keys() if key not in allowed)
113+
if extra:
114+
return "Permission denied: pet agent_state.update only allows user_model; disallowed keys: " + ", ".join(extra)
115+
return None
116+
117+
80118
def _status_value(value: Any) -> str:
81119
if isinstance(value, Enum):
82120
return str(value.value or "")
@@ -545,6 +583,10 @@ def _check_permission(
545583
if group is None:
546584
return None
547585

586+
pet_err = _check_pet_context_permission(by, op_name, target_actor_id=target_actor_id)
587+
if pet_err:
588+
return pet_err
589+
548590
role = "user" if by == "user" else get_effective_role(group, by)
549591
if role in {"user", "foreman"}:
550592
if op_name in {"agent_state.update", "agent_state.clear"} and target_actor_id and by not in {"system", target_actor_id}:
@@ -1503,6 +1545,10 @@ def _mark_change(index: int, op_name: str, detail: str, *, task_id: str = "") ->
15031545
perm_err = _check_permission(by, op_name, group_id, target_actor_id=actor_id)
15041546
if perm_err:
15051547
raise ValueError(perm_err)
1548+
if by == PET_ACTOR_ID:
1549+
pet_update_err = _validate_pet_agent_state_update(raw, actor_id=actor_id)
1550+
if pet_update_err:
1551+
raise ValueError(pet_update_err)
15061552
agent = _get_or_create_agent(agents_state, actor_id)
15071553
updated = False
15081554
hot = agent.hot if isinstance(agent.hot, AgentStateHot) else AgentStateHot()
@@ -1583,6 +1629,10 @@ def _mark_change(index: int, op_name: str, detail: str, *, task_id: str = "") ->
15831629
perm_err = _check_permission(by, op_name, group_id, target_actor_id=actor_id)
15841630
if perm_err:
15851631
raise ValueError(perm_err)
1632+
if by == PET_ACTOR_ID:
1633+
pet_update_err = _validate_pet_agent_state_update(raw, actor_id=actor_id)
1634+
if pet_update_err:
1635+
raise ValueError(pet_update_err)
15861636
agent = _get_or_create_agent(agents_state, actor_id)
15871637
agent.hot = AgentStateHot()
15881638
agent.warm = AgentStateWarm()

src/cccc/daemon/messaging/chat_ops.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
targets_any_agent,
2121
)
2222
from ...kernel.scope import detect_scope
23+
from ...kernel.pet_actor import PET_ACTOR_ID, get_pet_actor
2324
from ...util.time import utc_now_iso
2425
from .delivery import (
2526
flush_pending_messages,
@@ -37,6 +38,13 @@ def _error(code: str, message: str, *, details: Optional[Dict[str, Any]] = None)
3738
return DaemonResponse(ok=False, error=DaemonError(code=code, message=message, details=(details or {})))
3839

3940

41+
def _is_internal_pet_sender(group: Any, by: str) -> bool:
42+
actor_id = str(by or "").strip()
43+
if actor_id != PET_ACTOR_ID:
44+
return False
45+
return isinstance(get_pet_actor(group), dict)
46+
47+
4048
def _wake_group_on_human_message(
4149
group: Any,
4250
*,
@@ -328,6 +336,11 @@ def handle_send(
328336
group = load_group(group_id)
329337
if group is None:
330338
return _error("group_not_found", f"group not found: {group_id}")
339+
if _is_internal_pet_sender(group, by):
340+
return _error(
341+
"pet_visible_chat_forbidden",
342+
"Pet cannot send or reply visible chat directly; use pet decisions instead.",
343+
)
331344

332345
group = _wake_group_on_human_message(
333346
group,
@@ -546,6 +559,11 @@ def handle_reply(
546559
group = load_group(group_id)
547560
if group is None:
548561
return _error("group_not_found", f"group not found: {group_id}")
562+
if _is_internal_pet_sender(group, by):
563+
return _error(
564+
"pet_visible_chat_forbidden",
565+
"Pet cannot send or reply visible chat directly; use pet decisions instead.",
566+
)
549567

550568
group = _wake_group_on_human_message(
551569
group,

0 commit comments

Comments
 (0)