unaltraweb 0.5.0 → 0.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,254 @@
1
+ # Checkout-free MCP launcher: owner preparation report
2
+
3
+ Date: 2026-09-29. Hub handoff: **unaltraweb owner preparation**, following
4
+ `gacontext/src/bash/mcp_factories/handoffs/owner-preparation-prompts.md` and its
5
+ existing-manual baseline. Tracking: [issue 76](https://github.com/dosquartsdedocs/unaltraweb/issues/76).
6
+
7
+ ## Source and delivery status
8
+
9
+ - Primary checkout: `unaltraweb`; branch `feat/76-distributed-mcp-launcher`.
10
+ - Base/source checkpoint: `260271e41b17be0f95dcb8047014c35cb97142ee`.
11
+ - Implementation is an **uncommitted owner working-tree change**, not a release,
12
+ tag, pushed branch or PR. The hub can ingest this report without changing its
13
+ catalogue or activating a client registration.
14
+ - The full published 0.5.0 GHCR runtime already contains the factory and supports
15
+ checkout-free site work. No replacement image release is needed to run it.
16
+ - The published 0.5.0 wheel lacks the standalone host launcher. The new wheel
17
+ contents require a future reviewed package release under a new version. The
18
+ local test wheel still reports source version 0.5.0 and must never replace the
19
+ published wheel of that name.
20
+
21
+ Changed paths:
22
+
23
+ ```text
24
+ Makefile
25
+ pyproject.toml
26
+ packaging/launcher/Makefile
27
+ src/unaltraweb_mcp/docker_launcher.py
28
+ scripts/unaltraweb-mcp-bootstrap.sh
29
+ scripts/test_wheel_install.py
30
+ scripts/validate_distribution.py
31
+ test/test_distributed_launcher.py
32
+ test/distributed_launcher_smoke.py
33
+ docs/_documentation/en/40-distribution.md
34
+ docs/agents/mcp-contract.md
35
+ docs/agents/owner-preparation-2026-09-29.md
36
+ ```
37
+
38
+ The source `mcp-factory.yml`, component BOM/schema, published receipt, managed
39
+ consumer scaffolds and their version selections are unchanged. Preserved hashes:
40
+
41
+ | File | SHA-256 |
42
+ | --- | --- |
43
+ | `src/unaltraweb_mcp/component-contract.json` | `f3de4b289fc6ae66ddc86bf6a701b3a2cfaed66ab6303f64252c92639a7aab98` |
44
+ | `release-candidates.json` | `e57a9dbb041c1e7fcd781cd1524f4de0317382e0f7303bb36bfa59c0b6d1cb9b` |
45
+
46
+ ## Native installed profile and capability boundary
47
+
48
+ The wheel installs `share/unaltraweb-launcher/Makefile`, the **byte-identical native
49
+ schema-v1 `mcp-factory.yml`**, and the existing bootstrap, project-ID, CSV mount and
50
+ cleanup scripts under `scripts/`. It uses the current `runtime.kind: container`,
51
+ `runtime.package_manager: docker`, consumer binding, `transport.command` with
52
+ `make -C ${factoryRoot} mcp-stdio`, and inherited `MCP_CONSUMER_WORKSPACE`.
53
+ Here `${factoryRoot}` names the installed launcher directory. No new H1 fields or
54
+ alternative central schema were introduced. Companion lifecycle flags and exact
55
+ tool/resource inventories remain those in the native manifest.
56
+
57
+ `unaltraweb-mcp-docker path` resolves that directory from installed wheel metadata.
58
+ `manifest` prints the descriptor. `prepare`, `check`, `smoke`, `serve` and `down`
59
+ delegate to packaged shell scripts. Preparation may pull; check/smoke require a
60
+ local image, use its immutable local ID and mount neither source nor consumer.
61
+ Serve requires an explicit consumer and preserves both canonical host-path mounts
62
+ and the original factory/project labels. Cleanup retains its exact project scope.
63
+
64
+ The profile directory is independently relocatable with Make, POSIX shell, Linux
65
+ core utilities and Docker. The console adapter requires Python. The native
66
+ `unaltraweb-mcp` CLI retains the BOM's exact package-only and factory-required
67
+ inventories: shipping a Docker adapter does not put Ruby/core/TeX/Chromium or
68
+ worker/companion implementations into the wheel. `new-web`, doctor and inspection
69
+ remain usable natively; full MCP serving and build/PDF orchestration execute in
70
+ the selected GHCR image.
71
+
72
+ ## Published identities and dependency closure
73
+
74
+ Native identities use existing BOM `kind`, `version`, `release`, `reference` and
75
+ `image_repository`, and manifest `mcp_dependencies[].uv_spec`. Container references
76
+ below are in `ghcr.io/dosquartsdedocs/`; each hash is an OCI repository digest,
77
+ not a Docker image configuration ID.
78
+
79
+ | Component / repository | Version | Exact selected digest |
80
+ | --- | --- | --- |
81
+ | Full MCP `unaltraweb-mcp` | 0.5.0 | `sha256:36d17edbade77edb40a687f6a744203c6329acb33fbc2eb255e88d9ff1a42c98` |
82
+ | Base Ruby/Jekyll `unaltraweb` (receipt) | 0.5.0 | `sha256:4c7bfa51b8f936993ca5a1350d8b63ac3af77f88e97065a9b4a223583528fa8e` |
83
+ | PDF `unaltraweb-manual-pdf` | 0.5.0 | `sha256:9e0b3a45753c170b795e9a9d6df61580085c113436beac5bf6c8de69b6562097` |
84
+ | Python `unaltraweb-compute-python` | 0.4.0 | `sha256:18cb269811bd4005800382da25a480ec2bca7eac8d0501ad1ef36bad1c0f8cd9` |
85
+ | R `unaltraweb-compute-r` | 0.4.0 | `sha256:928ffb93f221e09e8b929157dee473b838e061915a2eb67224e4124b85f81837` |
86
+ | Chromium `unaltraweb-web-capture` | 0.4.0 | `sha256:0bf1bc67fe63e1440bffe708a168beefa11c54441650a871ab99380d362f7c1e` |
87
+ | Historical full MCP `unaltraweb-mcp` | 0.4.0 | `sha256:389bc585cdb4fc89d3372f4896a55fe26e15df38b46bc114ce44fdb3f1c8deb9` |
88
+ | Historical PDF `unaltraweb-manual-pdf` | 0.4.0 tuple | `sha256:bb3e373f8a512495eeed684c23ad904d298c2e20a6dc3ade0d0a60c7ec9a9f11` |
89
+
90
+ The runtime layer is embedded in the full MCP image. The four workers remain
91
+ separate feature-selected containers; compute/capture were inventoried, not
92
+ rendered again in this preparation. Companions remain separate MCP servers:
93
+
94
+ | Companion | Native selected wheel and SHA-256 |
95
+ | --- | --- |
96
+ | Diavisuals 0.4.0 | `diavisuals[mcp] @ https://github.com/dosquartsdedocs/diavisuals/releases/download/v0.4.0/diavisuals-0.4.0-py3-none-any.whl#sha256=bfedcc9e2554f25ce4a1c33e556f352210848fccb8da800d1c3900dd86c48c93` |
97
+ | Vegavisuals 0.4.0 | `vegavisuals[mcp] @ https://github.com/dosquartsdedocs/vegavisuals/releases/download/v0.4.0/vegavisuals-0.4.0-py3-none-linux_x86_64.whl#sha256=b52ffa743643dd6b5e0320e7a9aa0cd500ea06262b7d5098c0c3f94a379bc0ea` |
98
+
99
+ Both dependencies retain `required/install/build/check/smoke: true` and
100
+ `init/update: false`. Their renderer archive identities and earlier real receipt
101
+ acceptance are in [visual-companions-0.4.0.md](visual-companions-0.4.0.md); that
102
+ 2026-09-26 evidence is not presented as a new rendering run here. Historical core
103
+ 0.4.0 selects companions 0.3.1; the 0.5.0 receipt verifier does not promise to accept
104
+ those older provider identities.
105
+
106
+ ## Synthetic acceptance and isolation
107
+
108
+ Platform: Linux/amd64, host Python 3.12, Docker 27.2.0. A non-editable wheel was
109
+ installed with `--no-deps --no-index` into
110
+ `/tmp/opencode/unaltraweb-owner-20260929/venv`. Its launcher metadata resolved
111
+ outside the source checkout. The retained test wheel SHA-256 is
112
+ `a8b2136ffb23970980b86b70e64d509e2e24500f1e032b023f857cde22a7988f`.
113
+
114
+ All acceptance consumers, Git indexes, caches, generated PDFs and Jekyll outputs
115
+ are synthetic under `/tmp/opencode/unaltraweb-owner-20260929/`. No host core was
116
+ mounted. Preview inspection verified only the synthetic consumer bind and no
117
+ Docker socket. Ports were allocated on loopback; preview stop and digest-bound
118
+ PDF preview cleanup passed. Full stdio sessions used the installed launcher.
119
+
120
+ | Scaffold / host build | Active MCP / PDF | Profile coverage | Result |
121
+ | --- | --- | --- | --- |
122
+ | 0.5.0 / MCP build | 0.5.0 / 0.5.0 | All four profiles | Empty detection, creation, site check, build, preview, HTTP and HTML audit pass; manual PDF/cover pass. Ports 32770–32773. |
123
+ | 0.4.0 / 0.4.0 host build | 0.4.0 / historical pinned PDF | `unaltremanual` | Host build/PDF, MCP build, preview, HTTP and HTML audit pass. Port 32776. New image-background and update-advisory tools are absent as expected. |
124
+ | 0.4.0 / no earlier host build cache | 0.5.0 / 0.5.0 | `unaltremanual` | Pass with fresh synthetic runtime cache, port 32774. Update advisory reports `update_available`; no sync is applied. |
125
+ | 0.4.0 / successful 0.4.0 host build first | 0.5.0 / 0.5.0 | `unaltremanual` | Site check and PDF pass; MCP build fails because retained `tmp/Gemfile.local.lock` selects `google-protobuf (4.36.1)`, unavailable in the 0.5.0 image. No automatic cache deletion or pin migration. |
126
+
127
+ Evidence directories are `released-050-full`, `released-040-manual-full`,
128
+ `mixed-fresh-040-050`, and `mixed-retained-lock-final`. Runs retain `evidence.json`,
129
+ server stderr and actual outputs, including the reproduced mixed-cache failure's
130
+ MCP response, host-build log and generated lock. The initial
131
+ four-profile run hit the terminal's 120-second limit after one profile; its owned
132
+ test session was cleaned by exact consumer identity and the complete run passed
133
+ with a 600-second outer limit. An initial historical runner call to the newer
134
+ `image_background_check` was corrected to inspect actual capabilities; it was a
135
+ test-driver mismatch, not a claimed 0.4.0 feature.
136
+
137
+ Retained PDF hashes:
138
+
139
+ - 0.5.0 and fresh 0.4.0-scaffold/0.5.0 runtime:
140
+ `4e6e405ed690caf075ec33ea04090d2a5c4bfbf0fb0a49a35cdab819c6f40939`.
141
+ - Historical host/runtime 0.4.0:
142
+ `68f067addf5e009364edb42042de2410af5ce8c38543556852d4498bd9663a75`.
143
+
144
+ Both covers were visually inspected. The 0.5.0 PDF has nine pages including the
145
+ synthetic Coordinates chapter, contents and expected draft marker. This is
146
+ technical acceptance, not human publication approval.
147
+
148
+ ## Owner gates and exact reproduction
149
+
150
+ Passed:
151
+
152
+ ```bash
153
+ PYTHONPATH=src python3 -m unittest discover -s test -p 'test_*.py'
154
+ make distribution-check
155
+ make workflow-check
156
+ make wheel-check
157
+ make mcp-check mcp-smoke \
158
+ MCP_RUNTIME_IMAGE=unaltraweb-owner-76-runtime:20260929 \
159
+ MCP_IMAGE=unaltraweb-owner-76-mcp:20260929-final \
160
+ MANUAL_PDF_DEV_IMAGE=unaltraweb-owner-76-pdf:20260929 \
161
+ MCP_SMOKE_MANUAL_PDF_IMAGE=unaltraweb-owner-76-pdf:20260929 \
162
+ MCP_SMOKE_PROJECT=/tmp/opencode/unaltraweb-owner-20260929/owner-preview-final
163
+ docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp \
164
+ --entrypoint make unaltraweb-owner-76-mcp:20260929 -C /opt/unaltraweb gem-check
165
+ make --silent --no-print-directory \
166
+ -C /tmp/opencode/unaltraweb-owner-20260929/venv/share/unaltraweb-launcher \
167
+ mcp-check mcp-smoke
168
+ PYTHONPATH=src python3 -m unaltraweb_mcp.cli --project docs mcp prose-check \
169
+ --target _documentation/en/40-distribution.md
170
+ make docs-build DOCKER_IMAGE=unaltraweb-owner-76-runtime:20260929
171
+ git diff --check
172
+ ```
173
+
174
+ Unit suite: **553 tests, 30 optional skips**. `wheel-check` verifies all four
175
+ factory-free native scaffolds and every byte of the launcher/helper closure.
176
+ The installed-profile smoke uses the published image, no socket/source mount and
177
+ `--network none`. Owner source smoke additionally runs a real stale PDF worker and
178
+ a socket-free persistent preview. Docker's content-addressed build cache reused
179
+ existing layers; test image tags and consumer/Python/Jekyll caches were separate.
180
+ The owner smoke now creates a fresh unique fixture by default and refuses any
181
+ existing explicit `MCP_SMOKE_PROJECT`, preventing its fixture reset from touching
182
+ prior work. Prose check reports only one pre-existing informational long-sentence
183
+ finding in the publication-workflow paragraph; docs build and diff check pass.
184
+ Existing `:dev`, manual build and TIGIT local worker image IDs were rechecked and
185
+ preserved. Test image configuration IDs:
186
+
187
+ - `unaltraweb-owner-76-runtime:20260929`: `sha256:df6a55511b5e40c3fde8a10326263cc7a87723b8c090e8333d64bf848c3a853f`.
188
+ - `unaltraweb-owner-76-mcp:20260929`: `sha256:dd795ad90dc30547ae09c9175e06f296af1072f4277a2b02d8122000c7378e07`.
189
+ - Final source gate `unaltraweb-owner-76-mcp:20260929-final`: `sha256:1ef3d4e7ac5fbb60b66103415c87c30bb432802016ccc30b46b896de3bf35a1b`.
190
+ - `unaltraweb-owner-76-pdf:20260929`: `sha256:6c7c5411b4f570fcdcfb1f984a9d9a0e36cc54d606bf459ca67929af0bc0d145`.
191
+
192
+ The host `make gem-check` initially refused the absent semver-tagged base image;
193
+ the exact gate then passed inside the isolated owner image using its local Ruby.
194
+ `make distribution-release-check` correctly **fails** at this post-release source
195
+ checkpoint: it is not the receipt-only child of the recorded candidate source.
196
+ The published receipt is preserved rather than rewritten to make a source
197
+ preparation look publication-ready.
198
+
199
+ Acceptance driver (fresh output directories are required):
200
+
201
+ ```bash
202
+ L=/tmp/opencode/unaltraweb-owner-20260929/venv/bin/unaltraweb-mcp-docker
203
+ MCP05=ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:36d17edbade77edb40a687f6a744203c6329acb33fbc2eb255e88d9ff1a42c98
204
+ MCP04=ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:389bc585cdb4fc89d3372f4896a55fe26e15df38b46bc114ce44fdb3f1c8deb9
205
+ python3 test/distributed_launcher_smoke.py --launcher "$L" --image "$MCP05" \
206
+ --output /tmp/opencode/unaltraweb-owner-20260929/released-050-full
207
+ python3 test/distributed_launcher_smoke.py --launcher "$L" --image "$MCP04" \
208
+ --profiles unaltremanual --host-build \
209
+ --output /tmp/opencode/unaltraweb-owner-20260929/released-040-manual-full
210
+ python3 test/distributed_launcher_smoke.py --launcher "$L" --image "$MCP05" \
211
+ --scaffold-image "$MCP04" --profiles unaltremanual \
212
+ --output /tmp/opencode/unaltraweb-owner-20260929/mixed-fresh-040-050
213
+ python3 test/distributed_launcher_smoke.py --launcher "$L" --image "$MCP05" \
214
+ --scaffold-image "$MCP04" --profiles unaltremanual --host-build \
215
+ --output /tmp/opencode/unaltraweb-owner-20260929/mixed-retained-lock-final
216
+ ```
217
+
218
+ The final command intentionally reproduces the retained-lock failure in a new
219
+ output directory. The original `mixed-040-050` run used the driver's then-default
220
+ host-build step before that option was made explicit.
221
+
222
+ ## Range adoption points, gaps and next action for the hub
223
+
224
+ 1. **Install/discovery:** map the installed `path` and existing descriptor into the
225
+ accepted hub contract. Do not treat a source checkout's `make mcp-check` (which
226
+ builds) as equivalent to the installed profile's prepared-image check.
227
+ 2. **Compatibility versus exact selection:** `mcp_dependencies` and
228
+ `distribution.py:companion_dependency_requirements` currently select exact
229
+ provider versions/wheels; `site_tools.py` receipt validation also requires exact
230
+ provider version/release. A future accepted range must be tested there without
231
+ weakening request/input/artifact hashes or equating wheel hashes with images.
232
+ 3. **Consumer tuple:** `consumer_integration`, scaffold rendering/update planning,
233
+ the Make image selection and native Gemfile pins remain atomic reviewed
234
+ selections. Preserve their concrete revision and hashes after any future range
235
+ resolution. Fix/test runtime-specific generated Bundler-cache handling before
236
+ promising transparent switching across the mixed 0.4.0/0.5.0 tuple.
237
+ 4. **Coverage:** tested endpoints are 0.4.0 and 0.5.0, not a promised `[0.4,0.6)`
238
+ interval. The other three 0.4.0 profiles, non-Linux/amd64 platforms, independent
239
+ new compute/capture/companion rendering and stable publication channels were
240
+ not covered by this preparation.
241
+ 5. **Delivery:** review the owner diff, assign a new package version through the
242
+ normal release procedure, bind final artifacts to new receipt evidence, then
243
+ adopt the installed profile in a separate synthetic hub pilot. The full 0.5.0
244
+ image can remain selected. Client activation and each real-manual upgrade need
245
+ their own later owner session.
246
+
247
+ TIG/TIGIT retain 0.4.0 build defaults; Geodisseny retains 0.5.0. Their sources,
248
+ generated artifacts, registrations and worker identities were not migrated.
249
+ During this session the author separately approved storage cleanup: five stopped
250
+ old test containers were removed, and exact unused/unshared unaltraweb cache IDs
251
+ were submitted to BuildKit pruning (131.1 kB actually reclaimed). Active stdio
252
+ containers, manual previews, images and volumes were not removed by that cleanup.
253
+ Other concurrent Docker activity changed disk availability; its reclaimed space
254
+ is not attributed to this preparation.
@@ -1,6 +1,11 @@
1
1
  # Published visual companions: native acceptance and release handoff
2
2
 
3
3
  Tracking: [issue 67](https://github.com/dosquartsdedocs/unaltraweb/issues/67).
4
+ Publication update: the acceptance is included in
5
+ [unaltraweb v0.5.0](https://github.com/dosquartsdedocs/unaltraweb/releases/tag/v0.5.0),
6
+ receipt `3fa855378dcc61dc7b84b8b03812698042e02fed`, and the post-release launcher
7
+ selects MCP digest `36d17edbade77edb40a687f6a744203c6329acb33fbc2eb255e88d9ff1a42c98`.
8
+ The preparation notes and original 0.4.0 image below retain their historical context.
4
9
  Owner branch: `fix/67-published-visual-companions`, based on
5
10
  `bfabaa2738ddd24725f5e386d60b58a9a8294316` after an eligible primary-checkout
6
11
  preflight. This is next-release source, not a replacement for published core 0.4.0.
@@ -62,7 +62,7 @@ def build(output: Path, *, source: Path = ROOT) -> str:
62
62
 
63
63
  def inspect_gem(path: Path) -> None:
64
64
  editorial_files = ["scripts/editorial_check.py", "src/unaltraweb_mcp/editorial.py", "src/unaltraweb_mcp/editorial_sources.py"]
65
- inspection_files = editorial_files + ["scripts/image_background_check.py", "src/unaltraweb_mcp/image_backgrounds.py",
65
+ inspection_files = editorial_files + ["src/unaltraweb_mcp/bundler_runtime.py", "scripts/image_background_check.py", "src/unaltraweb_mcp/image_backgrounds.py",
66
66
  "src/unaltraweb_mcp/image_probe.py", "src/unaltraweb_mcp/processes.py"]
67
67
  with tarfile.open(path, mode="r") as package:
68
68
  data_member = package.extractfile("data.tar.gz")
@@ -35,6 +35,9 @@ def main() -> int:
35
35
  source,
36
36
  ignore=shutil.ignore_patterns(
37
37
  ".git",
38
+ ".venv",
39
+ ".cache",
40
+ ".jekyll-cache",
38
41
  "build",
39
42
  "dist",
40
43
  "*.egg-info",
@@ -58,6 +61,21 @@ def main() -> int:
58
61
  raise RuntimeError(f"Expected one wheel, found: {wheels}")
59
62
  with zipfile.ZipFile(wheels[0]) as archive:
60
63
  names = archive.namelist()
64
+ launcher_prefix = f"{wheels[0].name.split('-')[0]}-{wheels[0].name.split('-')[1]}.data/data/share/unaltraweb-launcher/"
65
+ launcher_files = {
66
+ "Makefile": "packaging/launcher/Makefile",
67
+ "mcp-factory.yml": "mcp-factory.yml",
68
+ **{f"scripts/{name}.sh": f"scripts/{name}.sh" for name in (
69
+ "unaltraweb-mcp-bootstrap", "unaltraweb-mcp-project-id",
70
+ "unaltraweb-mcp-cleanup", "unaltraweb-docker-mount",
71
+ )},
72
+ }
73
+ actual_launcher = {name.removeprefix(launcher_prefix) for name in names if name.startswith(launcher_prefix)}
74
+ if actual_launcher != set(launcher_files):
75
+ raise RuntimeError(f"Wheel launcher closure mismatch: {actual_launcher}")
76
+ for installed_path, source_path in launcher_files.items():
77
+ if archive.read(launcher_prefix + installed_path) != (ROOT / source_path).read_bytes():
78
+ raise RuntimeError(f"Wheel launcher asset differs from source: {installed_path}")
61
79
  forbidden_roots = {"docs", "scripts", "_layouts", "_includes", "_sass"}
62
80
  leaked = []
63
81
  for name in names:
@@ -71,6 +89,7 @@ def main() -> int:
71
89
  raise RuntimeError(f"Factory assets leaked into wheel: {leaked}")
72
90
  required = [
73
91
  "unaltraweb_mcp/calibre_import.py",
92
+ "unaltraweb_mcp/bundler_runtime.py",
74
93
  "unaltraweb_mcp/component-contract.json",
75
94
  "unaltraweb_mcp/component-contract.schema.json",
76
95
  "unaltraweb_mcp/manual_pdf_preview.py",
@@ -105,6 +124,33 @@ def main() -> int:
105
124
  python = environment / "bin/python"
106
125
  cli = environment / "bin/unaltraweb-mcp"
107
126
  run([str(python), "-m", "pip", "install", "--no-deps", str(wheels[0])], cwd=temp, env=env)
127
+ docker_cli = environment / "bin/unaltraweb-mcp-docker"
128
+ launcher = Path(run([str(docker_cli), "path"], cwd=temp).stdout.strip())
129
+ if launcher != environment / "share/unaltraweb-launcher":
130
+ raise RuntimeError(f"Launcher did not resolve its installed wheel data: {launcher}")
131
+ if run([str(docker_cli), "manifest"], cwd=temp).stdout != (ROOT / "mcp-factory.yml").read_text(encoding="utf-8"):
132
+ raise RuntimeError("Installed native manifest drifted from discovery contract")
133
+ no_consumer_env = {key: value for key, value in env.items() if key not in {"MCP_CONSUMER_WORKSPACE", "UNALTRAWEB_PROJECT"}}
134
+ refused = run([str(docker_cli), "serve"], cwd=temp, env=no_consumer_env, expected=2)
135
+ if "requires --project" not in refused.stderr:
136
+ raise RuntimeError("Installed launcher accepted an implicit consumer")
137
+ fake_bin = temp / "fake-bin"
138
+ fake_bin.mkdir()
139
+ fake_docker = fake_bin / "docker"
140
+ fake_docker.write_text("#!/bin/sh\n[ \"$1 $2\" = 'image inspect' ] || exit 99\n[ \"$5\" = \"$EXPECTED_IMAGE\" ] || exit 98\nprintf 'sha256:fixture\\n'\n", encoding="utf-8")
141
+ fake_docker.chmod(0o755)
142
+ expected_image = json.loads((ROOT / "src/unaltraweb_mcp/component-contract.json").read_text())["components"]["mcp"]["reference"]
143
+ launcher_env = {**no_consumer_env, "PATH": f"{fake_bin}:{env['PATH']}", "EXPECTED_IMAGE": expected_image}
144
+ launcher_env.pop("UNALTRAWEB_MCP_IMAGE", None)
145
+ launcher_env.pop("MCP_RELEASE_IMAGE", None)
146
+ prepared_image = run([str(docker_cli), "prepare"], cwd=temp, env=launcher_env).stdout.strip()
147
+ if prepared_image != "sha256:fixture":
148
+ raise RuntimeError("Installed preparation did not use the packaged bootstrap")
149
+ run(["make", "--silent", "--no-print-directory", "-C", str(launcher), "mcp-build"], cwd=temp, env=launcher_env)
150
+ run([str(docker_cli), "prepare", "--image", "explicit:override"], cwd=temp,
151
+ env={**launcher_env, "EXPECTED_IMAGE": "explicit:override", "UNALTRAWEB_MCP_IMAGE": "environment:override"})
152
+ run([str(docker_cli), "prepare"], cwd=temp,
153
+ env={**launcher_env, "EXPECTED_IMAGE": "environment:override", "UNALTRAWEB_MCP_IMAGE": "environment:override"})
108
154
  version = run([str(cli), "version"], cwd=temp).stdout.strip()
109
155
  doctor = json.loads(run([str(cli), "doctor"], cwd=temp).stdout)
110
156
  if not doctor["ok"] or doctor["mode"] != "wheel" or not doctor["limited"]:
@@ -3,15 +3,18 @@ set -eu
3
3
 
4
4
  usage() {
5
5
  cat <<'USAGE'
6
- Usage: unaltraweb-mcp-bootstrap [--project PATH] [--image IMAGE]
6
+ Usage: unaltraweb-mcp-bootstrap [--project PATH] [--image IMAGE] [--prepare|--check|--smoke]
7
7
 
8
8
  Start one Dockerized unaltraweb MCP stdio server for the selected workspace.
9
9
  If --project is omitted, MCP_CONSUMER_WORKSPACE is used, then the current directory.
10
+ --prepare inspects or pulls the selected image without mounting a consumer.
11
+ --check and --smoke require that image locally; neither builds nor pulls.
10
12
  USAGE
11
13
  }
12
14
 
13
- image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp:0.5.0}"
15
+ image="${UNALTRAWEB_MCP_IMAGE:-ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:36d17edbade77edb40a687f6a744203c6329acb33fbc2eb255e88d9ff1a42c98}"
14
16
  project="${MCP_CONSUMER_WORKSPACE:-${UNALTRAWEB_PROJECT:-}}"
17
+ operation=serve
15
18
 
16
19
  while [ "$#" -gt 0 ]; do
17
20
  case "$1" in
@@ -25,6 +28,11 @@ while [ "$#" -gt 0 ]; do
25
28
  image="$2"
26
29
  shift 2
27
30
  ;;
31
+ --prepare|--check|--smoke)
32
+ [ "$operation" = serve ] || { printf '%s\n' 'Select only one operation.' >&2; exit 2; }
33
+ operation="${1#--}"
34
+ shift
35
+ ;;
28
36
  -h|--help)
29
37
  usage
30
38
  exit 0
@@ -37,6 +45,28 @@ while [ "$#" -gt 0 ]; do
37
45
  esac
38
46
  done
39
47
 
48
+ resolve_image() {
49
+ if ! resolved_image="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
50
+ case "$operation" in
51
+ check|smoke)
52
+ printf 'Image is not prepared: %s. Run --prepare first.\n' "$image" >&2
53
+ exit 1
54
+ ;;
55
+ esac
56
+ docker pull "$image" >&2
57
+ resolved_image="$(docker image inspect --format '{{.Id}}' "$image")"
58
+ fi
59
+ }
60
+
61
+ if [ "$operation" != serve ]; then
62
+ resolve_image
63
+ case "$operation" in
64
+ prepare) printf '%s\n' "$resolved_image"; exit 0 ;;
65
+ check) exec docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp --entrypoint unaltraweb-mcp "$resolved_image" version ;;
66
+ smoke) exec docker run --rm --network none --user "$(id -u):$(id -g)" -e HOME=/tmp --entrypoint python3 "$resolved_image" /opt/unaltraweb/test/mcp_smoke.py ;;
67
+ esac
68
+ fi
69
+
40
70
  if [ -z "$project" ]; then
41
71
  project="$PWD"
42
72
  fi
@@ -80,10 +110,7 @@ if [ -S "$docker_socket" ]; then
80
110
  esac
81
111
  fi
82
112
 
83
- if ! resolved_image="$(docker image inspect --format '{{.Id}}' "$image" 2>/dev/null)"; then
84
- docker pull "$image" >/dev/null
85
- resolved_image="$(docker image inspect --format '{{.Id}}' "$image")"
86
- fi
113
+ resolve_image
87
114
  image_reference="$image"
88
115
  case "$image_reference" in
89
116
  ghcr.io/dosquartsdedocs/unaltraweb-mcp@sha256:*) ;;
@@ -75,7 +75,7 @@ def component_version_errors(contract: dict[str, Any]) -> list[str]:
75
75
  """Retain the real version of already-published, digest-pinned workers."""
76
76
  version = str(contract["release"]["version"])
77
77
  errors = []
78
- reusable_workers = {"compute_python", "compute_r", "web_capture"}
78
+ reusable_workers = {"compute_python", "compute_r", "web_capture", "manual_pdf"}
79
79
  for component_id in ["gem", "wheel", "runtime", "mcp", "compute_python", "compute_r", "web_capture", "manual_pdf"]:
80
80
  selected = contract["components"][component_id]
81
81
  if (component_id in reusable_workers and selected["kind"] == "container"
@@ -168,6 +168,20 @@ def validate(root: Path = ROOT) -> list[str]:
168
168
  errors.append("component contract schema does not select version 1")
169
169
 
170
170
  pyproject = tomllib.loads((root / "pyproject.toml").read_text(encoding="utf-8"))
171
+ launcher_files = {
172
+ "share/unaltraweb-launcher": ["packaging/launcher/Makefile", "mcp-factory.yml"],
173
+ "share/unaltraweb-launcher/scripts": [
174
+ "scripts/unaltraweb-mcp-bootstrap.sh", "scripts/unaltraweb-mcp-project-id.sh",
175
+ "scripts/unaltraweb-mcp-cleanup.sh", "scripts/unaltraweb-docker-mount.sh",
176
+ ],
177
+ }
178
+ if pyproject.get("tool", {}).get("setuptools", {}).get("data-files") != launcher_files:
179
+ errors.append("wheel must distribute exactly the native Docker launcher and its helper closure")
180
+ launcher_image = make_value(root / "Makefile", "MCP_RELEASE_IMAGE")
181
+ if not re.fullmatch(r"ghcr\.io/dosquartsdedocs/unaltraweb-mcp@sha256:[0-9a-f]{64}", launcher_image):
182
+ errors.append("MCP_RELEASE_IMAGE must select a full GHCR MCP digest")
183
+ if make_value(root / "packaging/launcher/Makefile", "MCP_RELEASE_IMAGE") != component_reference("mcp"):
184
+ errors.append("installed launcher must select its own package release image")
171
185
  project = pyproject.get("project", {})
172
186
  if "version" in project or project.get("dynamic") != ["version"]:
173
187
  errors.append("pyproject version must be derived from the package component contract")
@@ -242,7 +256,7 @@ def validate(root: Path = ROOT) -> list[str]:
242
256
  pinned_text = {
243
257
  "scripts/computations/render.py": [component_reference("compute_python"), component_reference("compute_r")],
244
258
  "scripts/web_captures/render.py": [component_reference("web_capture")],
245
- "scripts/unaltraweb-mcp-bootstrap.sh": [component_reference("mcp")],
259
+ "scripts/unaltraweb-mcp-bootstrap.sh": [launcher_image],
246
260
  "Dockerfile.mcp": [component_reference("runtime")],
247
261
  }
248
262
  for relative, references in pinned_text.items():
@@ -485,6 +499,11 @@ def publish_ref_errors(
485
499
  if not isinstance(component, dict):
486
500
  errors.append(f"unknown publish component: {component_id}")
487
501
  continue
502
+ if (component_id == "manual_pdf" and component.get("release_status") == "released"
503
+ and re.fullmatch(r"ghcr\.io/dosquartsdedocs/unaltraweb-manual-pdf@sha256:[0-9a-f]{64}", str(component.get("reference", "")))):
504
+ # Already-published, unchanged worker. It is not a new candidate for
505
+ # this coordinated core release and receives no new semver identity.
506
+ continue
488
507
  if str(component.get("version") or "") != version:
489
508
  errors.append(f"{component_id} publish version does not match distribution release {version}")
490
509
  if str(component.get("release") or "") != release_tag: