memgres 0.11.0__tar.gz → 0.12.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {memgres-0.11.0 → memgres-0.12.1}/PKG-INFO +2 -1
- {memgres-0.11.0 → memgres-0.12.1}/README.md +1 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/__init__.py +2 -1
- {memgres-0.11.0 → memgres-0.12.1}/memgres/_version.py +1 -1
- {memgres-0.11.0 → memgres-0.12.1}/memgres/config.py +6 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/info.py +8 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/links.py +57 -6
- {memgres-0.11.0 → memgres-0.12.1}/memgres/mcp_server.py +86 -44
- memgres-0.12.1/memgres/migrations/0023_relink_after_parser_fix.sql +16 -0
- memgres-0.12.1/memgres/paths.py +49 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/schema.py +5 -1
- {memgres-0.11.0 → memgres-0.12.1}/memgres/store.py +71 -2
- {memgres-0.11.0 → memgres-0.12.1}/memgres/vector/base.py +5 -1
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/PKG-INFO +2 -1
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/SOURCES.txt +4 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_links.py +43 -1
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_list.py +1 -1
- memgres-0.12.1/tests/test_mcp_error_messages.py +207 -0
- memgres-0.12.1/tests/test_required_fields.py +195 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_server_info.py +20 -1
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_server_integration.py +2 -1
- {memgres-0.11.0 → memgres-0.12.1}/LICENSE +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/admin.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/admin_cli.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/blame.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/bootstrap.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/delimiters.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/diffing.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/embed_worker.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/embeddings.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/healthcheck.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/identity.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/indexing.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/lines.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0001_core.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0002_identity.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0003_history_author.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0004_title.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0005_chunk_index.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0006_reader_floor.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0007_embed_retry.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0008_service_roles.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0009_create_namespace_right.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0010_namespace_alias.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0011_drop_default_namespace.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0012_user_profile.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0013_hash_version.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0014_access_request_no_fk.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0015_normalize_tags.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0016_valid_at.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0017_memory_link.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0018_links_built.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0019_memory_usage.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0020_memory_usage_no_fk.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0021_enrollment_key.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/migrations/0022_user_disabled.sql +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/periodic.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/reembed.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/relink.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/search.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/segments.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/server.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/tags.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/token_cli.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/vector/__init__.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/vector/pgvector.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/vector/qdrant.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres/worker.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/dependency_links.txt +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/entry_points.txt +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/requires.txt +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/memgres.egg-info/top_level.txt +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/pyproject.toml +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/setup.cfg +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_admin_two_way.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_blame_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_chunk_index.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_claim_and_reembed.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_config.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_diffing.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_embed_worker.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_embeddings.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_enrollment.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_healthcheck.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_identity_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_lexical_match.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_limits.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_admin_tools.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_http_transport.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_instructions.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_recall_schema.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_tool_visibility.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_mcp_tool_visibility_http.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_migration_upgrade.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_multi_space_search.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_path_addressing.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_qdrant_ca.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_qdrant_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_replace_build.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_require_title.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_retention.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_roles_bootstrap.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_search_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_security_followups.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_security_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_segments.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_segments_store.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_snippets.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_store_integration.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_tags.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_token_sink.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_usage.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_valid_at.py +0 -0
- {memgres-0.11.0 → memgres-0.12.1}/tests/test_write_ergonomics.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: memgres
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.12.1
|
|
4
4
|
Summary: Drop-in memory for AI agents: one Postgres, lexical + semantic recall, diff-versioned history, GDPR-erasable.
|
|
5
5
|
Author: mozgsml
|
|
6
6
|
License-Expression: MIT
|
|
@@ -216,6 +216,7 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
216
216
|
| `MEMGRES_TOKEN` | — | default token used when a call passes none (single-tenant endpoints) |
|
|
217
217
|
| `MEMGRES_TOKEN_SINK` | — | absolute directory a minted secret is **written to** (`<token-id>.token`, `0600`) instead of being returned. Set it when provisioning is done by an agent — a secret in a tool result is a secret in a transcript. See [docs/TENANCY.md](docs/TENANCY.md) |
|
|
218
218
|
| `MEMGRES_TREE` | `true` | `ltree` path column + GiST index (fast subtree select) |
|
|
219
|
+
| `MEMGRES_REQUIRED_FIELDS` | — | comma-separated fields a content-storing write must carry (`source`, `reason`), refused by name when absent. A policy about a corpus, not a property of the software: a scratch database wants none, a corporate one wants `source`. `move`/`retag` are exempt. Announced in `server_info.write_requirements` |
|
|
219
220
|
| `MEMGRES_REQUIRE_TITLE` | `true` | `true` = a write that stores content must supply `title`. Captions are what name a memory in results and what title-weighted ranking weighs; `move`/`retag` are exempt (they store no content) |
|
|
220
221
|
| `MEMGRES_REQUIRE_PARENT` | `false` | `true` = a node's parent path must already exist |
|
|
221
222
|
| `MEMGRES_HISTORY` | `true` | keep the hash-chained diff history (deleted with the record) |
|
|
@@ -180,6 +180,7 @@ Everything is env, all optional (defaults suit a single-user embed). Full list i
|
|
|
180
180
|
| `MEMGRES_TOKEN` | — | default token used when a call passes none (single-tenant endpoints) |
|
|
181
181
|
| `MEMGRES_TOKEN_SINK` | — | absolute directory a minted secret is **written to** (`<token-id>.token`, `0600`) instead of being returned. Set it when provisioning is done by an agent — a secret in a tool result is a secret in a transcript. See [docs/TENANCY.md](docs/TENANCY.md) |
|
|
182
182
|
| `MEMGRES_TREE` | `true` | `ltree` path column + GiST index (fast subtree select) |
|
|
183
|
+
| `MEMGRES_REQUIRED_FIELDS` | — | comma-separated fields a content-storing write must carry (`source`, `reason`), refused by name when absent. A policy about a corpus, not a property of the software: a scratch database wants none, a corporate one wants `source`. `move`/`retag` are exempt. Announced in `server_info.write_requirements` |
|
|
183
184
|
| `MEMGRES_REQUIRE_TITLE` | `true` | `true` = a write that stores content must supply `title`. Captions are what name a memory in results and what title-weighted ranking weighs; `move`/`retag` are exempt (they store no content) |
|
|
184
185
|
| `MEMGRES_REQUIRE_PARENT` | `false` | `true` = a node's parent path must already exist |
|
|
185
186
|
| `MEMGRES_HISTORY` | `true` | keep the hash-chained diff history (deleted with the record) |
|
|
@@ -23,7 +23,7 @@ from .schema import migrate, SchemaMismatch, SCHEMA_VERSION
|
|
|
23
23
|
from .search import Hit, recall
|
|
24
24
|
from .blame import annotate, annotate_grouped, reconstruct, replay
|
|
25
25
|
from .store import (Store, Memory, Conflict, NotFound, TooLarge, NoParent,
|
|
26
|
-
MissingTitle)
|
|
26
|
+
MissingTitle, MissingField)
|
|
27
27
|
from .identity import (
|
|
28
28
|
Principal, AuthError, SpaceNotFound,
|
|
29
29
|
resolve, resolve_space, new_token, valid_format,
|
|
@@ -38,6 +38,7 @@ __all__ = [
|
|
|
38
38
|
"Config", "load_config",
|
|
39
39
|
"Store", "Memory", "Conflict", "NotFound", "TooLarge", "NoParent",
|
|
40
40
|
"MissingTitle",
|
|
41
|
+
"MissingField",
|
|
41
42
|
"make_diff", "apply_diff", "content_hash", "DiffConflict",
|
|
42
43
|
"Embedder", "get_embedder",
|
|
43
44
|
"migrate", "SchemaMismatch", "SCHEMA_VERSION",
|
|
@@ -79,6 +79,8 @@ class Config:
|
|
|
79
79
|
# organization
|
|
80
80
|
tree_enabled: bool # ltree path column + GiST index for fast subtree selection
|
|
81
81
|
require_title: bool # True = a write that stores CONTENT must caption it
|
|
82
|
+
required_fields: tuple # extra fields a content-storing write must carry
|
|
83
|
+
# (MEMGRES_REQUIRED_FIELDS=source), refused when absent
|
|
82
84
|
require_parent: bool # False = sparse paths (create food.apple with no food row);
|
|
83
85
|
# True = a node's parent path must already exist as a memory
|
|
84
86
|
# history
|
|
@@ -222,6 +224,10 @@ def load() -> Config:
|
|
|
222
224
|
token_sink=_str("MEMGRES_TOKEN_SINK", ""),
|
|
223
225
|
tree_enabled=_bool("MEMGRES_TREE", True),
|
|
224
226
|
require_title=_bool("MEMGRES_REQUIRE_TITLE", True),
|
|
227
|
+
required_fields=tuple(
|
|
228
|
+
f for f in (x.strip().lower()
|
|
229
|
+
for x in _str("MEMGRES_REQUIRED_FIELDS", "").split(","))
|
|
230
|
+
if f),
|
|
225
231
|
require_parent=_bool("MEMGRES_REQUIRE_PARENT", False),
|
|
226
232
|
history_enabled=_bool("MEMGRES_HISTORY", True),
|
|
227
233
|
fts_language=_str("MEMGRES_FTS_LANGUAGE", "simple"),
|
|
@@ -65,6 +65,14 @@ def server_info(cfg: Config, embed_dim: Optional[int] = None) -> dict:
|
|
|
65
65
|
"renew_on_read": bool(days > 0 and cfg.renew_on_read),
|
|
66
66
|
"policy": _retention_policy(days, cfg.renew_on_read),
|
|
67
67
|
},
|
|
68
|
+
# What a write MUST carry here. Announced rather than discovered from a
|
|
69
|
+
# refusal: a client that learns the rule by being rejected has already
|
|
70
|
+
# composed the memory, and a rule nobody can see before writing is a rule
|
|
71
|
+
# that gets satisfied with junk on the second attempt.
|
|
72
|
+
"write_requirements": {
|
|
73
|
+
"title": cfg.require_title,
|
|
74
|
+
"fields": list(cfg.required_fields),
|
|
75
|
+
},
|
|
68
76
|
"recall_modes": ["lexical"] if lexical_only
|
|
69
77
|
else ["lexical", "semantic", "hybrid", "auto"],
|
|
70
78
|
"vector_backend": cfg.vector_backend,
|
|
@@ -52,10 +52,11 @@ from typing import Dict, List, Optional
|
|
|
52
52
|
# edges so they are visible, never resolved — we do not own the address space.
|
|
53
53
|
KNOWN_SCHEMES = ("idea", "file")
|
|
54
54
|
|
|
55
|
-
#
|
|
56
|
-
#
|
|
57
|
-
#
|
|
58
|
-
|
|
55
|
+
# What a path is lives in `paths.py`, so the parser and the store cannot drift
|
|
56
|
+
# apart again — that drift is exactly what dropped every hyphenated link before
|
|
57
|
+
# 0.12.0. Kept under the old name because the rest of this module reads better
|
|
58
|
+
# with it.
|
|
59
|
+
from .paths import PATH_RE as _PATH
|
|
59
60
|
|
|
60
61
|
_LINK = re.compile(r"\[\[([^\[\]\n]+)\]\]")
|
|
61
62
|
_FENCE = re.compile(r"```.*?```|~~~.*?~~~", re.S)
|
|
@@ -82,8 +83,58 @@ class Link:
|
|
|
82
83
|
end: int = 0
|
|
83
84
|
|
|
84
85
|
|
|
86
|
+
def _blank_indented(original: str, text: str) -> str:
|
|
87
|
+
"""Blank markdown's OTHER code block: a run of lines indented by four spaces
|
|
88
|
+
(or a tab), opened by a blank line.
|
|
89
|
+
|
|
90
|
+
Fences are not the only way to show code, and the indented form is what a
|
|
91
|
+
hand-written example tends to use. A frp config pasted that way put
|
|
92
|
+
``[[proxies]]`` — a TOML array-of-tables header, and a perfectly well-formed
|
|
93
|
+
path — into the link graph as a dangling edge to a memory nobody will ever
|
|
94
|
+
write. The parser must ignore code wherever markdown says code is.
|
|
95
|
+
|
|
96
|
+
A blank line inside the block does not end it; the first non-blank line back
|
|
97
|
+
at the margin does. Opening on a blank line is what keeps an ordinary list
|
|
98
|
+
item or a wrapped line — indented, but not preceded by a blank — from being
|
|
99
|
+
read as code.
|
|
100
|
+
|
|
101
|
+
🔴 Which lines are code is decided from `original`, and the blanking applied
|
|
102
|
+
to `text` — the copy where spans and fences are already spaces. Deciding from
|
|
103
|
+
the blanked copy instead made every line that STARTED with an inline code
|
|
104
|
+
span look indented: "`[[a]]` but [[b]] counts" became four-plus leading
|
|
105
|
+
spaces, the whole line was swallowed as a block, and a real link next to a
|
|
106
|
+
code span disappeared. Both strings are length-preserving, so their lines
|
|
107
|
+
correspond one to one.
|
|
108
|
+
"""
|
|
109
|
+
def blank(line: str) -> str:
|
|
110
|
+
return "".join(" " if c != "\n" else "\n" for c in line)
|
|
111
|
+
|
|
112
|
+
src = original.splitlines(keepends=True)
|
|
113
|
+
dst = text.splitlines(keepends=True)
|
|
114
|
+
out, in_block, prev_blank = [], False, True
|
|
115
|
+
for i, line in enumerate(src):
|
|
116
|
+
stripped = line.strip("\n")
|
|
117
|
+
is_blank = not stripped.strip()
|
|
118
|
+
indented = stripped.startswith(" ") or stripped.startswith("\t")
|
|
119
|
+
current = dst[i] if i < len(dst) else line
|
|
120
|
+
if in_block:
|
|
121
|
+
if is_blank or indented:
|
|
122
|
+
out.append(current if is_blank else blank(current))
|
|
123
|
+
else:
|
|
124
|
+
in_block = False
|
|
125
|
+
out.append(current)
|
|
126
|
+
elif indented and prev_blank:
|
|
127
|
+
in_block = True
|
|
128
|
+
out.append(blank(current))
|
|
129
|
+
else:
|
|
130
|
+
out.append(current)
|
|
131
|
+
prev_blank = is_blank
|
|
132
|
+
return "".join(out)
|
|
133
|
+
|
|
134
|
+
|
|
85
135
|
def _blank_code(body: str) -> str:
|
|
86
|
-
"""Replace code spans and
|
|
136
|
+
"""Replace code spans and code blocks — fenced AND indented — with spaces of
|
|
137
|
+
the same length.
|
|
87
138
|
|
|
88
139
|
Same length, not removal, so every offset in the returned text still lines up
|
|
89
140
|
with the original — the parser does not need that today, but an anchor
|
|
@@ -92,7 +143,7 @@ def _blank_code(body: str) -> str:
|
|
|
92
143
|
"""
|
|
93
144
|
def blank(m: re.Match) -> str:
|
|
94
145
|
return "".join(" " if c != "\n" else "\n" for c in m.group(0))
|
|
95
|
-
return _CODE_SPAN.sub(blank, _FENCE.sub(blank, body))
|
|
146
|
+
return _blank_indented(body, _CODE_SPAN.sub(blank, _FENCE.sub(blank, body)))
|
|
96
147
|
|
|
97
148
|
|
|
98
149
|
def _classify(target: str) -> Optional[str]:
|
|
@@ -32,8 +32,10 @@ from typing_extensions import Annotated
|
|
|
32
32
|
|
|
33
33
|
try: # mcp SDK >= 2.0 renamed the module fastmcp -> mcpserver
|
|
34
34
|
from mcp.server.mcpserver import Context
|
|
35
|
+
from mcp.server.mcpserver.exceptions import ToolError
|
|
35
36
|
except ImportError: # mcp SDK 1.x
|
|
36
37
|
from mcp.server.fastmcp import Context
|
|
38
|
+
from mcp.server.fastmcp.exceptions import ToolError
|
|
37
39
|
|
|
38
40
|
from . import admin, identity
|
|
39
41
|
from .config import Config, load
|
|
@@ -269,6 +271,40 @@ def _mcp(name: str, instructions: Optional[str] = None):
|
|
|
269
271
|
return server
|
|
270
272
|
|
|
271
273
|
|
|
274
|
+
# Refusals whose text is written FOR the caller and safe to show them: what is
|
|
275
|
+
# missing, which namespace to name, why the diff did not apply, what they may
|
|
276
|
+
# not do. Everything else — a psycopg error, a bug — stays masked, because those
|
|
277
|
+
# messages carry schema and internals rather than instructions.
|
|
278
|
+
_SPEAKABLE = (ValueError, PermissionError, LookupError)
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _speaking(fn):
|
|
282
|
+
"""Re-raise a domain refusal as the SDK's own error type, which is the only
|
|
283
|
+
kind whose message reaches the client.
|
|
284
|
+
|
|
285
|
+
Everything else the SDK catches becomes `Error executing tool <name>` with no
|
|
286
|
+
detail — and that is what the caller sees. The refusals here are written to
|
|
287
|
+
be read: "this deployment requires `source` … as an ADDRESS", "you can reach
|
|
288
|
+
2 namespaces, name the one you mean". Masking them turns a fixable mistake
|
|
289
|
+
into a mystery, and an agent that cannot see the reason repeats the same call
|
|
290
|
+
— which is exactly what happened: five identical edits in a row, then the
|
|
291
|
+
same on reads, against a server that was explaining itself into the void.
|
|
292
|
+
"""
|
|
293
|
+
import functools
|
|
294
|
+
|
|
295
|
+
@functools.wraps(fn)
|
|
296
|
+
def wrapper(*args, **kwargs):
|
|
297
|
+
try:
|
|
298
|
+
return fn(*args, **kwargs)
|
|
299
|
+
except ToolError:
|
|
300
|
+
raise
|
|
301
|
+
except _SPEAKABLE as e:
|
|
302
|
+
# `from e` keeps the original in the server log, where the traceback
|
|
303
|
+
# belongs; the client gets the sentence, not the stack.
|
|
304
|
+
raise ToolError(str(e)) from e
|
|
305
|
+
return wrapper
|
|
306
|
+
|
|
307
|
+
|
|
272
308
|
def _mem(m) -> dict:
|
|
273
309
|
return m.to_dict(stringify_dates=True) # MCP layer needs plain strings
|
|
274
310
|
|
|
@@ -292,6 +328,12 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
292
328
|
_worker, cfg, backend = wire_server(cfg, embedder)
|
|
293
329
|
mcp = _mcp("memgres", instructions=_instruction_text())
|
|
294
330
|
|
|
331
|
+
def tool(*d_args, **d_kwargs):
|
|
332
|
+
"""`mcp.tool()`, but the refusal text survives the trip to the client."""
|
|
333
|
+
def deco(fn):
|
|
334
|
+
return mcp.tool(*d_args, **d_kwargs)(_speaking(fn))
|
|
335
|
+
return deco
|
|
336
|
+
|
|
295
337
|
import os as _os
|
|
296
338
|
|
|
297
339
|
# The control-plane tools. `auto` registers them wherever there are
|
|
@@ -389,7 +431,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
389
431
|
"""
|
|
390
432
|
return identity.resolve(conn, cfg, token)
|
|
391
433
|
|
|
392
|
-
@
|
|
434
|
+
@tool()
|
|
393
435
|
def memory_write(body: Optional[str] = None, id: Optional[str] = None,
|
|
394
436
|
at: Optional[str] = None,
|
|
395
437
|
if_moved: IfMovedArg = "error",
|
|
@@ -445,7 +487,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
445
487
|
reason=reason, valid_at=valid_at, space=space,
|
|
446
488
|
space_id=space_id))
|
|
447
489
|
|
|
448
|
-
@
|
|
490
|
+
@tool()
|
|
449
491
|
def memory_get(id: Optional[str] = None, at: Optional[str] = None,
|
|
450
492
|
if_moved: Literal["follow", "error"] = "follow",
|
|
451
493
|
lines: Optional[str] = None,
|
|
@@ -471,7 +513,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
471
513
|
if_moved=if_moved, lines=lines,
|
|
472
514
|
space=space, space_id=space_id))
|
|
473
515
|
|
|
474
|
-
@
|
|
516
|
+
@tool()
|
|
475
517
|
def memory_recall(query: str, k: int = 10,
|
|
476
518
|
mode: Literal["lexical", "semantic", "hybrid", "auto"] = "auto",
|
|
477
519
|
match: Optional[Literal["any", "all"]] = None,
|
|
@@ -515,7 +557,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
515
557
|
snippet=snippet, full_body=full_body, bodies=bodies,
|
|
516
558
|
match_tags=match_tags, space=space, space_id=space_id)]
|
|
517
559
|
|
|
518
|
-
@
|
|
560
|
+
@tool()
|
|
519
561
|
def memory_list(path_prefix: Optional[str] = None,
|
|
520
562
|
tags: Optional[List[str]] = None, limit: int = 50,
|
|
521
563
|
offset: int = 0, bodies: bool = False,
|
|
@@ -543,7 +585,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
543
585
|
limit=limit, offset=offset, bodies=bodies,
|
|
544
586
|
match_tags=match_tags, space=space, space_id=space_id)
|
|
545
587
|
|
|
546
|
-
@
|
|
588
|
+
@tool()
|
|
547
589
|
def memory_tags(prefix: Optional[str] = None, k: int = 50,
|
|
548
590
|
space: Spaces = None, space_id: Spaces = None,
|
|
549
591
|
ctx: Context = None) -> List[dict]:
|
|
@@ -558,7 +600,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
558
600
|
return _store(conn).tags(_token(ctx), prefix=prefix, k=k,
|
|
559
601
|
space=space, space_id=space_id)
|
|
560
602
|
|
|
561
|
-
@
|
|
603
|
+
@tool()
|
|
562
604
|
def memory_links(id: Optional[str] = None, at: Optional[str] = None,
|
|
563
605
|
direction: Literal["in", "out", "both"] = "both",
|
|
564
606
|
space: Optional[str] = None, space_id: Optional[str] = None,
|
|
@@ -579,7 +621,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
579
621
|
at=at or None, direction=direction,
|
|
580
622
|
space=space, space_id=space_id)
|
|
581
623
|
|
|
582
|
-
@
|
|
624
|
+
@tool()
|
|
583
625
|
def memory_server_info(ctx: Context = None) -> dict:
|
|
584
626
|
"""The server's version + schema_version and its effective limits and
|
|
585
627
|
capabilities (write ceilings, embed provider/model/dim, available recall
|
|
@@ -589,7 +631,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
589
631
|
dim = embedder.dim if embedder is not None else None
|
|
590
632
|
return server_info(cfg, embed_dim=dim)
|
|
591
633
|
|
|
592
|
-
@
|
|
634
|
+
@tool()
|
|
593
635
|
def memory_blame(id: Optional[str] = None, at: Optional[str] = None,
|
|
594
636
|
grouped: bool = True, lines: Optional[str] = None,
|
|
595
637
|
space: Optional[str] = None, space_id: Optional[str] = None,
|
|
@@ -609,7 +651,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
609
651
|
return s.annotate(tok, id, lines=want, at=at, space=space,
|
|
610
652
|
space_id=space_id)
|
|
611
653
|
|
|
612
|
-
@
|
|
654
|
+
@tool()
|
|
613
655
|
def memory_history(id: Optional[str] = None, at: Optional[str] = None,
|
|
614
656
|
space: Optional[str] = None,
|
|
615
657
|
space_id: Optional[str] = None,
|
|
@@ -622,7 +664,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
622
664
|
return _store(conn).history(_token(ctx), id, at=at,
|
|
623
665
|
space=space, space_id=space_id)
|
|
624
666
|
|
|
625
|
-
@
|
|
667
|
+
@tool()
|
|
626
668
|
def memory_move(new_path: str, id: Optional[str] = None,
|
|
627
669
|
at: Optional[str] = None,
|
|
628
670
|
if_moved: Literal["error", "follow"] = "error",
|
|
@@ -638,7 +680,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
638
680
|
reason=reason, space=space,
|
|
639
681
|
space_id=space_id))
|
|
640
682
|
|
|
641
|
-
@
|
|
683
|
+
@tool()
|
|
642
684
|
def memory_forget(id: Optional[str] = None, at: Optional[str] = None,
|
|
643
685
|
space: Optional[str] = None,
|
|
644
686
|
space_id: Optional[str] = None,
|
|
@@ -652,7 +694,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
652
694
|
_token(ctx), id, at=at, space=space, space_id=space_id)}
|
|
653
695
|
|
|
654
696
|
# ─── identity: spaces & tokens (open/managed modes) ─────────────────────
|
|
655
|
-
@
|
|
697
|
+
@tool()
|
|
656
698
|
def memory_list_spaces(ctx: Context = None) -> List[dict]:
|
|
657
699
|
"""List the namespaces you can reach — your own plus any shared with you —
|
|
658
700
|
with each one's id, name, description, permission, and your `alias` for it
|
|
@@ -661,7 +703,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
661
703
|
with pool.connection() as conn, conn.transaction():
|
|
662
704
|
return identity.list_spaces(conn, _uid(conn, _token(ctx)))
|
|
663
705
|
|
|
664
|
-
@
|
|
706
|
+
@tool()
|
|
665
707
|
def memory_create_space(name: str, description: str = "",
|
|
666
708
|
instruction: str = "",
|
|
667
709
|
ctx: Context = None) -> dict:
|
|
@@ -675,7 +717,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
675
717
|
description=description, instruction=instruction)
|
|
676
718
|
return {"id": nsid, "name": name}
|
|
677
719
|
|
|
678
|
-
@
|
|
720
|
+
@tool()
|
|
679
721
|
def memory_set_alias(alias: str, space_id: str,
|
|
680
722
|
ctx: Context = None) -> dict:
|
|
681
723
|
"""Give a namespace a name of your own, for when a bare name is ambiguous
|
|
@@ -688,7 +730,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
688
730
|
identity.create_alias(conn, uid, alias, space_id)
|
|
689
731
|
return {"alias": alias, "space_id": space_id}
|
|
690
732
|
|
|
691
|
-
@
|
|
733
|
+
@tool()
|
|
692
734
|
def memory_drop_alias(alias: str,
|
|
693
735
|
ctx: Context = None) -> dict:
|
|
694
736
|
"""Remove one of your namespace aliases. The namespace itself is
|
|
@@ -698,7 +740,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
698
740
|
return {"dropped": identity.drop_alias(conn, uid, alias)}
|
|
699
741
|
|
|
700
742
|
if cfg.key_mode == "managed":
|
|
701
|
-
@
|
|
743
|
+
@tool()
|
|
702
744
|
def memory_enroll(key: EnrollKeyArg, ctx: Context = None) -> dict:
|
|
703
745
|
"""Claim an account with a one-time enrollment key.
|
|
704
746
|
|
|
@@ -734,7 +776,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
734
776
|
"tools you can now use are not showing yet.")
|
|
735
777
|
return out
|
|
736
778
|
|
|
737
|
-
@
|
|
779
|
+
@tool()
|
|
738
780
|
def memory_issue_token(permission: str = "write", space: Optional[str] = None,
|
|
739
781
|
space_id: Optional[str] = None, label: str = "",
|
|
740
782
|
expires_days: Optional[int] = None,
|
|
@@ -789,7 +831,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
789
831
|
out.update({"permission": permission, "namespace_id": nsid})
|
|
790
832
|
return out
|
|
791
833
|
|
|
792
|
-
@
|
|
834
|
+
@tool()
|
|
793
835
|
def memory_request_access(space_id: str, permission: str = "read",
|
|
794
836
|
ctx: Context = None) -> dict:
|
|
795
837
|
"""Ask the owner of a namespace to let you in.
|
|
@@ -804,14 +846,14 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
804
846
|
namespace_id=space_id,
|
|
805
847
|
permission=permission)
|
|
806
848
|
|
|
807
|
-
@
|
|
849
|
+
@tool()
|
|
808
850
|
def memory_list_tokens(ctx: Context = None) -> List[dict]:
|
|
809
851
|
"""List your tokens (metadata only — never the secret)."""
|
|
810
852
|
with pool.connection() as conn, conn.transaction():
|
|
811
853
|
out = identity.list_tokens(conn, _admin_uid(conn, _token(ctx)))
|
|
812
854
|
return _iso(out, *_TOKEN_TIMES)
|
|
813
855
|
|
|
814
|
-
@
|
|
856
|
+
@tool()
|
|
815
857
|
def memory_revoke_token(token_id: str,
|
|
816
858
|
ctx: Context = None) -> dict:
|
|
817
859
|
"""Revoke one of your tokens by id (kills it immediately)."""
|
|
@@ -822,7 +864,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
822
864
|
raise identity.AuthError("not your token")
|
|
823
865
|
return {"revoked": identity.revoke_token(conn, token_id)}
|
|
824
866
|
|
|
825
|
-
@
|
|
867
|
+
@tool()
|
|
826
868
|
def memory_whoami(ctx: Context = None) -> dict:
|
|
827
869
|
"""Who you are and what you may do: user id, service role, this
|
|
828
870
|
credential's permission ceiling and namespace scope, plus the
|
|
@@ -834,7 +876,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
834
876
|
# ─── control plane: provisioning (authorized in `admin`, not here) ───────
|
|
835
877
|
if admin_surface:
|
|
836
878
|
|
|
837
|
-
@
|
|
879
|
+
@tool()
|
|
838
880
|
def memory_admin_list_users(role: Optional[str] = None, limit: int = 50,
|
|
839
881
|
offset: int = 0,
|
|
840
882
|
ctx: Context = None) -> List[dict]:
|
|
@@ -846,7 +888,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
846
888
|
role=role, limit=limit, offset=offset)
|
|
847
889
|
return _iso(out, "created_at")
|
|
848
890
|
|
|
849
|
-
@
|
|
891
|
+
@tool()
|
|
850
892
|
def memory_admin_create_user(name: str = "", description: str = "",
|
|
851
893
|
role: str = "user",
|
|
852
894
|
can_create_namespace: bool = False,
|
|
@@ -872,7 +914,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
872
914
|
department=department, position=position)
|
|
873
915
|
return {"id": uid}
|
|
874
916
|
|
|
875
|
-
@
|
|
917
|
+
@tool()
|
|
876
918
|
def memory_admin_edit_user(user_id: str,
|
|
877
919
|
name: Optional[str] = None,
|
|
878
920
|
description: Optional[str] = None,
|
|
@@ -891,7 +933,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
891
933
|
conn, _principal(conn, _token(ctx)), user_id=user_id,
|
|
892
934
|
**{k: v for k, v in fields.items() if v is not None})
|
|
893
935
|
|
|
894
|
-
@
|
|
936
|
+
@tool()
|
|
895
937
|
def memory_admin_set_can_create_namespace(user_id: str, allowed: bool,
|
|
896
938
|
ctx: Context = None) -> dict:
|
|
897
939
|
"""Grant or withdraw a user's right to create namespaces. Without it
|
|
@@ -902,7 +944,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
902
944
|
conn, _principal(conn, _token(ctx)),
|
|
903
945
|
user_id=user_id, allowed=allowed)
|
|
904
946
|
|
|
905
|
-
@
|
|
947
|
+
@tool()
|
|
906
948
|
def memory_admin_set_role(user_id: str, role: str,
|
|
907
949
|
ctx: Context = None) -> dict:
|
|
908
950
|
"""Set a user's service role (user | user_manager | superadmin).
|
|
@@ -912,7 +954,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
912
954
|
return admin.set_role(conn, _principal(conn, _token(ctx)),
|
|
913
955
|
user_id=user_id, role=role)
|
|
914
956
|
|
|
915
|
-
@
|
|
957
|
+
@tool()
|
|
916
958
|
def memory_admin_list_namespaces(owner_user_id: Optional[str] = None,
|
|
917
959
|
limit: int = 50, offset: int = 0,
|
|
918
960
|
ctx: Context = None) -> List[dict]:
|
|
@@ -925,7 +967,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
925
967
|
owner_user_id=owner_user_id, limit=limit, offset=offset)
|
|
926
968
|
return _iso(out, "created_at")
|
|
927
969
|
|
|
928
|
-
@
|
|
970
|
+
@tool()
|
|
929
971
|
def memory_admin_create_namespace(name: str, owner_user_id: str,
|
|
930
972
|
description: str = "",
|
|
931
973
|
instruction: str = "",
|
|
@@ -940,7 +982,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
940
982
|
description=description, instruction=instruction)
|
|
941
983
|
return {"id": nsid}
|
|
942
984
|
|
|
943
|
-
@
|
|
985
|
+
@tool()
|
|
944
986
|
def memory_admin_edit_namespace(space_id: str,
|
|
945
987
|
description: Optional[str] = None,
|
|
946
988
|
instruction: Optional[str] = None,
|
|
@@ -959,7 +1001,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
959
1001
|
namespace_id=space_id, description=description,
|
|
960
1002
|
instruction=instruction, name=name)
|
|
961
1003
|
|
|
962
|
-
@
|
|
1004
|
+
@tool()
|
|
963
1005
|
def memory_admin_count_orphans(ctx: Context = None) -> dict:
|
|
964
1006
|
"""How many memories are stranded in the pre-identity namespace.
|
|
965
1007
|
`single` mode stores everything under one nameless namespace; after a
|
|
@@ -969,7 +1011,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
969
1011
|
with pool.connection() as conn:
|
|
970
1012
|
return admin.count_orphans(conn, _principal(conn, _token(ctx)))
|
|
971
1013
|
|
|
972
|
-
@
|
|
1014
|
+
@tool()
|
|
973
1015
|
def memory_admin_adopt_orphans(space_id: str,
|
|
974
1016
|
ctx: Context = None) -> dict:
|
|
975
1017
|
"""Move every stranded `single`-mode memory into a real namespace.
|
|
@@ -981,7 +1023,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
981
1023
|
conn, _principal(conn, _token(ctx)),
|
|
982
1024
|
namespace_id=space_id, vectors=backend)
|
|
983
1025
|
|
|
984
|
-
@
|
|
1026
|
+
@tool()
|
|
985
1027
|
def memory_admin_add_member(space_id: str, user_id: str,
|
|
986
1028
|
permission: str = "read",
|
|
987
1029
|
ctx: Context = None) -> dict:
|
|
@@ -994,7 +1036,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
994
1036
|
namespace_id=space_id, user_id=user_id,
|
|
995
1037
|
permission=permission)
|
|
996
1038
|
|
|
997
|
-
@
|
|
1039
|
+
@tool()
|
|
998
1040
|
def memory_admin_remove_member(space_id: str, user_id: str,
|
|
999
1041
|
ctx: Context = None) -> dict:
|
|
1000
1042
|
"""Take a shared namespace away again. `removed: false` means they
|
|
@@ -1009,7 +1051,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1009
1051
|
return admin.remove_member(conn, _principal(conn, _token(ctx)),
|
|
1010
1052
|
namespace_id=space_id, user_id=user_id)
|
|
1011
1053
|
|
|
1012
|
-
@
|
|
1054
|
+
@tool()
|
|
1013
1055
|
def memory_admin_transfer_namespace(
|
|
1014
1056
|
space_id: str, new_owner_user_id: str,
|
|
1015
1057
|
keep_previous_owner: Optional[str] = "admin",
|
|
@@ -1028,7 +1070,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1028
1070
|
new_owner_user_id=new_owner_user_id,
|
|
1029
1071
|
keep_previous_owner=keep_previous_owner)
|
|
1030
1072
|
|
|
1031
|
-
@
|
|
1073
|
+
@tool()
|
|
1032
1074
|
def memory_admin_list_requests(space_id: str,
|
|
1033
1075
|
ctx: Context = None) -> List[dict]:
|
|
1034
1076
|
"""Pending requests to join a namespace you administer — who asked,
|
|
@@ -1038,7 +1080,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1038
1080
|
namespace_id=space_id)
|
|
1039
1081
|
return _iso(out, "created_at")
|
|
1040
1082
|
|
|
1041
|
-
@
|
|
1083
|
+
@tool()
|
|
1042
1084
|
def memory_admin_decide_access(request_id: str, approve: bool = True,
|
|
1043
1085
|
expect_permission: Optional[str] = None,
|
|
1044
1086
|
ctx: Context = None) -> dict:
|
|
@@ -1056,7 +1098,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1056
1098
|
expect_permission=expect_permission)
|
|
1057
1099
|
return {"request_id": request_id, "approved": approve}
|
|
1058
1100
|
|
|
1059
|
-
@
|
|
1101
|
+
@tool()
|
|
1060
1102
|
def memory_admin_set_disabled(user_id: str, disabled: bool = True,
|
|
1061
1103
|
ctx: Context = None) -> dict:
|
|
1062
1104
|
"""Switch an account off, or back on.
|
|
@@ -1070,7 +1112,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1070
1112
|
return admin.set_disabled(conn, _principal(conn, _token(ctx)),
|
|
1071
1113
|
user_id=user_id, disabled=disabled)
|
|
1072
1114
|
|
|
1073
|
-
@
|
|
1115
|
+
@tool()
|
|
1074
1116
|
def memory_admin_list_members(space_id: str,
|
|
1075
1117
|
ctx: Context = None) -> List[dict]:
|
|
1076
1118
|
"""Who can reach a namespace — the owner first, then everyone shared
|
|
@@ -1080,7 +1122,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1080
1122
|
namespace_id=space_id)
|
|
1081
1123
|
return _iso(out, "created_at")
|
|
1082
1124
|
|
|
1083
|
-
@
|
|
1125
|
+
@tool()
|
|
1084
1126
|
def memory_admin_issue_token(user_id: str, permission: str = "write",
|
|
1085
1127
|
space_id: Optional[str] = None,
|
|
1086
1128
|
label: str = "",
|
|
@@ -1112,7 +1154,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1112
1154
|
out["warning"] = minted["warning"]
|
|
1113
1155
|
return out
|
|
1114
1156
|
|
|
1115
|
-
@
|
|
1157
|
+
@tool()
|
|
1116
1158
|
def memory_admin_create_enrollment(
|
|
1117
1159
|
user_id: str, permission: str = "write",
|
|
1118
1160
|
space_id: Optional[str] = None, label: str = "",
|
|
@@ -1146,7 +1188,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1146
1188
|
out["expires_at"] = out["expires_at"].isoformat()
|
|
1147
1189
|
return admin.deliver_key(out, cfg.token_sink)
|
|
1148
1190
|
|
|
1149
|
-
@
|
|
1191
|
+
@tool()
|
|
1150
1192
|
def memory_admin_list_enrollments(user_id: Optional[str] = None,
|
|
1151
1193
|
ctx: Context = None) -> List[dict]:
|
|
1152
1194
|
"""Enrollment keys and what became of them — `state` is pending,
|
|
@@ -1158,7 +1200,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1158
1200
|
user_id=user_id)
|
|
1159
1201
|
return _iso(out, "created_at", "expires_at", "used_at", "revoked_at")
|
|
1160
1202
|
|
|
1161
|
-
@
|
|
1203
|
+
@tool()
|
|
1162
1204
|
def memory_admin_revoke_enrollment(enrollment_id: str,
|
|
1163
1205
|
ctx: Context = None) -> dict:
|
|
1164
1206
|
"""Kill an unredeemed enrollment key. False means it was already
|
|
@@ -1169,7 +1211,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1169
1211
|
conn, _principal(conn, _token(ctx)),
|
|
1170
1212
|
enrollment_id=enrollment_id)}
|
|
1171
1213
|
|
|
1172
|
-
@
|
|
1214
|
+
@tool()
|
|
1173
1215
|
def memory_admin_list_tokens(user_id: str,
|
|
1174
1216
|
ctx: Context = None) -> List[dict]:
|
|
1175
1217
|
"""A user's tokens — metadata only, never the secret."""
|
|
@@ -1178,7 +1220,7 @@ def build_server(cfg: Optional[Config] = None):
|
|
|
1178
1220
|
user_id=user_id)
|
|
1179
1221
|
return _iso(out, *_TOKEN_TIMES)
|
|
1180
1222
|
|
|
1181
|
-
@
|
|
1223
|
+
@tool()
|
|
1182
1224
|
def memory_admin_revoke_token(token_id: str,
|
|
1183
1225
|
ctx: Context = None) -> dict:
|
|
1184
1226
|
"""Kill any user's token immediately. False means it was already
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
-- Re-derive the link graph, because the PARSER changed, not the schema.
|
|
2
|
+
--
|
|
3
|
+
-- Until now `_PATH` rejected the hyphen, so every `[[infra.servers.video-production]]`
|
|
4
|
+
-- was classified as prose and dropped — not stored as a dangling edge, dropped —
|
|
5
|
+
-- while `[[proxies]]` from a TOML example indented as code was stored as a real
|
|
6
|
+
-- one. Both halves are fixed in `links.py`; neither fix reaches a body that is
|
|
7
|
+
-- already written, since edges are derived on write.
|
|
8
|
+
--
|
|
9
|
+
-- Clearing the flag makes the existing one-time backfill (`relink.rebuild`, run
|
|
10
|
+
-- at startup by `maybe_backfill`) do the pass again over every body. It rewrites
|
|
11
|
+
-- only the edge table: bodies, history and the hash chain are untouched, so this
|
|
12
|
+
-- is repeatable and costs nothing but a scan.
|
|
13
|
+
--
|
|
14
|
+
-- Additive by nature — an older client reading this database is no worse off
|
|
15
|
+
-- than it was, so the compatibility floor does not move.
|
|
16
|
+
UPDATE memgres_meta SET links_built = false;
|