meshcode 1.7.0__tar.gz → 1.8.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 (28) hide show
  1. {meshcode-1.7.0 → meshcode-1.8.2}/PKG-INFO +1 -1
  2. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/__init__.py +1 -1
  3. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/comms_v4.py +36 -1
  4. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/backend.py +3 -1
  5. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/server.py +18 -2
  6. meshcode-1.8.2/meshcode/preferences.py +171 -0
  7. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/run_agent.py +10 -2
  8. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/PKG-INFO +1 -1
  9. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/SOURCES.txt +1 -0
  10. {meshcode-1.7.0 → meshcode-1.8.2}/pyproject.toml +1 -1
  11. {meshcode-1.7.0 → meshcode-1.8.2}/README.md +0 -0
  12. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/cli.py +0 -0
  13. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/invites.py +0 -0
  14. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/launcher.py +0 -0
  15. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/launcher_install.py +0 -0
  16. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/__init__.py +0 -0
  17. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/__main__.py +0 -0
  18. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/realtime.py +0 -0
  19. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/test_backend.py +0 -0
  20. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/meshcode_mcp/test_realtime.py +0 -0
  21. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/protocol_v2.py +0 -0
  22. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/secrets.py +0 -0
  23. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode/setup_clients.py +0 -0
  24. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/dependency_links.txt +0 -0
  25. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/entry_points.txt +0 -0
  26. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/requires.txt +0 -0
  27. {meshcode-1.7.0 → meshcode-1.8.2}/meshcode.egg-info/top_level.txt +0 -0
  28. {meshcode-1.7.0 → meshcode-1.8.2}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.7.0
3
+ Version: 1.8.2
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -1,2 +1,2 @@
1
1
  """MeshCode — Real-time communication between AI agents."""
2
- __version__ = "1.7.0"
2
+ __version__ = "1.8.2"
@@ -1946,9 +1946,14 @@ if __name__ == "__main__":
1946
1946
  agent = sys.argv[2]
1947
1947
  proj_override = flags.get("project")
1948
1948
  editor_override = flags.get("editor")
1949
+ perm_override = flags.get("permission") # bypass | safe | ask
1950
+ if "--bypass" in sys.argv:
1951
+ perm_override = "bypass"
1952
+ elif "--safe" in sys.argv:
1953
+ perm_override = "safe"
1949
1954
  import importlib
1950
1955
  _run = importlib.import_module("meshcode.run_agent").run
1951
- sys.exit(_run(agent, project=proj_override, editor_override=editor_override))
1956
+ sys.exit(_run(agent, project=proj_override, editor_override=editor_override, permission_override=perm_override))
1952
1957
 
1953
1958
  elif cmd == "invite":
1954
1959
  # meshcode invite <project> <agent> [--role "..."] [--days N]
@@ -2017,6 +2022,36 @@ if __name__ == "__main__":
2017
2022
  sys.exit(1)
2018
2023
  login(key)
2019
2024
 
2025
+ elif cmd == "prefs":
2026
+ # meshcode prefs permission-mode [bypass|safe|ask] (no arg = show)
2027
+ # meshcode prefs reset
2028
+ from meshcode.preferences import (
2029
+ get_permission_mode, set_permission_mode, reset_permission_mode,
2030
+ VALID_PERMISSION_MODES,
2031
+ )
2032
+ sub = sys.argv[2] if len(sys.argv) > 2 else ""
2033
+ if sub == "permission-mode":
2034
+ if len(sys.argv) > 3:
2035
+ mode = sys.argv[3].lower()
2036
+ if mode not in VALID_PERMISSION_MODES:
2037
+ print(f"[ERROR] mode must be one of: {', '.join(sorted(VALID_PERMISSION_MODES))}")
2038
+ sys.exit(1)
2039
+ ok = set_permission_mode(mode)
2040
+ print(f"[meshcode] permission_mode = {mode}" if ok else "[ERROR] failed to save")
2041
+ sys.exit(0 if ok else 1)
2042
+ else:
2043
+ cur = get_permission_mode()
2044
+ print(f"permission_mode: {cur or '(unset — will prompt on next run)'}")
2045
+ sys.exit(0)
2046
+ elif sub == "reset":
2047
+ reset_permission_mode()
2048
+ print("[meshcode] preferences reset")
2049
+ sys.exit(0)
2050
+ else:
2051
+ print("Usage: meshcode prefs permission-mode [bypass|safe|ask]")
2052
+ print(" meshcode prefs reset")
2053
+ sys.exit(1)
2054
+
2020
2055
  elif cmd == "launcher":
2021
2056
  # meshcode launcher {install|uninstall|start|stop|restart|status|logs|test}
2022
2057
  try:
@@ -164,7 +164,7 @@ def register_agent(project: str, name: str, role: str = "") -> Dict:
164
164
  }
165
165
 
166
166
 
167
- def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None) -> Dict:
167
+ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any, msg_type: str = "msg", parent_msg_id: Optional[str] = None, sensitive: bool = False) -> Dict:
168
168
  if not isinstance(payload, dict):
169
169
  payload = {"text": str(payload)}
170
170
  msg = {
@@ -177,6 +177,8 @@ def send_message(project_id: str, from_agent: str, to_agent: str, payload: Any,
177
177
  }
178
178
  if parent_msg_id:
179
179
  msg["parent_msg_id"] = parent_msg_id
180
+ if sensitive:
181
+ msg["is_sensitive"] = True
180
182
  result = sb_insert("mc_messages", msg)
181
183
  if isinstance(result, dict) and result.get("_error"):
182
184
  return {"error": result["_error"]}
@@ -363,6 +363,14 @@ meshcode_wait / meshcode_check / meshcode_read now includes an `id` field
363
363
  (use it for in_reply_to) and a `parent_id` field (so you can see whether
364
364
  the message you received is itself a reply to something).
365
365
 
366
+ SENSITIVE DATA: when sharing API keys, credentials, customer data, personal
367
+ information, secrets, or anything that should not be in a public log, set
368
+ sensitive=True in your meshcode_send call. Sensitive messages are NEVER
369
+ included in public meshwork exports, even if the meshwork owner publishes
370
+ the meshwork. The flag is enforced at the database level — there is no way
371
+ for a public viewer to read sensitive messages. Use this liberally — false
372
+ positives are harmless, false negatives are a privacy leak.
373
+
366
374
  LOOP-SAFETY RULES (IMPORTANT — prevent token-burning ping-pong):
367
375
  - Do NOT reply "Done" / "OK" / "Got it" to every message. If a message
368
376
  is a status update or pure ack, just process it and call meshcode_wait
@@ -505,7 +513,8 @@ except Exception:
505
513
  # ----------------- TOOLS -----------------
506
514
 
507
515
  @mcp.tool()
508
- def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None) -> Dict[str, Any]:
516
+ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None,
517
+ sensitive: bool = False) -> Dict[str, Any]:
509
518
  """Send a message to another agent in the meshwork.
510
519
 
511
520
  If you only have plain text, just pass it as a string and it will be
@@ -520,6 +529,13 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None) -> D
520
529
  message is a threaded reply — the dashboard renders it under the
521
530
  original. Use this especially when replying to one of several
522
531
  outstanding broadcasts so humans can tell which is which.
532
+ sensitive: If True, this message will NEVER appear in a public meshwork
533
+ export, regardless of whether the meshwork is published. ALWAYS
534
+ set this to True when sharing API keys, credentials, customer
535
+ data, secrets, personal information, or anything that should not
536
+ be visible to anyone outside the meshwork. The owner can publish
537
+ the meshwork without leaking sensitive messages, because the
538
+ sensitive flag is enforced at the database level.
523
539
  """
524
540
  if isinstance(message, str):
525
541
  payload: Dict[str, Any] = {"text": message}
@@ -528,7 +544,7 @@ def meshcode_send(to: str, message: Any, in_reply_to: Optional[str] = None) -> D
528
544
  else:
529
545
  payload = {"text": str(message)}
530
546
  return be.send_message(_PROJECT_ID, AGENT_NAME, to, payload, msg_type="msg",
531
- parent_msg_id=in_reply_to)
547
+ parent_msg_id=in_reply_to, sensitive=sensitive)
532
548
 
533
549
 
534
550
  @mcp.tool()
@@ -0,0 +1,171 @@
1
+ """Per-user CLI preferences for MeshCode.
2
+
3
+ Stores small non-secret settings (like permission_mode for `meshcode run`)
4
+ in ~/.meshcode/preferences.json with mode 600. Not the same as the API key
5
+ storage in secrets.py — that goes to the OS keychain. This file is for
6
+ opinion/UX preferences only, no credentials.
7
+
8
+ Currently tracked:
9
+ permission_mode one of "bypass" | "safe" | "ask" | None (unset)
10
+ - "bypass": meshcode run passes --dangerously-skip-permissions
11
+ to Claude Code so the autonomous loop runs without prompts.
12
+ - "safe": meshcode run does NOT pass that flag, so Claude
13
+ prompts the user to approve every tool call. Kills the
14
+ autonomous loop in practice — exists for paranoid users.
15
+ - "ask": prompt every launch.
16
+ - None: never set; meshcode run prompts on first use,
17
+ saves the choice, and uses it from then on.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ import json
22
+ import os
23
+ import sys
24
+ import time
25
+ from pathlib import Path
26
+ from typing import Any, Dict, Optional
27
+
28
+ PREFS_PATH = Path.home() / ".meshcode" / "preferences.json"
29
+
30
+ VALID_PERMISSION_MODES = {"bypass", "safe", "ask"}
31
+ DEFAULT_PERMISSION_MODE_FOR_NON_TTY = "bypass"
32
+
33
+
34
+ def load_prefs() -> Dict[str, Any]:
35
+ """Load the prefs file. Returns {} if missing or unparseable."""
36
+ if not PREFS_PATH.exists():
37
+ return {}
38
+ try:
39
+ return json.loads(PREFS_PATH.read_text())
40
+ except Exception:
41
+ return {}
42
+
43
+
44
+ def save_prefs(prefs: Dict[str, Any]) -> bool:
45
+ """Atomic write of the prefs file with mode 600."""
46
+ try:
47
+ PREFS_PATH.parent.mkdir(parents=True, exist_ok=True)
48
+ tmp = PREFS_PATH.with_suffix(".tmp")
49
+ tmp.write_text(json.dumps(prefs, indent=2))
50
+ try:
51
+ os.chmod(tmp, 0o600)
52
+ except Exception:
53
+ pass
54
+ tmp.replace(PREFS_PATH)
55
+ return True
56
+ except Exception as e:
57
+ print(f"[meshcode] WARNING: could not save prefs: {e}", file=sys.stderr)
58
+ return False
59
+
60
+
61
+ def get_permission_mode() -> Optional[str]:
62
+ """Returns 'bypass' / 'safe' / 'ask' / None."""
63
+ val = load_prefs().get("permission_mode")
64
+ if isinstance(val, str) and val in VALID_PERMISSION_MODES:
65
+ return val
66
+ return None
67
+
68
+
69
+ def set_permission_mode(mode: str) -> bool:
70
+ """Persist a new permission mode. Returns True on success."""
71
+ if mode not in VALID_PERMISSION_MODES:
72
+ print(f"[meshcode] ERROR: invalid permission mode '{mode}'. Use: {', '.join(sorted(VALID_PERMISSION_MODES))}", file=sys.stderr)
73
+ return False
74
+ prefs = load_prefs()
75
+ prefs["permission_mode"] = mode
76
+ prefs["permission_mode_set_at"] = int(time.time())
77
+ return save_prefs(prefs)
78
+
79
+
80
+ def reset_permission_mode() -> bool:
81
+ """Forget the permission mode so the next launch prompts again."""
82
+ prefs = load_prefs()
83
+ prefs.pop("permission_mode", None)
84
+ prefs.pop("permission_mode_set_at", None)
85
+ return save_prefs(prefs)
86
+
87
+
88
+ def prompt_permission_mode() -> str:
89
+ """Interactive first-run prompt. Returns the chosen mode and saves it.
90
+
91
+ If stdin is not a TTY (CI, automation, no terminal), silently picks
92
+ DEFAULT_PERMISSION_MODE_FOR_NON_TTY ('bypass') and saves it. The user
93
+ can change it later with `meshcode prefs permission-mode <mode>`.
94
+ """
95
+ if not sys.stdin.isatty():
96
+ set_permission_mode(DEFAULT_PERMISSION_MODE_FOR_NON_TTY)
97
+ return DEFAULT_PERMISSION_MODE_FOR_NON_TTY
98
+
99
+ print("", file=sys.stderr)
100
+ print("[meshcode] PERMISSION MODE — choose once (saved for future runs)", file=sys.stderr)
101
+ print("[meshcode]", file=sys.stderr)
102
+ print("[meshcode] Mesh agents work best with bypass mode: tools run without", file=sys.stderr)
103
+ print("[meshcode] confirmation prompts, so the autonomous loop is not interrupted.", file=sys.stderr)
104
+ print("[meshcode] Equivalent to running `claude --dangerously-skip-permissions`.", file=sys.stderr)
105
+ print("[meshcode]", file=sys.stderr)
106
+ print("[meshcode] In bypass mode, your agents can run shell commands, edit files,", file=sys.stderr)
107
+ print("[meshcode] and access the network without asking. Only use bypass in", file=sys.stderr)
108
+ print("[meshcode] directories where you're OK with the agent acting as you.", file=sys.stderr)
109
+ print("[meshcode]", file=sys.stderr)
110
+ print("[meshcode] [B] Bypass mode (recommended for autonomous mesh agents)", file=sys.stderr)
111
+ print("[meshcode] [s] Safe mode (you approve every tool call — kills the loop)", file=sys.stderr)
112
+ print("[meshcode] [a] Ask me each launch", file=sys.stderr)
113
+ print("[meshcode]", file=sys.stderr)
114
+ try:
115
+ ans = input("[meshcode] Pick [B/s/a] (default B): ").strip().lower()
116
+ except (EOFError, KeyboardInterrupt):
117
+ ans = ""
118
+
119
+ if ans in ("", "b", "bypass"):
120
+ chosen = "bypass"
121
+ elif ans in ("s", "safe"):
122
+ chosen = "safe"
123
+ elif ans in ("a", "ask"):
124
+ chosen = "ask"
125
+ else:
126
+ print(f"[meshcode] '{ans}' not recognized — defaulting to bypass.", file=sys.stderr)
127
+ chosen = "bypass"
128
+
129
+ set_permission_mode(chosen)
130
+ print(f"[meshcode] ✓ Saved as '{chosen}'. Change later with: meshcode prefs permission-mode <mode>", file=sys.stderr)
131
+ print("", file=sys.stderr)
132
+ return chosen
133
+
134
+
135
+ def resolve_permission_mode(per_launch_override: Optional[str] = None) -> str:
136
+ """Resolve the effective permission mode for a single launch.
137
+
138
+ Order:
139
+ 1. per-launch override (--bypass / --safe flag) wins
140
+ 2. saved preference
141
+ 3. interactive prompt (saves choice if user is at a TTY)
142
+ 4. fallback to 'bypass' for non-TTY environments
143
+ """
144
+ if per_launch_override in VALID_PERMISSION_MODES:
145
+ return per_launch_override
146
+
147
+ saved = get_permission_mode()
148
+ if saved == "ask":
149
+ # The user opted into being prompted every launch
150
+ return prompt_permission_mode_quick()
151
+ if saved in ("bypass", "safe"):
152
+ return saved
153
+
154
+ # Unset → first-run prompt + save
155
+ return prompt_permission_mode()
156
+
157
+
158
+ def prompt_permission_mode_quick() -> str:
159
+ """One-line prompt for users who picked 'ask each launch'.
160
+
161
+ Does NOT save — the saved preference stays as 'ask'.
162
+ """
163
+ if not sys.stdin.isatty():
164
+ return DEFAULT_PERMISSION_MODE_FOR_NON_TTY
165
+ try:
166
+ ans = input("[meshcode] Permission mode for this launch [B/s] (default B): ").strip().lower()
167
+ except (EOFError, KeyboardInterrupt):
168
+ ans = ""
169
+ if ans in ("s", "safe"):
170
+ return "safe"
171
+ return "bypass"
@@ -25,6 +25,8 @@ import sys
25
25
  from pathlib import Path
26
26
  from typing import Optional, Tuple
27
27
 
28
+ from .preferences import resolve_permission_mode
29
+
28
30
  WORKSPACES_ROOT = Path.home() / "meshcode"
29
31
  REGISTRY_PATH = WORKSPACES_ROOT / ".registry.json"
30
32
 
@@ -97,7 +99,7 @@ def _detect_editor() -> Optional[str]:
97
99
  return None
98
100
 
99
101
 
100
- def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None) -> int:
102
+ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str] = None, permission_override: Optional[str] = None) -> int:
101
103
  """Launch the user's editor with ONLY the named agent's MCP server loaded."""
102
104
  found = _find_agent_workspace(agent, project)
103
105
  if not found:
@@ -121,12 +123,18 @@ def run(agent: str, project: Optional[str] = None, editor_override: Optional[str
121
123
  if editor == "claude":
122
124
  # Claude Code: pass --mcp-config to point at the workspace's .mcp.json
123
125
  # and --strict-mcp-config so it ignores ~/.claude.json's mcpServers.
126
+ mode = resolve_permission_mode(permission_override)
124
127
  cmd = [
125
128
  editor,
126
129
  "--mcp-config", str(ws / ".mcp.json"),
127
130
  "--strict-mcp-config",
128
- "--dangerously-skip-permissions",
129
131
  ]
132
+ if mode == "bypass":
133
+ cmd.append("--dangerously-skip-permissions")
134
+ print(f"[meshcode] Permission mode: bypass (autonomous loop, no prompts)")
135
+ else:
136
+ print(f"[meshcode] Permission mode: safe (Claude will prompt for every tool call)")
137
+ print(f"[meshcode] Tip: change with `meshcode prefs permission-mode bypass`")
130
138
  try:
131
139
  os.chdir(ws)
132
140
  except Exception:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: meshcode
3
- Version: 1.7.0
3
+ Version: 1.8.2
4
4
  Summary: Real-time communication between AI agents — Supabase-backed CLI
5
5
  Author-email: MeshCode <hello@meshcode.io>
6
6
  License: MIT
@@ -6,6 +6,7 @@ meshcode/comms_v4.py
6
6
  meshcode/invites.py
7
7
  meshcode/launcher.py
8
8
  meshcode/launcher_install.py
9
+ meshcode/preferences.py
9
10
  meshcode/protocol_v2.py
10
11
  meshcode/run_agent.py
11
12
  meshcode/secrets.py
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "meshcode"
7
- version = "1.7.0"
7
+ version = "1.8.2"
8
8
  description = "Real-time communication between AI agents — Supabase-backed CLI"
9
9
  readme = "README.md"
10
10
  license = {text = "MIT"}
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes