@remnic/core 9.3.672 → 9.3.674
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-audit.js +2 -2
- package/dist/access-cli.js +25 -23
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.js +12 -12
- package/dist/access-mcp.js +11 -11
- package/dist/access-schema.js +3 -3
- package/dist/access-service.js +9 -9
- package/dist/active-recall.js +3 -1
- package/dist/active-recall.js.map +1 -1
- package/dist/chunk-3BQOQYRB.js +33 -0
- package/dist/chunk-3BQOQYRB.js.map +1 -0
- package/dist/{chunk-KJOYHNS7.js → chunk-7OGJQP7T.js} +4 -4
- package/dist/{chunk-EJYFPRED.js → chunk-B55KFEGS.js} +2 -2
- package/dist/{chunk-BTLNC5YM.js → chunk-GNAMDNGT.js} +5 -13
- package/dist/chunk-GNAMDNGT.js.map +1 -0
- package/dist/{chunk-KQAFEZQX.js → chunk-IPLYGWQF.js} +5 -5
- package/dist/{chunk-MLVMBV2C.js → chunk-IUZWBCJX.js} +8 -40
- package/dist/chunk-IUZWBCJX.js.map +1 -0
- package/dist/{chunk-PYTATYUV.js → chunk-ODWI5XU2.js} +2 -2
- package/dist/{chunk-AYGT6VBC.js → chunk-OG7A6AZX.js} +4 -4
- package/dist/{chunk-4QZ7H6FN.js → chunk-Q6MIDQEL.js} +2 -2
- package/dist/{chunk-UDJLF3BO.js → chunk-QLRYXOAD.js} +2 -2
- package/dist/chunk-QO3AILZN.js +89 -0
- package/dist/chunk-QO3AILZN.js.map +1 -0
- package/dist/{chunk-M3WF2AB6.js → chunk-R37A3BEW.js} +25 -25
- package/dist/{chunk-FP4ISXI3.js → chunk-SDLJ2W7S.js} +6 -6
- package/dist/{chunk-CXKETYZ7.js → chunk-SF45RQDX.js} +3 -3
- package/dist/{chunk-ZUPFMHJA.js → chunk-T2AOOHDA.js} +2 -2
- package/dist/{chunk-7K5Q6COX.js → chunk-TVVEYCNW.js} +4 -4
- package/dist/{chunk-ZQJHKN7J.js → chunk-XVVEKF5I.js} +17 -17
- package/dist/{chunk-7O5CFNN4.js → chunk-ZLINDOBG.js} +4 -4
- package/dist/cli.js +23 -21
- package/dist/config.d.ts +1 -1
- package/dist/config.js +3 -1
- package/dist/connectors/index.d.ts +6 -2
- package/dist/connectors/index.js +6 -2
- package/dist/conversation-index/backend.js +2 -2
- package/dist/emit-legacy-tools.d.ts +61 -0
- package/dist/emit-legacy-tools.js +12 -0
- package/dist/emit-legacy-tools.js.map +1 -0
- package/dist/index.js +45 -43
- package/dist/index.js.map +1 -1
- package/dist/namespaces/migrate.js +5 -5
- package/dist/namespaces/search.js +4 -4
- package/dist/operator-toolkit.js +10 -8
- package/dist/orchestrator.js +14 -14
- package/dist/resume-bundles.js +4 -2
- package/dist/schemas.d.ts +22 -22
- package/dist/search/factory.js +3 -3
- package/dist/search/index.js +3 -3
- package/dist/transfer/autodetect.js +1 -1
- package/dist/transfer/backup.js +1 -1
- package/dist/transfer/capsule-export.js +2 -2
- package/dist/transfer/types.d.ts +12 -12
- package/package.json +2 -2
- package/src/config.test.ts +408 -6
- package/src/config.ts +12 -56
- package/src/connectors/index.ts +2 -15
- package/src/connectors/paths.ts +50 -0
- package/src/emit-legacy-tools.test.ts +297 -0
- package/src/emit-legacy-tools.ts +204 -0
- package/dist/chunk-BTLNC5YM.js.map +0 -1
- package/dist/chunk-MLVMBV2C.js.map +0 -1
- /package/dist/{chunk-KJOYHNS7.js.map → chunk-7OGJQP7T.js.map} +0 -0
- /package/dist/{chunk-EJYFPRED.js.map → chunk-B55KFEGS.js.map} +0 -0
- /package/dist/{chunk-KQAFEZQX.js.map → chunk-IPLYGWQF.js.map} +0 -0
- /package/dist/{chunk-PYTATYUV.js.map → chunk-ODWI5XU2.js.map} +0 -0
- /package/dist/{chunk-AYGT6VBC.js.map → chunk-OG7A6AZX.js.map} +0 -0
- /package/dist/{chunk-4QZ7H6FN.js.map → chunk-Q6MIDQEL.js.map} +0 -0
- /package/dist/{chunk-UDJLF3BO.js.map → chunk-QLRYXOAD.js.map} +0 -0
- /package/dist/{chunk-M3WF2AB6.js.map → chunk-R37A3BEW.js.map} +0 -0
- /package/dist/{chunk-FP4ISXI3.js.map → chunk-SDLJ2W7S.js.map} +0 -0
- /package/dist/{chunk-CXKETYZ7.js.map → chunk-SF45RQDX.js.map} +0 -0
- /package/dist/{chunk-ZUPFMHJA.js.map → chunk-T2AOOHDA.js.map} +0 -0
- /package/dist/{chunk-7K5Q6COX.js.map → chunk-TVVEYCNW.js.map} +0 -0
- /package/dist/{chunk-ZQJHKN7J.js.map → chunk-XVVEKF5I.js.map} +0 -0
- /package/dist/{chunk-7O5CFNN4.js.map → chunk-ZLINDOBG.js.map} +0 -0
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { tmpdir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import test from "node:test";
|
|
6
|
+
|
|
7
|
+
import {
|
|
8
|
+
resolveEmitLegacyTools,
|
|
9
|
+
resolveNamespaceCatalogEnabled,
|
|
10
|
+
} from "./emit-legacy-tools.js";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Run `body` with XDG_CONFIG_HOME pointing at a throwaway dir so the
|
|
14
|
+
* sticky-legacy `hasLegacyConnectorEntries()` check inside the resolvers
|
|
15
|
+
* never reads the real machine state. `withLegacyEntry` seeds one
|
|
16
|
+
* persisted connector file under the standard
|
|
17
|
+
* `$XDG_CONFIG_HOME/engram/.engram-connectors/connectors/` layout.
|
|
18
|
+
*/
|
|
19
|
+
function withIsolatedConnectorsDir<T>(
|
|
20
|
+
withLegacyEntry: boolean,
|
|
21
|
+
body: () => T,
|
|
22
|
+
): T {
|
|
23
|
+
const prev = process.env.XDG_CONFIG_HOME;
|
|
24
|
+
const root = mkdtempSync(path.join(tmpdir(), "emit-legacy-tools-test-"));
|
|
25
|
+
process.env.XDG_CONFIG_HOME = root;
|
|
26
|
+
try {
|
|
27
|
+
if (withLegacyEntry) {
|
|
28
|
+
const dir = path.join(root, "engram", ".engram-connectors", "connectors");
|
|
29
|
+
mkdirSync(dir, { recursive: true });
|
|
30
|
+
writeFileSync(path.join(dir, "codex-cli.json"), "{}\n");
|
|
31
|
+
}
|
|
32
|
+
return body();
|
|
33
|
+
} finally {
|
|
34
|
+
if (prev === undefined) delete process.env.XDG_CONFIG_HOME;
|
|
35
|
+
else process.env.XDG_CONFIG_HOME = prev;
|
|
36
|
+
rmSync(root, { recursive: true, force: true });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ============================================================================
|
|
41
|
+
// resolveEmitLegacyTools
|
|
42
|
+
// ============================================================================
|
|
43
|
+
|
|
44
|
+
test("resolveEmitLegacyTools: fresh install with absent raw falls through to sticky-legacy (false)", () => {
|
|
45
|
+
// No legacy connector evidence on disk → sticky-legacy returns false.
|
|
46
|
+
withIsolatedConnectorsDir(false, () => {
|
|
47
|
+
assert.equal(resolveEmitLegacyTools(false, {}), false);
|
|
48
|
+
// Same shape with legacy connector evidence → sticky-legacy true.
|
|
49
|
+
});
|
|
50
|
+
withIsolatedConnectorsDir(true, () => {
|
|
51
|
+
assert.equal(resolveEmitLegacyTools(false, {}), true);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("resolveEmitLegacyTools: explicit operator opt-out via raw honored", () => {
|
|
56
|
+
// Raw has the key with a real value (false) and configValue agrees.
|
|
57
|
+
withIsolatedConnectorsDir(false, () => {
|
|
58
|
+
assert.equal(
|
|
59
|
+
resolveEmitLegacyTools(false, { emitLegacyTools: false }),
|
|
60
|
+
false,
|
|
61
|
+
"raw operator opt-out via file is honored",
|
|
62
|
+
);
|
|
63
|
+
// Even on a sticky-legacy true install, raw false wins.
|
|
64
|
+
});
|
|
65
|
+
withIsolatedConnectorsDir(true, () => {
|
|
66
|
+
assert.equal(
|
|
67
|
+
resolveEmitLegacyTools(false, { emitLegacyTools: false }),
|
|
68
|
+
false,
|
|
69
|
+
"raw operator opt-out overrides sticky-legacy",
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("resolveEmitLegacyTools: raw null/undefined treated as absent (round-2 review)", () => {
|
|
75
|
+
withIsolatedConnectorsDir(false, () => {
|
|
76
|
+
// Fresh install: raw null + merged null → sticky-legacy false.
|
|
77
|
+
assert.equal(
|
|
78
|
+
resolveEmitLegacyTools(null, { emitLegacyTools: null }),
|
|
79
|
+
false,
|
|
80
|
+
"fresh install with raw null resolves to false via sticky-legacy",
|
|
81
|
+
);
|
|
82
|
+
assert.equal(
|
|
83
|
+
resolveEmitLegacyTools(undefined, { emitLegacyTools: undefined }),
|
|
84
|
+
false,
|
|
85
|
+
"fresh install with raw undefined resolves to false via sticky-legacy",
|
|
86
|
+
);
|
|
87
|
+
assert.equal(
|
|
88
|
+
resolveEmitLegacyTools(undefined, { emitLegacyTools: null }),
|
|
89
|
+
false,
|
|
90
|
+
"mixed null/undefined in raw resolves to false via sticky-legacy",
|
|
91
|
+
);
|
|
92
|
+
});
|
|
93
|
+
withIsolatedConnectorsDir(true, () => {
|
|
94
|
+
// Upgraded install: raw null + sticky evidence → true.
|
|
95
|
+
assert.equal(
|
|
96
|
+
resolveEmitLegacyTools(null, { emitLegacyTools: null }),
|
|
97
|
+
true,
|
|
98
|
+
"upgraded install with raw null resolves to true via sticky-legacy",
|
|
99
|
+
);
|
|
100
|
+
assert.equal(
|
|
101
|
+
resolveEmitLegacyTools(undefined, { emitLegacyTools: undefined }),
|
|
102
|
+
true,
|
|
103
|
+
"upgraded install with raw undefined resolves to true via sticky-legacy",
|
|
104
|
+
);
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("resolveEmitLegacyTools: runtime true overrides schema default (round-2 review)", () => {
|
|
109
|
+
// Schema default is `false`. When raw is missing the key but merged
|
|
110
|
+
// carries `true` (runtime gateway set it), the resolver honors the
|
|
111
|
+
// runtime override instead of dropping it as schema-default
|
|
112
|
+
// materialization.
|
|
113
|
+
withIsolatedConnectorsDir(false, () => {
|
|
114
|
+
assert.equal(
|
|
115
|
+
resolveEmitLegacyTools(true, {}),
|
|
116
|
+
true,
|
|
117
|
+
"merged true with empty raw treated as runtime operator intent",
|
|
118
|
+
);
|
|
119
|
+
// Merged false (the schema default) with empty raw → sticky fallback.
|
|
120
|
+
assert.equal(
|
|
121
|
+
resolveEmitLegacyTools(false, {}),
|
|
122
|
+
false,
|
|
123
|
+
"merged false (schema default) with empty raw falls through to sticky-legacy",
|
|
124
|
+
);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test("resolveEmitLegacyTools: null raw passed directly (not via undefined) is normalized (round-3 review)", () => {
|
|
129
|
+
withIsolatedConnectorsDir(false, () => {
|
|
130
|
+
// Null raw + merged schema-default false → sticky-legacy fallback (no throw).
|
|
131
|
+
assert.equal(
|
|
132
|
+
resolveEmitLegacyTools(false, null),
|
|
133
|
+
false,
|
|
134
|
+
"null raw with emitLegacyTools merged schema default falls through to sticky-legacy",
|
|
135
|
+
);
|
|
136
|
+
// Null raw + emitLegacyTools merged true (runtime intent) → honored.
|
|
137
|
+
assert.equal(
|
|
138
|
+
resolveEmitLegacyTools(true, null),
|
|
139
|
+
true,
|
|
140
|
+
"null raw with emitLegacyTools runtime true honored as operator intent",
|
|
141
|
+
);
|
|
142
|
+
});
|
|
143
|
+
withIsolatedConnectorsDir(true, () => {
|
|
144
|
+
assert.equal(
|
|
145
|
+
resolveEmitLegacyTools(false, null),
|
|
146
|
+
true,
|
|
147
|
+
"null raw with legacy evidence keeps aliases on",
|
|
148
|
+
);
|
|
149
|
+
});
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
test("resolveEmitLegacyTools: runtime-over-file precedence (round-4 review)", () => {
|
|
153
|
+
// File says false, runtime sets true → merged configValue (true) wins.
|
|
154
|
+
withIsolatedConnectorsDir(false, () => {
|
|
155
|
+
assert.equal(
|
|
156
|
+
resolveEmitLegacyTools(true, { emitLegacyTools: false }),
|
|
157
|
+
true,
|
|
158
|
+
"runtime true overrides file false (rawOperatorConfig has the key)",
|
|
159
|
+
);
|
|
160
|
+
// Symmetric: file says true, runtime says false → merged false wins.
|
|
161
|
+
assert.equal(
|
|
162
|
+
resolveEmitLegacyTools(false, { emitLegacyTools: true }),
|
|
163
|
+
false,
|
|
164
|
+
"runtime false overrides file true",
|
|
165
|
+
);
|
|
166
|
+
});
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
test("resolveEmitLegacyTools: raw has key with non-boolean value coerced or throws", () => {
|
|
170
|
+
// Boolean-like strings coerce; garbage throws via coerceBooleanLikeOrThrow.
|
|
171
|
+
assert.equal(
|
|
172
|
+
resolveEmitLegacyTools("true", {}),
|
|
173
|
+
true,
|
|
174
|
+
"string 'true' coerces to true",
|
|
175
|
+
);
|
|
176
|
+
assert.equal(
|
|
177
|
+
resolveEmitLegacyTools("false", {}),
|
|
178
|
+
false,
|
|
179
|
+
"string 'false' coerces to false",
|
|
180
|
+
);
|
|
181
|
+
assert.equal(
|
|
182
|
+
resolveEmitLegacyTools(1, {}),
|
|
183
|
+
true,
|
|
184
|
+
"number 1 coerces to true",
|
|
185
|
+
);
|
|
186
|
+
assert.equal(
|
|
187
|
+
resolveEmitLegacyTools(0, {}),
|
|
188
|
+
false,
|
|
189
|
+
"number 0 coerces to false",
|
|
190
|
+
);
|
|
191
|
+
assert.throws(
|
|
192
|
+
() => resolveEmitLegacyTools("maybe", {}),
|
|
193
|
+
/emitLegacyTools must be a boolean-like value/,
|
|
194
|
+
"unrecognized string throws via coerceBooleanLikeOrThrow",
|
|
195
|
+
);
|
|
196
|
+
assert.throws(
|
|
197
|
+
() => resolveEmitLegacyTools(2, {}),
|
|
198
|
+
/emitLegacyTools must be a boolean-like value/,
|
|
199
|
+
"unrecognized number throws via coerceBooleanLikeOrThrow",
|
|
200
|
+
);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test("resolveEmitLegacyTools: env var overrides (REMNIC_ preferred, ENGRAM_ legacy)", () => {
|
|
204
|
+
const prevRemnic = process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
205
|
+
const prevEngram = process.env.ENGRAM_EMIT_LEGACY_TOOLS;
|
|
206
|
+
try {
|
|
207
|
+
process.env.REMNIC_EMIT_LEGACY_TOOLS = "false";
|
|
208
|
+
assert.equal(resolveEmitLegacyTools(undefined, undefined), false);
|
|
209
|
+
process.env.REMNIC_EMIT_LEGACY_TOOLS = "true";
|
|
210
|
+
assert.equal(resolveEmitLegacyTools(undefined, undefined), true);
|
|
211
|
+
delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
212
|
+
process.env.ENGRAM_EMIT_LEGACY_TOOLS = "false";
|
|
213
|
+
assert.equal(resolveEmitLegacyTools(undefined, undefined), false);
|
|
214
|
+
process.env.ENGRAM_EMIT_LEGACY_TOOLS = "true";
|
|
215
|
+
assert.equal(resolveEmitLegacyTools(undefined, undefined), true);
|
|
216
|
+
} finally {
|
|
217
|
+
if (prevRemnic === undefined) delete process.env.REMNIC_EMIT_LEGACY_TOOLS;
|
|
218
|
+
else process.env.REMNIC_EMIT_LEGACY_TOOLS = prevRemnic;
|
|
219
|
+
if (prevEngram === undefined) delete process.env.ENGRAM_EMIT_LEGACY_TOOLS;
|
|
220
|
+
else process.env.ENGRAM_EMIT_LEGACY_TOOLS = prevEngram;
|
|
221
|
+
}
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// ============================================================================
|
|
225
|
+
// resolveNamespaceCatalogEnabled
|
|
226
|
+
// ============================================================================
|
|
227
|
+
|
|
228
|
+
test("resolveNamespaceCatalogEnabled: default is true when absent", () => {
|
|
229
|
+
assert.equal(
|
|
230
|
+
resolveNamespaceCatalogEnabled(undefined, undefined),
|
|
231
|
+
true,
|
|
232
|
+
"absent configValue + absent raw → schema default true",
|
|
233
|
+
);
|
|
234
|
+
assert.equal(
|
|
235
|
+
resolveNamespaceCatalogEnabled(undefined, {}),
|
|
236
|
+
true,
|
|
237
|
+
"absent configValue + empty raw → schema default true",
|
|
238
|
+
);
|
|
239
|
+
assert.equal(
|
|
240
|
+
resolveNamespaceCatalogEnabled(true, {}),
|
|
241
|
+
true,
|
|
242
|
+
"merged true (schema default) with empty raw → fall through to default",
|
|
243
|
+
);
|
|
244
|
+
});
|
|
245
|
+
|
|
246
|
+
test("resolveNamespaceCatalogEnabled: merged false (different from schema default) honored as runtime intent", () => {
|
|
247
|
+
// Schema default is true. A merged `false` from the runtime API is
|
|
248
|
+
// operator intent to disable → honor it even though the schema default
|
|
249
|
+
// would be `true`.
|
|
250
|
+
assert.equal(
|
|
251
|
+
resolveNamespaceCatalogEnabled(false, {}),
|
|
252
|
+
false,
|
|
253
|
+
"merged false differs from schema default true — runtime operator intent honored",
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
test("resolveNamespaceCatalogEnabled: runtime (merged) value wins over raw file value", () => {
|
|
258
|
+
assert.equal(
|
|
259
|
+
resolveNamespaceCatalogEnabled(true, { namespaceCatalogEnabled: false }),
|
|
260
|
+
true,
|
|
261
|
+
"merged true wins over raw false (runtime-over-file precedence)",
|
|
262
|
+
);
|
|
263
|
+
assert.equal(
|
|
264
|
+
resolveNamespaceCatalogEnabled(false, { namespaceCatalogEnabled: true }),
|
|
265
|
+
false,
|
|
266
|
+
"merged false wins over raw true (runtime-over-file precedence)",
|
|
267
|
+
);
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
test("resolveNamespaceCatalogEnabled: null raw normalized (round-3 review)", () => {
|
|
271
|
+
// Schema default true. Merged false with null raw differs from default →
|
|
272
|
+
// runtime intent honored.
|
|
273
|
+
assert.equal(
|
|
274
|
+
resolveNamespaceCatalogEnabled(false, null),
|
|
275
|
+
false,
|
|
276
|
+
"null raw with merged false (runtime intent) honored",
|
|
277
|
+
);
|
|
278
|
+
// Merged true (schema default) with null raw → fall through to default.
|
|
279
|
+
assert.equal(
|
|
280
|
+
resolveNamespaceCatalogEnabled(true, null),
|
|
281
|
+
true,
|
|
282
|
+
"null raw with merged true (schema default) preserved",
|
|
283
|
+
);
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
test("resolveNamespaceCatalogEnabled: malformed values throw (rule #51)", () => {
|
|
287
|
+
assert.throws(
|
|
288
|
+
() => resolveNamespaceCatalogEnabled("maybe", {}),
|
|
289
|
+
/namespaceCatalogEnabled must be a boolean-like value/,
|
|
290
|
+
"unrecognized string throws via coerceBooleanLikeOrThrow",
|
|
291
|
+
);
|
|
292
|
+
assert.throws(
|
|
293
|
+
() => resolveNamespaceCatalogEnabled(2, {}),
|
|
294
|
+
/namespaceCatalogEnabled must be a boolean-like value/,
|
|
295
|
+
"unrecognized number throws via coerceBooleanLikeOrThrow",
|
|
296
|
+
);
|
|
297
|
+
});
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* emit-legacy-tools — resolution cluster for the `emitLegacyTools` and
|
|
3
|
+
* `namespaceCatalogEnabled` config gates (issues #1427, #1499, #1550).
|
|
4
|
+
*
|
|
5
|
+
* Extracted from packages/remnic-core/src/config.ts (PR #1593, round 6)
|
|
6
|
+
* so the raw-vs-effective split, runtime-override precedence, and
|
|
7
|
+
* fileConfig-null normalization logic specific to these two gates
|
|
8
|
+
* stays out of the god-file `config.ts`. The export surface is exactly
|
|
9
|
+
* the two resolvers plus the `coerceBooleanLikeOrThrow` helper that
|
|
10
|
+
* wraps the local `coerceBooleanLike` — every other config gate in
|
|
11
|
+
* `parseConfig` keeps using its own inline coercion or the
|
|
12
|
+
* `coerceBooleanLike` already exported from
|
|
13
|
+
* `packages/remnic-core/src/connectors/coerce.ts`.
|
|
14
|
+
*
|
|
15
|
+
* Precedence (PR #1593 rounds 1-4, plus the null/loader hardening from
|
|
16
|
+
* rounds 3-5):
|
|
17
|
+
*
|
|
18
|
+
* 1. `configValue` (the first arg) is the MERGED config
|
|
19
|
+
* (runtime-over-file via the
|
|
20
|
+
* `{...fileConfig, ...api.pluginConfig}` spread in src/index.ts).
|
|
21
|
+
* If it's a real boolean, it represents what the operator wants,
|
|
22
|
+
* so honor it. We only fall through when it's the schema-default
|
|
23
|
+
* materialization with no operator authoring in raw.
|
|
24
|
+
* 2. `rawOperatorConfig` (the second arg) is the operator-supplied
|
|
25
|
+
* config block BEFORE the OpenClaw manifest layer applies schema
|
|
26
|
+
* defaults — i.e. the file-backed `loadPluginConfigFromFile` output.
|
|
27
|
+
* When raw has the key with a non-null/undefined value, the file
|
|
28
|
+
* layer authored it. The merged `configValue` reflects the full
|
|
29
|
+
* operator intent (file + runtime), so `configValue` is still
|
|
30
|
+
* authoritative. raw presence is used only as the "operator
|
|
31
|
+
* authored this key" signal — if raw is missing AND configValue
|
|
32
|
+
* equals the schema default, only the schema layer materialized the
|
|
33
|
+
* key (no operator intent anywhere) and we fall through to env /
|
|
34
|
+
* sticky-legacy.
|
|
35
|
+
* 3. Legacy callers (raw undefined): trust configValue as before to
|
|
36
|
+
* preserve the 121+ existing call sites that pass only one arg.
|
|
37
|
+
*
|
|
38
|
+
* Defensive normalization (PR #1593 round 3): JSON null on disk for the
|
|
39
|
+
* operator config block surfaces as `null` in rawOperatorConfig. Both
|
|
40
|
+
* resolvers normalize `null` to `{}` so the `"key" in rawOperatorConfig`
|
|
41
|
+
* check never throws. The file loader
|
|
42
|
+
* (`loadPluginConfigFromFile`) also normalizes null to undefined
|
|
43
|
+
* before reaching here.
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
import { readEnvVar } from "./runtime/env.js";
|
|
47
|
+
import { hasLegacyConnectorEntries } from "./connectors/paths.js";
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Coerce common string/number representations of a boolean to a real
|
|
51
|
+
* boolean. Returns `undefined` when the value cannot be interpreted, so
|
|
52
|
+
* callers can fail fast via `coerceBooleanLikeOrThrow`. Guards against
|
|
53
|
+
* the "string `false` is truthy" footgun (CLAUDE.md gotcha #36) when
|
|
54
|
+
* config values arrive from CLI/env/JSON sources where booleans are
|
|
55
|
+
* sometimes string-typed.
|
|
56
|
+
*
|
|
57
|
+
* Local copy — the canonical implementation lives in
|
|
58
|
+
* `packages/remnic-core/src/connectors/coerce.ts` but is private to that
|
|
59
|
+
* module's `coerceBool` export; duplicating here keeps the god-file
|
|
60
|
+
* contract clean (no cross-module pull) for the two gates that need
|
|
61
|
+
* fail-fast rejection.
|
|
62
|
+
*/
|
|
63
|
+
function coerceBooleanLike(value: unknown): boolean | undefined {
|
|
64
|
+
if (typeof value === "boolean") return value;
|
|
65
|
+
if (typeof value === "number") {
|
|
66
|
+
if (value === 1) return true;
|
|
67
|
+
if (value === 0) return false;
|
|
68
|
+
return undefined;
|
|
69
|
+
}
|
|
70
|
+
if (typeof value === "string") {
|
|
71
|
+
const normalized = value.trim().toLowerCase();
|
|
72
|
+
if (
|
|
73
|
+
normalized === "true" ||
|
|
74
|
+
normalized === "1" ||
|
|
75
|
+
normalized === "yes" ||
|
|
76
|
+
normalized === "on"
|
|
77
|
+
) {
|
|
78
|
+
return true;
|
|
79
|
+
}
|
|
80
|
+
if (
|
|
81
|
+
normalized === "false" ||
|
|
82
|
+
normalized === "0" ||
|
|
83
|
+
normalized === "no" ||
|
|
84
|
+
normalized === "off"
|
|
85
|
+
) {
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Coerce a present boolean-like gate value or fail fast. A PRESENT but
|
|
94
|
+
* unrecognized value ("fales", 2) is REJECTED rather than silently
|
|
95
|
+
* defaulting (CLAUDE.md rule #51) — shared by both resolvers below so
|
|
96
|
+
* the rejection behavior cannot drift between them.
|
|
97
|
+
*/
|
|
98
|
+
function coerceBooleanLikeOrThrow(label: string, value: unknown): boolean {
|
|
99
|
+
const coerced = coerceBooleanLike(value);
|
|
100
|
+
if (coerced === undefined) {
|
|
101
|
+
throw new Error(
|
|
102
|
+
`${label} must be a boolean-like value (true/false/1/0/yes/no/on/off); got ${JSON.stringify(value)}`,
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return coerced;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Resolve the `emitLegacyTools` opt-out (issue #1427, defaults revised in
|
|
110
|
+
* #1550). Precedence: operator-set raw config, then merged (post-defaults)
|
|
111
|
+
* config, then the REMNIC_/ENGRAM_ env var, then a sticky-legacy default —
|
|
112
|
+
* `true` only when existing legacy connector entries are present on disk
|
|
113
|
+
* (`hasLegacyConnectorEntries`), `false` for fresh installs.
|
|
114
|
+
*/
|
|
115
|
+
export function resolveEmitLegacyTools(
|
|
116
|
+
configValue: unknown,
|
|
117
|
+
rawOperatorConfig: Record<string, unknown> | undefined | null,
|
|
118
|
+
runtimeSet?: ReadonlySet<string>,
|
|
119
|
+
): boolean {
|
|
120
|
+
// Defensive null normalization — see file header for the rationale.
|
|
121
|
+
if (rawOperatorConfig === null) rawOperatorConfig = {};
|
|
122
|
+
// Schema default for `emitLegacyTools` is `false` (issue #1550).
|
|
123
|
+
const SCHEMA_DEFAULT = false;
|
|
124
|
+
// Round 8: `runtimeAuthored` is now informational only — the resolver
|
|
125
|
+
// does not consult it (see the comment on the check below). Kept as a
|
|
126
|
+
// local so the signature-bound `runtimeSet` parameter remains useful
|
|
127
|
+
// for future refactors and tests can read this state if needed.
|
|
128
|
+
const runtimeAuthored = runtimeSet?.has("emitLegacyTools") ?? false;
|
|
129
|
+
if (rawOperatorConfig !== undefined) {
|
|
130
|
+
if (configValue !== undefined && configValue !== null) {
|
|
131
|
+
const rawValue = (rawOperatorConfig as Record<string, unknown>).emitLegacyTools;
|
|
132
|
+
const rawAuthored =
|
|
133
|
+
"emitLegacyTools" in rawOperatorConfig &&
|
|
134
|
+
rawValue !== null &&
|
|
135
|
+
rawValue !== undefined;
|
|
136
|
+
// Round 8 (PR #1593): revert the runtimeAuthored gate. OpenClaw's
|
|
137
|
+
// `applyDefaults: true` materialization means api.pluginConfig keys
|
|
138
|
+
// can't reliably signal operator authorship; the schema-default
|
|
139
|
+
// comparison alone is the right signal (chatgpt-codex-connector P1
|
|
140
|
+
// on src/index.ts:1348, round 8).
|
|
141
|
+
if (rawAuthored || configValue !== SCHEMA_DEFAULT) {
|
|
142
|
+
return coerceBooleanLikeOrThrow("emitLegacyTools", configValue);
|
|
143
|
+
}
|
|
144
|
+
} else if ("emitLegacyTools" in rawOperatorConfig) {
|
|
145
|
+
const rawValue = (rawOperatorConfig as Record<string, unknown>).emitLegacyTools;
|
|
146
|
+
if (rawValue !== null && rawValue !== undefined) {
|
|
147
|
+
return coerceBooleanLikeOrThrow("emitLegacyTools", rawValue);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
} else if (configValue !== undefined && configValue !== null) {
|
|
151
|
+
// Legacy caller (no rawOperatorConfig) — trust the merged value.
|
|
152
|
+
return coerceBooleanLikeOrThrow("emitLegacyTools", configValue);
|
|
153
|
+
}
|
|
154
|
+
const envRaw =
|
|
155
|
+
readEnvVar("REMNIC_EMIT_LEGACY_TOOLS") ?? readEnvVar("ENGRAM_EMIT_LEGACY_TOOLS");
|
|
156
|
+
if (envRaw !== undefined) {
|
|
157
|
+
return coerceBooleanLikeOrThrow("REMNIC_EMIT_LEGACY_TOOLS", envRaw);
|
|
158
|
+
}
|
|
159
|
+
return hasLegacyConnectorEntries();
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Resolve the `namespaceCatalogEnabled` opt-out (issue #1499). Same
|
|
164
|
+
* raw-vs-effective split as `resolveEmitLegacyTools` — schema-default
|
|
165
|
+
* hardening at the helper level so adding a `false` default later cannot
|
|
166
|
+
* silently flip behavior on upgraded installs (#1550 class hardening).
|
|
167
|
+
*/
|
|
168
|
+
export function resolveNamespaceCatalogEnabled(
|
|
169
|
+
configValue: unknown,
|
|
170
|
+
rawOperatorConfig: Record<string, unknown> | undefined | null,
|
|
171
|
+
runtimeSet?: ReadonlySet<string>,
|
|
172
|
+
): boolean {
|
|
173
|
+
// Defensive null normalization — see file header for the rationale.
|
|
174
|
+
if (rawOperatorConfig === null) rawOperatorConfig = {};
|
|
175
|
+
// Schema default is `true` (the catalog is opt-out).
|
|
176
|
+
const SCHEMA_DEFAULT = true;
|
|
177
|
+
// Round 8: same as the emit variant — `runtimeAuthored` is
|
|
178
|
+
// informational only, the resolver does not consult it.
|
|
179
|
+
const runtimeAuthored = runtimeSet?.has("namespaceCatalogEnabled") ?? false;
|
|
180
|
+
if (rawOperatorConfig !== undefined) {
|
|
181
|
+
if (configValue !== undefined && configValue !== null) {
|
|
182
|
+
const rawValue = (rawOperatorConfig as Record<string, unknown>)
|
|
183
|
+
.namespaceCatalogEnabled;
|
|
184
|
+
const rawAuthored =
|
|
185
|
+
"namespaceCatalogEnabled" in rawOperatorConfig &&
|
|
186
|
+
rawValue !== null &&
|
|
187
|
+
rawValue !== undefined;
|
|
188
|
+
// Round 8: same rollback as resolveEmitLegacyTools.
|
|
189
|
+
if (rawAuthored || configValue !== SCHEMA_DEFAULT) {
|
|
190
|
+
return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", configValue);
|
|
191
|
+
}
|
|
192
|
+
} else if ("namespaceCatalogEnabled" in rawOperatorConfig) {
|
|
193
|
+
const rawValue = (rawOperatorConfig as Record<string, unknown>)
|
|
194
|
+
.namespaceCatalogEnabled;
|
|
195
|
+
if (rawValue !== null && rawValue !== undefined) {
|
|
196
|
+
return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", rawValue);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
} else if (configValue !== undefined && configValue !== null) {
|
|
200
|
+
// Legacy caller (no rawOperatorConfig) — trust the merged value.
|
|
201
|
+
return coerceBooleanLikeOrThrow("namespaceCatalogEnabled", configValue);
|
|
202
|
+
}
|
|
203
|
+
return SCHEMA_DEFAULT;
|
|
204
|
+
}
|