From d475cb6a9a12ee3cb9eba4dd2e5adfab46bf8b53 Mon Sep 17 00:00:00 2001 From: huangruiteng <14976749+huangruiteng@users.noreply.github.com> Date: Wed, 16 Sep 2026 18:31:46 +0800 Subject: [PATCH] feat(steward): make a team plan name the Goal it staffs The team preview contract did not say which Goal a plan was for. The admission that validates its lanes is given host facts for one Goal's registered Agents, while the apply derived its Goal from the settlement it arrived in, so the same plan could be admitted against one Goal's Agents and materialized under another. The wiring slice that has to supply `team_plan_context` cannot be honest until that ambiguity is gone: there was no fact for the host to describe. A validated preview now names the exact Goal it staffs, next to the lanes, the quota envelope, the acceptance signal and the stop condition. The id is a registry-shaped id rather than free text, so a path, a sentence or an unbounded string cannot become the Goal a settlement materializes into. The apply refuses a plan whose named Goal differs from its settlement, before it loads anything or writes anything, so admission and settlement always describe one Goal. An unknown Goal is still refused by its own check, which the existing test now exercises through a plan that names that same unknown Goal instead of relying on the settlement's Goal. Verified: tests/test_steward_team_plan_preview.py, tests/test_steward_team_plan_apply.py, tests/extensions/test_governed_capability_execution.py and tests/test_manager_ssh_evidence.py 67 passed, including a preview that omits the Goal, four shapes of invalid Goal id, a plan retargeted to another Goal that creates nothing and still lets the named Goal's own plan apply, and the unknown-Goal refusal. examples/docs-governance-smoke.py ok. Signed-off-by: huangruiteng <14976749+huangruiteng@users.noreply.github.com> --- .../rfcs/harness-selection-dsh-pi-v0.md | 6 +++- .../rfcs/harness-selection-dsh-pi-v0.zh-CN.md | 8 +++-- .../governed_transition_proposal.py | 30 ++++++++++++++----- tests/test_steward_team_plan_apply.py | 20 ++++++++++++- tests/test_steward_team_plan_preview.py | 19 ++++++++++++ 5 files changed, 72 insertions(+), 11 deletions(-) diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md index dde20aef8d..1873cfd42a 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.md @@ -496,6 +496,8 @@ A proposal of kind `steward_team_plan_preview` (`steward_team_plan_preview_v0`) is validated before anything may be applied, and a validated preview names, and may not invent: +- the exact Goal the plan staffs, so the admission that validates its lanes and + the settlement that materializes them describe one Goal rather than two; - each lane and the Agent that runs it, resolved from the Agents Core already registers for the Goal, at most 8 lanes; - that lane's first bounded Todo, with its declared priority (P0..P3), task @@ -529,7 +531,9 @@ Shipped enforcement, in delivery order: kind at `PRE_SETTLEMENT`. The apply re-validates the proposal against the Goal's registered Agents and the shipped advancement action kinds, creates the first bounded Todo of each *ready* lane through the canonical Todo owner, - creates nothing for a gap lane, and refuses an unknown Goal before any write. + creates nothing for a gap lane, refuses an unknown Goal before any write, and + refuses a plan whose named Goal differs from its settlement, so a plan + admitted against one Goal's Agents cannot be retargeted into another's. The receipt records the proposal digest, so a replayed settlement reuses the same lane Todo instead of adding a second row, and the receipt names every lane Todo the settlement ensured. diff --git a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md index 0fce316d8f..2d10e0c738 100644 --- a/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md +++ b/docs/architecture/rfcs/harness-selection-dsh-pi-v0.zh-CN.md @@ -389,6 +389,8 @@ Chat Turn 也早已把 `response.proposals` 投影成 `proposal.ready` 事件。 kind 为 `steward_team_plan_preview`(`steward_team_plan_preview_v0`)的提案,在任何落地 之前先被校验;校验通过的预览必须点名、且不得编造: +- 这次配人的**确切 Goal**,使"验证 lanes 的准入"与"建成 lanes 的结算"描述的是同一个 Goal + 而不是两个; - 每条 lane 及其运行的 Agent,且只能来自 Core 已为该 Goal 注册的 Agent,最多 8 条 lane; - 该 lane 的首个有界 Todo,含其声明优先级(P0..P3)、task class 与 action kind; - 约束这些 lane 的 quota 包络; @@ -413,8 +415,10 @@ Todo 创建、quota 或 goal policy——复用预览点名的身份,不得扩 3. **落地**(`#4524`,`c159a15b3`):受治理提案所有者在 `PRE_SETTLEMENT` 相位分派该 kind, 落地时重新按本 Goal 已注册 Agent 与本机 shipment 的 advancement action kind 校验,经 canonical Todo owner 为每条 **ready** lane 创建首个有界 Todo,gap lane 不创建任何东西, - 未知 Goal 在任何写入前就被拒绝,回执记录 proposal digest,因此重放结算复用同一条 lane - Todo 而不会新增第二行,并且回执点名这次确保的每一条 lane Todo。 + 未知 Goal 在任何写入前就被拒绝,且**点名 Goal 与结算 Goal 不一致的计划会被拒绝**,因此 + 按某个 Goal 的 Agent 通过准入的计划无法被改投到另一个 Goal;回执记录 proposal digest, + 因此重放结算复用同一条 lane Todo 而不会新增第二行,并且回执点名这次确保的每一条 lane + Todo。 这条入端口径目前在线上仍是**惰性**的,本节不作相反声明:还没有任何生产调用方传入 `team_plan_context`,因此模型产出的预览会在准入处被丢弃,而不会浮现给业主确认;提供准入事实 diff --git a/loopx/control_plane/work_items/governed_transition_proposal.py b/loopx/control_plane/work_items/governed_transition_proposal.py index 5c3f16a830..ad67f03133 100644 --- a/loopx/control_plane/work_items/governed_transition_proposal.py +++ b/loopx/control_plane/work_items/governed_transition_proposal.py @@ -263,19 +263,26 @@ def _apply_team_plan( ) -> dict[str, Any]: """Create the confirmed lanes' first bounded Todos through the Todo owner. - The plan is re-validated here against this Goal's registered Agents and the - shipped advancement action kinds, so a proposal cannot become work by - bypassing admission. Only lanes the preview already marked ready are - materialized; a lane the preview reported as a gap stays a gap and creates - nothing, and the canonical Todo owner decides whether a row is added or - reused, which makes a replayed settlement idempotent. - """ + The plan is re-validated here against this Goal's registered Agents and the + shipped advancement action kinds, so a proposal cannot become work by + bypassing admission. Only lanes the preview already marked ready are + materialized; a lane the preview reported as a gap stays a gap and creates + nothing, and the canonical Todo owner decides whether a row is added or + reused, which makes a replayed settlement idempotent. + """ from ...agent_registry import registered_agent_ids_for_goal from ...history import load_registry from ...registry import registry_goals from ..todos.contract import TODO_ACTION_KIND_ADVANCEMENT_VALUES + # The plan names the Goal it staffs, and it may not be retargeted by the + # settlement it arrives in: admitting a plan against one Goal's agents and + # then creating its lanes under another would be a silent widening. + if str(proposal.get("goal_id") or "") != goal_id: + raise ValueError( + "steward team plan proposal names a different Goal than its settlement" + ) registry = load_registry(registry_path) goal = next( ( @@ -461,6 +468,7 @@ def settle_governed_transition_proposals( STEWARD_TEAM_PLAN_PREVIEW_SCHEMA_VERSION = "steward_team_plan_preview_v0" STEWARD_TEAM_PLAN_LANE_LIMIT = 8 STEWARD_TEAM_PLAN_PRIORITIES = ("P0", "P1", "P2", "P3") +_GOAL_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9_.-]{0,159}$") STEWARD_TEAM_PLAN_GAP_REASONS = ( "agent_not_registered", "capability_not_granted", @@ -501,6 +509,13 @@ def validate_steward_team_plan_preview( raise ValueError("steward team plan preview schema_version is invalid") if plan.get("kind") != STEWARD_TEAM_PLAN_PREVIEW_KIND: raise ValueError("steward team plan preview kind is invalid") + # The plan names the Goal it staffs. Without that, the admission that + # validates its lanes and the settlement that materializes them would each + # have to guess which Goal's agents the host should describe, and a plan + # could be admitted against one Goal's facts and applied under another's. + goal_id = _plan_text(plan.get("goal_id"), "goal_id") + if not _GOAL_ID.fullmatch(goal_id): + raise ValueError("steward team plan preview requires an exact Goal id") registered = {str(value) for value in registered_agent_ids} action_kinds = {str(value) for value in supported_action_kinds} lanes_value = plan.get("lanes") @@ -585,6 +600,7 @@ def validate_steward_team_plan_preview( preview = { "schema_version": STEWARD_TEAM_PLAN_PREVIEW_SCHEMA_VERSION, "kind": STEWARD_TEAM_PLAN_PREVIEW_KIND, + "goal_id": goal_id, "objective": _plan_text(plan.get("objective"), "objective"), "lanes": lanes, "gaps": gaps, diff --git a/tests/test_steward_team_plan_apply.py b/tests/test_steward_team_plan_apply.py index 915a6fdf9f..4cab5f9e0d 100644 --- a/tests/test_steward_team_plan_apply.py +++ b/tests/test_steward_team_plan_apply.py @@ -89,6 +89,7 @@ def _proposal(*, agent_id: str = AGENT_ID, extra_lane: dict | None = None) -> di return { "schema_version": "steward_team_plan_preview_v0", "kind": "steward_team_plan_preview", + "goal_id": GOAL_ID, "proposal_id": "proposal-team-plan", "objective": "Stand up the intake lane", "quota_envelope": {"slots_per_day": 4}, @@ -169,6 +170,8 @@ def test_a_gap_lane_creates_nothing_and_a_replay_adds_no_second_row( def test_an_unknown_goal_is_refused_before_any_todo(tmp_path: Path) -> None: project, registry_path = _fixture(tmp_path) + unknown = _proposal() + unknown["goal_id"] = "goal-that-does-not-exist" with pytest.raises(ValueError, match="unknown Goal"): settle_governed_transition_proposals( @@ -176,7 +179,7 @@ def test_an_unknown_goal_is_refused_before_any_todo(tmp_path: Path) -> None: goal_id="goal-that-does-not-exist", agent_id=AGENT_ID, effect_id="effect-team-plan", - proposals=[_proposal()], + proposals=[unknown], existing_receipts=[], checkpoint=lambda _receipts: None, phase=GovernedTransitionSettlementPhase.PRE_SETTLEMENT, @@ -185,6 +188,21 @@ def test_an_unknown_goal_is_refused_before_any_todo(tmp_path: Path) -> None: assert "loopx:todo " not in _todos(project) +def test_a_plan_cannot_be_retargeted_to_another_goal(tmp_path: Path) -> None: + """Admission facts and the applied Goal have to be the same Goal.""" + + project, registry_path = _fixture(tmp_path) + retargeted = _proposal() + retargeted["goal_id"] = "some-other-goal" + + with pytest.raises(ValueError, match="different Goal than its settlement"): + _settle(registry_path, retargeted) + + # Nothing was created, and the named Goal's own plan still applies. + assert "loopx:todo " not in _todos(project) + assert _settle(registry_path, _proposal())[0]["action"] == "created" + + def _second_lane() -> dict: return { "lane_id": "lane-beta", diff --git a/tests/test_steward_team_plan_preview.py b/tests/test_steward_team_plan_preview.py index 9496873d49..ad37721cbc 100644 --- a/tests/test_steward_team_plan_preview.py +++ b/tests/test_steward_team_plan_preview.py @@ -17,6 +17,7 @@ def _plan(**overrides: object) -> dict[str, object]: plan: dict[str, object] = { "schema_version": STEWARD_TEAM_PLAN_PREVIEW_SCHEMA_VERSION, "kind": STEWARD_TEAM_PLAN_PREVIEW_KIND, + "goal_id": "team-plan-fixture", "objective": "Ship the intake lane", "quota_envelope": {"slots_per_day": 4}, "stop_condition": "Stop when the owner withdraws the request", @@ -50,12 +51,30 @@ def test_a_staffed_lane_becomes_a_preview_that_cannot_apply() -> None: preview = _validate(_plan()) assert preview["applies"] is False + assert preview["goal_id"] == "team-plan-fixture" assert preview["gaps"] == [] assert preview["lanes"][0]["staffing"] == "ready" assert preview["lanes"][0]["first_todo"]["priority"] == "P1" assert preview["quota_envelope"] == {"slots_per_day": 4} +def test_the_preview_must_name_the_goal_it_staffs() -> None: + """Admission and settlement both need one named Goal's facts.""" + + without_goal = _plan() + del without_goal["goal_id"] + with pytest.raises(ValueError, match="goal_id must be a non-empty string"): + _validate(without_goal) + + # A Goal id is an exact registry id, not free text: a path, a sentence or an + # unbounded string cannot become the Goal a settlement materializes into. + for invalid in ("", " ", "../escape", "goal with spaces", "x" * 161): + with pytest.raises(ValueError): + _validate(_plan(goal_id=invalid)) + with pytest.raises(ValueError, match="requires an exact Goal id"): + _validate(_plan(goal_id="../escape")) + + def test_an_unregistered_agent_becomes_a_gap_instead_of_being_invented() -> None: plan = _plan() plan["lanes"][0]["agent_id"] = "agent-not-registered" # type: ignore[index]