Skip to content

ZCode key-order normalization still triggers foreign-edit after #2393 #2759

Closed

Description

@rodrigueez

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

  1. Use an OpenCodex version containing fix(zcode): tolerate derived model metadata drift #2393 and enable the ZCode integration.
  2. Confirm the integration is newly applied.
  3. Let ZCode open/save ~/.zcode/v2/config.json normally.
  4. 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
  1. Run:
ocx integration client status --client zcode --json

The 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

  1. added
    bugSomething isn't working
    cliCLI, config inject, packaging flags
    providerProvider adapters, OpenAI-compat presets, upstream API quirks
    on Aug 27, 2026
  2. Ingwannu commented on Aug 27, 2026

    @Ingwannu
    Owner

    Confirmed against current dev at 7ca954ffd: this is a real ownership-classification bug.

    canonicalContribution() sorts fragment paths, but it passes each nested fragment.value directly to JSON.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 reports conflict / foreign-edit after ZCode's key-order normalization.

    I will keep the fix narrow:

    • recursively canonicalize JSON object keys before hashing;
    • preserve array order, because arrays such as modalities and reasoning ladders are semantically ordered;
    • add a direct fingerprint regression and a real ZCode writer/state regression;
    • keep actual edits to protected connection fields, model membership, modalities, and authoritative context values fail-closed.

    I am targeting dev and will link the PR with exact-head verification. I am leaving the issue open until the fix is merged.

  3. lidge-jun commented on Aug 27, 2026

    @lidge-jun
    Owner

    리뷰 · 우선순위 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 - 조각 순서만 고정하고 중첩 키 순서 회귀가 없습니다

    메인테이너의 판단이 필요한 지점

    • 재귀 키 정렬을 canonicalContribution 한곳에 둘지, ZCode 전용으로만 둘지. 한곳에 두는 쪽이 다른 클라이언트 키 정규화에도 같습니다
    • 배열 순서는 모달리티와 reasoning ladder처럼 의미가 있으므로 그대로 둘지 (Ingwannu 안과 같음)
    • 실제 연결 필드, 모델 구성, 권위 있는 context 변경은 지금처럼 충돌로 남을지

    너의 추천
    열어 두세요. 다음 PR은 canonicalContribution 에서 객체 키만 재귀 정렬하고 배열 순서는 유지한 뒤, 지문 유닛과 ZCode writer/state 회귀를 같이 넣으면 됩니다. 실제 값 변경은 지금처럼 충돌로 남기세요. 리포터 확인이 끝나기 전에 이슈를 닫지 마세요. 태그/퍼블리시는 하지 마세요.

    이 댓글은 grok-bot이 작성했습니다

  4. Ingwannu commented on Aug 27, 2026

    @Ingwannu
    Owner

    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 dev base.

    I am leaving this issue open until #2761 has exact-head CI, another maintainer's approval, and is merged.

  5. lidge-jun commented on Aug 27, 2026

    @lidge-jun
    Owner

    Landed via #2761 at d1def68

    dev now 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이 작성했습니다

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI, config inject, packaging flagslanded-via-maintainerOriginal PR closed after landing via a maintainer merge trainproviderProvider adapters, OpenAI-compat presets, upstream API quirks

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions