superlocalmemory 4.0.2 → 4.0.4
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.
- package/CHANGELOG.md +48 -0
- package/README.md +36 -40
- package/package.json +1 -1
- package/plugin/.claude-plugin/plugin.json +1 -1
- package/plugin/CLAUDE.md +3 -3
- package/plugin/agents/slm-governance-advisor.md +1 -1
- package/plugin/agents/slm-loop-runner.md +1 -1
- package/plugin/agents/slm-memory-advisor.md +1 -1
- package/plugin/agents/slm-optimize-advisor.md +1 -1
- package/plugin/requirements.txt +1 -1
- package/plugin/skills/slm-cache/SKILL.md +1 -1
- package/plugin/skills/slm-compress/SKILL.md +1 -1
- package/plugin/skills/slm-governance/SKILL.md +1 -1
- package/plugin/skills/slm-graph/SKILL.md +1 -1
- package/plugin/skills/slm-loop/SKILL.md +1 -1
- package/plugin/skills/slm-mesh/SKILL.md +1 -1
- package/plugin/skills/slm-profile/SKILL.md +4 -4
- package/plugin/skills/slm-recall/SKILL.md +1 -1
- package/plugin/skills/slm-remember/SKILL.md +1 -1
- package/plugin/skills/slm-scope/SKILL.md +1 -1
- package/plugin/skills/slm-session/SKILL.md +1 -1
- package/plugin/skills/slm-status/SKILL.md +1 -1
- package/plugin-src/rules/AGENTS.md +3 -3
- package/plugin-src/skills/slm-cache/SKILL.md +1 -1
- package/plugin-src/skills/slm-compress/SKILL.md +1 -1
- package/plugin-src/skills/slm-governance/SKILL.md +1 -1
- package/plugin-src/skills/slm-graph/SKILL.md +1 -1
- package/plugin-src/skills/slm-loop/SKILL.md +1 -1
- package/plugin-src/skills/slm-mesh/SKILL.md +1 -1
- package/plugin-src/skills/slm-profile/SKILL.md +4 -4
- package/plugin-src/skills/slm-recall/SKILL.md +1 -1
- package/plugin-src/skills/slm-remember/SKILL.md +1 -1
- package/plugin-src/skills/slm-scope/SKILL.md +1 -1
- package/plugin-src/skills/slm-session/SKILL.md +1 -1
- package/plugin-src/skills/slm-status/SKILL.md +1 -1
- package/pyproject.toml +1 -1
- package/scripts/postinstall-interactive.js +1 -0
- package/scripts/postinstall.js +4 -0
- package/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/cli/commands.py +10 -19
- package/src/superlocalmemory/cli/host_upgrades.py +175 -0
- package/src/superlocalmemory/cli/main.py +29 -4
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +184 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/profiles.py +28 -13
- package/src/superlocalmemory/mcp/server.py +7 -6
- package/src/superlocalmemory/mcp/tools_brain.py +89 -4
- package/src/superlocalmemory/server/routes/brain.py +6 -1
- package/src/superlocalmemory/storage/_migration_internals.py +4 -0
- package/src/superlocalmemory/storage/_schema_version.py +2 -2
- package/src/superlocalmemory/storage/agent_experience.py +26 -4
- package/src/superlocalmemory/storage/external_evidence.py +359 -0
- package/src/superlocalmemory/storage/migration_runner.py +5 -0
- package/src/superlocalmemory/storage/migrations/M041_external_evidence_receipts.py +189 -0
- package/src/superlocalmemory/storage/migrations/__init__.py +2 -0
- package/src/superlocalmemory/ui/js/od-brain.js +9 -0
- package/src/superlocalmemory/ui/js/od-mcp.js +6 -6
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Consented, non-destructive upgrades for existing host integrations.
|
|
2
|
+
|
|
3
|
+
Package installation owns the SLM executable. This module owns the separate
|
|
4
|
+
operator-approved step that refreshes SLM-owned integration assets. It never
|
|
5
|
+
discovers a host and edits it implicitly: preview is the default and apply
|
|
6
|
+
requires either explicit hosts or an explicit all-detected acknowledgement.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
from argparse import Namespace
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from typing import Any
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def validate_upgrade_request(*, apply: bool, hosts: list[str], all_detected: bool) -> None:
|
|
17
|
+
"""Reject unbounded mutations before any host filesystem access."""
|
|
18
|
+
if apply and not hosts and not all_detected:
|
|
19
|
+
raise ValueError("--apply requires at least one --host or --all-detected")
|
|
20
|
+
if hosts and all_detected:
|
|
21
|
+
raise ValueError("choose explicit --host values or --all-detected, not both")
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def cmd_upgrade_hosts(args: Namespace) -> None:
|
|
25
|
+
"""Preview or explicitly apply host integration refreshes."""
|
|
26
|
+
hosts = list(getattr(args, "hosts", []) or [])
|
|
27
|
+
apply = bool(getattr(args, "apply", False))
|
|
28
|
+
all_detected = bool(getattr(args, "all_detected", False))
|
|
29
|
+
try:
|
|
30
|
+
validate_upgrade_request(apply=apply, hosts=hosts, all_detected=all_detected)
|
|
31
|
+
except ValueError as exc:
|
|
32
|
+
print(f"Error: {exc}")
|
|
33
|
+
return
|
|
34
|
+
|
|
35
|
+
from superlocalmemory.hooks.portable_kit import supported_ides
|
|
36
|
+
|
|
37
|
+
detected = set(_detected_hosts())
|
|
38
|
+
targets = sorted(detected) if all_detected else (hosts or sorted(detected))
|
|
39
|
+
managed_hosts = set(supported_ides()) | {"claude-code", "hermes"}
|
|
40
|
+
unknown = sorted(set(targets) - managed_hosts)
|
|
41
|
+
if unknown:
|
|
42
|
+
print(f"Error: unsupported host(s): {', '.join(unknown)}")
|
|
43
|
+
return
|
|
44
|
+
if not targets:
|
|
45
|
+
print("No existing SLM host integrations were detected.")
|
|
46
|
+
print("Run `slm setup` for first-time setup, or use `--host <name>` to target one host.")
|
|
47
|
+
return
|
|
48
|
+
|
|
49
|
+
operation = "Applying" if apply else "Previewing"
|
|
50
|
+
print(f"{operation} SLM host upgrades: {', '.join(targets)}")
|
|
51
|
+
if not apply:
|
|
52
|
+
print("No host files will be changed. Re-run with --apply and explicit targets to proceed.")
|
|
53
|
+
|
|
54
|
+
for host in targets:
|
|
55
|
+
result = _upgrade_host(host, apply=apply, already_integrated=host in detected)
|
|
56
|
+
status = result.get("status", "error")
|
|
57
|
+
detail = result.get("detail", "")
|
|
58
|
+
print(f" [{status}] {host}: {detail}")
|
|
59
|
+
|
|
60
|
+
if apply:
|
|
61
|
+
print("Restart affected host applications, then run `slm doctor` to verify SLM itself.")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def _detected_hosts(home: Path | None = None) -> list[str]:
|
|
65
|
+
"""Return only hosts already containing an SLM-owned integration block."""
|
|
66
|
+
from superlocalmemory.hooks.portable_kit import (
|
|
67
|
+
IDE_MATRIX,
|
|
68
|
+
_load_config,
|
|
69
|
+
_ParseError,
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
effective_home = home or Path.home()
|
|
73
|
+
detected: list[str] = []
|
|
74
|
+
for host, desc in IDE_MATRIX.items():
|
|
75
|
+
if not desc.fmt:
|
|
76
|
+
continue
|
|
77
|
+
path = effective_home / desc.mcp_path_global
|
|
78
|
+
try:
|
|
79
|
+
data = _load_config(path, desc.fmt)
|
|
80
|
+
except _ParseError:
|
|
81
|
+
continue
|
|
82
|
+
if _contains_slm_block(data, desc):
|
|
83
|
+
detected.append(host)
|
|
84
|
+
|
|
85
|
+
# Claude Code's plugin is host-managed. A local SLM hook installation is
|
|
86
|
+
# the durable evidence that its SLM integration exists; upgrading the
|
|
87
|
+
# plugin remains an explicit Claude Code marketplace action.
|
|
88
|
+
try:
|
|
89
|
+
from superlocalmemory.hooks.claude_code_hooks import check_status
|
|
90
|
+
|
|
91
|
+
if check_status().get("installed"):
|
|
92
|
+
detected.append("claude-code")
|
|
93
|
+
except Exception:
|
|
94
|
+
pass
|
|
95
|
+
|
|
96
|
+
# Hermes has a native YAML mapping rather than the portable-kit schema.
|
|
97
|
+
# Detect it for reporting, but never overwrite it with an invented format.
|
|
98
|
+
hermes_path = effective_home / ".hermes" / "config.yaml"
|
|
99
|
+
try:
|
|
100
|
+
hermes = _load_config(hermes_path, "yaml")
|
|
101
|
+
servers = hermes.get("mcp_servers", {})
|
|
102
|
+
if isinstance(servers, dict) and "superlocalmemory" in servers:
|
|
103
|
+
detected.append("hermes")
|
|
104
|
+
except _ParseError:
|
|
105
|
+
pass
|
|
106
|
+
return sorted(set(detected))
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def _contains_slm_block(data: dict[str, Any], desc: Any) -> bool:
|
|
110
|
+
if desc.fmt == "yaml":
|
|
111
|
+
providers = data.get(desc.server_key, [])
|
|
112
|
+
return isinstance(providers, list) and any(
|
|
113
|
+
isinstance(item, dict)
|
|
114
|
+
and item.get("params", {}).get("serverName") == "superlocalmemory"
|
|
115
|
+
for item in providers
|
|
116
|
+
)
|
|
117
|
+
servers = data.get(desc.server_key, {})
|
|
118
|
+
return isinstance(servers, dict) and "superlocalmemory" in servers
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
def _upgrade_host(host: str, *, apply: bool, already_integrated: bool) -> dict[str, str]:
|
|
122
|
+
"""Refresh assets without degrading a working, host-specific MCP block."""
|
|
123
|
+
if host == "claude-code":
|
|
124
|
+
return {
|
|
125
|
+
"status": "plugin-managed",
|
|
126
|
+
"detail": "run `claude plugin update superlocalmemory@qualixar` in Claude Code",
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if host == "hermes":
|
|
130
|
+
if already_integrated:
|
|
131
|
+
return {
|
|
132
|
+
"status": "verified",
|
|
133
|
+
"detail": "existing native Hermes SLM mapping preserved",
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
"status": "blocked",
|
|
137
|
+
"detail": "Hermes is not connected; follow docs/ide-setup.md before retrying",
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if host == "codex" and already_integrated:
|
|
141
|
+
from superlocalmemory.hooks.codex_assets import install_assets
|
|
142
|
+
from superlocalmemory.hooks.codex_hooks import install_hooks
|
|
143
|
+
|
|
144
|
+
assets, hooks = install_assets(dry_run=not apply), install_hooks(dry_run=not apply)
|
|
145
|
+
if not assets.get("success") or not hooks.get("success"):
|
|
146
|
+
return {"status": "blocked", "detail": "could not refresh SLM-owned Codex assets"}
|
|
147
|
+
action = "would refresh" if not apply else "refreshed"
|
|
148
|
+
return {
|
|
149
|
+
"status": "updated" if apply else "preview",
|
|
150
|
+
"detail": (
|
|
151
|
+
f"{action} SLM-owned Codex skills, agents, and hooks; "
|
|
152
|
+
"MCP block preserved"
|
|
153
|
+
),
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if already_integrated:
|
|
157
|
+
return {
|
|
158
|
+
"status": "verified",
|
|
159
|
+
"detail": "existing SLM MCP block preserved; no portable host assets require refresh",
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
# An explicitly named, not-yet-connected host is a first-time connection.
|
|
163
|
+
# This branch is never reached by --all-detected.
|
|
164
|
+
from superlocalmemory.hooks.portable_kit import connect_ide
|
|
165
|
+
|
|
166
|
+
result = connect_ide(host, dry_run=not apply)
|
|
167
|
+
if result.get("error"):
|
|
168
|
+
return {"status": "blocked", "detail": str(result["error"])}
|
|
169
|
+
action = result.get("mcp_config", "unknown")
|
|
170
|
+
if not apply:
|
|
171
|
+
return {"status": "preview", "detail": f"would refresh SLM MCP block ({action})"}
|
|
172
|
+
return {"status": "updated", "detail": f"SLM MCP block {action}"}
|
|
173
|
+
|
|
174
|
+
|
|
175
|
+
__all__ = ("cmd_upgrade_hosts", "validate_upgrade_request")
|
|
@@ -69,7 +69,7 @@ documentation:
|
|
|
69
69
|
|
|
70
70
|
|
|
71
71
|
_NO_DAEMON_COMMANDS = {
|
|
72
|
-
"setup", "mode", "provider", "connect", "migrate", "mcp", "warmup", "hooks", "codex",
|
|
72
|
+
"setup", "mode", "provider", "connect", "upgrade-hosts", "migrate", "mcp", "warmup", "hooks", "codex",
|
|
73
73
|
"config", "evolve", "db",
|
|
74
74
|
# v3.4.22 escape hatches — never auto-start the daemon on these.
|
|
75
75
|
"disable", "enable", "clear-cache", "reconfigure", "benchmark",
|
|
@@ -123,12 +123,17 @@ def main() -> None:
|
|
|
123
123
|
and sys.argv[1] == "connect"
|
|
124
124
|
and "--dry-run" in sys.argv[2:]
|
|
125
125
|
)
|
|
126
|
+
_is_host_upgrade_preview = (
|
|
127
|
+
len(sys.argv) >= 2
|
|
128
|
+
and sys.argv[1] == "upgrade-hosts"
|
|
129
|
+
and "--apply" not in sys.argv[2:]
|
|
130
|
+
)
|
|
126
131
|
|
|
127
132
|
# WP-07: lazy first-run init — runs after hook/mcp fast-paths so stdout
|
|
128
133
|
# is never polluted on those paths (CRIT-3, MCP JSON-RPC purity).
|
|
129
134
|
# Guarded: any failure must not crash the CLI (AC4).
|
|
130
135
|
_is_mcp_cmd = len(sys.argv) >= 2 and sys.argv[1] == "mcp"
|
|
131
|
-
if not _is_mcp_cmd and not _is_metadata_cmd and not _is_connect_dry_run:
|
|
136
|
+
if not _is_mcp_cmd and not _is_metadata_cmd and not _is_connect_dry_run and not _is_host_upgrade_preview:
|
|
132
137
|
try:
|
|
133
138
|
from superlocalmemory.cli._lazy_init import _ensure_initialized
|
|
134
139
|
_ensure_initialized()
|
|
@@ -148,7 +153,7 @@ def main() -> None:
|
|
|
148
153
|
|
|
149
154
|
# One-time post-upgrade banner — silent for fresh installs and
|
|
150
155
|
# same-version runs. Guarded against I/O errors internally.
|
|
151
|
-
if not _is_mcp_stdio and not _is_metadata_cmd and not _is_connect_dry_run:
|
|
156
|
+
if not _is_mcp_stdio and not _is_metadata_cmd and not _is_connect_dry_run and not _is_host_upgrade_preview:
|
|
152
157
|
from superlocalmemory.cli.version_banner import check_and_emit_upgrade_banner
|
|
153
158
|
if check_and_emit_upgrade_banner(_ver):
|
|
154
159
|
# First post-upgrade invocation: apply the data-dir migration if
|
|
@@ -285,6 +290,23 @@ def main() -> None:
|
|
|
285
290
|
),
|
|
286
291
|
)
|
|
287
292
|
|
|
293
|
+
upgrade_p = sub.add_parser(
|
|
294
|
+
"upgrade-hosts",
|
|
295
|
+
help="Preview or explicitly refresh installed SLM host integrations",
|
|
296
|
+
)
|
|
297
|
+
upgrade_p.add_argument(
|
|
298
|
+
"--host", action="append", dest="hosts", default=[],
|
|
299
|
+
help="Explicit host to refresh (repeatable; for example: codex, cursor)",
|
|
300
|
+
)
|
|
301
|
+
upgrade_p.add_argument(
|
|
302
|
+
"--all-detected", action="store_true", default=False,
|
|
303
|
+
help="Target only hosts that already contain an SLM integration",
|
|
304
|
+
)
|
|
305
|
+
upgrade_p.add_argument(
|
|
306
|
+
"--apply", action="store_true", default=False,
|
|
307
|
+
help="Apply the previewed changes; default is read-only preview",
|
|
308
|
+
)
|
|
309
|
+
|
|
288
310
|
migrate_p = sub.add_parser("migrate", help="Migrate data from V2 to V3 schema")
|
|
289
311
|
migrate_p.add_argument(
|
|
290
312
|
"--rollback", action="store_true", help="Rollback migration",
|
|
@@ -966,7 +988,10 @@ def main() -> None:
|
|
|
966
988
|
sys.exit(0)
|
|
967
989
|
|
|
968
990
|
# V3.3.19: Auto-trigger setup wizard on first use
|
|
969
|
-
if not (
|
|
991
|
+
if not (
|
|
992
|
+
(args.command == "connect" and getattr(args, "dry_run", False))
|
|
993
|
+
or (args.command == "upgrade-hosts" and not getattr(args, "apply", False))
|
|
994
|
+
):
|
|
970
995
|
from superlocalmemory.cli.setup_wizard import check_first_use
|
|
971
996
|
check_first_use(args.command)
|
|
972
997
|
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""Versioned, read-only contract boundary for Bounded Loops MCP evidence."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
import shutil
|
|
9
|
+
import stat
|
|
10
|
+
from collections.abc import Awaitable, Callable
|
|
11
|
+
from copy import deepcopy
|
|
12
|
+
from datetime import timedelta
|
|
13
|
+
from pathlib import Path
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
CONTRACT_ID = "bounded-loops.dev/slm-bridge/v1"
|
|
17
|
+
_OBSERVATION_TIMEOUT_SECONDS = 5.0
|
|
18
|
+
_MAX_MCP_TEXT_BYTES = 2 * 1024 * 1024
|
|
19
|
+
_ADVERTISEMENT = {
|
|
20
|
+
"id": CONTRACT_ID,
|
|
21
|
+
"tool": "bl_graph_evidence",
|
|
22
|
+
"operation": "observe_terminal_run",
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class BridgeUnavailable(ValueError):
|
|
27
|
+
"""The installed producer does not advertise a compatible bridge contract."""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def supports_bridge(capabilities: dict[str, Any]) -> bool:
|
|
31
|
+
"""Negotiate on the declared public contract, never producer semver."""
|
|
32
|
+
advertised = capabilities.get("evidence_contracts")
|
|
33
|
+
return isinstance(advertised, list) and any(
|
|
34
|
+
isinstance(item, dict)
|
|
35
|
+
and all(item.get(key) == value for key, value in _ADVERTISEMENT.items())
|
|
36
|
+
for item in advertised
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bridge_payload(evidence: dict[str, Any], *, profile_id: str) -> dict[str, Any]:
|
|
41
|
+
"""Attach active-profile identity after refusing incompatible evidence."""
|
|
42
|
+
if evidence.get("contract") != CONTRACT_ID:
|
|
43
|
+
raise BridgeUnavailable("unsupported bounded-loops evidence contract")
|
|
44
|
+
if evidence.get("eligible_for_learning") is not False:
|
|
45
|
+
raise BridgeUnavailable("bounded-loops evidence is not observation-only")
|
|
46
|
+
# The producer has organisation/project metadata for its own control plane.
|
|
47
|
+
# SLM stores only the v1 observation receipt needed by its profile-scoped
|
|
48
|
+
# learning database; retaining arbitrary producer extensions would turn a
|
|
49
|
+
# versioned contract into an unbounded schema sink.
|
|
50
|
+
fields = (
|
|
51
|
+
"contract",
|
|
52
|
+
"workspace_id",
|
|
53
|
+
"run_ref",
|
|
54
|
+
"run_id",
|
|
55
|
+
"outcome",
|
|
56
|
+
"run_state",
|
|
57
|
+
"demonstration",
|
|
58
|
+
"eligible_for_learning",
|
|
59
|
+
"terminal_at",
|
|
60
|
+
"graph_digest",
|
|
61
|
+
"plan_digest",
|
|
62
|
+
"policy_digest",
|
|
63
|
+
"receipt",
|
|
64
|
+
"nodes",
|
|
65
|
+
)
|
|
66
|
+
if any(field not in evidence for field in fields):
|
|
67
|
+
raise BridgeUnavailable("bounded-loops evidence is missing required v1 fields")
|
|
68
|
+
return {field: deepcopy(evidence[field]) for field in fields} | {"profile_id": profile_id}
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
async def observe_terminal_runs(
|
|
72
|
+
call_tool: Callable[[str, dict[str, Any]], Awaitable[dict[str, Any]]], *, profile_id: str
|
|
73
|
+
) -> list[dict[str, Any]]:
|
|
74
|
+
"""Collect only producer-advertised terminal evidence over an injected MCP transport."""
|
|
75
|
+
discovery = await call_tool("bl_capabilities", {})
|
|
76
|
+
if discovery.get("status") != "ok" or not supports_bridge(discovery.get("capabilities", {})):
|
|
77
|
+
raise BridgeUnavailable("bounded-loops does not advertise slm-bridge/v1")
|
|
78
|
+
listing = await call_tool("bl_graph_terminal_runs", {"limit": 100})
|
|
79
|
+
if listing.get("status") != "ok" or listing.get("contract") != CONTRACT_ID:
|
|
80
|
+
raise BridgeUnavailable("bounded-loops terminal listing is unavailable")
|
|
81
|
+
runs = listing.get("runs")
|
|
82
|
+
if not isinstance(runs, list):
|
|
83
|
+
raise BridgeUnavailable("bounded-loops terminal listing is malformed")
|
|
84
|
+
# The producer's limit is advisory. Keep this explicit operation bounded
|
|
85
|
+
# even against a compatible but faulty/malicious producer.
|
|
86
|
+
runs = runs[:100]
|
|
87
|
+
observed: list[dict[str, Any]] = []
|
|
88
|
+
for run in runs:
|
|
89
|
+
if not isinstance(run, dict) or not isinstance(run.get("run_ref"), str):
|
|
90
|
+
raise BridgeUnavailable("bounded-loops terminal listing is malformed")
|
|
91
|
+
response = await call_tool("bl_graph_evidence", {"run_ref": run["run_ref"]})
|
|
92
|
+
if response.get("status") == "unavailable":
|
|
93
|
+
continue
|
|
94
|
+
if response.get("status") != "ok" or not isinstance(response.get("evidence"), dict):
|
|
95
|
+
raise BridgeUnavailable("bounded-loops evidence response is malformed")
|
|
96
|
+
observed.append(bridge_payload(response["evidence"], profile_id=profile_id))
|
|
97
|
+
return observed
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
async def observe_from_stdio(*, command: str, cwd: str, profile_id: str) -> list[dict[str, Any]]:
|
|
101
|
+
"""Run one bounded, explicit MCP 2 observation; never call from recall or remember."""
|
|
102
|
+
executable, workspace = Path(command), Path(cwd)
|
|
103
|
+
if (
|
|
104
|
+
not executable.is_absolute()
|
|
105
|
+
or not executable.is_file()
|
|
106
|
+
or not workspace.is_absolute()
|
|
107
|
+
or not workspace.is_dir()
|
|
108
|
+
or workspace.is_symlink()
|
|
109
|
+
):
|
|
110
|
+
raise BridgeUnavailable(
|
|
111
|
+
"bounded-loops bridge requires an approved executable and workspace"
|
|
112
|
+
)
|
|
113
|
+
try:
|
|
114
|
+
executable = executable.resolve(strict=True)
|
|
115
|
+
workspace = workspace.resolve(strict=True)
|
|
116
|
+
mode = executable.stat().st_mode
|
|
117
|
+
except OSError as exc:
|
|
118
|
+
raise BridgeUnavailable("bounded-loops bridge path is unavailable") from exc
|
|
119
|
+
if not stat.S_ISREG(mode) or (
|
|
120
|
+
os.name != "nt" and mode & (stat.S_IWGRP | stat.S_IWOTH)
|
|
121
|
+
):
|
|
122
|
+
raise BridgeUnavailable("bounded-loops executable is not a trusted regular file")
|
|
123
|
+
if executable.stat().st_uid not in {0, os.geteuid()}:
|
|
124
|
+
raise BridgeUnavailable("bounded-loops executable owner is not trusted")
|
|
125
|
+
|
|
126
|
+
from mcp import ClientSession, StdioServerParameters
|
|
127
|
+
from mcp.client.stdio import stdio_client
|
|
128
|
+
|
|
129
|
+
try:
|
|
130
|
+
parameters = StdioServerParameters(
|
|
131
|
+
command=str(executable), args=[], cwd=str(workspace)
|
|
132
|
+
)
|
|
133
|
+
async with stdio_client(parameters) as (read, write):
|
|
134
|
+
async with ClientSession(
|
|
135
|
+
read,
|
|
136
|
+
write,
|
|
137
|
+
read_timeout_seconds=timedelta(seconds=_OBSERVATION_TIMEOUT_SECONDS),
|
|
138
|
+
) as session:
|
|
139
|
+
async def observe() -> list[dict[str, Any]]:
|
|
140
|
+
await session.initialize()
|
|
141
|
+
|
|
142
|
+
async def call(name: str, arguments: dict[str, Any]) -> dict[str, Any]:
|
|
143
|
+
result = await session.call_tool(name, arguments)
|
|
144
|
+
if result.isError:
|
|
145
|
+
raise BridgeUnavailable(
|
|
146
|
+
"bounded-loops rejected the observation request"
|
|
147
|
+
)
|
|
148
|
+
texts = [item.text for item in result.content if hasattr(item, "text")]
|
|
149
|
+
if len(texts) != 1 or len(texts[0].encode("utf-8")) > _MAX_MCP_TEXT_BYTES:
|
|
150
|
+
raise BridgeUnavailable("bounded-loops returned an invalid MCP payload")
|
|
151
|
+
try:
|
|
152
|
+
payload = json.loads(texts[0])
|
|
153
|
+
except json.JSONDecodeError as exc:
|
|
154
|
+
raise BridgeUnavailable("bounded-loops returned invalid JSON") from exc
|
|
155
|
+
if not isinstance(payload, dict):
|
|
156
|
+
raise BridgeUnavailable("bounded-loops returned an invalid MCP payload")
|
|
157
|
+
return payload
|
|
158
|
+
|
|
159
|
+
return await observe_terminal_runs(call, profile_id=profile_id)
|
|
160
|
+
return await asyncio.wait_for(observe(), timeout=_OBSERVATION_TIMEOUT_SECONDS)
|
|
161
|
+
except BridgeUnavailable:
|
|
162
|
+
raise
|
|
163
|
+
except Exception as exc:
|
|
164
|
+
raise BridgeUnavailable("bounded-loops observation timed out or could not start") from exc
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def observe_installed(*, workspace: str, profile_id: str) -> list[dict[str, Any]]:
|
|
168
|
+
"""Observe a user-installed producer without accepting an agent command.
|
|
169
|
+
|
|
170
|
+
Discovery deliberately resolves exactly the public ``bounded-loops-mcp``
|
|
171
|
+
executable. It does not accept a command, shell fragment, or arguments
|
|
172
|
+
from an MCP caller; the only caller-supplied value is the existing project
|
|
173
|
+
workspace whose Bounded Loops state is to be read.
|
|
174
|
+
"""
|
|
175
|
+
command = shutil.which("bounded-loops-mcp")
|
|
176
|
+
if command is None:
|
|
177
|
+
raise BridgeUnavailable("bounded-loops-mcp is not installed")
|
|
178
|
+
if not Path(command).is_absolute():
|
|
179
|
+
raise BridgeUnavailable("bounded-loops-mcp discovery returned an unsafe path")
|
|
180
|
+
if Path(command).resolve().name not in {"bounded-loops-mcp", "bounded-loops-mcp.exe"}:
|
|
181
|
+
raise BridgeUnavailable("bounded-loops-mcp discovery returned an unsafe executable")
|
|
182
|
+
return await observe_from_stdio(
|
|
183
|
+
command=str(Path(command).resolve()), cwd=workspace, profile_id=profile_id
|
|
184
|
+
)
|
|
@@ -613,7 +613,8 @@ class LearningDatabase:
|
|
|
613
613
|
row[0]
|
|
614
614
|
for row in conn.execute(
|
|
615
615
|
"SELECT name FROM sqlite_master WHERE type='table' "
|
|
616
|
-
"AND name IN ('agent_experiences', 'cognitive_turn_receipts'
|
|
616
|
+
"AND name IN ('agent_experiences', 'cognitive_turn_receipts', "
|
|
617
|
+
"'external_evidence_receipts')"
|
|
617
618
|
)
|
|
618
619
|
}
|
|
619
620
|
if profile_id is None:
|
|
@@ -23,7 +23,15 @@ _PROFILE_CORE: frozenset[str] = frozenset({ # 14
|
|
|
23
23
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
24
24
|
})
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
# Portable Brain evidence must reach the coding-host profile shipped by the
|
|
27
|
+
# Claude/Codex plugins, not only an unrestricted server.
|
|
28
|
+
_PROFILE_BRAIN: frozenset[str] = frozenset({
|
|
29
|
+
"get_brain_evidence_status", "record_agent_experience",
|
|
30
|
+
"record_cognitive_turn", "finalize_cognitive_turn",
|
|
31
|
+
"observe_bounded_loop_evidence",
|
|
32
|
+
})
|
|
33
|
+
|
|
34
|
+
_PROFILE_CODE: frozenset[str] = _PROFILE_CORE | _PROFILE_BRAIN | frozenset({ # 29
|
|
27
35
|
"build_code_graph", "get_blast_radius", "query_graph",
|
|
28
36
|
"semantic_search_code", "get_review_context", "detect_changes",
|
|
29
37
|
# switch_profile lets a plugin/IDE session change the active workspace over
|
|
@@ -40,23 +48,22 @@ _PROFILE_FULL_MESH: frozenset[str] = frozenset({ # 8
|
|
|
40
48
|
"mesh_state", "mesh_lock", "mesh_events", "mesh_status",
|
|
41
49
|
})
|
|
42
50
|
|
|
43
|
-
_PROFILE_FULL: frozenset[str] = frozenset({ #
|
|
51
|
+
_PROFILE_FULL: frozenset[str] = frozenset({ # 39 base — EXPLICIT literal, NOT runtime _ESSENTIAL_TOOLS (OQ-2)
|
|
44
52
|
"remember", "recall", "search", "fetch", "list_recent", "delete_memory", "update_memory",
|
|
45
53
|
"get_status", "session_init", "observe", "close_session", "report_feedback", "forget",
|
|
46
54
|
"run_maintenance", "consolidate_cognitive", "get_soft_prompts", "set_mode", "report_outcome",
|
|
47
55
|
"log_tool_event", "get_assertions", "reinforce_assertion", "contradict_assertion",
|
|
56
|
+
"get_brain_evidence_status", "record_agent_experience",
|
|
57
|
+
"record_cognitive_turn", "finalize_cognitive_turn",
|
|
58
|
+
"observe_bounded_loop_evidence",
|
|
48
59
|
"evolve_skill", "skill_health", "skill_lineage", "switch_profile",
|
|
49
60
|
"slm_compress", "slm_retrieve", "slm_cache_set", "slm_cache_get", "slm_optimize_stats",
|
|
50
61
|
# v3.8.0: bounded-loop tools (CLI + /slm-loop command + MCP).
|
|
51
62
|
"slm_loop_run", "slm_loop_history", "slm_loop_show",
|
|
52
|
-
#
|
|
53
|
-
|
|
54
|
-
# (SLM_MCP_PROFILE=full42) and the published tool-count table depends on
|
|
55
|
-
# them; adding a tool here would make "full42" serve 43 tools. New tools
|
|
56
|
-
# reach users through the `whole` profile until a profile rename is shipped.
|
|
57
|
-
}) | _PROFILE_FULL_MESH # 42
|
|
63
|
+
# prestage_context remains registered but deliberately raw-server-only.
|
|
64
|
+
}) | _PROFILE_FULL_MESH # 47
|
|
58
65
|
|
|
59
|
-
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ #
|
|
66
|
+
_PROFILE_POWER: frozenset[str] = _PROFILE_FULL | frozenset({ # 59
|
|
60
67
|
"get_version", "get_mode", "health", "consistency_check", "recall_trace",
|
|
61
68
|
"get_lifecycle_status", "set_retention_policy", "compact_memories",
|
|
62
69
|
"get_behavioral_patterns", "audit_trail", "quantize", "get_retention_stats",
|
|
@@ -79,21 +86,29 @@ _PROFILE_DEFINITIONS: dict[str, frozenset[str]] = {
|
|
|
79
86
|
# at server startup. Any other value is a configuration error (fail closed).
|
|
80
87
|
_PROFILE_ALIASES: dict[str, str] = {
|
|
81
88
|
"core14": "core",
|
|
82
|
-
# 3.8.0
|
|
89
|
+
# 3.8.0 and later additions grew code/full/power; every historical count
|
|
83
90
|
# power. Every historical count-suffixed name is kept so a v3.6/3.7/early-
|
|
84
91
|
# 3.8 config still resolves (back-compat); new 3.8.0 counts added alongside.
|
|
85
92
|
"code20": "code",
|
|
86
93
|
"code21": "code",
|
|
87
94
|
"code24": "code",
|
|
95
|
+
"code28": "code",
|
|
96
|
+
"code29": "code",
|
|
88
97
|
"full38": "full",
|
|
89
98
|
"full39": "full",
|
|
90
99
|
"full42": "full",
|
|
100
|
+
"full46": "full",
|
|
101
|
+
"full47": "full",
|
|
91
102
|
"power50": "power",
|
|
92
103
|
"power51": "power",
|
|
93
104
|
"power54": "power",
|
|
105
|
+
"power58": "power",
|
|
106
|
+
"power59": "power",
|
|
94
107
|
"mesh8": "mesh",
|
|
95
108
|
"whole81": "whole",
|
|
96
109
|
"whole84": "whole",
|
|
110
|
+
"whole91": "whole",
|
|
111
|
+
"whole92": "whole",
|
|
97
112
|
}
|
|
98
113
|
|
|
99
114
|
# Plain-English descriptions for UI display.
|
|
@@ -101,8 +116,8 @@ _PROFILE_ALIASES: dict[str, str] = {
|
|
|
101
116
|
# one sentence, user-facing language only.
|
|
102
117
|
PROFILE_DESCRIPTIONS: dict[str, str] = {
|
|
103
118
|
"core": "Essential memory: store, recall, search, sessions",
|
|
104
|
-
"code": "Core + code
|
|
105
|
-
"full": "All everyday memory, optimization, and mesh tools",
|
|
106
|
-
"power": "Everything in full plus advanced governance
|
|
119
|
+
"code": "Core + code graph, portable Brain evidence, and profile switching (default for IDE coding agents)",
|
|
120
|
+
"full": "All everyday memory, portable Brain evidence, optimization, and mesh tools",
|
|
121
|
+
"power": "Everything in full plus advanced governance and behavioral tools",
|
|
107
122
|
"mesh": "Cross-device mesh coordination only",
|
|
108
123
|
}
|
|
@@ -77,13 +77,13 @@ def reset_engine():
|
|
|
77
77
|
|
|
78
78
|
# Register tools and resources -------------------------------------------------
|
|
79
79
|
#
|
|
80
|
-
# Essential-only default:
|
|
80
|
+
# Essential-only default: 39 base tools + 8 mesh tools = 47 registered.
|
|
81
81
|
# when mesh is enabled. Set ``SLM_MCP_ALL_TOOLS=1`` to expose the full
|
|
82
82
|
# toolset. Rationale: IDEs cap at 50-100 tools total (Cursor,
|
|
83
83
|
# Antigravity, Windsurf) and a maximal SLM registration crowds out
|
|
84
84
|
# other MCP servers the user may have installed.
|
|
85
85
|
# Admin/diagnostics tools remain available via CLI (`slm <command>`).
|
|
86
|
-
# Set SLM_MCP_ALL_TOOLS=1 to enable all
|
|
86
|
+
# Set SLM_MCP_ALL_TOOLS=1 to enable all 92 tools (power users).
|
|
87
87
|
|
|
88
88
|
import os as _os_reg
|
|
89
89
|
|
|
@@ -100,13 +100,14 @@ _ESSENTIAL_TOOLS: set[str] = {
|
|
|
100
100
|
# v4.0.2 portable Brain evidence: profile-scoped receipt reads/writes.
|
|
101
101
|
"get_brain_evidence_status", "record_agent_experience",
|
|
102
102
|
"record_cognitive_turn", "finalize_cognitive_turn",
|
|
103
|
+
# v4.0.4: explicit, optional observation from the separately installed
|
|
104
|
+
# Bounded Loops MCP producer. It never participates in recall/ranking.
|
|
105
|
+
"observe_bounded_loop_evidence",
|
|
103
106
|
# Memory management (2)
|
|
104
107
|
"forget", "run_maintenance",
|
|
105
108
|
# NOTE: prestage_context IS registered (see register_prestage_tool below)
|
|
106
|
-
# but is deliberately absent from the default surface.
|
|
107
|
-
#
|
|
108
|
-
# user-facing config contract and the published tool-count table depends on
|
|
109
|
-
# them. New tools reach users via the `whole` profile until a rename ships.
|
|
109
|
+
# but is deliberately absent from the default surface. Brain evidence is
|
|
110
|
+
# intentionally present: it is the portable contract for installed agents.
|
|
110
111
|
# Infinite memory + learning (4)
|
|
111
112
|
"consolidate_cognitive", "get_soft_prompts",
|
|
112
113
|
"set_mode", "report_outcome",
|