Repository navigation
ZCode key-order normalization still triggers foreign-edit after #2393 #2759
Description
Client or integration
Other
Area
CLI
Summary
After #2393, ZCode still causes the managed provider.opencodex integration to become conflict / foreign-edit after ZCode saves its config.
In this reproduction, the protected provider values remain semantically unchanged, but ZCode reorders JSON object keys such as moving options before enabled / source, and moving per-model limit before modalities. OpenCodex still fingerprints these nested objects through JSON.stringify(fragment.value), so key-order-only rewrites change the protected fingerprint.
I expected JSON object key order to be ignored for ownership classification while real changes to protected fields such as options.baseURL, provider identity, model names/modalities, or authoritative context values continue to produce a hard conflict.
This appears to be a follow-up to #2389 / #2393: the derived metadata paths handled by #2393 are tolerated, but ZCode's object-key normalization still changes the protected contribution fingerprint.
Reproduction
- Use an OpenCodex version containing fix(zcode): tolerate derived model metadata drift #2393 and enable the ZCode integration.
- Confirm the integration is newly applied.
- Let ZCode open/save
~/.zcode/v2/config.jsonnormally. - Compare the generated provider against the file on disk:
ocx export --client zcode --json \
| jq '.provider.opencodex' > /tmp/zcode-expected.json
jq '.provider.opencodex' ~/.zcode/v2/config.json \
> /tmp/zcode-actual.json
diff -u /tmp/zcode-expected.json /tmp/zcode-actual.json- Run:
ocx integration client status --client zcode --jsonThe integration reports conflict / foreign-edit.
Representative key-order-only diff at the provider level:
{
"name": "OpenCodex",
"kind": "openai-compatible",
- "enabled": true,
- "source": "custom",
"options": {
"apiKey": "opencodex-loopback",
"baseURL": "http://127.0.0.1:10100/v1",
"apiKeyRequired": true
},
+ "enabled": true,
+ "source": "custom",
"models": {Representative per-model reorder with the same authoritative context value:
"gpt-5.6-sol": {
"name": "gpt-5.6-sol (native)",
+ "limit": {
+ "context": 350000
+ },
"modalities": {
"input": ["text", "image"],
"output": ["text"]
- },
- "limit": {
- "context": 350000
}
}The full diff also contains ZCode-derived reasoning, limit.output, and filled limit.context values that are within the refreshable paths introduced by #2393.
Current canonicalContribution() sorts fragment paths, then serializes each nested fragment.value directly:
export function canonicalContribution(contribution: ManagedContribution): string {
const sorted = [...contribution.fragments].sort((a, b) => {
const left = a.path.join("\u0000");
const right = b.path.join("\u0000");
return left < right ? -1 : left > right ? 1 : 0;
});
return JSON.stringify(sorted.map(fragment => [fragment.path, fragment.value]));
}protectedContributionFingerprint() removes only the declared refreshable paths and then calls this same canonicalization. Because nested object keys are not recursively canonicalized, JSON key-order normalization changes the protected fingerprint even when protected values are identical.
Version
2.33.0
Operating system
macOS 27.0, Apple Silicon
Provider and model
Not provider-specific
Logs or error output
$ ocx integration client status --client zcode --json
{
"clientId": "zcode",
"state": "conflict",
"installed": true,
"configPath": "<HOME>/.zcode/v2/config.json",
"reason": "foreign-edit",
"appliedAt": "2026-08-27T12:09:59.043Z",
"lastOpId": "18f13a13-96c4-49f8-96b5-10b91102e8be",
"snapshotCount": 10,
"retentionDegraded": false
}Screenshots and supporting files
A full diff -u between ocx export --client zcode --json and the on-disk provider.opencodex shows repeated object-key reordering across provider and model entries. The semantic values of protected fields shown above remain unchanged.
The same diff also shows expected ZCode-derived metadata additions, for example reasoning, limit.output, and default limit.context, which #2393 explicitly intended to tolerate.
Redacted configuration
{
"provider": {
"opencodex": {
"name": "OpenCodex",
"kind": "openai-compatible",
"options": {
"apiKey": "opencodex-loopback",
"baseURL": "http://127.0.0.1:10100/v1",
"apiKeyRequired": true
},
"enabled": true,
"source": "custom",
"models": {
"example/model": {
"name": "example/model",
"limit": { "context": 350000 },
"modalities": {
"input": ["text"],
"output": ["text"]
}
}
}
}
}
}Checks
- I searched existing issues and documentation.
- I removed secrets, tokens, account details, request credentials, and personal data.
Activity
Ingwannu commentedon Aug 27, 2026
Confirmed against current
devat7ca954ffd: this is a real ownership-classification bug.canonicalContribution()sorts fragment paths, but it passes each nestedfragment.valuedirectly toJSON.stringify. I reproduced two semantically identical ZCode provider blocks producing different canonical bytes when only nested object insertion order changed.protectedContributionFingerprint()inherits the same behavior, so the operation-scoped protection added in #2393 still reportsconflict / foreign-editafter ZCode's key-order normalization.I will keep the fix narrow:
I am targeting
devand will link the PR with exact-head verification. I am leaving the issue open until the fix is merged.lidge-jun commentedon Aug 27, 2026
리뷰 · 우선순위 72 / 80
이 이슈는 ZCode가 설정 파일을 저장할 때 JSON 객체 키 순서만 바꿔도 OpenCodex 연동이 conflict / foreign-edit 로 떨어진다는 보고입니다. 리포터는 #2393 이후에도, 보호해야 할 값(연결 주소, 모델 이름과 모달리티, 우리가 넣은 context)은 그대로인데 options 가 enabled/source 앞으로 가거나 모델 안의 limit 이 modalities 앞으로 가면 지문이 달라진다고 적었습니다. Ingwannu가 지금 dev(HEAD 7ca954f, package.json 2.34.0)에서 같은 현상을 재현해 확인했습니다.
지금 dev의 canonicalContribution()은 조각 경로 배열만 정렬합니다. 각 조각의 값(fragment.value)은 JSON.stringify 에 그대로 넣습니다. 자바스크립트 JSON.stringify 는 객체 키를 삽입된 순서대로 찍습니다. 값이 같아도 키 순서만 다르면 바이트가 달라집니다. protectedContributionFingerprint()는 #2393이 넣은 새로고침 가능 경로(reasoning, limit.output, 비권위 limit.context)를 지운 뒤 같은 canonicalContribution()을 호출합니다. 파생 필드는 무시해도, 남은 객체의 키 순서는 그대로 해시에 들어갑니다.
테스트도 그 구멍과 맞습니다. integrations-state 테스트의 fragment order 케이스는 조각 배열을 뒤집었을 때만 같다고 봅니다. 중첩 객체 키 순서는 검사하지 않습니다. 그래서 #2393의 보호 범위가 맞아도 ZCode의 키 정규화는 여전히 외국 편집으로 분류됩니다. types.ts/config.ts 분할로 무효가 되는 종류가 아닙니다. 미리보기 배포는 계획에 없습니다.
src/integrations/ownership.ts:28 - canonicalContribution 은 path만 정렬하고 value 는 그대로 stringify 합니다
src/integrations/ownership.ts:34 - JSON.stringify(fragment.value)가 삽입 순서를 보존해서 키만 바뀌어도 해시가 바뀝니다
src/integrations/ownership-policy.ts:128 - protectedContributionFingerprint 가 같은 canonical 을 쓰므로 #2393 보호를 우회하지 못합니다
src/integrations/ownership-policy.ts:48 - cloneFragment 의 structuredClone 도 키 삽입 순서를 유지합니다
src/integrations/ownership-policy.ts:63 - refreshablePathsOf 는 reasoning/limit.output/비권위 context만 허용합니다. 키 순서는 이 목록이 아닙니다
tests/integrations-state.test.ts:501 - 조각 순서만 고정하고 중첩 키 순서 회귀가 없습니다
메인테이너의 판단이 필요한 지점
너의 추천
열어 두세요. 다음 PR은 canonicalContribution 에서 객체 키만 재귀 정렬하고 배열 순서는 유지한 뒤, 지문 유닛과 ZCode writer/state 회귀를 같이 넣으면 됩니다. 실제 값 변경은 지금처럼 충돌로 남기세요. 리포터 확인이 끝나기 전에 이슈를 닫지 마세요. 태그/퍼블리시는 하지 마세요.
이 댓글은 grok-bot이 작성했습니다
Ingwannu commentedon Aug 27, 2026
The fix is now open as #2761 at commit
63941b583.The patch keeps released ownership records compatible, adds key-order-independent semantic fingerprints for new records, and preserves array order plus fail-closed protected-field checks. The focused state/writer suite passes 92/92 on the exact
devbase.I am leaving this issue open until #2761 has exact-head CI, another maintainer's approval, and is merged.
lidge-jun commentedon Aug 27, 2026
Landed via #2761 at d1def68
devnow ignores nested JSON object key-order rewrites in the client-integration ownership classifier (exact fingerprints kept; semantic companion fingerprints added). Closing as completed.이 댓글은 grok-bot이 작성했습니다