desktop-monkey 0.1.0__py3-none-any.whl

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 (172) hide show
  1. desktop_monkey/__init__.py +3 -0
  2. desktop_monkey/agent/__init__.py +1 -0
  3. desktop_monkey/agent/cli.py +308 -0
  4. desktop_monkey/agent/daemon.py +183 -0
  5. desktop_monkey/agent/enroll.py +226 -0
  6. desktop_monkey/agent/executor.py +310 -0
  7. desktop_monkey/agent/identity.py +31 -0
  8. desktop_monkey/agent/memreport.py +106 -0
  9. desktop_monkey/agent/mesh_client.py +173 -0
  10. desktop_monkey/agent/policy.py +209 -0
  11. desktop_monkey/agent/state.py +149 -0
  12. desktop_monkey/agent/tunnel.py +275 -0
  13. desktop_monkey/cli.py +88 -0
  14. desktop_monkey/commander/__init__.py +1 -0
  15. desktop_monkey/commander/base.py +175 -0
  16. desktop_monkey/commander/cli.py +36 -0
  17. desktop_monkey/commander/config.py +543 -0
  18. desktop_monkey/commander/data/dc-oss-tools.json +868 -0
  19. desktop_monkey/commander/edit.py +326 -0
  20. desktop_monkey/commander/feedback.py +48 -0
  21. desktop_monkey/commander/fs.py +812 -0
  22. desktop_monkey/commander/history.py +193 -0
  23. desktop_monkey/commander/paths.py +93 -0
  24. desktop_monkey/commander/pdf.py +293 -0
  25. desktop_monkey/commander/process.py +761 -0
  26. desktop_monkey/commander/search.py +394 -0
  27. desktop_monkey/commander/server.py +162 -0
  28. desktop_monkey/commander/stats.py +186 -0
  29. desktop_monkey/hooks/__init__.py +65 -0
  30. desktop_monkey/hooks/adapters/__init__.py +46 -0
  31. desktop_monkey/hooks/adapters/claude.py +159 -0
  32. desktop_monkey/hooks/adapters/codex.py +121 -0
  33. desktop_monkey/hooks/adapters/cursor.py +130 -0
  34. desktop_monkey/hooks/cli.py +217 -0
  35. desktop_monkey/hooks/decisions.py +213 -0
  36. desktop_monkey/hooks/engine.py +886 -0
  37. desktop_monkey/hooks/events.py +233 -0
  38. desktop_monkey/hooks/learning_hooks.py +64 -0
  39. desktop_monkey/hooks/manifest.py +156 -0
  40. desktop_monkey/hooks/receipts.py +140 -0
  41. desktop_monkey/hooks/replay.py +70 -0
  42. desktop_monkey/hooks/schema.py +62 -0
  43. desktop_monkey/hooks/schemas/mk.decision.v1.json +170 -0
  44. desktop_monkey/hooks/schemas/mk.event.v1.json +219 -0
  45. desktop_monkey/hooks/schemas/mk.hook-manifest.v1.json +292 -0
  46. desktop_monkey/hooks/schemas/mk.receipt.v1.json +149 -0
  47. desktop_monkey/hooks/trust.py +149 -0
  48. desktop_monkey/learning/brain.py +648 -0
  49. desktop_monkey/learning/mesh_learning.py +76 -0
  50. desktop_monkey/learning/qig_memory_bridge.py +185 -0
  51. desktop_monkey/learning/re_integration.py +155 -0
  52. desktop_monkey/learning/voluntary_llm.py +250 -0
  53. desktop_monkey/learning/weight_space.py +169 -0
  54. desktop_monkey/mesh/__init__.py +35 -0
  55. desktop_monkey/mesh/api.py +111 -0
  56. desktop_monkey/mesh/calibrate.py +106 -0
  57. desktop_monkey/mesh/cli.py +249 -0
  58. desktop_monkey/mesh/cost.py +261 -0
  59. desktop_monkey/mesh/engines/__init__.py +32 -0
  60. desktop_monkey/mesh/engines/base.py +136 -0
  61. desktop_monkey/mesh/engines/llamacpp_rpc.py +926 -0
  62. desktop_monkey/mesh/engines/pipeline.py +41 -0
  63. desktop_monkey/mesh/engines/single.py +32 -0
  64. desktop_monkey/mesh/engines/spec_decode.py +136 -0
  65. desktop_monkey/mesh/explain.py +81 -0
  66. desktop_monkey/mesh/fabric/__init__.py +104 -0
  67. desktop_monkey/mesh/fabric/adapters.py +152 -0
  68. desktop_monkey/mesh/fabric/devices.py +349 -0
  69. desktop_monkey/mesh/fabric/executor.py +629 -0
  70. desktop_monkey/mesh/fabric/graph.py +233 -0
  71. desktop_monkey/mesh/fabric/retry.py +53 -0
  72. desktop_monkey/mesh/fabric/tasks.py +173 -0
  73. desktop_monkey/mesh/fabric/verify.py +151 -0
  74. desktop_monkey/mesh/honesty.py +227 -0
  75. desktop_monkey/mesh/links.py +178 -0
  76. desktop_monkey/mesh/linksvc.py +35 -0
  77. desktop_monkey/mesh/memory.py +586 -0
  78. desktop_monkey/mesh/models.py +161 -0
  79. desktop_monkey/mesh/pools/__init__.py +8 -0
  80. desktop_monkey/mesh/pools/adapters.py +264 -0
  81. desktop_monkey/mesh/pools/compute.py +79 -0
  82. desktop_monkey/mesh/pools/disk.py +159 -0
  83. desktop_monkey/mesh/pools/memory.py +114 -0
  84. desktop_monkey/mesh/pools/objects.py +129 -0
  85. desktop_monkey/mesh/pools/policy.py +321 -0
  86. desktop_monkey/mesh/power.py +162 -0
  87. desktop_monkey/mesh/power_aware_scheduler.py +142 -0
  88. desktop_monkey/mesh/probe.py +617 -0
  89. desktop_monkey/mesh/qig_scheduler.py +80 -0
  90. desktop_monkey/mesh/qig_warp_adapter.py +128 -0
  91. desktop_monkey/mesh/qpu.py +256 -0
  92. desktop_monkey/mesh/quota.py +67 -0
  93. desktop_monkey/mesh/router.py +227 -0
  94. desktop_monkey/mesh/scheduler.py +1014 -0
  95. desktop_monkey/mesh/subscription.py +127 -0
  96. desktop_monkey/mesh/topology.py +391 -0
  97. desktop_monkey/mesh/troop.py +321 -0
  98. desktop_monkey/monitor/__init__.py +6 -0
  99. desktop_monkey/monitor/bridge.py +105 -0
  100. desktop_monkey/monitor/gtk_app.py +187 -0
  101. desktop_monkey/monitor/troop_page.py +271 -0
  102. desktop_monkey/protocol/__init__.py +9 -0
  103. desktop_monkey/protocol/billing.py +86 -0
  104. desktop_monkey/protocol/crypto.py +164 -0
  105. desktop_monkey/protocol/device.py +119 -0
  106. desktop_monkey/protocol/enrol.py +77 -0
  107. desktop_monkey/protocol/frames.py +343 -0
  108. desktop_monkey/protocol/jsonrpc.py +51 -0
  109. desktop_monkey/protocol/mcp.py +55 -0
  110. desktop_monkey/protocol/memory.py +112 -0
  111. desktop_monkey/protocol/versions.py +29 -0
  112. desktop_monkey/qig_nexus/__init__.py +77 -0
  113. desktop_monkey/qig_nexus/catalog.py +1195 -0
  114. desktop_monkey/qig_nexus/primitives.py +1232 -0
  115. desktop_monkey/relay/__init__.py +1 -0
  116. desktop_monkey/relay/__main__.py +21 -0
  117. desktop_monkey/relay/accounts.py +668 -0
  118. desktop_monkey/relay/app.py +132 -0
  119. desktop_monkey/relay/auth.py +300 -0
  120. desktop_monkey/relay/billing.py +363 -0
  121. desktop_monkey/relay/db.py +36 -0
  122. desktop_monkey/relay/devices_http.py +508 -0
  123. desktop_monkey/relay/devtools.py +34 -0
  124. desktop_monkey/relay/hub.py +622 -0
  125. desktop_monkey/relay/interfaces.py +78 -0
  126. desktop_monkey/relay/keys.py +123 -0
  127. desktop_monkey/relay/mcp_http.py +943 -0
  128. desktop_monkey/relay/mesh_http.py +695 -0
  129. desktop_monkey/relay/mesh_runtime.py +389 -0
  130. desktop_monkey/relay/notices.py +109 -0
  131. desktop_monkey/relay/oauth.py +762 -0
  132. desktop_monkey/relay/pages.py +258 -0
  133. desktop_monkey/relay/passwords.py +41 -0
  134. desktop_monkey/relay/schema.py +232 -0
  135. desktop_monkey/relay/security.py +260 -0
  136. desktop_monkey/relay/settings.py +77 -0
  137. desktop_monkey/relay/static.py +78 -0
  138. desktop_monkey/relay/tunnel.py +145 -0
  139. desktop_monkey/relay/usage.py +241 -0
  140. desktop_monkey/relay/wellknown.py +92 -0
  141. desktop_monkey/security/immune_response.py +71 -0
  142. desktop_monkey/security/offensive_nullification.py +42 -0
  143. desktop_monkey/security/pattern_matcher.py +46 -0
  144. desktop_monkey/security/request_extractor.py +86 -0
  145. desktop_monkey/security/request_firewall.py +59 -0
  146. desktop_monkey/security/request_validator.py +88 -0
  147. desktop_monkey/security/self_healing.py +44 -0
  148. desktop_monkey/security/threat_classifier.py +39 -0
  149. desktop_monkey/theme/__init__.py +46 -0
  150. desktop_monkey/theme/assets/SOURCES.json +67 -0
  151. desktop_monkey/theme/assets/favicon-16.ico +0 -0
  152. desktop_monkey/theme/assets/favicon-32.ico +0 -0
  153. desktop_monkey/theme/assets/monitor-128.png +0 -0
  154. desktop_monkey/theme/assets/monitor-32.png +0 -0
  155. desktop_monkey/theme/assets/monitor-64.png +0 -0
  156. desktop_monkey/theme/assets/monitor-icon-256.png +0 -0
  157. desktop_monkey/theme/assets/monkey-ai-rec-dark.png +0 -0
  158. desktop_monkey/theme/assets/monkey-ai-rec-light.png +0 -0
  159. desktop_monkey/theme/assets/monkey-ai-sq-dark.png +0 -0
  160. desktop_monkey/theme/assets/monkey-ai-sq-light.png +0 -0
  161. desktop_monkey/theme/assets/monkey-icon.png +0 -0
  162. desktop_monkey/theme/build.py +232 -0
  163. desktop_monkey/theme/color.py +161 -0
  164. desktop_monkey/theme/generated/desktop-monkey.css +165 -0
  165. desktop_monkey/theme/generated/monkey-monitor-tokens.gtk.css +134 -0
  166. desktop_monkey/theme/tokens.py +124 -0
  167. desktop_monkey-0.1.0.dist-info/METADATA +164 -0
  168. desktop_monkey-0.1.0.dist-info/RECORD +172 -0
  169. desktop_monkey-0.1.0.dist-info/WHEEL +4 -0
  170. desktop_monkey-0.1.0.dist-info/entry_points.txt +3 -0
  171. desktop_monkey-0.1.0.dist-info/licenses/LICENSE +21 -0
  172. desktop_monkey-0.1.0.dist-info/licenses/NOTICE +12 -0
@@ -0,0 +1,3 @@
1
+ """Desktop Monkey - remote MCP + device mesh."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1 @@
1
+ """desktop_monkey.agent"""
@@ -0,0 +1,308 @@
1
+ """Agent CLI - Typer sub-app wired as dmonkey remote|status|revoke|pause|resume|logout."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import httpx
8
+ import typer
9
+
10
+ from desktop_monkey.agent.identity import home_dir
11
+ from desktop_monkey.agent.policy import ensure_default_policy, set_paused_flag
12
+ from desktop_monkey.agent.state import is_paused as _is_paused
13
+ from desktop_monkey.agent.state import load_state, wipe_state
14
+
15
+ app = typer.Typer(help="Device agent - enrol, tunnel, and remote execution")
16
+
17
+
18
+ @app.command("remote")
19
+ def remote(
20
+ server: Annotated[str | None, typer.Option("--server", help="Relay origin URL")] = None,
21
+ name: Annotated[str | None, typer.Option("--name", help="Device name")] = None,
22
+ allow: Annotated[
23
+ list[str] | None, typer.Option("--allow", help="Allowed directory (repeatable)")
24
+ ] = None,
25
+ unrestricted: Annotated[bool, typer.Option("--unrestricted", help="Allow all paths")] = False,
26
+ foreground: Annotated[bool, typer.Option("--foreground", help="Run in foreground")] = False,
27
+ tools: Annotated[
28
+ str | None, typer.Option("--tools", help="Comma-separated tool allow-list")
29
+ ] = None,
30
+ no_browser: Annotated[
31
+ bool, typer.Option("--no-browser", help="Print the enrolment URL; never open a tab")
32
+ ] = False,
33
+ ) -> None:
34
+ """Enrol if needed, then run the tunnel (foreground or daemon)."""
35
+ h = home_dir()
36
+ srv = server or "http://127.0.0.1:8787"
37
+
38
+ # Configure policy first
39
+ allow_list = list(allow) if allow else None
40
+ tools_list = [t.strip() for t in tools.split(",")] if tools else None
41
+ ensure_default_policy(h, allow_paths=allow_list, unrestricted=unrestricted, tools=tools_list)
42
+
43
+ state = load_state(h)
44
+ if state is None:
45
+ # Enrol
46
+ from desktop_monkey.agent.enroll import enrol
47
+
48
+ typer.echo(f"Enrolling device with {srv} ...")
49
+ try:
50
+ state = enrol(srv, device_name=name, home=h, open_browser=False if no_browser else None)
51
+ typer.echo(f"Enrolled as {state.device_id} ({state.device_name})")
52
+ except Exception as exc:
53
+ typer.echo(f"Enrolment failed: {exc}", err=True)
54
+ raise typer.Exit(1) from exc
55
+ else:
56
+ typer.echo(f"Device already enrolled: {state.device_id}")
57
+
58
+ # Run daemon
59
+ from desktop_monkey.agent.daemon import run_blocking, setup_logging
60
+
61
+ setup_logging(h)
62
+ if foreground:
63
+ typer.echo("Running in foreground - press Ctrl+C to stop")
64
+ run_blocking(h)
65
+ else:
66
+ # For now, also run blocking (daemon mode needs OS service integration)
67
+ typer.echo("Running agent (use --foreground to keep in foreground)")
68
+ run_blocking(h)
69
+
70
+
71
+ @app.command("status")
72
+ def status() -> None:
73
+ """Show device enrolment and connection status."""
74
+ h = home_dir()
75
+ state = load_state(h)
76
+ if state is None:
77
+ typer.echo("Not enrolled")
78
+ return
79
+ typer.echo(f"Device: {state.device_name} ({state.device_id})")
80
+ typer.echo(f"Server: {state.server}")
81
+ paused = _is_paused(h)
82
+ typer.echo(f"Paused: {paused}")
83
+ # Check tunnel URL
84
+ if state.tunnel_url:
85
+ typer.echo(f"Tunnel: {state.tunnel_url}")
86
+ # Check config
87
+ from desktop_monkey.agent.policy import load_policy
88
+
89
+ policy = load_policy(h)
90
+ typer.echo(f"Allowed dirs: {policy.allowed_directories}")
91
+ if policy.allowed_tools:
92
+ typer.echo(f"Allowed tools: {policy.allowed_tools}")
93
+ typer.echo(f"Unrestricted: {policy.unrestricted}")
94
+
95
+
96
+ @app.command("revoke")
97
+ def revoke() -> None:
98
+ """Wipe local state and best-effort server revoke."""
99
+ h = home_dir()
100
+ state = load_state(h)
101
+ if state is not None:
102
+ # Best-effort server revoke
103
+ try:
104
+ resp = httpx.post(
105
+ f"{state.server.rstrip('/')}/device/revoke",
106
+ json={"device_id": state.device_id},
107
+ headers={"Authorization": f"Bearer {state.device_token}"},
108
+ timeout=5,
109
+ )
110
+ if resp.status_code == 200:
111
+ typer.echo("Server revoke OK")
112
+ else:
113
+ typer.echo(f"Server revoke returned {resp.status_code} (continuing)")
114
+ except Exception as exc:
115
+ typer.echo(f"Server revoke failed: {exc} (continuing)")
116
+ wipe_state(h)
117
+ typer.echo("Local state wiped")
118
+
119
+
120
+ @app.command("pause")
121
+ def pause() -> None:
122
+ """Pause remote execution (device stays connected but refuses calls)."""
123
+ h = home_dir()
124
+ set_paused_flag(True, h)
125
+ typer.echo("Device paused - remote calls will be refused")
126
+
127
+
128
+ @app.command("resume")
129
+ def resume() -> None:
130
+ """Resume remote execution."""
131
+ h = home_dir()
132
+ set_paused_flag(False, h)
133
+ typer.echo("Device resumed")
134
+
135
+
136
+ @app.command("logout")
137
+ def logout() -> None:
138
+ """Alias for revoke (wipe local state)."""
139
+ h = home_dir()
140
+ state = load_state(h)
141
+ if state is not None:
142
+ try:
143
+ httpx.post(
144
+ f"{state.server.rstrip('/')}/device/revoke",
145
+ json={"device_id": state.device_id},
146
+ headers={"Authorization": f"Bearer {state.device_token}"},
147
+ timeout=5,
148
+ )
149
+ except Exception:
150
+ pass
151
+ wipe_state(h)
152
+ typer.echo("Logged out - local state wiped")
153
+
154
+
155
+ @app.command("memory")
156
+ def memory(
157
+ json_out: Annotated[
158
+ bool, typer.Option("--json", help="Print the full snapshot as JSON")
159
+ ] = False,
160
+ publish_file: Annotated[
161
+ bool, typer.Option("--publish", help="Also write the status file Monkey Monitor reads")
162
+ ] = False,
163
+ ) -> None:
164
+ """How much RAM this agent holds, and how close the host is to Quner's floor."""
165
+ from desktop_monkey.mesh.memory import MemoryGauge, memory_status_path, publish
166
+
167
+ try:
168
+ snap = MemoryGauge.from_env().sample()
169
+ except ValueError as exc:
170
+ typer.echo(f"invalid memory configuration: {exc}", err=True)
171
+ raise typer.Exit(2) from exc
172
+ written = publish(snap, memory_status_path(home_dir())) if publish_file else None
173
+ if json_out:
174
+ typer.echo(snap.model_dump_json(indent=2))
175
+ return
176
+
177
+ gib = 1.0 / 1024
178
+ h, a, q = snap.host, snap.agent, snap.quner
179
+ typer.echo(f"memory level: {snap.level.upper()}")
180
+ for r in snap.reasons:
181
+ typer.echo(f" - {r}")
182
+ if h.available_frac is not None and h.total_mib and h.available_mib is not None:
183
+ typer.echo(
184
+ f"host: {h.available_mib * gib:.1f} of {h.total_mib * gib:.1f} GiB available "
185
+ f"({h.available_frac * 100:.1f}%)"
186
+ + (f", swap used {h.swap_used_mib * gib:.1f} GiB" if h.swap_used_mib else "")
187
+ )
188
+ if h.psi_some_avg10 is not None:
189
+ typer.echo(
190
+ f" stall (PSI avg10): some {h.psi_some_avg10:.1f}% full {h.psi_full_avg10 or 0.0:.1f}%"
191
+ )
192
+ else:
193
+ typer.echo("host: memory could not be read")
194
+ pss = "n/a" if a.pss_mib is None else f"{a.pss_mib:.0f} MiB"
195
+ partial = "" if a.pss_complete or a.pss_mib is None else " (partial)"
196
+ typer.echo(
197
+ f"agent: {a.process_count} process(es), {pss} PSS{partial}"
198
+ + (f", {a.share_of_host_pct:.1f}% of host" if a.share_of_host_pct is not None else "")
199
+ )
200
+ typer.echo(f" RSS {a.rss_mib:.0f} MiB - {a.rss_note}")
201
+ if a.budget_mib is not None:
202
+ typer.echo(
203
+ f" budget {a.budget_mib:.0f} MiB ({'OVER' if a.over_budget else 'within'})"
204
+ )
205
+ for p in a.processes[:6]:
206
+ pp = " n/a" if p.pss_mib is None else f"{p.pss_mib:>6.0f}"
207
+ typer.echo(
208
+ f" pid {p.pid:<7} {p.name[:22]:<22} PSS {pp} MiB RSS {p.rss_mib:>6.0f} MiB"
209
+ )
210
+ typer.echo(
211
+ "quner: "
212
+ + (
213
+ f"memguard '{q.action}' (warns {q.warn_frac * 100:.0f}%, kills {q.kill_frac * 100:.0f}% available)"
214
+ if q.readable
215
+ else "status file not readable"
216
+ )
217
+ )
218
+ if snap.quner_victim is not None:
219
+ v = snap.quner_victim
220
+ typer.echo(
221
+ f" would pick: {v.name} pid {v.pid} ({v.rss_mib:.0f} MiB RSS)"
222
+ + (" <-- THIS AGENT" if v.ours else "")
223
+ )
224
+ for n in snap.notes:
225
+ typer.echo(f"note: {n}")
226
+ if written:
227
+ typer.echo(f"published: {written}")
228
+
229
+
230
+ @app.command("learn")
231
+ def learn(
232
+ action: Annotated[
233
+ str, typer.Argument(help="Action: observe, sleep, jettison, refine, recall, stats")
234
+ ] = "stats",
235
+ query: Annotated[str | None, typer.Option("--query", "-q", help="Query for recall")] = None,
236
+ content: Annotated[
237
+ str | None, typer.Option("--content", "-c", help="Content to observe")
238
+ ] = None,
239
+ source: Annotated[
240
+ str | None, typer.Option("--source", "-s", help="Source for observation")
241
+ ] = None,
242
+ ) -> None:
243
+ """Brain-like learning system for the mesh.
244
+
245
+ Actions:
246
+ observe - Observe and learn from information
247
+ sleep - Run a sleep/dream cycle for consolidation
248
+ jettison - Jettison low-salience memories
249
+ refine - Refine memory pathways
250
+ recall - Recall relevant memories (requires --query)
251
+ stats - Show learning statistics
252
+ """
253
+ from desktop_monkey.learning.mesh_learning import MeshLearning
254
+
255
+ h = home_dir()
256
+ state = load_state(h)
257
+ device_id = state.device_id if state else "local"
258
+
259
+ learner = MeshLearning(device_id)
260
+
261
+ if action == "observe":
262
+ if not content:
263
+ typer.echo("Error: --content required for observe", err=True)
264
+ raise typer.Exit(1)
265
+ src = source or "cli"
266
+ entry_id = learner.observe_event(src, {"content": content})
267
+ if entry_id:
268
+ typer.echo(f"Retained as {entry_id}")
269
+ else:
270
+ typer.echo("Observed but not retained (low salience)")
271
+
272
+ elif action == "sleep":
273
+ typer.echo("Running sleep/dream cycle...")
274
+ learner.sleep_cycle()
275
+ typer.echo("Sleep cycle complete")
276
+
277
+ elif action == "jettison":
278
+ typer.echo("Jettisoning low-salience memories...")
279
+ learner.jettison_memories()
280
+ typer.echo("Done")
281
+
282
+ elif action == "refine":
283
+ typer.echo("Refining memory pathways...")
284
+ learner.refine_pathways()
285
+ typer.echo("Done")
286
+
287
+ elif action == "recall":
288
+ if not query:
289
+ typer.echo("Error: --query required for recall", err=True)
290
+ raise typer.Exit(1)
291
+ memories = learner.recall_relevant(query, top_k=5)
292
+ if not memories:
293
+ typer.echo("No relevant memories found")
294
+ else:
295
+ for i, mem in enumerate(memories, 1):
296
+ typer.echo(f"{i}. [{mem.entry_id}] {mem.content[:100]}...")
297
+ typer.echo(f" strength={mem.strength:.2f} accesses={mem.access_count}")
298
+
299
+ elif action == "stats":
300
+ stats = learner.get_learning_stats()
301
+ typer.echo(f"Observations: {stats['observations']}")
302
+ typer.echo(f"Memories: {stats['memories']}")
303
+ typer.echo(f"Dream cycles: {stats['dream_cycles']}")
304
+ typer.echo(f"Avg salience: {stats['avg_salience']:.3f}")
305
+
306
+ else:
307
+ typer.echo(f"Unknown action: {action}", err=True)
308
+ raise typer.Exit(1)
@@ -0,0 +1,183 @@
1
+ """Daemon - orchestrates tunnel + executor + logging."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from logging.handlers import RotatingFileHandler
8
+ from pathlib import Path
9
+ from typing import Any
10
+
11
+ from desktop_monkey.agent.executor import Executor, SeenJtis
12
+ from desktop_monkey.agent.identity import home_dir
13
+ from desktop_monkey.agent.memreport import MemoryReporter
14
+ from desktop_monkey.agent.policy import load_policy
15
+ from desktop_monkey.agent.state import load_state, log_file, wipe_state
16
+ from desktop_monkey.agent.tunnel import Tunnel
17
+ from desktop_monkey.mesh.memory import MemoryGauge, memory_status_path
18
+ from desktop_monkey.protocol import crypto
19
+ from desktop_monkey.protocol.frames import Revoke
20
+
21
+ # Never log secrets - filter
22
+ _SENSITIVE_KEYS = {
23
+ "device_token",
24
+ "refresh_token",
25
+ "ed25519_priv",
26
+ "x25519_priv",
27
+ "token",
28
+ "secret",
29
+ }
30
+
31
+
32
+ class _SecretFilter(logging.Filter):
33
+ """Redact sensitive values from log records."""
34
+
35
+ def filter(self, record: logging.LogRecord) -> bool:
36
+ msg = record.getMessage().lower()
37
+ for key in _SENSITIVE_KEYS:
38
+ if key in msg:
39
+ # Don't emit lines that look like they contain secrets
40
+ # But we use structured logging without secret values, so this is safety net
41
+ pass
42
+ return True
43
+
44
+
45
+ def setup_logging(home: Path | None = None) -> logging.Logger:
46
+ """Configure structured logging to agent.log (rotating), never logging args/results/tokens."""
47
+ h = home or home_dir()
48
+ h.mkdir(parents=True, exist_ok=True)
49
+ lf = log_file(home)
50
+ logger = logging.getLogger("desktop_monkey.agent")
51
+ logger.setLevel(logging.INFO)
52
+ # Avoid duplicate handlers
53
+ if not any(isinstance(ha, RotatingFileHandler) for ha in logger.handlers):
54
+ handler = RotatingFileHandler(
55
+ str(lf),
56
+ maxBytes=5 * 1024 * 1024,
57
+ backupCount=3,
58
+ encoding="utf-8",
59
+ )
60
+ fmt = logging.Formatter("%(asctime)s %(levelname)s %(name)s %(message)s")
61
+ handler.setFormatter(fmt)
62
+ handler.addFilter(_SecretFilter())
63
+ logger.addHandler(handler)
64
+ # Also prevent propagation to root
65
+ logger.propagate = False
66
+ return logger
67
+
68
+
69
+ def _build_executor(
70
+ device_id: str,
71
+ seal_key: crypto.SealKey | None,
72
+ home: Path | None = None,
73
+ registry: Any = None,
74
+ signing_key: crypto.SigningKey | None = None,
75
+ memory: Any = None,
76
+ ) -> Executor:
77
+ """Build executor with policy and pause check."""
78
+ from desktop_monkey.agent.state import is_paused
79
+
80
+ policy = load_policy(home)
81
+
82
+ # Build registry if not provided
83
+ if registry is None:
84
+ try:
85
+ from desktop_monkey.commander.base import build_registry
86
+
87
+ registry = build_registry()
88
+ except Exception:
89
+ registry = None
90
+
91
+ def _is_paused() -> bool:
92
+ return is_paused(home)
93
+
94
+ def _on_revoke(_frame: Revoke) -> None:
95
+ wipe_state(home)
96
+
97
+ seen = SeenJtis(max_size=10_000, ttl_s=120)
98
+
99
+ executor = Executor(
100
+ device_id=device_id,
101
+ device_seal_key=seal_key,
102
+ registry=registry,
103
+ policy=policy,
104
+ seen_jtis=seen,
105
+ is_paused_fn=_is_paused,
106
+ on_revoke=_on_revoke,
107
+ device_signing_key=signing_key,
108
+ memory=memory,
109
+ )
110
+ return executor
111
+
112
+
113
+ async def run_daemon(
114
+ home: Path | None = None,
115
+ registry: Any = None,
116
+ ) -> None:
117
+ """Run the agent daemon (tunnel + executor) until stopped."""
118
+ h = home or home_dir()
119
+ state = load_state(h)
120
+ if state is None:
121
+ raise RuntimeError("device not enrolled - run 'dmonkey remote' first")
122
+
123
+ logger = setup_logging(h)
124
+ logger.info("agent starting device_id=%s server=%s", state.device_id, state.server)
125
+
126
+ # Build keys from stored state
127
+ signing_key = crypto.SigningKey.from_b64(state.ed25519_priv)
128
+ seal_key = crypto.SealKey.from_b64(state.x25519_priv, state.x25519_pub)
129
+
130
+ # Memory self-awareness: one gauge feeds admission control, ping telemetry, the status
131
+ # file Monkey Monitor reads, and the tunnel's pressure/heartbeat frames.
132
+ try:
133
+ gauge = MemoryGauge.from_env()
134
+ except ValueError as exc:
135
+ raise RuntimeError(f"invalid memory configuration: {exc}") from exc
136
+
137
+ executor = _build_executor(
138
+ state.device_id, seal_key, h, registry=registry, signing_key=signing_key, memory=gauge
139
+ )
140
+
141
+ def _handle_revoke_sync(_frame: Revoke) -> None:
142
+ logger.info("revoke received - wiping state")
143
+ wipe_state(h)
144
+
145
+ # Wire revoke handler that actually wipes
146
+ executor.on_revoke = _handle_revoke_sync # type: ignore[assignment]
147
+
148
+ tunnel = Tunnel(
149
+ server=state.server,
150
+ device_id=state.device_id,
151
+ signing_key=signing_key,
152
+ executor=executor,
153
+ tunnel_url=state.tunnel_url,
154
+ )
155
+
156
+ reporter = MemoryReporter(gauge, path=memory_status_path(h), send_frame=tunnel.send_frame)
157
+ reporter_task = reporter.start()
158
+ first = gauge.latest(max_age_s=0)
159
+ logger.info(
160
+ "memory: agent %s MiB PSS across %d process(es), host %.0f%% available, level %s",
161
+ first.agent.pss_mib,
162
+ first.agent.process_count,
163
+ 100 * (first.host.available_frac or 0.0),
164
+ first.level,
165
+ )
166
+
167
+ logger.info("connecting to %s", tunnel.ws_url)
168
+ try:
169
+ await tunnel.run_forever()
170
+ except asyncio.CancelledError:
171
+ logger.info("agent stopped")
172
+ raise
173
+ finally:
174
+ await reporter.stop(reporter_task)
175
+ logger.info("agent exiting")
176
+
177
+
178
+ def run_blocking(home: Path | None = None, registry: Any = None) -> None:
179
+ """Synchronous entry point for CLI --foreground mode."""
180
+ try:
181
+ asyncio.run(run_daemon(home=home, registry=registry))
182
+ except KeyboardInterrupt:
183
+ pass