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