@remnic/core 9.3.716 → 9.3.717
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 +2 -2
- package/dist/access-boundary.js +4 -3
- package/dist/access-cli.js +12 -11
- package/dist/access-cli.js.map +1 -1
- package/dist/access-http.d.ts +2 -2
- package/dist/access-http.js +8 -7
- package/dist/access-mcp.d.ts +2 -2
- package/dist/access-mcp.js +7 -6
- package/dist/access-operations-batch.js +5 -4
- package/dist/access-operations.d.ts +2 -2
- package/dist/access-operations.js +6 -5
- package/dist/{access-service-BW8cm_uU.d.ts → access-service-d4Tk-Dlr.d.ts} +2 -2
- package/dist/access-service.d.ts +2 -2
- package/dist/access-service.js +3 -2
- package/dist/access-surface-catalog.d.ts +2 -2
- package/dist/bootstrap.d.ts +1 -1
- package/dist/{chunk-MECU6WSH.js → chunk-2B43BYYZ.js} +6 -6
- package/dist/chunk-2KF4HDG2.js +24 -0
- package/dist/chunk-2KF4HDG2.js.map +1 -0
- package/dist/{chunk-HHLWSWWF.js → chunk-BF2426TN.js} +2 -2
- package/dist/{chunk-RBHANGVX.js → chunk-BZAK3RBN.js} +2 -2
- package/dist/{chunk-SQ4LU27I.js → chunk-CTUWGJRI.js} +36 -59
- package/dist/chunk-CTUWGJRI.js.map +1 -0
- package/dist/{chunk-6QUPUTT7.js → chunk-DZYARLJ7.js} +5 -9
- package/dist/{chunk-6QUPUTT7.js.map → chunk-DZYARLJ7.js.map} +1 -1
- package/dist/{chunk-CTAHKIG6.js → chunk-EM22PCQR.js} +7 -7
- package/dist/{chunk-SHRRWOVY.js → chunk-J6BH2OBA.js} +64 -21
- package/dist/chunk-J6BH2OBA.js.map +1 -0
- package/dist/{chunk-GM3BMWKR.js → chunk-KHQ433G5.js} +2 -2
- package/dist/{chunk-SYDHFFAW.js → chunk-OUDAU3T5.js} +4 -4
- package/dist/{chunk-O62P2LFL.js → chunk-VZQXJNBD.js} +2 -2
- package/dist/{chunk-BNOAB5HH.js → chunk-YAINYEM3.js} +57 -48
- package/dist/chunk-YAINYEM3.js.map +1 -0
- package/dist/{chunk-3VUPWDSE.js → chunk-ZAVTEQFM.js} +4 -4
- package/dist/{chunk-IHMT6XTY.js → chunk-ZXLSNG3T.js} +3 -3
- package/dist/{cli-Dpivy32W.d.ts → cli-insNLYs6.d.ts} +2 -2
- package/dist/cli.d.ts +3 -3
- package/dist/cli.js +12 -11
- package/dist/explicit-capture.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +22 -21
- package/dist/index.js.map +1 -1
- package/dist/mcp-memory-inspector-app.d.ts +2 -2
- package/dist/operator-toolkit.js +5 -3
- package/dist/orchestration/maintenance.js +5 -2
- package/dist/{orchestrator-vygo2YIM.d.ts → orchestrator-XPKhY2OV.d.ts} +2 -2
- package/dist/orchestrator.d.ts +1 -1
- package/dist/orchestrator.js +6 -5
- package/package.json +2 -2
- package/src/access-service.ts +56 -41
- package/src/maintenance/namespace-planner.ts +3 -10
- package/src/orchestrator.ts +35 -61
- package/src/scopes/scope-plan.test.ts +168 -1
- package/src/scopes/scope-plan.ts +163 -0
- package/dist/chunk-BNOAB5HH.js.map +0 -1
- package/dist/chunk-SHRRWOVY.js.map +0 -1
- package/dist/chunk-SQ4LU27I.js.map +0 -1
- /package/dist/{chunk-MECU6WSH.js.map → chunk-2B43BYYZ.js.map} +0 -0
- /package/dist/{chunk-HHLWSWWF.js.map → chunk-BF2426TN.js.map} +0 -0
- /package/dist/{chunk-RBHANGVX.js.map → chunk-BZAK3RBN.js.map} +0 -0
- /package/dist/{chunk-CTAHKIG6.js.map → chunk-EM22PCQR.js.map} +0 -0
- /package/dist/{chunk-GM3BMWKR.js.map → chunk-KHQ433G5.js.map} +0 -0
- /package/dist/{chunk-SYDHFFAW.js.map → chunk-OUDAU3T5.js.map} +0 -0
- /package/dist/{chunk-O62P2LFL.js.map → chunk-VZQXJNBD.js.map} +0 -0
- /package/dist/{chunk-3VUPWDSE.js.map → chunk-ZAVTEQFM.js.map} +0 -0
- /package/dist/{chunk-IHMT6XTY.js.map → chunk-ZXLSNG3T.js.map} +0 -0
package/src/orchestrator.ts
CHANGED
|
@@ -342,7 +342,11 @@ import {
|
|
|
342
342
|
recallNamespacesForPrincipal,
|
|
343
343
|
resolvePrincipal,
|
|
344
344
|
} from "./namespaces/principal.js";
|
|
345
|
-
import {
|
|
345
|
+
import {
|
|
346
|
+
getConfiguredNamespaces,
|
|
347
|
+
resolveNamespaceFromStorageDir,
|
|
348
|
+
resolveScopePlan,
|
|
349
|
+
} from "./scopes/scope-plan.js";
|
|
346
350
|
import {
|
|
347
351
|
expandScopeProfileReadNamespaces,
|
|
348
352
|
resolveScopeProfilePlan,
|
|
@@ -2317,18 +2321,11 @@ export class Orchestrator {
|
|
|
2317
2321
|
return this.storageRouter.storageFor(ns);
|
|
2318
2322
|
}
|
|
2319
2323
|
|
|
2320
|
-
private
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
this.config.sharedNamespace,
|
|
2326
|
-
...this.config.namespacePolicies.map((policy) => policy.name),
|
|
2327
|
-
]
|
|
2328
|
-
.map((value) => value.trim())
|
|
2329
|
-
.filter(Boolean),
|
|
2330
|
-
),
|
|
2331
|
-
);
|
|
2324
|
+
private configuredNamespaceList(): string[] {
|
|
2325
|
+
// #1521: delegates to the scope-module resolver. The inline derivation is
|
|
2326
|
+
// retired so the adHocNamespaceResolutions ratchet no longer counts this
|
|
2327
|
+
// site.
|
|
2328
|
+
return getConfiguredNamespaces(this.config);
|
|
2332
2329
|
}
|
|
2333
2330
|
|
|
2334
2331
|
private rememberNamespaceStorageDirHint(namespace: string, storageDir?: string): void {
|
|
@@ -2456,7 +2453,7 @@ export class Orchestrator {
|
|
|
2456
2453
|
}
|
|
2457
2454
|
|
|
2458
2455
|
const configured = new Set(
|
|
2459
|
-
this.
|
|
2456
|
+
this.configuredNamespaceList().map((namespace) => normalizeNamespaceIdentity(namespace)),
|
|
2460
2457
|
);
|
|
2461
2458
|
const preferredByStorageDir = new Map<
|
|
2462
2459
|
string,
|
|
@@ -2570,7 +2567,7 @@ export class Orchestrator {
|
|
|
2570
2567
|
new Set(
|
|
2571
2568
|
(options.namespaces?.length
|
|
2572
2569
|
? options.namespaces
|
|
2573
|
-
: this.
|
|
2570
|
+
: this.configuredNamespaceList()
|
|
2574
2571
|
)
|
|
2575
2572
|
.map((value) => value.trim())
|
|
2576
2573
|
.filter(Boolean),
|
|
@@ -4138,7 +4135,7 @@ export class Orchestrator {
|
|
|
4138
4135
|
if (options?.dryRun !== true) {
|
|
4139
4136
|
try {
|
|
4140
4137
|
await this.processEntitySynthesisQueue(
|
|
4141
|
-
this.
|
|
4138
|
+
this.storageDirNamespace(targetStorage.dir),
|
|
4142
4139
|
5,
|
|
4143
4140
|
);
|
|
4144
4141
|
} catch (error) {
|
|
@@ -13971,7 +13968,7 @@ export class Orchestrator {
|
|
|
13971
13968
|
)
|
|
13972
13969
|
return false;
|
|
13973
13970
|
if (
|
|
13974
|
-
this.
|
|
13971
|
+
this.storageDirNamespace(targetStorage.dir) ===
|
|
13975
13972
|
this.config.sharedNamespace
|
|
13976
13973
|
)
|
|
13977
13974
|
return false;
|
|
@@ -14797,7 +14794,7 @@ export class Orchestrator {
|
|
|
14797
14794
|
let targetNamespaceName =
|
|
14798
14795
|
baseNamespace && baseNamespace.length > 0
|
|
14799
14796
|
? baseNamespace
|
|
14800
|
-
: this.
|
|
14797
|
+
: this.storageDirNamespace(targetStorage.dir);
|
|
14801
14798
|
let routedRuleId: string | undefined;
|
|
14802
14799
|
let routedNamespaceExplicit = false;
|
|
14803
14800
|
if (routeRules.length > 0) {
|
|
@@ -14838,7 +14835,7 @@ export class Orchestrator {
|
|
|
14838
14835
|
fact.scope === "global" &&
|
|
14839
14836
|
!routedNamespaceExplicit
|
|
14840
14837
|
) {
|
|
14841
|
-
const currentNs = this.
|
|
14838
|
+
const currentNs = this.storageDirNamespace(targetStorage.dir);
|
|
14842
14839
|
if (currentNs !== this.config.sharedNamespace && profileAllowsSharedWrites) {
|
|
14843
14840
|
try {
|
|
14844
14841
|
targetStorage = await this.storageRouter.storageFor(
|
|
@@ -15106,7 +15103,7 @@ export class Orchestrator {
|
|
|
15106
15103
|
this.qmd.isAvailable() &&
|
|
15107
15104
|
faithfulnessEnforceStatus !== "pending_review"
|
|
15108
15105
|
) {
|
|
15109
|
-
const targetNamespace = this.
|
|
15106
|
+
const targetNamespace = this.storageDirNamespace(targetStorage.dir);
|
|
15110
15107
|
const contradiction = await this.checkForContradiction(
|
|
15111
15108
|
fact.content,
|
|
15112
15109
|
writeCategory,
|
|
@@ -15525,7 +15522,7 @@ export class Orchestrator {
|
|
|
15525
15522
|
memoryId: parentId,
|
|
15526
15523
|
category: writeCategory,
|
|
15527
15524
|
content: fact.content,
|
|
15528
|
-
namespace: this.
|
|
15525
|
+
namespace: this.storageDirNamespace(targetStorage.dir),
|
|
15529
15526
|
confidence: fact.confidence,
|
|
15530
15527
|
source: "extraction",
|
|
15531
15528
|
}),
|
|
@@ -15536,7 +15533,7 @@ export class Orchestrator {
|
|
|
15536
15533
|
|
|
15537
15534
|
// Suggest links for this memory (Phase 3A)
|
|
15538
15535
|
if (this.config.memoryLinkingEnabled && this.qmd.isAvailable()) {
|
|
15539
|
-
const targetNamespace = this.
|
|
15536
|
+
const targetNamespace = this.storageDirNamespace(targetStorage.dir);
|
|
15540
15537
|
const suggestedLinks = await this.suggestLinksForMemory(
|
|
15541
15538
|
fact.content,
|
|
15542
15539
|
writeCategory,
|
|
@@ -15642,7 +15639,7 @@ export class Orchestrator {
|
|
|
15642
15639
|
memoryId,
|
|
15643
15640
|
category: writeCategory,
|
|
15644
15641
|
content: fact.content,
|
|
15645
|
-
namespace: this.
|
|
15642
|
+
namespace: this.storageDirNamespace(targetStorage.dir),
|
|
15646
15643
|
confidence: fact.confidence,
|
|
15647
15644
|
source: "extraction",
|
|
15648
15645
|
}),
|
|
@@ -15788,7 +15785,7 @@ export class Orchestrator {
|
|
|
15788
15785
|
const baseTouchNamespace =
|
|
15789
15786
|
baseNamespace && baseNamespace.length > 0
|
|
15790
15787
|
? baseNamespace
|
|
15791
|
-
: this.
|
|
15788
|
+
: this.storageDirNamespace(storage.dir);
|
|
15792
15789
|
};
|
|
15793
15790
|
const recordDurableNonFactWrite = () => {
|
|
15794
15791
|
durableNonFactWritten = true;
|
|
@@ -17979,7 +17976,7 @@ export class Orchestrator {
|
|
|
17979
17976
|
|
|
17980
17977
|
const fallbackNamespace =
|
|
17981
17978
|
fallbackStorageDir !== null
|
|
17982
|
-
? this.
|
|
17979
|
+
? this.storageDirNamespace(fallbackStorageDir)
|
|
17983
17980
|
: this.config.defaultNamespace;
|
|
17984
17981
|
if (
|
|
17985
17982
|
recallNamespaces.length === 0 ||
|
|
@@ -18169,7 +18166,7 @@ export class Orchestrator {
|
|
|
18169
18166
|
|
|
18170
18167
|
const fallbackNamespace =
|
|
18171
18168
|
fallbackStorageDir !== null
|
|
18172
|
-
? this.
|
|
18169
|
+
? this.storageDirNamespace(fallbackStorageDir)
|
|
18173
18170
|
: this.config.defaultNamespace;
|
|
18174
18171
|
maybeAddStorage(fallbackStorage, fallbackNamespace);
|
|
18175
18172
|
|
|
@@ -20034,41 +20031,18 @@ export class Orchestrator {
|
|
|
20034
20031
|
return namespaceIdentityFromToken(m[1]) ?? m[1];
|
|
20035
20032
|
}
|
|
20036
20033
|
|
|
20037
|
-
private
|
|
20038
|
-
|
|
20039
|
-
|
|
20040
|
-
|
|
20041
|
-
|
|
20042
|
-
|
|
20043
|
-
|
|
20044
|
-
|
|
20045
|
-
|
|
20046
|
-
|
|
20047
|
-
|
|
20048
|
-
|
|
20049
|
-
// (`namespaceIdentityToken(decoded) === dirName`) is TAUTOLOGICAL for a
|
|
20050
|
-
// canonical token string, so it cannot tell a tokenized dir for `alpha` apart
|
|
20051
|
-
// from the legacy raw root of a namespace literally named `ns-616c706861`
|
|
20052
|
-
// (codex NRCve). A dir name that is itself a KNOWN namespace (configured or
|
|
20053
|
-
// catalog-owned at this exact storage root) is therefore preserved as the
|
|
20054
|
-
// literal namespace BEFORE attempting to decode it.
|
|
20055
|
-
if (this.configuredNamespaces().includes(dirName)) {
|
|
20056
|
-
return dirName;
|
|
20057
|
-
}
|
|
20058
|
-
this.loadNamespaceStorageDirHintsFromCatalog();
|
|
20059
|
-
const hintedNamespaces = this.namespaceStorageDirHints.get(resolvedStorageDir);
|
|
20060
|
-
if (hintedNamespaces?.has(dirName)) {
|
|
20061
|
-
return dirName;
|
|
20062
|
-
}
|
|
20063
|
-
if (hintedNamespaces?.size === 1) {
|
|
20064
|
-
const [hintedNamespace] = hintedNamespaces;
|
|
20065
|
-
if (hintedNamespace) return hintedNamespace;
|
|
20066
|
-
}
|
|
20067
|
-
const decoded = namespaceIdentityFromToken(dirName);
|
|
20068
|
-
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
20069
|
-
return decoded;
|
|
20070
|
-
}
|
|
20071
|
-
return dirName;
|
|
20034
|
+
private storageDirNamespace(storageDir: string): string {
|
|
20035
|
+
// #1521: delegates to the scope-module resolver. The inline dir→namespace
|
|
20036
|
+
// derivation (token round-trip guard, catalog hints) is retired so the
|
|
20037
|
+
// adHocNamespaceResolutions ratchet no longer counts this site. Hints are
|
|
20038
|
+
// loaded lazily via the callback (only after early returns, matching the
|
|
20039
|
+
// original behavior — codex P2).
|
|
20040
|
+
return resolveNamespaceFromStorageDir(storageDir, {
|
|
20041
|
+
config: this.config,
|
|
20042
|
+
configuredNamespaces: this.configuredNamespaceList(),
|
|
20043
|
+
hints: this.namespaceStorageDirHints,
|
|
20044
|
+
loadHints: () => this.loadNamespaceStorageDirHintsFromCatalog(),
|
|
20045
|
+
});
|
|
20072
20046
|
}
|
|
20073
20047
|
|
|
20074
20048
|
// #1522: catalog touch methods removed — touches now happen at the storage chokepoint.
|
|
@@ -21,7 +21,12 @@
|
|
|
21
21
|
import assert from "node:assert/strict";
|
|
22
22
|
import test from "node:test";
|
|
23
23
|
|
|
24
|
-
import {
|
|
24
|
+
import {
|
|
25
|
+
getConfiguredNamespaces,
|
|
26
|
+
resolveNamespaceFromStorageDir,
|
|
27
|
+
resolveScopePlan,
|
|
28
|
+
resolveWritableNamespaceValue,
|
|
29
|
+
} from "./scope-plan.js";
|
|
25
30
|
import {
|
|
26
31
|
combineNamespaces,
|
|
27
32
|
lcmSessionKeyForNamespace,
|
|
@@ -358,3 +363,165 @@ test("scope-plan: LCM session ids match lcmSessionKeyForNamespace encoding", ()
|
|
|
358
363
|
);
|
|
359
364
|
assert.deepEqual([...plan.lcmReadSessionIds], expected);
|
|
360
365
|
});
|
|
366
|
+
|
|
367
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
368
|
+
// Fitness tests: centralized namespace-resolution helpers (#1521 step 3–4)
|
|
369
|
+
//
|
|
370
|
+
// These pin the three pure helpers that replace the ad-hoc resolution scattered
|
|
371
|
+
// across orchestrator.ts and access-service.ts. They MUST match the pre-migration
|
|
372
|
+
// behavior byte-for-byte (the snapshots were derived from the original inline
|
|
373
|
+
// implementations).
|
|
374
|
+
// ──────────────────────────────────────────────────────────────────────────
|
|
375
|
+
|
|
376
|
+
test("getConfiguredNamespaces: default + shared + policies, trimmed and deduped", () => {
|
|
377
|
+
const config = baseConfig({
|
|
378
|
+
defaultNamespace: "default",
|
|
379
|
+
sharedNamespace: "shared",
|
|
380
|
+
namespacePolicies: [
|
|
381
|
+
{ name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
382
|
+
{ name: "team-b", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
383
|
+
],
|
|
384
|
+
});
|
|
385
|
+
const ns = getConfiguredNamespaces(config);
|
|
386
|
+
assert.deepEqual(ns, ["default", "shared", "team-a", "team-b"]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
test("getConfiguredNamespaces: trims whitespace and drops empty", () => {
|
|
390
|
+
const config = baseConfig({
|
|
391
|
+
defaultNamespace: " default ",
|
|
392
|
+
sharedNamespace: "",
|
|
393
|
+
namespacePolicies: [
|
|
394
|
+
{ name: "team-a ", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
395
|
+
],
|
|
396
|
+
});
|
|
397
|
+
const ns = getConfiguredNamespaces(config);
|
|
398
|
+
assert.deepEqual(ns, ["default", "team-a"]);
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
test("getConfiguredNamespaces: deduplicates identical names", () => {
|
|
402
|
+
const config = baseConfig({
|
|
403
|
+
defaultNamespace: "default",
|
|
404
|
+
sharedNamespace: "default",
|
|
405
|
+
namespacePolicies: [
|
|
406
|
+
{ name: "default", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
407
|
+
],
|
|
408
|
+
});
|
|
409
|
+
const ns = getConfiguredNamespaces(config);
|
|
410
|
+
assert.deepEqual(ns, ["default"]);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
// ── resolveNamespaceFromStorageDir ──────────────────────────────────────────
|
|
414
|
+
|
|
415
|
+
test("resolveNamespaceFromStorageDir: namespaces disabled → always default", () => {
|
|
416
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
417
|
+
const ns = resolveNamespaceFromStorageDir("/some/path/namespaces/team-a", {
|
|
418
|
+
config,
|
|
419
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
420
|
+
});
|
|
421
|
+
assert.equal(ns, "default");
|
|
422
|
+
});
|
|
423
|
+
|
|
424
|
+
test("resolveNamespaceFromStorageDir: memory root → default", () => {
|
|
425
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
426
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory", {
|
|
427
|
+
config,
|
|
428
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
429
|
+
});
|
|
430
|
+
assert.equal(ns, "default");
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
test("resolveNamespaceFromStorageDir: configured namespace dir → that namespace", () => {
|
|
434
|
+
const config = baseConfig({
|
|
435
|
+
memoryDir: "/data/memory",
|
|
436
|
+
namespacePolicies: [
|
|
437
|
+
{ name: "team-a", readPrincipals: ["*"], writePrincipals: ["*"] },
|
|
438
|
+
],
|
|
439
|
+
});
|
|
440
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/team-a", {
|
|
441
|
+
config,
|
|
442
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
443
|
+
});
|
|
444
|
+
assert.equal(ns, "team-a");
|
|
445
|
+
});
|
|
446
|
+
|
|
447
|
+
test("resolveNamespaceFromStorageDir: tokenized dir → decoded namespace", () => {
|
|
448
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
449
|
+
// "alpha" tokenizes to ns-616c706861
|
|
450
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/ns-616c706861", {
|
|
451
|
+
config,
|
|
452
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
453
|
+
});
|
|
454
|
+
assert.equal(ns, "alpha");
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
test("resolveNamespaceFromStorageDir: unknown dir → literal dir name", () => {
|
|
458
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
459
|
+
const ns = resolveNamespaceFromStorageDir("/data/memory/namespaces/custom-ns", {
|
|
460
|
+
config,
|
|
461
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
462
|
+
});
|
|
463
|
+
assert.equal(ns, "custom-ns");
|
|
464
|
+
});
|
|
465
|
+
|
|
466
|
+
test("resolveNamespaceFromStorageDir: catalog hint resolves single-hint dir", () => {
|
|
467
|
+
const config = baseConfig({ memoryDir: "/data/memory" });
|
|
468
|
+
const resolvedDir = "/data/memory/namespaces/dynamic-xyz";
|
|
469
|
+
const hints = new Map([[resolvedDir, new Set(["my-dynamic-ns"])]]);
|
|
470
|
+
const ns = resolveNamespaceFromStorageDir(resolvedDir, {
|
|
471
|
+
config,
|
|
472
|
+
configuredNamespaces: getConfiguredNamespaces(config),
|
|
473
|
+
hints,
|
|
474
|
+
});
|
|
475
|
+
assert.equal(ns, "my-dynamic-ns");
|
|
476
|
+
});
|
|
477
|
+
|
|
478
|
+
// ── resolveWritableNamespaceValue ───────────────────────────────────────────
|
|
479
|
+
|
|
480
|
+
test("resolveWritableNamespaceValue: undefined namespace → default, ok", () => {
|
|
481
|
+
const config = baseConfig();
|
|
482
|
+
const result = resolveWritableNamespaceValue(undefined, "sess-1", undefined, config);
|
|
483
|
+
assert.equal(result.ok, true);
|
|
484
|
+
if (result.ok) assert.equal(result.namespace, "default");
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
test("resolveWritableNamespaceValue: explicit namespace, writable principal → ok", () => {
|
|
488
|
+
const config = baseConfig({
|
|
489
|
+
namespacePolicies: [
|
|
490
|
+
{ name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
491
|
+
],
|
|
492
|
+
});
|
|
493
|
+
const result = resolveWritableNamespaceValue("team-a", "alice:sess-1", "alice", config);
|
|
494
|
+
assert.equal(result.ok, true);
|
|
495
|
+
if (result.ok) assert.equal(result.namespace, "team-a");
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
test("resolveWritableNamespaceValue: explicit namespace, non-writable principal → not_writable", () => {
|
|
499
|
+
const config = baseConfig({
|
|
500
|
+
namespacePolicies: [
|
|
501
|
+
{ name: "team-a", readPrincipals: ["alice"], writePrincipals: ["alice"] },
|
|
502
|
+
],
|
|
503
|
+
});
|
|
504
|
+
const result = resolveWritableNamespaceValue("team-a", "bob:sess-1", "bob", config);
|
|
505
|
+
assert.equal(result.ok, false);
|
|
506
|
+
if (!result.ok) {
|
|
507
|
+
assert.equal(result.reason, "not_writable");
|
|
508
|
+
assert.equal(result.namespace, "team-a");
|
|
509
|
+
}
|
|
510
|
+
});
|
|
511
|
+
|
|
512
|
+
test("resolveWritableNamespaceValue: non-default namespace with namespaces disabled → unsupported", () => {
|
|
513
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
514
|
+
const result = resolveWritableNamespaceValue("custom-ns", "sess-1", undefined, config);
|
|
515
|
+
assert.equal(result.ok, false);
|
|
516
|
+
if (!result.ok) {
|
|
517
|
+
assert.equal(result.reason, "unsupported");
|
|
518
|
+
assert.equal(result.namespace, "custom-ns");
|
|
519
|
+
}
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
test("resolveWritableNamespaceValue: namespaces disabled, default namespace → ok", () => {
|
|
523
|
+
const config = baseConfig({ namespacesEnabled: false });
|
|
524
|
+
const result = resolveWritableNamespaceValue("default", "sess-1", undefined, config);
|
|
525
|
+
assert.equal(result.ok, true);
|
|
526
|
+
if (result.ok) assert.equal(result.namespace, "default");
|
|
527
|
+
});
|
package/src/scopes/scope-plan.ts
CHANGED
|
@@ -33,10 +33,16 @@
|
|
|
33
33
|
|
|
34
34
|
import {
|
|
35
35
|
canReadNamespace,
|
|
36
|
+
canWriteNamespace,
|
|
36
37
|
defaultNamespaceForPrincipal,
|
|
37
38
|
recallNamespacesForPrincipal,
|
|
38
39
|
resolvePrincipal,
|
|
39
40
|
} from "../namespaces/principal.js";
|
|
41
|
+
import {
|
|
42
|
+
namespaceIdentityFromToken,
|
|
43
|
+
namespaceIdentityToken,
|
|
44
|
+
} from "../namespaces/identity.js";
|
|
45
|
+
import path from "node:path";
|
|
40
46
|
import type { ResolvedScopeProfilePlan } from "../namespaces/scope-profiles.js";
|
|
41
47
|
import {
|
|
42
48
|
expandScopeProfileReadNamespaces,
|
|
@@ -318,3 +324,160 @@ export function resolveScopePlan(options: ResolveScopePlanOptions): ScopePlan {
|
|
|
318
324
|
scopeProfilePlan,
|
|
319
325
|
};
|
|
320
326
|
}
|
|
327
|
+
|
|
328
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
329
|
+
// Centralized namespace-resolution helpers (issue #1521 step 3–4)
|
|
330
|
+
//
|
|
331
|
+
// The three functions below consolidate the ad-hoc namespace-resolution logic
|
|
332
|
+
// that was previously scattered across orchestrator.ts (namespaceFromStorageDir,
|
|
333
|
+
// configuredNamespaces) and access-service.ts (resolveWritableNamespace). By
|
|
334
|
+
// living in THIS module they are excluded from the adHocNamespaceResolutions
|
|
335
|
+
// ratchet, and every consumer reaches them through a single well-known import
|
|
336
|
+
// instead of reimplementing the derivation inline (#1519's failure mode).
|
|
337
|
+
// ────────────────────────────────────────────────────────────────────────────
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* The configured namespace set: default + shared + every namespace-policy
|
|
341
|
+
* name, trimmed and de-duplicated. Pure — reads only from `config`.
|
|
342
|
+
*
|
|
343
|
+
* Replaces the `configuredNamespaces()` private method that lived on both
|
|
344
|
+
* `Orchestrator` and the maintenance namespace-planner (two copies of the
|
|
345
|
+
* same logic).
|
|
346
|
+
*/
|
|
347
|
+
export function getConfiguredNamespaces(config: PluginConfig): string[] {
|
|
348
|
+
return Array.from(
|
|
349
|
+
new Set(
|
|
350
|
+
[
|
|
351
|
+
config.defaultNamespace,
|
|
352
|
+
config.sharedNamespace,
|
|
353
|
+
...config.namespacePolicies.map((policy) => policy.name),
|
|
354
|
+
]
|
|
355
|
+
.map((value) => value.trim())
|
|
356
|
+
.filter(Boolean),
|
|
357
|
+
),
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Options for {@link resolveNamespaceFromStorageDir}.
|
|
363
|
+
*/
|
|
364
|
+
export interface ResolveNamespaceFromStorageDirOptions {
|
|
365
|
+
/** Plugin config (memoryDir, defaultNamespace, namespacesEnabled, …). */
|
|
366
|
+
readonly config: PluginConfig;
|
|
367
|
+
/**
|
|
368
|
+
* Pre-computed configured-namespace set (from {@link getConfiguredNamespaces}).
|
|
369
|
+
* Passed by the caller so this function does not re-derive it on every call.
|
|
370
|
+
*/
|
|
371
|
+
readonly configuredNamespaces: readonly string[];
|
|
372
|
+
/**
|
|
373
|
+
* Catalog-owned dir→namespace hints (best-effort union of configured +
|
|
374
|
+
* catalog-discovered). May be `undefined` when no catalog hints are loaded.
|
|
375
|
+
*/
|
|
376
|
+
readonly hints?: ReadonlyMap<string, ReadonlySet<string>> | undefined;
|
|
377
|
+
/**
|
|
378
|
+
* Lazy catalog-hint loader. Called ONLY after the early returns (disabled,
|
|
379
|
+
* memory root, no namespace segment, configured namespace) — matching the
|
|
380
|
+
* original lazy behavior so an eager load does not mark hints as loaded
|
|
381
|
+
* before the catalog file exists (codex P2).
|
|
382
|
+
*/
|
|
383
|
+
readonly loadHints?: () => void;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Derive the namespace a storage directory belongs to.
|
|
388
|
+
*
|
|
389
|
+
* Pure (no orchestrator state): the caller passes `config`, the configured set,
|
|
390
|
+
* and any catalog hints. The logic is byte-identical to the private
|
|
391
|
+
* `namespaceFromStorageDir` method that previously lived on `Orchestrator`
|
|
392
|
+
* (including the token round-trip guard from codex round 6).
|
|
393
|
+
*/
|
|
394
|
+
export function resolveNamespaceFromStorageDir(
|
|
395
|
+
storageDir: string,
|
|
396
|
+
options: ResolveNamespaceFromStorageDirOptions,
|
|
397
|
+
): string {
|
|
398
|
+
const { config } = options;
|
|
399
|
+
if (!config.namespacesEnabled) return config.defaultNamespace;
|
|
400
|
+
const resolvedStorageDir = path.resolve(storageDir);
|
|
401
|
+
const resolvedMemoryDir = path.resolve(config.memoryDir);
|
|
402
|
+
if (resolvedStorageDir === resolvedMemoryDir) return config.defaultNamespace;
|
|
403
|
+
const m = resolvedStorageDir.match(/[\\/]namespaces[\\/]([^\\/]+)$/);
|
|
404
|
+
if (!m?.[1]) return config.defaultNamespace;
|
|
405
|
+
const dirName = m[1];
|
|
406
|
+
// Token-shaped raw names (codex P2 — NBsFz): a dir name might be a tokenized
|
|
407
|
+
// identity OR a literal raw namespace name that merely LOOKS like a token.
|
|
408
|
+
// A dir name that is itself a KNOWN namespace is preserved BEFORE decoding.
|
|
409
|
+
if (options.configuredNamespaces.includes(dirName)) {
|
|
410
|
+
return dirName;
|
|
411
|
+
}
|
|
412
|
+
// Load catalog hints ONLY after the early returns (codex P2: an eager load
|
|
413
|
+
// before the catalog file exists would mark hints as loaded and skip later
|
|
414
|
+
// catalog-derived rows).
|
|
415
|
+
options.loadHints?.();
|
|
416
|
+
const hints = options.hints;
|
|
417
|
+
const hintedNamespaces = hints?.get(resolvedStorageDir);
|
|
418
|
+
if (hintedNamespaces?.has(dirName)) {
|
|
419
|
+
return dirName;
|
|
420
|
+
}
|
|
421
|
+
if (hintedNamespaces?.size === 1) {
|
|
422
|
+
const [hintedNamespace] = hintedNamespaces;
|
|
423
|
+
if (hintedNamespace) return hintedNamespace;
|
|
424
|
+
}
|
|
425
|
+
const decoded = namespaceIdentityFromToken(dirName);
|
|
426
|
+
if (decoded && namespaceIdentityToken(decoded) === dirName) {
|
|
427
|
+
return decoded;
|
|
428
|
+
}
|
|
429
|
+
return dirName;
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Result of resolving a writable namespace: either the authorized namespace
|
|
434
|
+
* or a structured rejection. The caller decides how to surface the rejection
|
|
435
|
+
* (e.g. as an `EngramAccessInputError` in the access-service layer), keeping
|
|
436
|
+
* this module free of transport-specific error types.
|
|
437
|
+
*/
|
|
438
|
+
export type WritableNamespaceResult =
|
|
439
|
+
| { readonly ok: true; readonly namespace: string }
|
|
440
|
+
| { readonly ok: false; readonly reason: "unsupported" | "not_writable"; readonly namespace: string };
|
|
441
|
+
|
|
442
|
+
/**
|
|
443
|
+
* Resolve and authorize a writable namespace.
|
|
444
|
+
*
|
|
445
|
+
* Combines the three steps that `AccessService.resolveWritableNamespace`
|
|
446
|
+
* performed inline: (1) resolve the namespace value (trim → default fallback →
|
|
447
|
+
* validate against `namespacesEnabled`), (2) resolve the principal, (3) check
|
|
448
|
+
* `canWriteNamespace`. Returns a structured result so this module stays free of
|
|
449
|
+
* `EngramAccessInputError` (avoiding a circular dependency on access-service.ts).
|
|
450
|
+
*
|
|
451
|
+
* The caller throws `EngramAccessInputError` when `ok === false` — the access-
|
|
452
|
+
* service wrapper does this in one place.
|
|
453
|
+
*/
|
|
454
|
+
export function resolveWritableNamespaceValue(
|
|
455
|
+
namespace: string | undefined,
|
|
456
|
+
sessionKey: string | undefined,
|
|
457
|
+
authenticatedPrincipal: string | undefined,
|
|
458
|
+
config: PluginConfig,
|
|
459
|
+
): WritableNamespaceResult {
|
|
460
|
+
const requested = namespace?.trim();
|
|
461
|
+
let resolved: string;
|
|
462
|
+
if (!requested) {
|
|
463
|
+
resolved = config.defaultNamespace;
|
|
464
|
+
} else if (
|
|
465
|
+
!config.namespacesEnabled &&
|
|
466
|
+
requested !== config.defaultNamespace
|
|
467
|
+
) {
|
|
468
|
+
return { ok: false, reason: "unsupported", namespace: requested };
|
|
469
|
+
} else {
|
|
470
|
+
resolved = requested;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const trusted = authenticatedPrincipal?.trim();
|
|
474
|
+
const principal =
|
|
475
|
+
typeof trusted === "string" && trusted.length > 0
|
|
476
|
+
? trusted
|
|
477
|
+
: resolvePrincipal(sessionKey, config);
|
|
478
|
+
|
|
479
|
+
if (!canWriteNamespace(principal, resolved, config)) {
|
|
480
|
+
return { ok: false, reason: "not_writable", namespace: resolved };
|
|
481
|
+
}
|
|
482
|
+
return { ok: true, namespace: resolved };
|
|
483
|
+
}
|