@remnic/core 9.3.680 → 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-cli.js +115 -101
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +48 -46
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +44 -42
- 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 +6 -6
- 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 +40 -40
- 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/{capsule-crypto-YO5QJ6L3.js → capsule-crypto-7FJQINUR.js} +2 -2
- package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
- 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-BXLOS5AJ.js → chunk-2NLLXCJG.js} +2 -2
- 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-DL6H3D7S.js → chunk-ARV3AUOM.js} +2 -2
- 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-5TEYIXMP.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-OBM7EVFU.js → chunk-H4BDNIKQ.js} +53 -53
- 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-VDX2J7OX.js → chunk-KQAFEZQX.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-W67ZZDHO.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-DWQPM67F.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-JI6HWBYL.js → chunk-UDJLF3BO.js} +2 -2
- 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-DQEMWVMT.js → chunk-X7Y7WX73.js} +1 -1
- 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 +75 -73
- 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/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 +4 -4
- package/dist/transfer/capsule-export.js +4 -4
- package/dist/transfer/capsule-import.js +3 -3
- package/dist/transfer/import-sqlite.js +2 -2
- package/dist/transfer/types.d.ts +32 -32
- package/dist/verified-recall.js +8 -8
- package/package.json +2 -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/dist/chunk-5TEYIXMP.js.map +0 -1
- package/dist/chunk-DWQPM67F.js.map +0 -1
- /package/dist/{capsule-crypto-YO5QJ6L3.js.map → access-boundary.js.map} +0 -0
- /package/dist/{chunk-BXLOS5AJ.js.map → chunk-2NLLXCJG.js.map} +0 -0
- /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-DL6H3D7S.js.map → chunk-ARV3AUOM.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-OBM7EVFU.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-VDX2J7OX.js.map → chunk-KQAFEZQX.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-W67ZZDHO.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-JI6HWBYL.js.map → chunk-UDJLF3BO.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-DQEMWVMT.js.map → chunk-X7Y7WX73.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,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the access boundary (issue #1525).
|
|
3
|
+
*
|
|
4
|
+
* Covers the normalization matrix the boundary owns (rules 17/28/36/48/51),
|
|
5
|
+
* the registry's validate-then-invoke contract, and the rule-51 "list valid
|
|
6
|
+
* options" error format. The surface-coverage fitness test lives in
|
|
7
|
+
* `access-surface-catalog.test.ts`.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import assert from "node:assert/strict";
|
|
11
|
+
import test from "node:test";
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
__resetRegistryForTest,
|
|
15
|
+
coerceBooleanLike,
|
|
16
|
+
coercePositiveInteger,
|
|
17
|
+
defineOperation,
|
|
18
|
+
formatZodIssues,
|
|
19
|
+
getOperation,
|
|
20
|
+
listRegisteredOperations,
|
|
21
|
+
normalizeOptionalPath,
|
|
22
|
+
type OperationContext,
|
|
23
|
+
} from "./access-boundary.js";
|
|
24
|
+
import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
|
|
25
|
+
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
// Test fixtures
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
|
|
30
|
+
function makeMockService(handlers: Partial<EngramAccessService> = {}): EngramAccessService {
|
|
31
|
+
return { ...handlers } as unknown as EngramAccessService;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const ctx = (service: EngramAccessService, principal?: string): OperationContext => ({
|
|
35
|
+
service,
|
|
36
|
+
authenticatedPrincipal: principal,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
// Reset the pilot registrations between files so defineOperation's "already
|
|
40
|
+
// registered" guard doesn't fire when the real `access-operations.ts` is
|
|
41
|
+
// imported by sibling test files in the same process.
|
|
42
|
+
test.afterEach(() => {
|
|
43
|
+
__resetRegistryForTest();
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// ---------------------------------------------------------------------------
|
|
47
|
+
// coerceBooleanLike — rule 36 (string "false" is truthy)
|
|
48
|
+
// ---------------------------------------------------------------------------
|
|
49
|
+
|
|
50
|
+
test("coerceBooleanLike: accepts real booleans", () => {
|
|
51
|
+
assert.equal(coerceBooleanLike(true), true);
|
|
52
|
+
assert.equal(coerceBooleanLike(false), false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("coerceBooleanLike: treats absence as undefined (caller keeps its default)", () => {
|
|
56
|
+
assert.equal(coerceBooleanLike(undefined), undefined);
|
|
57
|
+
assert.equal(coerceBooleanLike(null), undefined);
|
|
58
|
+
assert.equal(coerceBooleanLike(""), undefined);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("coerceBooleanLike: coerces every documented spelling, case-insensitive", () => {
|
|
62
|
+
for (const truthy of ["true", "TRUE", "1", "yes", "YES", "on"]) {
|
|
63
|
+
assert.equal(coerceBooleanLike(truthy), true, `${truthy} should be true`);
|
|
64
|
+
}
|
|
65
|
+
for (const falsy of ["false", "False", "0", "no", "No", "off"]) {
|
|
66
|
+
assert.equal(coerceBooleanLike(falsy), false, `${falsy} should be false`);
|
|
67
|
+
}
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("coerceBooleanLike: rejects values Boolean() would silently mis-coerce", () => {
|
|
71
|
+
// These are the exact cases rule 36 exists for — `Boolean("false") === true`
|
|
72
|
+
// would let `--installExtension=false` silently enable the flag.
|
|
73
|
+
assert.throws(() => coerceBooleanLike("not-a-bool"), EngramAccessInputError);
|
|
74
|
+
assert.throws(() => coerceBooleanLike(2), EngramAccessInputError);
|
|
75
|
+
assert.throws(() => coerceBooleanLike({}), EngramAccessInputError);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------
|
|
79
|
+
// coercePositiveInteger — rule 28 (numeric strings at the edge)
|
|
80
|
+
// ---------------------------------------------------------------------------
|
|
81
|
+
|
|
82
|
+
test("coercePositiveInteger: absence → undefined", () => {
|
|
83
|
+
assert.equal(coercePositiveInteger(undefined, "limit"), undefined);
|
|
84
|
+
assert.equal(coercePositiveInteger(null, "limit"), undefined);
|
|
85
|
+
assert.equal(coercePositiveInteger("", "limit"), undefined);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
test("coercePositiveInteger: coerces numeric strings the service would later reject", () => {
|
|
89
|
+
assert.equal(coercePositiveInteger("5", "limit"), 5);
|
|
90
|
+
assert.equal(coercePositiveInteger("5555", "port"), 5555);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("coercePositiveInteger: accepts numbers that pass the integer/positive guard", () => {
|
|
94
|
+
assert.equal(coercePositiveInteger(7, "limit"), 7);
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test("coercePositiveInteger: rejects zero, negatives, non-integers, booleans", () => {
|
|
98
|
+
// `Number(true) === 1` would silently pass without this guard.
|
|
99
|
+
for (const bad of [0, -1, 1.5, "0", "-3", "1.5", true, false, NaN]) {
|
|
100
|
+
assert.throws(() => coercePositiveInteger(bad, "limit"), EngramAccessInputError);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// normalizeOptionalPath — rule 17 (~ expansion via the shared helper)
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
|
|
108
|
+
test("normalizeOptionalPath: absence → undefined", () => {
|
|
109
|
+
assert.equal(normalizeOptionalPath(undefined), undefined);
|
|
110
|
+
assert.equal(normalizeOptionalPath(null), undefined);
|
|
111
|
+
assert.equal(normalizeOptionalPath(""), undefined);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
test("normalizeOptionalPath: expands ~ via expandTildePath, never ad-hoc regex", () => {
|
|
115
|
+
const original = process.env.HOME;
|
|
116
|
+
const fakeHome = "/tmp/remnic-boundary-home";
|
|
117
|
+
process.env.HOME = fakeHome;
|
|
118
|
+
try {
|
|
119
|
+
assert.equal(normalizeOptionalPath("~/memory"), `${fakeHome}/memory`);
|
|
120
|
+
} finally {
|
|
121
|
+
process.env.HOME = original;
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
test("normalizeOptionalPath: rejects non-string shapes loudly", () => {
|
|
126
|
+
assert.throws(() => normalizeOptionalPath(42), EngramAccessInputError);
|
|
127
|
+
assert.throws(() => normalizeOptionalPath({ path: "x" }), EngramAccessInputError);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
// ---------------------------------------------------------------------------
|
|
131
|
+
// formatZodIssues — rule 51 (list valid options, never silently default)
|
|
132
|
+
// ---------------------------------------------------------------------------
|
|
133
|
+
|
|
134
|
+
test("formatZodIssues: names the offending field and lists enum options", async () => {
|
|
135
|
+
const { z } = await import("zod");
|
|
136
|
+
const schema = z.object({ mode: z.enum(["auto", "no_recall"]) });
|
|
137
|
+
const result = schema.safeParse({ mode: "bogus" });
|
|
138
|
+
assert.ok(!result.success);
|
|
139
|
+
const message = formatZodIssues(result.error);
|
|
140
|
+
assert.match(message, /mode/);
|
|
141
|
+
assert.match(message, /auto.*no_recall/);
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
test("formatZodIssues: includes the (root) path for top-level errors", async () => {
|
|
145
|
+
const { z } = await import("zod");
|
|
146
|
+
const schema = z.object({}).strict();
|
|
147
|
+
const result = schema.safeParse({ rogue: 1 });
|
|
148
|
+
assert.ok(!result.success);
|
|
149
|
+
const message = formatZodIssues(result.error);
|
|
150
|
+
assert.ok(message.length > 0);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
// ---------------------------------------------------------------------------
|
|
154
|
+
// defineOperation / getOperation — registry contract
|
|
155
|
+
// ---------------------------------------------------------------------------
|
|
156
|
+
|
|
157
|
+
test("defineOperation: parses then invokes the handler with the typed input", async () => {
|
|
158
|
+
let observed: { x?: number } | undefined;
|
|
159
|
+
const op = defineOperation<{ x?: number }, { doubled: number }>({
|
|
160
|
+
name: "memory_get",
|
|
161
|
+
description: "test",
|
|
162
|
+
schema: (await import("zod")).object({ x: (await import("zod")).number().optional() }),
|
|
163
|
+
handler: async (input) => {
|
|
164
|
+
observed = input;
|
|
165
|
+
return { doubled: (input.x ?? 0) * 2 };
|
|
166
|
+
},
|
|
167
|
+
});
|
|
168
|
+
const out = await op.run({ x: 21 }, ctx(makeMockService()));
|
|
169
|
+
assert.equal(out.doubled, 42);
|
|
170
|
+
assert.deepEqual(observed, { x: 21 });
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
test("defineOperation: rejects invalid input with EngramAccessInputError BEFORE the handler runs", async () => {
|
|
174
|
+
let ran = false;
|
|
175
|
+
const op = defineOperation<{ x: number }, void>({
|
|
176
|
+
name: "memory_get",
|
|
177
|
+
description: "test",
|
|
178
|
+
schema: (await import("zod")).object({ x: (await import("zod")).number() }),
|
|
179
|
+
handler: async () => {
|
|
180
|
+
ran = true;
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
await assert.rejects(
|
|
184
|
+
() => op.run({ x: "not-a-number" }, ctx(makeMockService())),
|
|
185
|
+
(err: unknown) => err instanceof EngramAccessInputError,
|
|
186
|
+
);
|
|
187
|
+
assert.equal(ran, false, "handler must not run on validation failure");
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
test("defineOperation: duplicate registration is a programming error, not an input fault", async () => {
|
|
191
|
+
const { z } = await import("zod");
|
|
192
|
+
const spec = {
|
|
193
|
+
name: "memory_get" as const,
|
|
194
|
+
description: "first",
|
|
195
|
+
schema: z.object({}),
|
|
196
|
+
handler: async () => undefined,
|
|
197
|
+
};
|
|
198
|
+
defineOperation(spec);
|
|
199
|
+
assert.throws(() => defineOperation(spec), /already registered/);
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("getOperation / listRegisteredOperations: round-trip a registration", async () => {
|
|
203
|
+
const { z } = await import("zod");
|
|
204
|
+
defineOperation({
|
|
205
|
+
name: "memory_search",
|
|
206
|
+
description: "test",
|
|
207
|
+
schema: z.object({}),
|
|
208
|
+
handler: async () => undefined,
|
|
209
|
+
});
|
|
210
|
+
assert.ok(getOperation("memory_search"));
|
|
211
|
+
assert.ok(listRegisteredOperations().includes("memory_search"));
|
|
212
|
+
});
|
|
@@ -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
|
}
|