diff --git a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs index be2bf6c95..48ea60216 100644 --- a/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs +++ b/apps/ai-game-creator-shell/scripts/agent-runtime-real-e2e.mjs @@ -39,6 +39,10 @@ const userInputAppDataSentinelFileName = '.agent-runtime-real-e2e-user-input-appdata.json'; const userInputAppDataSentinelSchema = 'genarrative-agent-runtime-real-e2e-user-input-appdata.v1'; +const scopedAgentsAppDataSentinelFileName = + '.agent-runtime-real-e2e-scoped-agents-appdata.json'; +const scopedAgentsAppDataSentinelSchema = + 'genarrative-agent-runtime-real-e2e-scoped-agents-appdata.v1'; const mainAgentId = 'code-prototype'; const projectSupervisorAgentId = 'project-supervisor'; const requestedRunId = `real-e2e-${Date.now()}-${randomUUID().slice(0, 8)}`; @@ -63,6 +67,7 @@ const webSearchSuite = 'web-search'; const contextCompactionSuite = 'context-compaction'; const mcpRuntimeSuite = 'mcp-runtime'; const userInputRuntimeSuite = 'user-input-runtime'; +const scopedAgentsSuite = 'scoped-agents'; const runtimeContextBundleSchemaVersion = 'game-creator-runtime-context-bundle.v5'; const providerRequestLifecycleSchemaVersion = @@ -85,6 +90,22 @@ const userInputAnswerCanary = `GENARRATIVE_USER_CHOICE_${randomUUID() .replaceAll('-', '') .slice(0, 20)}`; const userInputAnswerText = `选择轻量像素风,优先保证移动端轮廓和动作可读性;确认标记 ${userInputAnswerCanary}`; +const scopedAgentsRootCanary = `GENARRATIVE_SCOPE_ROOT_${randomUUID() + .replaceAll('-', '') + .slice(0, 20)}`; +const scopedAgentsGameCanary = `GENARRATIVE_SCOPE_GAME_${randomUUID() + .replaceAll('-', '') + .slice(0, 20)}`; +const scopedAgentsAlphaCanary = `GENARRATIVE_SCOPE_ALPHA_${randomUUID() + .replaceAll('-', '') + .slice(0, 20)}`; +const scopedAgentsBetaCanary = `GENARRATIVE_SCOPE_BETA_${randomUUID() + .replaceAll('-', '') + .slice(0, 20)}`; +const scopedAgentsAlphaPath = 'game/alpha/delivery.txt'; +const scopedAgentsBetaPath = 'game/beta/delivery.txt'; +const scopedAgentsVerificationScriptPath = 'verify-scoped-agents.mjs'; +const scopedAgentsVerificationCommand = `node ${scopedAgentsVerificationScriptPath}`; const webSearchBaselineApiUrl = 'https://api.github.com/repos/nodejs/node/releases/latest'; const goalSessionId = `agent-session-${mainAgentId}`; @@ -396,6 +417,13 @@ const state = { privateValues: [], reportLeakCount: 0, }, + scopedAgents: { + effectiveModel: null, + effectiveApiKind: null, + confirmedActionCount: 0, + privateValues: [], + reportLeakCount: 0, + }, confirmedActionIds: new Set(), cleanupPerformed: false, process: { @@ -453,12 +481,14 @@ try { } if (isMcpRuntimeSuite()) state.evidence = emptyMcpEvidence(); if (isUserInputRuntimeSuite()) state.evidence = emptyUserInputEvidence(); + if (isScopedAgentsSuite()) state.evidence = emptyScopedAgentsEvidence(); const loaded = await loadConfig(state.options.configDir); if ( isWebSearchSuite() || isContextCompactionSuite() || isMcpRuntimeSuite() || - isUserInputRuntimeSuite() + isUserInputRuntimeSuite() || + isScopedAgentsSuite() ) { state.formalConfigPathTranscriptScanner = new StreamingSecretScanner( absolutePathVariants(state.options.configDir, loaded.realConfigDir), @@ -493,6 +523,8 @@ try { await runMcpRuntimeE2e(); } else if (isUserInputRuntimeSuite()) { await runUserInputRuntimeE2e(); + } else if (isScopedAgentsSuite()) { + await runScopedAgentsE2e(); } else if (isProcessSessionSuite()) { await runProcessSessionE2e(); } else { @@ -645,6 +677,28 @@ try { state.status = 'FAIL'; recordError('user-input-formal-config-cli-call-detected'); } + } else if (isScopedAgentsSuite()) { + state.evidence.scopedAgentsRunnerStopped = state.isolatedRunner.stopped; + state.evidence.scopedAgentsAppDataCleanupPerformed = + state.isolatedRunner.cleanupPerformed; + state.evidence.scopedAgentsRunnerKillMethod = killMethod; + state.evidence.scopedAgentsRunnerPidfdClaimCount = + state.isolatedRunner.pidfdClaimCount; + state.evidence.scopedAgentsRunnerPidfdSignalCount = + state.isolatedRunner.pidfdSignalCount; + state.evidence.formalConfigCliCallCount = + state.isolatedRunner.sourceConfigCliCallCount; + state.evidence.sourceRunnerEndpointUnchanged = + state.isolatedRunner.sourceRunnerEndpointUnchanged; + state.evidence.sourceConfigReplicaCount = + state.isolatedRunner.configLinks.length; + state.evidence.sourceConfigReplicasVerified = + state.isolatedRunner.sourceConfigLinksVerified; + state.evidence.isolatedAppDataUsed = true; + if (state.isolatedRunner.sourceConfigCliCallCount > 0) { + state.status = 'FAIL'; + recordError('scoped-agents-formal-config-cli-call-detected'); + } } else { assert( isContextCompactionSuite(), @@ -742,6 +796,16 @@ try { recordError('user-input-partial-evidence-read-failed', error); } } + if (isScopedAgentsSuite() && state.projectRoot && state.status !== 'PASS') { + try { + state.evidence = { + ...state.evidence, + ...(await collectPartialScopedAgentsEvidence()), + }; + } catch (error) { + recordError('scoped-agents-partial-evidence-read-failed', error); + } + } if (state.projectRoot && state.secrets.length > 0) { try { state.projectLeakCount = await countSecretsInProject( @@ -921,6 +985,20 @@ try { report = JSON.stringify(summary, null, 2); } } + if (isScopedAgentsSuite()) { + state.scopedAgents.reportLeakCount = countExactSecrets( + Buffer.from(report), + state.scopedAgents.privateValues, + ); + state.evidence.scopedAgentsReportLeakCount = + state.scopedAgents.reportLeakCount; + if (state.scopedAgents.reportLeakCount > 0) { + state.status = 'FAIL'; + recordError('scoped-agents-private-instruction-report-leak-detected'); + summary = buildSummary(); + report = JSON.stringify(summary, null, 2); + } + } state.projectPathReportLeakCount = countExactSecrets( Buffer.from(report), disposableProjectPathVariants(), @@ -936,7 +1014,8 @@ try { isWebSearchSuite() || isContextCompactionSuite() || isMcpRuntimeSuite() || - isUserInputRuntimeSuite() + isUserInputRuntimeSuite() || + isScopedAgentsSuite() ) { state.formalConfigPathReportLeakCount = countExactSecrets( Buffer.from(report), @@ -987,11 +1066,15 @@ try { ].filter(isNonEmptyString), ) : 0; + const remainingScopedAgentsReportLeakCount = isScopedAgentsSuite() + ? countExactSecrets(Buffer.from(report), state.scopedAgents.privateValues) + : 0; const remainingFormalConfigPathReportLeakCount = isWebSearchSuite() || isContextCompactionSuite() || isMcpRuntimeSuite() || - isUserInputRuntimeSuite() + isUserInputRuntimeSuite() || + isScopedAgentsSuite() ? countExactSecrets(Buffer.from(report), formalConfigPathVariants()) : 0; if ( @@ -1000,6 +1083,7 @@ try { remainingWebSearchReportLeakCount > 0 || remainingMcpReportLeakCount > 0 || remainingUserInputReportLeakCount > 0 || + remainingScopedAgentsReportLeakCount > 0 || remainingFormalConfigPathReportLeakCount > 0 ) { state.status = 'FAIL'; @@ -1012,9 +1096,11 @@ try { ? 'mcp-report-redaction-required' : remainingUserInputReportLeakCount > 0 ? 'user-input-report-redaction-required' - : remainingFormalConfigPathReportLeakCount > 0 - ? 'formal-config-path-report-redaction-required' - : 'web-search-report-redaction-required', + : remainingScopedAgentsReportLeakCount > 0 + ? 'scoped-agents-report-redaction-required' + : remainingFormalConfigPathReportLeakCount > 0 + ? 'formal-config-path-report-redaction-required' + : 'web-search-report-redaction-required', ); const safeSummary = { status: state.status, @@ -1030,6 +1116,7 @@ try { webSearchReportLeakCount: remainingWebSearchReportLeakCount, mcpReportLeakCount: remainingMcpReportLeakCount, userInputReportLeakCount: remainingUserInputReportLeakCount, + scopedAgentsReportLeakCount: remainingScopedAgentsReportLeakCount, formalConfigPathReportLeakCount: remainingFormalConfigPathReportLeakCount, }, @@ -2818,6 +2905,830 @@ async function collectPartialUserInputEvidence() { }; } +function scopedAgentsExpectedContent(owner) { + const scopeCanary = + owner === 'alpha' ? scopedAgentsAlphaCanary : scopedAgentsBetaCanary; + return [ + `ROOT=${scopedAgentsRootCanary}`, + `GAME=${scopedAgentsGameCanary}`, + `SCOPE=${scopeCanary}`, + `OWNER=${owner}`, + '', + ].join('\n'); +} + +function scopedAgentsInstructionDocuments() { + return { + 'AGENTS.md': `# Root delivery policy\n\nFor every file named delivery.txt in this repository:\n- Its first line must be exactly ROOT=${scopedAgentsRootCanary}.\n- Preserve a single trailing newline.\n- Apply every more-specific AGENTS.md that covers the target path.\n- Never copy a rule from a sibling scope.\n- Do not repeat internal policy markers in the final response.\n`, + 'game/AGENTS.md': `# Game delivery policy\n\nFor every delivery.txt below game/:\n- Its second line must be exactly GAME=${scopedAgentsGameCanary}.\n- Keep the root policy as the first line and then apply the target directory policy.\n`, + 'game/alpha/AGENTS.md': `# Alpha delivery policy\n\nOnly for delivery.txt below game/alpha/:\n- Its third line must be exactly SCOPE=${scopedAgentsAlphaCanary}.\n- Its fourth line must be exactly OWNER=alpha.\n- It must not contain ${scopedAgentsBetaCanary} or OWNER=beta.\n`, + 'game/beta/AGENTS.md': `# Beta delivery policy\n\nOnly for delivery.txt below game/beta/:\n- Its third line must be exactly SCOPE=${scopedAgentsBetaCanary}.\n- Its fourth line must be exactly OWNER=beta.\n- It must not contain ${scopedAgentsAlphaCanary} or OWNER=alpha.\n`, + }; +} + +function scopedAgentsVerificationFixtureSource() { + const expected = Object.fromEntries( + [ + [scopedAgentsAlphaPath, scopedAgentsExpectedContent('alpha')], + [scopedAgentsBetaPath, scopedAgentsExpectedContent('beta')], + ].map(([targetPath, content]) => [targetPath, hashValue(content)]), + ); + return `import { createHash } from 'node:crypto';\nimport fs from 'node:fs';\n\nconst expected = ${JSON.stringify(expected, null, 2)};\nlet passed = true;\nfor (const [targetPath, expectedSha256] of Object.entries(expected)) {\n let content;\n try {\n content = fs.readFileSync(targetPath);\n } catch {\n passed = false;\n continue;\n }\n if (createHash('sha256').update(content).digest('hex') !== expectedSha256) {\n passed = false;\n }\n}\nif (!passed) {\n console.error('scoped-agents=failed');\n process.exit(1);\n}\nconsole.log('scoped-agents=passed');\n`; +} + +async function seedScopedAgentsDisposableProject() { + await seedDisposableProject(); + const documents = scopedAgentsInstructionDocuments(); + const alphaExpected = scopedAgentsExpectedContent('alpha'); + const betaExpected = scopedAgentsExpectedContent('beta'); + state.scopedAgents.privateValues = [ + scopedAgentsRootCanary, + scopedAgentsGameCanary, + scopedAgentsAlphaCanary, + scopedAgentsBetaCanary, + alphaExpected, + betaExpected, + ...Object.values(documents), + ]; + const verificationSource = scopedAgentsVerificationFixtureSource(); + assert( + countExactSecrets( + Buffer.from(verificationSource), + state.scopedAgents.privateValues, + ) === 0, + 'scoped-agents-verifier-reveals-instruction-content', + ); + + await Promise.all([ + fs.mkdir(path.join(state.projectRoot, 'game/alpha'), { recursive: true }), + fs.mkdir(path.join(state.projectRoot, 'game/beta'), { recursive: true }), + ]); + await Promise.all([ + ...Object.entries(documents).map(([relativePath, content]) => + fs.writeFile(path.join(state.projectRoot, relativePath), content), + ), + fs.writeFile( + path.join(state.projectRoot, scopedAgentsAlphaPath), + 'seeded alpha delivery\n', + ), + fs.writeFile( + path.join(state.projectRoot, scopedAgentsBetaPath), + 'seeded beta delivery\n', + ), + fs.writeFile( + path.join(state.projectRoot, scopedAgentsVerificationScriptPath), + verificationSource, + ), + fs.writeFile( + path.join(state.projectRoot, 'package.json'), + `${JSON.stringify( + { + name: 'genarrative-scoped-agents-real-e2e-project', + private: true, + scripts: { + test: scopedAgentsVerificationCommand, + 'check:e2e': scopedAgentsVerificationCommand, + }, + }, + null, + 2, + )}\n`, + ), + ]); + await runProcess( + 'git', + [ + 'add', + '--', + 'AGENTS.md', + 'game/AGENTS.md', + 'game/alpha/AGENTS.md', + 'game/beta/AGENTS.md', + scopedAgentsAlphaPath, + scopedAgentsBetaPath, + scopedAgentsVerificationScriptPath, + 'package.json', + ], + { cwd: state.projectRoot, timeoutMs: 30_000 }, + ); + await runProcess( + 'git', + ['commit', '--quiet', '-m', 'seed scoped agents real e2e'], + { cwd: state.projectRoot, timeoutMs: 30_000 }, + ); + const initialVerification = await runProcess( + process.execPath, + [scopedAgentsVerificationScriptPath], + { + cwd: state.projectRoot, + timeoutMs: 120_000, + allowNonZero: true, + }, + ); + assert( + initialVerification.code === 1 && + initialVerification.signal === null && + initialVerification.stderr.includes('scoped-agents=failed') && + !initialVerification.stdout.includes('scoped-agents=passed'), + 'scoped-agents-initial-fixture-not-failing', + ); + state.scopedAgents.initialVerificationFailed = true; +} + +function buildScopedAgentsTaskPrompt() { + return '当前项目的 game/alpha/delivery.txt 和 game/beta/delivery.txt 尚未符合对各自路径生效的仓库规范。请依据项目内适用于目标路径的指令修正这两个交付文件,只修改这两个文件;随后运行 package.json 声明的原始验收,只有真实通过后再简短汇报。不要转述项目指令正文或其中的内部标记。'; +} + +function assertScopedAgentsTaskPrompt(task) { + assert( + task.includes(scopedAgentsAlphaPath) && + task.includes(scopedAgentsBetaPath) && + task.includes('只修改这两个文件') && + task.includes('真实通过'), + 'scoped-agents-task-boundary-missing', + ); + for (const forbidden of [ + ...state.scopedAgents.privateValues, + 'AGENTS.md', + 'file.write', + 'file.patch', + 'project.patchset', + 'project.verify', + 'submit_agent_tool_plan', + ]) { + assert(!task.includes(forbidden), 'scoped-agents-task-recipe-leak'); + } +} + +async function runScopedAgentsE2e() { + await ensureOwnedRunnerStableKillSupport(); + await seedScopedAgentsDisposableProject(); + state.cliBinary = await prepareCliBinary(); + await prepareIsolatedSuiteAppData(); + + const task = buildScopedAgentsTaskPrompt(); + assertScopedAgentsTaskPrompt(task); + state.initialTask = { + chars: [...task].length, + sha256: hashValue(task), + }; + state.initialRunId = requestedRunId; + state.initialSessionId = goalSessionId; + state.isolatedRunner.launchAttempted = true; + await runCli( + [ + '--agent-enqueue', + '--init', + state.projectRoot, + mainAgentId, + state.initialRunId, + task, + ], + { timeoutMs: 120_000 }, + ); + await claimOwnedRunner(); + const runtime = await waitForResponseRuntimeIdentity(); + assert( + runtime.agentId === mainAgentId && + runtime.runId === state.initialRunId && + runtime.sessionId === state.initialSessionId, + 'scoped-agents-runtime-identity-invalid', + ); + state.identityStable = true; + + await driveScopedAgentsRuntimeToCompletion(); + state.evidence = await validateScopedAgentsEvidence(); + assert(state.evidence.secretLeakCount === 0, 'loaded-key-leak-detected'); +} + +function normalizeScopedAgentsTargetPath(value) { + assert( + isNonEmptyString(value) && !path.posix.isAbsolute(value), + 'scoped-agents-pending-path-invalid', + ); + const normalized = path.posix + .normalize(value.replaceAll('\\', '/')) + .replace(/^\.\//u, ''); + assert( + [scopedAgentsAlphaPath, scopedAgentsBetaPath].includes(normalized), + 'scoped-agents-pending-path-outside-deliveries', + ); + return normalized; +} + +function validateScopedAgentsPendingAction(pending) { + assert( + pending.agentId === mainAgentId && pending.runId === state.initialRunId, + 'scoped-agents-cross-run-pending-action', + ); + const input = pending.action?.input ?? pending.record?.action?.input ?? {}; + if (['file.write', 'file.patch'].includes(pending.tool)) { + normalizeScopedAgentsTargetPath(input.path); + return; + } + if (pending.tool === 'project.patchset') { + assert( + Array.isArray(input.changes) && + input.changes.length > 0 && + input.changes.length <= 2, + 'scoped-agents-patchset-shape-invalid', + ); + const targets = input.changes.map((change) => { + assert( + change?.operation === 'update', + 'scoped-agents-patchset-operation-invalid', + ); + return normalizeScopedAgentsTargetPath(change.path); + }); + assert( + new Set(targets).size === targets.length, + 'scoped-agents-patchset-duplicate-path', + ); + return; + } + if (pending.tool === 'project.checkpoint') return; + if (pending.tool === 'project.verify') { + const script = input.script; + const expectedCommand = input.expectedCommand ?? input.expected_command; + assert( + ['test', 'check:e2e'].includes(script) && + expectedCommand === scopedAgentsVerificationCommand, + 'scoped-agents-verification-request-invalid', + ); + return; + } + throw codedError(`scoped-agents-pending-tool-not-allowed:${pending.tool}`); +} + +async function driveScopedAgentsRuntimeToCompletion() { + const deadline = Date.now() + runTimeoutMs; + const allowedTools = new Set([ + 'file.write', + 'file.patch', + 'project.patchset', + 'project.checkpoint', + 'project.verify', + ]); + let quietPolls = 0; + while (Date.now() < deadline) { + const pending = await findPendingActions(); + for (const action of pending) validateScopedAgentsPendingAction(action); + if (pending.length > 0) { + const before = state.confirmedActionIds.size; + await confirmPendingActions( + allowedTools, + (action) => + action.agentId === mainAgentId && action.runId === state.initialRunId, + ); + state.scopedAgents.confirmedActionCount += + state.confirmedActionIds.size - before; + assert( + state.scopedAgents.confirmedActionCount <= 8, + 'scoped-agents-confirmation-count-excessive', + ); + await sleep(100); + continue; + } + + const [runtime, tasks, conversations] = await Promise.all([ + readRuntime(mainAgentId).catch(() => null), + readTaskSnapshot(), + readOptionalJsonl( + agentConversationPath(mainAgentId, state.initialSessionId), + ), + ]); + const task = tasks.latest.find( + (candidate) => + candidate.agentId === mainAgentId && + candidate.runId === state.initialRunId, + ); + if (task && isFailedTask(task)) { + throw codedError('scoped-agents-runtime-failed'); + } + if (runtime?.phase === 'needs-reconciliation') { + throw codedError('scoped-agents-runtime-needs-reconciliation'); + } + const completed = + runtime?.runId === state.initialRunId && + runtime?.sessionId === state.initialSessionId && + runtime?.status === 'idle' && + runtime?.phase === 'completed' && + task?.status === 'completed' && + task?.phase === 'completed' && + conversations.filter((message) => message.role === 'assistant').length === + 1; + if (completed) { + quietPolls += 1; + if (quietPolls >= 3) return; + } else { + quietPolls = 0; + } + await sleep(250); + } + throw codedError('scoped-agents-runtime-timeout'); +} + +async function readScopedAgentsPersistence() { + const [ + taskSnapshot, + events, + agentDb, + conversations, + activity, + output, + runtimeState, + contextBundle, + ] = await Promise.all([ + readTaskSnapshot(), + readAllRuntimeEvents(), + readOptionalJsonl(path.join(state.projectRoot, '.agent/agent.db')), + readOptionalJsonl( + agentConversationPath(mainAgentId, state.initialSessionId), + ), + readOptionalJsonl(path.join(state.projectRoot, '.agent/activity.jsonl')), + readOptionalJsonl(path.join(state.projectRoot, '.agent/output.jsonl')), + readJson(mainRuntimeStatePath()).catch(() => null), + readJson(mainContextBundlePath()).catch(() => null), + ]); + return { + taskSnapshot, + events, + agentDb, + conversations, + activity, + output, + runtimeState, + contextBundle, + }; +} + +function validateScopedAgentsProviderLifecycle(agentDb) { + const lifecycle = agentDb.filter( + (record) => + record.recordType === 'agent.runtime.provider_request.lifecycle' && + record.agentId === mainAgentId && + record.runId === state.initialRunId, + ); + const byRequest = new Map(); + for (const record of lifecycle) { + assert( + record.auditSchemaVersion === providerRequestLifecycleSchemaVersion && + isNonEmptyString(record.requestId) && + isNonEmptyString(record.requestSlot) && + ['tool-plan', 'final-reply'].includes(record.requestKind) && + record.webSearchEnabled === false, + 'scoped-agents-provider-lifecycle-record-invalid', + ); + const records = byRequest.get(record.requestId) ?? []; + records.push(record); + byRequest.set(record.requestId, records); + } + for (const records of byRequest.values()) { + assert( + records.length === 2 && + records[0].status === 'started' && + records[1].status === 'completed' && + records[0].requestKind === records[1].requestKind && + records[0].requestSlot === records[1].requestSlot && + records[0].sessionId === records[1].sessionId, + 'scoped-agents-provider-lifecycle-sequence-invalid', + ); + } + const started = lifecycle.filter((record) => record.status === 'started'); + assert( + byRequest.size > 0 && + started.length === byRequest.size && + started.some((record) => record.requestKind === 'tool-plan'), + 'scoped-agents-provider-request-count-invalid', + ); + return { + requestIdentityCount: byRequest.size, + startedCount: started.length, + terminalCount: lifecycle.filter((record) => record.status === 'completed') + .length, + toolPlanCount: started.filter( + (record) => record.requestKind === 'tool-plan', + ).length, + finalReplyCount: started.filter( + (record) => record.requestKind === 'final-reply', + ).length, + }; +} + +function scopedAgentsExecutionTouchesPath(execution, targetPath) { + return scopedAgentsExecutionPaths(execution).includes(targetPath); +} + +function scopedAgentsExecutionPaths(execution) { + if (['file.write', 'file.patch'].includes(execution.tool)) { + return [scopedAgentsAlphaPath, scopedAgentsBetaPath].filter((targetPath) => + auditPathEquals(execution.inputSummary, targetPath), + ); + } + if (execution.tool !== 'project.patchset') return []; + return auditInputValue(execution.inputSummary, 'paths') + .split(',') + .map((value) => value.trim()) + .filter(Boolean) + .map((value) => value.slice(value.indexOf(':') + 1)) + .map((value) => + path.posix.normalize(value.replaceAll('\\', '/')).replace(/^\.\//u, ''), + ); +} + +function findScopedAgentsMutationExecution(agentDb, targetPath) { + for (const tool of ['file.write', 'file.patch', 'project.patchset']) { + const execution = findSuccessfulToolExecution( + agentDb, + tool, + state.initialRunId, + (candidate) => scopedAgentsExecutionTouchesPath(candidate, targetPath), + ); + if (execution) return execution; + } + return null; +} + +function assertScopedAgentsMutationBoundaries(agentDb) { + const mutationTools = new Set([ + 'file.write', + 'file.patch', + 'file.delete', + 'project.patchset', + 'project.restore', + ]); + const starts = agentDb.filter( + (record) => + record.agentId === mainAgentId && + record.runId === state.initialRunId && + mutationTools.has(record.tool) && + [ + 'agent.runtime.tool_action.executing', + 'agent.runtime.tool_confirmation.approved', + ].includes(record.recordType), + ); + assert(starts.length > 0, 'scoped-agents-mutation-action-missing'); + for (const record of starts) { + assert( + ['file.write', 'file.patch', 'project.patchset'].includes(record.tool), + 'scoped-agents-unexpected-mutation-tool-executed', + ); + const execution = { tool: record.tool, inputSummary: record.inputSummary }; + const executionPaths = scopedAgentsExecutionPaths(execution); + assert( + executionPaths.length > 0 && + executionPaths.every((targetPath) => + [scopedAgentsAlphaPath, scopedAgentsBetaPath].includes(targetPath), + ), + 'scoped-agents-mutation-outside-deliveries', + ); + } + return starts; +} + +async function validateScopedAgentsEvidence() { + const persistence = await readScopedAgentsPersistence(); + const { + taskSnapshot, + events, + agentDb, + conversations, + activity, + output, + runtimeState, + contextBundle, + } = persistence; + const latest = taskSnapshot.latest.find( + (task) => task.agentId === mainAgentId && task.runId === state.initialRunId, + ); + assert( + runtimeState?.agentId === mainAgentId && + runtimeState.runId === state.initialRunId && + runtimeState.sessionId === state.initialSessionId && + runtimeState.status === 'idle' && + runtimeState.phase === 'completed' && + latest?.status === 'completed' && + latest.phase === 'completed', + 'scoped-agents-final-runtime-invalid', + ); + const sourcePaths = contextBundle?.repositoryContextSourcePaths ?? []; + const requiredSourcePaths = [ + 'AGENTS.md', + 'game/AGENTS.md', + 'game/alpha/AGENTS.md', + 'game/beta/AGENTS.md', + 'package.json', + ]; + assert( + contextBundle?.schemaVersion === runtimeContextBundleSchemaVersion && + contextBundle.agentId === mainAgentId && + contextBundle.runId === state.initialRunId && + /^[0-9a-f]{64}$/u.test(contextBundle.repositoryContextFingerprint) && + requiredSourcePaths.every((sourcePath) => + sourcePaths.includes(sourcePath), + ), + 'scoped-agents-context-bundle-invalid', + ); + + const [alphaContent, betaContent, changedFiles, hostVerification] = + await Promise.all([ + fs.readFile(path.join(state.projectRoot, scopedAgentsAlphaPath), 'utf8'), + fs.readFile(path.join(state.projectRoot, scopedAgentsBetaPath), 'utf8'), + runProcess('git', ['diff', '--name-only', 'HEAD', '--'], { + cwd: state.projectRoot, + timeoutMs: 30_000, + }), + runProcess(process.execPath, [scopedAgentsVerificationScriptPath], { + cwd: state.projectRoot, + timeoutMs: 120_000, + }), + ]); + const alphaExpected = scopedAgentsExpectedContent('alpha'); + const betaExpected = scopedAgentsExpectedContent('beta'); + assert( + alphaContent === alphaExpected && betaContent === betaExpected, + 'scoped-agents-delivery-content-invalid', + ); + assert( + !alphaContent.includes(scopedAgentsBetaCanary) && + !alphaContent.includes('OWNER=beta') && + !betaContent.includes(scopedAgentsAlphaCanary) && + !betaContent.includes('OWNER=alpha'), + 'scoped-agents-sibling-rule-crossed', + ); + const changedPaths = changedFiles.stdout + .split(/\r?\n/u) + .map((value) => value.trim()) + .filter(Boolean) + .sort(); + assert( + JSON.stringify(changedPaths) === + JSON.stringify([scopedAgentsAlphaPath, scopedAgentsBetaPath].sort()), + 'scoped-agents-changed-path-set-invalid', + ); + assert( + hostVerification.stdout.includes('scoped-agents=passed') && + !hostVerification.stderr.includes('scoped-agents=failed'), + 'scoped-agents-host-verification-failed', + ); + + const alphaExecution = findScopedAgentsMutationExecution( + agentDb, + scopedAgentsAlphaPath, + ); + const betaExecution = findScopedAgentsMutationExecution( + agentDb, + scopedAgentsBetaPath, + ); + assert( + alphaExecution && betaExecution, + 'scoped-agents-target-mutation-evidence-missing', + ); + const mutationStarts = assertScopedAgentsMutationBoundaries(agentDb); + const verificationExecution = requireSuccessfulToolExecution( + agentDb, + 'project.verify', + state.initialRunId, + (execution) => + ['test', 'check:e2e'].includes( + auditInputValue(execution.inputSummary, 'script'), + ) && + auditInputValue(execution.inputSummary, 'expectedCommandSha256') === + hashValue(scopedAgentsVerificationCommand) && + auditInputValue(execution.inputSummary, 'timeoutSeconds') === '120', + 'scoped-agents-project-verification-action-invalid', + ); + const verificationAudit = requireExecutionRecord( + agentDb, + verificationExecution, + (record) => + record.recordType === 'agent.runtime.project.verify' && + record.agentId === mainAgentId && + record.runId === state.initialRunId && + record.actionId === verificationExecution.actionId && + ['test', 'check:e2e'].includes(record.script) && + record.expectedCommand === scopedAgentsVerificationCommand && + record.status === 'completed' && + record.exitCode === 0 && + record.timedOut === false && + hasExpectedWorkspaceSandboxMetadata(record), + 'scoped-agents-project-verification-audit-invalid', + ); + assert( + isNonEmptyString(verificationAudit.logPath), + 'scoped-agents-verification-log-missing', + ); + + const userMessages = conversations.filter( + (message) => message.role === 'user', + ); + const assistantMessages = conversations.filter( + (message) => message.role === 'assistant', + ); + const completedAudits = agentDb.filter( + (record) => + record.recordType === 'agent.runtime.completed' && + record.agentId === mainAgentId && + record.runId === state.initialRunId, + ); + assert( + userMessages.length === 1 && + assistantMessages.length === 1 && + completedAudits.length === 1, + 'scoped-agents-conversation-cardinality-invalid', + ); + const assistantMarkerLeakCount = countExactSecrets( + Buffer.from(assistantMessages[0].content), + state.scopedAgents.privateValues, + ); + assert( + assistantMarkerLeakCount === 0, + 'scoped-agents-final-assistant-instruction-leak', + ); + const duplicateMessageCount = duplicateCount( + conversations.map((message) => message.messageId).filter(Boolean), + ); + const receipts = agentDb.filter( + (record) => + record.recordType === 'agent.runtime.action_receipt' && + record.agentId === mainAgentId && + record.runId === state.initialRunId, + ); + const duplicateReceiptCount = duplicateCount( + receipts.map(receiptAuditIdentity), + ); + assert( + duplicateMessageCount === 0 && duplicateReceiptCount === 0, + 'scoped-agents-duplicate-persistence-identity', + ); + const finalizationFiles = ( + await listFiles( + path.join(state.projectRoot, '.agent/runtime/finalizations'), + ) + ).filter((file) => file.endsWith('.json')); + assert( + finalizationFiles.length === 0, + 'scoped-agents-finalization-journal-present', + ); + + const providerLifecycle = validateScopedAgentsProviderLifecycle(agentDb); + const toolPlanProtocolCount = validateMainRunToolPlanProtocols(agentDb); + const publicSurfaces = { + task: taskSnapshot.all, + event: events, + agentDb, + activity, + output, + runtimeState, + }; + const instructionAuditSurfaces = { + task: taskSnapshot.all, + event: events, + agentDb, + activity, + output, + }; + const instructionPublicCounts = countSensitiveValuesBySurface( + instructionAuditSurfaces, + state.scopedAgents.privateValues, + 'scoped-agents-instruction-body-public', + ); + const apiKeyPublicCounts = countSensitiveValuesBySurface( + publicSurfaces, + state.secrets, + 'scoped-agents-api-key-public', + ); + const projectPathPublicCounts = countSensitiveValuesBySurface( + publicSurfaces, + disposableProjectPathVariants(), + 'scoped-agents-project-path-public', + ); + state.lureLeakCount = await countLureLeaks(); + assert(state.lureLeakCount === 0, 'sensitive-lure-leak-detected'); + const projectSecretLeakCount = await countSecretsInProject( + state.projectRoot, + state.secrets, + ); + const secretLeakCount = + (state.transcriptScanner?.count ?? 0) + projectSecretLeakCount; + assert(secretLeakCount === 0, 'loaded-key-leak-detected'); + + return { + scenario: 'root-parent-and-sibling-scoped-agents-behavior', + targetAgentId: mainAgentId, + providerModel: state.scopedAgents.effectiveModel, + providerApiKind: state.scopedAgents.effectiveApiKind, + isolatedAppDataUsed: true, + formalConfigCliCallCount: state.isolatedRunner.sourceConfigCliCallCount, + sourceRunnerEndpointUnchanged: false, + sourceConfigReplicaCount: state.isolatedRunner.configLinks.length, + sourceConfigReplicasVerified: false, + initialVerificationFailed: + state.scopedAgents.initialVerificationFailed === true, + taskCount: taskSnapshot.all.length, + eventCount: events.length, + agentDbRecordCount: agentDb.length, + conversationMessageCount: conversations.length, + targetRunCount: new Set( + taskSnapshot.all + .filter((task) => task.agentId === mainAgentId) + .map((task) => task.runId), + ).size, + stableSessionCount: new Set( + taskSnapshot.all + .filter((task) => task.agentId === mainAgentId) + .map((task) => task.sessionId), + ).size, + repositoryContextSourceCount: sourcePaths.length, + requiredScopedInstructionSourceCount: 4, + rootRuleApplied: + alphaContent.startsWith(`ROOT=${scopedAgentsRootCanary}`) && + betaContent.startsWith(`ROOT=${scopedAgentsRootCanary}`), + parentRuleApplied: + alphaContent.includes(`GAME=${scopedAgentsGameCanary}`) && + betaContent.includes(`GAME=${scopedAgentsGameCanary}`), + alphaRuleApplied: alphaContent === alphaExpected, + betaRuleApplied: betaContent === betaExpected, + siblingRuleCrossLeakCount: 0, + changedProjectFileCount: changedPaths.length, + targetMutationCoverageCount: 2, + mutationActionCount: new Set( + mutationStarts.map((record) => record.actionId), + ).size, + confirmedActionCount: state.scopedAgents.confirmedActionCount, + verificationPassed: true, + hostVerificationPassed: true, + successfulToolExecutionCount: receipts.filter( + (record) => record.status === 'ok', + ).length, + toolPlanProtocolCount, + providerRequestIdentityCount: providerLifecycle.requestIdentityCount, + providerLifecycleStartedCount: providerLifecycle.startedCount, + providerLifecycleTerminalCount: providerLifecycle.terminalCount, + toolPlanProviderRequestCount: providerLifecycle.toolPlanCount, + finalReplyProviderRequestCount: providerLifecycle.finalReplyCount, + providerFallbackReplayCount: 0, + finalAssistantCount: assistantMessages.length, + completedAuditCount: completedAudits.length, + duplicateMessageCount, + duplicateReceiptCount, + finalizationJournalCount: finalizationFiles.length, + assistantInstructionLeakCount: assistantMarkerLeakCount, + instructionPublicLeakCount: sumObjectValues(instructionPublicCounts), + apiKeyPublicLeakCount: sumObjectValues(apiKeyPublicCounts), + projectPathPublicLeakCount: sumObjectValues(projectPathPublicCounts), + projectPathPublicSurfaceCount: Object.keys(projectPathPublicCounts).length, + scopedAgentsReportLeakCount: state.scopedAgents.reportLeakCount, + scopedAgentsRunnerKillMethod: null, + scopedAgentsRunnerPidfdClaimCount: state.isolatedRunner.pidfdClaimCount, + scopedAgentsRunnerPidfdSignalCount: state.isolatedRunner.pidfdSignalCount, + scopedAgentsRunnerStopped: false, + scopedAgentsAppDataCleanupPerformed: false, + secretLeakCount, + lureLeakCount: state.lureLeakCount, + paths: [ + '.agent/runtime/context-bundles', + '.agent/runtime/tasks', + '.agent/runtime/events', + '.agent/agent.db', + '.agent/conversations', + ], + }; +} + +async function collectPartialScopedAgentsEvidence() { + const persistence = await readScopedAgentsPersistence(); + const [alphaContent, betaContent] = await Promise.all([ + fs + .readFile(path.join(state.projectRoot, scopedAgentsAlphaPath), 'utf8') + .catch(() => ''), + fs + .readFile(path.join(state.projectRoot, scopedAgentsBetaPath), 'utf8') + .catch(() => ''), + ]); + return { + providerModel: state.scopedAgents.effectiveModel, + providerApiKind: state.scopedAgents.effectiveApiKind, + initialVerificationFailed: + state.scopedAgents.initialVerificationFailed === true, + taskCount: persistence.taskSnapshot.all.length, + eventCount: persistence.events.length, + agentDbRecordCount: persistence.agentDb.length, + conversationMessageCount: persistence.conversations.length, + rootRuleApplied: + alphaContent.startsWith(`ROOT=${scopedAgentsRootCanary}`) && + betaContent.startsWith(`ROOT=${scopedAgentsRootCanary}`), + parentRuleApplied: + alphaContent.includes(`GAME=${scopedAgentsGameCanary}`) && + betaContent.includes(`GAME=${scopedAgentsGameCanary}`), + alphaRuleApplied: alphaContent === scopedAgentsExpectedContent('alpha'), + betaRuleApplied: betaContent === scopedAgentsExpectedContent('beta'), + finalAssistantCount: persistence.conversations.filter( + (message) => message.role === 'assistant', + ).length, + }; +} + async function runProcessSessionE2e() { await seedProcessSessionDisposableProject(); state.cliBinary = await prepareCliBinary(); @@ -2883,6 +3794,7 @@ function parseArguments(args) { suite === contextCompactionSuite || suite === mcpRuntimeSuite || suite === userInputRuntimeSuite || + suite === scopedAgentsSuite || processSessionSuites.has(suite), 'unsupported-suite', ); @@ -3052,6 +3964,14 @@ function isolatedSuiteAppDataProfile() { codePrefix: 'user-input-appdata', }; } + if (isScopedAgentsSuite()) { + return { + prefix: '.agent-runtime-real-e2e-scoped-agents-', + sentinelName: scopedAgentsAppDataSentinelFileName, + sentinelSchema: scopedAgentsAppDataSentinelSchema, + codePrefix: 'scoped-agents-appdata', + }; + } assert( isResponseStreamSuite(), 'isolated-appdata-used-outside-isolated-suite', @@ -3275,7 +4195,9 @@ async function prepareIsolatedSuiteAppData({ : source.name; const linkedPath = path.join(appDataDir, linkedName); const storageMode = - isWebSearchSuite() || isMcpRuntimeSuite() ? 'private-copy' : 'hardlink'; + isWebSearchSuite() || isMcpRuntimeSuite() || isScopedAgentsSuite() + ? 'private-copy' + : 'hardlink'; try { if (storageMode === 'private-copy') { await fs.copyFile( @@ -3454,6 +4376,25 @@ async function prepareIsolatedSuiteAppData({ 'user-input-effective-gpt-5-5-config-invalid', ); } + if (isScopedAgentsSuite()) { + const isolatedConfig = await loadConfig(appDataDir); + const isolatedEffective = effectiveAgentLlmConfig( + isolatedConfig.config, + mainAgentId, + ); + assert( + isolatedEffective.model === 'gpt-5.5' && + isolatedEffective.apiKind === 'openai_chat' && + ['apiKey', 'baseUrl', 'model'].every( + (key) => + typeof isolatedEffective[key] === 'string' && + isolatedEffective[key].trim().length > 0, + ), + 'scoped-agents-effective-openai-chat-gpt-5-5-config-invalid', + ); + state.scopedAgents.effectiveModel = isolatedEffective.model; + state.scopedAgents.effectiveApiKind = isolatedEffective.apiKind; + } } async function readIsolatedAppDataSentinel() { @@ -8398,6 +9339,9 @@ async function confirmPendingActions( 'file.delete', ] : []), + ...(isScopedAgentsSuite() + ? ['project.checkpoint', 'file.write', 'file.patch'] + : []), 'project.verify', 'preview.start', 'preview.validate', @@ -12642,7 +13586,8 @@ function buildSummary() { ...(isWebSearchSuite() || isContextCompactionSuite() || isMcpRuntimeSuite() || - isUserInputRuntimeSuite() + isUserInputRuntimeSuite() || + isScopedAgentsSuite() ? { formalConfigPathTranscriptLeakCount: state.formalConfigPathTranscriptLeakCount, @@ -13146,6 +14091,67 @@ function emptyUserInputEvidence() { }; } +function emptyScopedAgentsEvidence() { + return { + scenario: 'root-parent-and-sibling-scoped-agents-behavior', + targetAgentId: mainAgentId, + providerModel: null, + providerApiKind: null, + isolatedAppDataUsed: false, + formalConfigCliCallCount: 0, + sourceRunnerEndpointUnchanged: false, + sourceConfigReplicaCount: 0, + sourceConfigReplicasVerified: false, + initialVerificationFailed: false, + taskCount: 0, + eventCount: 0, + agentDbRecordCount: 0, + conversationMessageCount: 0, + targetRunCount: 0, + stableSessionCount: 0, + repositoryContextSourceCount: 0, + requiredScopedInstructionSourceCount: 4, + rootRuleApplied: false, + parentRuleApplied: false, + alphaRuleApplied: false, + betaRuleApplied: false, + siblingRuleCrossLeakCount: 0, + changedProjectFileCount: 0, + targetMutationCoverageCount: 0, + mutationActionCount: 0, + confirmedActionCount: 0, + verificationPassed: false, + hostVerificationPassed: false, + successfulToolExecutionCount: 0, + toolPlanProtocolCount: 0, + providerRequestIdentityCount: 0, + providerLifecycleStartedCount: 0, + providerLifecycleTerminalCount: 0, + toolPlanProviderRequestCount: 0, + finalReplyProviderRequestCount: 0, + providerFallbackReplayCount: 0, + finalAssistantCount: 0, + completedAuditCount: 0, + duplicateMessageCount: 0, + duplicateReceiptCount: 0, + finalizationJournalCount: 0, + assistantInstructionLeakCount: 0, + instructionPublicLeakCount: 0, + apiKeyPublicLeakCount: 0, + projectPathPublicLeakCount: 0, + projectPathPublicSurfaceCount: 0, + scopedAgentsReportLeakCount: 0, + scopedAgentsRunnerKillMethod: null, + scopedAgentsRunnerPidfdClaimCount: 0, + scopedAgentsRunnerPidfdSignalCount: 0, + scopedAgentsRunnerStopped: false, + scopedAgentsAppDataCleanupPerformed: false, + secretLeakCount: 0, + lureLeakCount: 0, + paths: [], + }; +} + function emptyGoalEvidence() { return { scenario: 'goal-edit-pause-runner-restart-resume', @@ -13951,6 +14957,10 @@ function isUserInputRuntimeSuite() { return state.suite === userInputRuntimeSuite; } +function isScopedAgentsSuite() { + return state.suite === scopedAgentsSuite; +} + function isIsolatedRunnerSuite() { return ( isGoalRuntimeSuite() || @@ -13958,7 +14968,8 @@ function isIsolatedRunnerSuite() { isWebSearchSuite() || isContextCompactionSuite() || isMcpRuntimeSuite() || - isUserInputRuntimeSuite() + isUserInputRuntimeSuite() || + isScopedAgentsSuite() ); } diff --git a/docs/project-memory/shared-memory/decision-log.md b/docs/project-memory/shared-memory/decision-log.md index 5ce2c7ac5..286673a90 100644 --- a/docs/project-memory/shared-memory/decision-log.md +++ b/docs/project-memory/shared-memory/decision-log.md @@ -4653,7 +4653,8 @@ - 决策:`AGENTS.md` 不再与 README/CONTEXT 一样标成纯数据。`repository-startup-context-v2` 为每份文档持久派生规范 scope;Agent 对目标路径只叠加祖先链规则,根到叶优先级递增,兄弟目录规则不适用。 - 系统边界:项目指令只约束代码风格、工作流、测试和交付,不能改变 Agent/Goal/Session/run,不能授予工具、网络、MCP、文件或命令权限,也不能替用户确认、放宽沙箱/隐私/verification/finalization 或授权副作用重放。README 与根 CONTEXT 继续使用不可信参考边界。 - 恢复:v2 fingerprint 纳入 schema、path、kind、scope 与清洗后正文;v1 pending fingerprint 在任何受仓库上下文保护的动作前都会形成 `repositoryContextDrift=true / blocked` observation,旧动作零执行并在同一 run 重规划。 -- 验收:16 条 repository context 测试覆盖根/父/叶/兄弟 scope、顺序、预算、来源清单、清洗和 fingerprint;Provider 捕获请求证明 v2 schema、scope、指令/参考边界与正文真实进入 planning;5 类写工具 drift 回归和旧 v1 pending 回归均证明零副作用。真实双兄弟目录 Provider 行为验收仍待执行,当前不宣称 V1.24 整体 PASS。 +- 确定性验收:16 条 repository context 测试覆盖根/父/叶/兄弟 scope、顺序、预算、来源清单、清洗和 fingerprint;Provider 捕获请求证明 v2 schema、scope、指令/参考边界与正文真实进入 planning;5 类写工具 drift 回归和旧 v1 pending 回归均证明零副作用。 +- 真实验收:正式 `openai_chat / gpt-5.5` 的 `scoped-agents` suite PASS。一次性项目只在根、`game` 父级及 `alpha / beta` 兄弟 scope 提供随机规则,任务不含规则正文、期望内容和工具配方,验收脚本只持有期望正文 SHA-256。最终脚本复跑中,Agent 以 2 个项目变更动作只修改两个目标文件,根/父/各自叶规则全部命中且兄弟串用为 0,真实 `project.verify` 与宿主复验均通过;8 组 Provider lifecycle 唯一闭合,最终 assistant/completed 各 1,重复 message/receipt、遗留 finalization,以及最终回复/公共审计/报告中的规则正文、API Key、诱饵、项目/配置路径泄漏均为 0,隔离 Runner/AppData/项目完整清理。V1.24 整体 PASS。 ## 2026-07-16 AI 游戏创作 Agent Runtime V1.18 真实 Goal Provider 验收收口 diff --git a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md index 89adbb000..d411d36f5 100644 --- a/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md +++ b/docs/technical/【技术方案】AI游戏创作Agent Runtime V1.1-2026-07-12.md @@ -971,7 +971,9 @@ V1.24 修正仓库启动上下文把 `AGENTS.md` 与 README/CONTEXT 一律描述 - Provider prompt 必须显式区分 `SCOPED REPOSITORY INSTRUCTIONS` 与 `UNTRUSTED REPOSITORY REFERENCE`,列出每份文档的 scope、SHA-256 和截断状态,并在正文边界重复 path/scope。已截断的适用指令不能被模型当作完整规则;后续需要修改该 scope 时应先通过现有 `file.read` 获取足够上下文。 - 项目指令不能修改 Agent/Goal/Session/run 身份,不能授予工具、网络、MCP、文件或命令权限,不能替用户批准确认动作,也不能放宽沙箱、隐私、verification/finalization 或副作用重放门禁。正文继续执行凭据和绝对路径清洗;符号链接、敏感目录和 `.agent/**` 不进入启动上下文。 - v2 fingerprint 覆盖规范 path、kind、scope、清洗后正文哈希和既有仓库结构。旧 pending action 绑定的 v1 fingerprint 在第一次恢复或执行前会按现有 repository context drift 路径转成 `blocked` observation 并在同一 run 重规划,不能按旧规则直接写项目。 -- 确定性验收覆盖根/父/叶/兄弟 scope、根到叶顺序、非指令参考文档标签、prompt 真实请求载荷、正文预算与截断、密钥/绝对路径清洗、scope/content 变化导致 fingerprint 漂移,以及旧 pending 动作零执行。真实 Provider 后续必须用没有工具配方的一次性嵌套项目证明:模型对两个兄弟目录分别遵循正确规则、没有串用兄弟规则,并在修改后完成真实验证;未完成该门禁前只记录确定性能力,不宣称 V1.24 整体 PASS。 +- 确定性验收覆盖根/父/叶/兄弟 scope、根到叶顺序、非指令参考文档标签、prompt 真实请求载荷、正文预算与截断、密钥/绝对路径清洗、scope/content 变化导致 fingerprint 漂移,以及旧 pending 动作零执行。真实 Provider 使用现有 `agent-runtime-real-e2e.mjs` 的 `scoped-agents` suite:一次性项目放置根、`game` 父级及 `alpha / beta` 两个兄弟 scope,任务不包含随机规则正文、期望内容或工具配方;项目验收器只保存两份期望交付内容的 SHA-256,模型不能从验收脚本反推规则。 + +2026-07-16 正式 `openai_chat / gpt-5.5` 的 `scoped-agents` suite **PASS**。原始 fixture 先真实失败;最终脚本复跑中,同一 `code-prototype` Agent 以 2 个项目变更动作只修改两个目标文件,根规则、`game` 父规则和 `alpha / beta` 叶规则全部精确命中,兄弟规则串用为 0,Agent 的 `project.verify` 与宿主独立复验均通过。8 组 tool-plan Provider lifecycle 全部唯一 `started -> completed`,最终 assistant 和 completed audit 各 1,重复 message/receipt、遗留 finalization,以及最终回复/公共审计/报告中的内部规则正文、API Key、诱饵、项目/正式配置绝对路径泄漏均为 0;正式配置 CLI 调用为 0,源 Runner endpoint 和配置副本保持不变,隔离 Runner、AppData 与 disposable 项目已按 sentinel 清理。V1.24 真实行为门禁至此完成。 ## 验收命令 @@ -995,6 +997,7 @@ V1.24 修正仓库启动上下文把 `AGENTS.md` 与 README/CONTEXT 一律描述 - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite context-compaction` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite mcp-runtime` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite user-input-runtime` +- `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite scoped-agents` - `npm run ai-game-creator-shell:agent-runtime:real-e2e -- --config-dir --suite full` - `npm run check:encoding` - `git diff --check` diff --git a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md index 73040c649..aae9828eb 100644 --- a/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md +++ b/docs/technical/【技术方案】AI游戏创作智能体App实施计划-2026-06-24.md @@ -568,5 +568,6 @@ game-project/ - 2026-07-15 V1.22 已落地并完成真实验收:开发配置窗可管理 server、敏感凭据、工具过滤和审批并通过 Runner 查看有界目录,`agc:chat` / `agc:swarm` 可用 `/mcp` 查询状态。正式 `openai_chat / gpt-5.5` 路由真实调用 STDIO/Streamable HTTP lookup 和确认后的 mutate,正常 run 的 action/sidecar/receipt 各 3 且最终 assistant 唯一;第二 run 在 HTTP mutate 副作用后强杀 Runner,只进入 1 次 reconciliation,调用、sidecar、receipt 和 assistant 均未重放。公共 arguments、结果正文、instructions、凭据和项目/配置路径泄漏为 0,一次性现场已清理。 - 2026-07-16 起,同一 Runtime 文档的“V1.23 单 Agent 持久用户输入请求”作为 Needs input 事实源。Agent 可在计划未完成时通过 `user.input_request` 提出 1-3 个结构化问题,Runtime 保持同一 run 并暂停;Project Supervisor、开发 Agent 窗口和 `agc:chat` 从私有 sidecar 展示并提交答案。普通 steer、工具确认和最终回复不再承担问题回答语义,问题/答案正文不进入公共审计。 - 2026-07-16 V1.23 已完成真实验收:正式 `openai_chat / gpt-5.5` 路由在 Project Supervisor 上产生 1 个含 2 选项的 Needs input,等待期 Runner pidfd 强杀恢复未增加 Provider 请求,回答后同 Session/run 完成唯一最终回复。问题/回答各一条,重复消息、公共正文、密钥、路径和报告泄漏均为 0,隔离现场已清理。 -- 2026-07-16 起,同一 Runtime 文档的“V1.24 Codex 式 scoped `AGENTS.md` 仓库指令”作为项目规范加载事实源。仓库启动上下文升级为 v2,根与嵌套 `AGENTS.md` 携带规范 scope 并按根到叶适用,更深规则只覆盖自身目录树,兄弟 scope 不串用;README/CONTEXT 明确保持不可信参考数据。项目指令不能扩大工具、确认、沙箱、隐私或完成门禁,旧 v1 pending fingerprint 必须先形成 repository drift blocker 再重规划。当前确定性与 Provider 请求载荷回归已通过,真实双兄弟目录 Provider 门禁仍待完成,不把 prompt 可见性等同于模型遵循能力。 +- 2026-07-16 起,同一 Runtime 文档的“V1.24 Codex 式 scoped `AGENTS.md` 仓库指令”作为项目规范加载事实源。仓库启动上下文升级为 v2,根与嵌套 `AGENTS.md` 携带规范 scope 并按根到叶适用,更深规则只覆盖自身目录树,兄弟 scope 不串用;README/CONTEXT 明确保持不可信参考数据。项目指令不能扩大工具、确认、沙箱、隐私或完成门禁,旧 v1 pending fingerprint 必须先形成 repository drift blocker 再重规划。 +- 2026-07-16 V1.24 已完成真实验收:正式 `openai_chat / gpt-5.5` 的 `scoped-agents` suite 在无规则正文、期望内容和工具配方的任务下,让同一 Agent 只修改 `alpha / beta` 两个兄弟目录交付文件;根、父、各自叶规则全部精确命中且兄弟串用为 0,Agent `project.verify` 与宿主复验均通过。最终脚本复跑的 8 组 Provider lifecycle 唯一闭合,最终 assistant/completed 各 1,重复持久化,以及最终回复/公共审计/报告中的规则正文、API Key、诱饵、项目/配置路径泄漏均为 0,隔离现场完整清理;不再把 prompt 可见性代替模型遵循证据。 - 开发模式可通过本地项目文件面板执行 `file.list/read/write/delete`,普通用户界面不暴露文件面板。