修复 Native shell 委派协议回归
Project CI / Repository checks (pull_request) Successful in 1m13s
Project CI / Frontend tests (pull_request) Successful in 3m4s
Project CI / Backend tests (pull_request) Successful in 4m7s
Project CI / Native shell tests (pull_request) Successful in 14m30s

兼容未携带澄清字段的既有委派工具调用

同步原生委派 schema 契约测试

调整试玩失败门禁优先级并补充反向回归

记录委派协议演进与门禁优先级约束
This commit is contained in:
2026-08-12 19:48:01 +08:00
parent 4be14df443
commit 77983c05ed
4 changed files with 98 additions and 15 deletions
@@ -702,16 +702,25 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
.actions
.iter()
.any(|action| action.tool.trim() == "agent.route_manifest");
let latest_playtest_index = observations
.iter()
.rposition(|observation| observation.tool == "preview.validate");
let latest_playtest_is_failed = latest_playtest_index.is_some_and(|index| {
observations[index].status == "failed"
&& observations[index].summary == "浏览器验证未通过,请根据诊断修复后重试"
});
// A Supervisor without a mutation must still hit the same first-mutation
// liveness gate as every other autonomous Agent. Previously the presence
// of the Supervisor role alone could bypass this check.
// liveness gate as every other autonomous Agent. A concrete failed
// playtest is more specific and must reach its repair gate below.
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& loop_index > AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT
&& verification_gate.mutation_revision.is_none()
&& verification_gate.failed_playtest_revision.is_none()
&& !has_mutation
&& !has_code_asset_route
&& plan.response.trim().is_empty()
&& !has_specialist_delegation
&& !latest_playtest_is_failed
{
return Err(format!(
"{AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX}Supervisor 尚未提交首次项目 mutation 或有效协作动作,禁止继续只规划、读取、验证或空转"
@@ -758,10 +767,7 @@ pub(crate) fn validate_agent_runtime_autonomous_plan_liveness(
"{AGENT_RUNTIME_AUTONOMOUS_PREVIEW_AFTER_STATIC_LIVENESS_ERROR_PREFIX};当前 revision {project_revision} 已通过 game.static_smoke,下一步必须只调用 preview.validate 取得当前 revision 的桌面与移动真实试玩凭证;不得继续委派、更新计划、读取、搜索、查询状态、修改项目或返回最终回复"
));
}
let Some(latest_playtest_index) = observations
.iter()
.rposition(|observation| observation.tool == "preview.validate")
else {
let Some(latest_playtest_index) = latest_playtest_index else {
return Ok(());
};
let latest_playtest = &observations[latest_playtest_index];
@@ -1961,4 +1967,53 @@ mod tests {
.expect_err("Supervisor without a playtest must not bypass pre-mutation liveness");
assert!(error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX));
}
#[test]
fn autonomous_supervisor_latest_successful_playtest_does_not_bypass_pre_mutation_gate() {
let verification_gate = AgentRuntimeVerificationGate {
schema_version: "test".to_string(),
project_id: "test".to_string(),
agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
run_id: "supervisor-latest-playtest".to_string(),
requires_verification: false,
mutation_revision: None,
verified_revision: None,
last_mutation_tool: None,
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
failed_playtest_revision: None,
updated_at: 0,
};
let observations = [
AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "failed".to_string(),
summary: "浏览器验证未通过,请根据诊断修复后重试".to_string(),
detail: None,
},
AgentRuntimeToolObservation {
tool: "preview.validate".to_string(),
status: "ok".to_string(),
summary: "浏览器验证通过".to_string(),
detail: None,
},
];
let plan = AgentRuntimeToolPlan {
thinking_summary: "继续只读规划".to_string(),
..AgentRuntimeToolPlan::default()
};
let error = validate_agent_runtime_autonomous_plan_liveness(
GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID,
AGENT_RUNTIME_AUTONOMOUS_PRE_MUTATION_LOOP_LIMIT + 1,
0,
&verification_gate,
&observations,
&plan,
false,
false,
)
.expect_err("latest successful playtest must not retain an earlier failure exemption");
assert!(error.starts_with(AGENT_RUNTIME_AUTONOMOUS_LIVENESS_ERROR_PREFIX));
}
}
@@ -539,14 +539,25 @@ fn validate_native_agent_delegate_input(
true,
)?;
validate_native_delegate_string(object.get("runId"), "runId", 160, true)?;
validate_native_delegate_string(
object.get("continuationOfDelegationId"),
"continuationOfDelegationId",
160,
true,
)?;
validate_native_delegate_string(object.get("questionsSha256"), "questionsSha256", 64, true)?;
validate_native_delegate_string(object.get("answersSha256"), "answersSha256", 64, true)?;
if object.contains_key("continuationOfDelegationId") {
validate_native_delegate_string(
object.get("continuationOfDelegationId"),
"continuationOfDelegationId",
160,
true,
)?;
}
if object.contains_key("questionsSha256") {
validate_native_delegate_string(
object.get("questionsSha256"),
"questionsSha256",
64,
true,
)?;
}
if object.contains_key("answersSha256") {
validate_native_delegate_string(object.get("answersSha256"), "answersSha256", 64, true)?;
}
if object
.get("repairOfDelegationId")
.is_some_and(Value::is_string)
@@ -1589,6 +1600,18 @@ mod tests {
.expect("complete clarification continuation binding");
}
#[test]
fn native_agent_delegate_accepts_legacy_input_without_clarification_fields() {
let mut input = valid_delegate_input(Value::Null, Value::Null);
let object = input.as_object_mut().expect("delegate input object");
object.remove("continuationOfDelegationId");
object.remove("questionsSha256");
object.remove("answersSha256");
validate_native_agent_delegate_input(&input)
.expect("legacy delegate input without clarification fields");
}
#[test]
fn native_agent_delegate_rejects_partial_or_invalid_clarification_binding() {
let mut partial = valid_delegate_input(json!("delegation-id"), Value::Null);
@@ -7723,7 +7723,10 @@ fn agent_native_function_catalog_exposes_each_runtime_tool_with_core_schemas() {
"acceptanceCriteria",
"expectedArtifacts",
"repairOfDelegationId",
"runId"
"runId",
"continuationOfDelegationId",
"questionsSha256",
"answersSha256"
])
);
assert_eq!(
@@ -4740,6 +4740,8 @@
- 正确路径:child 返回短小的 `AGC_NEEDS_USER_INPUT_V1` envelopeRuntime 生成 `needs-user-input` delivery,父 Supervisor 认领后创建自己的 durable `user.input_request`。回答仍绑定原父 run,续建 child 由稳定 delegation identity 幂等控制。
- 验证:重复 wake / Runner 重启不得创建第二个用户输入 action;问题数量、字段长度、问题 SHA 和答案 SHA 不匹配时必须 fail-closed。child 直接请求用户输入仍应保持拒绝。
- 恢复加固:正常 completed child 的最终回复也必须进入 envelope 解析;回答后 pending 会被下一轮动作替换,因此 continuation 不能读取 current pending 作为证据,必须读取原 delivery 上的 durable request/answer 绑定。多个 child 各用一条用户请求逐一收束,禁止把不同 delivery 的问题和答案指纹拍平混用。
- 协议演进:`agent.delegate` 的澄清 continuation 字段虽然在 strict schema 中是 required nullable,但 Runtime 解析器仍必须接受完全未携带这三个字段的既有调用;只允许三者全缺失、全 `null` 或全为合法字符串,部分出现、部分字符串和非法 SHA 均失败关闭。新增 schema 字段时要同步原生函数目录断言与旧调用回归,避免协议修复轮次打乱 Supervisor 协作计划。
- 门禁优先级:已经存在真实 `preview.validate` 失败 observation 或 durable `failed_playtest_revision` 时,具体试玩修复与新 revision 重新验证门禁必须先于通用“首次 mutation”门禁;否则 Runtime 会把明确的试玩修复错误收窄成普通 pre-mutation repair,导致 Supervisor 无法选择正确的协作动作。
## 无限画布延迟草稿与零位移不能制造新状态(2026-08-11)