ast-mcp-server 0.8.1 → 0.9.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.
@@ -1,4 +1,5 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { homedir } from "node:os";
2
3
  import path from "node:path";
3
4
  const DEFAULT_BUSY_TIMEOUT_MS = 1_000;
4
5
  export const MAX_SYMBOL_INDEX_RUNTIME_COUNT = 1_000_000;
@@ -34,8 +35,11 @@ export function createInitialSymbolIndexRuntimeObservability(policy = disabledPo
34
35
  };
35
36
  }
36
37
  function disabledPolicy(reason) {
38
+ return memoryPolicy("disabled", reason);
39
+ }
40
+ function memoryPolicy(mode, reason) {
37
41
  return {
38
- mode: "disabled",
42
+ mode,
39
43
  backend: "memory",
40
44
  cache_root: null,
41
45
  busy_timeout_ms: DEFAULT_BUSY_TIMEOUT_MS,
@@ -50,20 +54,10 @@ function readBusyTimeout(value) {
50
54
  ? parsed
51
55
  : DEFAULT_BUSY_TIMEOUT_MS;
52
56
  }
53
- export function readSymbolIndexPersistencePolicy(environment = process.env) {
54
- const mode = environment.AST_SYMBOL_INDEX_PERSISTENCE ?? "disabled";
55
- if (mode === "disabled")
56
- return disabledPolicy("default");
57
- if (mode === "enabled")
58
- return disabledPolicy("enabled_not_released");
59
- if (mode !== "canary")
60
- return disabledPolicy("invalid_mode");
61
- const cacheRoot = environment.AST_SYMBOL_INDEX_CACHE_ROOT;
62
- if (!cacheRoot)
63
- return disabledPolicy("cache_root_missing");
64
- if (!path.isAbsolute(cacheRoot) || path.normalize(cacheRoot) !== cacheRoot) {
65
- return disabledPolicy("cache_root_invalid");
66
- }
57
+ function isAbsoluteNormalizedPath(value) {
58
+ return !value.includes("\u0000") && path.isAbsolute(value) && path.normalize(value) === value;
59
+ }
60
+ function sqlitePolicy(mode, cacheRoot, environment) {
67
61
  return {
68
62
  mode,
69
63
  backend: "sqlite",
@@ -72,6 +66,45 @@ export function readSymbolIndexPersistencePolicy(environment = process.env) {
72
66
  reason: "default",
73
67
  };
74
68
  }
69
+ export function readSymbolIndexPersistencePolicy(environment = process.env, resolveHomeDirectory = homedir) {
70
+ const mode = environment.AST_SYMBOL_INDEX_PERSISTENCE ?? "enabled";
71
+ if (mode === "disabled")
72
+ return disabledPolicy("default");
73
+ if (mode !== "canary" && mode !== "enabled")
74
+ return disabledPolicy("invalid_mode");
75
+ const explicitCacheRoot = environment.AST_SYMBOL_INDEX_CACHE_ROOT;
76
+ if (mode === "canary") {
77
+ if (!explicitCacheRoot)
78
+ return memoryPolicy(mode, "cache_root_missing");
79
+ if (!isAbsoluteNormalizedPath(explicitCacheRoot)) {
80
+ return memoryPolicy(mode, "cache_root_invalid");
81
+ }
82
+ return sqlitePolicy(mode, explicitCacheRoot, environment);
83
+ }
84
+ if (explicitCacheRoot !== undefined) {
85
+ if (!isAbsoluteNormalizedPath(explicitCacheRoot)) {
86
+ return memoryPolicy(mode, "cache_root_invalid");
87
+ }
88
+ return sqlitePolicy(mode, explicitCacheRoot, environment);
89
+ }
90
+ const xdgCacheHome = environment.XDG_CACHE_HOME;
91
+ if (xdgCacheHome && isAbsoluteNormalizedPath(xdgCacheHome)) {
92
+ return sqlitePolicy(mode, path.join(xdgCacheHome, "ast-mcp-server", "symbol-index"), environment);
93
+ }
94
+ let homeDirectory;
95
+ try {
96
+ homeDirectory = resolveHomeDirectory();
97
+ }
98
+ catch {
99
+ return memoryPolicy(mode, "cache_root_missing");
100
+ }
101
+ if (!homeDirectory)
102
+ return memoryPolicy(mode, "cache_root_missing");
103
+ if (!isAbsoluteNormalizedPath(homeDirectory)) {
104
+ return memoryPolicy(mode, "cache_root_invalid");
105
+ }
106
+ return sqlitePolicy(mode, path.join(homeDirectory, ".cache", "ast-mcp-server", "symbol-index"), environment);
107
+ }
75
108
  export function symbolIndexPolicyKey(policy) {
76
109
  return [
77
110
  policy.mode,
@@ -1,10 +1,29 @@
1
1
  import { createHash } from "node:crypto";
2
- import { lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
2
+ import { closeSync, constants, fchmodSync, fstatSync, lstatSync, openSync, realpathSync, } from "node:fs";
3
+ import { chmod, lstat, mkdir, open, realpath, rename, rm } from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import { createSymbolIndexFileEntry, createSymbolIndexRefreshPlan, createSymbolIndexSymbol, MAX_SYMBOL_INDEX_FILE_ENTRIES, MAX_SYMBOL_INDEX_QUERY_CANDIDATES, MAX_SYMBOL_INDEX_ROW_PAYLOAD_BYTES, MAX_SYMBOL_INDEX_SCANNED_SYMBOLS, MAX_SYMBOL_INDEX_TOTAL_PAYLOAD_BYTES, SYMBOL_INDEX_SCHEMA_VERSION, SymbolIndexScanLimitError, symbolIndexMatchRank, symbolIndexQueryOffset, symbolIndexRankWindow, symbolProjectionJsonByteLength, } from "./symbol-index.js";
5
6
  const SQLITE_HEADER = Buffer.from("SQLite format 3\u0000", "utf8");
6
7
  const DEFAULT_BUSY_TIMEOUT_MS = 1_000;
7
8
  const SQLITE_READ_PAGE_SIZE = 32;
9
+ const OWNER_ONLY_DIRECTORY_MODE = 0o700;
10
+ const OWNER_ONLY_FILE_MODE = 0o600;
11
+ const STICKY_DIRECTORY_MODE = 0o1000;
12
+ const GROUP_OR_OTHER_WRITE_MODE = 0o022;
13
+ const GROUP_OR_OTHER_ACCESS_MODE = 0o077;
14
+ const NO_FOLLOW = constants.O_NOFOLLOW ?? 0;
15
+ const LINUX_O_PATH = 0o10000000;
16
+ const SUPPORTS_OWNER_ONLY_MODES = process.platform === "linux" && typeof process.getuid === "function";
17
+ const SUPPORTS_DESCRIPTOR_BOUND_OPEN = process.platform === "linux";
18
+ function isDescriptorOpenCapabilityFailure(error) {
19
+ if (!SUPPORTS_DESCRIPTOR_BOUND_OPEN)
20
+ return false;
21
+ const code = error?.code ?? "";
22
+ if (["ENOENT", "ENOTDIR", "EACCES", "EPERM"].includes(code))
23
+ return true;
24
+ const message = error instanceof Error ? error.message.toLowerCase() : "";
25
+ return code === "ERR_SQLITE_ERROR" && message.includes("unable to open database file");
26
+ }
8
27
  const EXPECTED_TABLE_COLUMNS = {
9
28
  metadata: [
10
29
  { name: "key", type: "TEXT", notnull: 0, dflt_value: null, pk: 1 },
@@ -74,12 +93,203 @@ function assertSafeDatabasePath(filePath) {
74
93
  }
75
94
  return normalized;
76
95
  }
96
+ function assertTrustedDirectoryStats(stats, packageOwned) {
97
+ if (!SUPPORTS_OWNER_ONLY_MODES)
98
+ return;
99
+ const currentUid = process.getuid();
100
+ const mode = stats.mode & 0o7777;
101
+ if (stats.uid !== currentUid && stats.uid !== 0) {
102
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory ancestry is not owned by a trusted principal.");
103
+ }
104
+ if (packageOwned) {
105
+ if (stats.uid !== currentUid || (mode & GROUP_OR_OTHER_ACCESS_MODE) !== 0) {
106
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory is not private to the invoking owner.");
107
+ }
108
+ return;
109
+ }
110
+ if ((mode & GROUP_OR_OTHER_WRITE_MODE) !== 0 && (mode & STICKY_DIRECTORY_MODE) === 0) {
111
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory ancestry is writable by an untrusted principal.");
112
+ }
113
+ }
114
+ function assertTrustedCanonicalDirectoryChain(directoryPath) {
115
+ const parsed = path.parse(directoryPath);
116
+ const segments = path.relative(parsed.root, directoryPath).split(path.sep).filter(Boolean);
117
+ let current = parsed.root;
118
+ for (const segment of segments) {
119
+ current = path.join(current, segment);
120
+ const stats = lstatSync(current);
121
+ if (stats.isSymbolicLink() || !stats.isDirectory()) {
122
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory contains a non-canonical ancestor.");
123
+ }
124
+ assertTrustedDirectoryStats(stats, current === directoryPath);
125
+ }
126
+ if (realpathSync(directoryPath) !== directoryPath) {
127
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory resolves outside its canonical path.");
128
+ }
129
+ }
130
+ function sameArtifactIdentity(stats, expected) {
131
+ return stats.dev === expected.dev && stats.ino === expected.ino;
132
+ }
133
+ function assertPrivateArtifactStats(stats, message) {
134
+ if (!stats.isFile() || stats.nlink !== 1) {
135
+ throw new SymbolIndexStorageError("invalid_path", message);
136
+ }
137
+ if (SUPPORTS_OWNER_ONLY_MODES && stats.uid !== process.getuid()) {
138
+ throw new SymbolIndexStorageError("invalid_path", message);
139
+ }
140
+ }
141
+ function assertPreparedArtifactIdentity(filePath, expected) {
142
+ let stats;
143
+ try {
144
+ stats = lstatSync(filePath);
145
+ if (stats.isSymbolicLink() ||
146
+ !sameArtifactIdentity(stats, expected) ||
147
+ realpathSync(filePath) !== filePath) {
148
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact identity changed before use.");
149
+ }
150
+ assertPrivateArtifactStats(stats, "SQLite cache artifact is not a unique owned regular file.");
151
+ if (SUPPORTS_OWNER_ONLY_MODES && (stats.mode & 0o777) !== OWNER_ONLY_FILE_MODE) {
152
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact is not owner-only.");
153
+ }
154
+ }
155
+ catch (error) {
156
+ if (error instanceof SymbolIndexStorageError)
157
+ throw error;
158
+ throw wrapStorageError(error, "read_failed", "SQLite cache artifact could not be verified.");
159
+ }
160
+ }
161
+ function prepareExistingPrivateArtifact(filePath) {
162
+ let descriptor;
163
+ try {
164
+ descriptor = openSync(filePath, constants.O_RDWR | NO_FOLLOW);
165
+ const before = fstatSync(descriptor);
166
+ assertPrivateArtifactStats(before, "SQLite cache artifact is not a unique owned regular file.");
167
+ if (realpathSync(filePath) !== filePath) {
168
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact resolves outside its canonical path.");
169
+ }
170
+ if (SUPPORTS_OWNER_ONLY_MODES)
171
+ fchmodSync(descriptor, OWNER_ONLY_FILE_MODE);
172
+ const after = fstatSync(descriptor);
173
+ if (!sameArtifactIdentity(before, after)) {
174
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact identity changed while securing it.");
175
+ }
176
+ assertPrivateArtifactStats(after, "SQLite cache artifact changed while securing it.");
177
+ const identity = { dev: after.dev, ino: after.ino };
178
+ assertPreparedArtifactIdentity(filePath, identity);
179
+ return identity;
180
+ }
181
+ catch (error) {
182
+ if (error?.code === "ENOENT")
183
+ return null;
184
+ if (error instanceof SymbolIndexStorageError)
185
+ throw error;
186
+ if (error?.code === "ELOOP") {
187
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact must not be a symbolic link.", error);
188
+ }
189
+ throw wrapStorageError(error, "write_failed", "SQLite cache artifact could not be secured.");
190
+ }
191
+ finally {
192
+ if (descriptor !== undefined)
193
+ closeSync(descriptor);
194
+ }
195
+ }
196
+ function preparePrivateDatabaseArtifact(filePath) {
197
+ let descriptor;
198
+ try {
199
+ try {
200
+ descriptor = openSync(filePath, constants.O_RDWR | NO_FOLLOW);
201
+ }
202
+ catch (error) {
203
+ if (error.code !== "ENOENT")
204
+ throw error;
205
+ descriptor = openSync(filePath, constants.O_CREAT | constants.O_EXCL | constants.O_RDWR | NO_FOLLOW, OWNER_ONLY_FILE_MODE);
206
+ }
207
+ const before = fstatSync(descriptor);
208
+ assertPrivateArtifactStats(before, "SQLite cache artifact is not a unique owned regular file.");
209
+ if (realpathSync(filePath) !== filePath) {
210
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact resolves outside its canonical path.");
211
+ }
212
+ if (SUPPORTS_OWNER_ONLY_MODES)
213
+ fchmodSync(descriptor, OWNER_ONLY_FILE_MODE);
214
+ const after = fstatSync(descriptor);
215
+ if (!sameArtifactIdentity(before, after)) {
216
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact identity changed while securing it.");
217
+ }
218
+ assertPrivateArtifactStats(after, "SQLite cache artifact changed while securing it.");
219
+ const identity = { dev: after.dev, ino: after.ino };
220
+ assertPreparedArtifactIdentity(filePath, identity);
221
+ const openPath = SUPPORTS_DESCRIPTOR_BOUND_OPEN ? `/proc/self/fd/${descriptor}` : filePath;
222
+ if (SUPPORTS_DESCRIPTOR_BOUND_OPEN) {
223
+ let descriptorTarget;
224
+ try {
225
+ descriptorTarget = realpathSync(openPath);
226
+ }
227
+ catch (error) {
228
+ throw new SymbolIndexStorageError("capability_unavailable", "SQLite cache descriptor binding is unavailable on this Linux runtime.", error);
229
+ }
230
+ if (descriptorTarget !== filePath) {
231
+ throw new SymbolIndexStorageError("capability_unavailable", "SQLite cache descriptor binding is unavailable on this Linux runtime.");
232
+ }
233
+ }
234
+ const prepared = { descriptor, identity, openPath };
235
+ descriptor = undefined;
236
+ return prepared;
237
+ }
238
+ catch (error) {
239
+ if (error instanceof SymbolIndexStorageError)
240
+ throw error;
241
+ if (["EEXIST", "ELOOP"].includes(error?.code ?? "")) {
242
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache artifact changed while it was being prepared.", error);
243
+ }
244
+ throw wrapStorageError(error, "write_failed", "SQLite cache artifact could not be prepared.");
245
+ }
246
+ finally {
247
+ if (descriptor !== undefined)
248
+ closeSync(descriptor);
249
+ }
250
+ }
251
+ async function normalizeCreatedDirectory(directoryPath) {
252
+ if (!SUPPORTS_OWNER_ONLY_MODES)
253
+ return;
254
+ let handle;
255
+ try {
256
+ handle = await open(directoryPath, LINUX_O_PATH | (constants.O_DIRECTORY ?? 0) | NO_FOLLOW);
257
+ const opened = await handle.stat();
258
+ if (!opened.isDirectory()) {
259
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory contains a non-canonical ancestor.");
260
+ }
261
+ await chmod(`/proc/self/fd/${handle.fd}`, OWNER_ONLY_DIRECTORY_MODE);
262
+ const [hardened, named, physicalPath] = await Promise.all([
263
+ handle.stat(),
264
+ lstat(directoryPath),
265
+ realpath(directoryPath),
266
+ ]);
267
+ if (!sameArtifactIdentity(hardened, opened) ||
268
+ !sameArtifactIdentity(named, opened) ||
269
+ named.isSymbolicLink() ||
270
+ !named.isDirectory() ||
271
+ physicalPath !== directoryPath ||
272
+ (hardened.mode & 0o777) !== OWNER_ONLY_DIRECTORY_MODE ||
273
+ (named.mode & 0o777) !== OWNER_ONLY_DIRECTORY_MODE) {
274
+ throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory changed while it was secured.");
275
+ }
276
+ }
277
+ catch (error) {
278
+ if (error instanceof SymbolIndexStorageError)
279
+ throw error;
280
+ throw wrapStorageError(error, "write_failed", "SQLite cache directory could not be secured.");
281
+ }
282
+ finally {
283
+ await handle?.close();
284
+ }
285
+ }
77
286
  async function ensureCanonicalDirectory(directoryPath) {
78
287
  const parsed = path.parse(directoryPath);
79
288
  const segments = path.relative(parsed.root, directoryPath).split(path.sep).filter(Boolean);
80
289
  let current = parsed.root;
81
290
  for (const segment of segments) {
82
291
  current = path.join(current, segment);
292
+ let created = false;
83
293
  let stats;
84
294
  try {
85
295
  stats = await lstat(current);
@@ -89,18 +299,22 @@ async function ensureCanonicalDirectory(directoryPath) {
89
299
  throw wrapStorageError(error, "write_failed", "SQLite cache directory could not be inspected.");
90
300
  }
91
301
  try {
92
- await mkdir(current);
302
+ await mkdir(current, { mode: OWNER_ONLY_DIRECTORY_MODE });
303
+ created = true;
93
304
  }
94
305
  catch (mkdirError) {
95
306
  if (mkdirError.code !== "EEXIST") {
96
307
  throw wrapStorageError(mkdirError, "write_failed", "SQLite cache directory could not be created.");
97
308
  }
98
309
  }
310
+ if (created)
311
+ await normalizeCreatedDirectory(current);
99
312
  stats = await lstat(current);
100
313
  }
101
314
  if (stats.isSymbolicLink() || !stats.isDirectory()) {
102
315
  throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory contains a non-canonical ancestor.");
103
316
  }
317
+ assertTrustedDirectoryStats(stats, created || current === directoryPath);
104
318
  }
105
319
  if ((await realpath(directoryPath)) !== directoryPath) {
106
320
  throw new SymbolIndexStorageError("invalid_path", "SQLite cache directory resolves outside its canonical path.");
@@ -303,6 +517,21 @@ export async function openSQLiteSymbolIndexStore(filePath, options = {}) {
303
517
  }
304
518
  export async function quarantineSQLiteSymbolIndexFile(filePath) {
305
519
  const safePath = assertSafeDatabasePath(filePath);
520
+ const databaseIdentity = prepareExistingPrivateArtifact(safePath);
521
+ if (!databaseIdentity)
522
+ return null;
523
+ const sidecars = ["wal", "shm"].map((suffix) => {
524
+ const sidecarPath = `${safePath}-${suffix}`;
525
+ return {
526
+ path: sidecarPath,
527
+ identity: prepareExistingPrivateArtifact(sidecarPath),
528
+ };
529
+ });
530
+ assertPreparedArtifactIdentity(safePath, databaseIdentity);
531
+ for (const sidecar of sidecars) {
532
+ if (sidecar.identity)
533
+ assertPreparedArtifactIdentity(sidecar.path, sidecar.identity);
534
+ }
306
535
  const quarantinePath = `${safePath}.corrupt-${process.pid}-${Date.now()}`;
307
536
  try {
308
537
  await rename(safePath, quarantinePath);
@@ -312,10 +541,13 @@ export async function quarantineSQLiteSymbolIndexFile(filePath) {
312
541
  return null;
313
542
  throw wrapStorageError(error, "write_failed", "SQLite cache quarantine failed.");
314
543
  }
315
- await Promise.all([
316
- rm(`${safePath}-wal`, { force: true }),
317
- rm(`${safePath}-shm`, { force: true }),
318
- ]);
544
+ assertPreparedArtifactIdentity(quarantinePath, databaseIdentity);
545
+ for (const sidecar of sidecars) {
546
+ if (!sidecar.identity)
547
+ continue;
548
+ assertPreparedArtifactIdentity(sidecar.path, sidecar.identity);
549
+ await rm(sidecar.path);
550
+ }
319
551
  return quarantinePath;
320
552
  }
321
553
  export class SQLiteSymbolIndexStore {
@@ -325,13 +557,40 @@ export class SQLiteSymbolIndexStore {
325
557
  db;
326
558
  constructor(filePath, DatabaseSync, busyTimeoutMs = DEFAULT_BUSY_TIMEOUT_MS) {
327
559
  this.filePath = assertSafeDatabasePath(filePath);
560
+ assertTrustedCanonicalDirectoryChain(path.dirname(this.filePath));
328
561
  this.busyTimeoutMs =
329
562
  Number.isInteger(busyTimeoutMs) && busyTimeoutMs > 0
330
563
  ? busyTimeoutMs
331
564
  : DEFAULT_BUSY_TIMEOUT_MS;
332
- this.db = new DatabaseSync(this.filePath);
565
+ const preparedDatabase = preparePrivateDatabaseArtifact(this.filePath);
566
+ prepareExistingPrivateArtifact(`${this.filePath}-wal`);
567
+ prepareExistingPrivateArtifact(`${this.filePath}-shm`);
568
+ try {
569
+ this.db = new DatabaseSync(preparedDatabase.openPath);
570
+ }
571
+ catch (error) {
572
+ closeSync(preparedDatabase.descriptor);
573
+ if (isDescriptorOpenCapabilityFailure(error)) {
574
+ throw new SymbolIndexStorageError("capability_unavailable", "SQLite cache descriptor binding is unavailable on this Linux runtime.", error);
575
+ }
576
+ throw error;
577
+ }
578
+ try {
579
+ closeSync(preparedDatabase.descriptor);
580
+ }
581
+ catch (error) {
582
+ this.db.close();
583
+ this.db = null;
584
+ throw wrapStorageError(error, "write_failed", "SQLite cache descriptor could not be released safely.");
585
+ }
333
586
  try {
587
+ assertTrustedCanonicalDirectoryChain(path.dirname(this.filePath));
588
+ assertPreparedArtifactIdentity(this.filePath, preparedDatabase.identity);
334
589
  this.initialize();
590
+ assertTrustedCanonicalDirectoryChain(path.dirname(this.filePath));
591
+ assertPreparedArtifactIdentity(this.filePath, preparedDatabase.identity);
592
+ prepareExistingPrivateArtifact(`${this.filePath}-wal`);
593
+ prepareExistingPrivateArtifact(`${this.filePath}-shm`);
335
594
  }
336
595
  catch (error) {
337
596
  this.db.close();
package/docs/support.md CHANGED
@@ -1,19 +1,20 @@
1
1
  # Support policy
2
2
 
3
- This document defines the supported platform, runtime, persistence, and operational boundary for `ast-mcp-server` v0.8.1 and the current `Unreleased` development line.
3
+ This document defines the supported platform, runtime, persistence, and operational boundary for published `ast-mcp-server` v0.8.1 and the local `0.9.1` release candidate.
4
4
 
5
5
  ## Supported release target
6
6
 
7
- | Environment | Status | Contract |
8
- | ----------------------------------------------------------------------------- | ----------- | --------------------------------------------------------------------------------------------- |
9
- | Linux x64 with required GNU coreutils and procfs, Node.js 22.5.0 | Supported | v0.8.1 exact release matrix for MCP/CLI/package, lifecycle, mutation, and managed setup. |
10
- | Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | v0.8.1 exact release matrix for MCP/CLI/package, lifecycle, mutation, and managed setup. |
11
- | Other Linux architectures or systems without the required filesystem features | Unverified | Not supported until equivalent architecture, mutation, and filesystem-publication gates pass. |
12
- | macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
13
- | Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
14
- | Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided. |
7
+ | Environment | Status | Contract |
8
+ | ----------------------------------------------------------------------------- | ----------- | ---------------------------------------------------------------------------------------------- |
9
+ | Linux x64 with required GNU coreutils and procfs, Node.js 22.5.0 | Published | Immutable v0.8.1 release matrix; not the runtime floor of the current development tree. |
10
+ | Linux x64 with required GNU coreutils and procfs, exact Node.js 22.13.0 | Supported | Local 0.9.1 candidate package, persistence, MCP/CLI/package, lifecycle and mutation matrix. |
11
+ | Linux x64 with required GNU coreutils and procfs, current Node.js 24 line | Supported | Published v0.8.1 and local 0.9.1 candidate matrices, each bound to its own package/tree bytes. |
12
+ | Other Linux architectures or systems without the required filesystem features | Unverified | Not supported until equivalent architecture, mutation, and filesystem-publication gates pass. |
13
+ | macOS | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
14
+ | Windows | Unverified | Not supported until equivalent filesystem, process, package, mutation, and canary gates pass. |
15
+ | Remote, untrusted, or multi-tenant service use | Unsupported | No network authentication, sandbox, or tenant-isolation boundary is provided. |
15
16
 
16
- The package engine floor is Node.js `>=22.5.0`. The v0.8.1 release matrix exercises exact Node.js 22.5.0 and the current Node.js 24 line on Linux, including managed setup-file publication; a future version satisfying the engine range is not automatically a verified release target.
17
+ The local `0.9.1` candidate requires Node.js `>=22.13.0`; its matrix targets exact Node.js 22.13.0 and the governed Node.js 24 major. Published v0.8.1 retains its immutable `>=22.5.0` metadata and exact Node.js 22.5.0/24 release evidence. A future runtime satisfying an engine range is not automatically a verified release target.
17
18
 
18
19
  The checked-evidence freezer requires GNU coreutils 9.7 `mv` with `--update=none-fail`, `--no-copy`, and `--no-target-directory`. Managed setup-file mutation additionally requires that same `mv` with `--exchange`, GNU coreutils `ln -L -T`, procfs descriptor paths at `/proc/self/fd`, `O_DIRECTORY`, and `O_NOFOLLOW`. It fails closed when any verified Linux primitive is unavailable. Equivalent publication, exchange, descriptor-link, and descriptor-relative mutation semantics have not been verified on other operating systems or architectures.
19
20
 
@@ -33,9 +34,16 @@ Operation locks coordinate cooperating same-user processes. They do not stop edi
33
34
 
34
35
  ## Symbol-index persistence
35
36
 
36
- The supported default is memory-only indexing. When `AST_SYMBOL_INDEX_PERSISTENCE` is absent, persistence remains disabled and no persistent cache is created.
37
+ Published v0.8.1 remains memory-default. In the local `0.9.1` candidate, absence and explicit `enabled` select native SQLite; operators requiring no persistent index state must set `disabled` explicitly:
37
38
 
38
- The only candidate-authorized opt-in persistence policy is `canary`; it is not enabled by default in v0.8.1:
39
+ ```bash
40
+ AST_SYMBOL_INDEX_PERSISTENCE=disabled \
41
+ ast-mcp-server
42
+ ```
43
+
44
+ `disabled` selects memory before consulting HOME, XDG or a cache-root override and does not open existing SQLite files. Removing the variable is not rollback in the current line.
45
+
46
+ Default `enabled` resolves a root from a valid explicit `AST_SYMBOL_INDEX_CACHE_ROOT`, then `${XDG_CACHE_HOME}/ast-mcp-server/symbol-index`, then `${HOME}/.cache/ast-mcp-server/symbol-index`. A present invalid override fails closed; it is not ignored in favor of XDG/HOME. `canary` still requires an explicit absolute normalized root:
39
47
 
40
48
  ```bash
41
49
  AST_SYMBOL_INDEX_PERSISTENCE=canary \
@@ -43,13 +51,20 @@ AST_SYMBOL_INDEX_CACHE_ROOT=/absolute/isolated/cache/root \
43
51
  ast-mcp-server
44
52
  ```
45
53
 
46
- The cache root must be explicit and absolute. Use a dedicated local directory with permissions appropriate for the invoking user. Node.js 22.5.0 additionally requires `--experimental-sqlite` for this canary path.
54
+ On supported Linux x64, package-created cache directories are `0700`; SQLite main, WAL, SHM and quarantine files are `0600`. The implementation does not chmod pre-existing external parents and fails closed on symbolic, non-regular, multiply linked, wrong-owner, escaped or identity-substituted artifacts. SQLite remains derived data; compiler and synchronized source state remain authority.
55
+
56
+ Inspect or remove only canonical derived artifacts through:
57
+
58
+ ```bash
59
+ ast-tool cache inspect
60
+ ast-tool cache clear --yes
61
+ ```
47
62
 
48
- `AST_SYMBOL_INDEX_PERSISTENCE=enabled` is reserved and intentionally fails closed to memory-only with `policy_reason: "enabled_not_released"`. Unknown values and invalid/missing canary cache roots also fail closed. Canary evidence authorizes the opt-in capability; it does not authorize global or default activation.
63
+ Both commands are bounded and path-free. Clear preflights the tree, refuses unsafe or active SQLite artifacts, rechecks identity before unlink and preserves unknown regular files/directories. There is no automatic garbage collection in this candidate.
49
64
 
50
65
  ## Production-readiness acceptance
51
66
 
52
- The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1 and do not establish managed setup-file publication for that release. The separate exact v0.8.1 release matrix validates the current repository under both supported runtime lines, including managed setup, but does not rebind those historical reports. MCP-PROD-404 remains normative for admission:
67
+ The four retained Linux x64 production-readiness reports cover `ast-mcp-server` and `x-scraper` under Node.js 22.5.0 and Node.js 24 for package version 0.6.0, commit `2d0b21bbb80fae1acfca6a85d5891d87e68b59c1`, and tree `af931d49769fabdf06f623965a6cfe1f9afb8a81`. They are historical integration evidence only: they are not reattributed to v0.8.1, published-next v0.9.0, or the local 0.9.1 candidate. The exact 0.9.0 release matrix remains historical evidence for its immutable published-next bytes. A separate exact 0.9.1 release matrix is the candidate-specific gate for current repository bytes under both supported runtime lines, including managed setup; neither matrix rebinds the older retained reports. MCP-PROD-404 remains normative for admission:
53
68
 
54
69
  - zero semantic mismatches across disabled, cold, warm, restart, and rollback reads;
55
70
  - byte-identical real-repository status and worktree trees;
@@ -74,7 +89,7 @@ Before reporting a compatibility problem, capture:
74
89
  - exact `node --version` output and Node binary path;
75
90
  - package version;
76
91
  - MCP client and transport configuration;
77
- - whether persistence is disabled or running in explicit canary mode;
92
+ - the requested persistence policy and effective backend (`enabled`, `canary`, or `disabled`; SQLite or memory);
78
93
  - the bounded public error code and correlation identifier, if present.
79
94
 
80
95
  Do not attach source code, absolute project paths, cache contents, credentials, tokens, or private configuration unless they have been intentionally sanitized. Report suspected vulnerabilities through [SECURITY.md](../SECURITY.md), not a public issue.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ast-mcp-server",
3
- "version": "0.8.1",
3
+ "version": "0.9.1",
4
4
  "description": "Structural code MCP server and batch CLI for compact TypeScript/JavaScript reads and hash-bound AST edits.",
5
5
  "keywords": [
6
6
  "mcp",
@@ -40,7 +40,7 @@
40
40
  "docs/support.md"
41
41
  ],
42
42
  "engines": {
43
- "node": ">=22.5.0"
43
+ "node": ">=22.13.0"
44
44
  },
45
45
  "resolutions": {
46
46
  "hono": "4.12.34",
@@ -56,6 +56,7 @@
56
56
  "test:cli": "yarn build && node scripts/cli-smoke.mjs",
57
57
  "test:errors": "yarn build && node scripts/public-error-smoke.mjs",
58
58
  "test:package": "node scripts/package-smoke.mjs",
59
+ "test:local-registry": "node scripts/local-registry-consumer-smoke.mjs",
59
60
  "test:installed-agents": "yarn build && node scripts/installed-agent-guidance-smoke.mjs",
60
61
  "test:agent-fixtures": "node scripts/admit-agent-fixtures.mjs --check",
61
62
  "test:release-candidate": "node scripts/release-candidate-matrix.mjs",
@@ -96,5 +97,5 @@
96
97
  "typescript-eslint": "^8.65.0",
97
98
  "vitest": "^4.1.10"
98
99
  },
99
- "gitHead": "bc8026fa534262c293384b50a8dff0eaa2ca22da"
100
+ "gitHead": "a8b672cd2d1fb2afe65b9cb4a4f842ee75b3faca"
100
101
  }
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: structural-code-editing
3
3
  description: Leer, navegar y editar proyectos TypeScript/JavaScript mediante el resolver real del compilador y operaciones AST preparadas, revisadas y vinculadas por hash.
4
- version: "4.2.0"
4
+ version: "4.3.0"
5
5
  author: "yail"
6
6
  license: "ISC"
7
7
  metadata:
@@ -46,11 +46,11 @@ El setup falla cerrado ante un registro `ast` conflictivo. No eliminar ni reempl
46
46
 
47
47
  ## Runtime, soporte y errores
48
48
 
49
- El paquete requiere Node.js `>=22.5.0`. La matriz de release de v0.8.1 cubre Linux x64 con Node.js 22.5.0 y la línea actual de Node.js 24, incluida la publicación de archivos gestionados por setup. Esa mutación requiere además GNU coreutils 9.7 `mv` con `--update=none-fail`, `--exchange`, `--no-copy` y `--no-target-directory`, GNU coreutils `ln -L -T`, y procfs en `/proc/self/fd` con `O_DIRECTORY`/`O_NOFOLLOW`. Otras arquitecturas Linux, sistemas sin esos primitivos, macOS y Windows están sin verificar.
49
+ El candidato local 0.9.1 requiere Node.js `>=22.13.0` y se verifica con Node.js exacto 22.13.0 y la línea actual de Node.js 24. La versión publicada v0.8.1 conserva su matriz inmutable 22.5.0/24. La publicación de archivos gestionados por setup requiere además GNU coreutils 9.7 `mv` con `--update=none-fail`, `--exchange`, `--no-copy` y `--no-target-directory`, GNU coreutils `ln -L -T`, y procfs en `/proc/self/fd` con `O_DIRECTORY`/`O_NOFOLLOW`. Otras arquitecturas Linux, sistemas sin esos primitivos, macOS y Windows están sin verificar.
50
50
 
51
51
  El transporte soportado es stdio local bajo los permisos filesystem del usuario que invoca el proceso. No asumir autenticación HTTP, sandbox, aislamiento entre tenants ni una frontera segura para clientes remotos o no confiables.
52
52
 
53
- La persistencia del índice está deshabilitada por default. El modo `canary` requiere `AST_SYMBOL_INDEX_PERSISTENCE=canary` y un `AST_SYMBOL_INDEX_CACHE_ROOT` absoluto y aislado; `enabled` sigue sin publicarse y falla cerrado a memoria con `enabled_not_released`.
53
+ La persistencia del índice usa SQLite por default cuando `AST_SYMBOL_INDEX_PERSISTENCE` está ausente o vale `enabled`. El rollback inmediato es `AST_SYMBOL_INDEX_PERSISTENCE=disabled`; ese modo selecciona memoria antes de consultar HOME, XDG o un root explícito. `canary` conserva el requisito de `AST_SYMBOL_INDEX_CACHE_ROOT` absoluto, normalizado y aislado. Los fallos de policy o storage conservan la policy solicitada, cambian el backend efectivo a memoria y exponen una razón acotada sin paths. Administrar sólo artefactos derivados mediante `ast-tool cache inspect` y `ast-tool cache clear --yes`; no eliminar el árbol manualmente ni asumir GC automático.
54
54
 
55
55
  Los errores públicos de las tools MCP usan un vocabulario cerrado dentro de `{ "error": { "code", "message", "correlation_id" } }`, con `isError: true`. Conservar el `correlation_id` al reportar un fallo; no pedir ni exponer stacks, paths absolutos, source, cache, argumentos crudos, environment ni credenciales para diagnosticarlo. Los errores del comando local `setup` pueden incluir un path destino acotado para permitir inspeccionar una escritura incierta o pendiente, pero no argumentos, environment, credenciales ni output crudo del proveedor.
56
56
 
@@ -2,8 +2,8 @@
2
2
  "schema_version": 1,
3
3
  "algorithm": "sha256",
4
4
  "current": {
5
- "version": "4.2.0",
6
- "sha256": "43a07546fcc8417b5e937881e20d4bb87e57bba0bce1ae0c5bb58d75a6f75870"
5
+ "version": "4.3.0",
6
+ "sha256": "a8dc2ba8eecbf3a21d7a86092323a2674b0353f5ec1f2bb36ba7ed0cd2890c20"
7
7
  },
8
8
  "predecessors": [
9
9
  {
@@ -45,6 +45,16 @@
45
45
  "version": "4.2.0",
46
46
  "sha256": "b072ccf68ebd50a825dc9fdbdaaf35cdbf7045ae5fb9a9d7a412dc7b702ee7a0",
47
47
  "npm_versions": ["0.8.0"]
48
+ },
49
+ {
50
+ "version": "4.2.0",
51
+ "sha256": "43a07546fcc8417b5e937881e20d4bb87e57bba0bce1ae0c5bb58d75a6f75870",
52
+ "npm_versions": ["0.8.1"]
53
+ },
54
+ {
55
+ "version": "4.3.0",
56
+ "sha256": "f20e2d0a495b4749c457fc681fe4704266ffd2e37feab49e8db42803b0565d91",
57
+ "npm_versions": ["0.9.0"]
48
58
  }
49
59
  ]
50
60
  }