Skip to content

fix(up): stamp the authored config in devcontainer.metadata, not this machine's paths - #365

Open
pofallon wants to merge 2 commits into
mainfrom
fix/t115-metadata-raw-config
Open

fix(up): stamp the authored config in devcontainer.metadata, not this machine's paths#365
pofallon wants to merge 2 commits into
mainfrom
fix/t115-metadata-raw-config

Conversation

@pofallon

Copy link
Copy Markdown
Contributor

What

deacon substituted variables before stamping the devcontainer.metadata container label, baking the recording machine's absolute workspace path into metadata whose purpose is to describe the configuration to whoever reads the container later. This drains T115, the deferral that blocked deleting parity_state_diff.

The deciding evidence is the spec, not the reference

conformance/spec/113500f4/image-metadata.md's Merge Logic closes with:

Variables in string values will be substituted at the time the value is applied.

Recording is not applying. T115 was planned as "match the reference" (reference: divergent → align); it is actually spec: nonconformantconformant, so a wvr- waiver was never the right instrument.

Measuring widened the defect, and caught a second one

  • Not just mounts. A probe fixture against pinned oracle 0.87.0 showed the reference stamps templates for mounts, containerEnv, remoteEnv and postCreateCommand, while applying the substituted containerEnv value to Config.Env. Every picked string field was affected, not one.
  • Fixing only the stamp would have traded one divergence for another. With templates in the label, the --container-id read-back surfaces them. Measured: devcontainer exec --container-id prints LWF=[${localWorkspaceFolder}] and CWF=[${containerWorkspaceFolder}] literally, but LENV=[/home/vscode] — it re-substitutes what it can resolve without a workspace. deacon previously matched on ${localEnv:…} only by accident, because the value was baked in at stamp time. So config_from_metadata_label now applies SubstitutionContext::host_env_only().

How

ConfigLoader::load_with_overrides_and_substitution_raw returns (raw, substituted, report) — the reference's SubstitutedConfig { raw, config } — surfaced as ConfigLoadResult::raw_config. up picks the metadata entry from it at the top of the flow and threads the resulting serde_json::Value into both the single-container and compose stamp sites. build_container_metadata_label now takes that value rather than a &DevContainerConfig, so a substituted config cannot be handed in by accident — the signature carries the invariant.

One normalization change, named and single-key

case-state-dockerfile-nonroot still diverged after the fix, on nothing but key insertion order and two spaces:

deacon: [{"remoteUser":"dev","containerUser":"dev","containerEnv":{"DF_ENV":"yes"}}]
ref:    [ {"containerEnv":{"DF_ENV":"yes"},"containerUser":"dev","remoteUser":"dev"} ]

label_json_document (scope channel:chan-container-state) parses the one label whose value is a JSON document and compares it structurally. It is label_semantic applied one level deeper. It removes nothing: every key and value is still compared, array order is preserved (fragment order carries precedence), a malformed value is left verbatim so it still diverges, and a label outside the enumerated set is untouched. Aligning deacon's pick order with upstream's pickConfigProperties would instead pin deacon to an implementation detail of the reference's serializer with no meaning and no stability guarantee.

NORMALIZER_VERSION 5 → 6; the one committed snapshot was refreshed through the reviewed path (conformance-snapshot refresh) and the diff is the version field only.

A stale claim corrected in lockstep

POST_BRANCH_BEHAVIORS's bhv-container-identity-labels entry and src-obs-container-identity-labels both asserted devcontainer.metadata "compares byte-equal". That was measured on fx-up-basic, whose bare config contributes no picked property so both sides stamp [] — true there, false in general. A measurement on one fixture is not a claim about the field, which is the same lesson the keep-alive cmd field taught in T114.

Registry

  • New behavior bhv-container-metadata-label-authored (spec: conformant / reference: aligned / decision: follow-spec), enumerated in POST_BRANCH_BEHAVIORS with the reason it is not a variant of the labels behavior (that one is about which labels exist; this one about what the shared one contains — deacon set the label all along and still got its content wrong).
  • New source units: src-spec-metadata-substitution-timing (the normative clause) and src-obs-container-metadata-label-authored (the measurement).
  • Attached to case-state-single-container / case-state-mount-variety / case-state-dockerfile-nonroot. behaviors is excluded from caseHash, so no snapshot re-record.

Verification

Gate Result
cargo fmt --all -- --check pass
cargo clippy --all-targets --all-features -- -D warnings pass
cargo nextest run --profile dev-fast --no-fail-fast 3597 passed
validate / certify / migration check / snapshot check pass; certify 0 blocking, conservation 0 violations, snapshot 1 fresh
cargo nextest run --profile parity -E 'binary(=parity_conformance_runner)' all three case-state-* agree
integration_up_exec_identity (docker) 4 passed

Guards demonstrated to fail on the violation (FR-047), not merely to pass:

  • up_stamps_authored_templates_in_devcontainer_metadata — repointed the entry at the substituted config; it failed printing the leaked source=/tmp/.tmpKlx2Jh/sib path.
  • container_state_chain_applies_label_json_document — unwired the rule from the chain; it failed showing the label arriving as an unparsed string.
  • normalizer_version_is_bumped_for_named_rules fired on its own during this work and was updated with the new reason, not loosened.

The parity lane remains red on 51 chan-structured-output cases — the pre-existing T113 families, untouched here and documented as the designed consequence of retiring the blanket prune normalizer.

🤖 Generated with Claude Code

https://claude.ai/code/session_017UCGzJEFZd85HJqz1Wx2kE

… machine's paths

deacon substituted variables before stamping the `devcontainer.metadata`
container label, baking the recording machine's absolute workspace path into
metadata whose whole purpose is to describe the configuration to whoever reads
the container later.

The deciding evidence is the spec, not the reference. `image-metadata.md`'s Merge
Logic closes with "Variables in string values will be substituted at the time
the value is applied" — recording is not applying, so the recorded form is the
template. T115 was planned as "match the reference" (`reference: divergent`);
it is actually `spec: nonconformant`, and a waiver was never the right
instrument.

Measuring widened the defect and caught a second one:

- Not just `mounts`. A probe fixture against pinned oracle 0.87.0 showed the
  reference stamps templates for `mounts`, `containerEnv`, `remoteEnv` AND
  `postCreateCommand`, while applying the substituted `containerEnv` value to
  `Config.Env`. Every picked string field was affected.
- Fixing only the stamp would have traded one divergence for another. With
  templates in the label, the `--container-id` read-back surfaces them. The
  reference re-substitutes what it can there — `devcontainer exec
  --container-id` prints `LWF=[${localWorkspaceFolder}]` but
  `LENV=[/home/vscode]` — so `config_from_metadata_label` now applies a
  host-env-only pass. deacon previously matched on `${localEnv:…}` only by
  accident, because the value was baked in at stamp time.

`load_with_overrides_and_substitution_raw` returns `(raw, substituted, report)`
— the reference's `SubstitutedConfig { raw, config }` — surfaced as
`ConfigLoadResult::raw_config`. `up` picks the metadata entry from it at the top
of the flow and threads the resulting value into both stamp sites;
`build_container_metadata_label` takes that value rather than a
`&DevContainerConfig`, so a substituted config cannot be passed by accident.

One normalization change, named and single-key. `case-state-dockerfile-nonroot`
still diverged on nothing but key insertion order and two spaces, so
`label_json_document` (scope `channel:chan-container-state`) parses the one label
whose value IS a JSON document and compares it structurally. It removes nothing:
every key and value is still compared, array order is preserved (fragment order
carries precedence), a malformed value is left verbatim so it still diverges, and
labels outside the enumerated set are untouched. Aligning deacon's pick order
with upstream's `pickConfigProperties` would instead pin deacon to an
implementation detail of the reference's serializer. `NORMALIZER_VERSION` 5 → 6,
snapshot refreshed through the reviewed path.

Also corrects a stale claim in lockstep: `POST_BRANCH_BEHAVIORS` and
`src-obs-container-identity-labels` both asserted `devcontainer.metadata`
"compares byte-equal". True on `fx-up-basic`, whose bare config contributes no
picked property so both sides stamp `[]` — false in general. A measurement on
one fixture is not a claim about the field, which is the lesson the keep-alive
`cmd` field taught in T114.

All three `case-state-*` cases now report `agree` on the live differential; the
docker-gated regression test was demonstrated to fail on the pre-fix behavior.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UCGzJEFZd85HJqz1Wx2kE
The test hard-coded `${localEnv:PATH}` and compared against
`std::env::var("PATH")`. On Windows the variable is spelled `Path`:
`std::env::var` matches case-insensitively, but the substitution context looks it
up in a `HashMap` built from `env::vars()`, which does not. The token resolved to
the empty string and only the Windows lane failed.

It now takes the probe variable from `env::vars()` — the same iteration the
substitution context uses — so the test and the code agree about the name by
construction rather than by coincidence.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017UCGzJEFZd85HJqz1Wx2kE
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

fix Bug fix

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant