@remnic/core 9.14.0 → 9.15.1

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.
Files changed (167) hide show
  1. package/dist/access-admin-ops-surface.d.ts +4 -1
  2. package/dist/access-admin-ops-surface.js +18 -18
  3. package/dist/access-boundary.d.ts +4 -1
  4. package/dist/access-boundary.js +18 -19
  5. package/dist/access-cli.js +64 -66
  6. package/dist/access-cli.js.map +1 -1
  7. package/dist/access-http.d.ts +2 -1
  8. package/dist/access-http.js +24 -25
  9. package/dist/access-identity-continuity-surface.js +18 -18
  10. package/dist/access-lcm-surface.d.ts +4 -1
  11. package/dist/access-lcm-surface.js +18 -18
  12. package/dist/access-mcp.d.ts +4 -1
  13. package/dist/access-mcp.js +23 -24
  14. package/dist/access-namespace-preflight-DgJtZCmU.d.ts +146 -0
  15. package/dist/access-namespace-preflight.d.ts +8 -0
  16. package/dist/access-namespace-preflight.js +158 -0
  17. package/dist/access-observe-write-surface.d.ts +4 -1
  18. package/dist/access-observe-write-surface.js +18 -18
  19. package/dist/access-operations-batch.js +20 -21
  20. package/dist/access-operations.d.ts +8 -5
  21. package/dist/access-operations.js +22 -23
  22. package/dist/access-recall-concurrency.d.ts +4 -1
  23. package/dist/access-recall-concurrency.js +18 -18
  24. package/dist/access-recall-response.d.ts +4 -1
  25. package/dist/access-recall-response.js +18 -18
  26. package/dist/access-recall-surface.d.ts +4 -1
  27. package/dist/access-recall-surface.js +18 -18
  28. package/dist/access-schema.d.ts +76 -76
  29. package/dist/access-schema.js +3 -3
  30. package/dist/{access-service-DbZ1G3zV.d.ts → access-service-bw2p9ULX.d.ts} +6 -114
  31. package/dist/access-service.d.ts +4 -1
  32. package/dist/access-service.js +18 -18
  33. package/dist/access-surface-catalog.d.ts +4 -1
  34. package/dist/active-recall.js +1 -1
  35. package/dist/briefing.js +5 -5
  36. package/dist/{capsule-crypto-GWVG7LGC.js → capsule-crypto-7FJQINUR.js} +2 -2
  37. package/dist/capsule-crypto-7FJQINUR.js.map +1 -0
  38. package/dist/causal-consolidation.js +6 -6
  39. package/dist/{chunk-OWHERGF2.js → chunk-2NLLXCJG.js} +2 -2
  40. package/dist/{chunk-5RZHHANR.js → chunk-63P3PJED.js} +50 -50
  41. package/dist/{chunk-ALSGMEJR.js → chunk-7CPOZH6Y.js} +5 -5
  42. package/dist/{chunk-TY6NBWV4.js → chunk-7FNHLCQE.js} +2 -2
  43. package/dist/{chunk-MMGGVZ4Q.js → chunk-7VZWZVYG.js} +40 -18
  44. package/dist/{chunk-MMGGVZ4Q.js.map → chunk-7VZWZVYG.js.map} +1 -1
  45. package/dist/{chunk-HLG7VAZP.js → chunk-BOP256DB.js} +2 -2
  46. package/dist/{chunk-7G7DVA2C.js → chunk-BVC242OS.js} +2 -2
  47. package/dist/{chunk-CAZ2SHEP.js → chunk-CCNSCRJ2.js} +2 -2
  48. package/dist/{chunk-3332THSR.js → chunk-CVZYPFEH.js} +4 -4
  49. package/dist/{chunk-SK7AMF2J.js → chunk-DETNBTZS.js} +3 -3
  50. package/dist/{chunk-226RNKRU.js → chunk-FFF4XIGB.js} +32 -32
  51. package/dist/{chunk-AS6UOOXF.js → chunk-FMMZQDCT.js} +2 -2
  52. package/dist/{chunk-YQQYS2K4.js → chunk-H3MDWWGN.js} +2 -2
  53. package/dist/{chunk-TC5HBIIT.js → chunk-HBYQAOED.js} +2 -2
  54. package/dist/{chunk-3HYWNJDA.js → chunk-HWFK24UD.js} +420 -42
  55. package/dist/chunk-HWFK24UD.js.map +1 -0
  56. package/dist/{chunk-YO3ZB3PJ.js → chunk-I7DUEPE2.js} +30 -15
  57. package/dist/chunk-I7DUEPE2.js.map +1 -0
  58. package/dist/{chunk-YUQOAKPX.js → chunk-JRXN5LYL.js} +5 -5
  59. package/dist/{chunk-5W7CTZ2E.js → chunk-KQAFEZQX.js} +4 -4
  60. package/dist/{chunk-PYHOAXTI.js → chunk-LRGSLV7M.js} +4 -4
  61. package/dist/{chunk-A56B2AJR.js → chunk-MUQ3BD2C.js} +3 -3
  62. package/dist/{chunk-QJAQNXWI.js → chunk-ONHRRJZM.js} +6 -6
  63. package/dist/{chunk-DF552QUN.js → chunk-QCLQ4BGA.js} +5 -5
  64. package/dist/{chunk-IAHNKI2E.js → chunk-QDNE35FU.js} +2 -2
  65. package/dist/{chunk-W2OST2HH.js → chunk-QLOOCKKE.js} +2 -2
  66. package/dist/{chunk-I4NJ2CDY.js → chunk-RRHEV5HA.js} +4 -6
  67. package/dist/{chunk-I4NJ2CDY.js.map → chunk-RRHEV5HA.js.map} +1 -1
  68. package/dist/{chunk-7HZMTXXB.js → chunk-RWSLO24I.js} +10 -10
  69. package/dist/{chunk-UDIJMITR.js → chunk-SDZWOEZH.js} +22 -22
  70. package/dist/chunk-SDZWOEZH.js.map +1 -0
  71. package/dist/{chunk-QZGN4773.js → chunk-SW2KEZNX.js} +3 -3
  72. package/dist/{chunk-O4K3DWK2.js → chunk-SWVW42CG.js} +2 -2
  73. package/dist/{chunk-UVYI6VIX.js → chunk-X7Y7WX73.js} +1 -1
  74. package/dist/{chunk-CQ6WP3PA.js → chunk-XII2XSHL.js} +5 -5
  75. package/dist/{chunk-YSDVEPV2.js → chunk-XKFXY3ZA.js} +10 -10
  76. package/dist/{chunk-AVPXXV6J.js → chunk-XKGPIWGI.js} +4 -4
  77. package/dist/{chunk-IQFS4AU7.js → chunk-YEU4VEU4.js} +2 -2
  78. package/dist/{chunk-EZ25VE3G.js → chunk-YNDLCWXS.js} +4 -4
  79. package/dist/{chunk-Y6XODVJ4.js → chunk-YS7KIG3A.js} +8 -8
  80. package/dist/{chunk-XRNU7OEQ.js → chunk-ZNFVYUYF.js} +3 -3
  81. package/dist/{cli-5tNLVTvJ.d.ts → cli-DzPVFfGe.d.ts} +1 -1
  82. package/dist/cli.d.ts +5 -2
  83. package/dist/cli.js +45 -46
  84. package/dist/compounding/engine.js +5 -5
  85. package/dist/config.js +1 -1
  86. package/dist/connectors/codex-materialize-runner.js +5 -5
  87. package/dist/connectors/index.js +5 -5
  88. package/dist/contradiction/index.js +4 -4
  89. package/dist/dashboard-runtime.js +2 -2
  90. package/dist/entity-retrieval.js +5 -5
  91. package/dist/index.d.ts +406 -405
  92. package/dist/index.js +76 -78
  93. package/dist/lcm/index.js +3 -3
  94. package/dist/maintenance/memory-governance.js +5 -5
  95. package/dist/maintenance/rebuild-memory-lifecycle-ledger.js +5 -5
  96. package/dist/maintenance/rebuild-memory-projection.js +6 -6
  97. package/dist/mcp-memory-inspector-app.d.ts +4 -1
  98. package/dist/namespaces/migrate.js +11 -11
  99. package/dist/namespaces/search.js +5 -5
  100. package/dist/namespaces/storage.js +5 -5
  101. package/dist/offline-sync-impression-drain.js +4 -4
  102. package/dist/operator-toolkit.js +16 -16
  103. package/dist/orchestration/maintenance.js +9 -9
  104. package/dist/orchestrator.js +65 -66
  105. package/dist/qmd.js +4 -4
  106. package/dist/resume-bundles.js +1 -1
  107. package/dist/schemas.d.ts +54 -54
  108. package/dist/search/factory.js +4 -4
  109. package/dist/search/index.js +6 -6
  110. package/dist/semantic-consolidation.js +6 -6
  111. package/dist/semantic-rule-promotion.js +5 -5
  112. package/dist/semantic-rule-verifier.js +5 -5
  113. package/dist/shared-context/manager.d.ts +8 -8
  114. package/dist/storage.js +4 -4
  115. package/dist/transfer/backup.js +2 -2
  116. package/dist/transfer/capsule-export.js +2 -2
  117. package/dist/transfer/capsule-import.js +2 -2
  118. package/dist/transfer/import-sqlite.js +2 -2
  119. package/dist/transfer/types.d.ts +66 -66
  120. package/dist/verified-recall.js +5 -5
  121. package/package.json +2 -2
  122. package/src/access-http.test.ts +91 -0
  123. package/src/access-http.ts +13 -14
  124. package/src/access-namespace-preflight.test.ts +111 -0
  125. package/src/access-namespace-preflight.ts +111 -0
  126. package/src/access-operations-batch.ts +25 -2
  127. package/src/access-service-observe-scope.test.ts +81 -0
  128. package/src/access-service.ts +64 -32
  129. package/src/scopes/scope-plan.ts +66 -0
  130. package/dist/chunk-3HYWNJDA.js.map +0 -1
  131. package/dist/chunk-KF5EUVKQ.js +0 -289
  132. package/dist/chunk-KF5EUVKQ.js.map +0 -1
  133. package/dist/chunk-UDIJMITR.js.map +0 -1
  134. package/dist/chunk-YO3ZB3PJ.js.map +0 -1
  135. /package/dist/{capsule-crypto-GWVG7LGC.js.map → access-namespace-preflight.js.map} +0 -0
  136. /package/dist/{chunk-OWHERGF2.js.map → chunk-2NLLXCJG.js.map} +0 -0
  137. /package/dist/{chunk-5RZHHANR.js.map → chunk-63P3PJED.js.map} +0 -0
  138. /package/dist/{chunk-ALSGMEJR.js.map → chunk-7CPOZH6Y.js.map} +0 -0
  139. /package/dist/{chunk-TY6NBWV4.js.map → chunk-7FNHLCQE.js.map} +0 -0
  140. /package/dist/{chunk-HLG7VAZP.js.map → chunk-BOP256DB.js.map} +0 -0
  141. /package/dist/{chunk-7G7DVA2C.js.map → chunk-BVC242OS.js.map} +0 -0
  142. /package/dist/{chunk-CAZ2SHEP.js.map → chunk-CCNSCRJ2.js.map} +0 -0
  143. /package/dist/{chunk-3332THSR.js.map → chunk-CVZYPFEH.js.map} +0 -0
  144. /package/dist/{chunk-SK7AMF2J.js.map → chunk-DETNBTZS.js.map} +0 -0
  145. /package/dist/{chunk-226RNKRU.js.map → chunk-FFF4XIGB.js.map} +0 -0
  146. /package/dist/{chunk-AS6UOOXF.js.map → chunk-FMMZQDCT.js.map} +0 -0
  147. /package/dist/{chunk-YQQYS2K4.js.map → chunk-H3MDWWGN.js.map} +0 -0
  148. /package/dist/{chunk-TC5HBIIT.js.map → chunk-HBYQAOED.js.map} +0 -0
  149. /package/dist/{chunk-YUQOAKPX.js.map → chunk-JRXN5LYL.js.map} +0 -0
  150. /package/dist/{chunk-5W7CTZ2E.js.map → chunk-KQAFEZQX.js.map} +0 -0
  151. /package/dist/{chunk-PYHOAXTI.js.map → chunk-LRGSLV7M.js.map} +0 -0
  152. /package/dist/{chunk-A56B2AJR.js.map → chunk-MUQ3BD2C.js.map} +0 -0
  153. /package/dist/{chunk-QJAQNXWI.js.map → chunk-ONHRRJZM.js.map} +0 -0
  154. /package/dist/{chunk-DF552QUN.js.map → chunk-QCLQ4BGA.js.map} +0 -0
  155. /package/dist/{chunk-IAHNKI2E.js.map → chunk-QDNE35FU.js.map} +0 -0
  156. /package/dist/{chunk-W2OST2HH.js.map → chunk-QLOOCKKE.js.map} +0 -0
  157. /package/dist/{chunk-7HZMTXXB.js.map → chunk-RWSLO24I.js.map} +0 -0
  158. /package/dist/{chunk-QZGN4773.js.map → chunk-SW2KEZNX.js.map} +0 -0
  159. /package/dist/{chunk-O4K3DWK2.js.map → chunk-SWVW42CG.js.map} +0 -0
  160. /package/dist/{chunk-UVYI6VIX.js.map → chunk-X7Y7WX73.js.map} +0 -0
  161. /package/dist/{chunk-CQ6WP3PA.js.map → chunk-XII2XSHL.js.map} +0 -0
  162. /package/dist/{chunk-YSDVEPV2.js.map → chunk-XKFXY3ZA.js.map} +0 -0
  163. /package/dist/{chunk-AVPXXV6J.js.map → chunk-XKGPIWGI.js.map} +0 -0
  164. /package/dist/{chunk-IQFS4AU7.js.map → chunk-YEU4VEU4.js.map} +0 -0
  165. /package/dist/{chunk-EZ25VE3G.js.map → chunk-YNDLCWXS.js.map} +0 -0
  166. /package/dist/{chunk-Y6XODVJ4.js.map → chunk-YS7KIG3A.js.map} +0 -0
  167. /package/dist/{chunk-XRNU7OEQ.js.map → chunk-ZNFVYUYF.js.map} +0 -0
@@ -0,0 +1,111 @@
1
+ import assert from "node:assert/strict";
2
+ import test from "node:test";
3
+
4
+ import "./access-operations.js";
5
+ import { getOperation } from "./access-boundary.js";
6
+ import { tokenCapabilityStore, type TokenCapabilities } from "./access-token-capabilities.js";
7
+ import {
8
+ assertNamespacePreflightPermitted,
9
+ resolveAuthorizedNamespaceWritablePreflight,
10
+ resolveQueryNamespaceWritablePreflight,
11
+ type EngramAccessNamespaceWritableRequest,
12
+ } from "./access-namespace-preflight.js";
13
+ import type { EngramAccessService } from "./access-service.js";
14
+
15
+ test("namespace_writable forwards coding context through the batch boundary", async () => {
16
+ let received: EngramAccessNamespaceWritableRequest | undefined;
17
+ const service = {
18
+ configRef: { defaultNamespace: "default" },
19
+ namespaceWritablePreflight: async (request: EngramAccessNamespaceWritableRequest) => {
20
+ received = request;
21
+ return { ok: true as const, namespace: "principal-project" };
22
+ },
23
+ } as unknown as EngramAccessService;
24
+ const operation = getOperation("namespace_writable");
25
+ assert.ok(operation);
26
+
27
+ const caps: TokenCapabilities = { version: 1, ops: ["observe"] };
28
+ const output = await tokenCapabilityStore.run(caps, () =>
29
+ operation.run(
30
+ {
31
+ sessionKey: "session-1",
32
+ cwd: "/workspace/project",
33
+ projectTag: "project-tag",
34
+ },
35
+ { service, authenticatedPrincipal: "principal" },
36
+ ),
37
+ );
38
+
39
+ assert.deepEqual(output, { result: { ok: true, namespace: "principal-project" } });
40
+ assert.deepEqual(received, {
41
+ sessionKey: "session-1",
42
+ authenticatedPrincipal: "principal",
43
+ cwd: "/workspace/project",
44
+ projectTag: "project-tag",
45
+ });
46
+ });
47
+
48
+ test("namespace_writable rejects unsupported write operations", async () => {
49
+ const operation = getOperation("namespace_writable");
50
+ assert.ok(operation);
51
+ const service = {
52
+ configRef: { defaultNamespace: "default" },
53
+ namespaceWritablePreflight: async () => ({ ok: true as const, namespace: "default" }),
54
+ } as unknown as EngramAccessService;
55
+
56
+ await assert.rejects(
57
+ () =>
58
+ tokenCapabilityStore.run({ version: 1, ops: ["observe"] }, () =>
59
+ operation.run({ op: "memory_stroe" }, { service }),
60
+ ),
61
+ /unsupported namespace preflight operation: memory_stroe/,
62
+ );
63
+ assert.throws(
64
+ () =>
65
+ resolveQueryNamespaceWritablePreflight(
66
+ { version: 1, ops: ["observe"] },
67
+ new URLSearchParams({ op: "memory_stroe" }),
68
+ undefined,
69
+ "default",
70
+ async () => ({ ok: true, namespace: "default" }),
71
+ ),
72
+ /unsupported namespace preflight operation: memory_stroe/,
73
+ );
74
+ });
75
+
76
+ test("namespace_writable rejects unavailable preflight and denied write scope", async () => {
77
+ assert.throws(
78
+ () => assertNamespacePreflightPermitted({ version: 1, ops: ["memory_get"] }),
79
+ /token is not permitted to run the namespace preflight/,
80
+ );
81
+ assert.deepEqual(
82
+ await resolveAuthorizedNamespaceWritablePreflight(
83
+ { version: 1, ops: ["observe"] },
84
+ { namespace: "default" },
85
+ "default",
86
+ "memory_store",
87
+ async () => ({ ok: true, namespace: "default" }),
88
+ ),
89
+ { ok: false, reason: "not_writable", namespace: "default" },
90
+ );
91
+ assert.deepEqual(
92
+ await resolveAuthorizedNamespaceWritablePreflight(
93
+ { version: 1, ops: ["observe"], namespaces: ["team-a"] },
94
+ {},
95
+ "default",
96
+ "observe",
97
+ async () => ({ ok: true, namespace: "default" }),
98
+ ),
99
+ { ok: false, reason: "not_writable", namespace: "default" },
100
+ );
101
+ assert.deepEqual(
102
+ await resolveAuthorizedNamespaceWritablePreflight(
103
+ { version: 1, ops: ["observe"], namespaces: ["team-a"] },
104
+ { namespace: "outside-scope" },
105
+ "default",
106
+ "observe",
107
+ async () => ({ ok: false, reason: "unsupported", namespace: "outside-scope" }),
108
+ ),
109
+ { ok: false, reason: "not_writable", namespace: "outside-scope" },
110
+ );
111
+ });
@@ -0,0 +1,111 @@
1
+ import { getOperation } from "./access-boundary.js";
2
+ import { capabilityAllowsOp, isNamespaceAllowed, type TokenCapabilities } from "./access-token-capabilities.js";
3
+ import { EngramAccessForbiddenError, EngramAccessInputError, NamespaceNotWritableError } from "./access-errors.js";
4
+ import type { WritableNamespaceResult } from "./scopes/scope-plan.js";
5
+
6
+ export interface EngramAccessNamespaceWritableRequest {
7
+ namespace?: string;
8
+ sessionKey?: string;
9
+ authenticatedPrincipal?: string;
10
+ cwd?: string;
11
+ projectTag?: string;
12
+ }
13
+
14
+ export function namespaceWritableRequest(
15
+ params: URLSearchParams,
16
+ authenticatedPrincipal?: string,
17
+ ): EngramAccessNamespaceWritableRequest {
18
+ const namespace = params.get("namespace") || undefined;
19
+ return {
20
+ namespace,
21
+ sessionKey: params.get("session") || undefined,
22
+ authenticatedPrincipal,
23
+ cwd: params.get("cwd") || undefined,
24
+ projectTag: params.get("projectTag") || undefined,
25
+ };
26
+ }
27
+
28
+ export function parseNamespacePreflightWriteOp(
29
+ value: string | undefined,
30
+ ): "observe" | "memory_store" {
31
+ if (value === undefined || value === "observe") return "observe";
32
+ if (value === "memory_store") return "memory_store";
33
+ throw new EngramAccessInputError(`unsupported namespace preflight operation: ${value}`);
34
+ }
35
+
36
+ export function assertNamespacePreflightPermitted(
37
+ caps: TokenCapabilities | undefined | null,
38
+ ): void {
39
+ const allowedOps = getOperation("namespace_writable")?.spec.allowedByOps ?? [];
40
+ if (!allowedOps.some((op) => capabilityAllowsOp(caps, op))) {
41
+ throw new EngramAccessForbiddenError("token is not permitted to run the namespace preflight");
42
+ }
43
+ }
44
+
45
+ export type NamespaceWritableResolver = (
46
+ request: EngramAccessNamespaceWritableRequest,
47
+ ) => Promise<string>;
48
+ export type NamespaceWritablePreflightResolver = (
49
+ request: EngramAccessNamespaceWritableRequest,
50
+ ) => Promise<WritableNamespaceResult>;
51
+
52
+ export async function resolveNamespaceWritablePreflight(
53
+ request: EngramAccessNamespaceWritableRequest,
54
+ resolveWritable: NamespaceWritableResolver,
55
+ ): Promise<WritableNamespaceResult> {
56
+ try {
57
+ return { ok: true, namespace: await resolveWritable(request) };
58
+ } catch (error) {
59
+ if (error instanceof NamespaceNotWritableError) {
60
+ return { ok: false, reason: "not_writable", namespace: error.attemptedNamespace };
61
+ }
62
+ if (error instanceof EngramAccessInputError) {
63
+ return {
64
+ ok: false,
65
+ reason: "unsupported",
66
+ namespace: request.namespace?.trim() ?? "",
67
+ };
68
+ }
69
+ throw error;
70
+ }
71
+ }
72
+
73
+ export async function resolveAuthorizedNamespaceWritablePreflight(
74
+ caps: TokenCapabilities | undefined | null,
75
+ request: EngramAccessNamespaceWritableRequest,
76
+ defaultNamespace: string,
77
+ writeOp: "observe" | "memory_store",
78
+ resolvePreflight: NamespaceWritablePreflightResolver,
79
+ ): Promise<WritableNamespaceResult> {
80
+ if (!capabilityAllowsOp(caps, writeOp)) {
81
+ return {
82
+ ok: false,
83
+ reason: "not_writable",
84
+ namespace: request.namespace?.trim() || defaultNamespace,
85
+ };
86
+ }
87
+
88
+ const result = await resolvePreflight(request);
89
+ if (!isNamespaceAllowed(caps, result.namespace, defaultNamespace)) {
90
+
91
+ return { ok: false, reason: "not_writable", namespace: result.namespace };
92
+ }
93
+ return result;
94
+ }
95
+
96
+ export function resolveQueryNamespaceWritablePreflight(
97
+ caps: TokenCapabilities | undefined | null,
98
+ params: URLSearchParams,
99
+ authenticatedPrincipal: string | undefined,
100
+ defaultNamespace: string,
101
+ resolvePreflight: NamespaceWritablePreflightResolver,
102
+ ): Promise<WritableNamespaceResult> {
103
+ assertNamespacePreflightPermitted(caps);
104
+ return resolveAuthorizedNamespaceWritablePreflight(
105
+ caps,
106
+ namespaceWritableRequest(params, authenticatedPrincipal),
107
+ defaultNamespace,
108
+ parseNamespacePreflightWriteOp(params.get("op") ?? undefined),
109
+ resolvePreflight,
110
+ );
111
+ }
@@ -18,7 +18,11 @@ import { EngramAccessForbiddenError } from "./access-errors.js";
18
18
  import { EngramAccessInputError, type EngramAccessService } from "./access-service.js";
19
19
  import { enforceNamespaceAllowList, tokenCapabilityStore } from "./access-token-capabilities.js";
20
20
  import { projectTagProjectId } from "./coding/coding-namespace.js";
21
- import { resolveNamespacePreflight } from "./scopes/scope-plan.js";
21
+ import {
22
+ parseNamespacePreflightWriteOp,
23
+ resolveAuthorizedNamespaceWritablePreflight,
24
+ type EngramAccessNamespaceWritableRequest,
25
+ } from "./access-namespace-preflight.js";
22
26
  import { expandTildePath } from "./utils/path.js";
23
27
  import { resolvePrincipal } from "./namespaces/principal.js";
24
28
  import { getRecallTimingStatus, isRecallTimingsOperator } from "./recall-timings.js";
@@ -114,7 +118,26 @@ defineOperation({ name: "recall_xray", description: "X-ray recall.", schema: str
114
118
  return { result: await ctx.service.recallXray({ query: defStr(input.query, ""), sessionKey: optStr(input.sessionKey), namespace: optStr(input.namespace), budget, authenticatedPrincipal: ctx.authenticatedPrincipal, ...(dr && dr !== "" ? { disclosure: dr as RecallDisclosure } : {}), ...(ctx.abortSignal ? { abortSignal: ctx.abortSignal } : {}) }) };
115
119
  },
116
120
  });
117
- defineOperation({ name: "namespace_writable", description: "Read-only preflight: is a namespace writable for the caller's principal?", allowedByOps: ["namespace_writable", "observe", "memory_store"], schema: strictSchema({ namespace: S.str, sessionKey: S.str, op: S.str }), handler: async (input, ctx) => { const ns = optStr(input.namespace); const sk = optStr(input.sessionKey); const writeOp = optStr(input.op) === "memory_store" ? "memory_store" : "observe"; return { result: resolveNamespacePreflight(tokenCapabilityStore.getStore(), ns && ns.length > 0 ? ns : undefined, sk && sk.length > 0 ? sk : undefined, ctx.authenticatedPrincipal, ctx.service.configRef, writeOp) }; } });
121
+ defineOperation({ name: "namespace_writable", description: "Read-only preflight: is a namespace writable for the caller's principal?", allowedByOps: ["namespace_writable", "observe", "memory_store"], schema: strictSchema({ namespace: S.str, sessionKey: S.str, op: S.str, cwd: S.str, projectTag: S.str }), handler: async (input, ctx) => {
122
+ const writeOp = parseNamespacePreflightWriteOp(optStr(input.op));
123
+ const namespace = optStr(input.namespace);
124
+ const request: EngramAccessNamespaceWritableRequest = {
125
+ sessionKey: optStr(input.sessionKey),
126
+ authenticatedPrincipal: ctx.authenticatedPrincipal,
127
+ cwd: optStr(input.cwd),
128
+ projectTag: optStr(input.projectTag),
129
+ ...(namespace ? { namespace } : {}),
130
+ };
131
+ return {
132
+ result: await resolveAuthorizedNamespaceWritablePreflight(
133
+ tokenCapabilityStore.getStore(),
134
+ request,
135
+ ctx.service.configRef.defaultNamespace,
136
+ writeOp,
137
+ (preflightRequest) => ctx.service.namespaceWritablePreflight(preflightRequest),
138
+ ),
139
+ };
140
+ } });
118
141
 
119
142
  // === WEARABLES ===
120
143
  defineOperation({ name: "wearables_status", description: "Wearables status.", schema: strictSchema({}), handler: async (_i, ctx) => ({ result: await ctx.service.wearablesStatus() }) });
@@ -33,6 +33,8 @@ import { join } from "node:path";
33
33
  import test from "node:test";
34
34
 
35
35
  import { EngramAccessService } from "./access-service.js";
36
+ import { tokenCapabilityStore } from "./access-token-capabilities.js";
37
+ import { resolveAuthorizedNamespaceWritablePreflight } from "./access-namespace-preflight.js";
36
38
  import { Orchestrator } from "./orchestrator.js";
37
39
  import type { StorageManager } from "./storage.js";
38
40
  import type { EngramAccessObserveRequest } from "./access-service.js";
@@ -604,6 +606,85 @@ test("#1505 (cursor hAp) scopeDebug.baseNamespace reports the principal self bas
604
606
  );
605
607
  });
606
608
 
609
+ test("#2080 preflight resolves an implicit project scope through the observe write resolver", async () => {
610
+ const probe = makeObserveProbe({
611
+ ...withSelfPolicyPrefix("pi-geek"),
612
+ namespacePolicies: [
613
+ { name: "default", readPrincipals: ["admin"], writePrincipals: ["admin"] },
614
+ { name: "pi-geek", readPrincipals: ["pi-geek"], writePrincipals: ["pi-geek"] },
615
+ ],
616
+ });
617
+ const service = new EngramAccessService(probe.orch);
618
+ const expected = combineNamespaces(
619
+ "pi-geek",
620
+ projectNamespaceName(projectTagProjectId("Acme/Webshop")),
621
+ );
622
+
623
+ const result = await (
624
+ service as unknown as {
625
+ namespaceWritablePreflight: (request: {
626
+ sessionKey: string;
627
+ authenticatedPrincipal: string;
628
+ projectTag: string;
629
+ }) => Promise<{ ok: boolean; namespace: string }>;
630
+ }
631
+ ).namespaceWritablePreflight({
632
+ sessionKey: "pi-geek:abc123",
633
+ authenticatedPrincipal: "pi-geek",
634
+ projectTag: "Acme/Webshop",
635
+ });
636
+
637
+ assert.deepEqual(result, { ok: true, namespace: expected });
638
+ await assert.rejects(
639
+ () =>
640
+ tokenCapabilityStore.run({ version: 1, namespaces: ["default"] }, () =>
641
+ (
642
+ service as unknown as {
643
+ resolveCodingScopedWriteNamespace: (request: {
644
+ sessionKey: string;
645
+ authenticatedPrincipal: string;
646
+ projectTag: string;
647
+ }) => Promise<string>;
648
+ }
649
+ ).resolveCodingScopedWriteNamespace({
650
+ sessionKey: "pi-geek:abc123",
651
+ authenticatedPrincipal: "pi-geek",
652
+ projectTag: "Acme/Webshop",
653
+ }),
654
+ ),
655
+ /not permitted/,
656
+ );
657
+ const deniedPreflight = await tokenCapabilityStore.run(
658
+ { version: 1, ops: ["observe"], namespaces: ["default"] },
659
+ () =>
660
+ resolveAuthorizedNamespaceWritablePreflight(
661
+ tokenCapabilityStore.getStore(),
662
+ {
663
+ sessionKey: "pi-geek:abc123",
664
+ authenticatedPrincipal: "pi-geek",
665
+ projectTag: "Acme/Webshop",
666
+ },
667
+ "default",
668
+ "observe",
669
+ (request) => service.namespaceWritablePreflight(request),
670
+ ),
671
+ );
672
+ assert.deepEqual(deniedPreflight, { ok: false, reason: "not_writable", namespace: expected });
673
+ await assert.rejects(
674
+ () =>
675
+ tokenCapabilityStore.run({ version: 1, namespaces: ["default"] }, () =>
676
+ service.observe(
677
+ observeRequest({
678
+ sessionKey: "pi-geek:abc123",
679
+ authenticatedPrincipal: "pi-geek",
680
+ projectTag: "Acme/Webshop",
681
+ }),
682
+ ),
683
+ ),
684
+ /not permitted/,
685
+ );
686
+ });
687
+
607
688
  test("#1495 the scope plan's writeNamespace matches resolveCodingScopedWriteNamespace (memory_store / suggestion_submit parity, rule 39)", async () => {
608
689
  // Regression guard: observe's effective scope MUST be identical to what the
609
690
  // explicit-write tools (memory_store / suggestion_submit) resolve via
@@ -6,6 +6,7 @@ import { readdirSync, unlinkSync } from "node:fs";
6
6
  import { createHash } from "node:crypto";
7
7
  import { ZodError } from "zod";
8
8
  import { AccessIdempotencyStore, hashAccessIdempotencyPayload } from "./access-idempotency.js";
9
+ import { enforceNamespaceAllowList, tokenCapabilityStore } from "./access-token-capabilities.js";
9
10
  import {
10
11
  recordCitationUsage as recordCitationUsageForAccess,
11
12
  type CitationUsageRequest,
@@ -130,8 +131,11 @@ import {
130
131
  import {
131
132
  type ScopePlan,
132
133
  resolveScopePlan,
134
+ resolveScopedWritableNamespaceValue,
133
135
  resolveWritableNamespaceValue,
136
+ type WritableNamespaceResult,
134
137
  } from "./scopes/scope-plan.js";
138
+ import { resolveNamespaceWritablePreflight, type EngramAccessNamespaceWritableRequest } from "./access-namespace-preflight.js";
135
139
  import { namespaceIdentityFromToken } from "./namespaces/identity.js";
136
140
  import { namespaceCollectionName } from "./namespaces/search.js";
137
141
  import { SecureStoreLockedError } from "./secure-store/index.js";
@@ -958,7 +962,7 @@ export interface CodingScopedWriteInput {
958
962
  cwd?: string;
959
963
  projectTag?: string;
960
964
  }
961
-
965
+ export type { EngramAccessNamespaceWritableRequest } from "./access-namespace-preflight.js";
962
966
  /**
963
967
  * Internal, single-resolution plan describing the effective memory scope for a
964
968
  * write-producing access request (#1495, seed for epic #1494). One plan is
@@ -1588,6 +1592,14 @@ export class EngramAccessService {
1588
1592
  return { principal, codingContext, overlay, profilePlan };
1589
1593
  }
1590
1594
 
1595
+ private assertTokenCanWriteNamespace(namespace: string): void {
1596
+ enforceNamespaceAllowList(
1597
+ tokenCapabilityStore.getStore(),
1598
+ namespace,
1599
+ this.orchestrator.config.defaultNamespace,
1600
+ );
1601
+ }
1602
+
1591
1603
  /**
1592
1604
  * Resolve the write namespace for explicit-write tools (memory_store /
1593
1605
  * suggestion_submit), project-scoping the write the same way recall does so a
@@ -1603,49 +1615,56 @@ export class EngramAccessService {
1603
1615
  sessionKey?: string;
1604
1616
  authenticatedPrincipal?: string;
1605
1617
  },
1618
+ enforceToken: boolean = true,
1606
1619
  ): Promise<string> {
1607
- const hasExplicitNamespace =
1608
- typeof request.namespace === "string" && request.namespace.trim().length > 0;
1609
- if (hasExplicitNamespace) {
1610
- return this.writableNamespaceFor(
1611
- request.namespace,
1620
+ const requested = request.namespace?.trim();
1621
+ if (requested) {
1622
+ const namespace = this.writableNamespaceFor(
1623
+ requested,
1612
1624
  request.sessionKey,
1613
1625
  request.authenticatedPrincipal,
1614
1626
  );
1627
+ if (enforceToken) this.assertTokenCanWriteNamespace(namespace);
1628
+ return namespace;
1615
1629
  }
1630
+
1616
1631
  const { principal, overlay, profilePlan } = await this.resolveCodingScopeInputs(request);
1617
- if (profilePlan) {
1618
- const selectedLayer = profilePlan.layers.find((layer) => layer.id === profilePlan.writeLayer);
1619
- const writeNamespaceReadable =
1620
- profilePlan.writeNamespace.length > 0 &&
1621
- profilePlan.readNamespaces.includes(profilePlan.writeNamespace);
1622
- if (!selectedLayer?.writable || !writeNamespaceReadable) {
1623
- throw new NamespaceNotWritableError(profilePlan.writeNamespace, principal,
1624
- `scope profile ${profilePlan.profileId} has no writable layer for principal ${principal ?? "anonymous"}`);
1625
- }
1626
- return profilePlan.writeNamespace;
1632
+ const result = resolveScopedWritableNamespaceValue({
1633
+ sessionKey: request.sessionKey,
1634
+ authenticatedPrincipal: request.authenticatedPrincipal,
1635
+ principal,
1636
+ codingOverlay: overlay,
1637
+ scopeProfile: profilePlan,
1638
+ config: this.orchestrator.config,
1639
+ });
1640
+ if (result.ok) {
1641
+ if (enforceToken) this.assertTokenCanWriteNamespace(result.namespace);
1642
+ return result.namespace;
1627
1643
  }
1628
- if (!overlay) {
1629
- // No coding overlay → unqualified write stays on config.defaultNamespace,
1630
- // exactly the pre-#1434 behavior (auth-checked, like the legacy path).
1631
- return this.writableNamespaceFor(
1632
- undefined,
1633
- request.sessionKey,
1634
- request.authenticatedPrincipal,
1644
+ if (profilePlan && result.namespace === profilePlan.writeNamespace) {
1645
+ throw new NamespaceNotWritableError(
1646
+ result.namespace,
1647
+ principal,
1648
+ `scope profile ${profilePlan.profileId} has no writable layer for principal ${principal ?? "anonymous"}`,
1635
1649
  );
1636
1650
  }
1637
- // Coding overlay → overlay onto the principal self base, the SAME namespace
1638
- // recall/observe/buffer-flush use. The result is a principal-owned
1639
- // `project-*` sub-namespace derived from this authorized base, so it needs
1640
- // no separate write policy.
1641
- const base = defaultNamespaceForPrincipal(principal, this.orchestrator.config);
1642
- if (!canWriteNamespace(principal, base, this.orchestrator.config)) {
1643
- throw new NamespaceNotWritableError(base, principal);
1651
+
1652
+ if (result.reason === "unsupported") {
1653
+ throw new EngramAccessInputError(`unsupported namespace: ${result.namespace}`);
1644
1654
  }
1645
- return combineNamespaces(base, overlay.namespace);
1655
+ throw new NamespaceNotWritableError(result.namespace, principal);
1656
+ }
1657
+
1658
+ async namespaceWritablePreflight(
1659
+ request: EngramAccessNamespaceWritableRequest,
1660
+ ): Promise<WritableNamespaceResult> {
1661
+ return resolveNamespaceWritablePreflight(
1662
+ request,
1663
+ (preflightRequest) => this.resolveCodingScopedWriteNamespace(preflightRequest, false),
1664
+ );
1646
1665
  }
1647
1666
 
1648
- /** Read-side mirror of {@link resolveCodingScopedWriteNamespace}. Decision
1667
+ /**
1649
1668
  * `list`/`get` use this so a record written by a project-scoped session is
1650
1669
  * listable/fetchable by the SAME session without manually supplying the
1651
1670
  * overlaid namespace (review P2). Derivation is IDENTICAL to the write path
@@ -1748,6 +1767,7 @@ export class EngramAccessService {
1748
1767
  request.sessionKey,
1749
1768
  request.authenticatedPrincipal,
1750
1769
  );
1770
+ this.assertTokenCanWriteNamespace(writeNamespace);
1751
1771
  return {
1752
1772
  principal,
1753
1773
  explicitNamespace: request.namespace!.trim(),
@@ -1825,6 +1845,15 @@ export class EngramAccessService {
1825
1845
  this.orchestrator.setCodingContextForSession(request.sessionKey!, null);
1826
1846
  }
1827
1847
  };
1848
+ const assertWriteNamespaceAllowed = (namespace: string): void => {
1849
+ try {
1850
+ this.assertTokenCanWriteNamespace(namespace);
1851
+ } catch (error) {
1852
+ clearSeededContext();
1853
+ throw error;
1854
+ }
1855
+ };
1856
+
1828
1857
 
1829
1858
  const overlaidBase = this.orchestrator.applyCodingNamespaceOverlay(
1830
1859
  request.sessionKey,
@@ -1876,6 +1905,7 @@ export class EngramAccessService {
1876
1905
  readNamespaces.includes(layer.namespace),
1877
1906
  ),
1878
1907
  );
1908
+ assertWriteNamespaceAllowed(profilePlan.writeNamespace);
1879
1909
  return {
1880
1910
  principal,
1881
1911
  baseNamespace: profilePlan.baseNamespace,
@@ -1928,6 +1958,7 @@ export class EngramAccessService {
1928
1958
  clearSeededContext();
1929
1959
  throw new NamespaceNotWritableError(baseNamespace, principal);
1930
1960
  }
1961
+ assertWriteNamespaceAllowed(writeNamespace);
1931
1962
  return {
1932
1963
  principal,
1933
1964
  // scopeDebug.baseNamespace must report the principal SELF base
@@ -1965,6 +1996,7 @@ export class EngramAccessService {
1965
1996
  const ns = combineNamespaces(baseNamespace, fallback);
1966
1997
  if (!readNamespaces.includes(ns)) readNamespaces.push(ns);
1967
1998
  }
1999
+ assertWriteNamespaceAllowed(writeNamespace);
1968
2000
  return {
1969
2001
  principal,
1970
2002
  baseNamespace,
@@ -489,6 +489,72 @@ export function resolveWritableNamespaceValue(
489
489
  return { ok: true, namespace: resolved };
490
490
  }
491
491
 
492
+ /**
493
+ * Inputs already derived by the access-service's read-only coding-context
494
+ * resolver. Keeping context discovery outside this pure function lets writes
495
+ * and preflight checks apply identical namespace and ACL rules.
496
+ */
497
+ export interface ScopedWritableNamespaceInput {
498
+ readonly namespace?: string;
499
+ readonly sessionKey?: string;
500
+ readonly authenticatedPrincipal?: string;
501
+ readonly principal: string | undefined;
502
+ readonly codingOverlay: CodingNamespaceOverlay | null;
503
+ readonly scopeProfile: ResolvedScopeProfilePlan | null;
504
+ readonly config: PluginConfig;
505
+ }
506
+
507
+ /**
508
+ * Resolve the namespace an explicit write or implicit coding-scoped write
509
+ * would use. The caller supplies the same derived coding inputs for the real
510
+ * write and the read-only preflight.
511
+ */
512
+ export function resolveScopedWritableNamespaceValue(
513
+ input: ScopedWritableNamespaceInput,
514
+ ): WritableNamespaceResult {
515
+ const requested = input.namespace?.trim();
516
+ if (requested) {
517
+ return resolveWritableNamespaceValue(
518
+ requested,
519
+ input.sessionKey,
520
+ input.authenticatedPrincipal,
521
+ input.config,
522
+ );
523
+ }
524
+ if (input.scopeProfile) {
525
+ const selectedLayer = input.scopeProfile.layers.find(
526
+ (layer) => layer.id === input.scopeProfile?.writeLayer,
527
+ );
528
+ const writeNamespaceReadable =
529
+ input.scopeProfile.writeNamespace.length > 0 &&
530
+ input.scopeProfile.readNamespaces.includes(input.scopeProfile.writeNamespace);
531
+ if (!selectedLayer?.writable || !writeNamespaceReadable) {
532
+ return {
533
+ ok: false,
534
+ reason: "not_writable",
535
+ namespace: input.scopeProfile.writeNamespace,
536
+ };
537
+ }
538
+ return { ok: true, namespace: input.scopeProfile.writeNamespace };
539
+ }
540
+ if (!input.codingOverlay) {
541
+ return resolveWritableNamespaceValue(
542
+ undefined,
543
+ input.sessionKey,
544
+ input.authenticatedPrincipal,
545
+ input.config,
546
+ );
547
+ }
548
+ const baseNamespace = defaultNamespaceForPrincipal(input.principal, input.config);
549
+ if (!canWriteNamespace(input.principal, baseNamespace, input.config)) {
550
+ return { ok: false, reason: "not_writable", namespace: baseNamespace };
551
+ }
552
+ return {
553
+ ok: true,
554
+ namespace: combineNamespaces(baseNamespace, input.codingOverlay.namespace),
555
+ };
556
+ }
557
+
492
558
  /**
493
559
  * Namespace preflight (issue #1888 part 3): would a write to `namespace` by a
494
560
  * caller carrying `caps` succeed? Combines the token's write-op scope, its