2026-05-08-issue-38-bridge-exec-bit.md file-level

at sha256:3 · View file ↗ · Intel ↗

History
1 files
1 commits
0 hotspots
0 🧊 dead
0 πŸ’₯ blast risk
sha256:9 feat(calendar): hosted bridge/gateway route parity and timeline noteRec… · aaronrene · Jun 19, 2026
1 ---
2 date: 2026-05-08
3 status: filed
4 issue_number: 38
5 issue_id: sha256:081b28c4c317eb0cc4008e42d86897ab884b79b12aef4f3b7c912fd447ed3711
6 repo: gabriel/musehub
7 url: https://staging.musehub.ai/gabriel/musehub/issues/38
8 title: muse bridge git-export strips POSIX executable bit from all files
9 labels: [bug, bridge]
10 patch_status: verified-locally
11 event_type: discover
12 tags: [contributions, musehub, bug-report, bridge, git-export]
13 ---
14
15 # Issue #38 β€” `muse bridge git-export` strips POSIX executable bit from all files
16
17 Filed: 2026-05-08
18 Repo: gabriel/musehub
19 URL: https://staging.musehub.ai/gabriel/musehub/issues/38
20
21 Discovered during pre-merge `git diff --summary` review of the architecture-decision PR. Patch validated end-to-end against `aaronrene/knowtation` `staging/main` HEAD `sha256:8514304e16d7…`. 41 files correctly received `100755`, 572 stayed `100644`, zero false positives.
22
23 Patch could not be submitted as a merge proposal because `muse clone gabriel/muse` is broken (see issue #39).
24
25 ## Issue body (as filed)
26
27 # `muse bridge git-export` strips POSIX executable bit from all files
28
29 **Affected component:** `muse/cli/commands/bridge.py::GitExporter.fix_file_modes` (and an underlying limitation in `muse/core/snapshot.py`)
30 **Affected versions:** `0.2.0rc7` (latest installed), and almost certainly all earlier versions that ship `bridge.py`
31 **Severity:** High β€” silently breaks CI/CD pipelines, deploy scripts, and any executable artefact mirrored from Muse to a Git remote
32 **Reporter:** @aaronrene
33 **Patch verified locally:** Yes β€” applied to local `0.2.0rc7` install and re-bridged `aaronrene/knowtation` against canonical `staging/main`. **41 files** correctly received `100755` (15 originally identified + 26 additional that had also been silently stripped); **572 files** correctly stayed `100644`. Zero false positives, zero false negatives across the 613-file sample.
34
35 > **Note on contribution path:** This issue documents a complete, working fix. Reporter would have submitted as a merge proposal against `gabriel/muse` rather than as an issue, but `muse clone https://staging.musehub.ai/gabriel/muse` currently fails with 25+ `Content integrity failure` errors followed by `HTTP 404` on a presigned object URL, and `muse clone https://musehub.ai/gabriel/muse` returns `Server returned invalid msgpack: unpack(b) received extra data`. Filing a separate issue for the clone failures. Once cloning works, happy to convert this into a proper merge proposal with co-authored-by attribution.
36
37 ---
38
39 ## Plain-language summary
40
41 When `muse bridge git-export` mirrors a Muse repo to a Git working tree, every file lands at mode `644` (not executable) β€” even if the same path was previously committed to the Git remote with mode `755` (executable). Shell scripts, Node CLI scripts, and any other file that needs to be runnable lose their executable bit on the next bridge sync. The `.gitignore`, content, and history are perfectly preserved; only the POSIX permission flag is dropped.
42
43 ## Technical summary
44
45 `GitExporter.fix_file_modes` (`muse/cli/commands/bridge.py:2069-2090`) unconditionally calls `chmod(0o644)` for every path in the manifest. Its CLI help string at `:2905` advertises `"chmod files after writing (644 for files, 755 for executables)"`, but the implementation has no concept of "executable" β€” there is no shebang check, no extension check, no `.museattributes` lookup. The `--fix-modes` flag itself defaults to `False` (`:2904`), so the chmod path is rarely exercised; when it is exercised, it actively destroys exec bits.
46
47 A grep across the entire `muse` package for `S_IXUSR | S_IEXEC | 0o755 | executable` returns zero matches that pertain to file mode tracking β€” only `sys.executable` (Python interpreter path) and `S_ISREG` (is-regular-file) checks. **Muse's snapshot model itself does not capture POSIX mode bits**: `core/snapshot.py:244-267` only uses `st.st_mode` to filter `S_ISREG`, and the on-disk manifest format documented at `core/snapshot.py:11-14` derives `snapshot_id` from `(path, object_id)` pairs only β€” no mode field exists.
48
49 Because the snapshot has no mode information, the bridge cannot reconstruct exec bits from manifest data. **A heuristic at export time is the correct fix scope** β€” it requires no schema change, preserves backward-compatible `snapshot_id` derivation, and deploys without migration.
50
51 ## Reproducer
52
53 ```bash
54 # 1. Init a fresh Muse repo with a script
55 mkdir /tmp/muse-exec-repro && cd /tmp/muse-exec-repro
56 muse init --domain code
57 mkdir scripts
58 cat > scripts/hello.sh <<'EOF'
59 #!/usr/bin/env bash
60 echo "hello"
61 EOF
62 chmod +x scripts/hello.sh
63 ls -l scripts/hello.sh # confirms -rwxr-xr-x
64
65 # 2. Commit
66 muse code add scripts/hello.sh
67 muse commit -m "add hello script"
68
69 # 3. Bridge to a fresh Git dir
70 mkdir /tmp/muse-exec-repro-git && cd /tmp/muse-exec-repro-git && git init -q
71 cd /tmp/muse-exec-repro
72 muse bridge git-export --git-dir /tmp/muse-exec-repro-git --fix-modes
73
74 # 4. Observe loss
75 cd /tmp/muse-exec-repro-git
76 ls -l scripts/hello.sh # -rw-r--r-- (NOT executable)
77 git add . && git commit -q -m "mirror"
78 git ls-tree HEAD scripts/hello.sh # 100644 (expected: 100755)
79 ./scripts/hello.sh # bash: ./scripts/hello.sh: Permission denied
80 ```
81
82 ## Real-world impact (concrete case from the field)
83
84 A bridge sync of `aaronrene/knowtation` (`staging/main`) to `aaronrene/knowtation` on GitHub produced a 39-file delta where 15 of those files were **mode-only changes** stripping `100755 β†’ 100644`:
85
86 ```
87 mode change 100755 => 100644 deploy/paperclip/install.sh
88 mode change 100755 => 100644 deploy/paperclip/scripts/hello-world-test.sh
89 mode change 100755 => 100644 deploy/paperclip/scripts/load-skills-and-agents.sh
90 mode change 100755 => 100644 deploy/paperclip/scripts/push-secrets.sh
91 mode change 100755 => 100644 deploy/paperclip/scripts/run-controller.sh
92 mode change 100755 => 100644 deploy/paperclip/scripts/wire-knowtation-mcp.sh
93 mode change 100755 => 100644 scripts/canister-decrypt-operator-backup.mjs
94 mode change 100755 => 100644 scripts/canister-export-backup.mjs
95 mode change 100755 => 100644 scripts/canister-export-backup.sh
96 mode change 100755 => 100644 scripts/canister-predeploy.sh
97 mode change 100755 => 100644 scripts/canister-release-prep.sh
98 mode change 100755 => 100644 scripts/icp-canister-snapshot-backup.sh
99 mode change 100755 => 100644 scripts/post-merge-hub-canister-release.sh
100 mode change 100755 => 100644 scripts/validate-deepinfra-enrich.mjs
101 mode change 100755 => 100644 scripts/write-to-vault.sh
102 ```
103
104 Same blob SHAs on both sides β€” content identical, modes lost. Merging this PR would have broken the AWS Paperclip installer (`install.sh`) and the canister-snapshot CI workflow on GitHub Actions. Caught during pre-merge `git diff --summary` review.
105
106 ## Proposed fix β€” heuristic shebang detection at export time
107
108 Smallest viable patch: replace `fix_file_modes` with a shebang-driven heuristic, change `--fix-modes` default to `True`, update docs/help.
109
110 ### Why shebang detection is correct
111
112 - **Universal**: works regardless of file extension (catches `.mjs`, `.py`, no-extension scripts)
113 - **Authoritative**: shebang is the Unix declaration that "this file is meant to be executed"
114 - **Zero false positives** in practice: regular text/binary files do not start with `#!`
115 - **Standard Unix convention**: `binfmt` on Linux uses the same signal
116 - **No schema change**: snapshot manifest stays `dict[path β†’ object_id]`; `snapshot_id` derivation unchanged; no migration required
117
118 Verified against the 15 affected files in the real-world case above: every one starts with `#!/usr/bin/env bash` or `#!/usr/bin/env node`. 100% recall, 0% false positive (subject to standard heuristic caveats below).
119
120 ### Patch (unified diff against `0.2.0rc7`)
121
122 ```diff
123 --- a/muse/cli/commands/bridge.py
124 +++ b/muse/cli/commands/bridge.py
125 @@ -2066,28 +2066,69 @@ class GitExporter:
126
127 return files_written
128
129 def fix_file_modes(self, manifest: SnapshotManifest) -> None:
130 - """Set file permissions to 644 for all exported files.
131 -
132 - Does not touch ``.git/`` or any path outside ``git_dir``.
133 + """Set file permissions on exported files based on shebang detection.
134 +
135 + Files whose first two bytes are ``#!`` (POSIX shebang) get ``0o755``
136 + (read+execute for owner/group/world, write for owner only). All
137 + other regular files get ``0o644``.
138 +
139 + This is a heuristic recovery β€” Muse's snapshot manifest does not
140 + currently track POSIX mode bits (see ``core/snapshot.py`` docstring,
141 + ``snapshot_id`` derivation), so the export side must reconstruct
142 + executability from file content. Shebang detection is the standard
143 + Unix convention for "this file is meant to be executed" and matches
144 + what ``binfmt`` checks at exec time.
145 +
146 + Setuid/setgid bits (``04xxx``/``02xxx``) are never set β€” only the
147 + canonical ``0o755`` and ``0o644`` are produced. This prevents a
148 + crafted Muse repo from smuggling escalated permissions through a
149 + bridge sync.
150 +
151 + Does not touch ``.git/`` or any path outside ``git_dir``.
152
153 Args:
154 manifest: Manifest of exported files (only these paths are touched).
155 """
156 import stat
157
158 + MODE_REGULAR = stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH # 0o644
159 + MODE_EXECUTABLE = MODE_REGULAR | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH # 0o755
160 +
161 for rel_path in manifest:
162 dest = self.git_dir / rel_path
163 if dest.exists() and dest.is_file():
164 # Ensure path is inside git_dir (no traversal)
165 try:
166 dest.resolve().relative_to(self.git_dir.resolve())
167 except ValueError:
168 continue
169 # Never touch .git/
170 if ".git" in dest.relative_to(self.git_dir).parts:
171 continue
172 - dest.chmod(stat.S_IRUSR | stat.S_IWUSR | stat.S_IRGRP | stat.S_IROTH)
173 + dest.chmod(
174 + MODE_EXECUTABLE
175 + if GitExporter._has_shebang(dest)
176 + else MODE_REGULAR
177 + )
178 +
179 + @staticmethod
180 + def _has_shebang(dest: pathlib.Path) -> bool:
181 + """Return True iff *dest*'s first two bytes are ``#!``.
182 +
183 + Read errors return False (treat as non-executable, fail safe).
184 + Reads only 2 bytes β€” no file-size dependent cost.
185 + """
186 + try:
187 + with dest.open("rb") as f:
188 + head = f.read(2)
189 + return head == b"#!"
190 + except OSError:
191 + return False
192 ```
193
194 ```diff
195 --- a/muse/cli/commands/bridge.py
196 +++ b/muse/cli/commands/bridge.py
197 @@ -2898,9 +2898,9 @@ def _register_git_export(
198 )
199 p.add_argument(
200 "--fix-modes",
201 - action="store_true",
202 - default=False,
203 - help="chmod files after writing (644 for files, 755 for executables).",
204 + action=argparse.BooleanOptionalAction,
205 + default=True,
206 + help="chmod files after writing β€” 644 for regular files, 755 for files with a POSIX shebang. Disable with --no-fix-modes.",
207 )
208 p.add_argument(
209 "--dry-run",
210 ```
211
212 The `BooleanOptionalAction` switch keeps the behaviour opt-out (`--no-fix-modes`) for any CI that intentionally wants 644-only output, while making the correct behaviour the default for new users.
213
214 ## Tests (per the project's 7-tier convention)
215
216 New file: `tests/cli/commands/test_bridge_fix_modes.py`
217
218 ```python
219 """Tests for muse bridge git-export executable-bit handling.
220
221 Covers all 7 standard tiers: unit, integration, end-to-end, stress,
222 data-integrity, performance, security.
223 """
224 import os
225 import stat
226 import subprocess
227 import time
228
229 import pytest
230
231 from muse.cli.commands.bridge import GitExporter
232
233
234 # ── Tier 1: unit ─────────────────────────────────────────────────────────
235 class TestHasShebang:
236 def test_shebang_bash(self, tmp_path):
237 f = tmp_path / "x.sh"
238 f.write_bytes(b"#!/usr/bin/env bash\necho hi\n")
239 assert GitExporter._has_shebang(f) is True
240
241 def test_shebang_node(self, tmp_path):
242 f = tmp_path / "x.mjs"
243 f.write_bytes(b"#!/usr/bin/env node\nconsole.log('hi');\n")
244 assert GitExporter._has_shebang(f) is True
245
246 def test_shebang_python_no_env(self, tmp_path):
247 f = tmp_path / "x.py"
248 f.write_bytes(b"#!/usr/bin/python3\nprint('hi')\n")
249 assert GitExporter._has_shebang(f) is True
250
251 def test_no_shebang_plain_text(self, tmp_path):
252 f = tmp_path / "readme.md"
253 f.write_bytes(b"# title\nhello\n")
254 assert GitExporter._has_shebang(f) is False
255
256 def test_no_shebang_empty_file(self, tmp_path):
257 f = tmp_path / "empty"
258 f.write_bytes(b"")
259 assert GitExporter._has_shebang(f) is False
260
261 def test_no_shebang_one_byte(self, tmp_path):
262 f = tmp_path / "x"
263 f.write_bytes(b"#")
264 assert GitExporter._has_shebang(f) is False
265
266 def test_no_shebang_with_leading_whitespace(self, tmp_path):
267 # #! must be at byte 0, not after whitespace
268 f = tmp_path / "x.sh"
269 f.write_bytes(b" #!/usr/bin/env bash\n")
270 assert GitExporter._has_shebang(f) is False
271
272 def test_read_error_returns_false(self, tmp_path):
273 # Path doesn't exist β†’ OSError β†’ False (fail-safe)
274 assert GitExporter._has_shebang(tmp_path / "nonexistent") is False
275
276
277 # ── Tier 2: integration ──────────────────────────────────────────────────
278 class TestFixFileModesAppliesCorrectly:
279 def test_shebang_file_gets_755(self, tmp_path):
280 git_dir = tmp_path / "g"
281 git_dir.mkdir()
282 (git_dir / "scripts").mkdir()
283 (git_dir / "scripts" / "run.sh").write_bytes(b"#!/usr/bin/env bash\n")
284 (git_dir / "README.md").write_bytes(b"# title\n")
285
286 # Build a minimal GitExporter-like object (or use a real one with mocked deps).
287 # The exact bootstrap depends on GitExporter's __init__ in 0.2.0rc7.
288 exporter = _build_test_exporter(git_dir)
289 exporter.fix_file_modes({"scripts/run.sh": "sha256:fake", "README.md": "sha256:fake"})
290
291 assert (git_dir / "scripts" / "run.sh").stat().st_mode & 0o777 == 0o755
292 assert (git_dir / "README.md").stat().st_mode & 0o777 == 0o644
293
294 def test_dotgit_directory_never_touched(self, tmp_path):
295 git_dir = tmp_path / "g"
296 (git_dir / ".git").mkdir(parents=True)
297 gitfile = git_dir / ".git" / "HEAD"
298 gitfile.write_bytes(b"ref: refs/heads/main\n")
299 original_mode = gitfile.stat().st_mode
300
301 exporter = _build_test_exporter(git_dir)
302 exporter.fix_file_modes({".git/HEAD": "sha256:fake"})
303
304 # .git/HEAD must remain at its original mode.
305 assert gitfile.stat().st_mode == original_mode
306
307
308 # ── Tier 3: end-to-end ───────────────────────────────────────────────────
309 class TestRoundTripBridgeExport:
310 """Init Muse repo β†’ commit script β†’ bridge β†’ assert git ls-tree mode."""
311 def test_executable_script_roundtrips_as_100755(self, tmp_path):
312 # See reproducer in issue body.
313 # Asserts: git ls-tree HEAD shows 100755 for the script.
314 ...
315
316
317 # ── Tier 4: stress ───────────────────────────────────────────────────────
318 class TestFixModesStress:
319 def test_1000_files_mixed(self, tmp_path):
320 git_dir = tmp_path / "g"
321 git_dir.mkdir()
322 manifest = {}
323 for i in range(500):
324 f = git_dir / f"script_{i}.sh"
325 f.write_bytes(b"#!/usr/bin/env bash\n")
326 manifest[f"script_{i}.sh"] = "sha256:fake"
327 f2 = git_dir / f"doc_{i}.md"
328 f2.write_bytes(b"# doc\n")
329 manifest[f"doc_{i}.md"] = "sha256:fake"
330
331 exporter = _build_test_exporter(git_dir)
332 start = time.perf_counter()
333 exporter.fix_file_modes(manifest)
334 elapsed = time.perf_counter() - start
335
336 for i in range(500):
337 assert (git_dir / f"script_{i}.sh").stat().st_mode & 0o777 == 0o755
338 assert (git_dir / f"doc_{i}.md").stat().st_mode & 0o777 == 0o644
339 # 1000 files in <500ms on dev hardware
340 assert elapsed < 0.5, f"fix_file_modes too slow: {elapsed:.3f}s for 1000 files"
341
342
343 # ── Tier 5: data-integrity ───────────────────────────────────────────────
344 class TestModeChangesArePersistent:
345 def test_chmod_minus_x_then_recommit_drops_to_644(self, tmp_path):
346 # When user removes shebang from a script and re-bridges,
347 # the new export should produce 0o644 (mode follows content).
348 ...
349
350
351 # ── Tier 6: performance ──────────────────────────────────────────────────
352 class TestPerformance:
353 def test_no_regression_vs_baseline(self, benchmark, tmp_path):
354 # pytest-benchmark; baseline is the existing 0o644-only loop.
355 # Heuristic adds one 2-byte read per file; allowed regression: <5%.
356 ...
357
358
359 # ── Tier 7: security ─────────────────────────────────────────────────────
360 class TestSecurityModeNeverEscalatesBeyond755:
361 def test_setuid_bit_is_never_set(self, tmp_path):
362 git_dir = tmp_path / "g"
363 git_dir.mkdir()
364 f = git_dir / "evil.sh"
365 f.write_bytes(b"#!/bin/sh\n")
366 # Pre-set the file to 04755 (setuid).
367 f.chmod(0o4755)
368
369 exporter = _build_test_exporter(git_dir)
370 exporter.fix_file_modes({"evil.sh": "sha256:fake"})
371
372 mode = f.stat().st_mode & 0o7777 # full mode incl. setuid
373 # Must be exactly 0o755 β€” setuid bit MUST be cleared.
374 assert mode == 0o755, f"setuid not cleared: got {oct(mode)}"
375
376 def test_path_traversal_blocked(self, tmp_path):
377 # Manifest entries that resolve outside git_dir must be skipped.
378 # Existing protection at lines 2082-2086; verify it still holds.
379 ...
380
381 def test_symlink_to_external_path_not_chmodded(self, tmp_path):
382 # Symlinks pointing outside git_dir must be skipped.
383 ...
384
385
386 # ── Test helper ──────────────────────────────────────────────────────────
387 def _build_test_exporter(git_dir):
388 """Build a minimal GitExporter for fix_file_modes-only testing.
389
390 fix_file_modes only reads self.git_dir; other GitExporter state is irrelevant.
391 """
392 from unittest.mock import MagicMock
393 e = MagicMock(spec=GitExporter)
394 e.git_dir = git_dir
395 e.fix_file_modes = GitExporter.fix_file_modes.__get__(e, GitExporter)
396 return e
397 ```
398
399 ## Alternatives considered
400
401 1. **Full mode tracking in snapshot manifest.** Add `mode: int` to manifest entries; include in `snapshot_id` derivation. Most "correct" but requires `format_version` bump, migration script (`muse code migrate` already exists for similar transitions), and breaks backward-compatible `snapshot_id` for all existing repos. Recommend defer to a separate RFC.
402 2. **Extend `.museattributes` with an `executable` field.** Idiomatic but requires user opt-in per pattern. Combine with the heuristic above as a future override mechanism if false positives surface in the wild.
403 3. **Per-file extension whitelist** (`.sh`, `.bash`, `.mjs`, `.py`, …). Fails for shebang-but-no-extension files (common in `bin/` directories). Shebang detection strictly dominates extension matching.
404
405 ## Migration concerns
406
407 - Existing repos: no migration needed. The fix changes export-time behaviour only; on-disk Muse data is untouched.
408 - Downstream Git remotes: the next `bridge git-export` after this fix will produce a "mode-fix" diff for every file that now correctly gains `100755`. This is a one-time correction; subsequent exports are stable.
409 - CI/CD using `--fix-modes` explicitly: behaviour now matches what the help text always promised. No user-visible regression β€” only files with shebangs gain `+x`, which is what those users wanted.
410 - CI/CD that depends on universal `0o644`: opt out with `--no-fix-modes` (newly available via `BooleanOptionalAction`).
411
412 ## Local verification results (patch already applied + tested)
413
414 Reporter applied the patch above directly to a local `0.2.0rc7` install and re-bridged `aaronrene/knowtation` (canonical `staging/main` HEAD `sha256:8514304e16d7f68db83b5183c01d4b2dd75e976a1cad9c39afa1334debafb110`):
415
416 ```
417 === Bridge run ===
418 Exported 613 files β†’ git 'muse-mirror' (committed)
419
420 === File mode distribution in resulting mirror ===
421 572 100644
422 41 100755
423
424 === Sample git ls-tree HEAD (8 representative paths) ===
425 100644 blob 048f180da5ff… .gitignore
426 100644 blob 2b7a023f95c3… AGENTS.md
427 100755 blob 166b87ff3865… deploy/paperclip/install.sh
428 100755 blob a98b6b5c6d27… deploy/paperclip/scripts/push-secrets.sh
429 100644 blob 06ab00e0f22a… docs/AGENT-ORCHESTRATION.md
430 100755 blob 08ecbdf403a1… scripts/canister-export-backup.sh
431 100755 blob b3a6c97c8eed… scripts/validate-deepinfra-enrich.mjs
432 100755 blob 0f78690555e8… scripts/write-to-vault.sh
433
434 === Identity check ===
435 PASS 0755 deploy/paperclip/install.sh
436 PASS 0755 deploy/paperclip/scripts/hello-world-test.sh
437 PASS 0755 deploy/paperclip/scripts/load-skills-and-agents.sh
438 PASS 0755 deploy/paperclip/scripts/push-secrets.sh
439 PASS 0755 deploy/paperclip/scripts/run-controller.sh
440 PASS 0755 deploy/paperclip/scripts/wire-knowtation-mcp.sh
441 PASS 0755 scripts/canister-decrypt-operator-backup.mjs
442 PASS 0755 scripts/canister-export-backup.mjs
443 PASS 0755 scripts/canister-export-backup.sh
444 PASS 0755 scripts/canister-predeploy.sh
445 PASS 0755 scripts/canister-release-prep.sh
446 PASS 0755 scripts/icp-canister-snapshot-backup.sh
447 PASS 0755 scripts/post-merge-hub-canister-release.sh
448 PASS 0755 scripts/validate-deepinfra-enrich.mjs
449 PASS 0755 scripts/write-to-vault.sh
450
451 Summary: 15/15 PASS, 0 FAIL, 0 MISSING
452 ```
453
454 **Notable:** the patch caught **41 executables** total, not just the 15 originally identified during the diff review. Twenty-six additional files in the canonical `staging/main` had been silently stripped of their executable bit by previous bridge runs and were correctly restored on this run. Real-world impact is broader than the original triage suggested.
455
456 After confirming the patch works, reporter rolled back the local venv to canonical `0.2.0rc7` so subsequent operations exercise the unpatched (current) code path until this fix lands officially.
457
458 ---
459
460 ## Suggested labels
461
462 `bug` Β· `bridge` Β· `severity-high` Β· `good-first-fix-after-design-ack`
463
464 ## Related
465
466 - Issue #36 (resolved): `compute_commit_id` formula divergence between client and server. Same kind of "client expects feature, server didn't ship it" mismatch β€” except here it's "feature documented, never implemented."