@remnic/core 9.3.753 → 9.3.754
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-admin-ops-surface.d.ts +1 -1
- package/dist/access-admin-ops-surface.js +26 -26
- package/dist/access-boundary.d.ts +1 -1
- package/dist/access-boundary.js +27 -27
- package/dist/access-cli.js +75 -75
- package/dist/access-http.d.ts +1 -1
- package/dist/access-http.js +34 -34
- package/dist/access-lcm-surface.d.ts +188 -0
- package/dist/access-lcm-surface.js +128 -0
- package/dist/access-lcm-surface.js.map +1 -0
- package/dist/access-mcp.d.ts +1 -1
- package/dist/access-mcp.js +31 -31
- package/dist/access-operations-batch.js +28 -28
- package/dist/access-operations.d.ts +5 -5
- package/dist/access-operations.js +30 -30
- package/dist/access-recall-surface.d.ts +1 -1
- package/dist/access-recall-surface.js +26 -26
- package/dist/access-schema.d.ts +74 -74
- package/dist/access-schema.js +6 -6
- package/dist/{access-service-MitduesD.d.ts → access-service-DKRV1zxh.d.ts} +4 -64
- package/dist/access-service.d.ts +1 -1
- package/dist/access-service.js +26 -26
- package/dist/access-surface-catalog.d.ts +1 -1
- package/dist/adapters/index.js +4 -4
- package/dist/adapters/registry.js +2 -2
- package/dist/{auto-sync-IEBKPGAM.js → auto-sync-G4YKBPOU.js} +5 -5
- package/dist/briefing.js +7 -7
- package/dist/calibration.js +2 -2
- package/dist/causal-consolidation.js +8 -8
- package/dist/{chunk-HAZVVOYT.js → chunk-24463PIV.js} +2 -2
- package/dist/{chunk-C3IHRWIC.js → chunk-2MHCWXX6.js} +2 -2
- package/dist/{chunk-GWGWBK5Y.js → chunk-2MR3MFQB.js} +7 -7
- package/dist/{chunk-YTU7WKYN.js → chunk-2OZ2JEUP.js} +2 -2
- package/dist/{chunk-AU7Q3LSC.js → chunk-2QSZNTDO.js} +4 -4
- package/dist/{chunk-EHYB325U.js → chunk-3DOD6ICJ.js} +2 -2
- package/dist/{chunk-KY4WOZ3P.js → chunk-4DVYRPLL.js} +42 -42
- package/dist/{chunk-VUA4S7P6.js → chunk-5J4OL7TJ.js} +4 -4
- package/dist/{chunk-EEO4YWQ7.js → chunk-7AL5WNPW.js} +2 -2
- package/dist/{chunk-BWOEYBRB.js → chunk-7CAJLUNW.js} +2 -2
- package/dist/{chunk-JES2O5Q6.js → chunk-BERWPOXU.js} +4 -4
- package/dist/{chunk-3HGZX26P.js → chunk-BQ6J6P3K.js} +8 -8
- package/dist/{chunk-XOPLMSVV.js → chunk-CJ276MDI.js} +2 -2
- package/dist/{chunk-TVISHYL5.js → chunk-COZEVVXZ.js} +2 -2
- package/dist/{chunk-ZIFPFKB2.js → chunk-CXCZ4LAI.js} +2 -2
- package/dist/{chunk-OUXPVWTZ.js → chunk-FLVL4L3L.js} +5 -5
- package/dist/{chunk-LZ73Y56P.js → chunk-G7LCG56O.js} +4 -4
- package/dist/{chunk-E3MRYDUQ.js → chunk-GAH5755F.js} +2 -2
- package/dist/{chunk-JGPURIEH.js → chunk-GPFXPC2V.js} +2 -2
- package/dist/{chunk-3ZIKDH6C.js → chunk-JBO6UBIE.js} +4 -4
- package/dist/{chunk-YNBEHUZH.js → chunk-KURUULFF.js} +2 -2
- package/dist/{chunk-ZECLLIMX.js → chunk-L4AS7TIG.js} +4998 -4950
- package/dist/chunk-L4AS7TIG.js.map +1 -0
- package/dist/{chunk-LQ6JI4VH.js → chunk-LD53WPMU.js} +4 -4
- package/dist/{chunk-QIRZI3C3.js → chunk-OH66EBCS.js} +2 -2
- package/dist/{chunk-OZMBT7N3.js → chunk-PJQMISPD.js} +2 -2
- package/dist/{chunk-PN5AMCK2.js → chunk-PYVZKEZZ.js} +53 -53
- package/dist/{chunk-DCWIQFNA.js → chunk-T422SYM6.js} +5 -5
- package/dist/{chunk-5CEJH5ZN.js → chunk-TAPSNIMA.js} +4 -4
- package/dist/{chunk-3NN73CM7.js → chunk-TH7WMHVK.js} +2 -2
- package/dist/{chunk-34THARMH.js → chunk-THZ4WR5L.js} +10 -10
- package/dist/{chunk-E6LENGYV.js → chunk-TIFF32QK.js} +2 -2
- package/dist/{chunk-RNWWAS5V.js → chunk-TJATYB3F.js} +11 -11
- package/dist/{chunk-KYHGLDFU.js → chunk-TXALJRYO.js} +6 -6
- package/dist/{chunk-DKIZ4BUQ.js → chunk-U2SMR57B.js} +7 -7
- package/dist/{chunk-VVSV2QVY.js → chunk-V4C4SFZB.js} +3 -3
- package/dist/{chunk-AXBRVBT6.js → chunk-VSU2NZGC.js} +2 -2
- package/dist/{chunk-N3VE7OCY.js → chunk-WS6GLJ7P.js} +2 -2
- package/dist/{chunk-ITLJATQI.js → chunk-WUVUTOUV.js} +2 -2
- package/dist/{chunk-WRFKZEO6.js → chunk-YQGHW5ML.js} +4 -4
- package/dist/{chunk-243P7YYV.js → chunk-YRBXBHU7.js} +2 -2
- package/dist/{chunk-ZMHDSIEX.js → chunk-ZJMSTFOR.js} +4 -4
- package/dist/{cli-DqonYBZ0.d.ts → cli-D-xw8Sp1.d.ts} +1 -1
- package/dist/cli.d.ts +2 -2
- package/dist/cli.js +55 -55
- package/dist/compounding/engine.js +7 -7
- package/dist/connectors/codex-materialize-runner.js +7 -7
- package/dist/connectors/index.js +7 -7
- package/dist/conversation-index/backend.js +2 -2
- package/dist/entity-retrieval.js +7 -7
- package/dist/extraction-faithfulness.js +3 -3
- package/dist/extraction-judge.js +3 -3
- package/dist/extraction.js +3 -3
- package/dist/fallback-llm.js +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +149 -149
- package/dist/lcm/engine.js +2 -2
- package/dist/lcm/index.js +4 -4
- package/dist/maintenance/memory-governance.js +7 -7
- package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +7 -7
- package/dist/maintenance/rebuild-memory-projection.js +8 -8
- package/dist/mcp-memory-inspector-app.d.ts +1 -1
- package/dist/namespaces/migrate.js +17 -17
- package/dist/namespaces/search.js +9 -9
- package/dist/namespaces/storage.js +7 -7
- package/dist/operator-toolkit.js +21 -21
- package/dist/orchestration/maintenance.js +9 -9
- package/dist/orchestrator.js +75 -75
- package/dist/recall-planner-llm.js +2 -2
- package/dist/schemas.d.ts +90 -90
- package/dist/search/factory.js +8 -8
- package/dist/search/index.js +13 -13
- package/dist/search/lancedb-backend.js +2 -2
- package/dist/search/meilisearch-backend.js +2 -2
- package/dist/search/orama-backend.js +2 -2
- package/dist/semantic-consolidation.js +8 -8
- package/dist/semantic-rule-promotion.js +7 -7
- package/dist/semantic-rule-verifier.js +7 -7
- package/dist/shared-context/manager.d.ts +8 -8
- package/dist/storage.js +6 -6
- package/dist/summarizer.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 +66 -66
- package/dist/verified-recall.js +7 -7
- package/package.json +2 -2
- package/src/access-lcm-surface.ts +517 -0
- package/src/access-service.ts +21 -417
- package/dist/chunk-ZECLLIMX.js.map +0 -1
- /package/dist/{auto-sync-IEBKPGAM.js.map → auto-sync-G4YKBPOU.js.map} +0 -0
- /package/dist/{chunk-HAZVVOYT.js.map → chunk-24463PIV.js.map} +0 -0
- /package/dist/{chunk-C3IHRWIC.js.map → chunk-2MHCWXX6.js.map} +0 -0
- /package/dist/{chunk-GWGWBK5Y.js.map → chunk-2MR3MFQB.js.map} +0 -0
- /package/dist/{chunk-YTU7WKYN.js.map → chunk-2OZ2JEUP.js.map} +0 -0
- /package/dist/{chunk-AU7Q3LSC.js.map → chunk-2QSZNTDO.js.map} +0 -0
- /package/dist/{chunk-EHYB325U.js.map → chunk-3DOD6ICJ.js.map} +0 -0
- /package/dist/{chunk-KY4WOZ3P.js.map → chunk-4DVYRPLL.js.map} +0 -0
- /package/dist/{chunk-VUA4S7P6.js.map → chunk-5J4OL7TJ.js.map} +0 -0
- /package/dist/{chunk-EEO4YWQ7.js.map → chunk-7AL5WNPW.js.map} +0 -0
- /package/dist/{chunk-BWOEYBRB.js.map → chunk-7CAJLUNW.js.map} +0 -0
- /package/dist/{chunk-JES2O5Q6.js.map → chunk-BERWPOXU.js.map} +0 -0
- /package/dist/{chunk-3HGZX26P.js.map → chunk-BQ6J6P3K.js.map} +0 -0
- /package/dist/{chunk-XOPLMSVV.js.map → chunk-CJ276MDI.js.map} +0 -0
- /package/dist/{chunk-TVISHYL5.js.map → chunk-COZEVVXZ.js.map} +0 -0
- /package/dist/{chunk-ZIFPFKB2.js.map → chunk-CXCZ4LAI.js.map} +0 -0
- /package/dist/{chunk-OUXPVWTZ.js.map → chunk-FLVL4L3L.js.map} +0 -0
- /package/dist/{chunk-LZ73Y56P.js.map → chunk-G7LCG56O.js.map} +0 -0
- /package/dist/{chunk-E3MRYDUQ.js.map → chunk-GAH5755F.js.map} +0 -0
- /package/dist/{chunk-JGPURIEH.js.map → chunk-GPFXPC2V.js.map} +0 -0
- /package/dist/{chunk-3ZIKDH6C.js.map → chunk-JBO6UBIE.js.map} +0 -0
- /package/dist/{chunk-YNBEHUZH.js.map → chunk-KURUULFF.js.map} +0 -0
- /package/dist/{chunk-LQ6JI4VH.js.map → chunk-LD53WPMU.js.map} +0 -0
- /package/dist/{chunk-QIRZI3C3.js.map → chunk-OH66EBCS.js.map} +0 -0
- /package/dist/{chunk-OZMBT7N3.js.map → chunk-PJQMISPD.js.map} +0 -0
- /package/dist/{chunk-PN5AMCK2.js.map → chunk-PYVZKEZZ.js.map} +0 -0
- /package/dist/{chunk-DCWIQFNA.js.map → chunk-T422SYM6.js.map} +0 -0
- /package/dist/{chunk-5CEJH5ZN.js.map → chunk-TAPSNIMA.js.map} +0 -0
- /package/dist/{chunk-3NN73CM7.js.map → chunk-TH7WMHVK.js.map} +0 -0
- /package/dist/{chunk-34THARMH.js.map → chunk-THZ4WR5L.js.map} +0 -0
- /package/dist/{chunk-E6LENGYV.js.map → chunk-TIFF32QK.js.map} +0 -0
- /package/dist/{chunk-RNWWAS5V.js.map → chunk-TJATYB3F.js.map} +0 -0
- /package/dist/{chunk-KYHGLDFU.js.map → chunk-TXALJRYO.js.map} +0 -0
- /package/dist/{chunk-DKIZ4BUQ.js.map → chunk-U2SMR57B.js.map} +0 -0
- /package/dist/{chunk-VVSV2QVY.js.map → chunk-V4C4SFZB.js.map} +0 -0
- /package/dist/{chunk-AXBRVBT6.js.map → chunk-VSU2NZGC.js.map} +0 -0
- /package/dist/{chunk-N3VE7OCY.js.map → chunk-WS6GLJ7P.js.map} +0 -0
- /package/dist/{chunk-ITLJATQI.js.map → chunk-WUVUTOUV.js.map} +0 -0
- /package/dist/{chunk-WRFKZEO6.js.map → chunk-YQGHW5ML.js.map} +0 -0
- /package/dist/{chunk-243P7YYV.js.map → chunk-YRBXBHU7.js.map} +0 -0
- /package/dist/{chunk-ZMHDSIEX.js.map → chunk-ZJMSTFOR.js.map} +0 -0
|
@@ -0,0 +1,517 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Access LCM read-surface (extracted from access-service.ts; god-file
|
|
3
|
+
* decomposition, #1526 playbook: verbatim move + live selfDeps wiring).
|
|
4
|
+
*
|
|
5
|
+
* Owns the LCM archive read path of the access layer: lcmSearch and the
|
|
6
|
+
* read-authorization resolution trio (LCM read namespace, raw-excerpt
|
|
7
|
+
* read namespace, and the ordered read-authorized LCM session-id set,
|
|
8
|
+
* #1495/#1505 read gates).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { resolveNamespaceCapabilities } from "./capabilities.js";
|
|
12
|
+
import { combineNamespaces, lcmSessionKeyForNamespace, resolveCodingNamespaceOverlay } from "./coding/coding-namespace.js";
|
|
13
|
+
import { log } from "./logger.js";
|
|
14
|
+
import { canWriteNamespace, defaultNamespaceForPrincipal, recallNamespacesForPrincipal } from "./namespaces/principal.js";
|
|
15
|
+
import type { Orchestrator } from "./orchestrator.js";
|
|
16
|
+
import {
|
|
17
|
+
EngramAccessInputError,
|
|
18
|
+
type EngramAccessLcmSearchRequest,
|
|
19
|
+
type EngramAccessLcmSearchResponse,
|
|
20
|
+
} from "./access-service.js";
|
|
21
|
+
|
|
22
|
+
export interface AccessLcmSurfaceDeps {
|
|
23
|
+
lcmSessionIdsForNamespaces(namespaces: string[], sessionKey: string): string[];
|
|
24
|
+
readonly orchestrator: Orchestrator;
|
|
25
|
+
resolveImplicitLcmReadFallbackNamespace(
|
|
26
|
+
principal: string | undefined,
|
|
27
|
+
): string | undefined;
|
|
28
|
+
resolveLcmReadNamespace(
|
|
29
|
+
explicitNamespace: string | undefined,
|
|
30
|
+
resolvedNamespace: string,
|
|
31
|
+
sessionKeyForOverlay: string | undefined,
|
|
32
|
+
authenticatedPrincipal: string | undefined,
|
|
33
|
+
purpose?: "read" | "write",
|
|
34
|
+
): string;
|
|
35
|
+
resolveLcmReadSessionIds(
|
|
36
|
+
explicitNamespace: string | undefined,
|
|
37
|
+
resolvedNamespace: string,
|
|
38
|
+
sessionKey: string,
|
|
39
|
+
authenticatedPrincipal: string | undefined,
|
|
40
|
+
): string[];
|
|
41
|
+
resolveLcmReadSessionKey(
|
|
42
|
+
explicitNamespace: string | undefined,
|
|
43
|
+
resolvedNamespace: string,
|
|
44
|
+
sessionKey: string,
|
|
45
|
+
authenticatedPrincipal: string | undefined,
|
|
46
|
+
purpose?: "read" | "write",
|
|
47
|
+
): string;
|
|
48
|
+
resolveReadableNamespace(namespace: string | undefined, principal?: string): string;
|
|
49
|
+
resolveRequestPrincipal(sessionKey: string | undefined, authenticatedPrincipal?: string): string | undefined;
|
|
50
|
+
resolveScopeProfileLcmReadNamespaces(
|
|
51
|
+
sessionKey: string | undefined,
|
|
52
|
+
authenticatedPrincipal: string | undefined,
|
|
53
|
+
): string[] | null;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class AccessLcmSurface {
|
|
57
|
+
constructor(
|
|
58
|
+
private readonly deps: AccessLcmSurfaceDeps,
|
|
59
|
+
) {}
|
|
60
|
+
|
|
61
|
+
async lcmSearch(request: EngramAccessLcmSearchRequest): Promise<EngramAccessLcmSearchResponse> {
|
|
62
|
+
if (!request.query || typeof request.query !== "string" || request.query.trim().length === 0) {
|
|
63
|
+
throw new EngramAccessInputError("query is required and must be a non-empty string");
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const principal = this.deps.resolveRequestPrincipal(request.sessionKey, request.authenticatedPrincipal);
|
|
67
|
+
const hasExplicitNamespace =
|
|
68
|
+
typeof request.namespace === "string" &&
|
|
69
|
+
request.namespace.trim().length > 0;
|
|
70
|
+
// Resolve the readable base namespace WITHOUT pre-authorizing `default`
|
|
71
|
+
// (#1505 thread NBHWz). An EXPLICIT namespace is still authorized strictly
|
|
72
|
+
// via `resolveReadableNamespace` (explicit reads must pass the ACL — throws
|
|
73
|
+
// on an unreadable explicit namespace). For an IMPLICIT read, derive the
|
|
74
|
+
// fallback from the ALREADY read-authorized recall namespace set instead of
|
|
75
|
+
// read-authorizing `config.defaultNamespace`: under a restrictive `default`
|
|
76
|
+
// READ policy where the principal's self namespace is readable, normal recall
|
|
77
|
+
// still succeeds via `recallNamespacesForPrincipal`, so `lcmSearch` must too
|
|
78
|
+
// (the same defect class the raw-excerpt path fixes). `undefined` ⇒ no
|
|
79
|
+
// readable LCM namespace exists, so return NO rows rather than throwing.
|
|
80
|
+
const profileLcmReadNamespaces = hasExplicitNamespace
|
|
81
|
+
? null
|
|
82
|
+
: this.deps.resolveScopeProfileLcmReadNamespaces(
|
|
83
|
+
request.sessionKey,
|
|
84
|
+
request.authenticatedPrincipal,
|
|
85
|
+
);
|
|
86
|
+
const namespace = hasExplicitNamespace
|
|
87
|
+
? this.deps.resolveReadableNamespace(request.namespace, principal)
|
|
88
|
+
: profileLcmReadNamespaces !== null
|
|
89
|
+
? profileLcmReadNamespaces[0]
|
|
90
|
+
: this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
|
|
91
|
+
|
|
92
|
+
if (!this.deps.orchestrator.lcmEngine || !this.deps.orchestrator.lcmEngine.enabled) {
|
|
93
|
+
return {
|
|
94
|
+
query: request.query,
|
|
95
|
+
namespace: namespace ?? this.deps.orchestrator.config.defaultNamespace,
|
|
96
|
+
results: [],
|
|
97
|
+
count: 0,
|
|
98
|
+
lcmEnabled: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// An active scope profile with no readable layers is authoritative: search
|
|
103
|
+
// no legacy/default LCM keys instead of falling back around the profile.
|
|
104
|
+
if (profileLcmReadNamespaces !== null && profileLcmReadNamespaces.length === 0) {
|
|
105
|
+
return {
|
|
106
|
+
query: request.query,
|
|
107
|
+
namespace: this.deps.orchestrator.config.defaultNamespace,
|
|
108
|
+
results: [],
|
|
109
|
+
count: 0,
|
|
110
|
+
lcmEnabled: true,
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// No readable LCM namespace for an IMPLICIT read (restrictive `default` READ
|
|
115
|
+
// policy, no readable overlay/self) ⇒ return NO rows instead of pre-
|
|
116
|
+
// authorizing the denied default (#1505 thread NBHWz). Normal recall still
|
|
117
|
+
// succeeds through the readable self namespace; LCM search degrades to empty.
|
|
118
|
+
if (namespace === undefined) {
|
|
119
|
+
return {
|
|
120
|
+
query: request.query,
|
|
121
|
+
namespace: this.deps.orchestrator.config.defaultNamespace,
|
|
122
|
+
results: [],
|
|
123
|
+
count: 0,
|
|
124
|
+
lcmEnabled: true,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const limit = Math.max(1, Math.min(request.limit ?? 10, 100));
|
|
129
|
+
// Route the LCM read session_id AND prefix through the SAME overlay-aware
|
|
130
|
+
// namespace `observe`'s write key and compaction use (#1505 round 3). A
|
|
131
|
+
// project-scoped `observe` with no explicit namespace archived under the
|
|
132
|
+
// coding-overlay namespace; deriving the prefix only from the readable
|
|
133
|
+
// `namespace` (as before) would search the raw key and miss those turns.
|
|
134
|
+
// The effective namespace is resolved ONCE from the real session
|
|
135
|
+
// (`request.sessionKey`) — the prefix is a search fragment with no bound
|
|
136
|
+
// coding context, so it inherits the same namespace. Collapses to the raw key
|
|
137
|
+
// for single-store / no-overlay / explicit-default flows (existing behavior).
|
|
138
|
+
const lcmReadNamespace = profileLcmReadNamespaces !== null
|
|
139
|
+
? profileLcmReadNamespaces[0] ?? this.deps.orchestrator.config.defaultNamespace
|
|
140
|
+
: this.deps.resolveLcmReadNamespace(
|
|
141
|
+
request.namespace,
|
|
142
|
+
namespace,
|
|
143
|
+
request.sessionKey,
|
|
144
|
+
request.authenticatedPrincipal,
|
|
145
|
+
);
|
|
146
|
+
// Ordered, read-authorized LCM read key SET for a concrete `sessionKey`
|
|
147
|
+
// (#1505 fallback unification + #1501 scope profiles). A branch-scoped
|
|
148
|
+
// session whose rows were archived at project/root scope is found by querying
|
|
149
|
+
// the primary overlay key first, then each fallback. When a scope profile is
|
|
150
|
+
// active, the profile's expanded read namespace set is authoritative,
|
|
151
|
+
// including the empty set.
|
|
152
|
+
const lcmSessionKeyIds = request.sessionKey
|
|
153
|
+
? profileLcmReadNamespaces !== null
|
|
154
|
+
? this.deps.lcmSessionIdsForNamespaces(
|
|
155
|
+
profileLcmReadNamespaces,
|
|
156
|
+
request.sessionKey,
|
|
157
|
+
)
|
|
158
|
+
: this.deps.resolveLcmReadSessionIds(
|
|
159
|
+
request.namespace,
|
|
160
|
+
namespace,
|
|
161
|
+
request.sessionKey,
|
|
162
|
+
request.authenticatedPrincipal,
|
|
163
|
+
)
|
|
164
|
+
: [undefined];
|
|
165
|
+
const lcmSessionPrefixes = request.sessionPrefix
|
|
166
|
+
? profileLcmReadNamespaces !== null && !request.sessionKey
|
|
167
|
+
? this.deps.lcmSessionIdsForNamespaces(
|
|
168
|
+
profileLcmReadNamespaces,
|
|
169
|
+
request.sessionPrefix,
|
|
170
|
+
)
|
|
171
|
+
: [
|
|
172
|
+
lcmSessionKeyForNamespace(
|
|
173
|
+
lcmReadNamespace,
|
|
174
|
+
request.sessionPrefix,
|
|
175
|
+
this.deps.orchestrator.config.defaultNamespace,
|
|
176
|
+
) ?? request.sessionPrefix,
|
|
177
|
+
]
|
|
178
|
+
: [undefined];
|
|
179
|
+
// SECURITY (#1495 P1 + codex P1 r2 "Require a scoped LCM filter before
|
|
180
|
+
// archive searches"): a sessionless, prefixless `lcmSearch` issues
|
|
181
|
+
// `searchContextFull(query, limit, undefined, undefined)`, an archive-wide
|
|
182
|
+
// FTS scan over EVERY `session_id`, including the sentinel-framed
|
|
183
|
+
// `<ns>`-scoped overlay/tenant rows. The LCM archive is keyed by the
|
|
184
|
+
// `session_id` STRING and is NOT partitioned by namespace, so an unscoped
|
|
185
|
+
// scan CANNOT be constrained to the caller's authorized namespace — neither
|
|
186
|
+
// an explicit `namespace` nor a readable `default` confines its results to
|
|
187
|
+
// rows the caller may read. The scan is therefore safe ONLY in single-store
|
|
188
|
+
// mode (namespaces disabled, one shared archive owned by the caller). When
|
|
189
|
+
// namespaces are ENABLED, an unscoped `lcmSearch` (no `sessionKey` AND no
|
|
190
|
+
// `sessionPrefix`) must be SUPPRESSED — return EMPTY — regardless of an
|
|
191
|
+
// explicit namespace or default-readability, so a caller authorized for
|
|
192
|
+
// `default` (or for one explicit namespace) cannot read other namespaces'
|
|
193
|
+
// transcript rows via the archive-wide scan (cross-tenant read leak). A
|
|
194
|
+
// SCOPED call (sessionKey or sessionPrefix present) is unaffected: it carries
|
|
195
|
+
// a namespace-framed `session_id` / prefix filter that already constrains the
|
|
196
|
+
// search to the caller's authorized, read-gated namespace.
|
|
197
|
+
const hasScopedSession =
|
|
198
|
+
(typeof request.sessionKey === "string" &&
|
|
199
|
+
request.sessionKey.length > 0) ||
|
|
200
|
+
lcmSessionPrefixes.some((prefix) => typeof prefix === "string" && prefix.length > 0);
|
|
201
|
+
if (!hasScopedSession && resolveNamespaceCapabilities(this.deps.orchestrator.config).namespaces === true) {
|
|
202
|
+
return {
|
|
203
|
+
query: request.query,
|
|
204
|
+
namespace,
|
|
205
|
+
results: [],
|
|
206
|
+
count: 0,
|
|
207
|
+
lcmEnabled: true,
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
// Query each LCM read key in order, merging + deduping rows (by
|
|
211
|
+
// sessionId+turnIndex) and preserving first-seen order, capped at `limit`.
|
|
212
|
+
// Use allSettled so one corrupt/failed namespace key cannot discard sibling
|
|
213
|
+
// results from other authorized profile namespaces.
|
|
214
|
+
const seenRows = new Set<string>();
|
|
215
|
+
const results: Array<{ sessionId: string; content: string; turnIndex: number }> = [];
|
|
216
|
+
const lcmSearches: Array<{
|
|
217
|
+
key: string | undefined;
|
|
218
|
+
prefix: string | undefined;
|
|
219
|
+
promise: Promise<Array<{ session_id: string; content: string; turn_index: number }>>;
|
|
220
|
+
}> = [];
|
|
221
|
+
for (const lcmSessionKey of lcmSessionKeyIds) {
|
|
222
|
+
for (const lcmSessionPrefix of lcmSessionPrefixes) {
|
|
223
|
+
lcmSearches.push({
|
|
224
|
+
key: lcmSessionKey,
|
|
225
|
+
prefix: lcmSessionPrefix,
|
|
226
|
+
promise: this.deps.orchestrator.lcmEngine.searchContextFull(
|
|
227
|
+
request.query,
|
|
228
|
+
limit,
|
|
229
|
+
lcmSessionKey,
|
|
230
|
+
lcmSessionPrefix,
|
|
231
|
+
) as Promise<Array<{ session_id: string; content: string; turn_index: number }>>,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
const settledSearches = await Promise.allSettled(
|
|
236
|
+
lcmSearches.map((search) => search.promise),
|
|
237
|
+
);
|
|
238
|
+
for (let i = 0; i < settledSearches.length; i += 1) {
|
|
239
|
+
if (results.length >= limit) break;
|
|
240
|
+
const settled = settledSearches[i];
|
|
241
|
+
if (!settled || settled.status === "rejected") {
|
|
242
|
+
const failed = lcmSearches[i];
|
|
243
|
+
log.warn(
|
|
244
|
+
`lcmSearch: failed for key=${failed?.key ?? "<none>"} prefix=${failed?.prefix ?? "<none>"}: ${settled?.status === "rejected" ? settled.reason : "missing result"}`,
|
|
245
|
+
);
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
for (const r of settled.value) {
|
|
249
|
+
const dedupeKey = `${r.session_id}\0${r.turn_index}`;
|
|
250
|
+
if (seenRows.has(dedupeKey)) continue;
|
|
251
|
+
seenRows.add(dedupeKey);
|
|
252
|
+
results.push({
|
|
253
|
+
sessionId: r.session_id,
|
|
254
|
+
content: r.content,
|
|
255
|
+
turnIndex: r.turn_index,
|
|
256
|
+
});
|
|
257
|
+
if (results.length >= limit) break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return {
|
|
262
|
+
query: request.query,
|
|
263
|
+
namespace,
|
|
264
|
+
results,
|
|
265
|
+
count: results.length,
|
|
266
|
+
lcmEnabled: true,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
/**
|
|
271
|
+
* Resolve the effective LCM NAMESPACE a same-session operation must prefix
|
|
272
|
+
* with (the namespace half of {@link resolveLcmReadSessionKey}). Split out so
|
|
273
|
+
* `lcmSearch` can apply ONE namespace to BOTH its `sessionKey` and its
|
|
274
|
+
* `sessionPrefix` — the prefix is a search fragment, not a real session, so its
|
|
275
|
+
* own coding context can't be looked up; it must inherit the namespace resolved
|
|
276
|
+
* from the real session (`sessionKeyForOverlay`).
|
|
277
|
+
*
|
|
278
|
+
* `purpose` selects the AUTHORIZATION gate applied before honouring the
|
|
279
|
+
* coding overlay (#1505 round 3 + round 4, codex P2):
|
|
280
|
+
*
|
|
281
|
+
* - `"read"` (`lcmSearch` / raw-excerpt recall): the overlay rows are only
|
|
282
|
+
* visible when the principal SELF base is in the READABLE RECALL SET — the
|
|
283
|
+
* same gate the orchestrator's `lcmReadNamespaceForSession` and the recall
|
|
284
|
+
* namespace set use (`recallNamespacesForPrincipal`, gated by both
|
|
285
|
+
* `defaultRecallNamespaces.includes("self")` AND `canReadNamespace`). A
|
|
286
|
+
* caller that passed the default read check must NOT receive
|
|
287
|
+
* `<principal>-project-*` rows the policy never granted (cross-tenant read
|
|
288
|
+
* leak). When the self base is not readable, keep the just-authorized
|
|
289
|
+
* namespace (collapses to the raw key on the default store).
|
|
290
|
+
*
|
|
291
|
+
* - `"write"` (`lcmCompactionFlush` / `lcmCompactionRecord`): these are
|
|
292
|
+
* write/maintenance operations on the SAME queue `observe` just wrote, so
|
|
293
|
+
* the gate must mirror observe's WRITE authorization (`canWriteNamespace`
|
|
294
|
+
* on the self base), NOT readability. A principal that can WRITE but not
|
|
295
|
+
* READ its self namespace (or whose `defaultRecallNamespaces` omits `self`)
|
|
296
|
+
* archived under the overlay key via `observe`; gating compaction by
|
|
297
|
+
* readability would fall back to the default/raw key and leave that queue
|
|
298
|
+
* never flushed/recorded (round-4 codex P2). Write-authorized ⇒ overlay
|
|
299
|
+
* key, matching the observe write key (rule 42 read/write parity; rule 39
|
|
300
|
+
* identical gates across paths).
|
|
301
|
+
*/
|
|
302
|
+
resolveLcmReadNamespace(
|
|
303
|
+
explicitNamespace: string | undefined,
|
|
304
|
+
resolvedNamespace: string,
|
|
305
|
+
sessionKeyForOverlay: string | undefined,
|
|
306
|
+
authenticatedPrincipal: string | undefined,
|
|
307
|
+
purpose: "read" | "write" = "read",
|
|
308
|
+
): string {
|
|
309
|
+
const hasExplicitNamespace =
|
|
310
|
+
typeof explicitNamespace === "string" && explicitNamespace.trim().length > 0;
|
|
311
|
+
if (hasExplicitNamespace) return resolvedNamespace;
|
|
312
|
+
// Mirror observe's write resolution: use the coding-overlay namespace when
|
|
313
|
+
// one applies, else the default store. NOT the principal self base — an
|
|
314
|
+
// unqualified observe archives under the default store, so a self-base prefix
|
|
315
|
+
// here would target a queue observe never wrote to (#1495).
|
|
316
|
+
const principal = this.deps.resolveRequestPrincipal(
|
|
317
|
+
sessionKeyForOverlay,
|
|
318
|
+
authenticatedPrincipal,
|
|
319
|
+
);
|
|
320
|
+
const base = defaultNamespaceForPrincipal(principal, this.deps.orchestrator.config);
|
|
321
|
+
const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
|
|
322
|
+
sessionKeyForOverlay,
|
|
323
|
+
base,
|
|
324
|
+
);
|
|
325
|
+
// No overlay → the default store (raw sessionKey), as before.
|
|
326
|
+
if (overlaid === base) return this.deps.orchestrator.config.defaultNamespace;
|
|
327
|
+
// Overlay applied. Authorize access to the principal's `<principal>-project-*`
|
|
328
|
+
// overlay base before switching the LCM key to it. The gate differs by
|
|
329
|
+
// operation purpose (see the doc comment): reads use the readable-recall-set
|
|
330
|
+
// gate (no cross-tenant read leak), writes use observe's write authorization
|
|
331
|
+
// (so compaction targets the same overlay queue observe wrote to).
|
|
332
|
+
const authorized =
|
|
333
|
+
purpose === "write"
|
|
334
|
+
? canWriteNamespace(principal, base, this.deps.orchestrator.config)
|
|
335
|
+
: recallNamespacesForPrincipal(
|
|
336
|
+
principal,
|
|
337
|
+
this.deps.orchestrator.config,
|
|
338
|
+
).includes(base);
|
|
339
|
+
if (authorized) return overlaid;
|
|
340
|
+
// Unauthorized overlay base. For READS, collapse to the DEFAULT STORE (the
|
|
341
|
+
// raw sessionKey) EXACTLY like the orchestrator's `lcmReadNamespaceForSession`
|
|
342
|
+
// (rule 39 / 42) — NOT the caller's `resolvedNamespace`, which for an implicit
|
|
343
|
+
// read can be a readable recall namespace (e.g. `shared`). Returning that
|
|
344
|
+
// would prefix LCM reads with `shared:sessionKey` while in-prompt recall uses
|
|
345
|
+
// the raw `sessionKey`, diverging `lcmSearch`/raw disclosure from orchestrator
|
|
346
|
+
// LCM reads (cursor "LCM read gate wrong fallback"). For an explicit read the
|
|
347
|
+
// method already returned at the top, so this only affects implicit reads.
|
|
348
|
+
// The (currently unused) write purpose preserves its prior `resolvedNamespace`
|
|
349
|
+
// fallback for backward compatibility.
|
|
350
|
+
if (purpose === "read") return this.deps.orchestrator.config.defaultNamespace;
|
|
351
|
+
return resolvedNamespace;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* Resolve the namespace the raw-disclosure excerpt lookup
|
|
356
|
+
* ({@link fetchRawExcerpts}) must prefix its LCM `session_id` with (#1505
|
|
357
|
+
* thread 2f7). Raw disclosure reads the SAME LCM archive `lcmSearch` and the
|
|
358
|
+
* in-prompt LCM sections read, so it MUST pass through the identical
|
|
359
|
+
* read-authorization gate — NOT `snapshot.namespace`, which records the
|
|
360
|
+
* effective WRITE/overlay namespace (`<principal>-project-*`) even when the
|
|
361
|
+
* principal can WRITE but not READ its self base (or `defaultRecallNamespaces`
|
|
362
|
+
* omits `self`). Routing through `resolveLcmReadNamespace(..., "read")` makes
|
|
363
|
+
* raw disclosure fall back to the default store exactly like normal recall +
|
|
364
|
+
* `lcmSearch`, so it never attaches overlay transcript rows the read gate
|
|
365
|
+
* excludes (cross-tenant read leak). Collapses to the default store / raw
|
|
366
|
+
* sessionKey for single-store / no-overlay / explicit-default flows, so
|
|
367
|
+
* single-user recall is byte-for-byte unchanged.
|
|
368
|
+
*
|
|
369
|
+
* Returns `undefined` when NO readable LCM namespace exists for an IMPLICIT
|
|
370
|
+
* (no explicit `namespace`) raw recall — i.e. a restrictive `default` READ
|
|
371
|
+
* policy denies the principal `default` AND no overlay/self namespace is
|
|
372
|
+
* readable. In that case the caller emits NO excerpts rather than throwing
|
|
373
|
+
* `namespace is not readable: default` (#1505 thread NBHWz): normal recall
|
|
374
|
+
* still succeeds via `recallNamespacesForPrincipal`, so `disclosure: "raw"`
|
|
375
|
+
* must degrade gracefully (empty excerpts), never pre-authorize `default`.
|
|
376
|
+
*
|
|
377
|
+
* IMPLICIT-namespace fallback selection derives from the ALREADY
|
|
378
|
+
* read-authorized recall namespace set (`recallNamespacesForPrincipal` +
|
|
379
|
+
* `canReadNamespace`) — the principal's self base when it is in the readable
|
|
380
|
+
* recall set, else `config.defaultNamespace` ONLY when the principal may read
|
|
381
|
+
* it. It NEVER pre-authorizes `default`. An EXPLICIT `namespace` is still
|
|
382
|
+
* authorized strictly via `resolveReadableNamespace` (explicit reads must pass
|
|
383
|
+
* the ACL — no behavior change).
|
|
384
|
+
*/
|
|
385
|
+
resolveRawExcerptReadNamespace(
|
|
386
|
+
explicitNamespace: string | undefined,
|
|
387
|
+
sessionKey: string | undefined,
|
|
388
|
+
authenticatedPrincipal: string | undefined,
|
|
389
|
+
): string | undefined {
|
|
390
|
+
const principal = this.deps.resolveRequestPrincipal(
|
|
391
|
+
sessionKey,
|
|
392
|
+
authenticatedPrincipal,
|
|
393
|
+
);
|
|
394
|
+
const hasExplicitNamespace =
|
|
395
|
+
typeof explicitNamespace === "string" &&
|
|
396
|
+
explicitNamespace.trim().length > 0;
|
|
397
|
+
if (hasExplicitNamespace) {
|
|
398
|
+
// Explicit reads must pass the ACL — authorize strictly, exactly as
|
|
399
|
+
// `lcmSearch` does (throws on an unreadable explicit namespace).
|
|
400
|
+
const resolvedNamespace = this.deps.resolveReadableNamespace(
|
|
401
|
+
explicitNamespace,
|
|
402
|
+
principal,
|
|
403
|
+
);
|
|
404
|
+
return this.deps.resolveLcmReadNamespace(
|
|
405
|
+
explicitNamespace,
|
|
406
|
+
resolvedNamespace,
|
|
407
|
+
sessionKey,
|
|
408
|
+
authenticatedPrincipal,
|
|
409
|
+
"read",
|
|
410
|
+
);
|
|
411
|
+
}
|
|
412
|
+
// IMPLICIT raw recall: an active scope profile owns the same LCM read
|
|
413
|
+
// namespace set used by recall and lcmSearch. Return the first profile
|
|
414
|
+
// namespace as the legacy raw-excerpt namespace hint; the concrete ordered
|
|
415
|
+
// key set is still produced by resolveLcmReadSessionIds(), which also treats
|
|
416
|
+
// an empty profile read set as authoritative.
|
|
417
|
+
const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
|
|
418
|
+
sessionKey,
|
|
419
|
+
authenticatedPrincipal,
|
|
420
|
+
);
|
|
421
|
+
if (profileReadNamespaces !== null) return profileReadNamespaces[0];
|
|
422
|
+
|
|
423
|
+
// Otherwise derive the read fallback from the ALREADY read-authorized recall
|
|
424
|
+
// namespace set — NEVER pre-authorize `default` (#1505 thread NBHWz). When
|
|
425
|
+
// namespaces are disabled the default store is the only namespace and is
|
|
426
|
+
// always readable (byte-for-byte single-user path).
|
|
427
|
+
const fallbackNamespace =
|
|
428
|
+
this.deps.resolveImplicitLcmReadFallbackNamespace(principal);
|
|
429
|
+
// No readable LCM namespace at all ⇒ no excerpts (caller short-circuits).
|
|
430
|
+
if (fallbackNamespace === undefined) return undefined;
|
|
431
|
+
return this.deps.resolveLcmReadNamespace(
|
|
432
|
+
explicitNamespace,
|
|
433
|
+
fallbackNamespace,
|
|
434
|
+
sessionKey,
|
|
435
|
+
authenticatedPrincipal,
|
|
436
|
+
"read",
|
|
437
|
+
);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
resolveLcmReadSessionIds(
|
|
441
|
+
explicitNamespace: string | undefined,
|
|
442
|
+
resolvedNamespace: string,
|
|
443
|
+
sessionKey: string,
|
|
444
|
+
authenticatedPrincipal: string | undefined,
|
|
445
|
+
): string[] {
|
|
446
|
+
const primary = this.deps.resolveLcmReadSessionKey(
|
|
447
|
+
explicitNamespace,
|
|
448
|
+
resolvedNamespace,
|
|
449
|
+
sessionKey,
|
|
450
|
+
authenticatedPrincipal,
|
|
451
|
+
"read",
|
|
452
|
+
);
|
|
453
|
+
const hasExplicitNamespace =
|
|
454
|
+
typeof explicitNamespace === "string" &&
|
|
455
|
+
explicitNamespace.trim().length > 0;
|
|
456
|
+
// Explicit namespace → no overlay fallbacks (the overlay never applies to an
|
|
457
|
+
// explicit read). Single key, unchanged.
|
|
458
|
+
if (hasExplicitNamespace) return [primary];
|
|
459
|
+
|
|
460
|
+
const profileReadNamespaces = this.deps.resolveScopeProfileLcmReadNamespaces(
|
|
461
|
+
sessionKey,
|
|
462
|
+
authenticatedPrincipal,
|
|
463
|
+
);
|
|
464
|
+
if (profileReadNamespaces !== null) {
|
|
465
|
+
return this.deps.lcmSessionIdsForNamespaces(profileReadNamespaces, sessionKey);
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const principal = this.deps.resolveRequestPrincipal(
|
|
469
|
+
sessionKey,
|
|
470
|
+
authenticatedPrincipal,
|
|
471
|
+
);
|
|
472
|
+
const base = defaultNamespaceForPrincipal(
|
|
473
|
+
principal,
|
|
474
|
+
this.deps.orchestrator.config,
|
|
475
|
+
);
|
|
476
|
+
const overlaid = this.deps.orchestrator.applyCodingNamespaceOverlay(
|
|
477
|
+
sessionKey,
|
|
478
|
+
base,
|
|
479
|
+
);
|
|
480
|
+
// No overlay → single default-store key, unchanged.
|
|
481
|
+
if (overlaid === base) return [primary];
|
|
482
|
+
// Overlay present but self base unreadable → the "read" gate already
|
|
483
|
+
// collapsed `primary` to the default store; do NOT add overlay fallbacks
|
|
484
|
+
// (they would be unauthorized `<principal>-project-*` keys). Single key.
|
|
485
|
+
const selfReadableInRecall = recallNamespacesForPrincipal(
|
|
486
|
+
principal,
|
|
487
|
+
this.deps.orchestrator.config,
|
|
488
|
+
).includes(base);
|
|
489
|
+
if (!selfReadableInRecall) return [primary];
|
|
490
|
+
// Self base readable → overlay rows authorized. Append one LCM key per coding
|
|
491
|
+
// read fallback (project → root), combined with the principal base for
|
|
492
|
+
// isolation — the SAME ordered set the orchestrator recall path searches.
|
|
493
|
+
const overlay = resolveCodingNamespaceOverlay(
|
|
494
|
+
this.deps.orchestrator.getCodingContextForSession(sessionKey),
|
|
495
|
+
this.deps.orchestrator.config.codingMode,
|
|
496
|
+
this.deps.orchestrator.config.defaultNamespace,
|
|
497
|
+
);
|
|
498
|
+
const fallbackNamespaces = (overlay?.readFallbacks ?? []).map((fallback) =>
|
|
499
|
+
combineNamespaces(base, fallback),
|
|
500
|
+
);
|
|
501
|
+
const out = [primary];
|
|
502
|
+
const seen = new Set<string>([primary]);
|
|
503
|
+
for (const ns of fallbackNamespaces) {
|
|
504
|
+
const key =
|
|
505
|
+
lcmSessionKeyForNamespace(
|
|
506
|
+
ns,
|
|
507
|
+
sessionKey,
|
|
508
|
+
this.deps.orchestrator.config.defaultNamespace,
|
|
509
|
+
) ?? sessionKey;
|
|
510
|
+
if (!seen.has(key)) {
|
|
511
|
+
seen.add(key);
|
|
512
|
+
out.push(key);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
return out;
|
|
516
|
+
}
|
|
517
|
+
}
|