修复AGC无人值守游戏生成阻断

普通Web工作台默认使用单Supervisor自主生成链路

补齐安全默认返工、结构化诊断与同Run恢复

绑定当前revision、入口摘要和双视口试玩完成凭证

增强Runner跨boot单飞恢复与game-chat来源隔离

新增真实Provider验收入口及确定性回归

同步更新技术方案、开发流程和共享决策
This commit is contained in:
kdletters
2026-08-14 07:14:25 +08:00
parent 7774642365
commit 02fff8f308
52 changed files with 4823 additions and 357 deletions
+1
View File
@@ -24,6 +24,7 @@
"agent-runtime:mixed-swarm-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-static-isolated-autonomous-chat",
"agent-runtime:supervisor-swarm-autonomous-chat-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-autonomous-chat",
"agent-runtime:supervisor-autonomous-playable-lane-defense-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-autonomous-playable-lane-defense",
"agent-runtime:supervisor-game-chat-single-main-playable-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable",
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-e2e": "node scripts/agent-runtime-deterministic-playable-e2e.mjs",
"agent-runtime:supervisor-autonomous-playable-lane-defense-deterministic-self-test": "node scripts/agent-runtime-deterministic-playable-e2e.mjs --self-test",
"agent-runtime:supervisor-swarm-transient-retry-real-e2e": "node scripts/agent-runtime-real-e2e.mjs --suite supervisor-swarm-transient-retry",
@@ -15,6 +15,7 @@ import {
scopedAgentsSuite,
steerRunnerKillSuite,
supervisorAutonomousPlayableLaneDefenseSuite,
supervisorGameChatSingleMainPlayableSuite,
supervisorSwarmAutonomousChatSuite,
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
supervisorSwarmFinalReplyTransientRetrySuite,
@@ -70,6 +71,7 @@ export function parseArguments(args) {
suite === supervisorSwarmToolPlanHandoffRunnerKillSuite ||
suite === supervisorSwarmAutonomousChatSuite ||
suite === supervisorAutonomousPlayableLaneDefenseSuite ||
suite === supervisorGameChatSingleMainPlayableSuite ||
suite === supervisorSwarmStaticIsolatedAutonomousChatSuite ||
suite === supervisorSwarmCollaborationPolicyMixedRecoverySuite ||
suite === steerRunnerKillSuite ||
@@ -28,7 +28,10 @@ import {
isGoalRuntimeSuite,
} from '../suites/goal.mjs';
import { isResponseStreamSuite } from '../suites/response-stream.mjs';
import { isSupervisorAutonomousPlayableLaneDefenseSuite } from '../suites/supervisor-autonomous-playable.mjs';
import {
isSupervisorAutonomousPlayableLaneDefenseSuite,
isSupervisorGameChatSingleMainPlayableSuite,
} from '../suites/supervisor-autonomous-playable.mjs';
import {
isSupervisorSwarmSuite,
supervisorSwarmVerificationFixtureSource,
@@ -40,20 +43,26 @@ import { effectiveAgentLlmConfig } from './config.mjs';
import { runProcess } from './process.mjs';
import { isIsolatedRunnerSuite } from './reporting.mjs';
export async function checkPrerequisites(config) {
const requiredAgents = isUserInputRuntimeSuite()
export function requiredAgentIdsForSuite() {
return isUserInputRuntimeSuite()
? [projectSupervisorAgentId]
: isSupervisorAutonomousPlayableLaneDefenseSuite()
? [projectSupervisorAgentId]
: isSupervisorSwarmSuite()
? [
projectSupervisorAgentId,
supervisorSwarmDesignAgentId,
supervisorSwarmQualityAgentId,
]
: isIsolatedRunnerSuite()
? [mainAgentId]
: [mainAgentId, 'quality-review'];
: isSupervisorGameChatSingleMainPlayableSuite()
? [projectSupervisorAgentId, mainAgentId]
: isSupervisorAutonomousPlayableLaneDefenseSuite()
? [projectSupervisorAgentId]
: isSupervisorSwarmSuite()
? [
projectSupervisorAgentId,
supervisorSwarmDesignAgentId,
supervisorSwarmQualityAgentId,
]
: isIsolatedRunnerSuite()
? [mainAgentId]
: [mainAgentId, 'quality-review'];
}
export async function checkPrerequisites(config) {
const requiredAgents = requiredAgentIdsForSuite();
const llmConfigured = requiredAgents.every((agentId) => {
const effective = effectiveAgentLlmConfig(config, agentId);
return ['apiKey', 'baseUrl', 'model'].every(
@@ -227,6 +227,9 @@ export const supervisorSwarmAutonomousChatSuite =
export const supervisorAutonomousPlayableLaneDefenseSuite =
'supervisor-autonomous-playable-lane-defense';
export const supervisorGameChatSingleMainPlayableSuite =
'supervisor-game-chat-single-main-playable';
export const supervisorSwarmStaticIsolatedAutonomousChatSuite =
'supervisor-swarm-static-isolated-autonomous-chat';
@@ -22,6 +22,7 @@ import {
} from '../harness/app-data.mjs';
import { parseArguments } from '../harness/config.mjs';
import { safeProcessFailureDiagnostic } from '../harness/process.mjs';
import { requiredAgentIdsForSuite } from '../harness/project.mjs';
import { isIsolatedRunnerSuite } from '../harness/reporting.mjs';
import { agentConversationPath } from '../harness/runtime.mjs';
import {
@@ -29,6 +30,7 @@ import {
commandFailureMarker,
commandPassedMarker,
isolatedAgentJoinClaimSchemaVersion,
mainAgentId,
projectSupervisorAgentId,
providerActionBatchSchemaVersion,
repoRoot,
@@ -43,6 +45,7 @@ import {
supervisorCollaborationPolicySchemaVersion,
supervisorCollaborationPolicySnapshotInitialBatchBinding,
supervisorCollaborationPolicySnapshotSchemaVersion,
supervisorGameChatSingleMainPlayableSuite,
supervisorSwarmAutonomousChatSuite,
supervisorSwarmCollaborationPolicyMixedRecoverySuite,
supervisorSwarmFollowupIsolatedReviews,
@@ -63,6 +66,8 @@ import {
buildSupervisorAutonomousPlayableStdin,
collectPartialSupervisorAutonomousPlayableEvidence,
isSupervisorAutonomousPlayableLaneDefenseSuite,
isSupervisorGameChatSingleMainPlayableSuite,
supervisorAutonomousPlayableMode,
} from './supervisor-autonomous-playable.mjs';
import {
buildSupervisorSwarmEvidence,
@@ -150,6 +155,69 @@ export async function runAgentRuntimeRealE2eSelfTests() {
const shellPackage = JSON.parse(
readFileSync(path.join(appRoot, 'package.json'), 'utf8'),
);
const previousSuiteForGameChatPlayable = state.suite;
state.suite = supervisorAutonomousPlayableLaneDefenseSuite;
const professionalPlayableMode = supervisorAutonomousPlayableMode();
const professionalPlayableRequiredAgents = requiredAgentIdsForSuite();
state.suite = supervisorGameChatSingleMainPlayableSuite;
const gameChatPlayableProfile = isolatedSuiteAppDataProfile();
const gameChatPlayableParsedArguments = parseArguments([
'--config-dir',
path.resolve('synthetic-game-chat-playable-config'),
'--suite',
supervisorGameChatSingleMainPlayableSuite,
]);
const gameChatPlayableMode = supervisorAutonomousPlayableMode();
const gameChatPlayableRequiredAgents = requiredAgentIdsForSuite();
const gameChatPlayablePackageCommandsRegistered =
shellPackage.scripts?.[
'agent-runtime:supervisor-game-chat-single-main-playable-real-e2e'
] ===
'node scripts/agent-runtime-real-e2e.mjs --suite supervisor-game-chat-single-main-playable' &&
rootPackage.scripts?.[
'ai-game-creator-shell:agent-runtime:supervisor-game-chat-single-main-playable-real-e2e'
] ===
'npm --prefix apps/ai-game-creator-shell run agent-runtime:supervisor-game-chat-single-main-playable-real-e2e --';
const gameChatPlayableSuiteRegistered =
gameChatPlayableParsedArguments.suite ===
supervisorGameChatSingleMainPlayableSuite &&
isSupervisorGameChatSingleMainPlayableSuite() &&
isSupervisorAutonomousPlayableLaneDefenseSuite() &&
!isSupervisorSwarmSuite() &&
isIsolatedRunnerSuite() &&
isolatedSuiteProtectsSourceAppData() &&
isolatedSuiteUsesSiblingAppData() &&
gameChatPlayableProfile.sentinelName ===
supervisorAutonomousPlayableAppDataSentinelFileName &&
gameChatPlayableProfile.sentinelSchema ===
supervisorAutonomousPlayableAppDataSentinelSchema;
const gameChatPlayableModeValidated =
professionalPlayableMode.gameChatSingleMain === false &&
professionalPlayableMode.rootSource === 'project-supervisor-cli' &&
professionalPlayableMode.evidenceAgentId === projectSupervisorAgentId &&
professionalPlayableMode.scenario ===
'project-supervisor-autonomous-playable-lane-defense' &&
professionalPlayableMode.cliFlags.length === 0 &&
gameChatPlayableMode.gameChatSingleMain === true &&
gameChatPlayableMode.rootSource === 'project-supervisor-game-chat' &&
gameChatPlayableMode.evidenceAgentId === mainAgentId &&
gameChatPlayableMode.scenario ===
'project-supervisor-game-chat-single-main-playable' &&
JSON.stringify(gameChatPlayableMode.cliFlags) ===
JSON.stringify(['--game-chat-smoke']);
const gameChatPlayableRequiredAgentsValidated =
JSON.stringify(professionalPlayableRequiredAgents) ===
JSON.stringify([projectSupervisorAgentId]) &&
JSON.stringify(gameChatPlayableRequiredAgents) ===
JSON.stringify([projectSupervisorAgentId, mainAgentId]);
state.suite = previousSuiteForGameChatPlayable;
assert(
gameChatPlayableSuiteRegistered &&
gameChatPlayablePackageCommandsRegistered &&
gameChatPlayableModeValidated &&
gameChatPlayableRequiredAgentsValidated,
'agent-runtime-real-e2e-self-test-game-chat-playable-suite-invalid',
);
const previousSuiteForAutonomousPlayable = state.suite;
state.suite = supervisorAutonomousPlayableLaneDefenseSuite;
const autonomousPlayableProfile = isolatedSuiteAppDataProfile();
@@ -1593,6 +1661,10 @@ export async function runAgentRuntimeRealE2eSelfTests() {
collaborationPolicySnapshotBindingStable: true,
durableSnapshotEligibilityAndContractBindingValidated: true,
sourceEndpointAbsentLifecycleGuardValidated,
gameChatPlayableSuiteRegistered,
gameChatPlayablePackageCommandsRegistered,
gameChatPlayableModeValidated,
gameChatPlayableRequiredAgentsValidated,
autonomousPlayableSuiteRegistered,
autonomousPlayablePackageCommandsRegistered,
autonomousPlayableDedicatedPathValidated: true,
@@ -52,6 +52,7 @@ import {
supervisorAutonomousPlayableSafeTurnOutcomes,
supervisorAutonomousPlayableSourceFieldMaxChars,
supervisorAutonomousPlayableSourceTotalMaxChars,
supervisorGameChatSingleMainPlayableSuite,
supervisorSwarmSessionId,
supervisorSwarmTerminalSidecarCleanupTimeoutMs,
} from '../runtime-state.mjs';
@@ -65,6 +66,29 @@ import {
validateSupervisorSwarmFinalization,
} from './supervisor-swarm.mjs';
const projectSupervisorCliSource = 'project-supervisor-cli';
const projectSupervisorGameChatSource = 'project-supervisor-game-chat';
const gameChatMainAgentId = 'code-prototype';
export function supervisorAutonomousPlayableMode() {
if (state.suite === supervisorGameChatSingleMainPlayableSuite) {
return {
gameChatSingleMain: true,
rootSource: projectSupervisorGameChatSource,
evidenceAgentId: gameChatMainAgentId,
scenario: 'project-supervisor-game-chat-single-main-playable',
cliFlags: ['--game-chat-smoke'],
};
}
return {
gameChatSingleMain: false,
rootSource: projectSupervisorCliSource,
evidenceAgentId: projectSupervisorAgentId,
scenario: 'project-supervisor-autonomous-playable-lane-defense',
cliFlags: [],
};
}
export function buildSupervisorAutonomousPlayableStdin() {
const task = supervisorAutonomousPlayableLaneDefenseTask;
assert(
@@ -776,6 +800,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
residualSidecars,
) {
const task = supervisorAutonomousPlayableLaneDefenseTask;
const mode = supervisorAutonomousPlayableMode();
const report = state.supervisorAutonomousPlayable.turnReport;
assert(
report?.schemaVersion === 'game-creator-swarm-turn-report.v1' &&
@@ -814,7 +839,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
);
assert(
parentTask?.task === task &&
parentTask.source === 'project-supervisor-cli' &&
parentTask.source === mode.rootSource &&
parentTask.status === 'completed' &&
parentTask.phase === 'completed' &&
parentTask.runProfile === autonomousGameBuildRunProfile &&
@@ -996,7 +1021,7 @@ export async function validateSupervisorAutonomousPlayableEvidence(
binding.rootRunId === state.initialRunId &&
binding.parentAgentId == null &&
binding.parentRunId == null &&
binding.source === 'project-supervisor-cli' &&
binding.source === mode.rootSource &&
binding.profile === autonomousGameBuildRunProfile &&
binding.profileFingerprint ===
hashJsonValue({
@@ -1019,6 +1044,67 @@ export async function validateSupervisorAutonomousPlayableEvidence(
);
const contract = contracts[0];
const receipt = receipts[0];
let evidenceBinding = binding;
let evidenceRunId = state.initialRunId;
if (mode.gameChatSingleMain) {
const manifest = JSON.parse(
decodeUtf8Fatal(
await fs.readFile(path.join(state.projectRoot, '.agent/manifest.json')),
'supervisor-autonomous-playable-manifest-invalid-utf8',
),
);
const tasks = Array.isArray(manifest.tasks) ? manifest.tasks : [];
assert(
tasks.length === 1 &&
tasks[0]?.id === gameChatMainAgentId &&
tasks[0]?.status === 'completed',
'supervisor-game-chat-single-main-manifest-invalid',
);
const rootChildTasks = persistence.taskSnapshot.latest.filter(
(candidate) =>
candidate.parentAgentId === projectSupervisorAgentId &&
candidate.parentRunId === state.initialRunId,
);
const rootChildRuntimes = persistence.runtimeStates.filter(
(candidate) =>
candidate.parentAgentId === projectSupervisorAgentId &&
candidate.parentRunId === state.initialRunId,
);
const codeTask = rootChildTasks[0];
const codeRuntime = rootChildRuntimes[0];
assert(
rootChildTasks.length === 1 &&
codeTask.agentId === gameChatMainAgentId &&
codeTask.status === 'completed' &&
codeTask.phase === 'completed' &&
codeTask.source === 'agent-ready-task-scheduler' &&
rootChildRuntimes.length === 1 &&
codeRuntime.agentId === gameChatMainAgentId &&
codeRuntime.runId === codeTask.runId &&
codeRuntime.phase === 'completed' &&
codeRuntime.pendingToolAction == null &&
codeRuntime.pendingAction == null,
'supervisor-game-chat-single-main-runtime-invalid',
);
evidenceRunId = codeTask.runId;
const codeBindings = runProfileBindings.filter(
(candidate) =>
candidate.agentId === gameChatMainAgentId &&
candidate.runId === evidenceRunId &&
candidate.rootAgentId === projectSupervisorAgentId &&
candidate.rootRunId === state.initialRunId &&
candidate.parentAgentId === projectSupervisorAgentId &&
candidate.parentRunId === state.initialRunId &&
candidate.profile === autonomousGameBuildRunProfile &&
candidate.source === 'agent-ready-task-scheduler' &&
candidate.parentBindingFingerprint === binding.bindingFingerprint,
);
assert(
codeBindings.length === 1,
'supervisor-game-chat-single-main-binding-invalid',
);
evidenceBinding = codeBindings[0];
}
const contractIdentity = {
schemaVersion: contract.schemaVersion,
projectId: contract.projectId,
@@ -1072,23 +1158,22 @@ export async function validateSupervisorAutonomousPlayableEvidence(
await readSupervisorSwarmJsonDirectory('.agent/runtime/verification')
).filter(
(gate) =>
gate.agentId === projectSupervisorAgentId &&
gate.runId === state.initialRunId,
gate.agentId === mode.evidenceAgentId && gate.runId === evidenceRunId,
);
const gate = gates[0];
const staticSmokeAudits = persistence.agentDb.filter(
(record) =>
record.recordType === 'agent.runtime.command.run_limited' &&
record.agentId === projectSupervisorAgentId &&
record.runId === state.initialRunId &&
record.agentId === mode.evidenceAgentId &&
record.runId === evidenceRunId &&
record.commandId === 'game.static_smoke' &&
record.status === 'completed',
);
assert(
gates.length === 1 &&
gate.lastVerificationTool === 'game.static_smoke' &&
gate.lastVerificationStatus === 'passed' &&
gate.verifiedRevision === revision.revision &&
gate.staticSmokeVerifiedRevision === revision.revision &&
staticSmokeAudits.length >= 1,
'supervisor-autonomous-playable-static-smoke-invalid',
);
@@ -1112,9 +1197,10 @@ export async function validateSupervisorAutonomousPlayableEvidence(
assert(
receipt.schemaVersion === autonomousPlaytestReceiptSchemaVersion &&
receipt.projectId === contract.projectId &&
receipt.agentId === projectSupervisorAgentId &&
receipt.runId === state.initialRunId &&
receipt.runProfileBindingFingerprint === binding.bindingFingerprint &&
receipt.agentId === mode.evidenceAgentId &&
receipt.runId === evidenceRunId &&
receipt.runProfileBindingFingerprint ===
evidenceBinding.bindingFingerprint &&
receipt.revision === revision.revision &&
receipt.gameIndex.path === 'game/index.html' &&
receipt.gameIndex.sha256 === finalGameIndexSha256 &&
@@ -1123,8 +1209,8 @@ export async function validateSupervisorAutonomousPlayableEvidence(
receipt.receiptFingerprint === hashJsonValue(receiptIdentity) &&
actionReceipts.filter(
(record) =>
record.agentId === projectSupervisorAgentId &&
record.runId === state.initialRunId &&
record.agentId === mode.evidenceAgentId &&
record.runId === evidenceRunId &&
record.actionId === receipt.actionId &&
record.actionFingerprint === receipt.actionFingerprint &&
record.tool === 'preview.validate' &&
@@ -1511,10 +1597,12 @@ export async function runSupervisorAutonomousPlayableLaneDefenseE2e() {
state.supervisorAutonomousPlayable.privateValues = [task];
state.supervisorSwarm.userTask = task;
state.supervisorSwarm.privateValues = [task];
const mode = supervisorAutonomousPlayableMode();
state.supervisorAutonomousPlayableCliSession = startInteractiveCli([
'--swarm-chat',
'--init',
'--autonomous-game-build',
...mode.cliFlags,
state.projectRoot,
]);
state.supervisorAutonomousPlayable.stdinWriteCount = 1;
@@ -1546,8 +1634,9 @@ export async function runSupervisorAutonomousPlayableLaneDefenseE2e() {
}
export function emptySupervisorAutonomousPlayableEvidence() {
const mode = supervisorAutonomousPlayableMode();
return {
scenario: 'project-supervisor-autonomous-playable-lane-defense',
scenario: mode.scenario,
targetAgentId: projectSupervisorAgentId,
runProfile: autonomousGameBuildRunProfile,
dedicatedZeroInterventionPath: true,
@@ -1688,5 +1777,12 @@ export function emptySupervisorAutonomousPlayableEvidence() {
}
export function isSupervisorAutonomousPlayableLaneDefenseSuite() {
return state.suite === supervisorAutonomousPlayableLaneDefenseSuite;
return (
state.suite === supervisorAutonomousPlayableLaneDefenseSuite ||
state.suite === supervisorGameChatSingleMainPlayableSuite
);
}
export function isSupervisorGameChatSingleMainPlayableSuite() {
return state.suite === supervisorGameChatSingleMainPlayableSuite;
}
@@ -259,10 +259,11 @@ pub(crate) fn validate_safe_game_html_runtime(html: &str, label: &str) -> Result
pub(crate) fn validate_game_html_smoke(html: &str) -> Result<(), String> {
let lower_html = html.to_ascii_lowercase();
validate_closed_game_script_blocks(&lower_html)?;
validate_executable_inline_javascript_syntax(html)?;
if !lower_html.contains("<canvas") {
return Err("游戏入口必须包含可渲染画布".to_string());
}
validate_closed_game_script_blocks(&lower_html)?;
validate_canvas_rendering_html(html, "游戏入口")?;
if !html.contains("requestAnimationFrame") {
return Err("游戏入口必须包含游戏主循环".to_string());
@@ -285,6 +285,79 @@ fn agent_runtime_action_receipt_safe_detail_with_owner(
)?;
return serde_json::to_string(&detail).ok();
}
if observation.tool == "command.run_limited" {
let summary = observation.summary.trim();
if !summary.starts_with("game.static_smoke ") {
return None;
}
let passed = observation.status == "ok" && summary == "game.static_smoke 已完成";
let failure_code = if passed {
None
} else {
Some(match summary {
"game.static_smoke 执行失败" => "static-smoke-failed",
"game.static_smoke 无法取得项目验证锁" => "verification-lock-unavailable",
"game.static_smoke 无法清除旧验证凭证" => "verification-reset-failed",
"game.static_smoke 结果无法形成有效验证凭证" => {
"verification-receipt-invalid"
}
_ => return None,
})
};
let diagnostic_check = observation.detail.as_deref().map_or("none", |detail| {
if detail.contains("JavaScript") {
"javascript-syntax"
} else {
"game-static-contract"
}
});
let persisted = observation
.detail
.as_deref()
.and_then(|value| serde_json::from_str::<serde_json::Value>(value).ok())
.filter(|value| {
value.get("commandId").and_then(serde_json::Value::as_str)
== Some("game.static_smoke")
});
let diagnostic = if let Some(value) = persisted.as_ref() {
if value.get("passed").and_then(serde_json::Value::as_bool) != Some(passed)
|| value.get("failureCode").and_then(serde_json::Value::as_str) != failure_code
{
return None;
}
value
.get("diagnostic")
.and_then(serde_json::Value::as_str)
.map(|value| redact_agent_runtime_error(root, value, 500))
} else {
observation
.detail
.as_deref()
.map(|value| redact_agent_runtime_error(root, value, 500))
};
if !passed && diagnostic.is_none() {
return None;
}
if receipt_owner.is_none() {
return serde_json::to_string(&serde_json::json!({
"commandId": "game.static_smoke",
"passed": passed,
"failureCode": failure_code,
"check": diagnostic_check,
"path": AGENT_RUNTIME_GAME_INDEX_PATH,
}))
.ok();
}
return serde_json::to_string(&serde_json::json!({
"commandId": "game.static_smoke",
"passed": passed,
"failureCode": failure_code,
"check": diagnostic_check,
"path": AGENT_RUNTIME_GAME_INDEX_PATH,
"diagnostic": diagnostic,
}))
.ok();
}
if observation.tool == "image.inspect" {
let detail = serde_json::from_str::<serde_json::Value>(
observation.detail.as_deref().unwrap_or_default(),
@@ -1960,6 +1960,7 @@ mod tests {
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
static_smoke_verified_game_index_sha256: None,
failed_playtest_revision: None,
updated_at: 0,
};
@@ -2001,6 +2002,7 @@ mod tests {
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
static_smoke_verified_game_index_sha256: None,
failed_playtest_revision: None,
updated_at: 0,
};
@@ -2036,6 +2038,7 @@ mod tests {
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
static_smoke_verified_game_index_sha256: None,
failed_playtest_revision: None,
updated_at: 0,
};
@@ -157,6 +157,7 @@ pub(crate) fn prepare_agent_runtime_project_mutation_locked(
gate.last_verification_tool = None;
gate.last_verification_status = None;
gate.static_smoke_verified_revision = None;
gate.static_smoke_verified_game_index_sha256 = None;
gate.failed_playtest_revision = None;
gate.updated_at = now;
if let Err(error) = write_game_creator_agent_runtime_verification_gate(root, &gate) {
@@ -359,8 +360,59 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
) -> Result<(), String> {
let current_revision = read_game_creator_agent_runtime_project_revision(root)?;
let revision_unchanged = current_revision.revision == expected_revision.revision;
let passed = passed && revision_unchanged;
let mut passed = passed && revision_unchanged;
let verification_tool = gate.last_verification_tool.clone();
let mut static_smoke_game_index_sha256 = None;
let mut static_smoke_credential_error = None;
if passed && verification_tool.as_deref() == Some("game.static_smoke") {
match read_autonomous_evidence_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"game.static_smoke 游戏入口",
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
) {
Ok(Some((digest, bytes))) => {
let html = match std::str::from_utf8(&bytes) {
Ok(html) => html,
Err(error) => {
passed = false;
static_smoke_credential_error = Some(format!(
"game.static_smoke 通过后 game/index.html 不是有效 UTF-8{error}"
));
""
}
};
if passed && !html.contains("<html") && !html.contains("<!doctype html") {
passed = false;
static_smoke_credential_error = Some(
"game.static_smoke 通过后 game/index.html 不再是 HTML 文档".to_string(),
);
}
if passed {
match validate_game_html_smoke(html) {
Ok(()) => static_smoke_game_index_sha256 = Some(digest.sha256),
Err(error) => {
passed = false;
static_smoke_credential_error = Some(format!(
"game.static_smoke 通过后 game/index.html 内容已变化且复核失败:{error}"
));
}
}
}
}
Ok(None) => {
passed = false;
static_smoke_credential_error =
Some("game.static_smoke 通过后 game/index.html 缺失".to_string());
}
Err(error) => {
passed = false;
static_smoke_credential_error = Some(format!(
"game.static_smoke 通过后无法绑定 game/index.html 摘要:{error}"
));
}
}
}
gate.verified_revision = passed
.then_some(current_revision.revision)
.filter(|value| *value > 0);
@@ -376,6 +428,8 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
}
if verification_tool.as_deref() == Some("game.static_smoke") {
gate.static_smoke_verified_revision = passed.then_some(current_revision.revision);
gate.static_smoke_verified_game_index_sha256 =
passed.then_some(static_smoke_game_index_sha256).flatten();
}
gate.last_verification_status = Some(
if passed {
@@ -393,6 +447,9 @@ pub(crate) fn finish_agent_runtime_project_verification_locked(
expected_revision.revision, current_revision.revision
));
}
if let Some(error) = static_smoke_credential_error {
return Err(error);
}
Ok(())
}
@@ -2010,8 +2010,7 @@ fn game_chat_fast_path_current_revision_is_verified(
&runtime.agent_id,
&runtime.run_id,
)?;
Ok(revision.revision > 0
&& agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision))
agent_runtime_static_smoke_passed_for_current_entry_at(root, &gate, revision.revision)
}
fn game_chat_fast_path_is_main_runtime(
@@ -2256,7 +2255,11 @@ pub(crate) fn game_chat_fast_path_plan_at(
revision.revision,
)?;
let current_revision_static_smoke_passed = owns_current_mutation
&& agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision);
&& agent_runtime_static_smoke_passed_for_current_entry_at(
root,
&gate,
revision.revision,
)?;
let current_revision_failed = owns_current_mutation
&& gate.last_verification_status.as_deref()
== Some(AGENT_RUNTIME_VERIFICATION_STATUS_FAILED);
@@ -3663,6 +3666,10 @@ mod tests {
gate.last_verification_tool = Some("game.static_smoke".to_string());
gate.static_smoke_verified_revision = Some(1);
gate.static_smoke_verified_game_index_sha256 =
fs::read(root.join(AGENT_RUNTIME_GAME_INDEX_PATH))
.ok()
.map(|bytes| format!("{:x}", Sha256::digest(bytes)));
write_game_creator_agent_runtime_verification_gate(&root, &gate)
.expect("write static smoke gate");
assert!(
@@ -1923,20 +1923,30 @@ async fn run_game_creator_agent_background_task_pass_without_deadline(
);
}
};
if let Err(error) = ensure_static_delegate_user_input_wait_at(
let waiting_for_user = match ensure_static_delegate_user_input_wait_at(
&root,
&mut runtime,
&deliveries,
) {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("Supervisor 用户澄清请求无法安全进入等待态:{error}"),
);
Ok(waiting) => waiting,
Err(error) => {
return fail_game_creator_agent_background_context_at(
&root,
&agent_id,
&session_id,
runtime,
&format!("Supervisor 用户澄清请求无法安全进入等待态:{error}"),
);
}
};
if waiting_for_user {
return AgentBackgroundTaskOutcome::WaitingForUserInput;
}
return AgentBackgroundTaskOutcome::WaitingForUserInput;
runtime.phase = "planning".to_string();
runtime.current_action =
"等待 Project Supervisor 采用安全默认值发起唯一返工".to_string();
runtime.waiting_on = "已转换为 needs-repair 的自主构建专业回执".to_string();
runtime.next_step = "调用 agent.delegate,并把 repairOfDelegationId 指向原 delivery;返工任务必须采用安全默认值继续,不能再询问用户".to_string();
} else if repair_required {
runtime.phase = "planning".to_string();
runtime.current_action = "等待 Project Supervisor 发起唯一返工".to_string();
File diff suppressed because it is too large Load Diff
@@ -329,6 +329,24 @@ pub(crate) fn ensure_static_delegate_user_input_wait_at(
runtime: &mut AgentRuntimeState,
deliveries: &[StaticDelegateDeliveryRecord],
) -> Result<bool, String> {
let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&runtime.agent_id,
&runtime.run_id,
)?
.ok_or_else(|| "needs-user-input 转换缺少当前 parent task".to_string())?;
if parent_task.session_id != runtime.session_id
|| parent_task.source != runtime.source
|| parent_task.run_profile != runtime.run_profile
|| parent_task.run_profile_binding_fingerprint != runtime.run_profile_binding_fingerprint
{
return Err("needs-user-input 转换的 parent task 与 runtime 身份不一致".to_string());
}
if convert_claimed_game_chat_user_input_deliveries_to_repair_at(root, &parent_task, deliveries)?
> 0
{
return Ok(false);
}
let mut pending_deliveries = deliveries.iter().filter(|delivery| {
delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
@@ -6145,7 +6145,21 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
));
}
};
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
let static_smoke_matches_current_entry =
match agent_runtime_static_smoke_passed_for_current_entry_at(
root,
&gate,
revision.revision,
) {
Ok(matches) => matches,
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-readiness 无法复核当前 game.static_smoke 入口摘要",
error,
));
}
};
if !static_smoke_matches_current_entry {
return Some(autonomous_completion_blocker(
"preview-readiness 尚未通过当前 revision 的 game.static_smoke",
format!(
@@ -6198,9 +6212,29 @@ fn autonomous_manifest_ready_task_completion_blocker_at_locked(
));
}
};
if receipt.revision != revision.revision {
let current_index = match read_autonomous_evidence_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"preview-playtest 当前游戏入口",
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
) {
Ok(Some((digest, _))) => digest,
Ok(None) => {
return Some(autonomous_completion_blocker(
"preview-playtest 当前游戏入口缺失",
"必须重新生成 game/index.html 并在当前 revision 执行 preview.validate。",
));
}
Err(error) => {
return Some(autonomous_completion_blocker(
"preview-playtest 当前游戏入口无法安全读取",
error,
));
}
};
if receipt.revision != revision.revision || receipt.game_index != current_index {
return Some(autonomous_completion_blocker(
"preview-playtest 浏览器试玩回执不属于当前 revision",
"preview-playtest 浏览器试玩回执不属于当前 revision 或入口产物",
format!(
"receiptRevision={}, currentRevision={}",
receipt.revision, revision.revision
@@ -6374,13 +6408,13 @@ fn game_chat_main_completion_blocker_at_locked(
),
));
}
let current_index_bytes = match read_autonomous_evidence_file_at(
let (current_index, current_index_bytes) = match read_autonomous_evidence_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"game-chat 主 Agent 游戏入口",
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
) {
Ok(Some((_, bytes))) => bytes,
Ok(Some(value)) => value,
Ok(None) => {
return Some(autonomous_completion_blocker(
"game-chat code-prototype 尚未生成 game/index.html",
@@ -6422,11 +6456,12 @@ fn game_chat_main_completion_blocker_at_locked(
));
}
};
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
if !agent_runtime_static_smoke_passed_for_entry(&gate, revision.revision, &current_index.sha256)
{
return Some(autonomous_completion_blocker(
"game-chat code-prototype 尚未通过当前 revision 的 game.static_smoke",
"game-chat code-prototype 尚未通过当前 game/index.html 的 game.static_smoke",
format!(
"currentRevision={}, staticSmokeVerifiedRevision={}",
"path=game/index.html, currentRevision={}, staticSmokeVerifiedRevision={}",
revision.revision,
gate.static_smoke_verified_revision
.map(|value| value.to_string())
@@ -6449,9 +6484,9 @@ fn game_chat_main_completion_blocker_at_locked(
));
}
};
if receipt.revision != revision.revision {
if receipt.revision != revision.revision || receipt.game_index != current_index {
return Some(autonomous_completion_blocker(
"game-chat code-prototype 试玩回执不属于当前 revision",
"game-chat code-prototype 试玩回执不属于当前 revision 或入口产物",
format!(
"receiptRevision={}, currentRevision={}",
receipt.revision, revision.revision
@@ -7309,6 +7344,52 @@ pub(in crate::agent) fn javascript_is_syntactically_valid(content: &str, is_modu
semantic.diagnostics.is_empty()
}
pub(in crate::agent) fn validate_executable_inline_javascript_syntax(
html: &str,
) -> Result<(), String> {
let lower = html.to_ascii_lowercase();
let mut cursor = 0usize;
let mut executable_index = 0usize;
while let Some(offset) = lower[cursor..].find('<') {
let tag_start = cursor + offset;
if lower[tag_start..].starts_with("<!--") {
cursor = lower[tag_start + 4..]
.find("-->")
.map(|end| tag_start + 4 + end + 3)
.unwrap_or(lower.len());
continue;
}
let Some(tag_after) = html_tag_end(&lower, tag_start) else {
break;
};
let tag = &lower[tag_start..tag_after];
if let Some(end) = html_non_executable_container_end(&lower, tag, tag_after - 1) {
cursor = end;
continue;
}
if !html_tag_starts_element(tag, "script") {
cursor = tag_after;
continue;
}
let Some((close_start, close_end)) =
raw_text_html_element_close(&lower, tag_after, "script")
else {
return Err("游戏入口的 <script> 代码块未闭合,疑似源码被截断".to_string());
};
if html_script_executes_in_modern_browser(tag) && !html_has_attribute(tag, "src") {
executable_index += 1;
let body = &html[tag_after..close_start];
if !javascript_is_syntactically_valid(body, html_script_is_module(tag)) {
return Err(format!(
"游戏入口第 {executable_index} 个可执行内联脚本不是有效 JavaScript"
));
}
}
cursor = close_end;
}
Ok(())
}
#[derive(Default)]
struct JavascriptLiteralSpanCollector {
strings: Vec<std::ops::Range<usize>>,
@@ -14689,6 +14770,23 @@ pub(in crate::agent) fn read_autonomous_playtest_receipt(
Ok(receipt)
}
fn browser_validation_has_required_passed_viewports(result: &BrowserValidationResult) -> bool {
result.viewport_results.len() == 2
&& [
BrowserValidationViewport::Desktop,
BrowserValidationViewport::Mobile,
]
.iter()
.all(|required| {
result
.viewport_results
.iter()
.filter(|viewport| viewport.viewport == *required && viewport.passed)
.count()
== 1
})
}
pub(in crate::agent) fn verify_autonomous_playtest_evidence_files_at(
root: &Path,
receipt: &AgentRuntimeAutonomousPlaytestReceipt,
@@ -14722,6 +14820,7 @@ pub(in crate::agent) fn verify_autonomous_playtest_evidence_files_at(
.as_ref()
.ok_or_else(|| "自主试玩浏览器报告缺少交互试玩结果".to_string())?;
if !report.passed
|| !browser_validation_has_required_passed_viewports(&report)
|| !playtest.passed
|| !playtest.matches_scenario_contract()
|| playtest.scenario != receipt.playtest_scenario
@@ -14753,6 +14852,7 @@ pub(in crate::agent) fn write_autonomous_playtest_receipt_at(
let expected_scenario_fingerprint =
browser_playtest_scenario_fingerprint(contract.playtest_scenario);
if !result.passed
|| !browser_validation_has_required_passed_viewports(result)
|| !playtest.passed
|| !playtest.matches_scenario_contract()
|| playtest.scenario != contract.playtest_scenario
@@ -14790,6 +14890,7 @@ pub(in crate::agent) fn write_autonomous_playtest_receipt_at(
.as_ref()
.ok_or_else(|| "持久浏览器报告缺少交互试玩结果".to_string())?;
if !persisted_report.passed
|| !browser_validation_has_required_passed_viewports(&persisted_report)
|| !persisted_playtest.passed
|| !persisted_playtest.matches_scenario_contract()
|| persisted_playtest.scenario != playtest.scenario
@@ -15115,7 +15216,8 @@ pub(in crate::agent) fn autonomous_game_build_completion_blocker_at_locked(
));
}
};
if !agent_runtime_static_smoke_passed_for_revision(&gate, revision.revision) {
if !agent_runtime_static_smoke_passed_for_entry(&gate, revision.revision, &current_index.sha256)
{
return Some(autonomous_completion_blocker(
"当前项目 revision 尚未通过 game.static_smoke",
format!(
@@ -111,7 +111,16 @@ fn game_chat_manifest_seed_projection_starts_only_the_single_main_agent() {
}
fn cropped_spritesheet_game_html() -> &'static str {
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);</script></body></html>"
"<!doctype html><html><body><p>目标:移动角色收集全部目标,避开障碍并获得胜利;失败后可按 R 重新开始。</p><canvas width=320 height=180></canvas><script>const canvas=document.querySelector('canvas');const context=canvas.getContext('2d');let playerX=0;const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';window.addEventListener('keydown',(event)=>{playerX+=event.key==='ArrowRight'?1:-1;});function draw(){context.clearRect(0,0,canvas.width,canvas.height);context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);context.fillText(String(playerX),8,172);requestAnimationFrame(draw);}requestAnimationFrame(draw);</script></body></html>"
}
fn with_static_smoke_contract(html: &str) -> String {
let contract = "<p>目标:移动角色收集全部目标并获得胜利;碰到危险即失败,按 R 重新开始。</p><script>const staticSmokeCanvas=document.querySelector('canvas');const staticSmokeContext=staticSmokeCanvas.getContext('2d');let staticSmokePlayerX=0;window.addEventListener('keydown',(event)=>{if(event.key==='ArrowRight'){staticSmokePlayerX+=1;}if(event.key==='r'||event.key==='R'){staticSmokePlayerX=0;}});function staticSmokeLoop(){staticSmokeContext.fillStyle='#ffffff';staticSmokeContext.fillRect(staticSmokePlayerX,0,1,1);requestAnimationFrame(staticSmokeLoop);}requestAnimationFrame(staticSmokeLoop);</script>";
if let Some(body_end) = html.rfind("</body>") {
format!("{}{}{}", &html[..body_end], contract, &html[body_end..])
} else {
format!("{html}{contract}")
}
}
fn executable_tetris_game_html() -> String {
@@ -5378,11 +5387,10 @@ fn autonomous_preview_manifest_tasks_accept_bound_current_revision_receipts() {
let readiness_child =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "preview-readiness");
let readiness_state = agent_runtime_state_from_task_record(&readiness_child);
advance_game_index_revision(
&root,
&parent_state,
"<!doctype html><title>静态检查通过</title><canvas></canvas>",
let readiness_html = with_static_smoke_contract(
"<!doctype html><html><body><title>静态检查通过</title><canvas></canvas></body></html>",
);
advance_game_index_revision(&root, &parent_state, &readiness_html);
mark_verification_passed(&root, &readiness_state, "game.static_smoke");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &readiness_state).is_none());
@@ -5655,6 +5663,50 @@ fn autonomous_playtest_receipt_rejects_previous_scenario_fingerprint() {
assert!(error.contains("场景指纹"), "unexpected error: {error}");
}
#[test]
fn autonomous_playtest_receipt_requires_passed_desktop_and_mobile_viewports() {
let (_temporary, root, state, contract) =
autonomous_fixture("做一个完整小游戏", "autonomous-required-viewports-run");
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><title>双视口试玩</title><canvas></canvas>",
);
let result =
browser_result_fixture(&root, &state, revision, BrowserPlaytestScenario::GenericV1);
let action = AgentRuntimeToolAction {
tool: "preview.validate".to_string(),
reason: Some("验证 desktop/mobile 试玩合同".to_string()),
input: serde_json::json!({}),
};
let action_fingerprint = agent_runtime_tool_action_fingerprint(&action, &state.current_task);
let action_id = agent_runtime_tool_action_id(&state.run_id, 1, 0, 1, &action_fingerprint);
let mut duplicate_desktop = result.clone();
duplicate_desktop.viewport_results[1].viewport = BrowserValidationViewport::Desktop;
write_autonomous_playtest_receipt_at(
&root,
&contract,
&action_id,
&action_fingerprint,
revision,
&duplicate_desktop,
)
.expect_err("duplicate desktop must not satisfy the mobile viewport gate");
let mut failed_mobile = result;
failed_mobile.viewport_results[1].passed = false;
write_autonomous_playtest_receipt_at(
&root,
&contract,
&action_id,
&action_fingerprint,
revision,
&failed_mobile,
)
.expect_err("failed mobile must not satisfy the dual viewport gate");
}
#[test]
fn stale_scenario_receipt_reads_as_missing_and_can_be_replaced() {
let (_temporary, root, state, contract) = autonomous_fixture(
@@ -5849,6 +5901,107 @@ fn game_chat_parent_completion_stops_after_main_agent_smoke_and_dual_viewport_pl
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none());
}
#[test]
fn game_chat_completion_rejects_same_revision_entry_rewrite_after_static_smoke() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮植物塔防游戏",
"game-chat-static-smoke-entry-rewrite",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
install_game_chat_existing_art_manifest(&root);
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("start the game-chat main agent");
let mut main_state = start_game_chat_main_agent(&root, &parent_state);
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut main_state);
let revision = advance_game_index_revision(&root, &main_state, cropped_spritesheet_game_html());
mark_verification_passed(&root, &main_state, "game.static_smoke");
let rewritten = format!(
"{}\n<!-- same-revision bypass rewrite -->",
cropped_spritesheet_game_html()
);
fs::write(root.join(AGENT_RUNTIME_GAME_INDEX_PATH), rewritten)
.expect("rewrite game entry without advancing project revision");
persist_game_chat_main_playtest_receipt(&root, &parent_state, &main_state, revision);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &main_state)
.expect("a static-smoke credential for different entry bytes must not complete");
assert_eq!(blocker.tool, "runtime.autonomous_completion");
assert!(blocker.summary.contains("game.static_smoke"));
assert!(blocker
.detail
.as_deref()
.is_some_and(|detail| detail.contains("game/index.html")));
}
#[test]
fn game_chat_prepared_finalization_rejects_same_revision_entry_rewrite() {
let _config_guard = crate::tests::write_test_local_config("{}".to_string());
let (_temporary, root, parent_state, _contract) = autonomous_fixture_with_source(
"创建一轮植物塔防游戏",
"game-chat-static-smoke-prepared-rewrite",
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
);
install_game_chat_existing_art_manifest(&root);
update_manifest_task_status_at(&root, "code-prototype", GameCreationAppTaskStatus::Running)
.expect("start the game-chat main agent");
let mut main_state = start_game_chat_main_agent(&root, &parent_state);
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut main_state);
let revision = advance_game_index_revision(&root, &main_state, cropped_spritesheet_game_html());
mark_verification_passed(&root, &main_state, "game.static_smoke");
persist_game_chat_main_playtest_receipt(&root, &parent_state, &main_state, revision);
main_state.status = "running".to_string();
main_state.phase = "finalizing".to_string();
main_state.current_action = "测试恢复 prepared finalization".to_string();
append_game_creator_agent_runtime_task(&root, &main_state)
.expect("append finalizing game-chat main task");
write_game_creator_agent_runtime_state(&root, &main_state)
.expect("persist finalizing game-chat main state");
let response = "不应复用旧静态检查凭证的完成回复";
let journal = build_game_creator_agent_runtime_finalization_journal(
&root,
&main_state,
response,
revision,
)
.expect("build prepared game-chat finalization");
write_game_creator_agent_runtime_finalization_journal(&root, &journal)
.expect("write prepared game-chat finalization");
append_game_creator_agent_runtime_finalization_lifecycle_stage(
&root,
&journal,
"prepared",
journal.prepared_at,
)
.expect("write prepared game-chat lifecycle");
fs::write(
root.join(AGENT_RUNTIME_GAME_INDEX_PATH),
format!(
"{}\n<!-- prepared same-revision bypass rewrite -->",
cropped_spritesheet_game_html()
),
)
.expect("rewrite game entry after prepared finalization");
assert_eq!(
resume_game_creator_agent_finalization_for_test_at(&root, &main_state.agent_id)
.expect("resume game-chat prepared finalization"),
"not-found"
);
let conversation = read_local_conversation_for_session_at(
&root,
Some(&main_state.agent_id),
Some(&main_state.session_id),
)
.expect("read game-chat main conversation");
assert!(!conversation
.messages
.iter()
.any(|message| message.role == "assistant" && message.content == response));
}
#[test]
fn game_chat_schedule_ready_tool_cannot_bypass_the_single_round_publish_boundary() {
let (_temporary, root, state, _contract) = autonomous_fixture_with_source(
@@ -6111,11 +6264,10 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
.expect("mark code prototype running");
let mut code_state = start_game_chat_main_agent(&root, &parent_state);
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut code_state);
let revision = advance_game_index_revision(
&root,
&code_state,
let pure_code_html = with_static_smoke_contract(
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
);
let revision = advance_game_index_revision(&root, &code_state, &pure_code_html);
mark_verification_passed(&root, &code_state, "game.static_smoke");
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
@@ -6131,7 +6283,8 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const sheet=new Image();sheet.src='../assets/art-spritesheet.png';const context=document.querySelector('canvas').getContext('2d');context.drawImage(sheet,0,0,32,32,0,0,64,64);</script></body></html>",
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';function neverDraw(){context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);}if(false) neverDraw();</script></body></html>",
] {
let revision = advance_game_index_revision(&root, &code_state, html);
let html = with_static_smoke_contract(html);
let revision = advance_game_index_revision(&root, &code_state, &html);
mark_verification_passed(&root, &code_state, "game.static_smoke");
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
assert!(
@@ -6140,11 +6293,10 @@ fn game_chat_code_prototype_requires_cropped_spritesheet_use() {
);
}
let overwritten_revision = advance_game_index_revision(
&root,
&code_state,
let overwritten_html = with_static_smoke_contract(
"<!doctype html><html><body><canvas width=320 height=180></canvas><script>const context=document.querySelector('canvas').getContext('2d');const playerArt=new Image();playerArt.src='../assets/art-spritesheet-slices/player.png';playerArt.src='data:image/png;base64,overridden';const targetArt=new Image();targetArt.src='../assets/art-spritesheet-slices/blocks-and-targets.png';const sceneArt=new Image();sceneArt.src='../assets/art-spritesheet-slices/obstacles-and-scene.png';const feedbackArt=new Image();feedbackArt.src='../assets/art-spritesheet-slices/feedback-effects.png';context.drawImage(playerArt,0,0,64,64);context.drawImage(targetArt,64,0,64,64);context.drawImage(sceneArt,128,0,64,64);context.drawImage(feedbackArt,192,0,64,64);</script></body></html>",
);
let overwritten_revision = advance_game_index_revision(&root, &code_state, &overwritten_html);
mark_verification_passed(&root, &code_state, "game.static_smoke");
persist_game_chat_main_playtest_receipt(
&root,
@@ -6262,11 +6414,10 @@ fn cli_code_prototype_keeps_registered_canvas_spritesheet_gate_when_editor_is_co
let code_record =
queue_autonomous_manifest_child_fixture(&root, &parent_state, "code-prototype");
let code_state = agent_runtime_state_from_task_record(&code_record);
advance_game_index_revision(
&root,
&code_state,
let art_spec_only_html = with_static_smoke_contract(
"<!doctype html><html><body><img src=\"../assets/art-spec.png\"><canvas></canvas></body></html>",
);
advance_game_index_revision(&root, &code_state, &art_spec_only_html);
mark_verification_passed(&root, &code_state, "game.static_smoke");
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &code_state)
.expect("CLI must still require the art spritesheet");
@@ -6370,11 +6521,10 @@ fn game_chat_code_prototype_fails_closed_without_generated_spritesheet() {
persist_game_chat_main_asset_audit_and_route(&root, &parent_state, &mut code_state);
fs::remove_file(root.join("assets/art-spritesheet.png"))
.expect("remove required art-spritesheet file");
let revision = advance_game_index_revision(
&root,
&code_state,
let pure_code_html = with_static_smoke_contract(
"<!doctype html><html><body><canvas></canvas><script>const context=document.querySelector('canvas').getContext('2d');context.fillRect(0,0,96,96);</script></body></html>",
);
let revision = advance_game_index_revision(&root, &code_state, &pure_code_html);
mark_verification_passed(&root, &code_state, "game.static_smoke");
persist_game_chat_main_playtest_receipt(&root, &parent_state, &code_state, revision);
@@ -7768,11 +7918,8 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest(
.as_deref()
.is_some_and(|detail| detail.contains("game/index.html(initial-placeholder)")));
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><title>可玩塔防</title><canvas></canvas>",
);
let valid_game = cropped_spritesheet_game_html().to_string();
let revision = advance_game_index_revision(&root, &state, &valid_game);
mark_verification_passed(&root, &state, "project.verify");
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state)
.expect("project.verify cannot replace static smoke");
@@ -7839,11 +7986,8 @@ fn autonomous_completion_requires_changed_index_static_smoke_and_bound_playtest(
.expect("persist autonomous playtest receipt");
assert!(autonomous_game_build_completion_blocker_at_locked(&root, &state).is_none());
advance_game_index_revision(
&root,
&state,
"<!doctype html><title>试玩后又修改</title><canvas></canvas>",
);
let changed_game = format!("{valid_game}\n<!-- 试玩后又修改 -->");
advance_game_index_revision(&root, &state, &changed_game);
mark_verification_passed(&root, &state, "game.static_smoke");
let blocker = autonomous_game_build_completion_blocker_at_locked(&root, &state)
.expect("stale playtest must block completion");
@@ -7856,11 +8000,10 @@ fn autonomous_prepared_finalization_without_playtest_is_discarded_before_assista
"做一个植物大战僵尸式塔防游戏",
"autonomous-prepared-finalization-run",
);
let revision = advance_game_index_revision(
&root,
&state,
"<!doctype html><title>尚未试玩的塔防</title><canvas></canvas>",
let unplayed_html = with_static_smoke_contract(
"<!doctype html><html><body><title>尚未试玩的塔防</title><canvas></canvas></body></html>",
);
let revision = advance_game_index_revision(&root, &state, &unplayed_html);
mark_verification_passed(&root, &state, "game.static_smoke");
state.status = "running".to_string();
state.phase = "finalizing".to_string();
@@ -7920,6 +8063,7 @@ fn autonomous_playtest_liveness_only_enforces_the_latest_preview_result() {
last_verification_tool: Some("game.static_smoke".to_string()),
last_verification_status: Some(AGENT_RUNTIME_VERIFICATION_STATUS_PASSED.to_string()),
static_smoke_verified_revision: Some(2),
static_smoke_verified_game_index_sha256: Some("a".repeat(64)),
failed_playtest_revision: None,
updated_at: 0,
};
@@ -286,6 +286,11 @@ pub(crate) struct AgentRuntimeVerificationGate {
/// proof that the exact same revision passed `game.static_smoke`.
#[serde(default)]
pub(crate) static_smoke_verified_revision: Option<u64>,
/// Bind the durable static-smoke credential to the exact entry bytes.
/// A project revision alone is insufficient because files can be changed
/// outside the Runtime mutation path without advancing that revision.
#[serde(default)]
pub(crate) static_smoke_verified_game_index_sha256: Option<String>,
#[serde(default)]
pub(crate) failed_playtest_revision: Option<u64>,
pub(crate) updated_at: u64,
@@ -49,6 +49,7 @@ pub(in crate::agent) fn default_agent_runtime_verification_gate(
last_verification_tool: None,
last_verification_status: None,
static_smoke_verified_revision: None,
static_smoke_verified_game_index_sha256: None,
failed_playtest_revision: None,
updated_at: 0,
})
@@ -125,6 +126,20 @@ pub(in crate::agent) fn validate_agent_runtime_verification_gate(
"Agent Runtime verification gate 的 failedPlaytestRevision 必须大于 0".to_string(),
);
}
if gate
.static_smoke_verified_game_index_sha256
.as_deref()
.is_some_and(|sha256| !is_lowercase_sha256(sha256))
{
return Err("Agent Runtime verification gate 的 static smoke 入口摘要无效".to_string());
}
if gate.static_smoke_verified_revision.is_none()
&& gate.static_smoke_verified_game_index_sha256.is_some()
{
return Err(
"Agent Runtime verification gate 的 static smoke 入口摘要缺少 revision".to_string(),
);
}
if gate
.failed_playtest_revision
.zip(gate.mutation_revision)
@@ -195,11 +210,35 @@ pub(in crate::agent) fn validate_agent_runtime_verification_gate(
Ok(())
}
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_revision(
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_entry(
gate: &AgentRuntimeVerificationGate,
revision: u64,
game_index_sha256: &str,
) -> bool {
revision > 0 && gate.static_smoke_verified_revision == Some(revision)
revision > 0
&& gate.static_smoke_verified_revision == Some(revision)
&& gate.static_smoke_verified_game_index_sha256.as_deref() == Some(game_index_sha256)
}
pub(in crate::agent) fn agent_runtime_static_smoke_passed_for_current_entry_at(
root: &Path,
gate: &AgentRuntimeVerificationGate,
revision: u64,
) -> Result<bool, String> {
let Some((game_index, _)) = read_autonomous_evidence_file_at(
root,
AGENT_RUNTIME_GAME_INDEX_PATH,
"game.static_smoke 当前游戏入口",
AGENT_RUNTIME_AUTONOMOUS_GAME_INDEX_MAX_BYTES,
)?
else {
return Ok(false);
};
Ok(agent_runtime_static_smoke_passed_for_entry(
gate,
revision,
&game_index.sha256,
))
}
pub(crate) fn read_game_creator_agent_runtime_project_revision(
@@ -35,7 +35,11 @@ pub(in crate::agent) use run_status::*;
pub(in crate::agent) use task_ops::*;
#[cfg(test)]
pub(crate) use delivery::build_static_delegate_result_for_child_at;
pub(crate) use delivery::{
build_static_delegate_result_for_child_at,
convert_game_chat_child_user_input_to_safe_default_repair,
trusted_game_chat_autonomous_root_parent_at,
};
#[cfg(test)]
pub(crate) use media::validate_agent_runtime_canvas_replacement_authorization_at;
@@ -51,7 +55,9 @@ pub(crate) use delegation::{
};
pub(crate) use delivery::{
agent_runtime_delegation_id, dispatch_isolated_agent_join_at,
game_chat_safe_default_repair_replacement, game_chat_safe_default_repair_task_instruction,
game_creator_agent_runtime_terminal_status, publish_game_creator_agent_delegate_result,
reconcile_claimed_game_chat_safe_default_half_states_at,
reconcile_game_creator_agent_delegate_receipts_at,
};
#[allow(unused_imports)]
@@ -286,18 +286,18 @@ fn validate_game_chat_main_art_delegation_at(
acceptance_criteria: &[String],
expected_artifacts: &[String],
repair_of_delegation_id: Option<&str>,
) -> Result<(), String> {
) -> Result<bool, String> {
let Some(binding) =
read_game_creator_agent_runtime_run_profile_binding(root, parent_agent_id, parent_run_id)?
else {
return Ok(());
return Ok(false);
};
let may_be_game_chat = binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|| (binding.source == "agent-ready-task-scheduler"
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID);
if !may_be_game_chat {
return Ok(());
return Ok(false);
}
let root_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
@@ -320,13 +320,32 @@ fn validate_game_chat_main_art_delegation_at(
);
}
if !is_game_chat_main {
return Ok(());
}
if repair_of_delegation_id.is_some() {
return Err(
"game-chat 主 Agent 美术委派不允许发起返工链;请认领当前回执后重新审计".to_string(),
);
return Ok(false);
}
let safe_default_repair = if let Some(original_delegation_id) = repair_of_delegation_id {
let parent_task = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
parent_agent_id,
parent_run_id,
)?
.ok_or_else(|| "game-chat 安全默认返工缺少 code-prototype 父任务".to_string())?;
let original = read_static_delegate_delivery_at(root, original_delegation_id)?
.ok_or_else(|| "game-chat 安全默认返工引用的原 delivery 不存在".to_string())?;
if original.repair_of_delegation_id.is_some()
|| original.target_agent_id != target_agent_id
|| original.acceptance_criteria != acceptance_criteria
|| original.expected_artifacts != expected_artifacts
|| !trusted_game_chat_safe_default_repair_delivery_at(root, &parent_task, &original)?
{
return Err(
"game-chat code-prototype 只能对同一安全默认 delivery 发起唯一、同合同的一层返工"
.to_string(),
);
}
true
} else {
false
};
if !matches!(target_agent_id, "art-director" | "art-asset-plan") {
return Err(
"game-chat code-prototype 只能按审计结果委派 art-director 或 art-asset-plan"
@@ -387,13 +406,14 @@ fn validate_game_chat_main_art_delegation_at(
&& delivery.parent_run_id == parent_run_id
&& delivery.parent_action_id == action_identity
&& delivery.target_agent_id == target_agent_id
&& delivery.repair_of_delegation_id.is_none()
&& delivery.repair_of_delegation_id.as_deref() == repair_of_delegation_id
&& delivery.status != StaticDelegateDeliveryStatus::Suppressed
});
if static_delegate_target_agent_ids_at(root, parent_agent_id, parent_run_id)?
.iter()
.any(|existing_target| existing_target == target_agent_id)
&& !same_action_replay
&& !safe_default_repair
{
return Err(format!(
"game-chat 每个审计缺口最多委派一次:target={target_agent_id}"
@@ -404,7 +424,7 @@ fn validate_game_chat_main_art_delegation_at(
{
return Err("game-chat code-prototype 同一时刻最多保留一个活跃美术委派".to_string());
}
Ok(())
Ok(safe_default_repair)
}
pub(crate) fn observe_agent_runtime_agent_delegate(
@@ -528,7 +548,68 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
detail: None,
};
}
if repair_of_delegation_id.is_some() && agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID {
let mut safe_default_repair_instruction = None;
let game_chat_safe_default_repair = if let Some(original_delegation_id) =
repair_of_delegation_id.as_deref()
{
let parent_task = match read_latest_game_creator_agent_runtime_task_by_run_id(
root,
agent_id,
parent_run_id,
) {
Ok(parent_task) => parent_task,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
};
let original = match read_static_delegate_delivery_at(root, original_delegation_id) {
Ok(original) => original,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
};
match (parent_task.as_ref(), original.as_ref()) {
(Some(parent_task), Some(original)) => {
match trusted_game_chat_safe_default_repair_delivery_at(root, parent_task, original)
{
Ok(authorized) => {
if authorized {
safe_default_repair_instruction = original
.structured_result
.as_ref()
.and_then(game_chat_safe_default_repair_task_instruction);
}
authorized
}
Err(error) => {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
}
}
_ => false,
}
} else {
false
};
if repair_of_delegation_id.is_some()
&& agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& !game_chat_safe_default_repair
{
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
@@ -573,7 +654,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
detail: None,
};
}
if let Err(error) = validate_game_chat_main_art_delegation_at(
let game_chat_safe_default_repair = match validate_game_chat_main_art_delegation_at(
root,
agent_id,
parent_run_id,
@@ -583,13 +664,16 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
&expected_artifacts,
repair_of_delegation_id.as_deref(),
) {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
Ok(safe_default_repair) => safe_default_repair,
Err(error) => {
return AgentRuntimeToolObservation {
tool: "agent.delegate".to_string(),
status: "failed".to_string(),
summary: redact_agent_runtime_project_paths(root, &error, 240),
detail: None,
};
}
};
if let Err(error) =
ensure_current_autonomous_ready_child_mutation_at_locked(root, agent_id, parent_run_id)
{
@@ -609,8 +693,11 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
&target_agent_id,
delegation_action_identity,
);
let delegated_task_text = safe_default_repair_instruction
.map(|instruction| format!("{instruction}\n\n{task}"))
.unwrap_or(task);
let delegated_task = match render_static_delegate_task_contract(
&task,
&delegated_task_text,
agent_id,
parent_run_id,
&delegation_id,
@@ -638,6 +725,7 @@ pub(crate) fn observe_agent_runtime_agent_delegate(
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& binding.root_agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
});
debug_assert!(!game_chat_safe_default_repair || main_art_delegation);
let run_id = if run_id_input.trim().is_empty() {
if agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID || main_art_delegation {
format!("delegated-{delegation_id}")
@@ -1,5 +1,432 @@
use super::*;
const GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION: &str = "game-chat-safe-default-repair.v1";
const GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE: &str = "child-needs-user-input";
const GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY: &str = "continue-with-safe-defaults";
const GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE: &str = "preference-clarification";
const GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE: &str = "sensitive-or-permission-request";
const GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT: &str = "use-safe-default";
const GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED: &str = "skip-denied";
const GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY: &str =
"专业 Agent 请求了用户补充信息;自主构建必须采用安全默认值继续,并由 Supervisor 发起唯一返工。";
const GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY: &str =
"自主构建已将专业 Agent 的补充信息请求转换为安全默认返工。";
fn game_chat_safe_default_request_allows_preference_default(
result: &StaticDelegateStructuredResult,
) -> bool {
let serialized = serde_json::to_string(&result.user_input_questions).unwrap_or_default();
let lower = serialized.to_ascii_lowercase();
if redact_secret_tokens(&serialized) != serialized
|| redact_absolute_path_tokens(&serialized) != serialized
|| [
".env",
"api key",
"api_key",
"apikey",
"token",
"secret",
"password",
"credential",
"authorization",
"cookie",
"bearer ",
"权限",
"授权",
"删除",
"发布",
"支付",
"外部副作用",
"permission",
"authorize",
"delete",
"publish",
"payment",
]
.iter()
.any(|marker| lower.contains(marker))
{
return false;
}
[
"偏好",
"视觉",
"配色",
"色彩",
"颜色",
"风格",
"主题",
"布局",
"难度",
"节奏",
"preference",
"visual",
"palette",
"color",
"style",
"theme",
"layout",
"difficulty",
"pace",
]
.iter()
.any(|marker| lower.contains(marker))
}
fn game_chat_safe_default_repair_error(result: &StaticDelegateStructuredResult) -> String {
let use_preference_default = game_chat_safe_default_request_allows_preference_default(result);
serde_json::json!({
"schemaVersion": GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION,
"code": GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE,
"strategy": GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY,
"reasonCode": if use_preference_default {
GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE
} else {
GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE
},
"defaultDecision": if use_preference_default {
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT
} else {
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED
},
"requestSha256": result.user_input_questions_sha256,
"summary": GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY,
})
.to_string()
}
fn game_chat_safe_default_repair_marker(
result: &StaticDelegateStructuredResult,
) -> Option<serde_json::Value> {
if result.contract_status != StaticDelegateContractStatus::NeedsRepair
|| !result.user_input_questions.is_empty()
|| result.user_input_questions_sha256.is_some()
{
return None;
}
let marker = serde_json::from_str::<serde_json::Value>(result.error.as_deref()?).ok()?;
let reason = marker.get("reasonCode")?.as_str()?;
let decision = marker.get("defaultDecision")?.as_str()?;
let request_sha256 = marker.get("requestSha256")?.as_str()?;
(marker.get("schemaVersion")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_SCHEMA_VERSION
&& marker.get("code")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_CODE
&& marker.get("strategy")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_STRATEGY
&& marker.get("summary")?.as_str()? == GAME_CHAT_SAFE_DEFAULT_REPAIR_SUMMARY
&& matches!(
(reason, decision),
(
GAME_CHAT_SAFE_DEFAULT_REASON_PREFERENCE,
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT
) | (
GAME_CHAT_SAFE_DEFAULT_REASON_SENSITIVE,
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED
)
)
&& request_sha256.len() == 64
&& request_sha256.bytes().all(|byte| byte.is_ascii_hexdigit()))
.then_some(marker)
}
pub(crate) fn game_chat_safe_default_repair_result_is_valid(
result: &StaticDelegateStructuredResult,
) -> bool {
game_chat_safe_default_repair_marker(result).is_some()
}
pub(crate) fn game_chat_safe_default_repair_task_instruction(
result: &StaticDelegateStructuredResult,
) -> Option<&'static str> {
let marker = game_chat_safe_default_repair_marker(result)?;
match marker.get("defaultDecision")?.as_str()? {
GAME_CHAT_SAFE_DEFAULT_DECISION_USE_DEFAULT => Some(
"Runtime 安全默认决策:use-safe-default。仅采用无需用户补充、无需新增权限的普通偏好默认值继续;不得再次询问用户。",
),
GAME_CHAT_SAFE_DEFAULT_DECISION_SKIP_DENIED => Some(
"Runtime 安全默认决策:skip-denied。跳过被拒绝、敏感、越权或无法证明安全的输入与动作,仅在原合同和现有权限内继续;不得再次询问用户。",
),
_ => None,
}
}
pub(crate) fn game_chat_safe_default_repair_replacement(
result: &StaticDelegateStructuredResult,
) -> Option<(String, StaticDelegateStructuredResult)> {
let mut replacement = result.clone();
if replacement.contract_status == StaticDelegateContractStatus::NeedsUserInput {
convert_game_chat_child_user_input_to_safe_default_repair(&mut replacement);
} else if !game_chat_safe_default_repair_result_is_valid(&replacement) {
return None;
}
Some((
GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY.to_string(),
replacement,
))
}
fn trusted_game_chat_autonomous_root_binding(
parent_task: &AgentRuntimeTaskRecord,
binding: &AgentRuntimeRunProfileBinding,
) -> bool {
parent_task.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& parent_task.run_id == binding.run_id
&& parent_task.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
&& parent_task.run_profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& parent_task.parent_agent_id.is_none()
&& parent_task.parent_run_id.is_none()
&& binding.agent_id == GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
&& binding.root_agent_id == binding.agent_id
&& binding.root_run_id == binding.run_id
&& binding.parent_agent_id.is_none()
&& binding.parent_run_id.is_none()
&& binding.source == AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
&& binding.profile == AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
&& parent_task.run_profile_binding_fingerprint == binding.binding_fingerprint
}
fn trusted_game_chat_autonomous_parent_chain_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
) -> Result<Option<AgentRuntimeRunProfileBinding>, String> {
let Some(parent_binding) = read_game_creator_agent_runtime_run_profile_binding(
root,
&parent_task.agent_id,
&parent_task.run_id,
)?
else {
return Ok(None);
};
if parent_task.agent_id != parent_binding.agent_id
|| parent_task.run_id != parent_binding.run_id
|| parent_task.source != parent_binding.source
|| parent_task.run_profile != parent_binding.profile
|| parent_task.parent_agent_id != parent_binding.parent_agent_id
|| parent_task.parent_run_id != parent_binding.parent_run_id
|| parent_task.run_profile_binding_fingerprint != parent_binding.binding_fingerprint
{
return Ok(None);
}
if trusted_game_chat_autonomous_root_binding(parent_task, &parent_binding) {
return Ok(Some(parent_binding));
}
if parent_task.agent_id != "code-prototype"
|| parent_task.source != "agent-ready-task-scheduler"
|| parent_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| parent_task.delegation_id.is_some()
|| parent_binding.parent_agent_id.as_deref()
!= Some(GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID)
|| parent_binding.parent_run_id.as_deref() != Some(parent_binding.root_run_id.as_str())
|| parent_binding.root_agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| parent_binding.parent_binding_fingerprint.is_none()
{
return Ok(None);
}
let Some(root_binding) = read_game_creator_agent_runtime_run_profile_binding(
root,
&parent_binding.root_agent_id,
&parent_binding.root_run_id,
)?
else {
return Ok(None);
};
let Some(root_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&root_binding.agent_id,
&root_binding.run_id,
)?
else {
return Ok(None);
};
if !trusted_game_chat_autonomous_root_binding(&root_task, &root_binding)
|| parent_binding.parent_binding_fingerprint.as_deref()
!= Some(root_binding.binding_fingerprint.as_str())
{
return Ok(None);
}
Ok(Some(parent_binding))
}
pub(crate) fn trusted_game_chat_autonomous_root_parent_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
) -> Result<bool, String> {
if parent_task.agent_id != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
|| parent_task.source != AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
|| parent_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| parent_task.parent_agent_id.is_some()
|| parent_task.parent_run_id.is_some()
{
return Ok(false);
}
let Some(binding) = read_game_creator_agent_runtime_run_profile_binding(
root,
&parent_task.agent_id,
&parent_task.run_id,
)?
else {
return Ok(false);
};
Ok(trusted_game_chat_autonomous_root_binding(
parent_task,
&binding,
))
}
fn trusted_game_chat_autonomous_child_delivery_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
delivery: &StaticDelegateDeliveryRecord,
supplied_child_task: Option<&AgentRuntimeTaskRecord>,
) -> Result<bool, String> {
let Some(parent_binding) = trusted_game_chat_autonomous_parent_chain_at(root, parent_task)?
else {
return Ok(false);
};
if delivery.parent_agent_id != parent_task.agent_id
|| delivery.parent_session_id != parent_task.session_id
|| delivery.parent_run_id != parent_task.run_id
|| agent_runtime_delegation_id(
&delivery.parent_agent_id,
&delivery.parent_run_id,
&delivery.target_agent_id,
&delivery.parent_action_id,
) != delivery.delegation_id
|| !matches!(
delivery.target_agent_id.as_str(),
"code-prototype" | "art-director" | "art-asset-plan"
)
|| (parent_task.agent_id == "code-prototype"
&& !matches!(
delivery.target_agent_id.as_str(),
"art-director" | "art-asset-plan"
))
{
return Err("game-chat 安全默认返工 delivery 与可信父责任链不一致".to_string());
}
let child_task = match supplied_child_task {
Some(task) => task.clone(),
None => read_latest_game_creator_agent_runtime_task_by_run_id(
root,
&delivery.target_agent_id,
&delivery.target_run_id,
)?
.ok_or_else(|| "game-chat 安全默认返工缺少专业 child task".to_string())?,
};
validate_static_delegate_delivery_for_child_result(delivery, parent_task, &child_task)?;
let child_binding = read_game_creator_agent_runtime_run_profile_binding(
root,
&child_task.agent_id,
&child_task.run_id,
)?
.ok_or_else(|| "game-chat 安全默认返工缺少专业 child binding".to_string())?;
if child_task.source != "agent-delegate"
|| child_task.run_profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| child_task.run_profile_binding_fingerprint != child_binding.binding_fingerprint
|| child_binding.agent_id != delivery.target_agent_id
|| child_binding.run_id != delivery.target_run_id
|| child_binding.source != "agent-delegate"
|| child_binding.profile != AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD
|| child_binding.root_agent_id != parent_binding.root_agent_id
|| child_binding.root_run_id != parent_binding.root_run_id
|| child_binding.parent_agent_id.as_deref() != Some(parent_task.agent_id.as_str())
|| child_binding.parent_run_id.as_deref() != Some(parent_task.run_id.as_str())
|| child_binding.parent_binding_fingerprint.as_deref()
!= Some(parent_binding.binding_fingerprint.as_str())
{
return Err("game-chat 安全默认返工专业 child binding/link 身份不一致".to_string());
}
Ok(true)
}
pub(crate) fn convert_game_chat_child_user_input_to_safe_default_repair(
result: &mut StaticDelegateStructuredResult,
) -> bool {
if result.contract_status != StaticDelegateContractStatus::NeedsUserInput {
return false;
}
let error = game_chat_safe_default_repair_error(result);
result.contract_status = StaticDelegateContractStatus::NeedsRepair;
result.user_input_questions.clear();
result.user_input_questions_sha256 = None;
result.error = Some(error);
true
}
pub(in crate::agent) fn convert_claimed_game_chat_user_input_deliveries_to_repair_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
deliveries: &[StaticDelegateDeliveryRecord],
) -> Result<usize, String> {
if trusted_game_chat_autonomous_parent_chain_at(root, parent_task)?.is_none() {
return Ok(0);
}
let mut converted = 0_usize;
for expected in deliveries.iter().filter(|delivery| {
delivery.parent_agent_id == parent_task.agent_id
&& delivery.parent_session_id == parent_task.session_id
&& delivery.parent_run_id == parent_task.run_id
&& delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.clarification_request_id.is_none()
&& delivery.clarification_answers_sha256.is_none()
&& delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
})
}) {
if !trusted_game_chat_autonomous_child_delivery_at(root, parent_task, expected, None)? {
continue;
}
replace_claimed_static_delegate_result_for_game_chat_safe_default_at(root, expected)?;
converted = converted.saturating_add(1);
}
Ok(converted)
}
pub(crate) fn reconcile_claimed_game_chat_safe_default_half_states_at(
root: &Path,
parent_agent_id: &str,
parent_run_id: &str,
) -> Result<(), String> {
let Some(parent_task) = read_latest_game_creator_agent_runtime_task_by_run_id(
root,
parent_agent_id,
parent_run_id,
)?
else {
return Ok(());
};
if trusted_game_chat_autonomous_parent_chain_at(root, &parent_task)?.is_none() {
return Ok(());
}
for delivery in claimed_static_delegate_deliveries_at(root, parent_agent_id, parent_run_id)? {
if delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
|| game_chat_safe_default_repair_result_is_valid(result)
}) {
if !trusted_game_chat_autonomous_child_delivery_at(root, &parent_task, &delivery, None)?
{
continue;
}
replace_claimed_static_delegate_result_for_game_chat_safe_default_at(root, &delivery)?;
}
}
Ok(())
}
pub(crate) fn trusted_game_chat_safe_default_repair_delivery_at(
root: &Path,
parent_task: &AgentRuntimeTaskRecord,
delivery: &StaticDelegateDeliveryRecord,
) -> Result<bool, String> {
Ok(
delivery.status == StaticDelegateDeliveryStatus::ClaimedByParent
&& delivery.result_summary.as_deref() == Some(GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY)
&& delivery
.structured_result
.as_ref()
.is_some_and(game_chat_safe_default_repair_result_is_valid)
&& trusted_game_chat_autonomous_child_delivery_at(root, parent_task, delivery, None)?,
)
}
pub(crate) fn agent_runtime_delegation_id(
parent_agent_id: &str,
parent_run_id: &str,
@@ -334,7 +761,22 @@ pub(in crate::agent) fn wake_waiting_static_delegate_parent_run_at(
&current_task.run_id,
)?;
let mut state = state;
ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)?;
if !ensure_static_delegate_user_input_wait_at(root, &mut state, &deliveries)? {
let state = advance_game_creator_agent_runtime_turn_at(
root,
state,
"planning",
"自主构建澄清已转换为安全默认返工",
"专业 Agent 回执已按安全默认策略进入 needs-repair,恢复同一父 run。",
)?;
let root = root.to_path_buf();
let agent_id = current_task.agent_id.clone();
let task = current_task.task.clone();
tauri::async_runtime::spawn(async move {
let _runtime_lock = runtime_lock;
drain_game_creator_agent_background_tasks(root, agent_id, task, state).await;
});
}
return Ok(true);
}
let state = advance_game_creator_agent_runtime_turn_at(
@@ -889,8 +1331,8 @@ pub(crate) fn publish_game_creator_agent_delegate_result(
return;
}
let was_dispatched = existing_delivery.status == StaticDelegateDeliveryStatus::Dispatched;
let safe_result_summary = truncate_agent_runtime_text(&result_detail, 140);
let structured_result = match build_static_delegate_result_for_child_at(
let mut safe_result_summary = truncate_agent_runtime_text(&result_detail, 140);
let mut structured_result = match build_static_delegate_result_for_child_at(
root,
&existing_delivery,
child_task,
@@ -918,6 +1360,24 @@ pub(crate) fn publish_game_creator_agent_delegate_result(
return;
}
};
match trusted_game_chat_autonomous_child_delivery_at(
root,
&parent_task,
&existing_delivery,
Some(child_task),
) {
Ok(true) => {
if convert_game_chat_child_user_input_to_safe_default_repair(&mut structured_result)
{
safe_result_summary = GAME_CHAT_SAFE_DEFAULT_RESULT_SUMMARY.to_string();
}
}
Ok(false) => {}
Err(error) => {
record_game_creator_agent_delegate_result_failure(root, child_task, &error);
return;
}
}
let delivery = match mark_static_delegate_delivery_ready_with_result_at(
root,
&child_task.agent_id,
@@ -24,6 +24,7 @@ pub(crate) enum CliCommand {
parent_agent_id: String,
initialize: bool,
run_profile: String,
supervisor_source: &'static str,
},
AgentEnqueue {
project_path: PathBuf,
@@ -687,7 +688,7 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
}));
}
if args.first().map(String::as_str) == Some("--swarm-chat") {
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] <本地项目绝对路径> [parentAgentId]";
const USAGE: &str = "用法:--swarm-chat [--init] [--autonomous-game-build] [--game-chat-smoke] <本地项目绝对路径> [parentAgentId]";
let mut rest = args[1..].to_vec();
let initialize = if let Some(index) = rest.iter().position(|arg| arg == "--init") {
rest.remove(index);
@@ -711,9 +712,36 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
}
_ => return Err(USAGE.to_string()),
};
let game_chat_smoke = match rest
.iter()
.filter(|arg| arg.as_str() == "--game-chat-smoke")
.count()
{
0 => false,
1 => {
let index = rest
.iter()
.position(|arg| arg == "--game-chat-smoke")
.expect("counted game-chat smoke flag");
rest.remove(index);
true
}
_ => return Err(USAGE.to_string()),
};
if !(1..=2).contains(&rest.len()) || rest.iter().any(|value| value.trim().is_empty()) {
return Err(USAGE.to_string());
}
if game_chat_smoke
&& (!autonomous_game_build
|| rest.get(1).is_some_and(|parent| {
parent.trim() != GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID
}))
{
return Err(
"--game-chat-smoke 仅允许 project-supervisor 的 --autonomous-game-build 受限验收入口"
.to_string(),
);
}
return Ok(Some(CliCommand::SwarmChat {
project_path: PathBuf::from(&rest[0]),
parent_agent_id: rest
@@ -726,6 +754,11 @@ pub(crate) fn parse_cli_command(args: &[String]) -> Result<Option<CliCommand>, S
} else {
AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string()
},
supervisor_source: if game_chat_smoke {
AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE
} else {
AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE
},
}));
}
if args.first().map(String::as_str) == Some("--agent-task") {
@@ -984,11 +1017,17 @@ pub(crate) fn run_cli_command(command: CliCommand) -> Result<(), String> {
parent_agent_id,
initialize,
run_profile,
supervisor_source,
} => {
let project_path = canonicalize_cli_path(&project_path, "本地项目路径", initialize)?;
require_external_agent_runner_for_cli_runtime_write(&project_path)?;
initialize_cli_agent_project(&project_path, initialize)?;
run_game_creator_swarm_chat_at(&project_path, &parent_agent_id, &run_profile)
run_game_creator_swarm_chat_at(
&project_path,
&parent_agent_id,
&run_profile,
supervisor_source,
)
}
CliCommand::AgentEnqueue {
project_path,
@@ -1869,6 +1908,7 @@ mod tests {
parent_agent_id: "code-prototype".to_string(),
initialize: true,
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
assert!(command.requires_external_agent_runner());
@@ -1948,6 +1988,7 @@ mod tests {
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
initialize: false,
run_profile: AGENT_RUNTIME_RUN_PROFILE_STANDARD.to_string(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
}
@@ -1970,10 +2011,57 @@ mod tests {
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
initialize: false,
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_CLI_SOURCE,
}
);
}
#[test]
fn swarm_chat_game_chat_smoke_flag_is_restricted_and_selects_trusted_source() {
let project_path = std::env::current_dir().expect("current directory");
let command = parse_cli_command(&[
"--swarm-chat".to_string(),
"--init".to_string(),
"--autonomous-game-build".to_string(),
"--game-chat-smoke".to_string(),
project_path.display().to_string(),
])
.expect("parse game-chat smoke")
.expect("game-chat smoke command");
assert_eq!(
command,
CliCommand::SwarmChat {
project_path,
parent_agent_id: GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID.to_string(),
initialize: true,
run_profile: AGENT_RUNTIME_RUN_PROFILE_AUTONOMOUS_GAME_BUILD.to_string(),
supervisor_source: AGENT_RUNTIME_SUPERVISOR_GAME_CHAT_SOURCE,
}
);
assert!(parse_cli_command(&[
"--swarm-chat".to_string(),
"--game-chat-smoke".to_string(),
"/tmp/game-project".to_string(),
])
.is_err());
assert!(parse_cli_command(&[
"--swarm-chat".to_string(),
"--autonomous-game-build".to_string(),
"--game-chat-smoke".to_string(),
"/tmp/game-project".to_string(),
"code-prototype".to_string(),
])
.is_err());
assert!(parse_cli_command(&[
"--swarm-chat".to_string(),
"--autonomous-game-build".to_string(),
"--game-chat-smoke".to_string(),
"--game-chat-smoke".to_string(),
"/tmp/game-project".to_string(),
])
.is_err());
}
#[test]
fn swarm_chat_rejects_missing_or_extra_arguments() {
assert!(parse_cli_command(&["--swarm-chat".to_string()]).is_err());
@@ -419,6 +419,7 @@ pub(crate) fn static_delegate_completion_barrier_at(
) -> Result<StaticDelegateCompletionBarrier, String> {
validate_static_delegate_id(parent_agent_id, "parentAgentId", 96)?;
validate_static_delegate_id(parent_run_id, "parentRunId", 160)?;
reconcile_claimed_game_chat_safe_default_half_states_at(root, parent_agent_id, parent_run_id)?;
let claims = list_static_delegate_claims_at(root)?
.into_iter()
.filter(|claim| {
@@ -1677,6 +1678,141 @@ fn write_static_delegate_delivery_at(
)
}
pub(crate) fn replace_claimed_static_delegate_result_for_game_chat_safe_default_at(
root: &Path,
expected: &StaticDelegateDeliveryRecord,
) -> Result<StaticDelegateDeliveryRecord, String> {
validate_static_delegate_delivery_record(expected)?;
let expected_result = expected
.structured_result
.as_ref()
.ok_or_else(|| "game-chat 安全默认返工 delivery 缺少 structuredResult".to_string())?;
let (result_summary, structured_result) =
game_chat_safe_default_repair_replacement(expected_result)
.ok_or_else(|| "game-chat 安全默认返工转换状态无效".to_string())?;
let expected_is_original =
expected_result.contract_status == StaticDelegateContractStatus::NeedsUserInput;
let expected_is_converted = expected.result_summary.as_deref() == Some(result_summary.as_str())
&& expected_result == &structured_result;
if expected.status != StaticDelegateDeliveryStatus::ClaimedByParent
|| expected.clarification_request_id.is_some()
|| expected.clarification_answers_sha256.is_some()
|| (!expected_is_original && !expected_is_converted)
{
return Err("game-chat 安全默认返工转换状态无效".to_string());
}
validate_static_delegate_structured_result(
&structured_result,
expected.terminal_status.as_deref().unwrap_or_default(),
&expected.expected_artifacts,
)?;
let claim_action_id = expected
.claimed_by_action_id
.as_deref()
.ok_or_else(|| "game-chat 安全默认返工 delivery 缺少 claim actionId".to_string())?;
let _claim_lock = acquire_static_delegate_claim_lock_at(
root,
&expected.parent_agent_id,
&expected.parent_run_id,
claim_action_id,
)?;
let _delivery_locks =
acquire_static_delegate_delivery_locks_at(root, vec![expected.delegation_id.clone()])?;
let mut delivery = read_static_delegate_delivery_at(root, &expected.delegation_id)?
.ok_or_else(|| {
format!(
"game-chat 安全默认返工 delivery 不存在:{}",
expected.delegation_id
)
})?;
validate_static_delegate_delivery_identity(&delivery, expected)?;
if delivery.status != StaticDelegateDeliveryStatus::ClaimedByParent
|| delivery.claimed_by_action_id.as_deref() != Some(claim_action_id)
|| delivery.clarification_request_id.is_some()
|| delivery.clarification_answers_sha256.is_some()
{
return Err("game-chat 安全默认返工 delivery 当前状态无效".to_string());
}
let mut claim = read_static_delegate_claim_at(
root,
&expected.parent_agent_id,
&expected.parent_run_id,
claim_action_id,
)?
.ok_or_else(|| "game-chat 安全默认返工缺少原 claim".to_string())?;
let receipt_index = claim
.receipts
.iter()
.position(|receipt| receipt.delegation_id == expected.delegation_id)
.ok_or_else(|| "game-chat 安全默认返工 claim 缺少原 delivery".to_string())?;
let receipt = &claim.receipts[receipt_index];
if receipt.target_agent_id != delivery.target_agent_id
|| receipt.status != delivery.terminal_status.as_deref().unwrap_or_default()
|| receipt.acceptance_criteria != delivery.acceptance_criteria
|| receipt.expected_artifacts != delivery.expected_artifacts
|| receipt.repair_of_delegation_id != delivery.repair_of_delegation_id
{
return Err("game-chat 安全默认返工 claim 与 delivery 身份冲突".to_string());
}
let delivery_is_original = delivery.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
});
let delivery_is_converted = delivery.result_summary.as_deref() == Some(result_summary.as_str())
&& delivery.structured_result.as_ref() == Some(&structured_result);
let receipt_is_original = receipt.structured_result.as_ref().is_some_and(|result| {
result.contract_status == StaticDelegateContractStatus::NeedsUserInput
});
let receipt_is_converted = receipt.summary == result_summary
&& receipt.structured_result.as_ref() == Some(&structured_result);
if (!delivery_is_original && !delivery_is_converted)
|| (!receipt_is_original && !receipt_is_converted)
{
return Err("game-chat 安全默认返工 delivery/claim 含非唯一转换差异".to_string());
}
if delivery_is_original {
let original = delivery
.structured_result
.as_ref()
.expect("original delivery result exists");
let (_, converted) = game_chat_safe_default_repair_replacement(original)
.ok_or_else(|| "game-chat 安全默认返工原 delivery 无法转换".to_string())?;
if converted != structured_result
|| (receipt_is_original
&& (receipt.summary != delivery.result_summary.as_deref().unwrap_or_default()
|| receipt.structured_result.as_ref() != Some(original)))
{
return Err("game-chat 安全默认返工 claim 与原 delivery 结果冲突".to_string());
}
} else if receipt_is_original {
let original = receipt
.structured_result
.as_ref()
.expect("original receipt result exists");
let (_, converted) = game_chat_safe_default_repair_replacement(original)
.ok_or_else(|| "game-chat 安全默认返工原 claim 无法转换".to_string())?;
if converted != structured_result {
return Err("game-chat 安全默认返工 converted delivery 与原 claim 冲突".to_string());
}
}
if !delivery_is_converted {
delivery.result_summary = Some(result_summary.clone());
delivery.structured_result = Some(structured_result.clone());
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(root, &delivery)?;
}
if !receipt_is_converted {
let receipt = &mut claim.receipts[receipt_index];
receipt.summary = result_summary;
receipt.structured_result = Some(structured_result);
claim.updated_at = unix_timestamp();
write_static_delegate_claim_at(root, &claim)?;
}
Ok(delivery)
}
fn read_static_delegate_claim_at(
root: &Path,
parent_agent_id: &str,
@@ -2271,6 +2407,140 @@ mod tests {
assert!(error.contains("user.input_request"));
}
#[test]
fn game_chat_safe_default_replacement_recovers_both_persisted_half_states() {
let root = std::env::temp_dir().join(format!(
"genarrative-safe-default-half-state-{}-{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.expect("system time after unix epoch")
.as_nanos()
));
init_local_game_project_at(&root, "project-1", "安全默认半状态恢复测试")
.expect("project init");
let parent_agent_id = GAME_CREATOR_PROJECT_SUPERVISOR_AGENT_ID;
let parent_run_id = "safe-default-half-state-parent-run";
bind_supervisor_collaboration_policy_snapshot_at(
&root,
parent_agent_id,
parent_run_id,
&SupervisorCollaborationPolicy::default(),
"legacy-current-project-policy",
)
.expect("bind collaboration snapshot");
for (suffix, convert_delivery_first) in [("delivery", true), ("claim", false)] {
let action_id = format!("safe-default-half-state-action-{suffix}");
let delegation_id = format!("safe-default-half-state-delegation-{suffix}");
let mut delivery = new_static_delegate_delivery_with_contract(
parent_agent_id,
"safe-default-half-state-session",
parent_run_id,
&format!("safe-default-half-state-parent-action-{suffix}"),
&delegation_id,
"code-prototype",
&format!("safe-default-half-state-child-session-{suffix}"),
&format!("safe-default-half-state-child-run-{suffix}"),
&["采用安全默认值继续".to_string()],
&[],
None,
);
create_or_read_static_delegate_delivery_at(&root, &delivery).expect("create delivery");
let original = build_static_delegate_structured_result_at(
&root,
"completed",
&[],
false,
None,
None,
None,
Some(&format!(
"{STATIC_DELEGATE_USER_INPUT_PREFIX}{}",
serde_json::json!({
"questions": [{
"id": "visual_style",
"header": "风格",
"question": "请选择视觉风格",
"options": [
{"label": "明亮", "description": "使用明亮配色"},
{"label": "柔和", "description": "使用柔和配色"}
]
}]
})
)),
)
.expect("build needs-user-input result");
mark_static_delegate_delivery_ready_with_result_at(
&root,
&delivery.target_agent_id,
&delivery.target_session_id,
&delivery.target_run_id,
&delivery.delegation_id,
"completed",
"需要用户选择",
original.clone(),
)
.expect("mark delivery ready");
claim_ready_static_delegate_receipts_at(
&root,
parent_agent_id,
parent_run_id,
&action_id,
)
.expect("claim delivery");
delivery = read_static_delegate_delivery_at(&root, &delegation_id)
.expect("read delivery")
.expect("delivery exists");
let (summary, converted) = game_chat_safe_default_repair_replacement(&original)
.expect("convert safe default result");
if convert_delivery_first {
delivery.result_summary = Some(summary.clone());
delivery.structured_result = Some(converted.clone());
delivery.updated_at = unix_timestamp();
write_static_delegate_delivery_at(&root, &delivery).expect("write delivery half");
} else {
let mut claim = read_static_delegate_claim_at(
&root,
parent_agent_id,
parent_run_id,
&action_id,
)
.expect("read claim")
.expect("claim exists");
let receipt = claim
.receipts
.iter_mut()
.find(|receipt| receipt.delegation_id == delegation_id)
.expect("claim receipt");
receipt.summary = summary.clone();
receipt.structured_result = Some(converted.clone());
claim.updated_at = unix_timestamp();
write_static_delegate_claim_at(&root, &claim).expect("write claim half");
}
let reconciled = replace_claimed_static_delegate_result_for_game_chat_safe_default_at(
&root, &delivery,
)
.expect("reconcile half state");
assert_eq!(reconciled.result_summary.as_deref(), Some(summary.as_str()));
assert_eq!(reconciled.structured_result.as_ref(), Some(&converted));
let claim =
read_static_delegate_claim_at(&root, parent_agent_id, parent_run_id, &action_id)
.expect("read reconciled claim")
.expect("reconciled claim exists");
let receipt = claim
.receipts
.iter()
.find(|receipt| receipt.delegation_id == delegation_id)
.expect("reconciled receipt");
assert_eq!(receipt.summary, summary);
assert_eq!(receipt.structured_result.as_ref(), Some(&converted));
}
fs::remove_dir_all(root).ok();
}
#[test]
fn stale_prepared_claim_snapshot_cannot_downgrade_observed_claim() {
let root = std::env::temp_dir().join(format!(
@@ -33,5 +33,39 @@ pub(crate) use server::{
bind_loopback_listener_with_linux_fallback, run_external_agent_runner_server,
};
#[cfg(test)]
pub(crate) fn simulate_external_agent_runner_cross_boot_owner_claim_for_test(
root: &std::path::Path,
config_dir: &std::path::Path,
previous_boot_id: &str,
current_boot_id: &str,
) -> Result<(std::path::PathBuf, Option<String>), String> {
let previous = project_owner::acquire_external_agent_runner_project_execution_owner(
root,
previous_boot_id,
protocol::EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
)?;
drop(previous);
let state = state::ExternalAgentRunnerServerState::new(
config_dir.join(protocol::EXTERNAL_AGENT_RUNNER_ENDPOINT_FILE_NAME),
protocol::ExternalAgentRunnerEndpoint {
protocol_version: protocol::EXTERNAL_AGENT_RUNNER_PROTOCOL_VERSION,
pid: std::process::id(),
boot_id: current_boot_id.to_string(),
port: 41009,
token: "owner-provider-token-owner-provider-token".to_string(),
heartbeat_at: 1_725_000_000_000,
executable_fingerprint: Some("a".repeat(64)),
process_start_identity: None,
},
);
let claim = state.claim_project_execution_owner(root)?;
let result = (claim.root, claim.recovered_from_boot_id);
let second = state.claim_project_execution_owner(root)?;
if second.recovered_from_boot_id.is_some() {
return Err("同 boot execution owner 重入重复触发恢复".to_string());
}
Ok(result)
}
#[cfg(test)]
mod tests;
@@ -368,10 +368,12 @@ pub(super) fn dispatch_external_agent_runner_wake_pending_request(
pub(super) fn external_agent_runner_response_is_cacheable(
response: &ExternalAgentRunnerResponse,
) -> bool {
response
.error
.as_ref()
.is_none_or(|error| error.code != EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE)
response.error.as_ref().is_none_or(|error| {
!matches!(
error.code.as_str(),
EXTERNAL_AGENT_RUNNER_RETRYABLE_WAKE_ERROR_CODE | "project-execution-owned"
)
})
}
pub(super) fn cache_external_agent_runner_response_if_cacheable(
@@ -392,8 +394,77 @@ pub(super) fn cache_external_agent_runner_response_if_cacheable(
pub(super) fn dispatch_external_agent_runner_runtime_request(
request: &ExternalAgentRunnerRequest,
state: &ExternalAgentRunnerServerState,
) -> ExternalAgentRunnerResponse {
dispatch_external_agent_runner_runtime_request_with_owner_claim(request, state, |root| {
state.claim_project_execution_owner(root)
})
}
pub(super) fn dispatch_external_agent_runner_runtime_request_with_owner_claim(
request: &ExternalAgentRunnerRequest,
state: &ExternalAgentRunnerServerState,
claim_owner: impl Fn(&Path) -> Result<ExternalAgentRunnerProjectExecutionOwnerClaim, String>,
) -> ExternalAgentRunnerResponse {
let fingerprint = external_agent_runner_request_fingerprint(request);
{
let cache = lock_unpoisoned(&state.write_request_cache);
if let Some(cached) = cache.find(&request.request_id) {
if cached.fingerprint == fingerprint {
return cached.response.clone();
}
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"request-id-conflict",
"同一 requestId 不能用于不同请求",
);
}
}
let requires_project_execution_owner = matches!(
request.method.as_str(),
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
| "runtime.steer"
| "runtime.interrupt_for_steer_decision"
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact"
);
if requires_project_execution_owner && state.draining.load(Ordering::Acquire) {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 正在排空并准备退出,拒绝新的写请求",
);
}
let token = state.endpoint_snapshot().token;
let claimed_project_root = if requires_project_execution_owner {
let root = match external_agent_runner_request_root(request) {
Ok(root) => root,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
};
match claim_owner(&root) {
Ok(claim) => Some(claim.root),
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"project-execution-owned",
error,
);
}
}
} else {
None
};
let mut cache = lock_unpoisoned(&state.write_request_cache);
if let Some(cached) = cache.find(&request.request_id) {
if cached.fingerprint == fingerprint {
@@ -406,26 +477,6 @@ pub(super) fn dispatch_external_agent_runner_runtime_request(
);
}
if matches!(
request.method.as_str(),
"runtime.wake_pending"
| "runtime.resume"
| "runtime.continue_action"
| "runtime.steer"
| "runtime.interrupt_for_steer_decision"
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact"
) && state.draining.load(Ordering::Acquire)
{
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"runner-draining",
"Agent Runner 正在排空并准备退出,拒绝新的写请求",
);
}
let token = state.endpoint_snapshot().token;
let response = match request.method.as_str() {
"runtime.wake_pending"
| "runtime.resume"
@@ -435,26 +486,8 @@ pub(super) fn dispatch_external_agent_runner_runtime_request(
| "runtime.pause"
| "runtime.cancel"
| "runtime.compact" => {
let root = match external_agent_runner_request_root(request) {
Ok(root) => root,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"invalid-params",
error,
);
}
};
let root = match state.claim_project_execution_owner(&root) {
Ok(root) => root,
Err(error) => {
return ExternalAgentRunnerResponse::failure(
&request.request_id,
"project-execution-owned",
error,
);
}
};
let root = claimed_project_root
.expect("runtime write request must claim its project execution owner");
if request.method == "runtime.wake_pending" {
dispatch_external_agent_runner_wake_pending_request(request, &root, &token)
} else {

Some files were not shown because too many files have changed in this diff Show More