@remnic/core 9.3.679 → 9.3.681
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/dist/access-boundary.d.ts +178 -0
- package/dist/access-boundary.js +121 -0
- package/dist/access-boundary.js.map +1 -0
- package/dist/access-cli.js +114 -100
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +47 -45
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +43 -41
- package/dist/access-operations.d.ts +127 -0
- package/dist/access-operations.js +115 -0
- package/dist/access-operations.js.map +1 -0
- package/dist/access-schema.d.ts +34 -34
- package/dist/access-schema.js +3 -3
- package/dist/{access-service-S9oGKPZc.d.ts → access-service-DvA6jyHL.d.ts} +1 -1
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +39 -39
- package/dist/access-surface-catalog.d.ts +125 -0
- package/dist/access-surface-catalog.js +162 -0
- package/dist/access-surface-catalog.js.map +1 -0
- package/dist/adapters/index.js +7 -7
- package/dist/adapters/registry.js +3 -3
- package/dist/auto-sync-5CJBJMPZ.js +1 -1
- package/dist/briefing.js +8 -8
- package/dist/causal-behavior.js +5 -5
- package/dist/causal-chain.js +3 -3
- package/dist/causal-consolidation.js +16 -16
- package/dist/causal-retrieval.js +3 -3
- package/dist/causal-trajectory.js +1 -1
- package/dist/{chunk-JBPKEARU.js → chunk-2QSZNTDO.js} +7 -7
- package/dist/{chunk-3OKWZT7F.js → chunk-3IND7N4X.js} +2 -2
- package/dist/{chunk-GYSYLGNE.js → chunk-7MOTEVAA.js} +2 -2
- package/dist/{chunk-6T4LTI2F.js → chunk-7XH7VJN4.js} +4 -4
- package/dist/{chunk-AGNBY3VG.js → chunk-APJQ6UEA.js} +4 -4
- package/dist/{chunk-LZSMQHXC.js → chunk-ARLRTZZZ.js} +5 -5
- package/dist/{chunk-Q2H5U37U.js → chunk-B2B2IHUH.js} +2 -2
- package/dist/{chunk-SECQS4G4.js → chunk-BTVX7ZXZ.js} +5 -5
- package/dist/{chunk-DGEZKYVI.js → chunk-DOCTITOP.js} +4 -4
- package/dist/{chunk-EQYP3HA6.js → chunk-EG4TCVMU.js} +2 -2
- package/dist/{chunk-SLTKP5WJ.js → chunk-EW5KFXHL.js} +4 -4
- package/dist/{chunk-K2JYO6QV.js → chunk-FDSOMA6M.js} +28 -41
- package/dist/chunk-FDSOMA6M.js.map +1 -0
- package/dist/{chunk-CTCPB57O.js → chunk-G7Z3C2X6.js} +2 -2
- package/dist/{chunk-4PPMUNV5.js → chunk-H4BDNIKQ.js} +52 -52
- package/dist/{chunk-MTJ2LFAJ.js → chunk-H6PMGMNP.js} +2 -2
- package/dist/{chunk-7AAKSHDG.js → chunk-I3HSKQT7.js} +136 -136
- package/dist/{chunk-NXBXM7Q6.js → chunk-I75DF4FZ.js} +2 -2
- package/dist/{chunk-RC3AFF6Z.js → chunk-JD4SCARD.js} +1 -1
- package/dist/{chunk-LVTTO3VC.js → chunk-KACIOX42.js} +2 -2
- package/dist/{chunk-ATRB6Q25.js → chunk-KV6CX4ON.js} +2 -2
- package/dist/{chunk-VL5JJOOY.js → chunk-L5MUA6Q7.js} +5 -5
- package/dist/{chunk-PCGCQTU6.js → chunk-M4I3TREG.js} +75 -75
- package/dist/chunk-NHFXF4ZO.js +107 -0
- package/dist/chunk-NHFXF4ZO.js.map +1 -0
- package/dist/{chunk-MNUPGYIV.js → chunk-NQMBSSWW.js} +2 -2
- package/dist/{chunk-V4ZHKCGA.js → chunk-O2WELT5C.js} +5 -5
- package/dist/{chunk-Z6SEG36L.js → chunk-OUWAQVDJ.js} +4 -4
- package/dist/{chunk-57ME5VSI.js → chunk-Q5ZU3RNY.js} +4 -4
- package/dist/{chunk-ACYX37IM.js → chunk-QUA2JPH2.js} +6 -6
- package/dist/{chunk-UNZLU2MX.js → chunk-QVWM4C24.js} +37 -32
- package/dist/chunk-QVWM4C24.js.map +1 -0
- package/dist/{chunk-2AP4QJX5.js → chunk-TOQEZ63C.js} +8 -8
- package/dist/{chunk-EUM7CZFM.js → chunk-TY5NT3T3.js} +17 -17
- package/dist/{chunk-ZCVPFDHB.js → chunk-UAODC6GJ.js} +14 -14
- package/dist/{chunk-YJ4J2JJ2.js → chunk-UJDV2NLT.js} +9 -9
- package/dist/chunk-V254FAT5.js +85 -0
- package/dist/chunk-V254FAT5.js.map +1 -0
- package/dist/{chunk-3IE22DJ2.js → chunk-WEPMT6SC.js} +10 -10
- package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
- package/dist/{cli-B2Ve7R22.d.ts → cli-feUe-x3I.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +74 -72
- package/dist/compounding/engine.js +9 -9
- package/dist/connectors/codex-materialize-runner.js +9 -9
- package/dist/connectors/index.js +9 -9
- package/dist/consolidation-provenance-check.js +2 -2
- package/dist/contradiction/index.js +4 -4
- package/dist/dashboard-runtime.js +2 -2
- package/dist/entity-retrieval.js +7 -7
- package/dist/extraction.js +2 -2
- package/dist/{first-start-migration-PG5HBC3K.js → first-start-migration-FF7YFGRP.js} +4 -4
- package/dist/index.d.ts +2 -2
- package/dist/index.js +209 -207
- package/dist/index.js.map +1 -1
- package/dist/lcm/engine.js +4 -4
- package/dist/lcm/index.js +12 -12
- package/dist/maintenance/memory-governance.js +8 -8
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +9 -9
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +17 -17
- package/dist/namespaces/search.js +8 -8
- package/dist/namespaces/storage.js +8 -8
- package/dist/operator-toolkit.js +22 -22
- package/dist/orchestrator.js +70 -70
- package/dist/resume-bundles.js +1 -1
- package/dist/schemas.d.ts +50 -50
- package/dist/search/factory.js +7 -7
- package/dist/search/index.js +11 -11
- package/dist/search/lancedb-backend.js +3 -3
- package/dist/search/meilisearch-backend.js +3 -3
- package/dist/search/orama-backend.js +3 -3
- package/dist/semantic-consolidation.js +11 -11
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +8 -8
- package/dist/storage.js +6 -6
- package/dist/transfer/backup.js +2 -2
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/capsule-import.js +1 -1
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +38 -38
- package/dist/utils/serialize-mutations.d.ts +122 -0
- package/dist/utils/serialize-mutations.js +287 -0
- package/dist/utils/serialize-mutations.js.map +1 -0
- package/dist/verified-recall.js +8 -8
- package/package.json +12 -2
- package/src/access-boundary.test.ts +212 -0
- package/src/access-boundary.ts +235 -0
- package/src/access-cli.ts +32 -15
- package/src/access-http.ts +38 -28
- package/src/access-mcp.ts +41 -35
- package/src/access-operations.ts +157 -0
- package/src/access-surface-catalog.test.ts +772 -0
- package/src/access-surface-catalog.ts +218 -0
- package/src/utils/serialize-mutations.test.ts +1047 -0
- package/src/utils/serialize-mutations.ts +679 -0
- package/dist/chunk-K2JYO6QV.js.map +0 -1
- package/dist/chunk-UNZLU2MX.js.map +0 -1
- /package/dist/{chunk-JBPKEARU.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-3OKWZT7F.js.map → chunk-3IND7N4X.js.map} +0 -0
- /package/dist/{chunk-GYSYLGNE.js.map → chunk-7MOTEVAA.js.map} +0 -0
- /package/dist/{chunk-6T4LTI2F.js.map → chunk-7XH7VJN4.js.map} +0 -0
- /package/dist/{chunk-AGNBY3VG.js.map → chunk-APJQ6UEA.js.map} +0 -0
- /package/dist/{chunk-LZSMQHXC.js.map → chunk-ARLRTZZZ.js.map} +0 -0
- /package/dist/{chunk-Q2H5U37U.js.map → chunk-B2B2IHUH.js.map} +0 -0
- /package/dist/{chunk-SECQS4G4.js.map → chunk-BTVX7ZXZ.js.map} +0 -0
- /package/dist/{chunk-DGEZKYVI.js.map → chunk-DOCTITOP.js.map} +0 -0
- /package/dist/{chunk-EQYP3HA6.js.map → chunk-EG4TCVMU.js.map} +0 -0
- /package/dist/{chunk-SLTKP5WJ.js.map → chunk-EW5KFXHL.js.map} +0 -0
- /package/dist/{chunk-CTCPB57O.js.map → chunk-G7Z3C2X6.js.map} +0 -0
- /package/dist/{chunk-4PPMUNV5.js.map → chunk-H4BDNIKQ.js.map} +0 -0
- /package/dist/{chunk-MTJ2LFAJ.js.map → chunk-H6PMGMNP.js.map} +0 -0
- /package/dist/{chunk-7AAKSHDG.js.map → chunk-I3HSKQT7.js.map} +0 -0
- /package/dist/{chunk-NXBXM7Q6.js.map → chunk-I75DF4FZ.js.map} +0 -0
- /package/dist/{chunk-RC3AFF6Z.js.map → chunk-JD4SCARD.js.map} +0 -0
- /package/dist/{chunk-LVTTO3VC.js.map → chunk-KACIOX42.js.map} +0 -0
- /package/dist/{chunk-ATRB6Q25.js.map → chunk-KV6CX4ON.js.map} +0 -0
- /package/dist/{chunk-VL5JJOOY.js.map → chunk-L5MUA6Q7.js.map} +0 -0
- /package/dist/{chunk-PCGCQTU6.js.map → chunk-M4I3TREG.js.map} +0 -0
- /package/dist/{chunk-MNUPGYIV.js.map → chunk-NQMBSSWW.js.map} +0 -0
- /package/dist/{chunk-V4ZHKCGA.js.map → chunk-O2WELT5C.js.map} +0 -0
- /package/dist/{chunk-Z6SEG36L.js.map → chunk-OUWAQVDJ.js.map} +0 -0
- /package/dist/{chunk-57ME5VSI.js.map → chunk-Q5ZU3RNY.js.map} +0 -0
- /package/dist/{chunk-ACYX37IM.js.map → chunk-QUA2JPH2.js.map} +0 -0
- /package/dist/{chunk-2AP4QJX5.js.map → chunk-TOQEZ63C.js.map} +0 -0
- /package/dist/{chunk-EUM7CZFM.js.map → chunk-TY5NT3T3.js.map} +0 -0
- /package/dist/{chunk-ZCVPFDHB.js.map → chunk-UAODC6GJ.js.map} +0 -0
- /package/dist/{chunk-YJ4J2JJ2.js.map → chunk-UJDV2NLT.js.map} +0 -0
- /package/dist/{chunk-3IE22DJ2.js.map → chunk-WEPMT6SC.js.map} +0 -0
- /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
- /package/dist/{first-start-migration-PG5HBC3K.js.map → first-start-migration-FF7YFGRP.js.map} +0 -0
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single input-validation and error boundary for the CLI/MCP/HTTP access
|
|
3
|
+
* surfaces (issue #1525, epic #1520 Phase 1).
|
|
4
|
+
*
|
|
5
|
+
* Every operation that crosses the access-service facade passes through ONE
|
|
6
|
+
* registry entry: a zod-validated request envelope, a shared error mapper,
|
|
7
|
+
* and the "reject invalid input and list valid options" behavior that
|
|
8
|
+
* CLAUDE.md rules 14/17/24/28/36/48/51 previously had to be re-implemented
|
|
9
|
+
* per handler. The three surfaces become thin adapters — one operation
|
|
10
|
+
* definition, three transports — so a validation fix lands everywhere at
|
|
11
|
+
* once.
|
|
12
|
+
*
|
|
13
|
+
* Host-agnostic (rule 31): operation names carry no `openclaw-*`/`engram-*`
|
|
14
|
+
* prefix. Session/namespace tenancy stays in the handler layer (resolved via
|
|
15
|
+
* ScopePlan #1521); the boundary validates SHAPE, not tenancy.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { z } from "zod";
|
|
19
|
+
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
20
|
+
import { expandTildePath } from "./utils/path.js";
|
|
21
|
+
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
// Canonical operation names — host-agnostic (rule 31)
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Canonical operation ids. One id is shared by the MCP tool, the HTTP route,
|
|
28
|
+
* and the CLI command that expose the same operation. Add to this union as
|
|
29
|
+
* each domain-group migration PR (memory ops → connectors → namespaces …)
|
|
30
|
+
* lands; the fitness test in `access-surface-catalog.test.ts` treats the
|
|
31
|
+
* registered set as the migration state.
|
|
32
|
+
*/
|
|
33
|
+
export type OperationName =
|
|
34
|
+
| "memory_get"
|
|
35
|
+
| "memory_search"
|
|
36
|
+
| "memory_store";
|
|
37
|
+
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Operation context — what every handler receives
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Per-call context. `service` is the facade the handler delegates to; the
|
|
44
|
+
* boundary never reaches past it. `authenticatedPrincipal` is resolved by
|
|
45
|
+
* the SURFACE (MCP header / HTTP identity / CLI flag) before the boundary
|
|
46
|
+
* runs, so handlers stay principal-source-agnostic. `hooks` carries
|
|
47
|
+
* transport-level callbacks (e.g. HTTP write-quota enforcement) that must
|
|
48
|
+
* fire atomically inside the service call; surfaces that have no such hook
|
|
49
|
+
* leave it undefined.
|
|
50
|
+
*/
|
|
51
|
+
export interface OperationContext {
|
|
52
|
+
readonly service: EngramAccessService;
|
|
53
|
+
readonly authenticatedPrincipal?: string;
|
|
54
|
+
readonly hooks?: OperationHooks;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Transport-level callbacks a handler forwards into the service call. Kept
|
|
59
|
+
* narrow on purpose: the boundary owns validation + dispatch shape, not
|
|
60
|
+
* transport policy. Add fields here only when a surface genuinely needs a
|
|
61
|
+
* callback the service itself consumes.
|
|
62
|
+
*/
|
|
63
|
+
export interface OperationHooks {
|
|
64
|
+
/** HTTP write-quota gate; throws to reject the write when exhausted. */
|
|
65
|
+
readonly enforceWriteQuota?: () => void | Promise<void>;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
// Operation spec + bound operation
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
export interface OperationSpec<In, Out> {
|
|
73
|
+
/** Canonical operation id; matches an {@link OperationName}. */
|
|
74
|
+
readonly name: OperationName;
|
|
75
|
+
readonly description: string;
|
|
76
|
+
/** Zod schema validating the raw request envelope. */
|
|
77
|
+
readonly schema: z.ZodType<In>;
|
|
78
|
+
/** Handler invoked with the parsed input; throws EngramAccessInputError for domain faults. */
|
|
79
|
+
readonly handler: (input: In, ctx: OperationContext) => Promise<Out>;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export interface BoundOperation<In = unknown, Out = unknown> {
|
|
83
|
+
readonly spec: OperationSpec<In, Out>;
|
|
84
|
+
/** Validate the raw envelope, then invoke the handler. Throws EngramAccessInputError on any validation failure. */
|
|
85
|
+
readonly run: (rawInput: unknown, ctx: OperationContext) => Promise<Out>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ---------------------------------------------------------------------------
|
|
89
|
+
// Shared normalizers the boundary owns (rules 17, 28, 36, 48, 51)
|
|
90
|
+
// ---------------------------------------------------------------------------
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Coerce boolean-like strings at the edge (rule 36). Accepts actual booleans
|
|
94
|
+
* and the string spellings clients send ("true"/"false"/"1"/"0"/"yes"/"no"/
|
|
95
|
+
* "on"/"off", case-insensitive). Rejects anything else loudly — `Boolean("false")`
|
|
96
|
+
* would silently be `true`, which is the bug rule 36 exists to prevent.
|
|
97
|
+
*
|
|
98
|
+
* `undefined`/`null`/`""` → `undefined`, so callers can keep treating an
|
|
99
|
+
* absent flag as "use the default" without a separate presence check.
|
|
100
|
+
*/
|
|
101
|
+
export function coerceBooleanLike(value: unknown): boolean | undefined {
|
|
102
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
103
|
+
if (typeof value === "boolean") return value;
|
|
104
|
+
if (typeof value === "string") {
|
|
105
|
+
const lower = value.trim().toLowerCase();
|
|
106
|
+
if (lower === "true" || lower === "1" || lower === "yes" || lower === "on") return true;
|
|
107
|
+
if (lower === "false" || lower === "0" || lower === "no" || lower === "off") return false;
|
|
108
|
+
}
|
|
109
|
+
throw new EngramAccessInputError(
|
|
110
|
+
`expected a boolean-like value (true|false|1|0|yes|no|on|off); got ${JSON.stringify(value)}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Coerce + validate a positive integer from a numeric string or number
|
|
116
|
+
* (rule 28). Loosely-typed MCP/CLI clients send `"5"`; `typeof saved === "number"`
|
|
117
|
+
* on read-back would reject it later, so we coerce at the edge and reject
|
|
118
|
+
* booleans/objects loudly (`Number(true) === 1` would silently pass otherwise).
|
|
119
|
+
*
|
|
120
|
+
* `undefined`/`null`/`""` → `undefined`.
|
|
121
|
+
*/
|
|
122
|
+
export function coercePositiveInteger(value: unknown, label: string): number | undefined {
|
|
123
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
124
|
+
if (typeof value === "number") {
|
|
125
|
+
if (!Number.isFinite(value) || value <= 0 || !Number.isInteger(value)) {
|
|
126
|
+
throw new EngramAccessInputError(`${label} expects a positive integer; got ${JSON.stringify(value)}`);
|
|
127
|
+
}
|
|
128
|
+
return value;
|
|
129
|
+
}
|
|
130
|
+
if (typeof value === "string") {
|
|
131
|
+
const trimmed = value.trim();
|
|
132
|
+
if (!/^[+-]?\d+$/.test(trimmed)) {
|
|
133
|
+
throw new EngramAccessInputError(`${label} expects a positive integer; got ${JSON.stringify(value)}`);
|
|
134
|
+
}
|
|
135
|
+
const parsed = Number(trimmed);
|
|
136
|
+
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
|
|
137
|
+
throw new EngramAccessInputError(`${label} expects a positive integer; got ${JSON.stringify(value)}`);
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
throw new EngramAccessInputError(`${label} expects a positive integer; got ${JSON.stringify(value)}`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Expand `~` in a path-shaped input (rule 17). Node `fs` does NOT expand `~`;
|
|
146
|
+
* ad-hoc regex drifts. `undefined`/`null`/`""` → `undefined`.
|
|
147
|
+
*/
|
|
148
|
+
export function normalizeOptionalPath(value: unknown): string | undefined {
|
|
149
|
+
if (value === undefined || value === null || value === "") return undefined;
|
|
150
|
+
if (typeof value !== "string") {
|
|
151
|
+
throw new EngramAccessInputError(`expected a path string; got ${JSON.stringify(value)}`);
|
|
152
|
+
}
|
|
153
|
+
return expandTildePath(value);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Error formatting — rule 51: list valid options, never silently default
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Turn a zod failure into an {@link EngramAccessInputError} whose message
|
|
162
|
+
* names the offending field and — for enum/union issues — lists the valid
|
|
163
|
+
* options, so the caller can correct rather than guess (rule 51).
|
|
164
|
+
*/
|
|
165
|
+
export function formatZodIssues(error: z.ZodError): string {
|
|
166
|
+
const parts: string[] = [];
|
|
167
|
+
for (const issue of error.issues) {
|
|
168
|
+
const path = issue.path.length > 0 ? issue.path.join(".") : "(root)";
|
|
169
|
+
const options = enumOptionsFromIssue(issue);
|
|
170
|
+
const suffix = options ? `. Valid: ${options.join(", ")}` : "";
|
|
171
|
+
parts.push(`${path}: ${issue.message}${suffix}`);
|
|
172
|
+
}
|
|
173
|
+
return parts.length > 0
|
|
174
|
+
? `request validation failed: ${parts.join("; ")}`
|
|
175
|
+
: "request validation failed";
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function enumOptionsFromIssue(issue: z.ZodIssue): readonly string[] | undefined {
|
|
179
|
+
// zod exposes accepted enum values on the issue for ZodEnum / ZodNativeEnum
|
|
180
|
+
// and on the options of invalid_union discriminators. Reading them here
|
|
181
|
+
// keeps "list valid options" in ONE place rather than per handler.
|
|
182
|
+
if (issue.code === z.ZodIssueCode.invalid_enum_value) {
|
|
183
|
+
const rawOptions = (issue as { options?: unknown }).options;
|
|
184
|
+
if (Array.isArray(rawOptions)) {
|
|
185
|
+
return rawOptions.map((opt) => String(opt));
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
return undefined;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
// Registry
|
|
193
|
+
// ---------------------------------------------------------------------------
|
|
194
|
+
|
|
195
|
+
const registry = new Map<OperationName, BoundOperation>();
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Register an operation. Throws if the name is already registered — duplicate
|
|
199
|
+
* registration is a programming error, not a runtime input fault, so it throws
|
|
200
|
+
* a plain Error (not the input-error class surfaces translate for clients).
|
|
201
|
+
*/
|
|
202
|
+
export function defineOperation<In, Out>(spec: OperationSpec<In, Out>): BoundOperation<In, Out> {
|
|
203
|
+
if (registry.has(spec.name)) {
|
|
204
|
+
throw new Error(`access-boundary: operation already registered: ${spec.name}`);
|
|
205
|
+
}
|
|
206
|
+
const bound: BoundOperation<In, Out> = {
|
|
207
|
+
spec,
|
|
208
|
+
run: async (rawInput, ctx) => {
|
|
209
|
+
const parseResult = spec.schema.safeParse(rawInput);
|
|
210
|
+
if (!parseResult.success) {
|
|
211
|
+
throw new EngramAccessInputError(formatZodIssues(parseResult.error));
|
|
212
|
+
}
|
|
213
|
+
return spec.handler(parseResult.data, ctx);
|
|
214
|
+
},
|
|
215
|
+
};
|
|
216
|
+
// Store under the canonical name; the cast is safe because In/Out are
|
|
217
|
+
// erased at the registry boundary and recovered by callers via getOperation.
|
|
218
|
+
registry.set(spec.name, bound as unknown as BoundOperation);
|
|
219
|
+
return bound;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/** Look up a registered operation by canonical name. */
|
|
223
|
+
export function getOperation(name: OperationName): BoundOperation | undefined {
|
|
224
|
+
return registry.get(name);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/** All registered operation names. */
|
|
228
|
+
export function listRegisteredOperations(): readonly OperationName[] {
|
|
229
|
+
return [...registry.keys()];
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/** Test-only: clear the registry so pilot definitions can be re-registered. */
|
|
233
|
+
export function __resetRegistryForTest(): void {
|
|
234
|
+
registry.clear();
|
|
235
|
+
}
|
package/src/access-cli.ts
CHANGED
|
@@ -7,6 +7,10 @@ import { EngramAccessService } from "./access-service.js";
|
|
|
7
7
|
import { readEnvVar, resolveHomeDir } from "./runtime/env.js";
|
|
8
8
|
import { resolvePluginEntry } from "./plugin-entry-resolver.js";
|
|
9
9
|
import { expandTildePath } from "./utils/path.js";
|
|
10
|
+
import { getOperation } from "./access-boundary.js";
|
|
11
|
+
// Importing access-operations registers the pilot boundary operations as a
|
|
12
|
+
// side effect; the store command dispatches through the registry (issue #1525).
|
|
13
|
+
import "./access-operations.js";
|
|
10
14
|
|
|
11
15
|
const OPENCLAW_REMNIC_PLUGIN_IDS = ["openclaw-remnic", "openclaw-engram"] as const;
|
|
12
16
|
|
|
@@ -428,21 +432,34 @@ async function runStore(args: ParsedArgs, preferredId?: string): Promise<void> {
|
|
|
428
432
|
};
|
|
429
433
|
|
|
430
434
|
const { config, service } = buildRuntime(preferredId);
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
435
|
+
// Migrated through the access boundary (issue #1525): the store command
|
|
436
|
+
// dispatches through the same registry entry as the MCP tool and HTTP
|
|
437
|
+
// route, so validation/normalization is owned in ONE place. The CLI has no
|
|
438
|
+
// write-quota hook (it is a one-shot process), so no hooks are forwarded.
|
|
439
|
+
const op = getOperation("memory_store");
|
|
440
|
+
if (!op) {
|
|
441
|
+
throw new Error("access-boundary: operation not registered: memory_store");
|
|
442
|
+
}
|
|
443
|
+
const output = (await op.run(
|
|
444
|
+
{
|
|
445
|
+
namespace: storeArgs.namespace,
|
|
446
|
+
sessionKey: storeArgs.sessionKey,
|
|
447
|
+
content: storeArgs.content,
|
|
448
|
+
category: storeArgs.category,
|
|
449
|
+
confidence: storeArgs.confidence,
|
|
450
|
+
tags: storeArgs.tags,
|
|
451
|
+
entityRef: storeArgs.entityRef,
|
|
452
|
+
ttl: storeArgs.ttl,
|
|
453
|
+
sourceReason: storeArgs.sourceReason,
|
|
454
|
+
idempotencyKey: storeArgs.idempotencyKey,
|
|
455
|
+
dryRun: storeArgs.dryRun,
|
|
456
|
+
},
|
|
457
|
+
{
|
|
458
|
+
service,
|
|
459
|
+
authenticatedPrincipal: getLastOption(args, "principal") ?? config.agentAccessHttp.principal,
|
|
460
|
+
},
|
|
461
|
+
)) as { result: unknown };
|
|
462
|
+
console.log(JSON.stringify(output.result, null, 2));
|
|
446
463
|
}
|
|
447
464
|
|
|
448
465
|
function expandOptionalPath(value: string | undefined): string | undefined {
|
package/src/access-http.ts
CHANGED
|
@@ -7,7 +7,7 @@ import path from "node:path";
|
|
|
7
7
|
import { fileURLToPath, URL } from "node:url";
|
|
8
8
|
import { gunzipSync } from "node:zlib";
|
|
9
9
|
import { log } from "./logger.js";
|
|
10
|
-
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
10
|
+
import { EngramAccessInputError, type EngramAccessService, type EngramAccessMemoryResponse, type EngramAccessWriteResponse } from "./access-service.js";
|
|
11
11
|
import { WearablesInputError } from "./wearables/errors.js";
|
|
12
12
|
import { EngramMcpServer } from "./access-mcp.js";
|
|
13
13
|
import { validateRequest, type SchemaName, type SchemaTypeFor } from "./access-schema.js";
|
|
@@ -27,6 +27,11 @@ import {
|
|
|
27
27
|
} from "./graph-events.js";
|
|
28
28
|
import { expandTildePath } from "./utils/path.js";
|
|
29
29
|
import { projectTagProjectId } from "./coding/coding-namespace.js";
|
|
30
|
+
import { getOperation } from "./access-boundary.js";
|
|
31
|
+
// Importing access-operations registers the pilot boundary operations
|
|
32
|
+
// (memory_get / memory_store) as a side effect; the HTTP handlers below
|
|
33
|
+
// dispatch the migrated routes through the registry (issue #1525).
|
|
34
|
+
import "./access-operations.js";
|
|
30
35
|
|
|
31
36
|
export interface EngramAccessHttpServerOptions {
|
|
32
37
|
service: EngramAccessService;
|
|
@@ -1297,35 +1302,28 @@ export class EngramAccessHttpServer {
|
|
|
1297
1302
|
}
|
|
1298
1303
|
|
|
1299
1304
|
if (req.method === "POST" && pathname === "/engram/v1/memories") {
|
|
1305
|
+
// Migrated through the access boundary (issue #1525): the registry
|
|
1306
|
+
// entry owns schema validation, normalization, and service dispatch.
|
|
1307
|
+
// The HTTP transport resolves the request-scoped namespace and principal
|
|
1308
|
+
// BEFORE the boundary re-validates the cleaned envelope. The write-quota
|
|
1309
|
+
// hook is forwarded via ctx.hooks so it still fires atomically inside
|
|
1310
|
+
// the service's idempotent-write lock — never before, never on a replay
|
|
1311
|
+
// (#1434 invariant preserved by the boundary migration).
|
|
1300
1312
|
const body = await this.readValidatedBody(req, "memoryStore");
|
|
1301
|
-
const
|
|
1302
|
-
|
|
1303
|
-
idempotencyKey: body.idempotencyKey,
|
|
1304
|
-
dryRun: body.dryRun === true,
|
|
1305
|
-
sessionKey: body.sessionKey,
|
|
1306
|
-
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
1307
|
-
content: body.content,
|
|
1308
|
-
category: body.category,
|
|
1309
|
-
confidence: body.confidence,
|
|
1313
|
+
const envelope = {
|
|
1314
|
+
...body,
|
|
1310
1315
|
namespace: this.resolveNamespace(req, body.namespace),
|
|
1311
|
-
tags: body.tags,
|
|
1312
|
-
entityRef: body.entityRef,
|
|
1313
|
-
ttl: body.ttl,
|
|
1314
|
-
sourceReason: body.sourceReason,
|
|
1315
|
-
cwd: body.cwd,
|
|
1316
|
-
projectTag: body.projectTag,
|
|
1317
1316
|
};
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
});
|
|
1317
|
+
const op = getOperation("memory_store");
|
|
1318
|
+
if (!op) {
|
|
1319
|
+
throw new EngramAccessInputError("access-boundary: operation not registered: memory_store");
|
|
1320
|
+
}
|
|
1321
|
+
const output = (await op.run(envelope, {
|
|
1322
|
+
service: this.service,
|
|
1323
|
+
authenticatedPrincipal: this.resolveRequestPrincipal(req),
|
|
1324
|
+
hooks: { enforceWriteQuota: () => this.ensureWriteRateLimitAvailable() },
|
|
1325
|
+
})) as { result: EngramAccessWriteResponse };
|
|
1326
|
+
const response = output.result;
|
|
1329
1327
|
if (this.shouldCountWriteRateLimit(response as { dryRun?: boolean; idempotencyReplay?: boolean })) {
|
|
1330
1328
|
this.recordWriteRateLimitHit();
|
|
1331
1329
|
}
|
|
@@ -1387,7 +1385,19 @@ export class EngramAccessHttpServer {
|
|
|
1387
1385
|
if (req.method === "GET" && memoryMatch) {
|
|
1388
1386
|
const memoryId = decodeURIComponent(memoryMatch[1] ?? "");
|
|
1389
1387
|
const namespace = parsed.searchParams.get("namespace") ?? undefined;
|
|
1390
|
-
|
|
1388
|
+
// Migrated through the access boundary (issue #1525): the registry
|
|
1389
|
+
// entry owns memoryId presence/shape validation (rule 51: reject empty
|
|
1390
|
+
// ids loudly instead of silently passing "" into the service) and the
|
|
1391
|
+
// service dispatch.
|
|
1392
|
+
const op = getOperation("memory_get");
|
|
1393
|
+
if (!op) {
|
|
1394
|
+
throw new EngramAccessInputError("access-boundary: operation not registered: memory_get");
|
|
1395
|
+
}
|
|
1396
|
+
const output = (await op.run(
|
|
1397
|
+
{ memoryId, namespace: namespace ?? null },
|
|
1398
|
+
{ service: this.service, authenticatedPrincipal: this.resolveRequestPrincipal(req) },
|
|
1399
|
+
)) as { result: EngramAccessMemoryResponse };
|
|
1400
|
+
const response = output.result;
|
|
1391
1401
|
this.respondJson(res, response.found ? 200 : 404, response);
|
|
1392
1402
|
return;
|
|
1393
1403
|
}
|
package/src/access-mcp.ts
CHANGED
|
@@ -9,11 +9,15 @@ import {
|
|
|
9
9
|
type CapsuleImportRequest,
|
|
10
10
|
type CapsuleListRequest,
|
|
11
11
|
type DaySummaryRequest,
|
|
12
|
-
type MemoryStoreRequest,
|
|
13
12
|
type SchemaName,
|
|
14
13
|
type SchemaTypeFor,
|
|
15
14
|
type SuggestionSubmitRequest,
|
|
16
15
|
} from "./access-schema.js";
|
|
16
|
+
// Importing access-operations registers the pilot boundary operations
|
|
17
|
+
// (memory_get / memory_search / memory_store) as a side effect; callTool
|
|
18
|
+
// dispatches migrated tools through the registry (issue #1525).
|
|
19
|
+
import { getOperation, type OperationName } from "./access-boundary.js";
|
|
20
|
+
import "./access-operations.js";
|
|
17
21
|
import { readEnvVar } from "./runtime/env.js";
|
|
18
22
|
import type { RecallDisclosure, RecallPlanMode } from "./types.js";
|
|
19
23
|
import { validateBriefingFormat } from "./briefing.js";
|
|
@@ -94,6 +98,19 @@ function withToolAliases(tool: McpTool, emitLegacyTools = true): McpTool[] {
|
|
|
94
98
|
return emitLegacyTools ? [canonicalTool, tool] : [canonicalTool];
|
|
95
99
|
}
|
|
96
100
|
|
|
101
|
+
/**
|
|
102
|
+
* MCP tool name (legacy `engram.*` form, since {@link toLegacyToolName}
|
|
103
|
+
* canonicalizes incoming calls) → boundary operation it dispatches through.
|
|
104
|
+
* A tool appears here once its `access-operations.ts` registration lands and
|
|
105
|
+
* its surface-local validation is deleted. The fitness test
|
|
106
|
+
* (`access-surface-catalog.test.ts`) asserts this map and the catalog agree.
|
|
107
|
+
*/
|
|
108
|
+
const MCP_MIGRATED_OPERATIONS: Readonly<Record<string, OperationName>> = {
|
|
109
|
+
"engram.memory_get": "memory_get",
|
|
110
|
+
"engram.memory_search": "memory_search",
|
|
111
|
+
"engram.memory_store": "memory_store",
|
|
112
|
+
};
|
|
113
|
+
|
|
97
114
|
function resolveChatGptInspectorRecallSessionKey(
|
|
98
115
|
explicitSessionKey: string | undefined,
|
|
99
116
|
authenticatedPrincipal: string | undefined,
|
|
@@ -2278,6 +2295,29 @@ export class EngramMcpServer {
|
|
|
2278
2295
|
}
|
|
2279
2296
|
|
|
2280
2297
|
private async callTool(name: string, args: Record<string, unknown>, effectivePrincipal?: string, mcpSessionId?: string): Promise<unknown> {
|
|
2298
|
+
// Migrated operations dispatch through the access boundary (issue #1525):
|
|
2299
|
+
// one registry entry owns schema validation, normalization (rules
|
|
2300
|
+
// 17/28/36/48/51), and error mapping for every surface. The switch
|
|
2301
|
+
// below still serves the not-yet-migrated tools. memory_store keeps its
|
|
2302
|
+
// parseMcpRequest strict-keys guard (MCP transport contract) before the
|
|
2303
|
+
// boundary re-validates the cleaned body.
|
|
2304
|
+
const migrated = MCP_MIGRATED_OPERATIONS[toLegacyToolName(name)];
|
|
2305
|
+
if (migrated) {
|
|
2306
|
+
const op = getOperation(migrated);
|
|
2307
|
+
if (!op) {
|
|
2308
|
+
throw new EngramAccessInputError(`access-boundary: operation not registered: ${migrated}`);
|
|
2309
|
+
}
|
|
2310
|
+
const envelope =
|
|
2311
|
+
migrated === "memory_store" ? parseMcpRequest("memoryStore", args) : args;
|
|
2312
|
+
// The registry erases In/Out at the Map boundary; the caller knows the
|
|
2313
|
+
// concrete output shape ({ result: <service response> }) so the cast is
|
|
2314
|
+
// the type-erasure seam, not a leap of faith.
|
|
2315
|
+
const output = (await op.run(envelope, {
|
|
2316
|
+
service: this.service,
|
|
2317
|
+
authenticatedPrincipal: effectivePrincipal,
|
|
2318
|
+
})) as { result: unknown };
|
|
2319
|
+
return output.result;
|
|
2320
|
+
}
|
|
2281
2321
|
switch (toLegacyToolName(name)) {
|
|
2282
2322
|
case "engram.recall": {
|
|
2283
2323
|
// Forward `disclosure` only when the caller actually supplied it,
|
|
@@ -2744,12 +2784,6 @@ export class EngramMcpServer {
|
|
|
2744
2784
|
},
|
|
2745
2785
|
effectivePrincipal,
|
|
2746
2786
|
);
|
|
2747
|
-
case "engram.memory_get":
|
|
2748
|
-
return this.service.memoryGet(
|
|
2749
|
-
typeof args.memoryId === "string" ? args.memoryId : "",
|
|
2750
|
-
typeof args.namespace === "string" ? args.namespace : undefined,
|
|
2751
|
-
effectivePrincipal,
|
|
2752
|
-
);
|
|
2753
2787
|
case "engram.memory_timeline": {
|
|
2754
2788
|
const limit = typeof args.limit === "number" && Number.isFinite(args.limit) ? args.limit : 200;
|
|
2755
2789
|
return this.service.memoryTimeline(
|
|
@@ -2759,26 +2793,6 @@ export class EngramMcpServer {
|
|
|
2759
2793
|
effectivePrincipal,
|
|
2760
2794
|
);
|
|
2761
2795
|
}
|
|
2762
|
-
case "engram.memory_store": {
|
|
2763
|
-
const body: MemoryStoreRequest = parseMcpRequest("memoryStore", args);
|
|
2764
|
-
return this.service.memoryStore({
|
|
2765
|
-
schemaVersion: body.schemaVersion,
|
|
2766
|
-
idempotencyKey: body.idempotencyKey,
|
|
2767
|
-
dryRun: body.dryRun,
|
|
2768
|
-
sessionKey: body.sessionKey,
|
|
2769
|
-
authenticatedPrincipal: effectivePrincipal,
|
|
2770
|
-
content: body.content,
|
|
2771
|
-
category: body.category,
|
|
2772
|
-
confidence: body.confidence,
|
|
2773
|
-
namespace: body.namespace,
|
|
2774
|
-
tags: body.tags,
|
|
2775
|
-
entityRef: body.entityRef,
|
|
2776
|
-
ttl: body.ttl,
|
|
2777
|
-
sourceReason: body.sourceReason,
|
|
2778
|
-
cwd: body.cwd,
|
|
2779
|
-
projectTag: body.projectTag,
|
|
2780
|
-
});
|
|
2781
|
-
}
|
|
2782
2796
|
case "engram.suggestion_submit": {
|
|
2783
2797
|
const body: SuggestionSubmitRequest = parseMcpRequest("suggestionSubmit", args);
|
|
2784
2798
|
return this.service.suggestionSubmit({
|
|
@@ -3017,14 +3031,6 @@ export class EngramMcpServer {
|
|
|
3017
3031
|
expectedGuidelineVersion: typeof args.expectedGuidelineVersion === "number" ? args.expectedGuidelineVersion : undefined,
|
|
3018
3032
|
});
|
|
3019
3033
|
// ── Memory search & debug tools ──────────────────────────────────
|
|
3020
|
-
case "engram.memory_search":
|
|
3021
|
-
return this.service.memorySearch({
|
|
3022
|
-
query: typeof args.query === "string" ? args.query : "",
|
|
3023
|
-
namespace: typeof args.namespace === "string" ? args.namespace : undefined,
|
|
3024
|
-
maxResults: typeof args.maxResults === "number" && Number.isFinite(args.maxResults) ? args.maxResults : undefined,
|
|
3025
|
-
collection: typeof args.collection === "string" ? args.collection : undefined,
|
|
3026
|
-
principal: effectivePrincipal,
|
|
3027
|
-
});
|
|
3028
3034
|
case "engram.memory_profile":
|
|
3029
3035
|
return this.service.memoryProfile(
|
|
3030
3036
|
typeof args.namespace === "string" ? args.namespace : undefined,
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pilot operation definitions for the access boundary (issue #1525).
|
|
3
|
+
*
|
|
4
|
+
* Three operations migrate through the registry in this PR — `memory_get`,
|
|
5
|
+
* `memory_search`, and the `memory_store` write op — so the boundary's
|
|
6
|
+
* normalization matrix (rules 17/28/36/48/51) and shared error mapping reach
|
|
7
|
+
* MCP, HTTP, and CLI from one place. Domain-group migrations (memory ops →
|
|
8
|
+
* connectors → namespaces …) land as follow-up PRs that add `defineOperation`
|
|
9
|
+
* calls here and delete the surface-local validation they replace.
|
|
10
|
+
*
|
|
11
|
+
* Importing this module for its side effects registers the pilot operations;
|
|
12
|
+
* surfaces then dispatch via {@link getOperation} from `./access-boundary.js`.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { z } from "zod";
|
|
16
|
+
|
|
17
|
+
import { defineOperation } from "./access-boundary.js";
|
|
18
|
+
import { memoryStoreRequestSchema, type MemoryStoreRequest } from "./access-schema.js";
|
|
19
|
+
import type {
|
|
20
|
+
EngramAccessMemoryResponse,
|
|
21
|
+
EngramAccessWriteResponse,
|
|
22
|
+
} from "./access-service.js";
|
|
23
|
+
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// memory_get — fetch one memory by id
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* `memoryId` is required and non-empty (rule 51: the MCP dispatcher previously
|
|
30
|
+
* fell back to `typeof args.memoryId === "string" ? args.memoryId : ""`,
|
|
31
|
+
* silently passing an empty id into the service). `namespace` is
|
|
32
|
+
* `.nullable().optional()` because MCP clients send `null` (gotcha #2).
|
|
33
|
+
* `namespace` has no `.min(1)` because the pre-boundary handlers forwarded
|
|
34
|
+
* empty/whitespace strings, and `resolveNamespace` treats empty identically
|
|
35
|
+
* to absent (both trim to falsy → default namespace). Rejecting empty would
|
|
36
|
+
* break HTTP callers that send a bare `?namespace=` (Cursor review).
|
|
37
|
+
*/
|
|
38
|
+
const memoryGetSchema = z.object({
|
|
39
|
+
memoryId: z.string().trim().min(1, "memoryId is required").max(512),
|
|
40
|
+
namespace: z.string().trim().max(256).nullable().optional(),
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
export interface MemoryGetInput {
|
|
44
|
+
readonly memoryId: string;
|
|
45
|
+
readonly namespace?: string | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface MemoryGetOutput {
|
|
49
|
+
readonly result: EngramAccessMemoryResponse;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export const memoryGetOperation = defineOperation<MemoryGetInput, MemoryGetOutput>({
|
|
53
|
+
name: "memory_get",
|
|
54
|
+
description: "Fetch one memory by id.",
|
|
55
|
+
schema: memoryGetSchema,
|
|
56
|
+
handler: async (input, ctx) => {
|
|
57
|
+
const result = await ctx.service.memoryGet(
|
|
58
|
+
input.memoryId,
|
|
59
|
+
input.namespace ?? undefined,
|
|
60
|
+
ctx.authenticatedPrincipal,
|
|
61
|
+
);
|
|
62
|
+
return { result };
|
|
63
|
+
},
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// memory_search — semantic search across memories
|
|
68
|
+
// ---------------------------------------------------------------------------
|
|
69
|
+
|
|
70
|
+
const memorySearchSchema = z.object({
|
|
71
|
+
query: z.string().trim().min(1, "query is required").max(2048),
|
|
72
|
+
namespace: z.string().trim().max(256).nullable().optional(),
|
|
73
|
+
// No upper cap: the pre-boundary MCP handler forwarded any finite number to
|
|
74
|
+
// memorySearch, and the QMD/search backends honor large limits. Capping at
|
|
75
|
+
// 100 would reject existing clients that request larger result sets.
|
|
76
|
+
maxResults: z.number().int().min(1).nullable().optional(),
|
|
77
|
+
collection: z.string().trim().min(1).max(256).nullable().optional(),
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
export interface MemorySearchInput {
|
|
81
|
+
readonly query: string;
|
|
82
|
+
readonly namespace?: string | null;
|
|
83
|
+
readonly maxResults?: number | null;
|
|
84
|
+
readonly collection?: string | null;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export interface MemorySearchOutput {
|
|
88
|
+
readonly result: {
|
|
89
|
+
readonly query: string;
|
|
90
|
+
readonly results: ReadonlyArray<{ path: string; score: number; snippet: string }>;
|
|
91
|
+
readonly count: number;
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const memorySearchOperation = defineOperation<MemorySearchInput, MemorySearchOutput>({
|
|
96
|
+
name: "memory_search",
|
|
97
|
+
description: "Search memories across readable namespaces.",
|
|
98
|
+
schema: memorySearchSchema,
|
|
99
|
+
handler: async (input, ctx) => {
|
|
100
|
+
const result = await ctx.service.memorySearch({
|
|
101
|
+
query: input.query,
|
|
102
|
+
namespace: input.namespace ?? undefined,
|
|
103
|
+
maxResults: input.maxResults ?? undefined,
|
|
104
|
+
collection: input.collection ?? undefined,
|
|
105
|
+
principal: ctx.authenticatedPrincipal,
|
|
106
|
+
});
|
|
107
|
+
return { result };
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// memory_store — the pilot WRITE op
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
|
|
115
|
+
export type MemoryStoreInput = MemoryStoreRequest;
|
|
116
|
+
|
|
117
|
+
export interface MemoryStoreOutput {
|
|
118
|
+
readonly result: EngramAccessWriteResponse;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export const memoryStoreOperation = defineOperation<MemoryStoreInput, MemoryStoreOutput>({
|
|
122
|
+
name: "memory_store",
|
|
123
|
+
description: "Store an explicit memory through the access layer.",
|
|
124
|
+
// Reuse the existing schema verbatim — the migration is behavior-preserving;
|
|
125
|
+
// the schema's external contract is NOT changing in this PR (per the issue's
|
|
126
|
+
// pitfall note).
|
|
127
|
+
schema: memoryStoreRequestSchema,
|
|
128
|
+
handler: async (input, ctx) => {
|
|
129
|
+
const result = await ctx.service.memoryStore(
|
|
130
|
+
{
|
|
131
|
+
...input,
|
|
132
|
+
authenticatedPrincipal: ctx.authenticatedPrincipal,
|
|
133
|
+
},
|
|
134
|
+
// Forward transport-level hooks (e.g. HTTP's atomic write-quota gate)
|
|
135
|
+
// so the hook still fires inside the service's idempotent-write lock —
|
|
136
|
+
// never before, never on a replay (#1434 invariant preserved by the
|
|
137
|
+
// boundary migration).
|
|
138
|
+
ctx.hooks,
|
|
139
|
+
);
|
|
140
|
+
return { result };
|
|
141
|
+
},
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
// ---------------------------------------------------------------------------
|
|
145
|
+
// Surface registration map — what each transport calls the pilot ops
|
|
146
|
+
// ---------------------------------------------------------------------------
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* The canonical short names (no `engram.`/`remnic.` prefix) of the operations
|
|
150
|
+
* the boundary owns today. The fitness test treats this set as the migrated
|
|
151
|
+
* set; everything else on a surface is unmigrated and counted by the ratchet.
|
|
152
|
+
*/
|
|
153
|
+
export const REGISTERED_OPERATIONS = [
|
|
154
|
+
memoryGetOperation.spec.name,
|
|
155
|
+
memorySearchOperation.spec.name,
|
|
156
|
+
memoryStoreOperation.spec.name,
|
|
157
|
+
] as const;
|