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
29 changes: 28 additions & 1 deletion loopx/cli_commands/goal_channel_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -245,6 +245,30 @@ def _operation_error_packet(
return packet


def _safe_validation_reason(exc: BaseException) -> str:
"""Return the operator-actionable validation reason without private paths.

The typed-operation normalizer and proposal helpers raise code-authored
ValueErrors ("operation payload_digest does not match payload", "operation
executor binding is invalid", ...). Collapsing every one of them into
"the Goal Channel configuration is invalid" hides the real cause from the
operator and from the agent. Keep the message when it is clearly a
code-authored phrase, and fall back to the exception class otherwise.
"""

text = str(exc).strip()

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[P2] 仅公开明确安全的固定校验原因

New head publishes request-derived text while claiming a public-safe packet; the same base input does not.

最小修复:在既有错误 owner 区分固定公开原因与动态详情;仅输出明确允许的固定消息或类型化安全代码,对未知文本回退,并补完整 CLI 回归。

if (
not text
or len(text) > 240
or "/" in text
or "\\" in text
or "~" in text
or any(ord(character) < 32 for character in text)
):
return type(exc).__name__
return text


def run_goal_channel_operation(
args: argparse.Namespace,
*,
Expand Down Expand Up @@ -352,11 +376,14 @@ def run_goal_channel_operation(
blocker="operation_handoff_conflict",
summary=str(exc),
)
except ValueError:
except ValueError as exc:
# Keep the compatibility blocker, but never discard the real reason the
# typed-operation owner rejected this request.
return _operation_error_packet(
request=request,
blocker="invalid_configuration",
summary="the Goal Channel configuration is invalid",
details={"validation_reason": _safe_validation_reason(exc)},
)
except Exception:
return _operation_error_packet(
Expand Down
24 changes: 24 additions & 0 deletions tests/extensions/test_lark_goal_channel_operation.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
from loopx.cli_commands.goal_channel_operation import (
GoalChannelOperationContext,
_prepare_goal_channel_operation,
_safe_validation_reason,
run_goal_channel_operation,
)
from loopx.control_plane.collaboration.operation_handoff import agent_operation_action
Expand Down Expand Up @@ -2405,3 +2406,26 @@ def failing_record(
assert exc_info.value.failure_stage == "record_delivery_receipt"
assert exc_info.value.external_write_performed is None
assert any("+messages-send" in call for call in calls)


def test_safe_validation_reason_keeps_actionable_code_messages() -> None:
"""A rejected prepare must surface its real reason, not a generic line."""

assert (
_safe_validation_reason(ValueError("operation executor binding is invalid"))
== "operation executor binding is invalid"
)
assert (
_safe_validation_reason(
ValueError("operation payload_digest does not match payload")
)
== "operation payload_digest does not match payload"
)
# Anything that could carry a private path or control characters collapses
# to the exception class instead of reaching the operator packet.
assert (
_safe_validation_reason(ValueError("private detail: /home/operator/secret"))
== "ValueError"
)
assert _safe_validation_reason(ValueError("line\nbreak")) == "ValueError"
assert _safe_validation_reason(ValueError("")) == "ValueError"
Loading