Skip to content

Commit 672227d

Browse files
solomonneascodex
andauthored
fix(components): honor unified release manifests in reports (#408)
* fix(components): honor unified release manifests in reports Co-authored-by: Codex <codex@openai.com> * fix(components): normalize manifest fallback errors Co-authored-by: Codex <codex@openai.com> --------- Co-authored-by: Codex <codex@openai.com>
1 parent 2642ccc commit 672227d

6 files changed

Lines changed: 449 additions & 48 deletions

File tree

src/brigade/component_install.py

Lines changed: 58 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ class ComponentInstallError(RuntimeError):
5252
"""Raised when a component install step fails verification."""
5353

5454

55+
class ExactReleaseManifestError(RuntimeError):
56+
"""Raised when matching update state cannot provide a verified manifest."""
57+
58+
def __init__(self, message: str, manifest_path: Path) -> None:
59+
super().__init__(message)
60+
self.manifest_path = manifest_path
61+
62+
5563
_SETUP_ACTIONS: tuple[str, ...] = ("verify-cache", "download", "materialize", "smoke")
5664

5765

@@ -91,6 +99,51 @@ def resolve_roots(
9199
)
92100

93101

102+
def uses_bundled_compatibility_manifest() -> bool:
103+
"""Return whether automatic manifest selection starts at the bundled fallback."""
104+
bundled_path = templates.template_root() / "components" / "manifest-v1.json"
105+
return component_manifest.manifest_path() == bundled_path
106+
107+
108+
def load_verified_exact_release_manifest(
109+
roots: SetupRoots,
110+
) -> tuple[component_manifest.ComponentManifest, Path] | None:
111+
"""Read the current release manifest recorded in update state without mutating it."""
112+
from brigade import update_cmd
113+
114+
state_path = Path(component_paths.update_state_path(roots.data_root))
115+
state = update_cmd.load_update_state(state_path)
116+
if state is None or state.component_tag != f"v{brigade.__version__}":
117+
return None
118+
119+
cached = Path(component_paths.verified_manifest_path(roots.cache_root, state.component_manifest_sha256))
120+
if not cached.is_file():
121+
raise ExactReleaseManifestError("cached exact-release manifest is missing", cached)
122+
try:
123+
cached_bytes = cached.read_bytes()
124+
except OSError as exc:
125+
raise ExactReleaseManifestError(f"cached exact-release manifest cannot be read: {exc}", cached) from exc
126+
if hashlib.sha256(cached_bytes).hexdigest() != state.component_manifest_sha256:
127+
raise ExactReleaseManifestError("cached exact-release manifest digest does not match update state", cached)
128+
129+
release = update_cmd.ResolvedRelease(
130+
state.component_release_id,
131+
state.component_tag,
132+
brigade.__version__,
133+
state.component_target_commit,
134+
state.component_manifest_url,
135+
len(cached_bytes),
136+
state.component_manifest_sha256,
137+
cached_bytes,
138+
)
139+
try:
140+
return update_cmd.validate_release_manifest_bytes(release), cached
141+
except update_cmd.UpdateError as exc:
142+
raise ExactReleaseManifestError(str(exc), cached) from exc
143+
except ValueError as exc:
144+
raise ExactReleaseManifestError(f"cached exact-release manifest is invalid: {exc}", cached) from exc
145+
146+
94147
def build_setup_plan(
95148
manifest: component_manifest.ComponentManifest,
96149
*,
@@ -707,42 +760,25 @@ def _load_setup_manifest(
707760
if manifest_source != "auto":
708761
raise ComponentInstallError("manifest source must be auto or standalone")
709762

710-
bundled_path = templates.template_root() / "components" / "manifest-v1.json"
711-
if component_manifest.manifest_path() != bundled_path:
763+
if not uses_bundled_compatibility_manifest():
712764
return component_manifest.load(), None
713765

714766
from brigade import update_cmd
715767

716768
roots = resolve_roots(env=env)
717769
try:
718770
if offline:
719-
state_path = Path(component_paths.update_state_path(roots.data_root))
720-
state = update_cmd.load_update_state(state_path)
721-
if state is not None and state.component_tag == f"v{brigade.__version__}":
722-
cached = Path(component_paths.verified_manifest_path(roots.cache_root, state.component_manifest_sha256))
723-
if cached.is_file():
724-
cached_bytes = cached.read_bytes()
725-
if hashlib.sha256(cached_bytes).hexdigest() != state.component_manifest_sha256:
726-
raise ComponentInstallError("cached exact-release manifest digest does not match update state")
727-
release = update_cmd.ResolvedRelease(
728-
state.component_release_id,
729-
state.component_tag,
730-
brigade.__version__,
731-
state.component_target_commit,
732-
state.component_manifest_url,
733-
len(cached_bytes),
734-
state.component_manifest_sha256,
735-
cached_bytes,
736-
)
737-
return update_cmd.validate_release_manifest_bytes(release), None
771+
cached_manifest = load_verified_exact_release_manifest(roots)
772+
if cached_manifest is not None:
773+
return cached_manifest[0], None
738774
raise ComponentInstallError("offline setup requires a verified exact-release manifest cache")
739775

740776
release = update_cmd.resolve_release(update_cmd._DefaultHttp(), latest=False, tag=f"v{brigade.__version__}")
741777
update_cmd._cache_manifest(
742778
update_cmd.UpdatePaths(Path(roots.data_root), Path(roots.cache_root), Path("unused")), release
743779
)
744780
return update_cmd.validate_release_manifest_bytes(release), release
745-
except update_cmd.UpdateError as exc:
781+
except (update_cmd.UpdateError, ExactReleaseManifestError, ValueError) as exc:
746782
raise ComponentInstallError(f"exact release manifest setup failed: {exc}") from exc
747783

748784

src/brigade/component_report.py

Lines changed: 111 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -75,32 +75,63 @@ def inspect_components(
7575
) -> ComponentReport:
7676
"""Inspect managed native components without mutating user data."""
7777
environment = dict(env if env is not None else os.environ)
78-
manifest_source = manifest_path or component_manifest.manifest_path()
7978
manifest_unknown: tuple[str, ...] = ()
8079
manifest_schema_version: int | None = None
8180
manifest_revision: str | None = None
8281
manifest_brigade_version: str | None = None
82+
roots: component_install.SetupRoots | None = None
83+
environment_error: str | None = None
84+
try:
85+
roots = component_install.resolve_roots(env=environment, system=system)
86+
except ValueError as exc:
87+
environment_error = str(exc)
88+
89+
state_path = Path(
90+
component_paths.installed_state_path(roots.data_root if roots is not None else _UNAVAILABLE_DATA_ROOT)
91+
)
92+
installed_state, state_file_status = _read_installed_state(state_path) if roots is not None else (None, "missing")
93+
94+
manifest_source = manifest_path or component_manifest.manifest_path()
8395
manifest: component_manifest.ComponentManifest | None = None
8496
try:
85-
manifest = component_manifest.load(manifest_path) if manifest_path is not None else component_manifest.load()
97+
if manifest_path is not None:
98+
manifest = component_manifest.load(manifest_path)
99+
elif roots is not None and component_install.uses_bundled_compatibility_manifest():
100+
exact_release_manifest = component_install.load_verified_exact_release_manifest(roots)
101+
if exact_release_manifest is None:
102+
manifest = component_manifest.load()
103+
else:
104+
manifest, manifest_source = exact_release_manifest
105+
if installed_state is not None and not _installed_state_matches_manifest(installed_state, manifest):
106+
manifest = component_manifest.load()
107+
manifest_source = component_manifest.manifest_path()
108+
else:
109+
manifest = component_manifest.load()
110+
except component_install.ExactReleaseManifestError as exc:
111+
return _environment_blocked_report(
112+
manifest_source=exc.manifest_path,
113+
platform_error=str(exc),
114+
manifest=None,
115+
roots=roots,
116+
state_path=state_path,
117+
installed_state=installed_state,
118+
state_file_status=state_file_status,
119+
)
86120
except ValueError as exc:
87121
return _environment_blocked_report(
88122
manifest_source=manifest_source,
89123
platform_error=str(exc),
90124
manifest=None,
125+
roots=roots,
126+
state_path=state_path,
127+
installed_state=installed_state,
128+
state_file_status=state_file_status,
91129
)
92130
manifest_schema_version = manifest.schema_version
93131
manifest_revision = manifest.manifest_revision
94132
manifest_brigade_version = manifest.brigade_version
95133
manifest_unknown = manifest.unknown_component_diagnostics
96134

97-
roots: component_install.SetupRoots | None = None
98-
environment_error: str | None = None
99-
try:
100-
roots = component_install.resolve_roots(env=environment, system=system)
101-
except ValueError as exc:
102-
environment_error = str(exc)
103-
104135
platform: str | None = None
105136
platform_error: str | None = environment_error
106137
if environment_error is None:
@@ -115,11 +146,12 @@ def inspect_components(
115146
platform_error=platform_error or "component environment is unavailable",
116147
manifest=manifest,
117148
manifest_unknown_diagnostics=manifest_unknown,
149+
roots=roots,
150+
state_path=state_path,
151+
installed_state=installed_state,
152+
state_file_status=state_file_status,
118153
)
119154

120-
state_path = Path(component_paths.installed_state_path(roots.data_root))
121-
installed_state, state_file_status = _read_installed_state(state_path)
122-
123155
inspections: list[ComponentInspection] = []
124156
for component_id in component_manifest.KNOWN_COMPONENT_IDS:
125157
inspections.append(
@@ -160,9 +192,14 @@ def _environment_blocked_report(
160192
platform_error: str,
161193
manifest: component_manifest.ComponentManifest | None,
162194
manifest_unknown_diagnostics: tuple[str, ...] = (),
195+
roots: component_install.SetupRoots | None = None,
196+
state_path: Path | None = None,
197+
installed_state: component_state.InstalledState | None = None,
198+
state_file_status: STATE_FILE_STATUS = "missing",
163199
) -> ComponentReport:
164200
"""Return a read-only unsupported report when roots or manifest cannot be resolved."""
165-
state_path = Path(component_paths.installed_state_path(_UNAVAILABLE_DATA_ROOT))
201+
report_state_path = state_path or Path(component_paths.installed_state_path(_UNAVAILABLE_DATA_ROOT))
202+
data_root = roots.data_root if roots is not None else _UNAVAILABLE_DATA_ROOT
166203
components = tuple(
167204
ComponentInspection(
168205
component_id=component_id,
@@ -171,16 +208,36 @@ def _environment_blocked_report(
171208
expected_component_revision=(
172209
manifest.components[component_id].component_revision if manifest is not None else None
173210
),
174-
installed_component_revision=None,
211+
installed_component_revision=(
212+
installed_state.components[component_id].component_revision
213+
if installed_state is not None and component_id in installed_state.components
214+
else None
215+
),
175216
expected_asset_name=None,
176217
expected_byte_size=None,
177218
expected_sha256=None,
178-
installed_asset_name=None,
179-
installed_byte_size=None,
180-
installed_sha256=None,
181-
recorded_executable=None,
219+
installed_asset_name=(
220+
installed_state.components[component_id].asset_name
221+
if installed_state is not None and component_id in installed_state.components
222+
else None
223+
),
224+
installed_byte_size=(
225+
installed_state.components[component_id].byte_size
226+
if installed_state is not None and component_id in installed_state.components
227+
else None
228+
),
229+
installed_sha256=(
230+
installed_state.components[component_id].sha256
231+
if installed_state is not None and component_id in installed_state.components
232+
else None
233+
),
234+
recorded_executable=(
235+
installed_state.components[component_id].executable
236+
if installed_state is not None and component_id in installed_state.components
237+
else None
238+
),
182239
managed_executable_path=component_paths.managed_executable_path(
183-
_UNAVAILABLE_DATA_ROOT,
240+
data_root,
184241
component_id,
185242
),
186243
actual_byte_size=None,
@@ -198,16 +255,46 @@ def _environment_blocked_report(
198255
platform=None,
199256
platform_error=platform_error,
200257
state_schema_version=component_state.SCHEMA_VERSION,
201-
installed_state_path=str(state_path),
202-
state_file_status="missing",
203-
installed_manifest_revision=None,
204-
installed_brigade_version=None,
205-
installed_platform=None,
258+
installed_state_path=str(report_state_path),
259+
state_file_status=state_file_status,
260+
installed_manifest_revision=installed_state.manifest_revision if installed_state else None,
261+
installed_brigade_version=installed_state.brigade_version if installed_state else None,
262+
installed_platform=installed_state.platform if installed_state else None,
206263
manifest_unknown_diagnostics=manifest_unknown_diagnostics,
207264
components=components,
208265
)
209266

210267

268+
def _installed_state_matches_manifest(
269+
installed_state: component_state.InstalledState,
270+
manifest: component_manifest.ComponentManifest,
271+
) -> bool:
272+
"""Return whether installed components use the manifest's exact recorded coordinates."""
273+
if (
274+
installed_state.manifest_revision != manifest.manifest_revision
275+
or installed_state.brigade_version != manifest.brigade_version
276+
):
277+
return False
278+
for component_id in component_manifest.KNOWN_COMPONENT_IDS:
279+
installed = installed_state.components.get(component_id)
280+
if installed is None:
281+
return False
282+
component = manifest.components[component_id]
283+
try:
284+
asset = component_manifest.resolve_asset(manifest, component_id, installed_state.platform)
285+
except ValueError:
286+
return False
287+
if (
288+
installed.component_revision != component.component_revision
289+
or installed.asset_name != asset.asset_name
290+
or installed.byte_size != asset.byte_size
291+
or installed.sha256 != asset.sha256
292+
or installed.download_url != asset.download_url
293+
):
294+
return False
295+
return True
296+
297+
211298
def doctor_checks(
212299
*,
213300
env: Mapping[str, str] | None = None,

src/brigade/localio.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ def read_json_dict(path: Path) -> dict[str, Any] | None:
3737
"""Read a JSON object from path; return None when missing, invalid, or not a dict."""
3838
try:
3939
payload = json.loads(path.read_text())
40-
except (OSError, json.JSONDecodeError):
40+
except (OSError, UnicodeDecodeError, json.JSONDecodeError):
4141
return None
4242
return payload if isinstance(payload, dict) else None
4343

tests/test_component_install.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -810,6 +810,24 @@ def test_setup_auto_offline_translates_rejected_cached_manifest_update_errors(tm
810810
assert "Traceback" not in err
811811

812812

813+
def test_setup_auto_online_translates_invalid_release_manifest(tmp_path, monkeypatch):
814+
env, _release = _prepare_auto_release_setup(tmp_path, monkeypatch)
815+
monkeypatch.setattr(
816+
update_cmd,
817+
"validate_release_manifest_bytes",
818+
lambda _release: (_ for _ in ()).throw(ValueError("release manifest is invalid")),
819+
)
820+
821+
with pytest.raises(ComponentInstallError, match="exact release manifest setup failed: release manifest is invalid"):
822+
component_install._load_setup_manifest(
823+
manifest_path=None,
824+
manifest_source="auto",
825+
offline=False,
826+
opener=FakeOpener({}),
827+
env=env,
828+
)
829+
830+
813831
def test_setup_auto_online_translates_manifest_cache_collisions(tmp_path, monkeypatch, capsys):
814832
env, release = _prepare_auto_release_setup(tmp_path, monkeypatch)
815833
roots = resolve_roots(env=env, system="linux")

0 commit comments

Comments
 (0)