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.
- desktop_monkey/__init__.py +3 -0
- desktop_monkey/agent/__init__.py +1 -0
- desktop_monkey/agent/cli.py +308 -0
- desktop_monkey/agent/daemon.py +183 -0
- desktop_monkey/agent/enroll.py +226 -0
- desktop_monkey/agent/executor.py +310 -0
- desktop_monkey/agent/identity.py +31 -0
- desktop_monkey/agent/memreport.py +106 -0
- desktop_monkey/agent/mesh_client.py +173 -0
- desktop_monkey/agent/policy.py +209 -0
- desktop_monkey/agent/state.py +149 -0
- desktop_monkey/agent/tunnel.py +275 -0
- desktop_monkey/cli.py +88 -0
- desktop_monkey/commander/__init__.py +1 -0
- desktop_monkey/commander/base.py +175 -0
- desktop_monkey/commander/cli.py +36 -0
- desktop_monkey/commander/config.py +543 -0
- desktop_monkey/commander/data/dc-oss-tools.json +868 -0
- desktop_monkey/commander/edit.py +326 -0
- desktop_monkey/commander/feedback.py +48 -0
- desktop_monkey/commander/fs.py +812 -0
- desktop_monkey/commander/history.py +193 -0
- desktop_monkey/commander/paths.py +93 -0
- desktop_monkey/commander/pdf.py +293 -0
- desktop_monkey/commander/process.py +761 -0
- desktop_monkey/commander/search.py +394 -0
- desktop_monkey/commander/server.py +162 -0
- desktop_monkey/commander/stats.py +186 -0
- desktop_monkey/hooks/__init__.py +65 -0
- desktop_monkey/hooks/adapters/__init__.py +46 -0
- desktop_monkey/hooks/adapters/claude.py +159 -0
- desktop_monkey/hooks/adapters/codex.py +121 -0
- desktop_monkey/hooks/adapters/cursor.py +130 -0
- desktop_monkey/hooks/cli.py +217 -0
- desktop_monkey/hooks/decisions.py +213 -0
- desktop_monkey/hooks/engine.py +886 -0
- desktop_monkey/hooks/events.py +233 -0
- desktop_monkey/hooks/learning_hooks.py +64 -0
- desktop_monkey/hooks/manifest.py +156 -0
- desktop_monkey/hooks/receipts.py +140 -0
- desktop_monkey/hooks/replay.py +70 -0
- desktop_monkey/hooks/schema.py +62 -0
- desktop_monkey/hooks/schemas/mk.decision.v1.json +170 -0
- desktop_monkey/hooks/schemas/mk.event.v1.json +219 -0
- desktop_monkey/hooks/schemas/mk.hook-manifest.v1.json +292 -0
- desktop_monkey/hooks/schemas/mk.receipt.v1.json +149 -0
- desktop_monkey/hooks/trust.py +149 -0
- desktop_monkey/learning/brain.py +648 -0
- desktop_monkey/learning/mesh_learning.py +76 -0
- desktop_monkey/learning/qig_memory_bridge.py +185 -0
- desktop_monkey/learning/re_integration.py +155 -0
- desktop_monkey/learning/voluntary_llm.py +250 -0
- desktop_monkey/learning/weight_space.py +169 -0
- desktop_monkey/mesh/__init__.py +35 -0
- desktop_monkey/mesh/api.py +111 -0
- desktop_monkey/mesh/calibrate.py +106 -0
- desktop_monkey/mesh/cli.py +249 -0
- desktop_monkey/mesh/cost.py +261 -0
- desktop_monkey/mesh/engines/__init__.py +32 -0
- desktop_monkey/mesh/engines/base.py +136 -0
- desktop_monkey/mesh/engines/llamacpp_rpc.py +926 -0
- desktop_monkey/mesh/engines/pipeline.py +41 -0
- desktop_monkey/mesh/engines/single.py +32 -0
- desktop_monkey/mesh/engines/spec_decode.py +136 -0
- desktop_monkey/mesh/explain.py +81 -0
- desktop_monkey/mesh/fabric/__init__.py +104 -0
- desktop_monkey/mesh/fabric/adapters.py +152 -0
- desktop_monkey/mesh/fabric/devices.py +349 -0
- desktop_monkey/mesh/fabric/executor.py +629 -0
- desktop_monkey/mesh/fabric/graph.py +233 -0
- desktop_monkey/mesh/fabric/retry.py +53 -0
- desktop_monkey/mesh/fabric/tasks.py +173 -0
- desktop_monkey/mesh/fabric/verify.py +151 -0
- desktop_monkey/mesh/honesty.py +227 -0
- desktop_monkey/mesh/links.py +178 -0
- desktop_monkey/mesh/linksvc.py +35 -0
- desktop_monkey/mesh/memory.py +586 -0
- desktop_monkey/mesh/models.py +161 -0
- desktop_monkey/mesh/pools/__init__.py +8 -0
- desktop_monkey/mesh/pools/adapters.py +264 -0
- desktop_monkey/mesh/pools/compute.py +79 -0
- desktop_monkey/mesh/pools/disk.py +159 -0
- desktop_monkey/mesh/pools/memory.py +114 -0
- desktop_monkey/mesh/pools/objects.py +129 -0
- desktop_monkey/mesh/pools/policy.py +321 -0
- desktop_monkey/mesh/power.py +162 -0
- desktop_monkey/mesh/power_aware_scheduler.py +142 -0
- desktop_monkey/mesh/probe.py +617 -0
- desktop_monkey/mesh/qig_scheduler.py +80 -0
- desktop_monkey/mesh/qig_warp_adapter.py +128 -0
- desktop_monkey/mesh/qpu.py +256 -0
- desktop_monkey/mesh/quota.py +67 -0
- desktop_monkey/mesh/router.py +227 -0
- desktop_monkey/mesh/scheduler.py +1014 -0
- desktop_monkey/mesh/subscription.py +127 -0
- desktop_monkey/mesh/topology.py +391 -0
- desktop_monkey/mesh/troop.py +321 -0
- desktop_monkey/monitor/__init__.py +6 -0
- desktop_monkey/monitor/bridge.py +105 -0
- desktop_monkey/monitor/gtk_app.py +187 -0
- desktop_monkey/monitor/troop_page.py +271 -0
- desktop_monkey/protocol/__init__.py +9 -0
- desktop_monkey/protocol/billing.py +86 -0
- desktop_monkey/protocol/crypto.py +164 -0
- desktop_monkey/protocol/device.py +119 -0
- desktop_monkey/protocol/enrol.py +77 -0
- desktop_monkey/protocol/frames.py +343 -0
- desktop_monkey/protocol/jsonrpc.py +51 -0
- desktop_monkey/protocol/mcp.py +55 -0
- desktop_monkey/protocol/memory.py +112 -0
- desktop_monkey/protocol/versions.py +29 -0
- desktop_monkey/qig_nexus/__init__.py +77 -0
- desktop_monkey/qig_nexus/catalog.py +1195 -0
- desktop_monkey/qig_nexus/primitives.py +1232 -0
- desktop_monkey/relay/__init__.py +1 -0
- desktop_monkey/relay/__main__.py +21 -0
- desktop_monkey/relay/accounts.py +668 -0
- desktop_monkey/relay/app.py +132 -0
- desktop_monkey/relay/auth.py +300 -0
- desktop_monkey/relay/billing.py +363 -0
- desktop_monkey/relay/db.py +36 -0
- desktop_monkey/relay/devices_http.py +508 -0
- desktop_monkey/relay/devtools.py +34 -0
- desktop_monkey/relay/hub.py +622 -0
- desktop_monkey/relay/interfaces.py +78 -0
- desktop_monkey/relay/keys.py +123 -0
- desktop_monkey/relay/mcp_http.py +943 -0
- desktop_monkey/relay/mesh_http.py +695 -0
- desktop_monkey/relay/mesh_runtime.py +389 -0
- desktop_monkey/relay/notices.py +109 -0
- desktop_monkey/relay/oauth.py +762 -0
- desktop_monkey/relay/pages.py +258 -0
- desktop_monkey/relay/passwords.py +41 -0
- desktop_monkey/relay/schema.py +232 -0
- desktop_monkey/relay/security.py +260 -0
- desktop_monkey/relay/settings.py +77 -0
- desktop_monkey/relay/static.py +78 -0
- desktop_monkey/relay/tunnel.py +145 -0
- desktop_monkey/relay/usage.py +241 -0
- desktop_monkey/relay/wellknown.py +92 -0
- desktop_monkey/security/immune_response.py +71 -0
- desktop_monkey/security/offensive_nullification.py +42 -0
- desktop_monkey/security/pattern_matcher.py +46 -0
- desktop_monkey/security/request_extractor.py +86 -0
- desktop_monkey/security/request_firewall.py +59 -0
- desktop_monkey/security/request_validator.py +88 -0
- desktop_monkey/security/self_healing.py +44 -0
- desktop_monkey/security/threat_classifier.py +39 -0
- desktop_monkey/theme/__init__.py +46 -0
- desktop_monkey/theme/assets/SOURCES.json +67 -0
- desktop_monkey/theme/assets/favicon-16.ico +0 -0
- desktop_monkey/theme/assets/favicon-32.ico +0 -0
- desktop_monkey/theme/assets/monitor-128.png +0 -0
- desktop_monkey/theme/assets/monitor-32.png +0 -0
- desktop_monkey/theme/assets/monitor-64.png +0 -0
- desktop_monkey/theme/assets/monitor-icon-256.png +0 -0
- desktop_monkey/theme/assets/monkey-ai-rec-dark.png +0 -0
- desktop_monkey/theme/assets/monkey-ai-rec-light.png +0 -0
- desktop_monkey/theme/assets/monkey-ai-sq-dark.png +0 -0
- desktop_monkey/theme/assets/monkey-ai-sq-light.png +0 -0
- desktop_monkey/theme/assets/monkey-icon.png +0 -0
- desktop_monkey/theme/build.py +232 -0
- desktop_monkey/theme/color.py +161 -0
- desktop_monkey/theme/generated/desktop-monkey.css +165 -0
- desktop_monkey/theme/generated/monkey-monitor-tokens.gtk.css +134 -0
- desktop_monkey/theme/tokens.py +124 -0
- desktop_monkey-0.1.0.dist-info/METADATA +164 -0
- desktop_monkey-0.1.0.dist-info/RECORD +172 -0
- desktop_monkey-0.1.0.dist-info/WHEEL +4 -0
- desktop_monkey-0.1.0.dist-info/entry_points.txt +3 -0
- desktop_monkey-0.1.0.dist-info/licenses/LICENSE +21 -0
- desktop_monkey-0.1.0.dist-info/licenses/NOTICE +12 -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
|