完善Agent受控命令与推理配置
新增受控 command.exec 固定程序、参数策略、隔离环境和有界审计 接入项目 revision、验证资格、确认动作复核与失败核对门禁 支持全局及每 Agent 独立推理档位并提供发布默认配置 补齐前端配置、共享契约、Runtime 回归和真实 Provider 验收 同步开发流程、技术方案与项目共享决策记录
This commit is contained in:
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user