Repository navigation
Decide config-migration systematization (layer 3) #20
Description
Activity
- addedwayfinder:grillingWayfinder ticket: HITL grilling decisionWayfinder ticket: HITL grilling decision
on Jul 24, 2026 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-knowndefaults 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.jsonpersists forever per user via GCS home-sync (qbraid-DstacksCLAUDE.md §4.6).setup-codeq-config.shwrites the full config only if missing → it only ever touches brand-new users.update-codeq-token.shruns at pod start and on every codeq launch (viacodeq-wrapper.sh) and is the only convergence surface for existing users. Today it re-assertsprovider.qbraid.options.apiKey,.baseURL,mcp.pod_mcp.command(unconditional);mcp.pod_mcp.type/enabled(only-if-absent); and a hardcodedMODEL_MIGRATIONSmap for dead model ids. It touches nothing else.
Two facts that reframed the ticket's three options
- 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 deadaccount-v2baseURL). A genuinely absent field already gets the binary's new default on a fresh pod — no re-assert needed. - 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)
- 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-knownout as a primary mechanism, since it can't force-correct.) - 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). - 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. - 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.
- 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.mdcarries 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 themodel-migration map), the fields it must not touch (everything else = user-owned, e.g. thepermissionblock), 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-DstacksPR updatingupdate-codeq-token.shbefore/with the rollout. Captured as a named line in the codeq release checklist inCODEQ_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/opencodedefaults 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)
- 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"). - 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 groundingThere are two copies of
update-codeq-token.sh:qbraid-lab-base/scripts/update-codeq-token.sh— current; has the env-var-first token read, theaccount-v2 → account.qbraid.comcorrection, theMODEL_MIGRATIONSmap, andmcp.pod_mcp.commandre-assert.qbraid-overlay/scripts/update-codeq-token.sh— drifted to the pre-migration version (old qbraidrc-only token read,account-v2default, no model migrations, no mcp re-assert).
For this map's destination (the staging/prod K8s Lab path),
qbraid-lab-baseis 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-overlayis flagged "in flux" inqbraid-DstacksCLAUDE.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.
Part of #16
Question
How should codeq
config.jsonchanges reliably converge for existing users, given~/.config/codeq/persists via GCS home-sync andsetup-codeq-config.shonly writes when missing?Decide between (and spec the chosen one):
update-codeq-token.shas a versioned config-migration surface (a config schema version + ordered migration steps re-asserted on launch), vsAlso decide: what is the contract this repo owes
qbraid-Dstackswhen a codeq change adds/renames a config field?HITL. Output: chosen approach + the contract, execution-ready.