ast-mcp-server 0.12.0 → 0.13.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.
- package/CHANGELOG.md +21 -2
- package/README.md +59 -10
- package/SECURITY.md +1 -1
- package/cordis.patch.yml +38 -0
- package/dist/server.js +21 -2
- package/dist/services/agent-setup.js +85 -19
- package/dist/services/agent-targets.js +168 -66
- package/dist/services/compiler-worker-host.js +36 -16
- package/dist/services/compiler-worker-protocol.js +70 -2
- package/dist/services/h03-timeout-fixture.js +335 -0
- package/dist/services/opencode-config.js +84 -7
- package/dist/services/project.js +29 -16
- package/dist/services/public-errors.js +7 -5
- package/dist/services/runtime-policy.js +83 -2
- package/dist/tools/catalog.js +5 -2
- package/dist/tools/explore.js +5 -3
- package/dist/tools/get_impact.js +0 -1
- package/dist/tools/result.js +48 -0
- package/dist/worker-runtime.js +7 -2
- package/docs/support.md +7 -7
- package/package.json +22 -3
- package/skills/structural-code-editing/SKILL.md +1 -1
- package/skills/structural-code-editing/releases.json +6 -1
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
import { Buffer } from "node:buffer";
|
|
2
|
+
import { appendFileSync, readFileSync, statSync } from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { PUBLIC_ERROR_CODES, sanitizePublicText } from "./public-errors.js";
|
|
5
|
+
import { parseH03FixtureDescriptor, parseH05FixtureDescriptor, } from "./runtime-policy.js";
|
|
6
|
+
const FIXTURE_ID = /^[A-Za-z0-9_-]{1,64}$/;
|
|
7
|
+
export class H03TimeoutFixtureError extends Error {
|
|
8
|
+
name = "H03TimeoutFixtureError";
|
|
9
|
+
code = "STALE_GENERATION";
|
|
10
|
+
}
|
|
11
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
12
|
+
/** Closed test-only lifecycle controller used by H-05 through ast_get_project_status. */
|
|
13
|
+
export class H05LifecycleFixtureController {
|
|
14
|
+
#descriptor;
|
|
15
|
+
#generationAbort = new AbortController();
|
|
16
|
+
#gates = new Map();
|
|
17
|
+
#events = [];
|
|
18
|
+
#active = 0;
|
|
19
|
+
#abortListeners = 0;
|
|
20
|
+
#staleSettlements = 0;
|
|
21
|
+
// prettier-ignore
|
|
22
|
+
constructor(descriptor) { if (!statSync(descriptor.controlDirectory, { throwIfNoEntry: false })?.isDirectory())
|
|
23
|
+
throw new Error("H-05 fixture control path must be an existing directory."); this.#descriptor = Object.freeze({ ...descriptor }); }
|
|
24
|
+
// prettier-ignore
|
|
25
|
+
capture(command) { if (Object.keys(command).sort().join(",") !== "callId,correlationId,fixtureId" || !FIXTURE_ID.test(command.callId) || !FIXTURE_ID.test(command.fixtureId) || !UUID.test(command.correlationId))
|
|
26
|
+
throw new Error("Invalid H-05 fixture command."); return Object.freeze({ ...command, generation: this.#descriptor.generation, ownerToken: this.#descriptor.ownerToken }); }
|
|
27
|
+
// prettier-ignore
|
|
28
|
+
hold(identity) { this.#assertCurrent(identity); const key = this.#key(identity); if (this.#gates.has(key))
|
|
29
|
+
throw new Error("Invalid or duplicate H-05 fixture id."); let release; const promise = new Promise((resolve) => (release = resolve)); this.#gates.set(key, { promise, release }); }
|
|
30
|
+
// prettier-ignore
|
|
31
|
+
advance(descriptor) { if (descriptor.generation <= this.#descriptor.generation)
|
|
32
|
+
throw new Error("H-05 fixture generation must increase."); this.#descriptor = Object.freeze({ ...descriptor }); this.#generationAbort.abort(); this.#generationAbort = new AbortController(); for (const gate of this.#gates.values())
|
|
33
|
+
gate.release(); this.#gates.clear(); }
|
|
34
|
+
// prettier-ignore
|
|
35
|
+
async run(identity, context, operation) {
|
|
36
|
+
this.#assertIdentity(identity);
|
|
37
|
+
const eventIdentity = { callId: identity.callId, correlationId: identity.correlationId, fixtureId: identity.fixtureId, generation: identity.generation, ownerToken: identity.ownerToken };
|
|
38
|
+
const generationSignal = this.#generationAbort.signal, signal = AbortSignal.any([context.signal, generationSignal]);
|
|
39
|
+
const checkpoint = () => { if (!this.accepts(identity) || generationSignal.aborted)
|
|
40
|
+
throw new H03TimeoutFixtureError(); context.checkpoint(); };
|
|
41
|
+
const hooked = Object.freeze({ get sequence() { return context.sequence; }, get phase() { return context.phase; }, signal, checkpoint, markExecuting: () => context.markExecuting(), enterCompletionCritical: () => { checkpoint(); context.enterCompletionCritical(); } });
|
|
42
|
+
this.#active += 1;
|
|
43
|
+
this.#emit({ ...eventIdentity, phase: "started" });
|
|
44
|
+
try {
|
|
45
|
+
const gate = this.#gates.get(this.#key(identity));
|
|
46
|
+
if (gate)
|
|
47
|
+
await this.#awaitGate(identity, gate, hooked);
|
|
48
|
+
checkpoint();
|
|
49
|
+
const result = await operation(hooked);
|
|
50
|
+
checkpoint();
|
|
51
|
+
this.#emit({ ...eventIdentity, phase: "terminal", outcome: "succeeded" });
|
|
52
|
+
return result;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const code = error?.code, stale = !this.accepts(identity) || generationSignal.aborted, deadline = code === "OPERATION_DEADLINE_EXCEEDED", cancelled = code === "REQUEST_CANCELLED" || context.signal.aborted;
|
|
56
|
+
if (stale)
|
|
57
|
+
this.#staleSettlements += 1;
|
|
58
|
+
this.#emit({ ...eventIdentity, phase: "terminal", outcome: stale ? "STALE_GENERATION" : deadline ? code : cancelled ? "REQUEST_CANCELLED" : "failed" });
|
|
59
|
+
if (stale)
|
|
60
|
+
this.#staleSettlements -= 1;
|
|
61
|
+
throw stale ? new H03TimeoutFixtureError() : error;
|
|
62
|
+
}
|
|
63
|
+
finally {
|
|
64
|
+
this.#active -= 1;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
// prettier-ignore
|
|
68
|
+
drainEvents() { const events = Object.freeze(this.#events); this.#events = []; return events; }
|
|
69
|
+
// prettier-ignore
|
|
70
|
+
snapshot() { return Object.freeze({ active: this.#active, held: this.#gates.size, abortListeners: this.#abortListeners, timers: 0, staleSettlements: this.#staleSettlements }); }
|
|
71
|
+
// prettier-ignore
|
|
72
|
+
async #awaitGate(identity, gate, context) { let rejectAbort; const aborted = new Promise((_, reject) => (rejectAbort = reject)); const abort = () => { this.#gates.delete(this.#key(identity)); try {
|
|
73
|
+
context.checkpoint();
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
rejectAbort(error);
|
|
77
|
+
} }; context.signal.addEventListener("abort", abort, { once: true }); this.#abortListeners += 1; try {
|
|
78
|
+
if (context.signal.aborted)
|
|
79
|
+
abort();
|
|
80
|
+
await Promise.race([gate.promise, aborted]);
|
|
81
|
+
}
|
|
82
|
+
finally {
|
|
83
|
+
context.signal.removeEventListener("abort", abort);
|
|
84
|
+
this.#abortListeners -= 1;
|
|
85
|
+
} }
|
|
86
|
+
// prettier-ignore
|
|
87
|
+
#emit(event) { const normalized = Object.freeze(Object.fromEntries(Object.entries(event).filter(([, value]) => value !== undefined))); this.#events.push(normalized); appendFileSync(path.join(this.#descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify(normalized)}\n`, { encoding: "utf8", flag: "a" }); }
|
|
88
|
+
// prettier-ignore
|
|
89
|
+
accepts(identity) { return identity.generation === this.#descriptor.generation && identity.ownerToken === this.#descriptor.ownerToken; }
|
|
90
|
+
// prettier-ignore
|
|
91
|
+
#assertCurrent(identity) { this.#assertIdentity(identity); if (!this.accepts(identity))
|
|
92
|
+
throw new H03TimeoutFixtureError(); }
|
|
93
|
+
// prettier-ignore
|
|
94
|
+
#assertIdentity(identity) { if (!Object.isFrozen(identity) || !FIXTURE_ID.test(identity.callId) || !FIXTURE_ID.test(identity.fixtureId) || !UUID.test(identity.correlationId))
|
|
95
|
+
throw new Error("Invalid H-05 lifecycle identity."); }
|
|
96
|
+
// prettier-ignore
|
|
97
|
+
#key(identity) { return `${identity.generation}:${identity.fixtureId}`; }
|
|
98
|
+
}
|
|
99
|
+
export class H03TimeoutFixtureController {
|
|
100
|
+
#descriptor;
|
|
101
|
+
#gates = new Map();
|
|
102
|
+
#events = [];
|
|
103
|
+
#generation;
|
|
104
|
+
#generationAbort = new AbortController();
|
|
105
|
+
#active = 0;
|
|
106
|
+
#abortListeners = 0;
|
|
107
|
+
#staleSettlements = 0;
|
|
108
|
+
constructor(descriptor) {
|
|
109
|
+
const directory = statSync(descriptor.controlDirectory, { throwIfNoEntry: false });
|
|
110
|
+
if (!directory?.isDirectory())
|
|
111
|
+
throw new Error("H-03 fixture control path must be an existing directory.");
|
|
112
|
+
this.#descriptor = descriptor;
|
|
113
|
+
this.#generation = descriptor.generation;
|
|
114
|
+
}
|
|
115
|
+
hold(fixtureId) {
|
|
116
|
+
this.#assertId(fixtureId);
|
|
117
|
+
const key = this.#key(fixtureId, this.#generation);
|
|
118
|
+
if (this.#gates.has(key))
|
|
119
|
+
throw new Error("Invalid or duplicate H-03 fixture id.");
|
|
120
|
+
let release;
|
|
121
|
+
const promise = new Promise((resolve) => (release = resolve));
|
|
122
|
+
this.#gates.set(key, { promise, release });
|
|
123
|
+
}
|
|
124
|
+
release(fixtureId, generation) {
|
|
125
|
+
if (!FIXTURE_ID.test(fixtureId) || generation !== this.#generation)
|
|
126
|
+
return false;
|
|
127
|
+
const key = this.#key(fixtureId, generation);
|
|
128
|
+
const gate = this.#gates.get(key);
|
|
129
|
+
if (!gate)
|
|
130
|
+
return false;
|
|
131
|
+
this.#gates.delete(key);
|
|
132
|
+
gate.release();
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
135
|
+
advanceGeneration(generation) {
|
|
136
|
+
if (!Number.isSafeInteger(generation) || generation <= this.#generation)
|
|
137
|
+
throw new Error("H-03 fixture generation must increase.");
|
|
138
|
+
this.#generation = generation;
|
|
139
|
+
this.#generationAbort.abort();
|
|
140
|
+
this.#generationAbort = new AbortController();
|
|
141
|
+
for (const gate of this.#gates.values())
|
|
142
|
+
gate.release();
|
|
143
|
+
this.#gates.clear();
|
|
144
|
+
}
|
|
145
|
+
async run(fixtureId, generation, context, operation, callId = fixtureId) {
|
|
146
|
+
this.#assertId(fixtureId);
|
|
147
|
+
this.#assertId(callId);
|
|
148
|
+
if (generation !== this.#generation)
|
|
149
|
+
return this.#stale(fixtureId, generation, callId);
|
|
150
|
+
const generationSignal = this.#generationAbort.signal;
|
|
151
|
+
const signal = AbortSignal.any([context.signal, generationSignal]);
|
|
152
|
+
const hooked = this.#context(context, signal, generationSignal, fixtureId, generation, callId);
|
|
153
|
+
this.#active += 1;
|
|
154
|
+
this.#emit({ callId, fixtureId, generation, nonce: this.#descriptor.nonce, phase: "started" });
|
|
155
|
+
try {
|
|
156
|
+
const gate = this.#gates.get(this.#key(fixtureId, generation));
|
|
157
|
+
if (gate)
|
|
158
|
+
await this.#awaitGate(this.#key(fixtureId, generation), gate, hooked);
|
|
159
|
+
hooked.checkpoint();
|
|
160
|
+
const result = await operation(hooked);
|
|
161
|
+
hooked.checkpoint();
|
|
162
|
+
return result;
|
|
163
|
+
}
|
|
164
|
+
finally {
|
|
165
|
+
this.#active -= 1;
|
|
166
|
+
this.#emit({
|
|
167
|
+
callId,
|
|
168
|
+
fixtureId,
|
|
169
|
+
generation,
|
|
170
|
+
nonce: this.#descriptor.nonce,
|
|
171
|
+
phase: "terminal",
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
drainEvents() {
|
|
176
|
+
const events = Object.freeze(this.#events);
|
|
177
|
+
this.#events = [];
|
|
178
|
+
return events;
|
|
179
|
+
}
|
|
180
|
+
snapshot() {
|
|
181
|
+
return Object.freeze({
|
|
182
|
+
active: this.#active,
|
|
183
|
+
held: this.#gates.size,
|
|
184
|
+
abortListeners: this.#abortListeners,
|
|
185
|
+
staleSettlements: this.#staleSettlements,
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
#context(context, signal, generationSignal, fixtureId, generation, callId) {
|
|
189
|
+
const checkpoint = () => {
|
|
190
|
+
if (generationSignal.aborted || generation !== this.#generation)
|
|
191
|
+
this.#stale(fixtureId, generation, callId);
|
|
192
|
+
context.checkpoint();
|
|
193
|
+
};
|
|
194
|
+
return {
|
|
195
|
+
get sequence() {
|
|
196
|
+
return context.sequence;
|
|
197
|
+
},
|
|
198
|
+
get phase() {
|
|
199
|
+
return context.phase;
|
|
200
|
+
},
|
|
201
|
+
signal,
|
|
202
|
+
checkpoint,
|
|
203
|
+
markExecuting: () => context.markExecuting(),
|
|
204
|
+
enterCompletionCritical: () => {
|
|
205
|
+
checkpoint();
|
|
206
|
+
context.enterCompletionCritical();
|
|
207
|
+
},
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
async #awaitGate(key, gate, context) {
|
|
211
|
+
let rejectAbort;
|
|
212
|
+
const aborted = new Promise((_, reject) => (rejectAbort = reject));
|
|
213
|
+
const abort = () => {
|
|
214
|
+
if (this.#gates.get(key) === gate)
|
|
215
|
+
this.#gates.delete(key);
|
|
216
|
+
try {
|
|
217
|
+
context.checkpoint();
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
rejectAbort(error);
|
|
221
|
+
}
|
|
222
|
+
};
|
|
223
|
+
context.signal.addEventListener("abort", abort, { once: true });
|
|
224
|
+
this.#abortListeners += 1;
|
|
225
|
+
try {
|
|
226
|
+
if (context.signal.aborted)
|
|
227
|
+
abort();
|
|
228
|
+
await Promise.race([gate.promise, aborted]);
|
|
229
|
+
}
|
|
230
|
+
finally {
|
|
231
|
+
context.signal.removeEventListener("abort", abort);
|
|
232
|
+
this.#abortListeners -= 1;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
#stale(fixtureId, generation, callId) {
|
|
236
|
+
this.#staleSettlements += 1;
|
|
237
|
+
this.#emit({ callId, fixtureId, generation, nonce: this.#descriptor.nonce, phase: "stale" });
|
|
238
|
+
throw new H03TimeoutFixtureError();
|
|
239
|
+
}
|
|
240
|
+
#emit(event) {
|
|
241
|
+
this.#events.push(Object.freeze(event));
|
|
242
|
+
appendFileSync(path.join(this.#descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify(event)}\n`, { encoding: "utf8", flag: "a" });
|
|
243
|
+
}
|
|
244
|
+
#assertId(fixtureId) {
|
|
245
|
+
if (!FIXTURE_ID.test(fixtureId))
|
|
246
|
+
throw new Error("Invalid H-03 fixture id.");
|
|
247
|
+
}
|
|
248
|
+
#key(fixtureId, generation) {
|
|
249
|
+
return `${generation}:${fixtureId}`;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
let configuredH05Controller;
|
|
253
|
+
let configuredH05Descriptor;
|
|
254
|
+
// Capture the closed command once before scheduler enqueue; mutable files cannot relabel it.
|
|
255
|
+
// prettier-ignore
|
|
256
|
+
export function captureConfiguredH05Command() { const raw = process.env.AST_H05_FIXTURE; if (raw === undefined)
|
|
257
|
+
return undefined; const descriptor = parseH05FixtureDescriptor(raw); if (!descriptor)
|
|
258
|
+
throw new Error("Invalid H-05 fixture descriptor."); if (configuredH05Descriptor) {
|
|
259
|
+
const sameGeneration = descriptor.generation === configuredH05Descriptor.generation, sameIdentity = descriptor.controlDirectory === configuredH05Descriptor.controlDirectory && descriptor.nonce === configuredH05Descriptor.nonce && descriptor.ownerToken === configuredH05Descriptor.ownerToken;
|
|
260
|
+
if (sameGeneration && !sameIdentity)
|
|
261
|
+
throw new Error("H-05 fixture descriptor drift.");
|
|
262
|
+
if (descriptor.generation < configuredH05Descriptor.generation)
|
|
263
|
+
throw new Error("H-05 fixture generation regressed.");
|
|
264
|
+
if (descriptor.generation > configuredH05Descriptor.generation) {
|
|
265
|
+
configuredH05Controller?.advance({ ...configuredH05Descriptor, generation: descriptor.generation });
|
|
266
|
+
configuredH05Controller = undefined;
|
|
267
|
+
configuredH05Descriptor = descriptor;
|
|
268
|
+
const replacement = new H05LifecycleFixtureController(descriptor);
|
|
269
|
+
configuredH05Controller = replacement;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
configuredH05Controller = new H05LifecycleFixtureController(descriptor);
|
|
274
|
+
configuredH05Descriptor = descriptor;
|
|
275
|
+
} if (!configuredH05Controller)
|
|
276
|
+
throw new Error("H-05 fixture generation is retired."); const value = JSON.parse(readFileSync(path.join(descriptor.controlDirectory, "command.json"), "utf8")); if (Object.keys(value).sort().join(",") !== "callId,correlationId,fixtureId,mode,nonce,ownerToken" || value.nonce !== descriptor.nonce || value.ownerToken !== descriptor.ownerToken || !["hold", "pass", "readback"].includes(value.mode))
|
|
277
|
+
throw new Error("Invalid H-05 fixture command."); const command = Object.freeze({ ...value }); const captured = configuredH05Controller.capture({ callId: command.callId, correlationId: command.correlationId, fixtureId: command.fixtureId }); return Object.freeze({ controller: configuredH05Controller, descriptor, command, captured }); }
|
|
278
|
+
// prettier-ignore
|
|
279
|
+
export async function runConfiguredH05Fixture(context, operation, configured) { const { controller, descriptor, command, captured } = configured; if (!controller.accepts(captured))
|
|
280
|
+
throw new H03TimeoutFixtureError(); if (command.mode === "readback") {
|
|
281
|
+
const snapshot = controller.snapshot(), eventsDrained = controller.drainEvents().length;
|
|
282
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ ...snapshot, callId: captured.callId, correlationId: captured.correlationId, eventsDrained, fixtureId: captured.fixtureId, generation: captured.generation, phase: "readback" })}\n`);
|
|
283
|
+
const result = await operation(context);
|
|
284
|
+
if (!controller.accepts(captured))
|
|
285
|
+
throw new H03TimeoutFixtureError();
|
|
286
|
+
context.checkpoint();
|
|
287
|
+
return result;
|
|
288
|
+
} if (command.mode === "hold")
|
|
289
|
+
controller.hold(captured); return controller.run(captured, context, operation); }
|
|
290
|
+
let configuredController;
|
|
291
|
+
let configuredDescriptor;
|
|
292
|
+
const errorCommands = new WeakMap();
|
|
293
|
+
// Compact closed test-only wiring preserves PR3's hard review budget.
|
|
294
|
+
// prettier-ignore
|
|
295
|
+
function configuredFixture() { const descriptor = parseH03FixtureDescriptor(process.env.AST_H03_FIXTURE); if (!descriptor)
|
|
296
|
+
return undefined; if (!configuredController || configuredDescriptor?.generation !== descriptor.generation) {
|
|
297
|
+
configuredDescriptor = descriptor;
|
|
298
|
+
configuredController = new H03TimeoutFixtureController(descriptor);
|
|
299
|
+
} return { controller: configuredController, descriptor }; }
|
|
300
|
+
// prettier-ignore
|
|
301
|
+
function readCommand(descriptor) { const value = JSON.parse(readFileSync(path.join(descriptor.controlDirectory, "command.json"), "utf8")); if (Object.keys(value).sort().join(",") !== "callId,fixtureId,mode,nonce" || typeof value.callId !== "string" || !FIXTURE_ID.test(value.callId) || typeof value.fixtureId !== "string" || !FIXTURE_ID.test(value.fixtureId) || value.nonce !== descriptor.nonce || !["hold", "pass", "readback"].includes(value.mode))
|
|
302
|
+
throw new Error("Invalid H-03 fixture command."); return value; }
|
|
303
|
+
// Capture once before scheduler enqueue; later command.json writes cannot relabel this request.
|
|
304
|
+
// prettier-ignore
|
|
305
|
+
export function captureConfiguredH03Command() { const fixture = configuredFixture(); return fixture && Object.freeze({ ...fixture, command: readCommand(fixture.descriptor) }); }
|
|
306
|
+
// prettier-ignore
|
|
307
|
+
export function bindConfiguredH03Error(error, captured) { if (captured && typeof error === "object" && error !== null)
|
|
308
|
+
errorCommands.set(error, captured); }
|
|
309
|
+
export function configuredH05CorrelationId(error) {
|
|
310
|
+
const configured = typeof error === "object" && error !== null ? errorCommands.get(error) : undefined;
|
|
311
|
+
return configured && "captured" in configured ? configured.captured.correlationId : undefined;
|
|
312
|
+
}
|
|
313
|
+
// prettier-ignore
|
|
314
|
+
export async function runConfiguredH03Fixture(context, operation, captured) { const { controller, descriptor, command } = captured; if (command.mode === "readback") {
|
|
315
|
+
const snapshot = controller.snapshot(), eventsDrained = controller.drainEvents().length;
|
|
316
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ ...snapshot, callId: command.callId, eventsDrained, fixtureId: command.fixtureId, generation: descriptor.generation, nonce: descriptor.nonce, phase: "readback" })}\n`);
|
|
317
|
+
return operation(context);
|
|
318
|
+
} if (command.mode === "hold")
|
|
319
|
+
controller.hold(command.fixtureId); return controller.run(command.fixtureId, descriptor.generation, context, operation, command.callId); }
|
|
320
|
+
// prettier-ignore
|
|
321
|
+
export function emitConfiguredH03HostEvent(phase, generation) { const descriptor = parseH03FixtureDescriptor(process.env.AST_H03_FIXTURE); if (!descriptor)
|
|
322
|
+
return; appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ fixtureId: "host", generation, nonce: descriptor.nonce, phase })}\n`, { encoding: "utf8", flag: "a" }); }
|
|
323
|
+
// prettier-ignore
|
|
324
|
+
export function emitConfiguredH03ErrorEvidence(text, error) { const configured = typeof error === "object" && error !== null ? errorCommands.get(error) : undefined; if (!configured)
|
|
325
|
+
return; const { descriptor, command } = configured; if (!("captured" in configured)) {
|
|
326
|
+
appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${JSON.stringify({ callId: command.callId, fixtureId: command.fixtureId, generation: descriptor.generation, nonce: descriptor.nonce, phase: "error", result: { isError: true, error: { info: { name: error instanceof Error ? error.name : "Error" }, message: text } } })}\n`);
|
|
327
|
+
return;
|
|
328
|
+
} const { controller, captured } = configured; if (!controller.accepts(captured))
|
|
329
|
+
return; let publicError; try {
|
|
330
|
+
publicError = JSON.parse(text);
|
|
331
|
+
}
|
|
332
|
+
catch {
|
|
333
|
+
publicError = undefined;
|
|
334
|
+
} const source = publicError?.error, code = typeof source?.code === "string" && PUBLIC_ERROR_CODES.some((value) => value === source.code) ? source.code : "INTERNAL_ERROR", correlation_id = typeof source?.correlation_id === "string" && UUID.test(source.correlation_id) ? source.correlation_id : captured.correlationId, name = error instanceof Error && /^[A-Za-z][A-Za-z0-9]{0,63}$/.test(error.name) ? error.name : "Error", message = typeof source?.message === "string" ? sanitizePublicText(source.message) : "An internal error occurred."; const identity = { callId: captured.callId, correlationId: captured.correlationId, fixtureId: captured.fixtureId, generation: captured.generation, ownerToken: captured.ownerToken, phase: "error", resources: controller.snapshot(), result: { isError: true, error: { code, message, correlation_id, info: { name } } } }; let serialized = JSON.stringify(identity); if (Buffer.byteLength(serialized) > 4096)
|
|
335
|
+
serialized = JSON.stringify({ ...identity, result: { ...identity.result, error: { ...identity.result.error, message: "Evidence message omitted: bounded record exceeded." } } }); appendFileSync(path.join(descriptor.controlDirectory, "events.jsonl"), `${serialized}\n`); }
|
|
@@ -3,6 +3,49 @@ import { link, mkdir, mkdtemp, open, readFile, rename, rm, stat, writeFile, } fr
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { applyEdits, modify, parse } from "jsonc-parser";
|
|
6
|
+
export class OpenCodeConfigRecoveryError extends Error {
|
|
7
|
+
constructor(message, options) {
|
|
8
|
+
super(message, options);
|
|
9
|
+
this.name = "OpenCodeConfigRecoveryError";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
function desiredAst(nodeExecutable, serverEntryPath) {
|
|
13
|
+
return {
|
|
14
|
+
type: "local",
|
|
15
|
+
command: [nodeExecutable, serverEntryPath],
|
|
16
|
+
enabled: true,
|
|
17
|
+
environment: { AST_MCP_APPLY_GUARD: "allow" },
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
export function classifyOpenCodeAstRegistration(value, nodeExecutable, serverEntryPath) {
|
|
21
|
+
if (value === undefined)
|
|
22
|
+
return "missing";
|
|
23
|
+
if (value === null || typeof value !== "object" || Array.isArray(value))
|
|
24
|
+
return "conflict";
|
|
25
|
+
const registration = value;
|
|
26
|
+
const command = registration.command;
|
|
27
|
+
const baseMatches = registration.type === "local" &&
|
|
28
|
+
registration.enabled === true &&
|
|
29
|
+
Array.isArray(command) &&
|
|
30
|
+
command.length === 2 &&
|
|
31
|
+
command[0] === nodeExecutable &&
|
|
32
|
+
command[1] === serverEntryPath;
|
|
33
|
+
if (!baseMatches)
|
|
34
|
+
return "conflict";
|
|
35
|
+
const keys = Object.keys(registration).sort().join(",");
|
|
36
|
+
if (keys === "command,enabled,type")
|
|
37
|
+
return "repairable";
|
|
38
|
+
if (keys !== "command,enabled,environment,type")
|
|
39
|
+
return "conflict";
|
|
40
|
+
const environment = registration.environment;
|
|
41
|
+
return environment !== null &&
|
|
42
|
+
typeof environment === "object" &&
|
|
43
|
+
!Array.isArray(environment) &&
|
|
44
|
+
Object.keys(environment).length === 1 &&
|
|
45
|
+
environment.AST_MCP_APPLY_GUARD === "allow"
|
|
46
|
+
? "current"
|
|
47
|
+
: "conflict";
|
|
48
|
+
}
|
|
6
49
|
function hash(value) {
|
|
7
50
|
return createHash("sha256").update(value).digest("hex");
|
|
8
51
|
}
|
|
@@ -80,14 +123,11 @@ export async function planOpenCodeConfig(options) {
|
|
|
80
123
|
}
|
|
81
124
|
if (!parsed || typeof parsed !== "object")
|
|
82
125
|
throw new Error("OpenCode configuration must contain an object.");
|
|
83
|
-
const desired =
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
enabled: true,
|
|
87
|
-
};
|
|
88
|
-
if (parsed.mcp?.ast !== undefined && JSON.stringify(parsed.mcp.ast) !== JSON.stringify(desired))
|
|
126
|
+
const desired = desiredAst(options.nodeExecutable, options.serverEntryPath);
|
|
127
|
+
const registrationStatus = classifyOpenCodeAstRegistration(parsed.mcp?.ast, options.nodeExecutable, options.serverEntryPath);
|
|
128
|
+
if (registrationStatus === "conflict")
|
|
89
129
|
throw new Error("OpenCode configuration conflict at mcp.ast.");
|
|
90
|
-
const content =
|
|
130
|
+
const content = registrationStatus === "missing" || registrationStatus === "repairable"
|
|
91
131
|
? applyEdits(before, modify(before, ["mcp", "ast"], desired, {
|
|
92
132
|
formattingOptions: { insertSpaces: true, tabSize: 2, eol: "\n" },
|
|
93
133
|
}))
|
|
@@ -96,6 +136,7 @@ export async function planOpenCodeConfig(options) {
|
|
|
96
136
|
filePath: options.filePath,
|
|
97
137
|
beforeHash: hash(beforeExists ? before : ""),
|
|
98
138
|
beforeExists,
|
|
139
|
+
beforeContent: beforeExists ? before : "",
|
|
99
140
|
mode,
|
|
100
141
|
content,
|
|
101
142
|
status: content === before ? "unchanged" : "installed",
|
|
@@ -146,3 +187,39 @@ export async function applyOpenCodeConfigPlan(plan) {
|
|
|
146
187
|
await rm(temporary, { force: true });
|
|
147
188
|
}
|
|
148
189
|
}
|
|
190
|
+
async function inspectOpenCodeConfigPlanState(plan) {
|
|
191
|
+
let content = "";
|
|
192
|
+
let exists = true;
|
|
193
|
+
let mode;
|
|
194
|
+
try {
|
|
195
|
+
content = await readFile(plan.filePath, "utf8");
|
|
196
|
+
mode = (await stat(plan.filePath)).mode & 0o777;
|
|
197
|
+
}
|
|
198
|
+
catch (error) {
|
|
199
|
+
if (error.code === "ENOENT")
|
|
200
|
+
exists = false;
|
|
201
|
+
else
|
|
202
|
+
throw error;
|
|
203
|
+
}
|
|
204
|
+
if (!plan.beforeExists && !exists)
|
|
205
|
+
return "preimage";
|
|
206
|
+
if (plan.beforeExists && exists && content === plan.beforeContent && mode === plan.mode)
|
|
207
|
+
return "preimage";
|
|
208
|
+
if (exists && content === plan.content && mode === plan.mode)
|
|
209
|
+
return "postimage";
|
|
210
|
+
return "conflict";
|
|
211
|
+
}
|
|
212
|
+
export async function runOpenCodeConfigTransaction(plan, operation) {
|
|
213
|
+
try {
|
|
214
|
+
return await operation();
|
|
215
|
+
}
|
|
216
|
+
catch (error) {
|
|
217
|
+
const state = await inspectOpenCodeConfigPlanState(plan);
|
|
218
|
+
if (state === "postimage")
|
|
219
|
+
return undefined;
|
|
220
|
+
if (state === "conflict") {
|
|
221
|
+
throw new OpenCodeConfigRecoveryError("OpenCode configuration changed concurrently after an ambiguous write outcome; the concurrent bytes were preserved.", { cause: error });
|
|
222
|
+
}
|
|
223
|
+
throw error;
|
|
224
|
+
}
|
|
225
|
+
}
|
package/dist/services/project.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
3
|
import { Project } from "ts-morph";
|
|
4
|
+
// prettier-ignore
|
|
5
|
+
import { bindConfiguredH03Error, captureConfiguredH03Command, captureConfiguredH05Command, runConfiguredH03Fixture, runConfiguredH05Fixture } from "./h03-timeout-fixture.js";
|
|
4
6
|
import { InMemorySymbolIndex, SYMBOL_INDEX_SCHEMA_VERSION, } from "./symbol-index.js";
|
|
5
7
|
import { addSymbolIndexRuntimeCount, createInitialSymbolIndexRuntimeObservability, readSymbolIndexPersistencePolicy, symbolIndexCachePath, symbolIndexPolicyKey, } from "./symbol-index-policy.js";
|
|
6
8
|
import { openSQLiteSymbolIndexStore, quarantineSQLiteSymbolIndexFile, } from "./symbol-index-sqlite.js";
|
|
@@ -650,22 +652,33 @@ async function synchronizeSession(session, requestContext) {
|
|
|
650
652
|
throw error;
|
|
651
653
|
}
|
|
652
654
|
}
|
|
653
|
-
async function runSessionWithSyncPolicy(session, operation, allowSynchronizationFailure, requestContext) {
|
|
655
|
+
async function runSessionWithSyncPolicy(session, operation, allowSynchronizationFailure, requestContext, h03Fixture = false) {
|
|
654
656
|
requestContext.checkpoint();
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
657
|
+
const h05Command = h03Fixture ? captureConfiguredH05Command() : undefined;
|
|
658
|
+
// prettier-ignore
|
|
659
|
+
const h03Command = h05Command ? undefined : h03Fixture ? captureConfiguredH03Command() : undefined;
|
|
660
|
+
// prettier-ignore
|
|
661
|
+
try {
|
|
662
|
+
return await session.scheduler.run(async (operationContext) => {
|
|
663
|
+
operationContext.checkpoint();
|
|
664
|
+
try {
|
|
665
|
+
await synchronizeSession(session, operationContext);
|
|
666
|
+
}
|
|
667
|
+
catch (error) {
|
|
668
|
+
if (!allowSynchronizationFailure || isCooperativeInterruption(error))
|
|
669
|
+
throw error;
|
|
670
|
+
}
|
|
671
|
+
operationContext.checkpoint();
|
|
672
|
+
operationContext.markExecuting();
|
|
673
|
+
session.lastAccessSequence = nextProjectSessionAccessSequence();
|
|
674
|
+
const execute = (context) => operation(session.context, context);
|
|
675
|
+
return h05Command ? runConfiguredH05Fixture(operationContext, execute, h05Command) : h03Command ? runConfiguredH03Fixture(operationContext, execute, h03Command) : execute(operationContext);
|
|
676
|
+
}, { signal: requestContext.signal });
|
|
677
|
+
}
|
|
678
|
+
catch (error) {
|
|
679
|
+
bindConfiguredH03Error(error, h05Command ?? h03Command);
|
|
680
|
+
throw error;
|
|
681
|
+
}
|
|
669
682
|
}
|
|
670
683
|
export async function withProject(projectRoot, operation, requestContext = NO_REQUEST_CONTEXT) {
|
|
671
684
|
requestContext.checkpoint();
|
|
@@ -707,7 +720,7 @@ function readProjectStatus(session, requestContext) {
|
|
|
707
720
|
index_observability: { ...effectiveContext.symbolIndexObservability },
|
|
708
721
|
operation_queue: getQueueStatus(session),
|
|
709
722
|
};
|
|
710
|
-
}, true, requestContext);
|
|
723
|
+
}, true, requestContext, true);
|
|
711
724
|
}
|
|
712
725
|
function peekSessionStatus(session) {
|
|
713
726
|
const context = session.context;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Buffer } from "node:buffer";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { types as utilTypes } from "node:util";
|
|
4
|
+
import { configuredH05CorrelationId, emitConfiguredH03ErrorEvidence, } from "./h03-timeout-fixture.js";
|
|
4
5
|
export const PUBLIC_ERROR_CODES = Object.freeze([
|
|
5
6
|
"INVALID_INPUT",
|
|
6
7
|
"PROJECT_NOT_FOUND",
|
|
@@ -240,12 +241,13 @@ function serializePublicError(code, message, correlationId) {
|
|
|
240
241
|
};
|
|
241
242
|
return { envelope, text: JSON.stringify(envelope) };
|
|
242
243
|
}
|
|
243
|
-
export function renderPublicError(error, correlationId = randomUUID()) {
|
|
244
|
+
export function renderPublicError(error, correlationId = configuredH05CorrelationId(error) ?? randomUUID()) {
|
|
244
245
|
const classified = classifyPublicError(error);
|
|
246
|
+
const emitted = (value) => (emitConfiguredH03ErrorEvidence(value.text, error),
|
|
247
|
+
value);
|
|
245
248
|
const initial = serializePublicError(classified.code, classified.message, correlationId);
|
|
246
|
-
if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES)
|
|
247
|
-
return initial;
|
|
248
|
-
}
|
|
249
|
+
if (Buffer.byteLength(initial.text, "utf8") <= MAX_PUBLIC_ERROR_RESPONSE_BYTES)
|
|
250
|
+
return emitted(initial);
|
|
249
251
|
let lowerBudget = Buffer.byteLength(TRUNCATION_SUFFIX, "utf8");
|
|
250
252
|
let upperBudget = Buffer.byteLength(classified.message, "utf8") - 1;
|
|
251
253
|
let best = serializePublicError(classified.code, TRUNCATION_SUFFIX, correlationId);
|
|
@@ -260,5 +262,5 @@ export function renderPublicError(error, correlationId = randomUUID()) {
|
|
|
260
262
|
upperBudget = candidateBudget - 1;
|
|
261
263
|
}
|
|
262
264
|
}
|
|
263
|
-
return best;
|
|
265
|
+
return emitted(best);
|
|
264
266
|
}
|