Skip to content

Decide config-migration systematization (layer 3) #20

Description

@Kenny-Heitritter

Part of #16

Question

How should codeq config.json changes reliably converge for existing users, given ~/.config/codeq/ persists via GCS home-sync and setup-codeq-config.sh only writes when missing?

Decide between (and spec the chosen one):

  • Formalize update-codeq-token.sh as a versioned config-migration surface (a config schema version + ordered migration steps re-asserted on launch), vs
  • Keep the current ad-hoc per-field re-assert block and just require every codeq change that touches config to add its field there (documented contract), vs
  • Another approach.

Also decide: what is the contract this repo owes qbraid-Dstacks when a codeq change adds/renames a config field?

HITL. Output: chosen approach + the contract, execution-ready.

Activity

  1. Kenny-Heitritter commented on Jul 24, 2026

    @Kenny-Heitritter
    MemberAuthor

    Resolution — config-migration systematization (layer 3)

    Decision: keep a single, high-authority, idempotent re-assert surface in update-codeq-token.sh (qbraid-Dstacks); no versioned-migration framework, no .well-known defaults channel, no binary-side migration. The contract opencode owes qbraid-Dstacks is a scoped, registry-backed one.

    How layer 3 actually works today (grounding)

    • ~/.config/codeq/config.json persists forever per user via GCS home-sync (qbraid-Dstacks CLAUDE.md §4.6).
    • setup-codeq-config.sh writes the full config only if missing → it only ever touches brand-new users.
    • update-codeq-token.sh runs at pod start and on every codeq launch (via codeq-wrapper.sh) and is the only convergence surface for existing users. Today it re-asserts provider.qbraid.options.apiKey, .baseURL, mcp.pod_mcp.command (unconditional); mcp.pod_mcp.type/enabled (only-if-absent); and a hardcoded MODEL_MIGRATIONS map for dead model ids. It touches nothing else.

    Two facts that reframed the ticket's three options

    1. The codeq config schema is almost entirely .optional() (packages/opencode/src/config/config.ts) and the binary merges its own built-in defaults for absent fields. So a persisted config only causes staleness when it holds an explicit stale value (a dead model id, the dead account-v2 baseURL). A genuinely absent field already gets the binary's new default on a fresh pod — no re-assert needed.
    2. opencode has a native org-defaults channel — .well-known/opencode (config.ts:69–95). The binary fetches org config from a server endpoint on launch and merges it as lowest precedence (user config wins). It can ship new defaults but, being lowest-precedence, cannot force-correct an explicit stale value. Only the high-authority re-assert can.

    Decisions (in dependency order)

    1. Framing → one uniform surface. Do not split convergence into a separate defaults channel (.well-known) + a force-correct channel. A single high-authority re-assert handles both. (This rules .well-known out as a primary mechanism, since it can't force-correct.)
    2. Location → stays in update-codeq-token.sh (qbraid-Dstacks). The shell script is unavoidable regardless — apiKey/baseURL/mcp-command are pod-environment facts the binary can't know — so co-locating migrations there keeps convergence to literally one place. Moving migrations into the codeq binary was rejected because it would create a second surface (binary does schema, script does secrets), cutting against the one-place goal. Accepted costs: cross-repo coupling, and shipping a change needs a lab-base image rebuild (inherits the layer-1 gap).
    3. Mechanism → ad-hoc per-field re-assert + documented contract. No configVersion + ordered-migration framework. The re-assert runs every launch and every op is idempotent by construction; there is no ordered/stateful sequence. A version-stamped engine's main payoff (safe pruning of old migrations) is not wanted here — a user dormant on a dead model id should be corrected whenever they return, forever. A migration state machine in bash/jq for ~5 independent idempotent ops is over-engineering.
    4. Contract scope → only force-correct-worthy changes. A codeq change owes a paired Dstacks re-assert only when it: (a) renames a system-owned field, (b) retires a value id (dead model id / dead endpoint), (c) forces a changed default onto users who may hold an old explicit value, or (d) adds a new system-injected field the binary can't self-default (like apiKey/baseURL). Purely additive optional fields with a sane binary default owe nothing.
    5. Enforcement → doc + explicit system-owned-fields registry. opencode maintains an authoritative registry in CODEQ_DEPLOYMENT.md; a codeq PR changing it is the visible signal a paired Dstacks PR is due. No cross-repo CI machinery.

    The contract, stated

    • Boundary (registry): CODEQ_DEPLOYMENT.md carries the authoritative System-owned config fields registry: the exact fields Dstacks re-asserts (provider.qbraid.options.apiKey, .baseURL, mcp.pod_mcp.command/type/enabled, and the model-migration map), the fields it must not touch (everything else = user-owned, e.g. the permission block), and a running Retired ids / renamed fields list. This registry is the single source of truth the Dstacks re-assert block mirrors. It lives in opencode because that is where the schema lives.
    • Trigger: the four force-correct-worthy classes in decision (4) above. Anything else owes nothing.
    • Obligation (process): any codeq PR that changes the registry must be paired with a qbraid-Dstacks PR updating update-codeq-token.sh before/with the rollout. Captured as a named line in the codeq release checklist in CODEQ_DEPLOYMENT.md.

    Rejected

    • Versioned config-migration framework (schema-version field + ordered migrations, write-back) — over-engineering for a handful of independent, idempotent, every-launch re-asserts; pruning (its main benefit) is not wanted.
    • .well-known/opencode defaults channel as the mechanism — genuinely useful and it's opencode's blessed org-defaults path, but lowest-precedence so it cannot force-correct explicit stale values; it fails the one-uniform-surface test. (Left on record as a future option should a defaults-only need ever arise.)
    • Moving migrations into the codeq binary — co-locates schema+migrations in one repo but splits runtime convergence into two surfaces; rejected against the one-place goal.
    • "Every config change updates Dstacks" — busywork re-asserts for fields the binary already defaults; replaced by the scoped trigger.

    Execution-ready output (for the follow-up execution effort — NOT done here; this map is planning-only)

    1. opencode / CODEQ_DEPLOYMENT.md: add the System-owned config fields registry + Retired ids / renamed fields list + the scoped contract + the release-checklist line. Extends the existing sketch at lines 277–279 (contract step) and 305–308 ("Systematize layer 3").
    2. qbraid-Dstacks / update-codeq-token.sh: restructure the jq block into two labeled buckets — (1) environment-injected (apiKey, baseURL, mcp command/type/enabled) and (2) migrations / forced corrections (the model-migration map + any future rename/retire/forced-default) — with a header comment pointing at the registry. No behavior change.

    ⚠️ Execution caveat found while grounding

    There are two copies of update-codeq-token.sh:

    • qbraid-lab-base/scripts/update-codeq-token.sh — current; has the env-var-first token read, the account-v2 → account.qbraid.com correction, the MODEL_MIGRATIONS map, and mcp.pod_mcp.command re-assert.
    • qbraid-overlay/scripts/update-codeq-token.sh — drifted to the pre-migration version (old qbraidrc-only token read, account-v2 default, no model migrations, no mcp re-assert).

    For this map's destination (the staging/prod K8s Lab path), qbraid-lab-base is the canonical copy — it is the image that deploys to staging/prod (verified in #17). The contract must name it as canonical, and the two copies should be reconciled or de-duplicated during execution, or the registry-mirror guarantee is silently undermined on the overlay path. (qbraid-overlay is flagged "in flux" in qbraid-Dstacks CLAUDE.md §1, so this reconciliation may fold into that rework rather than a standalone fix.)

    No Terraform changes are required for layer 3. The only execution artifacts are the two documentation/organization edits above.

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

Metadata

Metadata

Labels

wayfinder:grillingWayfinder ticket: HITL grilling decision

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions