ast-mcp-server 0.8.1 → 0.9.0
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/CHANGELOG.md +28 -3
- package/README.md +13 -4
- package/SECURITY.md +1 -1
- package/dist/cli.js +21 -0
- package/dist/services/opencode-config.js +15 -2
- package/dist/services/project.js +12 -2
- package/dist/services/symbol-index-cache.js +847 -0
- package/dist/services/symbol-index-policy.js +48 -15
- package/dist/services/symbol-index-sqlite.js +266 -7
- package/docs/support.md +31 -16
- package/package.json +4 -3
- package/skills/structural-code-editing/SKILL.md +3 -3
- package/skills/structural-code-editing/releases.json +7 -2
|
@@ -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
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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 {
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
-
|
|
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
|
|
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.0` 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 |
|
|
10
|
-
| Linux x64 with required GNU coreutils and procfs,
|
|
11
|
-
|
|
|
12
|
-
|
|
|
13
|
-
|
|
|
14
|
-
|
|
|
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.0 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.0 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
|
|
17
|
+
The local `0.9.0` 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
|
-
|
|
37
|
+
Published v0.8.1 remains memory-default. In the local `0.9.0` candidate, absence and explicit `enabled` select native SQLite; operators requiring no persistent index state must set `disabled` explicitly:
|
|
37
38
|
|
|
38
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 or the local 0.9.0 candidate. The separate exact 0.9.0 release matrix is the candidate-specific gate for current repository bytes under both supported runtime lines, including managed setup; it does not rebind those historical 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
|
-
-
|
|
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.
|
|
3
|
+
"version": "0.9.0",
|
|
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.
|
|
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": "
|
|
100
|
+
"gitHead": "0a98602410cf797cab47c7378b68cd3cfb7d8783"
|
|
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.
|
|
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
|
|
49
|
+
El candidato local 0.9.0 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
|
|
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.
|
|
6
|
-
"sha256": "
|
|
5
|
+
"version": "4.3.0",
|
|
6
|
+
"sha256": "f20e2d0a495b4749c457fc681fe4704266ffd2e37feab49e8db42803b0565d91"
|
|
7
7
|
},
|
|
8
8
|
"predecessors": [
|
|
9
9
|
{
|
|
@@ -45,6 +45,11 @@
|
|
|
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"]
|
|
48
53
|
}
|
|
49
54
|
]
|
|
50
55
|
}
|