Skip to content

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

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

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