fix(up): stamp the authored config in devcontainer.metadata, not this machine's paths - #365
Open
pofallon wants to merge 2 commits into
Open
fix(up): stamp the authored config in devcontainer.metadata, not this machine's paths#365pofallon wants to merge 2 commits into
pofallon wants to merge 2 commits into
Conversation
… 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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
What
deacon substituted variables before stamping the
devcontainer.metadatacontainer 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 deletingparity_state_diff.The deciding evidence is the spec, not the reference
conformance/spec/113500f4/image-metadata.md's Merge Logic closes with:Recording is not applying. T115 was planned as "match the reference" (
reference: divergent→ align); it is actuallyspec: nonconformant→conformant, so awvr-waiver was never the right instrument.Measuring widened the defect, and caught a second one
mounts. A probe fixture against pinned oracle 0.87.0 showed the reference stamps templates formounts,containerEnv,remoteEnvandpostCreateCommand, while applying the substitutedcontainerEnvvalue toConfig.Env. Every picked string field was affected, not one.--container-idread-back surfaces them. Measured:devcontainer exec --container-idprintsLWF=[${localWorkspaceFolder}]andCWF=[${containerWorkspaceFolder}]literally, butLENV=[/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. Soconfig_from_metadata_labelnow appliesSubstitutionContext::host_env_only().How
ConfigLoader::load_with_overrides_and_substitution_rawreturns(raw, substituted, report)— the reference'sSubstitutedConfig { raw, config }— surfaced asConfigLoadResult::raw_config.uppicks the metadata entry from it at the top of the flow and threads the resultingserde_json::Valueinto both the single-container and compose stamp sites.build_container_metadata_labelnow 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-nonrootstill diverged after the fix, on nothing but key insertion order and two spaces:label_json_document(scopechannel:chan-container-state) parses the one label whose value is a JSON document and compares it structurally. It islabel_semanticapplied 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'spickConfigPropertieswould instead pin deacon to an implementation detail of the reference's serializer with no meaning and no stability guarantee.NORMALIZER_VERSION5 → 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'sbhv-container-identity-labelsentry andsrc-obs-container-identity-labelsboth asserteddevcontainer.metadata"compares byte-equal". That was measured onfx-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-alivecmdfield taught in T114.Registry
bhv-container-metadata-label-authored(spec: conformant/reference: aligned/decision: follow-spec), enumerated inPOST_BRANCH_BEHAVIORSwith 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).src-spec-metadata-substitution-timing(the normative clause) andsrc-obs-container-metadata-label-authored(the measurement).case-state-single-container/case-state-mount-variety/case-state-dockerfile-nonroot.behaviorsis excluded fromcaseHash, so no snapshot re-record.Verification
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo nextest run --profile dev-fast --no-fail-fastvalidate/certify/migration check/snapshot check0 blocking, conservation0 violations, snapshot1 freshcargo nextest run --profile parity -E 'binary(=parity_conformance_runner)'case-state-*agreeintegration_up_exec_identity(docker)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 leakedsource=/tmp/.tmpKlx2Jh/sibpath.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_rulesfired on its own during this work and was updated with the new reason, not loosened.The parity lane remains red on 51
chan-structured-outputcases — the pre-existing T113 families, untouched here and documented as the designed consequence of retiring the blanketprunenormalizer.🤖 Generated with Claude Code
https://claude.ai/code/session_017UCGzJEFZd85HJqz1Wx2kE