@remnic/core 9.3.768 → 9.3.769

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 (37) hide show
  1. package/dist/access-cli.js +5 -5
  2. package/dist/access-http.d.ts +51 -1
  3. package/dist/access-http.js +2 -2
  4. package/dist/access-mcp.d.ts +7 -1
  5. package/dist/access-mcp.js +5 -3
  6. package/dist/access-operations.d.ts +4 -4
  7. package/dist/access-schema.d.ts +68 -68
  8. package/dist/{chunk-TVLN5EZZ.js → chunk-CNXMWYLA.js} +78 -3
  9. package/dist/chunk-CNXMWYLA.js.map +1 -0
  10. package/dist/{chunk-UG274TNV.js → chunk-E2SPGGUI.js} +13 -8
  11. package/dist/chunk-E2SPGGUI.js.map +1 -0
  12. package/dist/{chunk-HBOPSFQQ.js → chunk-EVXI2I6G.js} +3 -3
  13. package/dist/{chunk-4DLFJJOQ.js → chunk-QMN3CIFS.js} +2 -2
  14. package/dist/{chunk-J3UJJZKI.js → chunk-SIPZ5UMK.js} +85 -5
  15. package/dist/chunk-SIPZ5UMK.js.map +1 -0
  16. package/dist/cli.js +3 -3
  17. package/dist/connectors/index.js +2 -2
  18. package/dist/index.d.ts +1 -1
  19. package/dist/index.js +11 -5
  20. package/dist/orchestrator.js +5 -5
  21. package/dist/schemas.d.ts +84 -84
  22. package/dist/shared-context/manager.d.ts +8 -8
  23. package/dist/tokens.d.ts +3 -1
  24. package/dist/tokens.js +3 -1
  25. package/dist/transfer/types.d.ts +66 -66
  26. package/package.json +2 -2
  27. package/src/access-http.test.ts +350 -0
  28. package/src/access-http.ts +160 -6
  29. package/src/access-mcp.ts +120 -3
  30. package/src/index.ts +3 -0
  31. package/src/tokens.test.ts +58 -0
  32. package/src/tokens.ts +19 -8
  33. package/dist/chunk-J3UJJZKI.js.map +0 -1
  34. package/dist/chunk-TVLN5EZZ.js.map +0 -1
  35. package/dist/chunk-UG274TNV.js.map +0 -1
  36. /package/dist/{chunk-HBOPSFQQ.js.map → chunk-EVXI2I6G.js.map} +0 -0
  37. /package/dist/{chunk-4DLFJJOQ.js.map → chunk-QMN3CIFS.js.map} +0 -0
package/src/access-mcp.ts CHANGED
@@ -78,7 +78,79 @@ type McpResource = {
78
78
  _meta?: Record<string, unknown>;
79
79
  };
80
80
 
81
- const MCP_PROTOCOL_VERSION = "2024-11-05";
81
+ /**
82
+ * Conservative allowlist of canonical MCP tool suffixes that are
83
+ * unambiguously read-only. Tools in this set are tagged with
84
+ * `annotations: { readOnlyHint: true }` so ChatGPT (and other MCP
85
+ * clients that honor the hint) can skip per-call confirmation.
86
+ *
87
+ * The list is suffix-based so it covers both the `remnic.*` and
88
+ * `engram.*` naming forms. Anything not on it stays unannotated:
89
+ * uncertainty is resolved as "might mutate".
90
+ *
91
+ * Excluded by construction: anything that writes, runs a pipeline,
92
+ * flushes, applies, records, imports, or destructively deletes.
93
+ */
94
+ const MCP_READ_ONLY_TOOL_SUFFIXES: Readonly<Record<string, true>> = {
95
+ recall: true,
96
+ recall_explain: true,
97
+ recall_tier_explain: true,
98
+ recall_xray: true,
99
+ briefing: true,
100
+ wearables_status: true,
101
+ transcript_day: true,
102
+ transcript_search: true,
103
+ transcript_memories: true,
104
+ action_confidence: true,
105
+ capsule_list: true,
106
+ procedural_stats: true,
107
+ memory_get: true,
108
+ memory_timeline: true,
109
+ entity_get: true,
110
+ review_queue_list: true,
111
+ lcm_search: true,
112
+ continuity_incident_list: true,
113
+ identity_anchor_get: true,
114
+ memory_identity: true,
115
+ memory_search: true,
116
+ memory_profile: true,
117
+ memory_entities_list: true,
118
+ memory_questions: true,
119
+ memory_last_recall: true,
120
+ memory_intent_debug: true,
121
+ memory_qmd_debug: true,
122
+ memory_graph_explain: true,
123
+ graph_snapshot: true,
124
+ review_list: true,
125
+ profiling_report: true,
126
+ peer_list: true,
127
+ peer_get: true,
128
+ peer_profile_get: true,
129
+ console_state: true,
130
+ dreams_status: true,
131
+ codegraph_list_projects: true,
132
+ codegraph_index_status: true,
133
+ codegraph_search_graph: true,
134
+ codegraph_trace_path: true,
135
+ codegraph_detect_changes: true,
136
+ codegraph_query_graph: true,
137
+ codegraph_get_schema: true,
138
+ codegraph_get_snippet: true,
139
+ codegraph_get_architecture: true,
140
+ codegraph_search_code: true,
141
+ };
142
+
143
+ /**
144
+ * MCP protocol versions this server understands, ordered newest → oldest.
145
+ * Exported so the HTTP transport can validate the `MCP-Protocol-Version`
146
+ * header and reject requests advertising an unknown version.
147
+ */
148
+ export const MCP_SUPPORTED_PROTOCOL_VERSIONS: readonly string[] = [
149
+ "2025-06-18",
150
+ "2025-03-26",
151
+ "2024-11-05",
152
+ ];
153
+ const MCP_DEFAULT_PROTOCOL_VERSION: string = MCP_SUPPORTED_PROTOCOL_VERSIONS[0] ?? "2025-06-18";
82
154
  const LEGACY_MCP_PREFIX = "engram.";
83
155
  const CANONICAL_MCP_PREFIX = "remnic.";
84
156
 
@@ -87,13 +159,27 @@ function toCanonicalToolName(name: string): string {
87
159
  ? `${CANONICAL_MCP_PREFIX}${name.slice(LEGACY_MCP_PREFIX.length)}`
88
160
  : name;
89
161
  }
90
-
91
162
  function toLegacyToolName(name: string): string {
92
163
  return name.startsWith(CANONICAL_MCP_PREFIX)
93
164
  ? `${LEGACY_MCP_PREFIX}${name.slice(CANONICAL_MCP_PREFIX.length)}`
94
165
  : name;
95
166
  }
96
167
 
168
+ /**
169
+ * Suffix-based allowlist matcher. Returns true for tools whose canonical
170
+ * suffix (after stripping `remnic.` or `engram.`) is in
171
+ * {@link MCP_READ_ONLY_TOOL_SUFFIXES}. Unprefixed names are treated as
172
+ * unannotated.
173
+ */
174
+ function isReadOnlyToolName(name: string): boolean {
175
+ for (const prefix of [CANONICAL_MCP_PREFIX, LEGACY_MCP_PREFIX]) {
176
+ if (name.startsWith(prefix)) {
177
+ return MCP_READ_ONLY_TOOL_SUFFIXES[name.slice(prefix.length)] === true;
178
+ }
179
+ }
180
+ return false;
181
+ }
182
+
97
183
  function withToolAliases(tool: McpTool, emitLegacyTools = true): McpTool[] {
98
184
  const canonicalName = toCanonicalToolName(tool.name);
99
185
  const canonicalTool = canonicalName === tool.name ? tool : { ...tool, name: canonicalName };
@@ -2342,6 +2428,17 @@ export class EngramMcpServer {
2342
2428
  );
2343
2429
  this.tools = [...this.tools, ...chatTools];
2344
2430
  }
2431
+ // Apply `readOnlyHint` annotations to the conservative read-only
2432
+ // allowlist. Done as a final pass so every spread (chat, codegraph,
2433
+ // coding_*, correction, etc.) inherits the annotation without
2434
+ // scattering the same logic across every `withToolAliases` call site.
2435
+ // Suffix-based matching covers both the `remnic.*` and `engram.*`
2436
+ // naming forms emitted by `withToolAliases`.
2437
+ this.tools = this.tools.map((tool) =>
2438
+ isReadOnlyToolName(tool.name) && tool.annotations?.readOnlyHint !== true
2439
+ ? { ...tool, annotations: { ...(tool.annotations ?? {}), readOnlyHint: true } }
2440
+ : tool,
2441
+ );
2345
2442
  }
2346
2443
 
2347
2444
  /** Get clientInfo for a specific MCP session. Returns undefined for non-MCP requests. */
@@ -2369,6 +2466,24 @@ export class EngramMcpServer {
2369
2466
  }
2370
2467
  if (method === "initialize") {
2371
2468
  const params = request.params ?? {};
2469
+ // MCP initialize REQUIRES params.protocolVersion (string). Reject a
2470
+ // missing/mistyped field with JSON-RPC invalid params instead of
2471
+ // silently negotiating (repo rule: never reinterpret invalid input).
2472
+ // An unsupported-but-well-formed version gets the spec-mandated
2473
+ // counter-offer below instead: the server answers with the newest
2474
+ // version it supports and the client decides whether to proceed.
2475
+ if (typeof params.protocolVersion !== "string" || params.protocolVersion.length === 0) {
2476
+ return {
2477
+ jsonrpc: "2.0",
2478
+ id,
2479
+ error: {
2480
+ code: -32602,
2481
+ message:
2482
+ "initialize requires params.protocolVersion (string); " +
2483
+ `supported versions: ${MCP_SUPPORTED_PROTOCOL_VERSIONS.join(", ")}`,
2484
+ },
2485
+ };
2486
+ }
2372
2487
  const rawClientInfo = params.clientInfo as { name?: string; version?: string } | undefined;
2373
2488
  // Generate a server-side session ID for this MCP session.
2374
2489
  // The caller should send this back as Mcp-Session-Id on subsequent requests.
@@ -2391,7 +2506,9 @@ export class EngramMcpServer {
2391
2506
  jsonrpc: "2.0",
2392
2507
  id,
2393
2508
  result: {
2394
- protocolVersion: MCP_PROTOCOL_VERSION,
2509
+ protocolVersion: MCP_SUPPORTED_PROTOCOL_VERSIONS.includes(params.protocolVersion)
2510
+ ? params.protocolVersion
2511
+ : MCP_DEFAULT_PROTOCOL_VERSION,
2395
2512
  capabilities: {
2396
2513
  tools: {},
2397
2514
  resources: {},
package/src/index.ts CHANGED
@@ -977,11 +977,14 @@ export {
977
977
  // ---------------------------------------------------------------------------
978
978
 
979
979
  export {
980
+ buildTokenEntry,
981
+ commitTokenEntry,
980
982
  generateToken,
981
983
  listTokens,
982
984
  revokeToken,
983
985
  getAllValidTokens,
984
986
  getAllValidTokensCached,
987
+ getAllValidTokenEntriesCached,
985
988
  resolveConnectorFromToken,
986
989
  loadTokenStore,
987
990
  saveTokenStore,
@@ -8,6 +8,7 @@ import {
8
8
  buildTokenEntry,
9
9
  commitTokenEntry,
10
10
  generateToken,
11
+ getAllValidTokenEntriesCached,
11
12
  getAllValidTokensCached,
12
13
  loadTokenStore,
13
14
  revokeToken,
@@ -208,3 +209,60 @@ test("generateToken uses a recognizable prefix for the omp connector", async ()
208
209
  await rm(dir, { recursive: true, force: true });
209
210
  }
210
211
  });
212
+
213
+ test("generateToken uses a recognizable prefix for the chatgpt connector", async () => {
214
+ const { dir, tokensPath } = await makeTempTokenPath();
215
+ try {
216
+ const entry = generateToken("chatgpt", tokensPath);
217
+ assert.equal(entry.connector, "chatgpt");
218
+ assert.ok(
219
+ entry.token.startsWith("remnic_cg_"),
220
+ `expected remnic_cg_ prefix, got ${entry.token}`,
221
+ );
222
+ } finally {
223
+ await rm(dir, { recursive: true, force: true });
224
+ }
225
+ });
226
+
227
+ test("commitTokenEntry replaces the prior chatgpt entry so re-linking rotates the token", async () => {
228
+ const { dir, tokensPath } = await makeTempTokenPath();
229
+ try {
230
+ const first = generateToken("chatgpt", tokensPath);
231
+ commitTokenEntry(first, tokensPath);
232
+ const second = generateToken("chatgpt", tokensPath);
233
+ commitTokenEntry(second, tokensPath);
234
+ assert.notEqual(first.token, second.token, "re-linking must mint a new token");
235
+ const store = loadTokenStore(tokensPath);
236
+ const chatgptEntries = store.tokens.filter((t) => t.connector === "chatgpt");
237
+ assert.equal(chatgptEntries.length, 1, "only one chatgpt entry survives the rotate");
238
+ assert.equal(chatgptEntries[0]?.token, second.token, "the new token is the surviving one");
239
+ } finally {
240
+ await rm(dir, { recursive: true, force: true });
241
+ }
242
+ });
243
+
244
+ test("entries snapshot is coherent with validation: mint and revoke take effect immediately", async () => {
245
+ const { dir, tokensPath } = await makeTempTokenPath();
246
+ try {
247
+ // Warm the cache so a stale snapshot WOULD be observable if mutation
248
+ // failed to invalidate it.
249
+ assert.deepEqual([...getAllValidTokenEntriesCached(tokensPath)], []);
250
+
251
+ // Mint-then-use: the fresh token resolves with its connector in the
252
+ // SAME snapshot call that validates it — no window where the token is
253
+ // valid but its identity is unknown.
254
+ const minted = generateToken("chatgpt", tokensPath);
255
+ const afterMint = getAllValidTokenEntriesCached(tokensPath);
256
+ const found = afterMint.find((entry) => entry.token === minted.token);
257
+ assert.ok(found, "freshly minted token must appear immediately");
258
+ assert.equal(found.connector, "chatgpt");
259
+ assert.deepEqual(getAllValidTokensCached(tokensPath), [minted.token]);
260
+
261
+ // Revoke-then-use: the token disappears from the snapshot immediately.
262
+ assert.equal(revokeToken("chatgpt", tokensPath), true);
263
+ assert.deepEqual([...getAllValidTokenEntriesCached(tokensPath)], []);
264
+ assert.deepEqual(getAllValidTokensCached(tokensPath), []);
265
+ } finally {
266
+ await rm(dir, { recursive: true, force: true });
267
+ }
268
+ });
package/src/tokens.ts CHANGED
@@ -36,6 +36,7 @@ const TOKEN_PREFIXES: Record<string, string> = {
36
36
  "windsurf": "remnic_ws_",
37
37
  "amp": "remnic_am_",
38
38
  "generic-mcp": "remnic_gm_",
39
+ "chatgpt": "remnic_cg_",
39
40
  };
40
41
 
41
42
  function defaultTokensPath(): string {
@@ -260,26 +261,36 @@ export function getAllValidTokens(tokensPath?: string): string[] {
260
261
  return loadTokenStore(tokensPath).tokens.map((t) => t.token);
261
262
  }
262
263
 
263
- // Cached token loader to avoid synchronous disk I/O on every HTTP request.
264
- // Re-reads tokens.json at most once per TTL interval (default 5s).
264
+ // Cached token-entry snapshot to avoid synchronous disk I/O on every HTTP
265
+ // request. Re-reads tokens.json at most once per TTL interval (default 5s).
266
+ // There is deliberately exactly ONE cache: validation (token strings) and
267
+ // identity (connector ids) are derived from the SAME snapshot, so a token
268
+ // can never validate against a fresher snapshot than the one that resolves
269
+ // its connector. saveTokenStore() invalidates on every mutation, so a
270
+ // freshly minted or revoked token is coherent immediately.
265
271
  const TOKEN_CACHE_TTL_MS = 5_000;
266
- let _cachedTokens: string[] = [];
272
+ let _cachedEntries: TokenEntry[] = [];
267
273
  let _cachedAt = 0;
268
274
  let _cachedPath: string | undefined;
269
275
 
270
276
  function invalidateTokenCache(): void {
271
- _cachedTokens = [];
277
+ _cachedEntries = [];
272
278
  _cachedAt = 0;
273
279
  _cachedPath = undefined;
274
280
  }
275
281
 
276
- export function getAllValidTokensCached(tokensPath?: string): string[] {
282
+ /** Cached token-entry snapshot ({token, connector} pairs). */
283
+ export function getAllValidTokenEntriesCached(tokensPath?: string): readonly TokenEntry[] {
277
284
  const now = Date.now();
278
- if (now - _cachedAt < TOKEN_CACHE_TTL_MS && tokensPath === _cachedPath) return _cachedTokens;
279
- _cachedTokens = getAllValidTokens(tokensPath);
285
+ if (now - _cachedAt < TOKEN_CACHE_TTL_MS && tokensPath === _cachedPath) return _cachedEntries;
286
+ _cachedEntries = loadTokenStore(tokensPath).tokens;
280
287
  _cachedAt = now;
281
288
  _cachedPath = tokensPath;
282
- return _cachedTokens;
289
+ return _cachedEntries;
290
+ }
291
+
292
+ export function getAllValidTokensCached(tokensPath?: string): string[] {
293
+ return getAllValidTokenEntriesCached(tokensPath).map((entry) => entry.token);
283
294
  }
284
295
 
285
296
  export function resolveConnectorFromToken(token: string, tokensPath?: string): string | undefined {