federaide-dev-nightly 2.0.8__tar.gz → 2.1.2__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 (41) hide show
  1. {federaide_dev_nightly-2.0.8/src/federaide_dev_nightly.egg-info → federaide_dev_nightly-2.1.2}/PKG-INFO +3 -2
  2. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/pyproject.toml +4 -3
  3. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2/src/federaide_dev_nightly.egg-info}/PKG-INFO +3 -2
  4. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federaide_dev_nightly.egg-info/SOURCES.txt +2 -0
  5. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federaide_dev_nightly.egg-info/requires.txt +2 -1
  6. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/agent.py +381 -29
  7. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/agent_core.py +1 -1
  8. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/chatgpt_auth.py +2 -4
  9. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/commands.py +17 -0
  10. federaide_dev_nightly-2.1.2/src/federate/git_auth.py +400 -0
  11. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/mcp_handler.py +1 -1
  12. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/orchestration.py +26 -2
  13. federaide_dev_nightly-2.1.2/src/federate/team_mesh.py +544 -0
  14. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/toolbox.py +58 -35
  15. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/LICENSE +0 -0
  16. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/MANIFEST.in +0 -0
  17. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/README.md +0 -0
  18. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/go/cmd/bridge/main.go +0 -0
  19. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/go/cmd/embed/main.go +0 -0
  20. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/go/cmd/search/main.go +0 -0
  21. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/go/go.mod +0 -0
  22. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/go/go.sum +0 -0
  23. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/scripts/build_go.sh +0 -0
  24. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/setup.cfg +0 -0
  25. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/setup.py +0 -0
  26. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federaide_dev_nightly.egg-info/dependency_links.txt +0 -0
  27. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federaide_dev_nightly.egg-info/entry_points.txt +0 -0
  28. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federaide_dev_nightly.egg-info/top_level.txt +0 -0
  29. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/__init__.py +0 -0
  30. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/__main__.py +0 -0
  31. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/app.py +0 -0
  32. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/audio_handler.py +0 -0
  33. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/execution.py +0 -0
  34. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/semantic_search.py +0 -0
  35. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/styles/fonts/Michroma-Regular.ttf +0 -0
  36. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/styles/fonts/SpaceGrotesk-Bold.ttf +0 -0
  37. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/styles/fonts/SpaceGrotesk-VariableFont_wght.ttf +0 -0
  38. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/styles/style.css +0 -0
  39. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/subagents.py +0 -0
  40. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/telegram_handler.py +0 -0
  41. {federaide_dev_nightly-2.0.8 → federaide_dev_nightly-2.1.2}/src/federate/textIDE.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: federaide-dev-nightly
3
- Version: 2.0.8
3
+ Version: 2.1.2
4
4
  Summary: A cross-platform terminal based p2p agent orchestration harness and universal automation system
5
5
  Author-email: ROCK-LAB-PRIVATE-LIMITED <arkadeep_ghosh@rocklab.in>
6
6
  License: GNU Affero General Public License v3
@@ -51,6 +51,7 @@ Requires-Dist: orjson==3.12.0
51
51
  Requires-Dist: cryptography==50.0.1
52
52
  Requires-Dist: ormsgpack==1.12.2
53
53
  Requires-Dist: rpds-py==2026.6.3
54
+ Requires-Dist: uuid-utils==0.17.1
54
55
  Requires-Dist: pywinpty; platform_system == "Windows"
55
56
  Provides-Extra: audio
56
57
  Requires-Dist: sounddevice; extra == "audio"
@@ -58,7 +59,7 @@ Requires-Dist: kokoro-onnx; extra == "audio"
58
59
  Requires-Dist: sherpa-onnx; extra == "audio"
59
60
  Provides-Extra: vision
60
61
  Requires-Dist: pyautogui; extra == "vision"
61
- Requires-Dist: pillow; extra == "vision"
62
+ Requires-Dist: pillow==12.3.0; extra == "vision"
62
63
  Provides-Extra: pdf
63
64
  Requires-Dist: weasyprint; extra == "pdf"
64
65
  Requires-Dist: pypdf; extra == "pdf"
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "federaide-dev-nightly"
7
- version = "2.0.8"
7
+ version = "2.1.2"
8
8
  description = "A cross-platform terminal based p2p agent orchestration harness and universal automation system"
9
9
  readme = "README.md"
10
10
  license = {text = "GNU Affero General Public License v3"}
@@ -57,6 +57,7 @@ dependencies = [
57
57
  "cryptography==50.0.1",
58
58
  "ormsgpack==1.12.2",
59
59
  "rpds-py==2026.6.3",
60
+ "uuid-utils==0.17.1",
60
61
  # Platform-specific hard dependencies
61
62
  "pywinpty; platform_system == 'Windows'",
62
63
  ]
@@ -71,7 +72,7 @@ audio = [
71
72
  # Autonomous Visual Computer Use / Computer Vision (optional)
72
73
  vision = [
73
74
  "pyautogui",
74
- "pillow",
75
+ "pillow==12.3.0",
75
76
  ]
76
77
  # PDF report generation for Deep Research (optional)
77
78
  pdf = [
@@ -108,4 +109,4 @@ Issues = "https://github.com/ROCK-LAB-PRIVATE-LIMITED/federaide/issues"
108
109
  where = ["src"]
109
110
 
110
111
  [tool.setuptools.package-data]
111
- federate = ["bin/*", "styles/*", "styles/fonts/*"]
112
+ federate = ["bin/*", "styles/*", "styles/fonts/*"]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: federaide-dev-nightly
3
- Version: 2.0.8
3
+ Version: 2.1.2
4
4
  Summary: A cross-platform terminal based p2p agent orchestration harness and universal automation system
5
5
  Author-email: ROCK-LAB-PRIVATE-LIMITED <arkadeep_ghosh@rocklab.in>
6
6
  License: GNU Affero General Public License v3
@@ -51,6 +51,7 @@ Requires-Dist: orjson==3.12.0
51
51
  Requires-Dist: cryptography==50.0.1
52
52
  Requires-Dist: ormsgpack==1.12.2
53
53
  Requires-Dist: rpds-py==2026.6.3
54
+ Requires-Dist: uuid-utils==0.17.1
54
55
  Requires-Dist: pywinpty; platform_system == "Windows"
55
56
  Provides-Extra: audio
56
57
  Requires-Dist: sounddevice; extra == "audio"
@@ -58,7 +59,7 @@ Requires-Dist: kokoro-onnx; extra == "audio"
58
59
  Requires-Dist: sherpa-onnx; extra == "audio"
59
60
  Provides-Extra: vision
60
61
  Requires-Dist: pyautogui; extra == "vision"
61
- Requires-Dist: pillow; extra == "vision"
62
+ Requires-Dist: pillow==12.3.0; extra == "vision"
62
63
  Provides-Extra: pdf
63
64
  Requires-Dist: weasyprint; extra == "pdf"
64
65
  Requires-Dist: pypdf; extra == "pdf"
@@ -24,10 +24,12 @@ src/federate/audio_handler.py
24
24
  src/federate/chatgpt_auth.py
25
25
  src/federate/commands.py
26
26
  src/federate/execution.py
27
+ src/federate/git_auth.py
27
28
  src/federate/mcp_handler.py
28
29
  src/federate/orchestration.py
29
30
  src/federate/semantic_search.py
30
31
  src/federate/subagents.py
32
+ src/federate/team_mesh.py
31
33
  src/federate/telegram_handler.py
32
34
  src/federate/textIDE.py
33
35
  src/federate/toolbox.py
@@ -28,6 +28,7 @@ orjson==3.12.0
28
28
  cryptography==50.0.1
29
29
  ormsgpack==1.12.2
30
30
  rpds-py==2026.6.3
31
+ uuid-utils==0.17.1
31
32
 
32
33
  [:platform_system == "Windows"]
33
34
  pywinpty
@@ -56,4 +57,4 @@ pypdf
56
57
 
57
58
  [vision]
58
59
  pyautogui
59
- pillow
60
+ pillow==12.3.0
@@ -1732,6 +1732,44 @@ class GitConflictModal(ModalScreen[str]):
1732
1732
  else:
1733
1733
  self.dismiss("abort")
1734
1734
 
1735
+ class MeshSeedModal(ModalScreen[str]):
1736
+ DEFAULT_CSS = """
1737
+ MeshSeedModal { align: center middle; background: $background 60%; }
1738
+ #seed_dialog { width: 75; height: auto; border: thick $primary; background: $surface; padding: 1 2; }
1739
+ #seed_input { margin-top: 1; margin-bottom: 1; height: auto; min-height: 4; max-height: 8; }
1740
+ .seed_help { color: $text-muted; margin-top: 0; margin-bottom: 1; text-wrap: wrap; height: auto; }
1741
+ .seed_buttons { layout: horizontal; height: auto; align: right middle; margin-top: 1; }
1742
+ .seed_buttons Button { margin-left: 1; }
1743
+ """
1744
+
1745
+ def __init__(self, current_seed: str = ""):
1746
+ super().__init__()
1747
+ self.current_seed = current_seed
1748
+
1749
+ def compose(self) -> ComposeResult:
1750
+ with Vertical(id="seed_dialog"):
1751
+ yield Label(" Mesh Master Seed Phrase", classes="pane_title")
1752
+ yield Label(
1753
+ "Enter your seed phrase or secret passphrase to synchronize identity and encryption keys across all your devices:",
1754
+ classes="seed_help"
1755
+ )
1756
+ yield TextArea(self.current_seed, id="seed_input", show_line_numbers=False)
1757
+ with Horizontal(classes="seed_buttons"):
1758
+ yield Button("Save Seed", id="btn_save_seed", variant="success")
1759
+ yield Button("Cancel", id="btn_cancel_seed", variant="error")
1760
+
1761
+ def on_mount(self):
1762
+ self.query_one("#seed_input", TextArea).focus()
1763
+
1764
+ @on(Button.Pressed, "#btn_save_seed")
1765
+ def on_save(self):
1766
+ val = self.query_one("#seed_input", TextArea).text.strip()
1767
+ self.dismiss(val)
1768
+
1769
+ @on(Button.Pressed, "#btn_cancel_seed")
1770
+ def on_cancel(self):
1771
+ self.dismiss(None)
1772
+
1735
1773
  class TeamsModal(ModalScreen[None]):
1736
1774
  DEFAULT_CSS = """
1737
1775
  TeamsModal { align: center middle; background: $background 60%; }
@@ -1748,8 +1786,15 @@ class TeamsModal(ModalScreen[None]):
1748
1786
  #team_folder_input { width: 1fr; }
1749
1787
  #team_git_user_input { width: 1fr; margin-right: 1; }
1750
1788
  #team_git_pat_input { width: 1fr; }
1751
- .team_form_row { layout: horizontal; height: auto; margin-top: 1; width: 100%; }
1752
- .team_form_row Button { width: 1fr; margin: 0 1; }
1789
+ .team_form_row { layout: horizontal; height: 3; margin-top: 1; width: 100%; }
1790
+ .team_form_row Button { width: 1fr; margin: 0 1; height: 3; }
1791
+ #team_readme_input { height: 8; min-height: 5; border: round $accent; background: $surface; margin-top: 1; margin-bottom: 1; }
1792
+ #mesh_relay_url_input { margin-top: 1; margin-bottom: 0; }
1793
+ #mesh_relay_user_input { width: 1fr; margin-right: 1; }
1794
+ #mesh_relay_pat_input { width: 1fr; }
1795
+ #mesh_seed_btn { width: 100%; height: 3; margin-top: 1; margin-bottom: 1; }
1796
+ #channel_peers_display { border: round $accent; background: $surface; padding: 1; margin-top: 1; margin-bottom: 1; min-height: 3; }
1797
+ #sync_channel_btn { width: 100%; height: 3; margin-bottom: 1; }
1753
1798
  #teams_bottom_buttons { height: auto; align: right middle; margin-top: 0; }
1754
1799
  #teams_bottom_buttons Button { margin-left: 1; }
1755
1800
  """
@@ -1784,6 +1829,20 @@ class TeamsModal(ModalScreen[None]):
1784
1829
  yield Button("Pull", id="pull_btn", variant="warning")
1785
1830
  yield Button("Push", id="push_btn", variant="warning")
1786
1831
  yield Button("Sync", id="sync_now_btn", variant="warning")
1832
+
1833
+ yield Label("Team Charter (README.md - Injected into all agents):", classes="section_label")
1834
+ yield TextArea(id="team_readme_input", show_line_numbers=False, soft_wrap=True)
1835
+
1836
+ yield Label("Channel Relay Repository (Team-Specific Coordination Channel):", classes="section_label")
1837
+ yield Input(placeholder="Channel Relay Repo URL (e.g. https://github.com/user/fed-relay-a.git)", id="mesh_relay_url_input")
1838
+ with Horizontal(classes="form_row"):
1839
+ yield Input(placeholder="Channel Git Username", id="mesh_relay_user_input")
1840
+ yield Input(placeholder="Channel Git PAT", id="mesh_relay_pat_input", password=True)
1841
+ yield Button("Configure Channel PSK Seed Phrase", id="mesh_seed_btn", variant="primary")
1842
+
1843
+ yield Label("Autodetected Channel Peers (via Relay):", classes="section_label")
1844
+ yield Static(id="channel_peers_display")
1845
+ yield Button("Sync Channel & Refresh Peers", id="sync_channel_btn", variant="primary")
1787
1846
 
1788
1847
  yield Label("Registered Teams:", classes="section_label")
1789
1848
  yield Vertical(id="team_list")
@@ -1810,8 +1869,39 @@ class TeamsModal(ModalScreen[None]):
1810
1869
  self.query_one("#team_remote_url_input", Input).value = remote_url
1811
1870
  self.query_one("#team_git_user_input", Input).value = git_user
1812
1871
  self.query_one("#team_git_pat_input", Input).value = git_pat
1872
+ readme_path = os.path.join(self.editing_path, "README.md")
1873
+ readme_text = ""
1874
+ if os.path.exists(readme_path):
1875
+ try:
1876
+ with open(readme_path, "r", encoding="utf-8") as f:
1877
+ readme_text = f.read()
1878
+ except Exception: pass
1879
+ self.query_one("#team_readme_input", TextArea).text = readme_text
1880
+
1881
+ import team_mesh
1882
+ r_url, r_user, r_pat = team_mesh.get_relay_credentials(self.editing_path)
1883
+ self.query_one("#mesh_relay_url_input", Input).value = r_url
1884
+ self.query_one("#mesh_relay_user_input", Input).value = r_user
1885
+ self.query_one("#mesh_relay_pat_input", Input).value = r_pat
1886
+ self.refresh_channel_peers_ui(self.editing_path)
1813
1887
  self.query_one("#save_team_btn", Button).label = "Update Team"
1814
1888
 
1889
+ def refresh_channel_peers_ui(self, t_path: str = None):
1890
+ if not self.is_mounted:
1891
+ return
1892
+ target = t_path or self.editing_path or toolbox.get_team_dir()
1893
+ import team_mesh
1894
+ peers = team_mesh.discover_peer_teams(target)
1895
+ try:
1896
+ display = self.query_one("#channel_peers_display", Static)
1897
+ if peers:
1898
+ lines = [f"[bold green]● {name}[/bold green]: [dim]{desc[:120]}{'...' if len(desc)>120 else ''}[/dim]" for name, desc in peers.items()]
1899
+ display.update("\n".join(lines))
1900
+ else:
1901
+ display.update("[dim]No peer teams detected on this channel yet. (Click 'Sync Channel & Refresh Peers')[/dim]")
1902
+ except Exception:
1903
+ pass
1904
+
1815
1905
  def clear_form(self):
1816
1906
  self.editing_path = None
1817
1907
  self.query_one("#form_header_label", Label).update("Create New Team Workspace:")
@@ -1820,8 +1910,12 @@ class TeamsModal(ModalScreen[None]):
1820
1910
  self.query_one("#team_remote_url_input", Input).value = ""
1821
1911
  self.query_one("#team_git_user_input", Input).value = ""
1822
1912
  self.query_one("#team_git_pat_input", Input).value = ""
1913
+ self.query_one("#team_readme_input", TextArea).text = ""
1914
+ self.query_one("#mesh_relay_url_input", Input).value = ""
1915
+ self.query_one("#mesh_relay_user_input", Input).value = ""
1916
+ self.query_one("#mesh_relay_pat_input", Input).value = ""
1917
+ self.query_one("#channel_peers_display", Static).update("[dim]Save team to connect to channel.[/dim]")
1823
1918
  self.query_one("#save_team_btn", Button).label = "Save Team"
1824
-
1825
1919
  def _handle_git_action(self, action_name: str, target_team_dir: str):
1826
1920
  def _run_action():
1827
1921
  try:
@@ -1945,37 +2039,17 @@ class TeamsModal(ModalScreen[None]):
1945
2039
  def _run_restore():
1946
2040
  try:
1947
2041
  from urllib.parse import urlparse, urlunparse, quote
1948
- import subprocess, tempfile, stat, contextlib, os
1949
-
1950
- @contextlib.contextmanager
1951
- def git_askpass_context(pat_str: str):
1952
- askpass_ext = ".bat" if os.name == "nt" else ".sh"
1953
- fd, askpass_path = tempfile.mkstemp(suffix=askpass_ext, text=True)
1954
- with os.fdopen(fd, "w") as f:
1955
- if os.name == "nt":
1956
- f.write("@echo off\nif \"%~1\"==\"\" exit /b 0\necho %FEDERAIDE_GIT_PASSWORD%\n")
1957
- else:
1958
- f.write("#!/bin/sh\necho \"$FEDERAIDE_GIT_PASSWORD\"\n")
1959
- if os.name != "nt":
1960
- os.chmod(askpass_path, stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR)
1961
-
1962
- env = os.environ.copy()
1963
- env["GIT_ASKPASS"] = askpass_path
1964
- env["FEDERAIDE_GIT_PASSWORD"] = pat_str
1965
- env["GIT_TERMINAL_PROMPT"] = "0"
1966
- try:
1967
- yield env
1968
- finally:
1969
- try: os.remove(askpass_path)
1970
- except Exception: pass
2042
+ import subprocess, os
2043
+ from git_auth import git_askpass_context
1971
2044
 
1972
2045
  parsed = urlparse(url)
1973
2046
  user_part = quote(user, safe='') if user else "git"
1974
- auth_netloc = f"{user_part}@{parsed.netloc}"
2047
+ host = parsed.netloc.split("@")[-1]
2048
+ auth_netloc = f"{user_part}@{host}"
1975
2049
  safe_url = urlunparse((parsed.scheme, auth_netloc, parsed.path, parsed.params, parsed.query, parsed.fragment))
1976
2050
 
1977
2051
  with git_askpass_context(pat) as env:
1978
- res = subprocess.run(["git", "clone", safe_url, target_path], capture_output=True, text=True, env=env)
2052
+ res = subprocess.run(["git", "-c", "credential.helper=", "clone", safe_url, target_path], capture_output=True, text=True, env=env)
1979
2053
 
1980
2054
  if res.returncode == 0:
1981
2055
  subprocess.run(["git", "remote", "set-url", "origin", url], cwd=target_path, capture_output=True)
@@ -2003,7 +2077,6 @@ class TeamsModal(ModalScreen[None]):
2003
2077
  pass
2004
2078
  self.app.call_from_thread(_reenable)
2005
2079
 
2006
- import threading
2007
2080
  threading.Thread(target=_run_restore, daemon=True).start()
2008
2081
 
2009
2082
  elif btn_id in ("sync_now_btn", "pull_btn", "push_btn"):
@@ -2019,7 +2092,77 @@ class TeamsModal(ModalScreen[None]):
2019
2092
  self.notify(f"{action.title()}ing with Git remote for '{os.path.basename(target_team_dir)}'...", severity="information")
2020
2093
  self._handle_git_action(action, target_team_dir)
2021
2094
 
2095
+ elif btn_id == "sync_channel_btn":
2096
+ target = self.editing_path or toolbox.get_team_dir()
2097
+ import team_mesh
2098
+ r_url = self.query_one("#mesh_relay_url_input", Input).value.strip()
2099
+ r_user = self.query_one("#mesh_relay_user_input", Input).value.strip()
2100
+ r_pat = self.query_one("#mesh_relay_pat_input", Input).value.strip()
2101
+ if not r_url or not r_pat:
2102
+ self.notify("Please enter the Channel Relay Repo URL and PAT first.", severity="error")
2103
+ return
2104
+
2105
+ # Save channel credentials immediately from inputs
2106
+ team_mesh.save_relay_credentials(url=r_url, user=r_user, pat=r_pat, team_dir=target)
2107
+
2108
+ readme_text = self.query_one("#team_readme_input", TextArea).text
2109
+ if readme_text:
2110
+ try:
2111
+ with open(os.path.join(target, "README.md"), "w", encoding="utf-8") as f:
2112
+ f.write(readme_text)
2113
+ except Exception:
2114
+ pass
2115
+
2116
+ t_name = self.query_one("#team_name_input", Input).value.strip() or os.path.basename(target).lstrip(".")
2117
+ self.notify("Publishing team manifest and syncing channel relay...", severity="information")
2118
+
2119
+ def _sync():
2120
+ # Publish team manifest (clones/inits, writes identity.json + README, and pushes)
2121
+ ok = team_mesh.publish_team_manifest(t_name, readme_text, team_dir=target)
2122
+ if not ok:
2123
+ ok = team_mesh.sync_relay_repo(target)
2124
+
2125
+ self.app.call_from_thread(self.refresh_channel_peers_ui, target)
2126
+ if ok:
2127
+ self.app.call_from_thread(self.notify, "Channel relay synced and team published!", severity="information")
2128
+ else:
2129
+ self.app.call_from_thread(self.notify, "Channel sync failed. Check relay URL and PAT permissions.", severity="error")
2130
+ threading.Thread(target=_sync, daemon=True).start()
2131
+
2132
+ elif btn_id == "mesh_seed_btn":
2133
+ import team_mesh
2134
+ target = self.editing_path or toolbox.get_team_dir()
2135
+ current_seed = team_mesh.get_or_create_user_seed(target)
2136
+ def handle_seed_save(new_seed):
2137
+ if new_seed:
2138
+ team_mesh.set_user_seed(new_seed, target)
2139
+ self.notify("Channel PSK Seed updated successfully.", severity="information")
2140
+ self.app.push_screen(MeshSeedModal(current_seed), handle_seed_save)
2141
+ return
2142
+
2022
2143
  elif btn_id == "save_team_btn":
2144
+ readme_text = self.query_one("#team_readme_input", TextArea).text
2145
+ import team_mesh
2146
+ target = self.editing_path or toolbox.get_team_dir()
2147
+ team_mesh.save_relay_credentials(
2148
+ url=self.query_one("#mesh_relay_url_input", Input).value,
2149
+ user=self.query_one("#mesh_relay_user_input", Input).value,
2150
+ pat=self.query_one("#mesh_relay_pat_input", Input).value,
2151
+ team_dir=target
2152
+ )
2153
+ if self.editing_path:
2154
+ try:
2155
+ with open(os.path.join(self.editing_path, "README.md"), "w", encoding="utf-8") as f:
2156
+ f.write(readme_text)
2157
+ except Exception: pass
2158
+
2159
+ t_active_name = self.query_one("#team_name_input", Input).value.strip() or os.path.basename(self.editing_path).lstrip(".")
2160
+ edit_path = self.editing_path
2161
+ old_name_arg = old_team_name if (old_team_name and old_team_name.lower() != t_active_name.lower()) else None
2162
+ def _bg_publish():
2163
+ team_mesh.publish_team_manifest(t_active_name, readme_text, team_dir=edit_path, old_team_name=old_name_arg)
2164
+ self.app.call_from_thread(self.refresh_channel_peers_ui, edit_path)
2165
+ threading.Thread(target=_bg_publish, daemon=True).start()
2023
2166
  t_name = self.query_one("#team_name_input", Input).value.strip()
2024
2167
  folder_val = self.query_one("#team_folder_input", Input).value.strip()
2025
2168
  url = self.query_one("#team_remote_url_input", Input).value.strip()
@@ -2037,12 +2180,20 @@ class TeamsModal(ModalScreen[None]):
2037
2180
 
2038
2181
  settings = toolbox.load_global_settings()
2039
2182
  registered = settings.get("registered_teams", [])
2183
+ old_team_name = None
2040
2184
  for t in registered:
2041
2185
  if os.path.abspath(os.path.expanduser(t.get("path", ""))) == target_path:
2186
+ old_team_name = t.get("name")
2042
2187
  t["name"] = name
2043
2188
  break
2044
2189
  settings["registered_teams"] = registered
2045
2190
  toolbox.save_global_settings(settings)
2191
+
2192
+ ts = toolbox.load_team_settings() if target_path == toolbox.get_team_dir() else {}
2193
+ if not old_team_name and ts.get("team_name"):
2194
+ old_team_name = ts.get("team_name")
2195
+ ts["team_name"] = name
2196
+ toolbox.save_team_settings(ts)
2046
2197
 
2047
2198
  toolbox.save_team_git_credentials(url, user, pat, team_dir=target_path)
2048
2199
  self.refresh_list()
@@ -2052,6 +2203,13 @@ class TeamsModal(ModalScreen[None]):
2052
2203
  target_path = folder_val if os.path.isabs(folder_val) else os.path.join(str(Path.home()), folder_val)
2053
2204
  name = t_name or os.path.basename(target_path).lstrip(".")
2054
2205
 
2206
+ if readme_text:
2207
+ os.makedirs(target_path, exist_ok=True)
2208
+ try:
2209
+ with open(os.path.join(target_path, "README.md"), "w", encoding="utf-8") as f:
2210
+ f.write(readme_text)
2211
+ except Exception: pass
2212
+
2055
2213
  self.dismiss()
2056
2214
  self.agent_view.switch_team(target_path, display_name=name)
2057
2215
  toolbox.save_team_git_credentials(url, user, pat, team_dir=target_path)
@@ -2100,6 +2258,9 @@ class TeamsModal(ModalScreen[None]):
2100
2258
  import keyring
2101
2259
  keyring.delete_password("Federate", f"git_user_{team_tag}")
2102
2260
  keyring.delete_password("Federate", f"git_pat_{team_tag}")
2261
+ keyring.delete_password("Federate", f"mesh_relay_user_{team_tag}")
2262
+ keyring.delete_password("Federate", f"mesh_relay_pat_{team_tag}")
2263
+ keyring.delete_password("Federate", f"mesh_seed_{team_tag}")
2103
2264
  try:
2104
2265
  import mcp_handler
2105
2266
  mcp_handler._clear_mcp_keyring_chunks(keyring, team_dir=t_path)
@@ -2117,8 +2278,19 @@ class TeamsModal(ModalScreen[None]):
2117
2278
  elif self.editing_path == t_path:
2118
2279
  self.clear_form()
2119
2280
 
2281
+ import team_mesh
2282
+ # Unpublish team manifest and remove from relay repo
2283
+ try:
2284
+ team_mesh.unpublish_team(t_name, team_dir=t_path)
2285
+ except Exception:
2286
+ pass
2287
+
2120
2288
  if choice == "purge":
2121
2289
  import shutil
2290
+ relay_local = team_mesh.get_relay_local_dir(t_path)
2291
+ if os.path.exists(relay_local):
2292
+ try: shutil.rmtree(relay_local, ignore_errors=True)
2293
+ except Exception: pass
2122
2294
  if t_path != str(Path.home()) and os.path.exists(t_path) and len(t_path) > 3:
2123
2295
  try:
2124
2296
  shutil.rmtree(t_path, ignore_errors=True)
@@ -2572,6 +2744,76 @@ class ScheduleModal(ModalScreen[None]):
2572
2744
  self.refresh_list()
2573
2745
  self.notify("Loaded task into inputs for modification.", severity="information")
2574
2746
 
2747
+ class IncomingDispatchModal(ModalScreen[str]):
2748
+ DEFAULT_CSS = """
2749
+ IncomingDispatchModal { align: center middle; background: $background 60%; }
2750
+ #dispatch_dialog { width: 75; height: auto; max-height: 80%; border: thick $accent; background: $surface; padding: 1 2; }
2751
+ .dispatch_header { background: $accent; color: $text; text-style: bold; text-align: center; width: 100%; height: 1; margin-bottom: 1; }
2752
+ .dispatch_body {
2753
+ margin: 1 0;
2754
+ max-height: 14;
2755
+ overflow-y: scroll;
2756
+ border: round $primary;
2757
+ background: $boost;
2758
+ padding: 1;
2759
+ }
2760
+ .dispatch_body Label {
2761
+ text-wrap: wrap;
2762
+ width: 100%;
2763
+ height: auto;
2764
+ }
2765
+ .dispatch_msg { margin: 1 0; text-style: bold; }
2766
+ .buttons { height: auto; align: right middle; margin-top: 1; }
2767
+ .buttons Button { margin-left: 1; }
2768
+ """
2769
+
2770
+ def __init__(self, sender_team: str, instruction: str, attachments: list, **kwargs):
2771
+ super().__init__(**kwargs)
2772
+ self.sender_team = sender_team
2773
+ self.instruction = instruction
2774
+ self.attachments = attachments
2775
+ self.time_left = 300
2776
+ self.timer = None
2777
+
2778
+ def compose(self) -> ComposeResult:
2779
+ with Vertical(id="dispatch_dialog"):
2780
+ yield Label(f" Incoming Dispatch from Team: {self.sender_team}", classes="dispatch_header")
2781
+ with Vertical(classes="dispatch_body"):
2782
+ yield Label(f"[bold cyan]Instruction:[/bold cyan]\n{self.instruction}\n")
2783
+ if self.attachments:
2784
+ files_str = "\n".join(f" • {os.path.basename(f)}" for f in self.attachments)
2785
+ yield Label(f"[bold yellow]Attachments ({len(self.attachments)}):[/bold yellow]\n{files_str}")
2786
+ else:
2787
+ yield Label("[dim]No attachments accompanied this dispatch.[/dim]")
2788
+ yield Label(f"Auto-accepting in [bold yellow]{self.time_left}s[/bold yellow] if no action taken...", id="dispatch_countdown", classes="dispatch_msg")
2789
+ with Horizontal(classes="buttons"):
2790
+ yield Button("Accept & Run", id="accept_btn", variant="success")
2791
+ yield Button("Dismiss", id="dismiss_btn", variant="error")
2792
+
2793
+ def on_mount(self):
2794
+ self.query_one("#accept_btn").focus()
2795
+ self.timer = self.set_interval(1.0, self.update_countdown)
2796
+
2797
+ def update_countdown(self):
2798
+ self.time_left -= 1
2799
+ if self.time_left <= 0:
2800
+ if self.timer:
2801
+ self.timer.stop()
2802
+ self.dismiss("accept")
2803
+ else:
2804
+ try:
2805
+ self.query_one("#dispatch_countdown", Label).update(
2806
+ f"Auto-accepting in [bold yellow]{self.time_left}s[/bold yellow] if no action taken..."
2807
+ )
2808
+ except Exception:
2809
+ pass
2810
+
2811
+ @on(Button.Pressed)
2812
+ def handle_choice(self, event: Button.Pressed):
2813
+ if self.timer:
2814
+ self.timer.stop()
2815
+ self.dismiss("accept" if event.button.id == "accept_btn" else "dismiss")
2816
+
2575
2817
  class ScheduledTaskPromptModal(ModalScreen[str]):
2576
2818
  DEFAULT_CSS = """
2577
2819
  ScheduledTaskPromptModal { align: center middle; background: $background 60%; }
@@ -3981,6 +4223,14 @@ class AIAgentView(Vertical):
3981
4223
  # Reload MCP background sessions for the new team
3982
4224
  toolbox.reload_mcp_servers()
3983
4225
 
4226
+ # Auto-publish identity manifest to relay for the newly activated team
4227
+ def _bg_mesh_init():
4228
+ try:
4229
+ import team_mesh
4230
+ team_mesh.auto_publish_manifest(clean_dir)
4231
+ except Exception: pass
4232
+ threading.Thread(target=_bg_mesh_init, daemon=True).start()
4233
+
3984
4234
  self._write_log(Rule(title=f"[bold #f2a813]TEAM WORKSPACE: {d_name.upper()}", style="dim"))
3985
4235
  self._write_log(get_welcome_banner(self))
3986
4236
  self.update_tokens()
@@ -4985,7 +5235,103 @@ class AIAgentView(Vertical):
4985
5235
  candidate = nxt
4986
5236
  return candidate
4987
5237
 
5238
+ @work(thread=True)
5239
+ def check_mesh_inbox(self):
5240
+ """Wire 3: Polls team channel relay, processes ACKs, and stages incoming dispatches."""
5241
+ if getattr(self, "_inbox_worker_busy", False) or getattr(self, "_dispatch_prompt_active", False) or getattr(self, "_running_agents", None):
5242
+ return
5243
+
5244
+ self._inbox_worker_busy = True
5245
+ try:
5246
+ self._do_check_mesh_inbox()
5247
+ finally:
5248
+ self._inbox_worker_busy = False
5249
+
5250
+ def _do_check_mesh_inbox(self):
5251
+
5252
+ try:
5253
+ import team_mesh
5254
+ except ImportError:
5255
+ return
5256
+
5257
+ team_dir = toolbox.get_team_dir()
5258
+ team_mesh.sync_relay_repo(team_dir)
5259
+ team_mesh.process_acks_and_purge(team_dir)
5260
+
5261
+ my_team = toolbox.get_team_name(team_dir)
5262
+ relay_dir = team_mesh.get_relay_local_dir(team_dir)
5263
+ my_inbox = os.path.join(relay_dir, "messages", my_team)
5264
+ if not os.path.exists(my_inbox):
5265
+ return
5266
+
5267
+ fedmsg_files = [f for f in os.listdir(my_inbox) if f.endswith(".fedmsg")]
5268
+ if not fedmsg_files:
5269
+ return
5270
+
5271
+ remaining_count = len(fedmsg_files) - 1
5272
+ fname = fedmsg_files[0]
5273
+ fpath = os.path.join(my_inbox, fname)
5274
+ processed_path = fpath + ".processed"
5275
+ try:
5276
+ # Rename immediately to prevent duplicate prompts on the next cycle
5277
+ os.replace(fpath, processed_path)
5278
+ with open(processed_path, "rb") as f:
5279
+ raw = f.read()
5280
+
5281
+ header = json.loads(raw.split(b"\n---SIG---\n")[0].decode("utf-8"))
5282
+ msg_id = header["msg_id"]
5283
+ sender_team = header["from_team"]
5284
+
5285
+ stage_dir = os.path.join(team_dir, ".inbox_artifacts")
5286
+ manifest, extracted = team_mesh.unpack_and_decrypt(raw, stage_dir, team_dir=team_dir)
5287
+
5288
+ # Single combined triple-tick ACK to avoid redundant network pushes
5289
+ team_mesh.emit_ack(msg_id, sender_team, status="triple_tick", team_dir=team_dir)
5290
+
5291
+ task_prompt = (
5292
+ f"[Dispatch from Team {sender_team}]: {manifest['instruction']}\n\n"
5293
+ f"Note: To send a response back to Team {sender_team}, use the tool "
5294
+ f"`send_team_dispatch(target_team='{sender_team}', instruction='...')`.\n"
5295
+ f"Note: You will be responding on behalf of your team not personally, so formulate the response accordingly."
5296
+ )
5297
+ if extracted:
5298
+ escaped_files = [f.replace(" ", r"\ ") for f in extracted]
5299
+ task_prompt += "\n\nAttached Files (Pre-staged locally):\n" + "\n".join(f"&{ef}" for ef in escaped_files)
5300
+
5301
+ if remaining_count > 0:
5302
+ self.notify(f"Note: {remaining_count} additional dispatch(es) queued in inbox.", severity="information")
5303
+
5304
+ self._dispatch_prompt_active = True
5305
+ def prompt_user():
5306
+ try:
5307
+ def handle_choice(choice: str):
5308
+ self._dispatch_prompt_active = False
5309
+ if choice == "accept":
5310
+ chat_input = self.query_one("#ai_chat_input", ChatInput)
5311
+ agent_name = getattr(self.active_agent, "name", "Rita")
5312
+ self.on_input_submitted(ChatInput.Submitted(chat_input, f"{task_prompt}"))
5313
+ else:
5314
+ staged_path = os.path.join(".inbox_artifacts", manifest.get("msg_id", ""))
5315
+ self.log_to_ui(
5316
+ f"[dim yellow]Incoming dispatch from Team '{sender_team}' was dismissed.\n"
5317
+ f"• Instruction: {manifest.get('instruction', '')[:120]}\n"
5318
+ f"• Staged files preserved at: {staged_path}[/dim yellow]"
5319
+ )
5320
+ self.app.push_screen(
5321
+ IncomingDispatchModal(sender_team, manifest.get("instruction", ""), extracted),
5322
+ handle_choice
5323
+ )
5324
+ except Exception as pe:
5325
+ self._dispatch_prompt_active = False
5326
+ self.log_to_ui(f"[bold red]Failed to present dispatch modal: {pe}[/bold red]")
5327
+
5328
+ self.app.call_from_thread(prompt_user)
5329
+ return
5330
+ except Exception as e:
5331
+ self.log_to_ui(f"[dim red]Mesh relay verification failed: {e}[/dim red]")
5332
+
4988
5333
  def tick_scheduler(self):
5334
+ self.check_mesh_inbox()
4989
5335
  if getattr(self, "shell_mode", False) or not hasattr(self, "schedule_manager") or getattr(self, "_scheduler_prompt_active", False):
4990
5336
  return
4991
5337
 
@@ -5145,6 +5491,12 @@ class AIAgentView(Vertical):
5145
5491
  if not remote_url or not pat:
5146
5492
  return
5147
5493
 
5494
+ # 1. Auto-publish mesh manifest to channel relay if configured
5495
+ try:
5496
+ import team_mesh
5497
+ team_mesh.auto_publish_manifest(t_dir)
5498
+ except Exception: pass
5499
+
5148
5500
  ok, msg = toolbox.sync_team_remote(action="pull", team_dir=t_dir)
5149
5501
  if ok:
5150
5502
  def _apply_updates():
@@ -427,7 +427,7 @@ def get_executor_core(agent_view, agent_config: AgentConfig):
427
427
  return True
428
428
  return False
429
429
 
430
- raw_tools = [toolbox.search_web, toolbox.perform_research, toolbox.render_pdf, toolbox.update_core_memory, toolbox.save_skill, toolbox.read_skill, toolbox.distill_journey, toolbox.delete_passive_skill, toolbox.list_skills, toolbox.mark_quagmire, toolbox.get_user_clarification, toolbox.search_episodic_memory, toolbox.retrieve_episodic_memory, toolbox.prepare_active_skill, toolbox.finalize_active_skill, toolbox.manage_active_skill, toolbox.fix_active_skill, toolbox.get_toolresult]
430
+ raw_tools = [toolbox.search_web, toolbox.perform_research, toolbox.render_pdf, toolbox.update_core_memory, toolbox.save_skill, toolbox.read_skill, toolbox.distill_journey, toolbox.delete_passive_skill, toolbox.list_skills, toolbox.mark_quagmire, toolbox.get_user_clarification, toolbox.search_episodic_memory, toolbox.retrieve_episodic_memory, toolbox.prepare_active_skill, toolbox.finalize_active_skill, toolbox.manage_active_skill, toolbox.fix_active_skill, toolbox.get_toolresult, toolbox.send_team_dispatch]
431
431
  if is_nomem:
432
432
  raw_tools = [t for t in raw_tools if getattr(t, "name", "") not in NOMEM_EXCLUDED_TOOLS]
433
433
  raw_tools.extend(toolbox.load_dynamic_tools(agent_config.name))