superlocalmemory 4.0.3 → 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 +22 -0
- package/README.md +11 -10
- 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/src/superlocalmemory/__init__.py +1 -1
- package/src/superlocalmemory/integrations/bounded_loops_mcp.py +184 -0
- package/src/superlocalmemory/learning/database.py +2 -1
- package/src/superlocalmemory/mcp/profiles.py +11 -5
- package/src/superlocalmemory/mcp/server.py +5 -2
- 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/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,28 @@ All notable changes to SuperLocalMemory will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [4.0.4] - 2026-08-15 — Optional Bounded Loops evidence bridge
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
- An optional MCP 2 bridge discovers the separately installed
|
|
12
|
+
`bounded-loops-mcp` producer at call time and negotiates the public
|
|
13
|
+
`bounded-loops.dev/slm-bridge/v1` capability rather than pinning a producer
|
|
14
|
+
package version.
|
|
15
|
+
- `observe_bounded_loop_evidence(workspace)` explicitly imports sanitized
|
|
16
|
+
terminal graph evidence using the producer's `run_ref` address. It is
|
|
17
|
+
available in the default, `code`, `full`, and `power` MCP surfaces.
|
|
18
|
+
- M041 stores typed, profile-scoped external evidence in `learning.db`; Living
|
|
19
|
+
Brain displays honest Bounded Loop observation totals and documentation
|
|
20
|
+
explains the independent-product contract.
|
|
21
|
+
|
|
22
|
+
### Safety
|
|
23
|
+
- The bridge accepts no caller-supplied executable or shell arguments, has a
|
|
24
|
+
five-second producer timeout, validates the exact v1 evidence shape, and
|
|
25
|
+
preserves `eligible_for_learning: false`.
|
|
26
|
+
- External receipts never enter recall, ranking, routing, reward, or automatic
|
|
27
|
+
learning. Profile erasure removes them and a durable tombstone blocks stale
|
|
28
|
+
processes from recreating them.
|
|
29
|
+
|
|
8
30
|
## [4.0.3] - 2026-08-15 — Consented host integration upgrades
|
|
9
31
|
|
|
10
32
|
### Added
|
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
</picture>
|
|
6
6
|
</p>
|
|
7
7
|
|
|
8
|
-
<h1 align="center">SuperLocalMemory V4.0.
|
|
8
|
+
<h1 align="center">SuperLocalMemory V4.0.4</h1>
|
|
9
9
|
|
|
10
10
|
<h2 align="center">Rent the LLM. Own the memory.</h2>
|
|
11
11
|
|
|
@@ -27,12 +27,12 @@ guarantee here is stated as a falsifiable invariant, tested under an adversarial
|
|
|
27
27
|
negative control, and shipped with the harness that regenerates the evidence:
|
|
28
28
|
<code>python benchmark/run_all.py --trials 200 --output-dir results/</code>. What each experiment
|
|
29
29
|
does <em>not</em> exercise is stated too.</p>
|
|
30
|
-
<p align="center"><code>v4.0.
|
|
30
|
+
<p align="center"><code>v4.0.4</code> — one control plane: <strong>SLM-Mesh</strong> peer coordination · multi-scope memory (personal / shared / global) · profiles · Cache · Compress · 7-layer retrieval · code graph · Entity Explorer · skill evolution · Modes A/B/C · GDPR retention & audit chain · bounded loops — across CLI, MCP, dashboard, the <strong>Claude plugin</strong>, the <strong>Codex add-on</strong>, and documented IDE integrations.<br/>
|
|
31
31
|
Proxy: <code>slm wrap claude</code> · MCP: add <code>slm_compress</code> to your config · Skill: zero-config</p>
|
|
32
32
|
<p align="center"><strong>Four public arXiv preprints</strong> · V4: <a href="https://arxiv.org/abs/2608.08253">arXiv:2608.08253</a> · companion archive: <a href="https://zenodo.org/records/21853302">Zenodo 21853302</a> (<a href="https://doi.org/10.5281/zenodo.21853302">DOI 10.5281/zenodo.21853302</a>) · prior preprints: <a href="https://arxiv.org/abs/2603.02240">2603.02240</a> · <a href="https://arxiv.org/abs/2603.14588">2603.14588</a> · <a href="https://arxiv.org/abs/2604.04514">2604.04514</a>.</p>
|
|
33
33
|
|
|
34
34
|
<p align="center">
|
|
35
|
-
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.0.
|
|
35
|
+
<a href="CHANGELOG.md"><img src="https://img.shields.io/badge/v4.0.4-Current_Release-2ea44f?style=for-the-badge&logo=checkmarx&logoColor=white" alt="v4.0.4 — Current Release"/></a>
|
|
36
36
|
<a href="https://arxiv.org/abs/2608.08253"><img src="https://img.shields.io/badge/arXiv-2608.08253-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="SuperLocalMemory 4.0 paper on arXiv:2608.08253"/></a>
|
|
37
37
|
<a href="https://zenodo.org/records/21853302"><img src="https://img.shields.io/badge/Zenodo-10.5281%2Fzenodo.21853302-1682D4?style=for-the-badge&logo=zenodo&logoColor=white" alt="V4 paper on Zenodo: 10.5281/zenodo.21853302"/></a>
|
|
38
38
|
<a href="https://arxiv.org/abs/2603.14588"><img src="https://img.shields.io/badge/arXiv-2603.14588-b31b1b?style=for-the-badge&logo=arxiv&logoColor=white" alt="arXiv Paper"/></a>
|
|
@@ -62,7 +62,7 @@ SuperLocalMemory V4 combines conventional dense and lexical retrieval with graph
|
|
|
62
62
|
|
|
63
63
|
**Memory with a sense of time.** SLM does not only store *what* an agent learned — it records *when*. Every fact carries ingestion timing and provenance; recall runs a dedicated temporal candidate channel alongside semantic, lexical, and associative retrieval; scenes and entity timelines reconstruct sequence; and the lifecycle lets neglected memory decay and self-archive instead of growing without bound. Time is a first-class ranking and lifecycle signal rather than a timestamp column an agent never reads — which is what lets a long-lived agent reason about how its context changed, not only what it currently holds.
|
|
64
64
|
|
|
65
|
-
**What V4.0.
|
|
65
|
+
**What V4.0.4 ships.** Existing users can now preview and explicitly refresh SLM-owned host integrations with `slm upgrade-hosts`, while package installation and MCP startup no longer silently modify host hook configuration. V4.0.4 also adds an optional, capability-negotiated Bounded Loops observation bridge: compatible terminal graph receipts enter a separate, profile-scoped `learning.db` table and appear in Living Brain, without affecting recall, ranking, routing, or automatic learning. V4 remains a *governed* local-first control plane — every canonical write is admitted, policy-authorized, tracked as durable per-store obligations (lexical, temporal, vector), and sealed by a completion manifest, so it is either fully applied or explicitly marked degraded, never silently half-done. Flagship surfaces in this release:
|
|
66
66
|
|
|
67
67
|
- **[SLM-Mesh](#slm-mesh-cross-session--cross-machine-coordination)** — authenticated cross-session and cross-machine peer coordination (messages, locks, shared state, inbox/outbox, optional discovery). Coordination only — not automatic replicated memory.
|
|
68
68
|
- **Multi-scope memory & profiles** — workspaces (profiles) plus `personal` / `shared` / `global` scopes; cross-profile recall is default-deny.
|
|
@@ -71,7 +71,7 @@ SuperLocalMemory V4 combines conventional dense and lexical retrieval with graph
|
|
|
71
71
|
- **Modes A / B / C** — local-only (A), on-device LLM enrichment (B), provider-assisted (C). An operating mode records technical locality facts; it does **not** determine EU AI Act legal compliance (that is deployment-context assessment — see [Privacy controls](#privacy-controls-and-operating-modes)).
|
|
72
72
|
- **GDPR posture, retention & audit chain** — export, fail-closed cross-store erasure, retention policies, and a hash-chained audit trail. Engineering controls for compliance programs, not a legal certification.
|
|
73
73
|
- **7-layer retrieval/recall stack & code graph** — multi-channel candidates (semantic, BM25, temporal, Hopfield, spreading activation) plus optional code-graph tools for blast radius and review context.
|
|
74
|
-
- **MCP profiles** — `code` exposes **
|
|
74
|
+
- **MCP profiles** — `code` exposes **29** tools for installed coding agents; `full` **47**; `power` **59**; `whole` **92** (all registered). Also `core` (14), `mesh` (8), and the unrestricted default surface (47 with mesh enabled).
|
|
75
75
|
- **Governed write path & verifiable transactions** — admission + policy control, a per-owner obligation ledger, and a hash-sealed completion manifest with a reconciler that redrives unmet obligations.
|
|
76
76
|
- **Self-healing lifecycle & admin remediation** — stale locks cleared on restart; list/resolve stuck operations from CLI, MCP, or the dashboard.
|
|
77
77
|
|
|
@@ -436,11 +436,11 @@ Control tool surface via `SLM_MCP_PROFILE`:
|
|
|
436
436
|
| Profile | Tools | Use case |
|
|
437
437
|
|:--------|:-----:|:---------|
|
|
438
438
|
| `core` | 14 | Memory, session, and optimize core |
|
|
439
|
-
| `code` |
|
|
439
|
+
| `code` | 29 | Core + portable Brain evidence + code-graph tools + profile switching + bounded loops |
|
|
440
440
|
| `mesh` | 8 | SLM-Mesh only — multi-session / multi-machine coordination |
|
|
441
|
-
| `full` |
|
|
442
|
-
| `power` |
|
|
443
|
-
| `whole` |
|
|
441
|
+
| `full` | 47 | Memory + portable Brain evidence + optimize + evolution + mesh + bounded loops |
|
|
442
|
+
| `power` | 59 | Full + administration, lifecycle, and diagnostics |
|
|
443
|
+
| `whole` | 92 | Every registered MCP tool |
|
|
444
444
|
|
|
445
445
|
**Precedence:** `ALL` > `TOOLS` > `PROFILE` > `default`
|
|
446
446
|
|
|
@@ -451,7 +451,7 @@ slm mcp
|
|
|
451
451
|
|
|
452
452
|
For a predictable small surface, set `core` explicitly. Leaving the variable
|
|
453
453
|
unset retains the compatibility default, whose mesh tools follow the local
|
|
454
|
-
mesh setting. Count-suffixed aliases remain for backward compatibility and emit a migration warning: `core14`, `code20`, `code21`, `code24`, `code28`, `mesh8`, `full38`, `full39`, `full42`, `full46`, `power50`, `power51`, `power54`, `power58`, `whole81`, `whole84`, `whole91`. Unknown names stop startup instead of silently selecting another tool set.
|
|
454
|
+
mesh setting. Count-suffixed aliases remain for backward compatibility and emit a migration warning: `core14`, `code20`, `code21`, `code24`, `code28`, `code29`, `mesh8`, `full38`, `full39`, `full42`, `full46`, `full47`, `power50`, `power51`, `power54`, `power58`, `power59`, `whole81`, `whole84`, `whole91`, `whole92`. Unknown names stop startup instead of silently selecting another tool set.
|
|
455
455
|
|
|
456
456
|
Per-IDE configs available for Claude Code, Cursor, Windsurf, VS Code Copilot, Continue, Gemini CLI, JetBrains, Zed, and more (15 configs in `ide/configs/`). See [docs/ide-setup.md](docs/ide-setup.md).
|
|
457
457
|
|
|
@@ -684,6 +684,7 @@ interface.
|
|
|
684
684
|
| Published benchmark evidence | [docs/benchmarks.md](docs/benchmarks.md) |
|
|
685
685
|
| CLI reference | [docs/cli-reference.md](docs/cli-reference.md) |
|
|
686
686
|
| MCP tools reference | [docs/mcp-tools.md](docs/mcp-tools.md) |
|
|
687
|
+
| Optional Bounded Loops bridge | [docs/bounded-loops-bridge.md](docs/bounded-loops-bridge.md) |
|
|
687
688
|
| Getting started | [docs/getting-started.md](docs/getting-started.md) |
|
|
688
689
|
| IDE setup (15 configs) | [docs/ide-setup.md](docs/ide-setup.md) |
|
|
689
690
|
| Teams, users, and RBAC | [docs/rbac-teams.md](docs/rbac-teams.md) |
|
package/package.json
CHANGED
package/plugin/CLAUDE.md
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- BEGIN SuperLocalMemory v4.0.
|
|
1
|
+
<!-- BEGIN SuperLocalMemory v4.0.4 -->
|
|
2
2
|
|
|
3
3
|
## SuperLocalMemory (SLM) — Agent Rules
|
|
4
4
|
|
|
@@ -39,6 +39,6 @@ slm-recall · slm-remember · slm-session · slm-status · slm-cache · slm-comp
|
|
|
39
39
|
### Subagents
|
|
40
40
|
slm-memory-advisor (memory decisions, session hygiene, scope/profile guidance) · slm-optimize-advisor (context compression + KV cache) · slm-governance-advisor (scope/roles/compliance/GDPR)
|
|
41
41
|
|
|
42
|
-
<!-- END SuperLocalMemory v4.0.
|
|
42
|
+
<!-- END SuperLocalMemory v4.0.4 -->
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.0.
|
|
44
|
+
SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later
|
|
@@ -77,4 +77,4 @@ slm-scope · slm-governance · slm-profile · slm-remember · slm-recall
|
|
|
77
77
|
# What NOT to do
|
|
78
78
|
Never session_init twice; never forget without dry-run preview; never store secrets; never bypass role checks; never claim an erasure succeeded without verifying via recall.
|
|
79
79
|
|
|
80
|
-
SuperLocalMemory v4.0.
|
|
80
|
+
SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later
|
|
@@ -46,4 +46,4 @@ slm-recall · slm-remember · slm-session · slm-scope · slm-profile · slm-gov
|
|
|
46
46
|
# What NOT to do
|
|
47
47
|
Never session_init twice; never forget dry_run=False without reporting preview; never dump a whole file into remember; never invent a memory; never claim "saved" without success:true / clean CLI exit; never bypass scope or governance restrictions.
|
|
48
48
|
|
|
49
|
-
SuperLocalMemory v4.0.
|
|
49
|
+
SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later
|
|
@@ -41,4 +41,4 @@ slm-compress · slm-cache · slm-status · slm-profile
|
|
|
41
41
|
# What NOT to do
|
|
42
42
|
Never compress code-for-edit/JSON-to-parse/<500 chars; never store secrets/ccr_ids; never let optimize failure block/alter the task; never claim a specific savings %; never carry ccr_ids across profile switches.
|
|
43
43
|
|
|
44
|
-
SuperLocalMemory v4.0.
|
|
44
|
+
SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later
|
package/plugin/requirements.txt
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
superlocalmemory==4.0.
|
|
1
|
+
superlocalmemory==4.0.4
|
|
@@ -31,9 +31,9 @@ project, a client engagement, a production vs staging environment.
|
|
|
31
31
|
| Profile | Tools | When to use |
|
|
32
32
|
|---------|-------|-------------|
|
|
33
33
|
| `core` | 14 tools — remember, recall, search, session, optimize | Minimal footprint, no code tools |
|
|
34
|
-
| `code` |
|
|
35
|
-
| `full` |
|
|
36
|
-
| `power` |
|
|
34
|
+
| `code` | 29 tools — core + portable Brain evidence + code graph + profile switching + bounded loops | Default for IDE/coding agents |
|
|
35
|
+
| `full` | 47 tools — code + all memory ops + mesh + bounded loops | Multi-session, team workflows |
|
|
36
|
+
| `power` | 59 tools — full + governance + behavioral tools | Enterprise, admin, audit use cases |
|
|
37
37
|
| `mesh` | 8 tools — mesh coordination only | Lightweight cross-session signalling |
|
|
38
38
|
|
|
39
39
|
The profile is set at MCP server startup via `SLM_MCP_PROFILE` in the MCP config.
|
|
@@ -145,4 +145,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
-
*SuperLocalMemory v4.0.
|
|
148
|
+
*SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later*
|
|
@@ -82,10 +82,10 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
82
82
|
|
|
83
83
|
## Tool reference (core profile — 14 tools)
|
|
84
84
|
|
|
85
|
-
> The MCP config ships `SLM_MCP_PROFILE=code` (
|
|
85
|
+
> The MCP config ships `SLM_MCP_PROFILE=code` (29 tools): the 14 core memory tools below
|
|
86
86
|
> **plus** 6 code-graph tools (`build_code_graph`, `get_blast_radius`, `query_graph`,
|
|
87
87
|
> `semantic_search_code`, `get_review_context`, `detect_changes`) and `switch_profile`.
|
|
88
|
-
> Use `full` (
|
|
88
|
+
> Use `full` (47 tools) to add mesh coordination. Use `power` (59 tools) for governance
|
|
89
89
|
> and audit tools. See slm-profile for profile switching.
|
|
90
90
|
|
|
91
91
|
| Tool | Signature (key params) | Notes |
|
|
@@ -128,4 +128,4 @@ When the SLM MCP server is unavailable, use these CLI equivalents:
|
|
|
128
128
|
- **slm-optimize-advisor** — context compression and KV cache
|
|
129
129
|
- **slm-governance-advisor** — scope/role compliance, retention policies, GDPR
|
|
130
130
|
|
|
131
|
-
SuperLocalMemory v4.0.
|
|
131
|
+
SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later
|
|
@@ -31,9 +31,9 @@ project, a client engagement, a production vs staging environment.
|
|
|
31
31
|
| Profile | Tools | When to use |
|
|
32
32
|
|---------|-------|-------------|
|
|
33
33
|
| `core` | 14 tools — remember, recall, search, session, optimize | Minimal footprint, no code tools |
|
|
34
|
-
| `code` |
|
|
35
|
-
| `full` |
|
|
36
|
-
| `power` |
|
|
34
|
+
| `code` | 29 tools — core + portable Brain evidence + code graph + profile switching + bounded loops | Default for IDE/coding agents |
|
|
35
|
+
| `full` | 47 tools — code + all memory ops + mesh + bounded loops | Multi-session, team workflows |
|
|
36
|
+
| `power` | 59 tools — full + governance + behavioral tools | Enterprise, admin, audit use cases |
|
|
37
37
|
| `mesh` | 8 tools — mesh coordination only | Lightweight cross-session signalling |
|
|
38
38
|
|
|
39
39
|
The profile is set at MCP server startup via `SLM_MCP_PROFILE` in the MCP config.
|
|
@@ -145,4 +145,4 @@ Name them differently in your MCP config (e.g. `superlocalmemory-personal` and
|
|
|
145
145
|
|
|
146
146
|
---
|
|
147
147
|
|
|
148
|
-
*SuperLocalMemory v4.0.
|
|
148
|
+
*SuperLocalMemory v4.0.4 · Qualixar · AGPL-3.0-or-later*
|
package/pyproject.toml
CHANGED
|
@@ -32,7 +32,7 @@ if "OMP_NUM_THREADS" not in os.environ:
|
|
|
32
32
|
os.environ["OMP_NUM_THREADS"] = "2"
|
|
33
33
|
# ---------------------------------------------------------------------------
|
|
34
34
|
|
|
35
|
-
__version__ = "4.0.
|
|
35
|
+
__version__ = "4.0.4"
|
|
36
36
|
|
|
37
37
|
_REQUIRED_VERSIONS = {
|
|
38
38
|
"sentence_transformers": "5.3.0",
|
|
@@ -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:
|