@neurcode-ai/cli 0.9.62 → 0.9.64
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/dist/commands/control-plane.js +7 -7
- package/dist/commands/control-plane.js.map +1 -1
- package/dist/commands/fix.d.ts.map +1 -1
- package/dist/commands/fix.js +108 -1
- package/dist/commands/fix.js.map +1 -1
- package/dist/commands/patch-apply.d.ts +2 -0
- package/dist/commands/patch-apply.d.ts.map +1 -1
- package/dist/commands/patch-apply.js +331 -19
- package/dist/commands/patch-apply.js.map +1 -1
- package/dist/commands/replay.js +5 -5
- package/dist/commands/replay.js.map +1 -1
- package/dist/commands/verify.d.ts.map +1 -1
- package/dist/commands/verify.js +29 -1
- package/dist/commands/verify.js.map +1 -1
- package/dist/commands/workspace.js +7 -7
- package/dist/commands/workspace.js.map +1 -1
- package/dist/daemon/server.d.ts +2 -2
- package/dist/daemon/server.d.ts.map +1 -1
- package/dist/daemon/server.js +1054 -36
- package/dist/daemon/server.js.map +1 -1
- package/dist/index.js +15 -4
- package/dist/index.js.map +1 -1
- package/dist/intent-engine/matcher.d.ts.map +1 -1
- package/dist/intent-engine/matcher.js +2 -0
- package/dist/intent-engine/matcher.js.map +1 -1
- package/dist/patch-engine/diff.d.ts +1 -1
- package/dist/patch-engine/diff.js +1 -1
- package/dist/patch-engine/generator.d.ts +9 -0
- package/dist/patch-engine/generator.d.ts.map +1 -1
- package/dist/patch-engine/generator.js +375 -17
- package/dist/patch-engine/generator.js.map +1 -1
- package/dist/patch-engine/index.d.ts +25 -25
- package/dist/patch-engine/index.d.ts.map +1 -1
- package/dist/patch-engine/index.js +134 -87
- package/dist/patch-engine/index.js.map +1 -1
- package/dist/patch-engine/patterns.d.ts +1 -1
- package/dist/patch-engine/patterns.d.ts.map +1 -1
- package/dist/patch-engine/patterns.js +277 -40
- package/dist/patch-engine/patterns.js.map +1 -1
- package/dist/patch-engine/rollback.d.ts +31 -0
- package/dist/patch-engine/rollback.d.ts.map +1 -0
- package/dist/patch-engine/rollback.js +275 -0
- package/dist/patch-engine/rollback.js.map +1 -0
- package/dist/patch-engine/safety.d.ts +28 -0
- package/dist/patch-engine/safety.d.ts.map +1 -0
- package/dist/patch-engine/safety.js +122 -0
- package/dist/patch-engine/safety.js.map +1 -0
- package/dist/patch-engine/transaction.d.ts +52 -0
- package/dist/patch-engine/transaction.d.ts.map +1 -0
- package/dist/patch-engine/transaction.js +93 -0
- package/dist/patch-engine/transaction.js.map +1 -0
- package/dist/utils/advisory-signals.d.ts +5 -0
- package/dist/utils/advisory-signals.d.ts.map +1 -1
- package/dist/utils/advisory-signals.js +50 -12
- package/dist/utils/advisory-signals.js.map +1 -1
- package/dist/utils/ai-debt-budget.d.ts.map +1 -1
- package/dist/utils/ai-debt-budget.js +5 -2
- package/dist/utils/ai-debt-budget.js.map +1 -1
- package/dist/utils/cli-json.d.ts.map +1 -1
- package/dist/utils/cli-json.js +80 -12
- package/dist/utils/cli-json.js.map +1 -1
- package/dist/utils/execution-bus.d.ts +10 -0
- package/dist/utils/execution-bus.d.ts.map +1 -1
- package/dist/utils/execution-bus.js +16 -0
- package/dist/utils/execution-bus.js.map +1 -1
- package/dist/utils/policy-compiler.d.ts +6 -0
- package/dist/utils/policy-compiler.d.ts.map +1 -1
- package/dist/utils/policy-compiler.js +20 -0
- package/dist/utils/policy-compiler.js.map +1 -1
- package/package.json +9 -8
- package/LICENSE +0 -201
package/dist/daemon/server.js
CHANGED
|
@@ -39,20 +39,181 @@ exports.startDaemon = startDaemon;
|
|
|
39
39
|
const http = __importStar(require("node:http"));
|
|
40
40
|
const path = __importStar(require("node:path"));
|
|
41
41
|
const fs = __importStar(require("node:fs"));
|
|
42
|
+
const node_crypto_1 = require("node:crypto");
|
|
43
|
+
const node_child_process_1 = require("node:child_process");
|
|
44
|
+
const patch_engine_1 = require("../patch-engine");
|
|
45
|
+
const diff_1 = require("../patch-engine/diff");
|
|
42
46
|
const execution_bus_1 = require("../utils/execution-bus");
|
|
43
47
|
const runtime_events_1 = require("../utils/runtime-events");
|
|
44
48
|
const control_plane_1 = require("../utils/control-plane");
|
|
45
49
|
const workspace_runtime_1 = require("../utils/workspace-runtime");
|
|
46
50
|
const replay_runtime_1 = require("../utils/replay-runtime");
|
|
51
|
+
const contracts_1 = require("@neurcode-ai/contracts");
|
|
47
52
|
// ── Configuration ──────────────────────────────────────────────────────────────
|
|
48
|
-
exports.DAEMON_PORT = 4321;
|
|
49
|
-
exports.DAEMON_HOST = '127.0.0.1';
|
|
53
|
+
exports.DAEMON_PORT = Number.parseInt(process.env.NEURCODE_DAEMON_PORT || '4321', 10) || 4321;
|
|
54
|
+
exports.DAEMON_HOST = process.env.NEURCODE_DAEMON_HOST || '127.0.0.1';
|
|
50
55
|
const SSE_RETRY_MS = 3000;
|
|
56
|
+
const REQUEST_ID_HEADER = 'x-neurcode-request-id';
|
|
57
|
+
const ALLOW_NON_LOOPBACK = ['1', 'true', 'yes', 'on'].includes(String(process.env.NEURCODE_DAEMON_ALLOW_REMOTE || '').trim().toLowerCase());
|
|
51
58
|
const runtimeEventClients = new Map();
|
|
52
59
|
let runtimeEventClientSeq = 0;
|
|
53
60
|
let runtimeEventUnsubscribe = null;
|
|
54
61
|
let runtimeEventTailTimer = null;
|
|
55
62
|
let runtimeEventTailCursor = null;
|
|
63
|
+
const DAEMON_MAX_ERROR_HISTORY = 40;
|
|
64
|
+
const DAEMON_MAX_ROUTE_SAMPLE = 1000;
|
|
65
|
+
const daemonOpsMetrics = {
|
|
66
|
+
startedAt: new Date().toISOString(),
|
|
67
|
+
requestsTotal: 0,
|
|
68
|
+
requestsByMethod: {},
|
|
69
|
+
requestsByRoute: {},
|
|
70
|
+
failuresTotal: 0,
|
|
71
|
+
retriableFailuresTotal: 0,
|
|
72
|
+
stalePreviewRejections: 0,
|
|
73
|
+
rollbackStaleRejections: 0,
|
|
74
|
+
patchApplied: 0,
|
|
75
|
+
patchPartial: 0,
|
|
76
|
+
patchRejected: 0,
|
|
77
|
+
rollbackApplied: 0,
|
|
78
|
+
rollbackRejected: 0,
|
|
79
|
+
recentErrors: [],
|
|
80
|
+
};
|
|
81
|
+
function normalizeRoutePath(url) {
|
|
82
|
+
const pathOnly = url.split('?')[0]?.trim() || '/';
|
|
83
|
+
return pathOnly.startsWith('/') ? pathOnly : `/${pathOnly}`;
|
|
84
|
+
}
|
|
85
|
+
function incrementMetricCounter(record, key) {
|
|
86
|
+
record[key] = (record[key] || 0) + 1;
|
|
87
|
+
}
|
|
88
|
+
function recordDaemonRequest(url, method) {
|
|
89
|
+
daemonOpsMetrics.requestsTotal += 1;
|
|
90
|
+
incrementMetricCounter(daemonOpsMetrics.requestsByMethod, method.toUpperCase());
|
|
91
|
+
const route = normalizeRoutePath(url);
|
|
92
|
+
incrementMetricCounter(daemonOpsMetrics.requestsByRoute, route);
|
|
93
|
+
// Keep route cardinality bounded for long-lived daemon sessions.
|
|
94
|
+
const routeKeys = Object.keys(daemonOpsMetrics.requestsByRoute);
|
|
95
|
+
if (routeKeys.length > DAEMON_MAX_ROUTE_SAMPLE) {
|
|
96
|
+
const overflow = routeKeys
|
|
97
|
+
.sort((left, right) => (daemonOpsMetrics.requestsByRoute[left] || 0) - (daemonOpsMetrics.requestsByRoute[right] || 0))
|
|
98
|
+
.slice(0, routeKeys.length - DAEMON_MAX_ROUTE_SAMPLE);
|
|
99
|
+
for (const key of overflow) {
|
|
100
|
+
delete daemonOpsMetrics.requestsByRoute[key];
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function recordDaemonFailure(sample) {
|
|
105
|
+
daemonOpsMetrics.failuresTotal += 1;
|
|
106
|
+
if (sample.retriable) {
|
|
107
|
+
daemonOpsMetrics.retriableFailuresTotal += 1;
|
|
108
|
+
}
|
|
109
|
+
daemonOpsMetrics.recentErrors.unshift({
|
|
110
|
+
at: new Date().toISOString(),
|
|
111
|
+
...sample,
|
|
112
|
+
});
|
|
113
|
+
if (daemonOpsMetrics.recentErrors.length > DAEMON_MAX_ERROR_HISTORY) {
|
|
114
|
+
daemonOpsMetrics.recentErrors.length = DAEMON_MAX_ERROR_HISTORY;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
function recordPatchOutcome(status) {
|
|
118
|
+
if (status === 'applied') {
|
|
119
|
+
daemonOpsMetrics.patchApplied += 1;
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
if (status === 'partial') {
|
|
123
|
+
daemonOpsMetrics.patchPartial += 1;
|
|
124
|
+
return;
|
|
125
|
+
}
|
|
126
|
+
if (status === 'stale_preview') {
|
|
127
|
+
daemonOpsMetrics.patchRejected += 1;
|
|
128
|
+
daemonOpsMetrics.stalePreviewRejections += 1;
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
if (status === 'rollback_applied') {
|
|
132
|
+
daemonOpsMetrics.rollbackApplied += 1;
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
if (status === 'rollback_stale') {
|
|
136
|
+
daemonOpsMetrics.rollbackRejected += 1;
|
|
137
|
+
daemonOpsMetrics.rollbackStaleRejections += 1;
|
|
138
|
+
return;
|
|
139
|
+
}
|
|
140
|
+
if (status === 'rollback_rejected') {
|
|
141
|
+
daemonOpsMetrics.rollbackRejected += 1;
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
daemonOpsMetrics.patchRejected += 1;
|
|
145
|
+
}
|
|
146
|
+
function buildDaemonOperationalSummary(cwd) {
|
|
147
|
+
const uptimeSeconds = Math.max(0, Math.floor((Date.now() - new Date(daemonOpsMetrics.startedAt).getTime()) / 1000));
|
|
148
|
+
const lockPath = path.resolve(cwd, '.neurcode', 'executions', '.lock');
|
|
149
|
+
let lockPresent = false;
|
|
150
|
+
let lockAgeMs = null;
|
|
151
|
+
try {
|
|
152
|
+
const stat = fs.statSync(lockPath);
|
|
153
|
+
lockPresent = stat.isFile();
|
|
154
|
+
lockAgeMs = Date.now() - stat.mtimeMs;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
lockPresent = false;
|
|
158
|
+
lockAgeMs = null;
|
|
159
|
+
}
|
|
160
|
+
const executionQuery = (0, execution_bus_1.queryExecutions)(cwd, {
|
|
161
|
+
limit: 200,
|
|
162
|
+
status: 'all',
|
|
163
|
+
});
|
|
164
|
+
const items = executionQuery.items || [];
|
|
165
|
+
const activeExecutions = items.filter((item) => item.status !== 'completed' && item.status !== 'failed').length;
|
|
166
|
+
const recentFailures = items
|
|
167
|
+
.filter((item) => item.status === 'failed')
|
|
168
|
+
.slice(0, 10)
|
|
169
|
+
.map((item) => ({
|
|
170
|
+
id: item.id,
|
|
171
|
+
type: item.type,
|
|
172
|
+
source: item.source,
|
|
173
|
+
actor: item.actor,
|
|
174
|
+
completedAt: item.completedAt,
|
|
175
|
+
message: item.result?.message || null,
|
|
176
|
+
}));
|
|
177
|
+
const patchAttempts = daemonOpsMetrics.patchApplied + daemonOpsMetrics.patchPartial + daemonOpsMetrics.patchRejected;
|
|
178
|
+
const rollbackAttempts = daemonOpsMetrics.rollbackApplied + daemonOpsMetrics.rollbackRejected;
|
|
179
|
+
return {
|
|
180
|
+
uptimeSeconds,
|
|
181
|
+
activeExecutions,
|
|
182
|
+
sseClients: runtimeEventClients.size,
|
|
183
|
+
requestTotals: {
|
|
184
|
+
total: daemonOpsMetrics.requestsTotal,
|
|
185
|
+
failures: daemonOpsMetrics.failuresTotal,
|
|
186
|
+
retriableFailures: daemonOpsMetrics.retriableFailuresTotal,
|
|
187
|
+
byMethod: daemonOpsMetrics.requestsByMethod,
|
|
188
|
+
topRoutes: Object.entries(daemonOpsMetrics.requestsByRoute)
|
|
189
|
+
.sort((left, right) => right[1] - left[1])
|
|
190
|
+
.slice(0, 12)
|
|
191
|
+
.map(([route, count]) => ({ route, count })),
|
|
192
|
+
},
|
|
193
|
+
patchStats: {
|
|
194
|
+
attempts: patchAttempts,
|
|
195
|
+
applied: daemonOpsMetrics.patchApplied,
|
|
196
|
+
partial: daemonOpsMetrics.patchPartial,
|
|
197
|
+
rejected: daemonOpsMetrics.patchRejected,
|
|
198
|
+
stalePreviewRejections: daemonOpsMetrics.stalePreviewRejections,
|
|
199
|
+
successRate: patchAttempts > 0 ? Number((daemonOpsMetrics.patchApplied / patchAttempts).toFixed(4)) : null,
|
|
200
|
+
},
|
|
201
|
+
rollbackStats: {
|
|
202
|
+
attempts: rollbackAttempts,
|
|
203
|
+
applied: daemonOpsMetrics.rollbackApplied,
|
|
204
|
+
rejected: daemonOpsMetrics.rollbackRejected,
|
|
205
|
+
staleRejections: daemonOpsMetrics.rollbackStaleRejections,
|
|
206
|
+
successRate: rollbackAttempts > 0 ? Number((daemonOpsMetrics.rollbackApplied / rollbackAttempts).toFixed(4)) : null,
|
|
207
|
+
},
|
|
208
|
+
executionLock: {
|
|
209
|
+
path: lockPath,
|
|
210
|
+
present: lockPresent,
|
|
211
|
+
ageMs: lockAgeMs,
|
|
212
|
+
},
|
|
213
|
+
recentFailures,
|
|
214
|
+
recentErrors: daemonOpsMetrics.recentErrors,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
56
217
|
// ── Request helpers ────────────────────────────────────────────────────────────
|
|
57
218
|
function readBody(req) {
|
|
58
219
|
return new Promise((resolve, reject) => {
|
|
@@ -63,7 +224,18 @@ function readBody(req) {
|
|
|
63
224
|
});
|
|
64
225
|
}
|
|
65
226
|
function send(res, status, body) {
|
|
66
|
-
const
|
|
227
|
+
const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
|
|
228
|
+
const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
|
|
229
|
+
? requestIdRaw.trim()
|
|
230
|
+
: null;
|
|
231
|
+
const payloadBody = (requestId
|
|
232
|
+
&& body
|
|
233
|
+
&& typeof body === 'object'
|
|
234
|
+
&& !Array.isArray(body)
|
|
235
|
+
&& !Object.prototype.hasOwnProperty.call(body, 'requestId'))
|
|
236
|
+
? { ...body, requestId }
|
|
237
|
+
: body;
|
|
238
|
+
const payload = JSON.stringify(payloadBody);
|
|
67
239
|
res.writeHead(status, {
|
|
68
240
|
'Content-Type': 'application/json',
|
|
69
241
|
'Content-Length': Buffer.byteLength(payload),
|
|
@@ -73,18 +245,349 @@ function send(res, status, body) {
|
|
|
73
245
|
function success(res, data) {
|
|
74
246
|
send(res, 200, { success: true, data });
|
|
75
247
|
}
|
|
76
|
-
function
|
|
77
|
-
|
|
248
|
+
function defaultFailureCode(status, error) {
|
|
249
|
+
if (status === 400)
|
|
250
|
+
return contracts_1.DAEMON_ERROR_CODES.badRequest;
|
|
251
|
+
if (status === 401)
|
|
252
|
+
return contracts_1.DAEMON_ERROR_CODES.unauthorized;
|
|
253
|
+
if (status === 403)
|
|
254
|
+
return contracts_1.DAEMON_ERROR_CODES.forbidden;
|
|
255
|
+
if (status === 404) {
|
|
256
|
+
if (/no route for/i.test(error))
|
|
257
|
+
return contracts_1.DAEMON_ERROR_CODES.routeNotFound;
|
|
258
|
+
return contracts_1.DAEMON_ERROR_CODES.notFound;
|
|
259
|
+
}
|
|
260
|
+
if (status === 408)
|
|
261
|
+
return contracts_1.DAEMON_ERROR_CODES.timeout;
|
|
262
|
+
if (status === 409)
|
|
263
|
+
return contracts_1.DAEMON_ERROR_CODES.conflict;
|
|
264
|
+
if (status === 422)
|
|
265
|
+
return contracts_1.DAEMON_ERROR_CODES.validationFailed;
|
|
266
|
+
if (status === 429)
|
|
267
|
+
return contracts_1.DAEMON_ERROR_CODES.rateLimited;
|
|
268
|
+
if (status >= 500)
|
|
269
|
+
return contracts_1.DAEMON_ERROR_CODES.internalError;
|
|
270
|
+
return contracts_1.DAEMON_ERROR_CODES.unknown;
|
|
271
|
+
}
|
|
272
|
+
function failure(res, error, status = 500, options = {}) {
|
|
273
|
+
const code = options.code ?? defaultFailureCode(status, error);
|
|
274
|
+
const retriable = options.retriable ?? status >= 500;
|
|
275
|
+
const requestIdRaw = res.getHeader(REQUEST_ID_HEADER);
|
|
276
|
+
const requestId = typeof requestIdRaw === 'string' && requestIdRaw.trim().length > 0
|
|
277
|
+
? requestIdRaw.trim()
|
|
278
|
+
: null;
|
|
279
|
+
const route = res.__neurcodeRoutePath || '/unknown';
|
|
280
|
+
recordDaemonFailure({
|
|
281
|
+
route,
|
|
282
|
+
requestId,
|
|
283
|
+
code,
|
|
284
|
+
message: error,
|
|
285
|
+
retriable,
|
|
286
|
+
});
|
|
287
|
+
send(res, status, {
|
|
288
|
+
success: false,
|
|
289
|
+
error,
|
|
290
|
+
code,
|
|
291
|
+
retriable,
|
|
292
|
+
details: options.details ?? null,
|
|
293
|
+
});
|
|
78
294
|
}
|
|
79
|
-
function addCorsHeaders(res,
|
|
295
|
+
function addCorsHeaders(res, req) {
|
|
80
296
|
// Wildcard is safe: daemon binds to 127.0.0.1 only and isLoopback() rejects
|
|
81
297
|
// any non-local TCP connection. CORS * just lets browsers read the response
|
|
82
298
|
// regardless of what origin the dashboard is served from (local dev, prod domain, etc).
|
|
299
|
+
const requestedHeadersRaw = req.headers['access-control-request-headers'];
|
|
300
|
+
const requestedHeaders = Array.isArray(requestedHeadersRaw)
|
|
301
|
+
? requestedHeadersRaw.join(',')
|
|
302
|
+
: (requestedHeadersRaw || '');
|
|
303
|
+
const allowedHeaders = new Set(['content-type', 'x-neurcode-source', 'x-neurcode-actor', REQUEST_ID_HEADER]
|
|
304
|
+
.concat(requestedHeaders.split(',').map((entry) => entry.trim().toLowerCase()).filter(Boolean)));
|
|
83
305
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
|
84
|
-
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, OPTIONS');
|
|
85
|
-
res.setHeader('Access-Control-Allow-Headers', '
|
|
306
|
+
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, PATCH, DELETE, OPTIONS');
|
|
307
|
+
res.setHeader('Access-Control-Allow-Headers', [...allowedHeaders].join(', '));
|
|
86
308
|
res.setHeader('Access-Control-Max-Age', '86400');
|
|
87
309
|
}
|
|
310
|
+
function resolveGitRoot(cwd) {
|
|
311
|
+
const result = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
|
|
312
|
+
encoding: 'utf-8',
|
|
313
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
314
|
+
});
|
|
315
|
+
if (result.status !== 0)
|
|
316
|
+
return null;
|
|
317
|
+
const value = typeof result.stdout === 'string' ? result.stdout.trim() : '';
|
|
318
|
+
return value.length > 0 ? value : null;
|
|
319
|
+
}
|
|
320
|
+
function captureGitDirtyPaths(cwd) {
|
|
321
|
+
const gitRoot = resolveGitRoot(cwd);
|
|
322
|
+
if (!gitRoot)
|
|
323
|
+
return null;
|
|
324
|
+
const statusResult = (0, node_child_process_1.spawnSync)('git', ['-C', cwd, 'status', '--porcelain=1', '-z', '--untracked-files=all'], {
|
|
325
|
+
encoding: 'utf-8',
|
|
326
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
327
|
+
});
|
|
328
|
+
if (statusResult.status !== 0 || typeof statusResult.stdout !== 'string')
|
|
329
|
+
return null;
|
|
330
|
+
const tokens = statusResult.stdout.split('\0').filter((entry) => entry.length > 0);
|
|
331
|
+
const dirty = new Set();
|
|
332
|
+
for (let index = 0; index < tokens.length; index += 1) {
|
|
333
|
+
const token = tokens[index];
|
|
334
|
+
if (token.length < 4)
|
|
335
|
+
continue;
|
|
336
|
+
const status = token.slice(0, 2);
|
|
337
|
+
const filePath = token.slice(3).trim();
|
|
338
|
+
if (filePath.length > 0) {
|
|
339
|
+
dirty.add(path.resolve(gitRoot, filePath));
|
|
340
|
+
}
|
|
341
|
+
const renamedOrCopied = status.includes('R') || status.includes('C');
|
|
342
|
+
if (renamedOrCopied && index + 1 < tokens.length) {
|
|
343
|
+
index += 1;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
return dirty;
|
|
347
|
+
}
|
|
348
|
+
function hashFileForDiff(absPath) {
|
|
349
|
+
try {
|
|
350
|
+
const stat = fs.statSync(absPath);
|
|
351
|
+
if (!stat.isFile())
|
|
352
|
+
return '<non-file>';
|
|
353
|
+
const content = fs.readFileSync(absPath);
|
|
354
|
+
return (0, node_crypto_1.createHash)('sha256').update(content).digest('hex');
|
|
355
|
+
}
|
|
356
|
+
catch {
|
|
357
|
+
return '<missing>';
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
function captureDirtyFileFingerprints(cwd) {
|
|
361
|
+
const dirtyPaths = captureGitDirtyPaths(cwd);
|
|
362
|
+
if (!dirtyPaths)
|
|
363
|
+
return null;
|
|
364
|
+
const map = new Map();
|
|
365
|
+
for (const dirtyPath of dirtyPaths) {
|
|
366
|
+
map.set(dirtyPath, hashFileForDiff(dirtyPath));
|
|
367
|
+
}
|
|
368
|
+
return map;
|
|
369
|
+
}
|
|
370
|
+
function isAllowedPatchSideEffect(absPath, targetAbsPath, cwd) {
|
|
371
|
+
if (absPath === targetAbsPath)
|
|
372
|
+
return true;
|
|
373
|
+
const rel = path.relative(cwd, absPath);
|
|
374
|
+
if (!rel || rel.startsWith('..'))
|
|
375
|
+
return false;
|
|
376
|
+
if (rel === 'neurcode.policy.compiled.json')
|
|
377
|
+
return true;
|
|
378
|
+
return rel === '.neurcode' || rel.startsWith(`.neurcode${path.sep}`);
|
|
379
|
+
}
|
|
380
|
+
function collectUnexpectedPatchSideEffects(before, after, targetAbsPath, cwd) {
|
|
381
|
+
if (!before || !after)
|
|
382
|
+
return [];
|
|
383
|
+
const added = [...after].filter((entry) => !before.has(entry));
|
|
384
|
+
const unexpected = added
|
|
385
|
+
.filter((entry) => !isAllowedPatchSideEffect(entry, targetAbsPath, cwd))
|
|
386
|
+
.map((entry) => path.relative(cwd, entry).replace(/\\/g, '/'))
|
|
387
|
+
.filter((entry) => entry.length > 0)
|
|
388
|
+
.sort();
|
|
389
|
+
return unexpected;
|
|
390
|
+
}
|
|
391
|
+
function collectUnexpectedPatchMutations(before, after, targetAbsPath, cwd) {
|
|
392
|
+
if (!before || !after)
|
|
393
|
+
return [];
|
|
394
|
+
const keys = new Set([...before.keys(), ...after.keys()]);
|
|
395
|
+
const unexpected = [];
|
|
396
|
+
for (const key of keys) {
|
|
397
|
+
const beforeHash = before.get(key) ?? '<missing>';
|
|
398
|
+
const afterHash = after.get(key) ?? '<missing>';
|
|
399
|
+
if (beforeHash === afterHash)
|
|
400
|
+
continue;
|
|
401
|
+
if (isAllowedPatchSideEffect(key, targetAbsPath, cwd))
|
|
402
|
+
continue;
|
|
403
|
+
const rel = path.relative(cwd, key).replace(/\\/g, '/');
|
|
404
|
+
if (rel.length > 0 && !rel.startsWith('..')) {
|
|
405
|
+
unexpected.push(rel);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
return unexpected.sort();
|
|
409
|
+
}
|
|
410
|
+
function patternDescriptor(kind, confidence, manualReviewRequired) {
|
|
411
|
+
const labelByKind = {
|
|
412
|
+
missing_validation: 'API input validation guard',
|
|
413
|
+
missing_timeout_handling: 'Outbound request timeout guard',
|
|
414
|
+
unsafe_fetch_without_retries: 'Outbound request retry guard',
|
|
415
|
+
missing_idempotency_keys: 'Mutation idempotency-key guard',
|
|
416
|
+
unsafe_file_uploads: 'Upload MIME/size validation guard',
|
|
417
|
+
missing_auth_middleware: 'Route authentication middleware',
|
|
418
|
+
missing_rate_limiting: 'Route rate limiting middleware',
|
|
419
|
+
missing_token_expiry: 'JWT expiry enforcement',
|
|
420
|
+
unsafe_inner_html_usage: 'Unsafe DOM sink replacement',
|
|
421
|
+
unsafe_sensitive_logging: 'Sensitive log redaction',
|
|
422
|
+
db_in_ui: 'Service-layer boundary placeholder',
|
|
423
|
+
todo_fixme: 'TODO/FIXME debt marker removal',
|
|
424
|
+
};
|
|
425
|
+
const confidenceModel = confidence === 'high'
|
|
426
|
+
? 'high'
|
|
427
|
+
: confidence === 'medium'
|
|
428
|
+
? 'medium'
|
|
429
|
+
: 'low';
|
|
430
|
+
return {
|
|
431
|
+
kind,
|
|
432
|
+
label: labelByKind[kind] || kind,
|
|
433
|
+
deterministic: true,
|
|
434
|
+
confidenceModel,
|
|
435
|
+
advisoryOnly: confidenceModel === 'low',
|
|
436
|
+
manualReviewRequired,
|
|
437
|
+
};
|
|
438
|
+
}
|
|
439
|
+
function summarizeDiff(diff) {
|
|
440
|
+
let addedLines = 0;
|
|
441
|
+
let removedLines = 0;
|
|
442
|
+
for (const line of diff.split('\n')) {
|
|
443
|
+
if (line.startsWith('+++') || line.startsWith('---') || line.startsWith('@@'))
|
|
444
|
+
continue;
|
|
445
|
+
if (line.startsWith('+'))
|
|
446
|
+
addedLines += 1;
|
|
447
|
+
if (line.startsWith('-'))
|
|
448
|
+
removedLines += 1;
|
|
449
|
+
}
|
|
450
|
+
const changedLines = addedLines + removedLines;
|
|
451
|
+
return {
|
|
452
|
+
addedLines,
|
|
453
|
+
removedLines,
|
|
454
|
+
changedLines,
|
|
455
|
+
summary: `${changedLines} changed line(s): +${addedLines} / -${removedLines}`,
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
function extractRequestInputUsage(content) {
|
|
459
|
+
const accessMatch = content.match(/\b(req|request)\.(body|params|query)\b/);
|
|
460
|
+
if (!accessMatch)
|
|
461
|
+
return null;
|
|
462
|
+
const receiver = accessMatch[1];
|
|
463
|
+
const field = accessMatch[2];
|
|
464
|
+
const fieldRegex = new RegExp(`\\b${receiver}\\.${field}\\.([A-Za-z_$][\\w$]*)\\b`, 'g');
|
|
465
|
+
const fields = [];
|
|
466
|
+
const seen = new Set();
|
|
467
|
+
let match = fieldRegex.exec(content);
|
|
468
|
+
while (match) {
|
|
469
|
+
const fieldName = match[1];
|
|
470
|
+
if (!seen.has(fieldName)) {
|
|
471
|
+
seen.add(fieldName);
|
|
472
|
+
fields.push(fieldName);
|
|
473
|
+
}
|
|
474
|
+
match = fieldRegex.exec(content);
|
|
475
|
+
}
|
|
476
|
+
return { receiver, field, fields };
|
|
477
|
+
}
|
|
478
|
+
function buildPatchPreviewReasoning(patternKind, targetPath, beforeContent) {
|
|
479
|
+
if (!patternKind)
|
|
480
|
+
return null;
|
|
481
|
+
if (patternKind === 'missing_validation') {
|
|
482
|
+
const usage = extractRequestInputUsage(beforeContent);
|
|
483
|
+
if (!usage) {
|
|
484
|
+
return {
|
|
485
|
+
summary: 'Adds deterministic API input validation guard.',
|
|
486
|
+
why: `This file accesses request input without a validation boundary check.`,
|
|
487
|
+
risk: 'Malformed input can cause runtime errors or unsafe processing paths.',
|
|
488
|
+
expectedOutcome: 'Invalid requests fail fast and valid requests continue unchanged.',
|
|
489
|
+
};
|
|
490
|
+
}
|
|
491
|
+
const noun = usage.field === 'body' ? 'request body' : usage.field === 'params' ? 'route params' : 'query params';
|
|
492
|
+
const fieldSummary = usage.fields.length > 0 ? usage.fields.join(', ') : 'no explicit property access detected';
|
|
493
|
+
return {
|
|
494
|
+
summary: `Adds deterministic validation before reading ${usage.receiver}.${usage.field}.`,
|
|
495
|
+
why: `${targetPath} reads ${noun} fields (${fieldSummary}) before validation.`,
|
|
496
|
+
risk: `Without boundary validation, malformed ${noun} may propagate into handler logic.`,
|
|
497
|
+
expectedOutcome: `Invalid ${noun} returns HTTP 400 early; valid requests keep existing behavior.`,
|
|
498
|
+
fields: usage.fields,
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
if (patternKind === 'db_in_ui') {
|
|
502
|
+
return {
|
|
503
|
+
summary: 'Suggests moving direct DB access behind a service boundary.',
|
|
504
|
+
why: `${targetPath} appears to perform direct data access in a non-service layer.`,
|
|
505
|
+
risk: 'Layering violations increase coupling and make behavior harder to govern.',
|
|
506
|
+
expectedOutcome: 'Patch inserts a deterministic placeholder to redirect to service-layer logic.',
|
|
507
|
+
};
|
|
508
|
+
}
|
|
509
|
+
if (patternKind === 'missing_auth_middleware') {
|
|
510
|
+
return {
|
|
511
|
+
summary: 'Adds deterministic authentication middleware to the route definition.',
|
|
512
|
+
why: `${targetPath} appears to expose a request handler without an auth middleware guard.`,
|
|
513
|
+
risk: 'Unauthenticated routes can expose sensitive behavior to unauthorized clients.',
|
|
514
|
+
expectedOutcome: 'Route execution is gated by requireAuth before handler logic runs.',
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
if (patternKind === 'missing_rate_limiting') {
|
|
518
|
+
return {
|
|
519
|
+
summary: 'Adds deterministic rate-limit middleware to the route definition.',
|
|
520
|
+
why: `${targetPath} appears to expose a request handler without rate limiting controls.`,
|
|
521
|
+
risk: 'Unbounded request rates can increase abuse, cost, and availability risks.',
|
|
522
|
+
expectedOutcome: 'Route applies rateLimitGuard before handler execution.',
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
if (patternKind === 'missing_timeout_handling') {
|
|
526
|
+
return {
|
|
527
|
+
summary: 'Adds deterministic timeout guard to outbound fetch call.',
|
|
528
|
+
why: `${targetPath} issues a fetch request without timeout protection.`,
|
|
529
|
+
risk: 'Unbounded network calls can hang request execution and degrade reliability under upstream latency.',
|
|
530
|
+
expectedOutcome: 'Fetch call aborts after timeout and fails fast instead of hanging.',
|
|
531
|
+
};
|
|
532
|
+
}
|
|
533
|
+
if (patternKind === 'unsafe_fetch_without_retries') {
|
|
534
|
+
return {
|
|
535
|
+
summary: 'Wraps outbound fetch call in deterministic retry guard.',
|
|
536
|
+
why: `${targetPath} makes outbound network calls without transient failure retry handling.`,
|
|
537
|
+
risk: 'Single transient failures can become user-facing errors and increase instability.',
|
|
538
|
+
expectedOutcome: 'Transient upstream failures retry deterministically before failing.',
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
if (patternKind === 'missing_idempotency_keys') {
|
|
542
|
+
return {
|
|
543
|
+
summary: 'Adds deterministic idempotency-key guard for side-effecting requests.',
|
|
544
|
+
why: `${targetPath} appears to process payment/order-like mutations without idempotency key enforcement.`,
|
|
545
|
+
risk: 'Duplicate requests can cause repeated side effects (double charges/orders).',
|
|
546
|
+
expectedOutcome: 'Requests missing idempotency key fail early with explicit error.',
|
|
547
|
+
};
|
|
548
|
+
}
|
|
549
|
+
if (patternKind === 'unsafe_file_uploads') {
|
|
550
|
+
return {
|
|
551
|
+
summary: 'Adds deterministic MIME and size guards for uploaded files.',
|
|
552
|
+
why: `${targetPath} appears to process uploaded files without boundary checks.`,
|
|
553
|
+
risk: 'Unbounded or unsafe uploads increase security and stability risk.',
|
|
554
|
+
expectedOutcome: 'Invalid upload payloads are rejected before processing.',
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
if (patternKind === 'missing_token_expiry') {
|
|
558
|
+
return {
|
|
559
|
+
summary: 'Adds deterministic token expiry to JWT signing call.',
|
|
560
|
+
why: `${targetPath} signs JWT tokens without an expiresIn option.`,
|
|
561
|
+
risk: 'Long-lived tokens increase replay and account-compromise blast radius.',
|
|
562
|
+
expectedOutcome: 'Tokens gain explicit expiry to enforce credential rotation windows.',
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
if (patternKind === 'unsafe_inner_html_usage') {
|
|
566
|
+
return {
|
|
567
|
+
summary: 'Replaces unsafe innerHTML assignment with textContent.',
|
|
568
|
+
why: `${targetPath} writes HTML content directly into the DOM using innerHTML.`,
|
|
569
|
+
risk: 'innerHTML assignments can expose XSS vectors when input is not trusted.',
|
|
570
|
+
expectedOutcome: 'DOM assignment becomes text-only rendering with reduced injection risk.',
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
if (patternKind === 'unsafe_sensitive_logging') {
|
|
574
|
+
return {
|
|
575
|
+
summary: 'Removes deterministic sensitive logging line.',
|
|
576
|
+
why: `${targetPath} appears to log secret-bearing fields (token/authorization/password).`,
|
|
577
|
+
risk: 'Sensitive log content can leak credentials to observability or audit sinks.',
|
|
578
|
+
expectedOutcome: 'Sensitive logging path is replaced with a neutral warning placeholder.',
|
|
579
|
+
};
|
|
580
|
+
}
|
|
581
|
+
if (patternKind === 'todo_fixme') {
|
|
582
|
+
return {
|
|
583
|
+
summary: 'Removes TODO/FIXME marker matched by policy.',
|
|
584
|
+
why: `${targetPath} includes TODO/FIXME comments tracked as governance debt.`,
|
|
585
|
+
risk: 'Unresolved TODO markers can hide missing implementation or review debt.',
|
|
586
|
+
expectedOutcome: 'Patch removes the marker; implementation must still be verified separately.',
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
return null;
|
|
590
|
+
}
|
|
88
591
|
function isExecutionActionType(value) {
|
|
89
592
|
if (typeof value !== 'string')
|
|
90
593
|
return false;
|
|
@@ -96,13 +599,134 @@ function isExecutionActionType(value) {
|
|
|
96
599
|
|| value === 'policy-sync'
|
|
97
600
|
|| value === 'intent-update');
|
|
98
601
|
}
|
|
602
|
+
function asObjectRecord(value) {
|
|
603
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
604
|
+
return null;
|
|
605
|
+
return value;
|
|
606
|
+
}
|
|
607
|
+
function asObjectArray(value) {
|
|
608
|
+
if (!Array.isArray(value))
|
|
609
|
+
return [];
|
|
610
|
+
return value
|
|
611
|
+
.map((entry) => asObjectRecord(entry))
|
|
612
|
+
.filter((entry) => entry !== null);
|
|
613
|
+
}
|
|
614
|
+
function toLegacyViolation(entry, fallbackSeverity) {
|
|
615
|
+
const file = typeof entry.file === 'string' && entry.file.trim().length > 0
|
|
616
|
+
? entry.file.trim()
|
|
617
|
+
: '';
|
|
618
|
+
const message = typeof entry.message === 'string' && entry.message.trim().length > 0
|
|
619
|
+
? entry.message.trim()
|
|
620
|
+
: '';
|
|
621
|
+
if (!file || !message)
|
|
622
|
+
return null;
|
|
623
|
+
const severity = typeof entry.severity === 'string' && entry.severity.trim().length > 0
|
|
624
|
+
? entry.severity.trim()
|
|
625
|
+
: fallbackSeverity;
|
|
626
|
+
const rule = typeof entry.rule === 'string' && entry.rule.trim().length > 0
|
|
627
|
+
? entry.rule.trim()
|
|
628
|
+
: typeof entry.policy === 'string' && entry.policy.trim().length > 0
|
|
629
|
+
? entry.policy.trim()
|
|
630
|
+
: '';
|
|
631
|
+
return { file, message, severity, rule };
|
|
632
|
+
}
|
|
633
|
+
function normalizeVerifyPayloadForLegacyClients(payload) {
|
|
634
|
+
if (!payload)
|
|
635
|
+
return null;
|
|
636
|
+
const existingViolations = asObjectArray(payload.violations)
|
|
637
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
638
|
+
.filter((entry) => entry !== null);
|
|
639
|
+
const blockingItems = asObjectArray(payload.blockingItems)
|
|
640
|
+
.map((entry) => toLegacyViolation(entry, 'block'))
|
|
641
|
+
.filter((entry) => entry !== null);
|
|
642
|
+
const advisoryItems = asObjectArray(payload.advisoryItems)
|
|
643
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
644
|
+
.filter((entry) => entry !== null);
|
|
645
|
+
const warnings = asObjectArray(payload.warnings)
|
|
646
|
+
.map((entry) => toLegacyViolation(entry, 'warn'))
|
|
647
|
+
.filter((entry) => entry !== null);
|
|
648
|
+
const merged = [...existingViolations];
|
|
649
|
+
const canonicalSeverity = (value) => {
|
|
650
|
+
const normalized = value.trim().toLowerCase();
|
|
651
|
+
if (normalized === 'block' || normalized === 'critical' || normalized === 'high')
|
|
652
|
+
return 'block';
|
|
653
|
+
if (normalized === 'warn' || normalized === 'warning' || normalized === 'advisory' || normalized === 'medium' || normalized === 'low')
|
|
654
|
+
return 'warn';
|
|
655
|
+
return normalized;
|
|
656
|
+
};
|
|
657
|
+
const canonicalKey = (entry) => `${entry.file}::${entry.rule}::${entry.message}::${canonicalSeverity(entry.severity)}`;
|
|
658
|
+
const seen = new Set(merged.map((entry) => canonicalKey(entry)));
|
|
659
|
+
for (const item of [...blockingItems, ...advisoryItems, ...warnings]) {
|
|
660
|
+
const key = canonicalKey(item);
|
|
661
|
+
if (seen.has(key))
|
|
662
|
+
continue;
|
|
663
|
+
seen.add(key);
|
|
664
|
+
merged.push(item);
|
|
665
|
+
}
|
|
666
|
+
if (merged.length === 0)
|
|
667
|
+
return payload;
|
|
668
|
+
return {
|
|
669
|
+
...payload,
|
|
670
|
+
violations: merged,
|
|
671
|
+
};
|
|
672
|
+
}
|
|
673
|
+
function normalizeFixPayloadForLegacyClients(payload) {
|
|
674
|
+
if (!payload)
|
|
675
|
+
return null;
|
|
676
|
+
const suggestions = asObjectArray(payload.suggestions);
|
|
677
|
+
if (suggestions.length === 0)
|
|
678
|
+
return payload;
|
|
679
|
+
const deduped = [];
|
|
680
|
+
const seen = new Set();
|
|
681
|
+
for (const suggestion of suggestions) {
|
|
682
|
+
const file = typeof suggestion.file === 'string' ? suggestion.file.trim() : '';
|
|
683
|
+
const line = typeof suggestion.line === 'number' && Number.isFinite(suggestion.line)
|
|
684
|
+
? String(Math.floor(suggestion.line))
|
|
685
|
+
: '';
|
|
686
|
+
const message = typeof suggestion.message === 'string' ? suggestion.message.trim() : '';
|
|
687
|
+
const rule = typeof suggestion.rule === 'string'
|
|
688
|
+
? suggestion.rule.trim()
|
|
689
|
+
: typeof suggestion.policy === 'string'
|
|
690
|
+
? suggestion.policy.trim()
|
|
691
|
+
: '';
|
|
692
|
+
const confidence = typeof suggestion.confidence === 'string' ? suggestion.confidence.trim().toLowerCase() : '';
|
|
693
|
+
const patch = asObjectRecord(suggestion.patch);
|
|
694
|
+
const patchDiff = patch && typeof patch.diff === 'string' ? patch.diff : '';
|
|
695
|
+
const key = `${file}::${line}::${rule}::${message}::${confidence}::${(0, node_crypto_1.createHash)('sha1').update(patchDiff).digest('hex')}`;
|
|
696
|
+
if (seen.has(key))
|
|
697
|
+
continue;
|
|
698
|
+
seen.add(key);
|
|
699
|
+
deduped.push(suggestion);
|
|
700
|
+
}
|
|
701
|
+
if (deduped.length === suggestions.length)
|
|
702
|
+
return payload;
|
|
703
|
+
return {
|
|
704
|
+
...payload,
|
|
705
|
+
suggestions: deduped,
|
|
706
|
+
_normalization: {
|
|
707
|
+
...(asObjectRecord(payload._normalization) || {}),
|
|
708
|
+
suggestionsDeduped: suggestions.length - deduped.length,
|
|
709
|
+
},
|
|
710
|
+
};
|
|
711
|
+
}
|
|
99
712
|
function isLoopback(req) {
|
|
713
|
+
if (ALLOW_NON_LOOPBACK)
|
|
714
|
+
return true;
|
|
100
715
|
const addr = req.socket.remoteAddress ?? '';
|
|
101
716
|
return addr === '127.0.0.1' || addr === '::1' || addr === '::ffff:127.0.0.1';
|
|
102
717
|
}
|
|
103
718
|
function toSource(req) {
|
|
104
719
|
const raw = req.headers['x-neurcode-source'];
|
|
105
|
-
|
|
720
|
+
let value = Array.isArray(raw) ? raw[0] : raw;
|
|
721
|
+
if (!value) {
|
|
722
|
+
try {
|
|
723
|
+
const requestUrl = new URL(req.url ?? '/', 'http://localhost');
|
|
724
|
+
value = requestUrl.searchParams.get('source') ?? undefined;
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
value = undefined;
|
|
728
|
+
}
|
|
729
|
+
}
|
|
106
730
|
if (!value)
|
|
107
731
|
return 'daemon';
|
|
108
732
|
const normalized = value.trim().toLowerCase();
|
|
@@ -335,8 +959,9 @@ async function handleVerify(req, res) {
|
|
|
335
959
|
failure(res, run.execution.result?.message || 'verify execution produced no payload');
|
|
336
960
|
return;
|
|
337
961
|
}
|
|
962
|
+
const normalizedPayload = normalizeVerifyPayloadForLegacyClients(run.primaryPayload ?? null) ?? run.primaryPayload;
|
|
338
963
|
success(res, {
|
|
339
|
-
...
|
|
964
|
+
...normalizedPayload,
|
|
340
965
|
_execution: {
|
|
341
966
|
id: run.execution.id,
|
|
342
967
|
type: run.execution.type,
|
|
@@ -361,9 +986,11 @@ async function handleFix(req, res) {
|
|
|
361
986
|
failure(res, run.execution.result?.message || 'fix execution produced no payload');
|
|
362
987
|
return;
|
|
363
988
|
}
|
|
989
|
+
const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
|
|
990
|
+
const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
364
991
|
success(res, {
|
|
365
|
-
...
|
|
366
|
-
verifyAfter:
|
|
992
|
+
...normalizedFixPayload,
|
|
993
|
+
verifyAfter: normalizedVerifyAfter ?? null,
|
|
367
994
|
_execution: {
|
|
368
995
|
id: run.execution.id,
|
|
369
996
|
type: run.execution.type,
|
|
@@ -388,9 +1015,11 @@ async function handleFixApplySafe(req, res) {
|
|
|
388
1015
|
failure(res, run.execution.result?.message || 'fix --apply-safe execution produced no payload');
|
|
389
1016
|
return;
|
|
390
1017
|
}
|
|
1018
|
+
const normalizedFixPayload = normalizeFixPayloadForLegacyClients(run.primaryPayload) ?? run.primaryPayload;
|
|
1019
|
+
const normalizedVerifyAfter = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
391
1020
|
success(res, {
|
|
392
|
-
...
|
|
393
|
-
verifyAfter:
|
|
1021
|
+
...normalizedFixPayload,
|
|
1022
|
+
verifyAfter: normalizedVerifyAfter ?? null,
|
|
394
1023
|
execution: run.execution,
|
|
395
1024
|
});
|
|
396
1025
|
}
|
|
@@ -408,24 +1037,36 @@ async function handlePatch(req, res) {
|
|
|
408
1037
|
failure(res, 'Missing or unsafe "file" field', 400);
|
|
409
1038
|
return;
|
|
410
1039
|
}
|
|
1040
|
+
const previewToken = typeof body.previewToken === 'string' && body.previewToken.trim().length > 0
|
|
1041
|
+
? body.previewToken.trim()
|
|
1042
|
+
: undefined;
|
|
1043
|
+
const cwd = process.cwd();
|
|
1044
|
+
const targetPath = file.trim();
|
|
1045
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1046
|
+
const beforeDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1047
|
+
const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
411
1048
|
// Capture file content before patch to detect real change
|
|
412
|
-
const absPath = path.resolve(process.cwd(), file);
|
|
413
1049
|
let contentBefore = null;
|
|
414
1050
|
try {
|
|
415
1051
|
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
416
1052
|
}
|
|
417
1053
|
catch { /* file may not exist */ }
|
|
1054
|
+
const primaryArgs = ['patch', '--file', targetPath];
|
|
1055
|
+
if (previewToken) {
|
|
1056
|
+
primaryArgs.push('--preview-token', previewToken);
|
|
1057
|
+
}
|
|
418
1058
|
const run = await (0, execution_bus_1.runExecution)({
|
|
419
1059
|
type: 'patch',
|
|
420
1060
|
source: toSource(req),
|
|
421
1061
|
actor: toActor(req),
|
|
422
|
-
target:
|
|
423
|
-
cwd
|
|
1062
|
+
target: targetPath,
|
|
1063
|
+
cwd,
|
|
424
1064
|
reverify: true,
|
|
1065
|
+
primaryArgs,
|
|
425
1066
|
});
|
|
426
1067
|
const patchData = run.primaryPayload ?? {
|
|
427
1068
|
success: false,
|
|
428
|
-
file,
|
|
1069
|
+
file: targetPath,
|
|
429
1070
|
message: run.execution.result?.message || 'No applicable patch found',
|
|
430
1071
|
};
|
|
431
1072
|
// Validate that the file actually changed on disk
|
|
@@ -437,12 +1078,298 @@ async function handlePatch(req, res) {
|
|
|
437
1078
|
}
|
|
438
1079
|
catch { /* ignore read error */ }
|
|
439
1080
|
}
|
|
1081
|
+
const afterDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1082
|
+
const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1083
|
+
const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
|
|
1084
|
+
const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
|
|
1085
|
+
const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
|
|
1086
|
+
const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
|
|
1087
|
+
const payloadTargetMatch = payloadFile.length > 0
|
|
1088
|
+
? path.resolve(cwd, payloadFile) === absPath
|
|
1089
|
+
: true;
|
|
1090
|
+
const patchSucceeded = patchData.success === true;
|
|
1091
|
+
const rawPatchStatus = typeof patchData.status === 'string' ? patchData.status : '';
|
|
1092
|
+
const patchStatus = rawPatchStatus === 'filesystem_changed_since_preview'
|
|
1093
|
+
? 'stale_preview'
|
|
1094
|
+
: !patchSucceeded
|
|
1095
|
+
? 'rejected'
|
|
1096
|
+
: changed && payloadTargetMatch && combinedSideEffects.length === 0
|
|
1097
|
+
? 'applied'
|
|
1098
|
+
: changed
|
|
1099
|
+
? 'partial'
|
|
1100
|
+
: 'rejected';
|
|
1101
|
+
const patchMessage = (() => {
|
|
1102
|
+
if (patchStatus === 'applied') {
|
|
1103
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1104
|
+
? patchData.message
|
|
1105
|
+
: `${contracts_1.STATUS_TERMS.safePatchApplied}`;
|
|
1106
|
+
}
|
|
1107
|
+
if (patchStatus === 'partial') {
|
|
1108
|
+
if (!payloadTargetMatch) {
|
|
1109
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: patch target mismatch detected between requested file and daemon payload file.`;
|
|
1110
|
+
}
|
|
1111
|
+
if (combinedSideEffects.length > 0) {
|
|
1112
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: patch introduced side effects in ${combinedSideEffects.length} additional file(s).`;
|
|
1113
|
+
}
|
|
1114
|
+
return `${contracts_1.STATUS_TERMS.safePatchApplied}. ${contracts_1.STATUS_TERMS.manualReviewRecommended}.`;
|
|
1115
|
+
}
|
|
1116
|
+
if (patchStatus === 'stale_preview') {
|
|
1117
|
+
return `${contracts_1.STATUS_TERMS.filesystemChangedSincePreview}. Regenerate patch preview and retry. ${contracts_1.STATUS_TERMS.retrySafe}.`;
|
|
1118
|
+
}
|
|
1119
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1120
|
+
? patchData.message
|
|
1121
|
+
: `${contracts_1.STATUS_TERMS.patchRejected}; no deterministic file-scoped change applied`;
|
|
1122
|
+
})();
|
|
1123
|
+
const reverifyRequired = patchStatus === 'applied' || patchStatus === 'partial';
|
|
1124
|
+
const stateLabel = patchStatus === 'stale_preview'
|
|
1125
|
+
? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
|
|
1126
|
+
: (0, contracts_1.toPatchStateLabel)(patchStatus).toLowerCase();
|
|
1127
|
+
recordPatchOutcome(patchStatus);
|
|
1128
|
+
const normalizedVerifyPayload = normalizeVerifyPayloadForLegacyClients(run.verificationPayload);
|
|
1129
|
+
success(res, {
|
|
1130
|
+
patch: {
|
|
1131
|
+
...patchData,
|
|
1132
|
+
file: payloadFile || targetPath,
|
|
1133
|
+
success: patchStatus === 'applied',
|
|
1134
|
+
rawSuccess: patchData.success === true,
|
|
1135
|
+
changed,
|
|
1136
|
+
status: patchStatus,
|
|
1137
|
+
targetMatch: payloadTargetMatch,
|
|
1138
|
+
sideEffects: combinedSideEffects,
|
|
1139
|
+
message: patchMessage,
|
|
1140
|
+
reverifyRequired,
|
|
1141
|
+
stateLabel,
|
|
1142
|
+
previewTokenUsed: previewToken ? true : false,
|
|
1143
|
+
},
|
|
1144
|
+
verify: normalizedVerifyPayload ?? null,
|
|
1145
|
+
execution: run.execution,
|
|
1146
|
+
});
|
|
1147
|
+
}
|
|
1148
|
+
async function handlePatchRollback(req, res) {
|
|
1149
|
+
let body = {};
|
|
1150
|
+
try {
|
|
1151
|
+
body = JSON.parse(await readBody(req));
|
|
1152
|
+
}
|
|
1153
|
+
catch {
|
|
1154
|
+
failure(res, 'Invalid JSON body', 400);
|
|
1155
|
+
return;
|
|
1156
|
+
}
|
|
1157
|
+
const file = body.file;
|
|
1158
|
+
const receiptId = typeof body.receiptId === 'string' ? body.receiptId.trim() : '';
|
|
1159
|
+
if (!file || typeof file !== 'string' || file.includes('..')) {
|
|
1160
|
+
failure(res, 'Missing or unsafe "file" field', 400);
|
|
1161
|
+
return;
|
|
1162
|
+
}
|
|
1163
|
+
if (!receiptId) {
|
|
1164
|
+
failure(res, 'Missing "receiptId" field', 400);
|
|
1165
|
+
return;
|
|
1166
|
+
}
|
|
1167
|
+
const cwd = process.cwd();
|
|
1168
|
+
const targetPath = file.trim();
|
|
1169
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1170
|
+
const beforeDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1171
|
+
const beforeDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1172
|
+
let contentBefore = null;
|
|
1173
|
+
try {
|
|
1174
|
+
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
1175
|
+
}
|
|
1176
|
+
catch { /* file may not exist */ }
|
|
1177
|
+
const run = await (0, execution_bus_1.runExecution)({
|
|
1178
|
+
type: 'patch',
|
|
1179
|
+
source: toSource(req),
|
|
1180
|
+
actor: toActor(req),
|
|
1181
|
+
target: targetPath,
|
|
1182
|
+
cwd,
|
|
1183
|
+
reverify: true,
|
|
1184
|
+
primaryArgs: ['patch', '--file', targetPath, '--rollback-receipt', receiptId, '--json'],
|
|
1185
|
+
});
|
|
1186
|
+
const patchData = run.primaryPayload ?? {
|
|
1187
|
+
success: false,
|
|
1188
|
+
file: targetPath,
|
|
1189
|
+
message: run.execution.result?.message || 'No rollback receipt could be applied',
|
|
1190
|
+
};
|
|
1191
|
+
let changed = false;
|
|
1192
|
+
if (patchData.success && contentBefore !== null) {
|
|
1193
|
+
try {
|
|
1194
|
+
const contentAfter = fs.readFileSync(absPath, 'utf-8');
|
|
1195
|
+
changed = contentAfter !== contentBefore;
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
// ignore read error
|
|
1199
|
+
}
|
|
1200
|
+
}
|
|
1201
|
+
const afterDirtyPaths = captureGitDirtyPaths(cwd);
|
|
1202
|
+
const afterDirtyFingerprints = captureDirtyFileFingerprints(cwd);
|
|
1203
|
+
const sideEffects = collectUnexpectedPatchSideEffects(beforeDirtyPaths, afterDirtyPaths, absPath, cwd);
|
|
1204
|
+
const mutatedSideEffects = collectUnexpectedPatchMutations(beforeDirtyFingerprints, afterDirtyFingerprints, absPath, cwd);
|
|
1205
|
+
const combinedSideEffects = [...new Set([...sideEffects, ...mutatedSideEffects])].sort();
|
|
1206
|
+
const payloadFile = typeof patchData.file === 'string' ? patchData.file : '';
|
|
1207
|
+
const payloadTargetMatch = payloadFile.length > 0
|
|
1208
|
+
? path.resolve(cwd, payloadFile) === absPath
|
|
1209
|
+
: true;
|
|
1210
|
+
const rawStatus = typeof patchData.status === 'string' ? patchData.status : '';
|
|
1211
|
+
const rollbackStatus = rawStatus === 'rollback_applied'
|
|
1212
|
+
? 'rollback_applied'
|
|
1213
|
+
: rawStatus === 'rollback_stale' || rawStatus === 'filesystem_changed_since_patch'
|
|
1214
|
+
? 'rollback_stale'
|
|
1215
|
+
: 'rollback_rejected';
|
|
1216
|
+
const rollbackSucceeded = patchData.success === true && rollbackStatus === 'rollback_applied' && payloadTargetMatch && combinedSideEffects.length === 0;
|
|
1217
|
+
const rollbackMessage = (() => {
|
|
1218
|
+
if (rollbackSucceeded) {
|
|
1219
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1220
|
+
? patchData.message
|
|
1221
|
+
: contracts_1.STATUS_TERMS.rollbackApplied;
|
|
1222
|
+
}
|
|
1223
|
+
if (!payloadTargetMatch) {
|
|
1224
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: rollback receipt target mismatch detected.`;
|
|
1225
|
+
}
|
|
1226
|
+
if (combinedSideEffects.length > 0) {
|
|
1227
|
+
return `${contracts_1.STATUS_TERMS.patchRejected}: rollback side effects detected in ${combinedSideEffects.length} additional file(s).`;
|
|
1228
|
+
}
|
|
1229
|
+
return (typeof patchData.message === 'string' && patchData.message.trim().length > 0)
|
|
1230
|
+
? patchData.message
|
|
1231
|
+
: contracts_1.STATUS_TERMS.patchRejected;
|
|
1232
|
+
})();
|
|
1233
|
+
recordPatchOutcome(rollbackStatus);
|
|
440
1234
|
success(res, {
|
|
441
|
-
patch: {
|
|
442
|
-
|
|
1235
|
+
patch: {
|
|
1236
|
+
...patchData,
|
|
1237
|
+
file: payloadFile || targetPath,
|
|
1238
|
+
success: rollbackSucceeded,
|
|
1239
|
+
rawSuccess: patchData.success === true,
|
|
1240
|
+
changed,
|
|
1241
|
+
status: rollbackStatus,
|
|
1242
|
+
targetMatch: payloadTargetMatch,
|
|
1243
|
+
sideEffects: combinedSideEffects,
|
|
1244
|
+
message: rollbackMessage,
|
|
1245
|
+
reverifyRequired: rollbackSucceeded,
|
|
1246
|
+
stateLabel: rollbackSucceeded
|
|
1247
|
+
? contracts_1.STATUS_TERMS.rollbackApplied.toLowerCase()
|
|
1248
|
+
: rollbackStatus === 'rollback_stale'
|
|
1249
|
+
? contracts_1.STATUS_TERMS.filesystemChangedSincePreview.toLowerCase()
|
|
1250
|
+
: contracts_1.STATUS_TERMS.patchRejected.toLowerCase(),
|
|
1251
|
+
previewTokenUsed: false,
|
|
1252
|
+
},
|
|
1253
|
+
verify: normalizeVerifyPayloadForLegacyClients(run.verificationPayload) ?? null,
|
|
443
1254
|
execution: run.execution,
|
|
444
1255
|
});
|
|
445
1256
|
}
|
|
1257
|
+
async function handlePatchPreview(req, res) {
|
|
1258
|
+
let body = {};
|
|
1259
|
+
try {
|
|
1260
|
+
body = JSON.parse(await readBody(req));
|
|
1261
|
+
}
|
|
1262
|
+
catch {
|
|
1263
|
+
failure(res, 'Invalid JSON body', 400);
|
|
1264
|
+
return;
|
|
1265
|
+
}
|
|
1266
|
+
const file = body.file;
|
|
1267
|
+
if (!file || typeof file !== 'string' || file.includes('..')) {
|
|
1268
|
+
failure(res, 'Missing or unsafe "file" field', 400);
|
|
1269
|
+
return;
|
|
1270
|
+
}
|
|
1271
|
+
const cwd = process.cwd();
|
|
1272
|
+
const targetPath = file.trim();
|
|
1273
|
+
const absPath = path.resolve(cwd, targetPath);
|
|
1274
|
+
let contentBefore = '';
|
|
1275
|
+
try {
|
|
1276
|
+
contentBefore = fs.readFileSync(absPath, 'utf-8');
|
|
1277
|
+
}
|
|
1278
|
+
catch {
|
|
1279
|
+
failure(res, `File not found: ${targetPath}`, 404);
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
const preview = (0, patch_engine_1.applyFirstMatchingPatch)(targetPath, contentBefore);
|
|
1283
|
+
if (!preview) {
|
|
1284
|
+
success(res, {
|
|
1285
|
+
success: false,
|
|
1286
|
+
file: targetPath,
|
|
1287
|
+
status: 'rejected',
|
|
1288
|
+
message: `No deterministic patch preview available for ${targetPath}`,
|
|
1289
|
+
beforeContent: contentBefore,
|
|
1290
|
+
afterContent: null,
|
|
1291
|
+
diff: null,
|
|
1292
|
+
changed: false,
|
|
1293
|
+
patternKind: null,
|
|
1294
|
+
patchConfidence: null,
|
|
1295
|
+
patchHash: null,
|
|
1296
|
+
previewToken: null,
|
|
1297
|
+
validation: null,
|
|
1298
|
+
recipe: null,
|
|
1299
|
+
pattern: null,
|
|
1300
|
+
whatChanges: null,
|
|
1301
|
+
rollbackPreviewDiff: null,
|
|
1302
|
+
whySafe: null,
|
|
1303
|
+
manualReviewRequired: true,
|
|
1304
|
+
supportedDeterministicPattern: false,
|
|
1305
|
+
reasoning: null,
|
|
1306
|
+
});
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
const reasoning = buildPatchPreviewReasoning(preview.patternKind, targetPath, contentBefore);
|
|
1310
|
+
const rollbackPreviewDiff = (0, diff_1.generateUnifiedDiff)(targetPath, preview.updatedContent, contentBefore);
|
|
1311
|
+
const changeSummary = summarizeDiff(preview.diff);
|
|
1312
|
+
const manualReviewRequired = preview.patchConfidence === 'low'
|
|
1313
|
+
|| preview.validation.safe !== true
|
|
1314
|
+
|| preview.recipe.requiresManualReview === true;
|
|
1315
|
+
const pattern = patternDescriptor(preview.patternKind, preview.patchConfidence, manualReviewRequired);
|
|
1316
|
+
const whySafe = {
|
|
1317
|
+
deterministic: true,
|
|
1318
|
+
validationPassed: preview.validation.safe === true,
|
|
1319
|
+
confidence: preview.patchConfidence,
|
|
1320
|
+
checks: preview.validation.checks,
|
|
1321
|
+
reasonCodes: preview.validation.reasonCodes,
|
|
1322
|
+
};
|
|
1323
|
+
if (!preview.validation.safe) {
|
|
1324
|
+
success(res, {
|
|
1325
|
+
success: false,
|
|
1326
|
+
file: targetPath,
|
|
1327
|
+
status: 'rejected',
|
|
1328
|
+
message: `Patch preview rejected by deterministic safety validation (${preview.validation.reasonCodes.join(', ') || 'unknown'}).`,
|
|
1329
|
+
beforeContent: contentBefore,
|
|
1330
|
+
afterContent: null,
|
|
1331
|
+
diff: preview.diff,
|
|
1332
|
+
changed: false,
|
|
1333
|
+
patternKind: preview.patternKind,
|
|
1334
|
+
patchConfidence: preview.patchConfidence,
|
|
1335
|
+
patchHash: preview.patchHash,
|
|
1336
|
+
previewToken: preview.previewToken,
|
|
1337
|
+
validation: preview.validation,
|
|
1338
|
+
recipe: preview.recipe,
|
|
1339
|
+
pattern,
|
|
1340
|
+
whatChanges: changeSummary,
|
|
1341
|
+
rollbackPreviewDiff,
|
|
1342
|
+
whySafe,
|
|
1343
|
+
manualReviewRequired,
|
|
1344
|
+
supportedDeterministicPattern: true,
|
|
1345
|
+
reasoning,
|
|
1346
|
+
});
|
|
1347
|
+
return;
|
|
1348
|
+
}
|
|
1349
|
+
success(res, {
|
|
1350
|
+
success: true,
|
|
1351
|
+
file: targetPath,
|
|
1352
|
+
status: 'preview',
|
|
1353
|
+
message: 'Patch preview generated',
|
|
1354
|
+
beforeContent: contentBefore,
|
|
1355
|
+
afterContent: preview.updatedContent,
|
|
1356
|
+
diff: preview.diff,
|
|
1357
|
+
changed: contentBefore !== preview.updatedContent,
|
|
1358
|
+
patternKind: preview.patternKind,
|
|
1359
|
+
patchConfidence: preview.patchConfidence,
|
|
1360
|
+
patchHash: preview.patchHash,
|
|
1361
|
+
previewToken: preview.previewToken,
|
|
1362
|
+
validation: preview.validation,
|
|
1363
|
+
recipe: preview.recipe,
|
|
1364
|
+
pattern,
|
|
1365
|
+
whatChanges: changeSummary,
|
|
1366
|
+
rollbackPreviewDiff,
|
|
1367
|
+
whySafe,
|
|
1368
|
+
manualReviewRequired,
|
|
1369
|
+
supportedDeterministicPattern: true,
|
|
1370
|
+
reasoning,
|
|
1371
|
+
});
|
|
1372
|
+
}
|
|
446
1373
|
async function handleExecute(req, res) {
|
|
447
1374
|
let body = {};
|
|
448
1375
|
try {
|
|
@@ -951,7 +1878,16 @@ async function handleRuntimeEventStream(req, res) {
|
|
|
951
1878
|
// ── Server factory ─────────────────────────────────────────────────────────────
|
|
952
1879
|
function createDaemonServer() {
|
|
953
1880
|
const server = http.createServer(async (req, res) => {
|
|
954
|
-
|
|
1881
|
+
const incomingRequestIdRaw = req.headers[REQUEST_ID_HEADER];
|
|
1882
|
+
const incomingRequestId = Array.isArray(incomingRequestIdRaw)
|
|
1883
|
+
? incomingRequestIdRaw[0]
|
|
1884
|
+
: incomingRequestIdRaw;
|
|
1885
|
+
const requestId = (typeof incomingRequestId === 'string'
|
|
1886
|
+
&& incomingRequestId.trim().length > 0)
|
|
1887
|
+
? incomingRequestId.trim().slice(0, 128)
|
|
1888
|
+
: `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
1889
|
+
res.setHeader(REQUEST_ID_HEADER, requestId);
|
|
1890
|
+
addCorsHeaders(res, req);
|
|
955
1891
|
if (req.method === 'OPTIONS') {
|
|
956
1892
|
res.writeHead(204);
|
|
957
1893
|
res.end();
|
|
@@ -963,6 +1899,9 @@ function createDaemonServer() {
|
|
|
963
1899
|
}
|
|
964
1900
|
const url = req.url ?? '/';
|
|
965
1901
|
const method = req.method ?? 'GET';
|
|
1902
|
+
const normalizedRoutePath = normalizeRoutePath(url);
|
|
1903
|
+
recordDaemonRequest(normalizedRoutePath, method);
|
|
1904
|
+
res.__neurcodeRoutePath = normalizedRoutePath;
|
|
966
1905
|
try {
|
|
967
1906
|
if (method === 'GET' && url === '/health') {
|
|
968
1907
|
let version = '0.0.0';
|
|
@@ -971,10 +1910,12 @@ function createDaemonServer() {
|
|
|
971
1910
|
version = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version ?? version;
|
|
972
1911
|
}
|
|
973
1912
|
catch { /* ignore */ }
|
|
1913
|
+
const operational = buildDaemonOperationalSummary(process.cwd());
|
|
974
1914
|
send(res, 200, {
|
|
975
1915
|
ok: true,
|
|
976
1916
|
version,
|
|
977
1917
|
cwd: process.cwd(),
|
|
1918
|
+
operational,
|
|
978
1919
|
executionBus: {
|
|
979
1920
|
schemaVersion: 'neurcode.execution.v1',
|
|
980
1921
|
supportedActions: ['verify', 'fix', 'patch', 'apply-safe', 'reverify', 'policy-sync', 'intent-update'],
|
|
@@ -983,6 +1924,11 @@ function createDaemonServer() {
|
|
|
983
1924
|
schemaVersion: 'neurcode.runtime-event.v1',
|
|
984
1925
|
streamPath: '/events/stream',
|
|
985
1926
|
},
|
|
1927
|
+
compatibility: {
|
|
1928
|
+
runtimeContractVersion: contracts_1.RUNTIME_COMPATIBILITY_CONTRACT_VERSION,
|
|
1929
|
+
cliJsonContractVersion: contracts_1.CLI_JSON_CONTRACT_VERSION,
|
|
1930
|
+
statusVocabularyVersion: contracts_1.STATUS_VOCABULARY_VERSION,
|
|
1931
|
+
},
|
|
986
1932
|
controlPlane: {
|
|
987
1933
|
schemaVersion: 'neurcode.control-plane.v1',
|
|
988
1934
|
path: '/control-plane',
|
|
@@ -998,6 +1944,14 @@ function createDaemonServer() {
|
|
|
998
1944
|
});
|
|
999
1945
|
return;
|
|
1000
1946
|
}
|
|
1947
|
+
if (method === 'GET' && (url === '/ops/summary' || url.startsWith('/ops/summary?'))) {
|
|
1948
|
+
success(res, {
|
|
1949
|
+
schemaVersion: 'neurcode.daemon.ops.v1',
|
|
1950
|
+
generatedAt: new Date().toISOString(),
|
|
1951
|
+
operational: buildDaemonOperationalSummary(process.cwd()),
|
|
1952
|
+
});
|
|
1953
|
+
return;
|
|
1954
|
+
}
|
|
1001
1955
|
if (method === 'GET' && url.startsWith('/executions')) {
|
|
1002
1956
|
if (url === '/executions' || url.startsWith('/executions?')) {
|
|
1003
1957
|
await handleListExecutions(req, res);
|
|
@@ -1038,11 +1992,11 @@ function createDaemonServer() {
|
|
|
1038
1992
|
await handleGetControlPlane(req, res);
|
|
1039
1993
|
return;
|
|
1040
1994
|
}
|
|
1041
|
-
if (method === 'POST' && url === '/control-plane/preview') {
|
|
1995
|
+
if (method === 'POST' && (url === '/control-plane/preview' || url.startsWith('/control-plane/preview?'))) {
|
|
1042
1996
|
await handlePreviewControlPlaneUpdate(req, res);
|
|
1043
1997
|
return;
|
|
1044
1998
|
}
|
|
1045
|
-
if (method === 'PUT' && url === '/control-plane') {
|
|
1999
|
+
if (method === 'PUT' && (url === '/control-plane' || url.startsWith('/control-plane?'))) {
|
|
1046
2000
|
await handleApplyControlPlaneUpdate(req, res);
|
|
1047
2001
|
return;
|
|
1048
2002
|
}
|
|
@@ -1066,26 +2020,26 @@ function createDaemonServer() {
|
|
|
1066
2020
|
return;
|
|
1067
2021
|
}
|
|
1068
2022
|
}
|
|
1069
|
-
if (method === 'POST' && url === '/workspaces') {
|
|
2023
|
+
if (method === 'POST' && (url === '/workspaces' || url.startsWith('/workspaces?'))) {
|
|
1070
2024
|
await handleCreateWorkspace(req, res);
|
|
1071
2025
|
return;
|
|
1072
2026
|
}
|
|
1073
|
-
const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate
|
|
2027
|
+
const activateMatch = url.match(/^\/workspaces\/([^/]+)\/activate(?:\?.*)?$/);
|
|
1074
2028
|
if (method === 'POST' && activateMatch) {
|
|
1075
2029
|
await handleActivateWorkspace(req, res, decodeURIComponent(activateMatch[1]));
|
|
1076
2030
|
return;
|
|
1077
2031
|
}
|
|
1078
|
-
const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories
|
|
2032
|
+
const addRepoMatch = url.match(/^\/workspaces\/([^/]+)\/repositories(?:\?.*)?$/);
|
|
1079
2033
|
if (method === 'POST' && addRepoMatch) {
|
|
1080
2034
|
await handleAddWorkspaceRepository(req, res, decodeURIComponent(addRepoMatch[1]));
|
|
1081
2035
|
return;
|
|
1082
2036
|
}
|
|
1083
|
-
const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)
|
|
2037
|
+
const updateWorkspaceMatch = url.match(/^\/workspaces\/([^/?]+)(?:\?.*)?$/);
|
|
1084
2038
|
if (method === 'PUT' && updateWorkspaceMatch) {
|
|
1085
2039
|
await handleUpdateWorkspace(req, res, decodeURIComponent(updateWorkspaceMatch[1]));
|
|
1086
2040
|
return;
|
|
1087
2041
|
}
|
|
1088
|
-
if (method === 'POST' && url === '/workspaces/execute') {
|
|
2042
|
+
if (method === 'POST' && (url === '/workspaces/execute' || url.startsWith('/workspaces/execute?'))) {
|
|
1089
2043
|
await handleExecuteWorkspace(req, res);
|
|
1090
2044
|
return;
|
|
1091
2045
|
}
|
|
@@ -1113,38 +2067,99 @@ function createDaemonServer() {
|
|
|
1113
2067
|
return;
|
|
1114
2068
|
}
|
|
1115
2069
|
}
|
|
1116
|
-
if (method === 'POST' && url === '/execute') {
|
|
2070
|
+
if (method === 'POST' && (url === '/execute' || url.startsWith('/execute?'))) {
|
|
1117
2071
|
await handleExecute(req, res);
|
|
1118
2072
|
return;
|
|
1119
2073
|
}
|
|
1120
|
-
if (method === 'POST' && url === '/verify') {
|
|
2074
|
+
if (method === 'POST' && (url === '/verify' || url.startsWith('/verify?'))) {
|
|
1121
2075
|
await handleVerify(req, res);
|
|
1122
2076
|
return;
|
|
1123
2077
|
}
|
|
1124
|
-
if (method === 'POST' && url === '/fix') {
|
|
2078
|
+
if (method === 'POST' && (url === '/fix' || url.startsWith('/fix?'))) {
|
|
1125
2079
|
await handleFix(req, res);
|
|
1126
2080
|
return;
|
|
1127
2081
|
}
|
|
1128
|
-
if (method === 'POST' && url === '/fix/apply-safe') {
|
|
2082
|
+
if (method === 'POST' && (url === '/fix/apply-safe' || url.startsWith('/fix/apply-safe?'))) {
|
|
1129
2083
|
await handleFixApplySafe(req, res);
|
|
1130
2084
|
return;
|
|
1131
2085
|
}
|
|
1132
|
-
if (method === 'POST' && url === '/patch') {
|
|
2086
|
+
if (method === 'POST' && (url === '/patch/preview' || url.startsWith('/patch/preview?'))) {
|
|
2087
|
+
await handlePatchPreview(req, res);
|
|
2088
|
+
return;
|
|
2089
|
+
}
|
|
2090
|
+
if (method === 'POST' && (url === '/patch/rollback' || url.startsWith('/patch/rollback?'))) {
|
|
2091
|
+
await handlePatchRollback(req, res);
|
|
2092
|
+
return;
|
|
2093
|
+
}
|
|
2094
|
+
if (method === 'POST' && (url === '/patch' || url.startsWith('/patch?'))) {
|
|
1133
2095
|
await handlePatch(req, res);
|
|
1134
2096
|
return;
|
|
1135
2097
|
}
|
|
1136
2098
|
failure(res, `No route for ${method} ${url}`, 404);
|
|
1137
2099
|
}
|
|
1138
2100
|
catch (err) {
|
|
1139
|
-
|
|
2101
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
2102
|
+
if (/execution lock busy|EEXIST: file already exists, open '.*\/\.lock'/.test(message)) {
|
|
2103
|
+
failure(res, 'Execution lock busy. Another daemon action is running; retry this request.', 409, {
|
|
2104
|
+
code: 'daemon.execution_lock_busy',
|
|
2105
|
+
retriable: true,
|
|
2106
|
+
details: { cause: message },
|
|
2107
|
+
});
|
|
2108
|
+
return;
|
|
2109
|
+
}
|
|
2110
|
+
failure(res, message, 500);
|
|
1140
2111
|
}
|
|
1141
2112
|
});
|
|
1142
2113
|
return server;
|
|
1143
2114
|
}
|
|
1144
2115
|
// ── Start function ─────────────────────────────────────────────────────────────
|
|
2116
|
+
function validateDaemonStartup(cwd) {
|
|
2117
|
+
const errors = [];
|
|
2118
|
+
const warnings = [];
|
|
2119
|
+
const nodeMajor = Number.parseInt(process.versions.node.split('.')[0] || '0', 10);
|
|
2120
|
+
if (!Number.isFinite(nodeMajor) || nodeMajor < 18) {
|
|
2121
|
+
errors.push(`Node.js >= 18 is required (detected ${process.versions.node}).`);
|
|
2122
|
+
}
|
|
2123
|
+
try {
|
|
2124
|
+
const stat = fs.statSync(cwd);
|
|
2125
|
+
if (!stat.isDirectory()) {
|
|
2126
|
+
errors.push(`Current working directory is not a directory: ${cwd}`);
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
catch (error) {
|
|
2130
|
+
errors.push(`Cannot access working directory ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2131
|
+
}
|
|
2132
|
+
try {
|
|
2133
|
+
const runtimeDir = path.resolve(cwd, '.neurcode');
|
|
2134
|
+
fs.mkdirSync(runtimeDir, { recursive: true });
|
|
2135
|
+
fs.accessSync(runtimeDir, fs.constants.R_OK | fs.constants.W_OK);
|
|
2136
|
+
}
|
|
2137
|
+
catch (error) {
|
|
2138
|
+
errors.push(`Runtime state directory .neurcode is not writable in ${cwd}: ${error instanceof Error ? error.message : String(error)}`);
|
|
2139
|
+
}
|
|
2140
|
+
const apiUrl = process.env.NEURCODE_API_URL || process.env.VITE_API_URL;
|
|
2141
|
+
if (!apiUrl) {
|
|
2142
|
+
warnings.push('NEURCODE_API_URL is not set; cloud compatibility probes may rely on default localhost API settings.');
|
|
2143
|
+
}
|
|
2144
|
+
if (ALLOW_NON_LOOPBACK) {
|
|
2145
|
+
warnings.push('Remote daemon access is enabled via NEURCODE_DAEMON_ALLOW_REMOTE. Ensure network access is restricted and trusted.');
|
|
2146
|
+
}
|
|
2147
|
+
return { errors, warnings };
|
|
2148
|
+
}
|
|
1145
2149
|
function startDaemon() {
|
|
2150
|
+
const cwd = process.cwd();
|
|
2151
|
+
const startupValidation = validateDaemonStartup(cwd);
|
|
2152
|
+
for (const warning of startupValidation.warnings) {
|
|
2153
|
+
console.warn(`⚠️ Daemon startup warning: ${warning}`);
|
|
2154
|
+
}
|
|
2155
|
+
if (startupValidation.errors.length > 0) {
|
|
2156
|
+
for (const error of startupValidation.errors) {
|
|
2157
|
+
console.error(`❌ Daemon startup validation error: ${error}`);
|
|
2158
|
+
}
|
|
2159
|
+
process.exit(1);
|
|
2160
|
+
}
|
|
1146
2161
|
const server = createDaemonServer();
|
|
1147
|
-
startRuntimeEventTailer(
|
|
2162
|
+
startRuntimeEventTailer(cwd);
|
|
1148
2163
|
if (!runtimeEventUnsubscribe) {
|
|
1149
2164
|
runtimeEventUnsubscribe = (0, runtime_events_1.onRuntimeEvent)((event) => {
|
|
1150
2165
|
broadcastRuntimeEvent(event);
|
|
@@ -1166,6 +2181,8 @@ function startDaemon() {
|
|
|
1166
2181
|
console.log(` POST /verify → execution bus: verify`);
|
|
1167
2182
|
console.log(` POST /fix → execution bus: fix + reverify`);
|
|
1168
2183
|
console.log(` POST /fix/apply-safe → execution bus: apply-safe + reverify`);
|
|
2184
|
+
console.log(` POST /patch/preview → deterministic patch preview (before/after diff)`);
|
|
2185
|
+
console.log(` POST /patch/rollback → deterministic rollback apply by receipt`);
|
|
1169
2186
|
console.log(` POST /patch → execution bus: patch + reverify`);
|
|
1170
2187
|
console.log(` POST /execute → unified execution endpoint`);
|
|
1171
2188
|
console.log(` GET /executions → execution history`);
|
|
@@ -1174,6 +2191,7 @@ function startDaemon() {
|
|
|
1174
2191
|
console.log(` GET /executions/:id/diff → verification + patch inspection`);
|
|
1175
2192
|
console.log(` GET /events → runtime event history`);
|
|
1176
2193
|
console.log(` GET /events/stream → SSE deterministic governance runtime`);
|
|
2194
|
+
console.log(` GET /ops/summary → daemon operational health + reliability metrics`);
|
|
1177
2195
|
console.log(` GET /control-plane → governance control-plane state + snapshots`);
|
|
1178
2196
|
console.log(` POST /control-plane/preview → deterministic config impact preview`);
|
|
1179
2197
|
console.log(` PUT /control-plane → apply deterministic governance config update`);
|
|
@@ -1190,7 +2208,7 @@ function startDaemon() {
|
|
|
1190
2208
|
console.log(` GET /replay/execution/:id → deterministic execution replay`);
|
|
1191
2209
|
console.log(` GET /replay/workspace/:id → deterministic workspace replay`);
|
|
1192
2210
|
console.log(` GET /replay/timeline → deterministic governance timeline replay`);
|
|
1193
|
-
console.log(`\n CWD: ${
|
|
2211
|
+
console.log(`\n CWD: ${cwd}`);
|
|
1194
2212
|
console.log(` Press Ctrl+C to stop.\n`);
|
|
1195
2213
|
});
|
|
1196
2214
|
const stop = () => {
|