Skip to content

Commit 60dcdaa

Browse files
committed
fix: canonicalize release control identities
1 parent 0e96cf0 commit 60dcdaa

8 files changed

Lines changed: 67 additions & 26 deletions

File tree

changelogs/releases/v1.19.0.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ v1.19.0 修复真实工程任务在会话、Hook、工具调用和恢复过程
2323

2424
- `MutationFootprintV2 → LayeredArtifactSlotRegistryV2 → ArtifactSlotDecisionV2 → TaskOwnedMutationLeaseV2 → TaskRecoveryStoreV5 prewrite → MutationObservationReceiptV1` 成为正式产物的单一写入决定链。
2525
- Hook、MCP 与宿主工具 mutation 统一进入观察适配层;unknown writer、跨 active-root、错误槽位、重复消费和未观察实际效果均不会被误报为成功。
26+
- `ArtifactSlotDecisionV2` 的根路径与摘要现在共用 canonical filesystem identity,Windows 短路径、大小写或规范路径别名不再把同一真实根误判为跨 active-root/project-root;不同真实根仍失败关闭。
2627
- `01-产品需求.md` 的产品提供型内容保持用户原始真相,AI 不得静默改写。
2728

2829
### TaskRecoveryStoreV5 与 generation 收敛
@@ -46,6 +47,7 @@ v1.19.0 修复真实工程任务在会话、Hook、工具调用和恢复过程
4647

4748
- MCP capability/integrity 证据由 server-owned 响应提供,客户端不再自行构造缺字段 schema;HostIdentityV2 区分 CLI、Desktop 与其他宿主证据,禁止跨宿主继承 PASS。
4849
- 宿主专项测试显式固定 fixture 的 host variant 并清除 ambient markers,避免在 Codex、Claude 或普通 shell 中运行同一测试得到不同红绿;生产 Stop enforcement 不因测试环境而改变。
50+
- Host instruction projection 与 Auto 语义探针绑定同一 canonical source;默认 `safety-only` 提醒放行、`strict` 阻断的当前语义会被正向校验,旧硬阻断句被负向拒绝。
4951
-`README.md``public-site/**/*.md``website/**/*.md` 退出 JavaScript 正文、章节、词句、计数和动态 marker 校验;站点代码、配置、组件、数据与显式 release build 仍按工程范围验证。
5052
- 语法级危险命令分类减少只读搜索和文档文本误拦,真实危险 sink 继续失败关闭。
5153

hooks/_runtime/artifact-slot-decision.cjs

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -219,6 +219,15 @@ function canonicalizeTarget(target, activeRoot, options = {}) {
219219
}
220220
}
221221

222+
function createArtifactRootIdentity(value, options = {}) {
223+
const resolved = path.resolve(String(value || ''))
224+
const canonical = canonicalizeTarget(resolved, resolved, options).activeRoot || resolved
225+
return {
226+
canonicalPath: canonical,
227+
digest: digest(comparable(canonical))
228+
}
229+
}
230+
222231
function normalizeArtifactRoots(value, options = {}) {
223232
if (value && typeof value === 'object' && !Array.isArray(value)) {
224233
return {
@@ -459,14 +468,8 @@ function decideArtifactMutation(input = {}, options = {}) {
459468
const anyFormal = inspected.some(item => item.status === 'logical' || ['unknown-formal', 'ambiguous-slot'].includes(item.classified?.matchType) || item.classified?.slot)
460469
const formalIntent = input.formalIntent === true || anyFormal
461470
const errors = []
462-
const activeRootIdentity = {
463-
canonicalPath: canonicalizeTarget(activeRoot, activeRoot, options).activeRoot || activeRoot,
464-
digest: digest(comparable(activeRoot))
465-
}
466-
const projectRootIdentity = {
467-
canonicalPath: canonicalizeTarget(projectRoot, projectRoot, options).activeRoot || projectRoot,
468-
digest: digest(comparable(projectRoot))
469-
}
471+
const activeRootIdentity = createArtifactRootIdentity(activeRoot, options)
472+
const projectRootIdentity = createArtifactRootIdentity(projectRoot, options)
470473

471474
if (!formalIntent) {
472475
const semantic = {
@@ -726,6 +729,7 @@ module.exports = {
726729
canonicalizeAgainstRoots,
727730
canonicalizeTarget,
728731
classifyRelativeTarget,
732+
createArtifactRootIdentity,
729733
decideArtifactMutation,
730734
enumerateTaskArtifacts,
731735
hasTaskArtifact,

hooks/_runtime/lifecycle.cjs

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,7 @@ const {
9595
} = require('./workflow-completion-contract.cjs')
9696
const {
9797
canonicalArtifactName,
98+
createArtifactRootIdentity,
9899
decideArtifactMutation,
99100
hasTaskArtifact: registryHasTaskArtifact,
100101
readLayeredArtifactSlotRegistry,
@@ -2445,11 +2446,6 @@ function sameResolvedPath(left, right) {
24452446
return process.platform === 'win32' ? a.toLowerCase() === b.toLowerCase() : a === b
24462447
}
24472448

2448-
function artifactRootIdentityDigest(value) {
2449-
const normalized = path.normalize(path.resolve(String(value || '')))
2450-
return stableDigest(process.platform === 'win32' ? normalized.toLowerCase() : normalized)
2451-
}
2452-
24532449
function isSha256(value) {
24542450
return /^[a-f0-9]{64}$/i.test(String(value || ''))
24552451
}
@@ -2582,14 +2578,16 @@ function validateMutationAuthorizationBundle(state, operation, options = {}) {
25822578
}
25832579
const currentActiveRoot = getActiveNamespaceRoot(state)
25842580
const currentProjectRoot = state.stickyProject?.physicalRoot || CONTEXT_ROOT
2581+
const currentActiveRootIdentity = createArtifactRootIdentity(currentActiveRoot)
2582+
const currentProjectRootIdentity = createArtifactRootIdentity(currentProjectRoot)
25852583
if ((decision?.activeRootIdentity?.canonicalPath &&
2586-
!sameResolvedPath(decision.activeRootIdentity.canonicalPath, currentActiveRoot)) ||
2587-
decision?.activeRootIdentity?.digest !== artifactRootIdentityDigest(currentActiveRoot)) {
2584+
!sameResolvedPath(decision.activeRootIdentity.canonicalPath, currentActiveRootIdentity.canonicalPath)) ||
2585+
decision?.activeRootIdentity?.digest !== currentActiveRootIdentity.digest) {
25882586
errors.push('artifact-active-root-drift')
25892587
}
25902588
if ((decision?.projectRootIdentity?.canonicalPath &&
2591-
!sameResolvedPath(decision.projectRootIdentity.canonicalPath, currentProjectRoot)) ||
2592-
decision?.projectRootIdentity?.digest !== artifactRootIdentityDigest(currentProjectRoot)) {
2589+
!sameResolvedPath(decision.projectRootIdentity.canonicalPath, currentProjectRootIdentity.canonicalPath)) ||
2590+
decision?.projectRootIdentity?.digest !== currentProjectRootIdentity.digest) {
25932591
errors.push('artifact-project-root-drift')
25942592
}
25952593
if (decision?.contextEpoch !== state.contextAcquisition?.contextEpoch ||

host-projections/AGENTS.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# DevCodex — Shared Host Kernel(generated)
22

33
> Generated from the unified DevCodex instructions source. Do not edit this file directly.
4-
> sourceDigest: e38178889dd9209c6975b2384061119d9d4b06c15577b49a3c36476e5968b6bc
4+
> sourceDigest: 13baf292a6f512aedc45493cf9205afff02e4463335829cc35b013d8c8d037d9
55
> configDigest: e2e2a4dde233366df758fb5fa1e9ac299a5f1dbd1b223390c4849e7d8ab9df3b
66
> Full fallback: user://agents/devcodex/instructions.full.md
77
> Host surface: Codex / Claude import / Gemini import / Grok native AGENTS.md

host-projections/copilot-instructions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# DevCodex — Copilot Host Kernel(generated)
22

33
> Generated from the unified DevCodex instructions source. Do not edit this file directly.
4-
> sourceDigest: e38178889dd9209c6975b2384061119d9d4b06c15577b49a3c36476e5968b6bc
4+
> sourceDigest: 13baf292a6f512aedc45493cf9205afff02e4463335829cc35b013d8c8d037d9
55
> configDigest: e2e2a4dde233366df758fb5fa1e9ac299a5f1dbd1b223390c4849e7d8ab9df3b
66
> Full fallback: user://agents/devcodex/instructions.full.md
77
> Host surface: GitHub Copilot CLI user-global instructions; Hooks/MCP/Skills are deployed separately

host-projections/coverage.json

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
22
"schemaVersion": "HostInstructionCoverageReceiptV1",
33
"sourceFile": "content/instructions.md",
4-
"sourceDigest": "e38178889dd9209c6975b2384061119d9d4b06c15577b49a3c36476e5968b6bc",
4+
"sourceDigest": "13baf292a6f512aedc45493cf9205afff02e4463335829cc35b013d8c8d037d9",
55
"configDigest": "e2e2a4dde233366df758fb5fa1e9ac299a5f1dbd1b223390c4849e7d8ab9df3b",
66
"mode": "kernel",
77
"fallback": {
88
"destination": "user://agents/devcodex/instructions.full.md",
9-
"sourceDigest": "e38178889dd9209c6975b2384061119d9d4b06c15577b49a3c36476e5968b6bc",
9+
"sourceDigest": "13baf292a6f512aedc45493cf9205afff02e4463335829cc35b013d8c8d037d9",
1010
"available": true
1111
},
1212
"budgets": {
@@ -307,12 +307,12 @@
307307
"host-projections/AGENTS.md": {
308308
"bytes": 11283,
309309
"lines": 91,
310-
"digest": "17aaca66a65c309f875e30f577482e2b8e1820960c6fbfde09a1184ea7f1f732"
310+
"digest": "73f73c120b839b043f7dd9f3ddd6a3065bdd3aeefee26865be7eadd0e8e11295"
311311
},
312312
"host-projections/copilot-instructions.md": {
313313
"bytes": 11308,
314314
"lines": 91,
315-
"digest": "bcd4cfdb3542f9c59bdfc904c67e3c723d4f4593271bc20dd7f523f08db304f2"
315+
"digest": "362fb188012c4866244335a68c375ca346f2ca5364f2305c8951235222fa83d5"
316316
},
317317
"host-projections/CLAUDE.md": {
318318
"bytes": 510,
@@ -330,5 +330,5 @@
330330
"valid": true,
331331
"errors": []
332332
},
333-
"receiptId": "host-instruction-coverage-b46f8a363fea504778b7dbc5a77092add8880311f1ea89c0a103aa2bceac6b3f"
333+
"receiptId": "host-instruction-coverage-49d2ea229fec7cb9d0320ad9a4a36c0d798209f2e45e22bf2f5f32971427622d"
334334
}

scripts/lib/validate-governance-prompts.js

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -346,14 +346,20 @@ function buildGovernancePromptChecks(ctx) {
346346
mustInclude('instructions/01-common.instructions.md', '全局默认 `@rocky`', '01-common default auto alias')
347347
mustInclude('instructions/01-common.instructions.md', '替换全局默认别名', '01-common auto alias replacement')
348348
mustInclude('instructions/01-common.instructions.md', '模糊提及', '01-common auto mode')
349-
mustInclude('instructions/01-common.instructions.md', '非白名单路径默认切回确认模式', '01-common auto mode')
349+
mustInclude('instructions/01-common.instructions.md', '非白名单路径在默认 `safety-only` 下提醒后继续已获正式授权的 mutation', '01-common auto safety-only mode')
350+
mustInclude('instructions/01-common.instructions.md', '在 `strict` 下切回确认并拦截', '01-common auto strict mode')
351+
mustNotInclude('instructions/01-common.instructions.md', '非白名单路径默认切回确认模式', '01-common legacy auto hard-block mode')
350352
mustInclude('skills/cp-gate/SKILL.md', '明确自然语言 auto 授权', 'cp-gate auto mode')
351353
mustInclude('skills/cp-gate/SKILL.md', 'extensions.devcodex.autoAliases', 'cp-gate auto alias')
352354
mustInclude('skills/cp-gate/SKILL.md', '全局默认 `@rocky`', 'cp-gate default auto alias')
353355
mustInclude('skills/cp-gate/SKILL.md', '替换全局默认别名', 'cp-gate auto alias replacement')
354356
mustInclude('skills/cp-gate/SKILL.md', '白名单路径', 'cp-gate auto mode')
357+
mustInclude('skills/cp-gate/SKILL.md', '非白名单路径在默认 `safety-only` 下提醒后继续已获正式授权的 mutation', 'cp-gate auto safety-only mode')
358+
mustInclude('skills/cp-gate/SKILL.md', '在 `strict` 模式下回确认并硬拦截', 'cp-gate auto strict mode')
355359
mustInclude('skills/cp-gate/SKILL.md', 'instruction-fallback', 'cp-gate auto mode')
356360
mustInclude('skills/compliance/SKILL.md', 'hook-enforced', 'compliance auto mode')
361+
mustInclude('skills/compliance/SKILL.md', '非白名单路径在正式 workflow authority 已满足后输出提醒并放行', 'compliance auto safety-only mode')
362+
mustInclude('skills/compliance/SKILL.md', '`strict` 模式下才按白名单执行 runtime 硬拦截', 'compliance auto strict mode')
357363
mustInclude('skills/compliance/SKILL.md', 'instruction-fallback', 'compliance auto mode')
358364
mustInclude('hooks/_runtime/lifecycle.cjs', 'AUTO_ALLOWED_PATH_PATTERNS', 'lifecycle runtime auto mode')
359365
mustInclude('hooks/_runtime/lifecycle.cjs', 'detectExecutionMode', 'lifecycle runtime auto mode')

scripts/test-artifact-mutation-authority.js

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ const {
1515
} = require('../hooks/_runtime/mutation-footprint.cjs')
1616
const {
1717
decideArtifactMutation,
18+
createArtifactRootIdentity,
1819
enumerateTaskArtifacts,
1920
hasTaskArtifact,
2021
isAuthoritativeTaskArtifact,
@@ -65,6 +66,32 @@ function decisionFor(payload, overrides = {}) {
6566
}
6667

6768
try {
69+
const canonicalIdentityRoot = path.join(tempRoot, 'canonical-root-identity')
70+
const aliasIdentityRoot = path.join(tempRoot, 'alias-root-identity')
71+
fs.mkdirSync(canonicalIdentityRoot, { recursive: true })
72+
const comparablePath = value => process.platform === 'win32'
73+
? path.resolve(value).toLowerCase()
74+
: path.resolve(value)
75+
const aliasFs = {
76+
realpathSync(value) {
77+
return comparablePath(value) === comparablePath(aliasIdentityRoot)
78+
? canonicalIdentityRoot
79+
: fs.realpathSync(value)
80+
}
81+
}
82+
assert.deepStrictEqual(
83+
createArtifactRootIdentity(aliasIdentityRoot, { fs: aliasFs }),
84+
createArtifactRootIdentity(canonicalIdentityRoot),
85+
'artifact root identity must bind the canonical filesystem root rather than an alias spelling'
86+
)
87+
const distinctIdentityRoot = path.join(tempRoot, 'distinct-root-identity')
88+
fs.mkdirSync(distinctIdentityRoot, { recursive: true })
89+
assert.notStrictEqual(
90+
createArtifactRootIdentity(distinctIdentityRoot).digest,
91+
createArtifactRootIdentity(canonicalIdentityRoot).digest,
92+
'different canonical filesystem roots must retain different identities'
93+
)
94+
6895
const overview = write('00-问题概况.md')
6996
const cp1 = write('01-问题确认.md')
7097
const cp2 = write('02-修复方案.md')
@@ -637,5 +664,9 @@ try {
637664
assert(fs.existsSync(overview) && fs.existsSync(cp1))
638665
process.stdout.write('test-artifact-mutation-authority: ok\n')
639666
} finally {
640-
fs.rmSync(tempRoot, { recursive: true, force: true })
667+
if (process.env.DEVCODEX_KEEP_TEST_ARTIFACTS === '1') {
668+
process.stdout.write(`[test-artifact-mutation-authority] retained ${tempRoot}\n`)
669+
} else {
670+
fs.rmSync(tempRoot, { recursive: true, force: true })
671+
}
641672
}

0 commit comments

Comments
 (0)