collab-runtime 0.9.2__tar.gz → 0.9.3__tar.gz

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.
Files changed (40) hide show
  1. {collab_runtime-0.9.2/collab_runtime.egg-info → collab_runtime-0.9.3}/PKG-INFO +1 -1
  2. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/dashboard_server.py +4 -2
  3. collab_runtime-0.9.3/collab/env_secrets.py +45 -0
  4. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/live_locks_watcher.py +31 -24
  5. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/lock_client.py +656 -35
  6. collab_runtime-0.9.3/collab/path_filter.py +174 -0
  7. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/safe_subprocess.py +10 -1
  8. {collab_runtime-0.9.2 → collab_runtime-0.9.3/collab_runtime.egg-info}/PKG-INFO +1 -1
  9. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab_runtime.egg-info/SOURCES.txt +2 -0
  10. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/pyproject.toml +1 -1
  11. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/LICENSE +0 -0
  12. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/README.md +0 -0
  13. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/__init__.py +0 -0
  14. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/__main__.py +0 -0
  15. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/agent_hooks.py +0 -0
  16. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/agent_identity.py +0 -0
  17. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/dashboard/dashboard-charts.js +0 -0
  18. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/dashboard/dashboard-filters.js +0 -0
  19. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/dashboard/dashboard-format.js +0 -0
  20. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/dashboard/index.html +0 -0
  21. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/errors.py +0 -0
  22. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/githooks.py +0 -0
  23. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/commit-msg +0 -0
  24. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/post-checkout +0 -0
  25. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/post-commit +0 -0
  26. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/post-merge +0 -0
  27. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/pre-commit +0 -0
  28. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/hook_templates/pre-push +0 -0
  29. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/logging_config.py +0 -0
  30. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/main.py +0 -0
  31. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/overlap.py +0 -0
  32. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/platform_probe.py +0 -0
  33. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/pr_overlap.py +0 -0
  34. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab/subprocess_bridge.py +0 -0
  35. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab_runtime.egg-info/dependency_links.txt +0 -0
  36. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab_runtime.egg-info/entry_points.txt +0 -0
  37. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab_runtime.egg-info/requires.txt +0 -0
  38. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/collab_runtime.egg-info/top_level.txt +0 -0
  39. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/docs/pypi/README.md +0 -0
  40. {collab_runtime-0.9.2 → collab_runtime-0.9.3}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: collab-runtime
3
- Version: 0.9.2
3
+ Version: 0.9.3
4
4
  Summary: Collaborative file locking runtime
5
5
  Author-email: KirilMT <kiril.mt95@gmail.com>
6
6
  License-Expression: MIT
@@ -230,9 +230,11 @@ def load_runtime_supabase_config(project_root: str) -> dict[str, Any]:
230
230
  val = os.getenv(name)
231
231
  return val
232
232
 
233
+ from .env_secrets import effective_anon_key, effective_service_role_key
234
+
233
235
  url = pick("SUPABASE_URL") or ""
234
- anon = pick("SUPABASE_ANON_KEY") or ""
235
- service = pick("SUPABASE_SERVICE_ROLE_KEY") or None
236
+ anon = effective_anon_key(pick("SUPABASE_ANON_KEY")) or ""
237
+ service = effective_service_role_key(pick("SUPABASE_SERVICE_ROLE_KEY"))
236
238
  user = _resolve_developer_name(project_root, file_vals)
237
239
  from . import agent_identity
238
240
 
@@ -0,0 +1,45 @@
1
+ """Resolve effective Supabase credentials from environment variables."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+ import re
7
+ from typing import Optional
8
+
9
+ # Placeholder patterns copied from scripts/setup.ps1 (Test-IsPlaceholderValue).
10
+ _PLACEHOLDER_ENV_RE = re.compile(
11
+ r"^(?:your[_-]|your[_-]?project|example|CHANGE_ME|change[_-]?me|"
12
+ r"replace[_-]?me|TODO|<team-)",
13
+ re.IGNORECASE,
14
+ )
15
+
16
+
17
+ def is_placeholder_env_value(value: Optional[str]) -> bool:
18
+ """Return True when *value* is unset or still a template placeholder."""
19
+ if value is None:
20
+ return True
21
+ stripped = value.strip()
22
+ if not stripped:
23
+ return True
24
+ return bool(_PLACEHOLDER_ENV_RE.match(stripped))
25
+
26
+
27
+ def effective_env_secret(value: Optional[str]) -> Optional[str]:
28
+ """Return a usable secret or None when *value* is blank/placeholder."""
29
+ if is_placeholder_env_value(value):
30
+ return None
31
+ return value.strip() if value else None
32
+
33
+
34
+ def effective_service_role_key(raw: Optional[str] = None) -> Optional[str]:
35
+ """Service role key for API calls, or None when placeholder/unset."""
36
+ if raw is None:
37
+ raw = os.getenv("SUPABASE_SERVICE_ROLE_KEY")
38
+ return effective_env_secret(raw)
39
+
40
+
41
+ def effective_anon_key(raw: Optional[str] = None) -> Optional[str]:
42
+ """Anon key for API calls, or None when placeholder/unset."""
43
+ if raw is None:
44
+ raw = os.getenv("SUPABASE_ANON_KEY")
45
+ return effective_env_secret(raw)
@@ -28,7 +28,8 @@ from typing import Any, Callable, Optional, Protocol, cast
28
28
 
29
29
  from dotenv import load_dotenv
30
30
 
31
- from . import agent_identity, platform_probe, safe_subprocess
31
+ from . import agent_identity, path_filter, platform_probe, safe_subprocess
32
+ from .env_secrets import effective_anon_key, effective_service_role_key
32
33
 
33
34
  # NOTE: do NOT import collab-local modules before the runtime root and sys.path
34
35
  # setup is complete. The import for `logging_config` is moved
@@ -487,21 +488,13 @@ def _min_auto_lock_hold_seconds() -> int:
487
488
 
488
489
 
489
490
  def _should_ignore_path(path: str) -> bool:
490
- """Return True for paths the watcher should skip."""
491
- norm = path.replace("\\", "/")
492
- if "/.git/" in norm or norm.startswith(".git/"):
493
- return True
494
- # Ignore runtime instance folders: they are environment artifacts and
495
- # should not produce collaborative file locks.
496
- if (
497
- norm == "instance"
498
- or norm.startswith("instance/")
499
- or norm.endswith("/instance")
500
- or "/instance/" in norm
501
- ):
502
- return True
503
- # Do not ignore runtime-relative project paths here.
504
- return False
491
+ """Return True for paths the watcher should skip.
492
+
493
+ Delegates to :func:`collab.path_filter.should_ignore_lock_path`, which also
494
+ honors ``COLLAB_LOCK_IGNORE`` and a project ``.collabignore`` so transient
495
+ scratch files never produce short-lived locks (#170).
496
+ """
497
+ return path_filter.should_ignore_lock_path(path, _PROJECT_ROOT)
505
498
 
506
499
 
507
500
  def _color(text: str, color: str) -> str:
@@ -734,6 +727,19 @@ def _process_new_files(client, branch: str, new_files: set[str]) -> None:
734
727
  data[0].get("agent_label"),
735
728
  data[0].get("agent_kind"),
736
729
  )
730
+ # #172: a conflict against your OWN developer id is never a
731
+ # cross-developer merge risk. Under sticky attribution (#169) this
732
+ # only happens for same-developer CROSS-AGENT edits, which the
733
+ # per-poll watcher would otherwise spam as WARNING + desktop
734
+ # notification. Log it once at DEBUG and skip the notification.
735
+ if str(owner) == DEVELOPER_ID:
736
+ logger.debug(
737
+ "Self-lock on %s held by %s (same developer) — "
738
+ "no cross-developer conflict; not renewing this poll.",
739
+ fp,
740
+ owner_display,
741
+ )
742
+ continue
737
743
  _active_conflicts.add(fp)
738
744
  msg = (
739
745
  f"CONFLICT: {fp} is locked by {owner_display} -- "
@@ -901,8 +907,8 @@ def _start_dashboard_server() -> str | None:
901
907
 
902
908
  injected = {
903
909
  "url": SUPABASE_URL or "",
904
- "anonKey": SUPABASE_ANON_KEY or "",
905
- "serviceKey": SUPABASE_SERVICE_ROLE_KEY or None,
910
+ "anonKey": effective_anon_key(SUPABASE_ANON_KEY) or "",
911
+ "serviceKey": effective_service_role_key(SUPABASE_SERVICE_ROLE_KEY),
906
912
  "user": DEVELOPER_ID or "",
907
913
  }
908
914
  url, _html_path = prepare_dashboard_server(
@@ -1441,11 +1447,10 @@ def _graceful_shutdown() -> None:
1441
1447
  _shutdown_done = True
1442
1448
 
1443
1449
  dev_id = DEVELOPER_ID
1444
- if dev_id and SUPABASE_URL and SUPABASE_ANON_KEY and create_client is not None:
1450
+ anon_key = effective_anon_key(SUPABASE_ANON_KEY)
1451
+ if dev_id and SUPABASE_URL and anon_key and create_client is not None:
1445
1452
  try:
1446
- client = cast(Callable[..., Any], create_client)(
1447
- SUPABASE_URL, SUPABASE_ANON_KEY
1448
- )
1453
+ client = cast(Callable[..., Any], create_client)(SUPABASE_URL, anon_key)
1449
1454
 
1450
1455
  # Determine which files are still in progress
1451
1456
  # (dirty OR committed-but-unpushed)
@@ -1913,7 +1918,7 @@ def main() -> None:
1913
1918
  )
1914
1919
  args = parser.parse_args()
1915
1920
 
1916
- if not SUPABASE_URL or not SUPABASE_ANON_KEY:
1921
+ if not SUPABASE_URL or not effective_anon_key(SUPABASE_ANON_KEY):
1917
1922
  logger.error(
1918
1923
  "Missing SUPABASE_URL or SUPABASE_ANON_KEY in .env.\n"
1919
1924
  "See .env.example for setup."
@@ -2047,7 +2052,9 @@ def main() -> None:
2047
2052
  sys.exit(1)
2048
2053
  # static analyzers may still treat create_client as Optional; cast for
2049
2054
  # their sake so they recognize the value is callable beyond this point.
2050
- client = cast(Callable[..., Any], create_client)(SUPABASE_URL, SUPABASE_ANON_KEY)
2055
+ client = cast(Callable[..., Any], create_client)(
2056
+ SUPABASE_URL, effective_anon_key(SUPABASE_ANON_KEY)
2057
+ )
2051
2058
 
2052
2059
  # Start local dashboard server for a clickable URL
2053
2060
  dashboard_url = _start_dashboard_server()