gabriel / muse public
test_cmd_sign.py python
629 lines 25.0 KB
Raw
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
1 """Tests for ``muse sign`` CLI subcommands.
2
3 Coverage:
4 - run_header: text output, JSON output, path+hub vs full URL
5 - run_verify: valid → exit 0, invalid sig → exit 1, expired → exit 1, JSON output
6 - run_whoami: env-var source, identity.toml source, JSON output
7 - run_curl: correct curl command format, Authorization header embedded
8 - run_payment: JSON output fields, domain separation from HTTP MSign
9 - _load_signing: exit 1 on missing identity, key-path override
10 """
11
12 from __future__ import annotations
13
14 import argparse
15 import io
16 import json
17 import sys
18 import time
19 import unittest
20 import unittest.mock
21 from collections.abc import Callable
22 from typing import TYPE_CHECKING
23 from unittest.mock import MagicMock, patch
24
25 from muse.core.types import b64url_decode, b64url_encode
26
27 if TYPE_CHECKING:
28 from muse.core.transport import SigningIdentity
29
30
31 # ---------------------------------------------------------------------------
32 # Shared helpers
33 # ---------------------------------------------------------------------------
34
35 def _make_signing(handle: str = "gabriel") -> "SigningIdentity":
36 """Return a real SigningIdentity with a fresh Ed25519 key."""
37 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
38 from muse.core.transport import SigningIdentity
39
40 return SigningIdentity(handle=handle, private_key=Ed25519PrivateKey.generate())
41
42
43 def _public_key_b64(signing: "SigningIdentity") -> str:
44 """Return URL-safe base64 public key (no padding) for a SigningIdentity."""
45 pub_raw = signing.private_key.public_key().public_bytes_raw() # type: ignore[attr-defined]
46 return b64url_encode(pub_raw)
47
48
49 def _make_header(signing: "SigningIdentity", method: str, url: str,
50 body: bytes = b"", ts: int = 1744000000) -> str:
51 """Build an MSign header value for testing verify subcommand."""
52 from muse.core.msign import build_msign_header
53 return build_msign_header(signing, method, url, body, ts=ts)
54
55
56 def _run_cmd(func: Callable[[argparse.Namespace], None], **kwargs: bool | int | str | None) -> argparse.Namespace:
57 """Build an argparse.Namespace from kwargs and call func(args)."""
58 args = argparse.Namespace(**kwargs)
59 func(args)
60 return args
61
62
63 # ---------------------------------------------------------------------------
64 # run_header
65 # ---------------------------------------------------------------------------
66
67 class TestRunHeader(unittest.TestCase):
68 def setUp(self) -> None:
69 self.signing = _make_signing("gabriel")
70
71 def _args(self, **kwargs: bool | int | str | None) -> argparse.Namespace:
72 defaults = dict(
73 method="POST",
74 path=None,
75 url="https://hub.example.com/gabriel/muse/push",
76 hub=None,
77 body=None,
78 body_file=None,
79 timestamp=1744000000,
80 key_path=None,
81 agent_id=None,
82 json_out=False,
83 )
84 defaults.update(kwargs)
85 return argparse.Namespace(**defaults)
86
87 def test_text_output_is_msign_header(self) -> None:
88 from muse.cli.commands.sign import run_header
89
90 args = self._args()
91 with patch("muse.cli.commands.sign._load_signing", return_value=self.signing), \
92 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
93 run_header(args)
94 output = mock_out.getvalue().strip()
95 assert output.startswith('MSign handle="gabriel"'), f"Unexpected: {output!r}"
96 assert "ts=1744000000" in output
97 assert ' sig="' in output
98
99 def test_json_output_fields(self) -> None:
100 from muse.cli.commands.sign import run_header
101
102 args = self._args(json_out=True)
103 with patch("muse.cli.commands.sign._load_signing", return_value=self.signing), \
104 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
105 run_header(args)
106 data = json.loads(mock_out.getvalue())
107 assert data["handle"] == "gabriel"
108 assert data["method"] == "POST"
109 assert data["signing_ts"] == 1744000000
110 assert data["algorithm"] == "ed25519"
111 assert "signature_b64" in data
112 assert "fingerprint" in data
113 assert "body_sha256" in data
114 assert data["header_value"].startswith("MSign")
115
116 def test_path_plus_hub_constructs_url(self) -> None:
117 """--path + --hub must construct a full URL for signing."""
118 from muse.cli.commands.sign import run_header
119
120 args = self._args(
121 path="/gabriel/muse/push",
122 url=None,
123 hub="https://staging.musehub.ai",
124 json_out=True,
125 )
126 with patch("muse.cli.commands.sign._load_signing", return_value=self.signing), \
127 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
128 run_header(args)
129 data = json.loads(mock_out.getvalue())
130 assert data["path"] == "/gabriel/muse/push"
131
132 def test_empty_body_uses_empty_sha256(self) -> None:
133 """No body → body_sha256 == sha256(b'') with sha256: prefix."""
134 import hashlib
135 from muse.cli.commands.sign import run_header
136 from muse.core.types import blob_id
137
138 args = self._args(json_out=True)
139 with patch("muse.cli.commands.sign._load_signing", return_value=self.signing), \
140 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
141 run_header(args)
142 data = json.loads(mock_out.getvalue())
143 expected = blob_id(b"")
144 assert data["body_sha256"] == expected
145
146 def test_inline_body_reflected_in_sha256(self) -> None:
147 """--body flag must be hashed into body_sha256 with sha256: prefix."""
148 from muse.cli.commands.sign import run_header
149 from muse.core.types import blob_id
150
151 args = self._args(body="hello world", json_out=True)
152 with patch("muse.cli.commands.sign._load_signing", return_value=self.signing), \
153 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
154 run_header(args)
155 data = json.loads(mock_out.getvalue())
156 expected = blob_id(b"hello world")
157 assert data["body_sha256"] == expected
158
159
160 # ---------------------------------------------------------------------------
161 # run_verify
162 # ---------------------------------------------------------------------------
163
164 class TestRunVerify(unittest.TestCase):
165 def setUp(self) -> None:
166 self.signing = _make_signing("gabriel")
167 self.pub_b64 = _public_key_b64(self.signing)
168 self.url = "https://hub.example.com/gabriel/muse/push"
169 self.ts = int(time.time())
170 self.header = _make_header(self.signing, "POST", self.url, b"", self.ts)
171
172 def _args(self, **kwargs: bool | int | str | None) -> argparse.Namespace:
173 defaults = dict(
174 header=self.header,
175 method="POST",
176 url=self.url,
177 public_key_b64=self.pub_b64,
178 body=None,
179 body_file=None,
180 max_age=30,
181 json_out=False,
182 )
183 defaults.update(kwargs)
184 return argparse.Namespace(**defaults)
185
186 def test_valid_header_exits_0(self) -> None:
187 from muse.cli.commands.sign import run_verify
188
189 args = self._args()
190 # Should not raise SystemExit.
191 run_verify(args)
192
193 def test_valid_header_json_output(self) -> None:
194 from muse.cli.commands.sign import run_verify
195
196 args = self._args(json_out=True)
197 with patch("sys.stdout", new_callable=io.StringIO) as mock_out:
198 run_verify(args)
199 data = json.loads(mock_out.getvalue())
200 assert data["valid"] is True
201 assert data["reason"] == "ok"
202
203 def test_wrong_public_key_exits_1(self) -> None:
204 from muse.cli.commands.sign import run_verify
205
206 other = _make_signing("other")
207 wrong_pub = _public_key_b64(other)
208 args = self._args(public_key_b64=wrong_pub)
209 with self.assertRaises(SystemExit) as cm:
210 run_verify(args)
211 assert cm.exception.code == 1
212
213 def test_wrong_public_key_json_valid_false(self) -> None:
214 from muse.cli.commands.sign import run_verify
215
216 other = _make_signing("other")
217 wrong_pub = _public_key_b64(other)
218 args = self._args(public_key_b64=wrong_pub, json_out=True)
219 with patch("sys.stdout", new_callable=io.StringIO) as mock_out:
220 with self.assertRaises(SystemExit):
221 run_verify(args)
222 data = json.loads(mock_out.getvalue())
223 assert data["valid"] is False
224 assert "reason" in data
225
226 def test_expired_timestamp_exits_1(self) -> None:
227 from muse.cli.commands.sign import run_verify
228
229 old_ts = int(time.time()) - 9999
230 old_header = _make_header(self.signing, "POST", self.url, b"", old_ts)
231 args = self._args(header=old_header, max_age=30)
232 with self.assertRaises(SystemExit) as cm:
233 run_verify(args)
234 assert cm.exception.code == 1
235
236 def test_malformed_header_exits_1(self) -> None:
237 from muse.cli.commands.sign import run_verify
238
239 args = self._args(header="Bearer garbage", json_out=True)
240 with patch("sys.stdout", new_callable=io.StringIO) as mock_out:
241 with self.assertRaises(SystemExit) as cm:
242 run_verify(args)
243 assert cm.exception.code == 1
244 data = json.loads(mock_out.getvalue())
245 assert data["valid"] is False
246
247 def test_method_mismatch_exits_1(self) -> None:
248 """A header signed for POST must not verify for GET."""
249 from muse.cli.commands.sign import run_verify
250
251 args = self._args(method="GET")
252 with self.assertRaises(SystemExit) as cm:
253 run_verify(args)
254 assert cm.exception.code == 1
255
256 def test_body_mismatch_exits_1(self) -> None:
257 """Changing the body after signing must invalidate the header."""
258 from muse.cli.commands.sign import run_verify
259
260 args = self._args(body="tampered")
261 with self.assertRaises(SystemExit) as cm:
262 run_verify(args)
263 assert cm.exception.code == 1
264
265 def test_custom_max_age_accepted(self) -> None:
266 """A very old timestamp is accepted if max_age is large enough."""
267 from muse.cli.commands.sign import run_verify
268
269 old_ts = int(time.time()) - 9999
270 old_header = _make_header(self.signing, "POST", self.url, b"", old_ts)
271 args = self._args(header=old_header, max_age=99999)
272 # Should not raise.
273 run_verify(args)
274
275
276 # ---------------------------------------------------------------------------
277 # run_curl
278 # ---------------------------------------------------------------------------
279
280 class TestRunCurl(unittest.TestCase):
281 def _args(self, **kwargs: bool | int | str | None) -> argparse.Namespace:
282 defaults = dict(
283 method="POST",
284 url="https://hub.example.com/gabriel/muse/push",
285 hub=None,
286 body=None,
287 body_file=None,
288 content_type="application/json",
289 timestamp=1744000000,
290 key_path=None,
291 agent_id=None,
292 json_out=False,
293 )
294 defaults.update(kwargs)
295 return argparse.Namespace(**defaults)
296
297 def test_curl_starts_with_curl(self) -> None:
298 from muse.cli.commands.sign import run_curl
299
300 signing = _make_signing()
301 args = self._args()
302 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
303 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
304 run_curl(args)
305 output = mock_out.getvalue()
306 assert output.strip().startswith("curl -X POST")
307
308 def test_authorization_header_embedded(self) -> None:
309 """The curl command must include the MSign Authorization header."""
310 from muse.cli.commands.sign import run_curl
311
312 signing = _make_signing()
313 args = self._args()
314 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
315 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
316 run_curl(args)
317 output = mock_out.getvalue()
318 assert "Authorization: MSign" in output
319
320 def test_url_appears_in_output(self) -> None:
321 from muse.cli.commands.sign import run_curl
322
323 signing = _make_signing()
324 url = "https://hub.example.com/gabriel/muse/push"
325 args = self._args(url=url)
326 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
327 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
328 run_curl(args)
329 assert url in mock_out.getvalue()
330
331 def test_body_file_uses_data_binary(self) -> None:
332 """--body-file must produce --data-binary @filename."""
333 from muse.cli.commands.sign import run_curl
334
335 signing = _make_signing()
336 args = self._args(body_file="/tmp/payload.bin")
337 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
338 patch("muse.cli.commands.sign._read_body", return_value=b"payload"), \
339 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
340 run_curl(args)
341 output = mock_out.getvalue()
342 assert "--data-binary @/tmp/payload.bin" in output
343
344 def test_inline_body_uses_data_flag(self) -> None:
345 """--body STRING must produce --data '...'."""
346 from muse.cli.commands.sign import run_curl
347
348 signing = _make_signing()
349 args = self._args(body='{"key": "value"}')
350 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
351 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
352 run_curl(args)
353 output = mock_out.getvalue()
354 assert "--data" in output
355 assert '{"key": "value"}' in output
356
357 def test_get_method(self) -> None:
358 from muse.cli.commands.sign import run_curl
359
360 signing = _make_signing()
361 args = self._args(method="GET")
362 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
363 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
364 run_curl(args)
365 assert "curl -X GET" in mock_out.getvalue()
366
367
368 # ---------------------------------------------------------------------------
369 # run_payment
370 # ---------------------------------------------------------------------------
371
372 class TestRunPayment(unittest.TestCase):
373 _NONCE = "a" * 64 # 64-char hex nonce
374
375 def _args(self, **kwargs: bool | int | str | None) -> argparse.Namespace:
376 defaults = dict(
377 from_handle="alice",
378 to_handle="bob",
379 amount=1_000_000,
380 nonce=self._NONCE,
381 currency="nanoMUSE",
382 memo="stem:sha256:abc123",
383 hub=None,
384 key_path=None,
385 agent_id=None,
386 timestamp=1744000000,
387 json_out=True,
388 )
389 defaults.update(kwargs)
390 return argparse.Namespace(**defaults)
391
392 def test_json_output_has_all_fields(self) -> None:
393 from muse.cli.commands.sign import run_payment
394
395 signing = _make_signing("alice")
396 args = self._args()
397 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
398 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
399 run_payment(args)
400 data = json.loads(mock_out.getvalue())
401 assert data["from_handle"] == "alice"
402 assert data["to_handle"] == "bob"
403 assert data["amount_nano"] == 1_000_000
404 assert data["currency"] == "nanoMUSE"
405 assert data["nonce_hex"] == self._NONCE
406 assert data["memo"] == "stem:sha256:abc123"
407 assert data["ts"] == 1744000000
408 assert "signature_b64" in data
409 assert "canonical_message" in data
410
411 def test_canonical_message_has_mpay_prefix(self) -> None:
412 """Payment canonical message must start with 'MPAY' domain separator."""
413 from muse.cli.commands.sign import run_payment
414
415 signing = _make_signing("alice")
416 args = self._args()
417 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
418 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
419 run_payment(args)
420 data = json.loads(mock_out.getvalue())
421 assert data["canonical_message"].startswith("MPAY\n")
422
423 def test_signature_is_verifiable(self) -> None:
424 """The payment signature must verify against the signer's public key."""
425 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
426 from muse.cli.commands.sign import run_payment
427 from muse.core.transport import SigningIdentity
428
429 private_key = Ed25519PrivateKey.generate()
430 signing = SigningIdentity(handle="alice", private_key=private_key)
431 args = self._args()
432 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
433 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
434 run_payment(args)
435 data = json.loads(mock_out.getvalue())
436
437 sig_bytes = b64url_decode(data["signature_b64"])
438 msg = data["canonical_message"].encode()
439 # Must not raise InvalidSignature.
440 private_key.public_key().verify(sig_bytes, msg)
441
442 def test_domain_separation_from_http_msign(self) -> None:
443 """Payment signature must NOT verify against the HTTP MSign canonical message."""
444 from cryptography.exceptions import InvalidSignature
445 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
446 from muse.cli.commands.sign import run_payment
447 from muse.core.msign import canonical_message
448 from muse.core.transport import SigningIdentity
449
450 private_key = Ed25519PrivateKey.generate()
451 signing = SigningIdentity(handle="alice", private_key=private_key)
452 args = self._args()
453 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
454 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
455 run_payment(args)
456 data = json.loads(mock_out.getvalue())
457
458 sig_bytes = b64url_decode(data["signature_b64"])
459 # Try to verify the payment sig against an HTTP canonical message — must fail.
460 http_msg = canonical_message("POST", "/alice/bob", 1744000000, b"", host="hub")
461 with self.assertRaises(InvalidSignature):
462 private_key.public_key().verify(sig_bytes, http_msg)
463
464 def test_deterministic_at_fixed_timestamp(self) -> None:
465 """Same key + same inputs + same ts → same signature every time."""
466 from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey
467 from muse.cli.commands.sign import run_payment
468 from muse.core.transport import SigningIdentity
469
470 private_key = Ed25519PrivateKey.generate()
471 signing = SigningIdentity(handle="alice", private_key=private_key)
472 args = self._args()
473
474 sigs: list[str] = []
475 for _ in range(3):
476 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
477 patch("sys.stdout", new_callable=io.StringIO) as mock_out:
478 run_payment(args)
479 sigs.append(json.loads(mock_out.getvalue())["signature_b64"])
480
481 assert sigs[0] == sigs[1] == sigs[2], "Payment signatures must be deterministic"
482
483 def test_text_output_prints_signature_to_stderr(self) -> None:
484 """Text mode must print payment info (including signature) to stderr only."""
485 from muse.cli.commands.sign import run_payment
486
487 signing = _make_signing("alice")
488 args = self._args(json_out=False)
489 with patch("muse.cli.commands.sign._load_signing", return_value=signing), \
490 patch("sys.stdout", new_callable=io.StringIO) as mock_out, \
491 patch("sys.stderr", new_callable=io.StringIO) as mock_err:
492 run_payment(args)
493 # stdout must be empty — signature no longer bleeds to stdout
494 assert mock_out.getvalue() == "", f"stdout must be empty in text mode, got: {mock_out.getvalue()!r}"
495 # stderr must contain the signature
496 stderr = mock_err.getvalue()
497 assert "Signature:" in stderr, f"'Signature:' missing from stderr: {stderr!r}"
498
499
500 # ---------------------------------------------------------------------------
501 # _load_signing — identity resolution
502 # ---------------------------------------------------------------------------
503
504 class TestLoadSigning(unittest.TestCase):
505 def test_exits_1_when_no_identity(self) -> None:
506 """When get_signing_identity returns None, must exit with code 1."""
507 from muse.cli.commands.sign import _load_signing
508
509 with patch("muse.cli.commands.sign.get_signing_identity", return_value=None, create=True):
510 # Patch the import inside _load_signing.
511 with patch("muse.cli.config.get_signing_identity", return_value=None):
512 with self.assertRaises(SystemExit) as cm:
513 _load_signing(hub="https://hub.example.com")
514 assert cm.exception.code == 1
515
516
517 # ---------------------------------------------------------------------------
518 # _load_signing call-site signature — stale key_path positional arg
519 # ---------------------------------------------------------------------------
520
521 class TestLoadSigningCallSites(unittest.TestCase):
522 """run_header / run_curl / run_payment must not pass stale key_path to _load_signing.
523
524 When --key-path was removed from the parser, three call sites were left
525 passing getattr(args, "key_path", None) as a positional arg.
526 _load_signing(hub, agent_id=None) only accepts 2 params, so passing 3
527 raises TypeError at runtime.
528
529 Coverage
530 --------
531 CS-1 run_header does not TypeError when agent_id is set
532 CS-2 run_curl does not TypeError when agent_id is set
533 CS-3 run_payment does not TypeError when agent_id is set
534 CS-4 agent_id is forwarded correctly by run_header (not lost to key_path slot)
535 """
536
537 def _signing(self) -> "SigningIdentity":
538 return _make_signing("gabriel")
539
540 def test_CS1_run_header_no_type_error(self) -> None:
541 """CS-1: run_header must not raise TypeError when key_path absent from args."""
542 from muse.cli.commands.sign import run_header
543
544 args = argparse.Namespace(
545 method="GET",
546 path=None,
547 url="https://hub.example.com/test",
548 hub=None,
549 body=None,
550 body_file=None,
551 timestamp=None,
552 agent_id=None,
553 json_out=False,
554 )
555 with patch("muse.cli.config.get_signing_identity", return_value=self._signing()), \
556 patch("sys.stdout", new_callable=io.StringIO):
557 run_header(args) # must not raise TypeError
558
559 def test_CS2_run_curl_no_type_error(self) -> None:
560 """CS-2: run_curl must not raise TypeError when key_path absent from args."""
561 from muse.cli.commands.sign import run_curl
562
563 args = argparse.Namespace(
564 method="GET",
565 url="https://hub.example.com/test",
566 hub=None,
567 body=None,
568 body_file=None,
569 content_type="application/json",
570 timestamp=None,
571 agent_id=None,
572 )
573 with patch("muse.cli.config.get_signing_identity", return_value=self._signing()), \
574 patch("sys.stdout", new_callable=io.StringIO):
575 run_curl(args) # must not raise TypeError
576
577 def test_CS3_run_payment_no_type_error(self) -> None:
578 """CS-3: run_payment must not raise TypeError when key_path absent from args."""
579 from muse.cli.commands.sign import run_payment
580
581 args = argparse.Namespace(
582 hub=None,
583 from_handle="alice",
584 to_handle="bob",
585 amount=1000000,
586 nonce="ab" * 32,
587 memo="",
588 currency="nanoMUSE",
589 timestamp=None,
590 agent_id=None,
591 json_out=True,
592 )
593 with patch("muse.cli.config.get_signing_identity", return_value=self._signing()), \
594 patch("sys.stdout", new_callable=io.StringIO):
595 run_payment(args) # must not raise TypeError
596
597 def test_CS4_agent_id_forwarded_by_run_header(self) -> None:
598 """CS-4: agent_id passed in args must reach get_signing_identity."""
599 from muse.cli.commands.sign import run_header
600
601 args = argparse.Namespace(
602 method="GET",
603 path=None,
604 url="https://hub.example.com/test",
605 hub="https://hub.example.com",
606 body=None,
607 body_file=None,
608 timestamp=None,
609 agent_id="my-agent",
610 json_out=False,
611 )
612 captured_kwargs: list[dict] = []
613
614 def _fake_get_signing(remote_url: str | None = None, agent_id: str | None = None) -> "SigningIdentity":
615 captured_kwargs.append({"remote_url": remote_url, "agent_id": agent_id})
616 return _make_signing("my-agent")
617
618 with patch("muse.cli.config.get_signing_identity", side_effect=_fake_get_signing), \
619 patch("sys.stdout", new_callable=io.StringIO):
620 run_header(args)
621
622 assert captured_kwargs, "get_signing_identity was not called"
623 assert captured_kwargs[0]["agent_id"] == "my-agent", (
624 f"agent_id not forwarded — got {captured_kwargs[0]['agent_id']!r}"
625 )
626
627
628 if __name__ == "__main__":
629 unittest.main()
File History 4 commits
sha256:81ae324db5ad375fbfe4834c6fcb378312cafad3cc92dec5d3e5c427306621a2 fix: remove commit_exists filter from have anchors — server… Sonnet 4.6 patch 21 days ago
sha256:36c3cb3e76619d4c30a6d9bf81b5ec4ff148e30dcfed913e3114ca7b43b81c7e fix: rename objects→blobs in push client and all stale test… Sonnet 4.6 patch 22 days ago
sha256:c06a9b9b9fee26c68ea725b44d54b2c0a171301ce9de746d5b656617b4463a9a fix: repair four test failures from post-migration audit Sonnet 4.6 patch 29 days ago
sha256:1900655993c83c4107067375548a7be823e471d2515830842f1a12cba4bd3cdf fix: unified object store migration — idempotent writes, JS… Sonnet 4.6 minor 29 days ago