完善Agent受控命令与推理配置

新增受控 command.exec 固定程序、参数策略、隔离环境和有界审计
接入项目 revision、验证资格、确认动作复核与失败核对门禁
支持全局及每 Agent 独立推理档位并提供发布默认配置
补齐前端配置、共享契约、Runtime 回归和真实 Provider 验收
同步开发流程、技术方案与项目共享决策记录
This commit is contained in:
AIGameCreator App
2026-07-12 23:22:11 +08:00
parent 5e627a4677
commit 49ea9eec67
18 changed files with 4068 additions and 111 deletions
@@ -18,6 +18,10 @@ const visibleText = 'GENARRATIVE_REAL_E2E_VISIBLE';
const patchedText = 'REAL_E2E_PATCHED';
const editorAssetPrompt = 'real e2e amber arcade token, transparent background';
const verificationCommand = 'node verify-e2e.mjs';
const commandFailureMarker = 'real-e2e-command=failed';
const commandPassedMarker = 'real-e2e-command=passed';
const failedCommandArgs = ['test'];
const successfulCommandArgs = ['run', 'check:e2e'];
const pollIntervalMs = 750;
const runTimeoutMs = 30 * 60 * 1000;
const commandOutputLimit = 4 * 1024 * 1024;
@@ -401,7 +405,10 @@ async function seedDisposableProject() {
{
name: 'genarrative-agent-runtime-real-e2e-project',
private: true,
scripts: { 'check:e2e': verificationCommand },
scripts: {
test: verificationCommand,
'check:e2e': verificationCommand,
},
},
null,
2,
@@ -409,7 +416,7 @@ async function seedDisposableProject() {
),
fs.writeFile(
path.join(state.projectRoot, 'verify-e2e.mjs'),
`import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nif (!html.includes('${patchedText}') || !html.includes('<canvas') || !html.includes('requestAnimationFrame') || !agents.includes('REPOSITORY_CONTEXT_MARKER')) process.exit(1);\nconsole.log('real-e2e-project.verify=passed');\n`,
`import fs from 'node:fs';\nconst html = fs.readFileSync('game/index.html', 'utf8');\nconst agents = fs.readFileSync('AGENTS.md', 'utf8');\nif (!html.includes('${patchedText}') || !html.includes('<canvas') || !html.includes('requestAnimationFrame') || !agents.includes('REPOSITORY_CONTEXT_MARKER')) { console.error('${commandFailureMarker}'); process.exit(1); }\nconsole.log('${commandPassedMarker}');\n`,
),
fs.writeFile(
path.join(state.projectRoot, 'game/index.html'),
@@ -482,12 +489,12 @@ function buildTaskPrompt(suite) {
: '本套件禁止调用 canvas.asset_generate。';
return `这是 Agent Runtime 真实 E2E,必须完整执行,不能跳过或口头声称完成。
1. 使用 repository context:先 project.index,并用 file.read 读取 AGENTS.md、package.json、game/index.html;不得读取任何敏感诱饵文件。
2. 修改前调用 project.checkpoint。随后优先用 file.patch,把 game/index.html 中唯一的 REAL_E2E_TARGET:before 精确替换为 ${patchedText};若精确 patch 不可用才允许 file.write。保留可见文本 ${visibleText} 和非空 canvas 动画。
2. 修改前先调用 command.exec,input 必须是 {"program":"npm","args":["test"],"cwd":".","timeoutSeconds":120};它必须真实失败并返回 ${commandFailureMarker},不得把失败当成完成。随后调用 project.checkpoint,再优先用 file.patch,把 game/index.html 中唯一的 REAL_E2E_TARGET:before 精确替换为 ${patchedText};若精确 patch 不可用才允许 file.write。保留可见文本 ${visibleText} 和非空 canvas 动画。
3. ${canvasStep}
4. 必须且只能调用一次 agent.spawn_isolated,joinMode=all,children 恰好三个:前两个 templateAgentId 都是 code-prototype,第三个是 quality-review。三个子任务只读检查 AGENTS.md 与各自已存在的 evidence.txt,不修改项目;expectedArtifacts 分别为 e2e/isolated-a/evidence.txt、e2e/isolated-b/evidence.txt、e2e/isolated-c/evidence.txt;writeScopes 分别为 e2e/isolated-a/**、e2e/isolated-b/**、e2e/isolated-c/**;每项 acceptanceCriteria 写“已读取 repository context 并给出独立结论”。必须等待三个子结果形成唯一一次 all join,不得重复 spawn。
5. 最后一次项目修改后,读取 package.json 的原始脚本并调用 project.verify,input 必须是 {"script":"check:e2e","expectedCommand":"${verificationCommand}","timeoutSeconds":120}。
5. 最后一次项目修改后先再次调用 command.exec,input 必须是 {"program":"npm","args":["run","check:e2e"],"cwd":".","timeoutSeconds":120},并取得 ${commandPassedMarker}。随后读取 package.json 的原始脚本并调用 project.verify,input 必须是 {"script":"check:e2e","expectedCommand":"${verificationCommand}","timeoutSeconds":120}。
6. 验证通过后调用 preview.validate,input 必须包含 {"viewports":["desktop","mobile"],"expectedText":["${visibleText}","${patchedText}"],"settleMs":1000,"failOnConsoleError":true},必须真实生成 desktop/mobile PNG 且通过。
7. 只有 repository context、checkpoint、read、patch/write、project.verify、preview.validate、三个隔离实例和单一 join 全部形成落盘证据后才可最终回复。不要输出或转述任何 API Key。`;
7. 只有 repository context、失败命令反馈、checkpoint、read、patch/write、成功命令复验、project.verify、preview.validate、三个隔离实例和单一 join 全部形成落盘证据后才可最终回复。不要输出或转述任何 API Key。`;
}
async function prepareCliBinary() {
@@ -715,6 +722,7 @@ async function confirmPendingActions() {
'project.checkpoint',
'file.patch',
'file.write',
'command.exec',
'project.verify',
'preview.validate',
'agent.spawn_isolated',
@@ -853,6 +861,90 @@ async function validateLandedEvidence() {
(execution) => auditPathEquals(execution.inputSummary, 'game/index.html'),
);
assert(Boolean(mutationExecution), 'file-mutation-evidence-missing');
const failedCommandArgsSha256 = createHash('sha256')
.update(JSON.stringify(failedCommandArgs))
.digest('hex');
const successfulCommandArgsSha256 = createHash('sha256')
.update(JSON.stringify(successfulCommandArgs))
.digest('hex');
const commandRecords = agentDb
.map((record, index) => ({ record, index }))
.filter(
({ record }) =>
record.recordType === 'agent.runtime.command.exec' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId,
);
assert(commandRecords.length === 2, 'command-exec-record-count-invalid');
const failedCommandRecord = commandRecords.find(
({ record }) => record.status === 'failed',
);
const successfulCommandRecord = commandRecords.find(
({ record }) => record.status === 'completed',
);
assert(
failedCommandRecord?.record.program === 'npm' &&
failedCommandRecord.record.argsCount === failedCommandArgs.length &&
failedCommandRecord.record.argsSha256 === failedCommandArgsSha256 &&
failedCommandRecord.record.cwd === '.' &&
Number.isInteger(failedCommandRecord.record.exitCode) &&
failedCommandRecord.record.exitCode !== 0 &&
failedCommandRecord.record.timedOut === false &&
failedCommandRecord.record.sourceChanged === false &&
failedCommandRecord.record.output?.includes(commandFailureMarker),
'command-exec-failure-record-invalid',
);
assert(
successfulCommandRecord?.record.program === 'npm' &&
successfulCommandRecord.record.argsCount ===
successfulCommandArgs.length &&
successfulCommandRecord.record.argsSha256 ===
successfulCommandArgsSha256 &&
successfulCommandRecord.record.cwd === '.' &&
successfulCommandRecord.record.exitCode === 0 &&
successfulCommandRecord.record.timedOut === false &&
successfulCommandRecord.record.sourceChanged === false &&
successfulCommandRecord.record.output?.includes(commandPassedMarker),
'command-exec-success-record-invalid',
);
assert(
commandRecords.every(
({ record }) =>
!Object.hasOwn(record, 'args') &&
!Object.hasOwn(record, 'arguments') &&
/^[0-9a-f]{64}$/u.test(record.argsSha256) &&
isNonEmptyString(record.actionId),
),
'command-exec-raw-argv-audit-leak',
);
const failedCommandObservationIndex = agentDb.findIndex(
(record) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.agentId === mainAgentId &&
record.runId === state.initialRunId &&
record.actionId === failedCommandRecord.record.actionId &&
record.tool === 'command.exec' &&
record.status === 'command-failed' &&
record.decision === 'approved',
);
assert(
failedCommandObservationIndex > failedCommandRecord.index,
'command-exec-failure-observation-missing',
);
const successfulCommandExecution = requireSuccessfulToolExecution(
agentDb,
'command.exec',
state.initialRunId,
(execution) =>
auditInputValue(execution.inputSummary, 'program') === 'npm' &&
auditInputValue(execution.inputSummary, 'argsCount') ===
String(successfulCommandArgs.length) &&
auditInputValue(execution.inputSummary, 'argsSha256') ===
successfulCommandArgsSha256 &&
auditInputValue(execution.inputSummary, 'cwd') === '.' &&
auditInputValue(execution.inputSummary, 'timeoutSeconds') === '120',
'command-exec-success-action-invalid',
);
const verificationExecution = requireSuccessfulToolExecution(
agentDb,
'project.verify',
@@ -912,10 +1004,23 @@ async function validateLandedEvidence() {
),
'project-index-not-before-repository-reads',
);
assert(
failedCommandObservationIndex < checkpointExecution.startIndex,
'checkpoint-not-after-failed-command-feedback',
);
assert(
checkpointExecution.completionIndex < mutationExecution.startIndex,
'checkpoint-not-before-file-mutation',
);
assert(
mutationExecution.completionIndex < successfulCommandExecution.startIndex,
'successful-command-not-after-file-mutation',
);
assert(
successfulCommandExecution.completionIndex <
verificationExecution.startIndex,
'project-verification-not-after-successful-command',
);
assert(
mutationExecution.completionIndex < verificationExecution.startIndex,
'verification-not-after-file-mutation',
@@ -1471,6 +1576,7 @@ async function validateLandedEvidence() {
...repositoryReadExecutions,
checkpointExecution,
mutationExecution,
successfulCommandExecution,
verificationExecution,
previewExecution,
spawnExecution,
@@ -1493,6 +1599,7 @@ async function validateLandedEvidence() {
repositoryContextSourceCount:
contextBundle.repositoryContextSourcePaths.length,
checkpointFileCount: checkpointRecord.fileCount,
commandExecRunCount: commandRecords.length,
editorApiAssetCount: editorAssetRecord ? 1 : 0,
verificationPassed: true,
browserValidationCount: browserReports.length,
@@ -1639,6 +1746,7 @@ function emptyEvidence() {
projectIndexExecutionCount: 0,
repositoryContextSourceCount: 0,
checkpointFileCount: 0,
commandExecRunCount: 0,
editorApiAssetCount: 0,
verificationPassed: false,
browserValidationCount: 0,
@@ -1828,7 +1936,8 @@ function validateConfirmedActionLifecycles(records) {
const observed = lifecycle.filter(
({ record }) =>
record.recordType === 'agent.runtime.tool_observation' &&
record.status === 'ok',
record.decision === 'approved' &&
record.status !== 'waiting-for-confirmation',
);
const required = lifecycle.filter(
({ record }) =>
@@ -1859,7 +1968,6 @@ function validateConfirmedActionLifecycles(records) {
) &&
waiting[0].record.tool === tool &&
observed[0].record.tool === tool &&
observed[0].record.decision === 'approved' &&
approved[0].record.tool === tool &&
waiting[0].record.actionFingerprint === actionFingerprint &&
approved[0].record.actionFingerprint === actionFingerprint &&
@@ -449,6 +449,32 @@ if (defaultAppConfig.llm?.apiKey !== '') {
throw new Error('AI game creator shell default llm.apiKey must stay empty');
}
const allowedLlmReasoningEfforts = new Set([
'default',
'low',
'medium',
'high',
]);
if (defaultAppConfig.llm?.reasoningEffort !== 'high') {
throw new Error(
'AI game creator shell default llm.reasoningEffort must stay high',
);
}
for (const [agentId, agentConfig] of Object.entries(
defaultAppConfig.agentLlm ?? {},
)) {
if (
agentConfig?.reasoningEffort !== undefined &&
!allowedLlmReasoningEfforts.has(agentConfig.reasoningEffort)
) {
throw new Error(
`AI game creator shell agentLlm.${agentId}.reasoningEffort is invalid`,
);
}
}
if (defaultAppConfig.editorApi?.apiKey !== '') {
throw new Error(
'AI game creator shell default editorApi.apiKey must stay empty',