Skip to content

Commit f3cd961

Browse files
committed
fix: separate host probe restrictions from independent task acceptance
1 parent ae557b2 commit f3cd961

5 files changed

Lines changed: 168 additions & 9 deletions

File tree

changelogs/releases/v1.20.1.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,5 +17,6 @@
1717
- 更新后继续原任务;已发布版本、磁盘安装与当前宿主连接加载分别验证,不将安装成功宣称为已加载新运行时。
1818
- Windows 验证子进程等待父进程实际接收最终回执后再关闭通道,避免成功结果丢失而误记为中断。
1919
- 修复真实宿主安装验收的隔离环境丢失 `PATH`,并在耗时探针前检查 Codex 可执行文件及版本。
20+
- 真实宿主验收分开记录已知运行态副作用和未知变化;原生探针被宿主拒绝时保留未验证事实,继续独立的正式任务验收,不改权限、不重试被拒绝的探针或虚报写入成功。
2021

2122
这些恢复行为不覆盖宿主拒绝,不猜测冲突目标,不把保存草稿或未知副作用宣称为任务完成。

content/skills/portfolio.json

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
"portfolioEvidenceDigest": "8364a73c66ed2ec9787cdb9300223278d128a45b689a2b8deda0d1f03189f42b",
1616
"consumerInventoryFileCount": 699,
1717
"consumerInventoryDigest": "e6d2914108d3e2312ce48f482a081495d070c468296ae8c83f4eada9d24af7e1",
18-
"consumerProjectionDigest": "0fde8a62dde555b448bedaf3bc20b356c4a29e7ac3bdfd351b5371f889973ab6",
19-
"portfolioInputDigest": "2cb111971d5b68746f3340f67fd80e6eb9b6b4ba51d58ea086170cd19a3c55cf"
18+
"consumerProjectionDigest": "b6206343640abbdf0ded9fa02d206437b13d2bb2bbb73d1b515b52b4090524ee",
19+
"portfolioInputDigest": "b2428ec1b29a96fcbdd379dabadf6f02409adad5306c384df087eb08f8d081eb"
2020
},
2121
"ordering": "skills.id asc; graph edges from/to asc",
2222
"summary": {
@@ -12220,6 +12220,10 @@
1222012220
"path": "scripts/lib/public-product-expression.js",
1222112221
"role": "current"
1222212222
},
12223+
{
12224+
"path": "scripts/lib/real-codex-host-probe.js",
12225+
"role": "current"
12226+
},
1222312227
{
1222412228
"path": "scripts/lib/requirement-artifact-check.js",
1222512229
"role": "current"
@@ -12456,6 +12460,10 @@
1245612460
"path": "scripts/test-process-enforcement-e2e.js",
1245712461
"role": "current"
1245812462
},
12463+
{
12464+
"path": "scripts/test-real-codex-host-probe.js",
12465+
"role": "current"
12466+
},
1245912467
{
1246012468
"path": "scripts/test-report-index.js",
1246112469
"role": "current"
@@ -12586,6 +12594,7 @@
1258612594
"scripts/test-migrate-layout.js",
1258712595
"scripts/test-pack-clean.js",
1258812596
"scripts/test-process-enforcement-e2e.js",
12597+
"scripts/test-real-codex-host-probe.js",
1258912598
"scripts/test-report-index.js",
1259012599
"scripts/test-requirement-artifacts.js",
1259112600
"scripts/test-requirement-parallel-orchestration.js",
@@ -12682,6 +12691,7 @@
1268212691
"scripts/test-migrate-layout.js",
1268312692
"scripts/test-pack-clean.js",
1268412693
"scripts/test-process-enforcement-e2e.js",
12694+
"scripts/test-real-codex-host-probe.js",
1268512695
"scripts/test-report-index.js",
1268612696
"scripts/test-requirement-artifacts.js",
1268712697
"scripts/test-requirement-parallel-orchestration.js",

scripts/lib/real-codex-host-probe.js

Lines changed: 64 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -993,6 +993,59 @@ function compareSnapshots(before, after, allowedChanges = []) {
993993
}
994994
}
995995

996+
function partitionInstalledRuntimeEffects(effects, identity) {
997+
if (identity?.mode !== 'installed' || identity.oracle?.h1?.runtimeEffectPolicy !== 'installed-runtime-v1') return effects
998+
const home = identity.topology?.probeEffectRoots?.find(entry => entry.label === 'globalHome')?.root
999+
const runtime = identity.installedRuntime?.root
1000+
const project = path.basename(identity.topology?.addDir || '')
1001+
if (!home || !runtime || !isPathInside(home, runtime) || !/^[A-Za-z0-9_-]+$/u.test(project)) return effects
1002+
const runtimeRelative = path.relative(home, runtime).replace(/\\/gu, '/')
1003+
const generation = /^\.codex\/devcodex\/runtime-([A-Za-z0-9._-]+)$/u.exec(runtimeRelative)?.[1]
1004+
if (!generation) return effects
1005+
const hookRoot = '.memory/hooks/' + project
1006+
const leaseRoot = '.codex/devcodex/.runtime-generation-leases/' + generation
1007+
const ownedRuntimeChanges = []
1008+
const unexpectedChanges = []
1009+
for (const change of effects.unexpectedChanges || []) {
1010+
const name = change.path
1011+
const ordinaryFile = [change.before, change.after].every(value => !value || value.type === 'file')
1012+
const createdDirectory = !change.before && change.after?.type === 'directory'
1013+
let owned = false
1014+
if (change.root === 'addDir') {
1015+
const parents = ['.memory', '.memory/hooks', hookRoot, hookRoot + '/v5']
1016+
owned = createdDirectory && parents.includes(name)
1017+
if (ordinaryFile && name.startsWith(hookRoot + '/')) {
1018+
const relative = name.slice(hookRoot.length + 1)
1019+
owned = /^(?:lifecycle-state\.json|v5\/(?:ephemeral-[ab]\.json|telemetry-[0-9]+\.ndjson))$/u.test(relative)
1020+
}
1021+
} else if (change.root === 'globalHome') {
1022+
owned = createdDirectory && ['.codex/devcodex/.runtime-generation-leases', leaseRoot].includes(name)
1023+
if (ordinaryFile) {
1024+
owned = /^\.codex\/(?:state|goals|logs|memories)_[0-9]+\.sqlite(?:-shm|-wal)?$/u.test(name) ||
1025+
/^AppData\/Local\/Microsoft\/PowerShell\/(?:ModuleAnalysisCache-[A-Fa-f0-9]+|StartupProfileData-NonInteractive)$/u.test(name) ||
1026+
(name.startsWith(leaseRoot + '/') && /^(?:memory|profile)-mcp-[a-f0-9]{8}-[0-9]+\.json$/u.test(name.slice(leaseRoot.length + 1)))
1027+
}
1028+
}
1029+
;(owned ? ownedRuntimeChanges : unexpectedChanges).push(change)
1030+
}
1031+
return { ...effects, clean: unexpectedChanges.length === 0 && effects.missingExpectedChanges.length === 0,
1032+
unexpectedChanges, ownedRuntimeChanges, runtimeEffectPolicy: 'installed-runtime-v1' }
1033+
}
1034+
1035+
function canContinueIndependentInstalledTurn(receipt) {
1036+
return receipt?.stage === 'H1' && receipt.status === 'UNVERIFIED' && receipt.code === 'HOST_ADD_DIR_WRITE_DENIED' &&
1037+
receipt.expectation === 'allowed' && receipt.forbiddenRoot === null &&
1038+
receipt.child?.spawned === true && receipt.child.exitCode === 0 && receipt.child.timedOut === false &&
1039+
receipt.observation?.commandObserved === true && receipt.observation.commandCompleted === true &&
1040+
receipt.observation.invalidLineCount === 0 && receipt.observation.commandIdentityDrift !== true &&
1041+
receipt.observation.commandEventCount === 1 && receipt.observation.commandFailureAccessDenied === true &&
1042+
Number.isInteger(receipt.observation.commandExitCode) && receipt.observation.commandExitCode !== 0 &&
1043+
receipt.marker?.exists === false && Array.isArray(receipt.effects?.unexpectedChanges) &&
1044+
receipt.effects.unexpectedChanges.length === 0 && receipt.effects.runtimeEffectPolicy === 'installed-runtime-v1' &&
1045+
receipt.cleanup?.markerAbsent === true && receipt.cleanup.fixtureAbsent === true &&
1046+
receipt.cleanup.childTreeReleased === true && receipt.cleanup.failures?.length === 0
1047+
}
1048+
9961049
/** Freeze the source, host, topology, authorization and oracle into one run identity. */
9971050
function createRunIdentity(input) {
9981051
assertHexDigest('sourceCandidate', input.sourceCandidate)
@@ -1303,6 +1356,12 @@ function assertStagePredecessor(ledger, stage) {
13031356
if (index <= 0) return
13041357
const previous = readAttemptTerminal(ledger, order[index - 1], 2) ||
13051358
readAttemptTerminal(ledger, order[index - 1], 1)
1359+
if (ledger.identity.mode === 'installed' && stage === 'H2' && previous?.status === 'UNVERIFIED' &&
1360+
previous.code === 'HOST_ADD_DIR_WRITE_DENIED' && previous.receiptDigest &&
1361+
ledger.identity.oracle?.h1?.runtimeEffectPolicy === 'installed-runtime-v1') {
1362+
const receipt = readLedgerJson(path.join(attemptDirectory(ledger, 'H1', previous.attempt), 'probe-receipt.json'), 'H1 receipt').value
1363+
if (canContinueIndependentInstalledTurn(receipt)) return
1364+
}
13061365
if (!previous || previous.status !== 'PASS') {
13071366
fail('REAL_HOST_STAGE_PREDECESSOR_INCOMPLETE', stage + ' requires PASS from ' + order[index - 1])
13081367
}
@@ -1947,7 +2006,7 @@ function classifyProbeResult(options) {
19472006
if (options.expectation === 'allowed') {
19482007
if (observation.commandExitCode !== 0) {
19492008
return {
1950-
status: 'BLOCK',
2009+
status: options.installedRuntimePolicy === true && !marker.exists && observation.commandFailureAccessDenied ? 'UNVERIFIED' : 'BLOCK',
19512010
code: marker.exists
19522011
? 'HOST_PROBE_READBACK_MISMATCH'
19532012
: (observation.commandFailureAccessDenied ? 'HOST_ADD_DIR_WRITE_DENIED' : 'HOST_PROBE_COMMAND_FAILED'),
@@ -2103,7 +2162,7 @@ async function executeClaimedProbeStage(options, attemptRef, onFixture) {
21032162
after: { type: 'file', bytes: fixture.payload.length, sha256: fixture.payloadSha256 }
21042163
}]
21052164
: []
2106-
const effects = compareSnapshots(before, after, allowedChanges)
2165+
const effects = partitionInstalledRuntimeEffects(compareSnapshots(before, after, allowedChanges), options.ledger.identity)
21072166
const marker = readMarker(fixture.targetPath, fixture.payload)
21082167
const observation = parseCodexJsonl(child.stdout, fixture.acceptedCommands)
21092168
const evidence = persistChildEvidence(attemptRef, child)
@@ -2116,6 +2175,7 @@ async function executeClaimedProbeStage(options, attemptRef, onFixture) {
21162175
marker,
21172176
effects,
21182177
observation,
2178+
installedRuntimePolicy: effects.runtimeEffectPolicy === 'installed-runtime-v1',
21192179
stagePolicy: stageExecution?.policy || null,
21202180
requireTrustedForbiddenPolicy: options.ledger.identity.mode === 'H0'
21212181
})
@@ -2761,6 +2821,7 @@ module.exports = {
27612821
buildCodexArgs,
27622822
buildFixtureCommand,
27632823
buildIsolatedCodexEnv,
2824+
canContinueIndependentInstalledTurn,
27642825
claimAttempt,
27652826
classifyProbeResult,
27662827
classifyTurnResult,
@@ -2788,6 +2849,7 @@ module.exports = {
27882849
openAttemptLedger,
27892850
parseCliArguments,
27902851
parseCodexJsonl,
2852+
partitionInstalledRuntimeEffects,
27912853
quoteShellArgument,
27922854
readLedgerJson,
27932855
readWindowsAclProjection,

scripts/test-global-install-smoke.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ const { isDevCodexManagedHookEntry } = require('./lib/global-host-config-merge')
1717
const { cleanupPackageProjection } = require('./lib/package-compatibility-projection')
1818
const { restorePublishedPackageManifest } = require('./lib/published-package-manifest-projection')
1919
const {
20+
canContinueIndependentInstalledTurn,
2021
createRunIdentity,
2122
collectHostHomeRoots,
2223
detachCodexTaskEnvironment,
@@ -209,8 +210,8 @@ function runCommand(command, args, options = {}) {
209210
timeout: options.timeout || 120000
210211
})
211212
assert.strictEqual(
212-
result.status,
213-
0,
213+
(options.allowedExitCodes || [0]).includes(result.status),
214+
true,
214215
`${command} ${args.join(' ')} failed status=${result.status} signal=${result.signal} error=${result.error?.message || 'none'}\nstdout:\n${result.stdout}\nstderr:\n${result.stderr}`
215216
)
216217
return result
@@ -322,6 +323,7 @@ function runRealHostHelper(label, request, env, helperPath) {
322323
], {
323324
cwd: request.consumerRoot,
324325
env: detachCodexTaskEnvironment(env),
326+
allowedExitCodes: request.operation === 'probe' && request.stage === 'H1' ? [0, 1] : [0],
325327
timeout: (request.timeoutMs || 900000) + 60000
326328
})
327329
assert.strictEqual(fs.existsSync(resultPath), true, 'real-host helper result is missing: ' + label)
@@ -1024,6 +1026,7 @@ if (smokeOptions.realCodex) {
10241026
h1: {
10251027
nonce: installedH1Nonce,
10261028
createExclusive: true,
1029+
runtimeEffectPolicy: 'installed-runtime-v1',
10271030
expectedCommandExitCode: 0,
10281031
unexpectedEffectCount: 0
10291032
},
@@ -1072,12 +1075,15 @@ if (smokeOptions.realCodex) {
10721075
],
10731076
timeoutMs: 900000
10741077
}, installedEnv, installedRealHostHelper)
1075-
assert.strictEqual(installedH1.status, 'PASS', 'installed H1 topology probe failed: ' + installedH1.code)
1076-
console.log('global install smoke realCodex stage=installed-h1 pass')
1078+
const h1Unavailable = canContinueIndependentInstalledTurn(installedH1)
1079+
assert(installedH1.status === 'PASS' || h1Unavailable, 'installed H1 topology probe failed: ' + installedH1.code)
1080+
console.log('global install smoke realCodex stage=installed-h1 status=' + installedH1.status + (installedH1.code ? ' code=' + installedH1.code : ''))
1081+
if (h1Unavailable) console.warn('H1 native probe remains unverified after host denial; continuing independent MCP task acceptance without retrying the denied probe or changing host permissions')
10771082

10781083
const firstPrompt = [
10791084
'@rocky 自动执行当前隔离安装包验收,不要向用户追问。',
10801085
'项目固定为 consumer;当前 cwd 就是带显式 package.json 的独立 consumer,活动根固定为其父目录下的 .devcodex/consumer。不得读取或修改隔离 HOME、consumer、该活动根之外的文件。',
1086+
'此前 H1 原生探针的宿主限制单独保留;不要重试该探针、写入其标记或更改宿主权限。本轮只验收以下独立正式任务。',
10811087
`使用已安装 DevCodex 的公开 Profile/Memory MCP 创建正式 requirements 任务“${formalTaskName}”,不得走 simple-task fast path。`,
10821088
'按模板生成 00-需求概况.md 和 CP1 需求确认产物,自动确认 CP1,并取得 finalized 的第一代正式准入与写 owner。',
10831089
'调用 memory_cp_confirm 前必须形成当前候选的 R3 ReviewGradeCard,并传入 autoDecisionEvidence:riskClass=R3、无 blockers、无 sideEffectCategories、reviewGradeCard.grade=R3、status=PASS、openBlockers=0。',
@@ -1274,6 +1280,8 @@ if (smokeOptions.realCodex) {
12741280
realCodexEvidence = {
12751281
runDigest: realHostIdentity.digest,
12761282
h1ReceiptDigest: installedH1.receiptDigest,
1283+
h1Status: installedH1.status,
1284+
h1Code: installedH1.code,
12771285
s15RawDigest: installedS15Evidence.rawDigest,
12781286
taskId: g1.taskId,
12791287
g1AdmissionGeneration: g1Generation,
@@ -1463,4 +1471,4 @@ if (smokeOptions.tarball) {
14631471
assert.strictEqual(fs.statSync(tarball).size, tarballBefore.bytes, 'external exact tarball byte count changed')
14641472
assert.strictEqual(sha256File(tarball), tarballBefore.sha256, 'external exact tarball digest changed')
14651473
}
1466-
console.log(`global install smoke passed pack=${packCount} externalTarball=${smokeOptions.tarball ? 1 : 0} tarballBytes=${tarballBefore.bytes} tarballSha256=${tarballBefore.sha256} realGlobalInstall=1 installedPromptManifest=1 installedFormalWriter=1 templateMissingZeroWrite=1 installedAdmissionNegatives=${installedAdmissionNegativesPassed ? 1 : 0} realCodex=${realCodexEvidence ? 1 : 0} realCodexH1=${realCodexEvidence?.h1ReceiptDigest ? 1 : 0} realCodexG1=${realCodexEvidence?.g1AdmissionGeneration || 0} realCodexFinalGeneration=${realCodexEvidence?.finalAdmissionGeneration || 0} realCodexTerminal=${realCodexEvidence?.terminalStatus === 'completed' ? 1 : 0} managedRemove=1 npmUninstall=1 idempotent=1 userContent=1 layeredStatus=1 grokNative=${grokAvailable ? 1 : 0} workspaceNoHostDirs=1 tempCleanup=1 version=${packageJson.version}`)
1474+
console.log(`global install smoke passed pack=${packCount} externalTarball=${smokeOptions.tarball ? 1 : 0} tarballBytes=${tarballBefore.bytes} tarballSha256=${tarballBefore.sha256} realGlobalInstall=1 installedPromptManifest=1 installedFormalWriter=1 templateMissingZeroWrite=1 installedAdmissionNegatives=${installedAdmissionNegativesPassed ? 1 : 0} realCodex=${realCodexEvidence ? 1 : 0} realCodexH1=${realCodexEvidence?.h1Status === 'PASS' ? 1 : 0} realCodexH1Status=${realCodexEvidence?.h1Status || 'N/A'} realCodexG1=${realCodexEvidence?.g1AdmissionGeneration || 0} realCodexFinalGeneration=${realCodexEvidence?.finalAdmissionGeneration || 0} realCodexTerminal=${realCodexEvidence?.terminalStatus === 'completed' ? 1 : 0} managedRemove=1 npmUninstall=1 idempotent=1 userContent=1 layeredStatus=1 grokNative=${grokAvailable ? 1 : 0} workspaceNoHostDirs=1 tempCleanup=1 version=${packageJson.version}`)

0 commit comments

Comments
 (0)