@rubytech/create-realagent 1.0.817 → 1.0.819
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/package.json +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts +2 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +168 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +30 -2
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +111 -3
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +191 -0
- package/payload/platform/lib/graph-write/src/index.ts +112 -6
- package/payload/platform/neo4j/edge-annotations.json +0 -8
- package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
- package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
- package/payload/platform/neo4j/schema.cypher +25 -22
- package/payload/platform/plugins/admin/PLUGIN.md +1 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +24 -6
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +7 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +26 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +8 -0
- package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js +10 -5
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +27 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -2
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts +20 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +46 -6
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
- package/payload/platform/scripts/logs-read.sh +8 -38
- package/payload/server/chunk-AJLGI7Y3.js +10067 -0
- package/payload/server/chunk-ON3LBL2Y.js +1114 -0
- package/payload/server/chunk-PXQA2MA3.js +2518 -0
- package/payload/server/client-pool-GBY5I2KQ.js +31 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
- package/payload/server/public/assets/{admin-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
- package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
- package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +790 -1896
package/payload/server/server.js
CHANGED
|
@@ -51,7 +51,7 @@ import {
|
|
|
51
51
|
vncLog,
|
|
52
52
|
waitForExit,
|
|
53
53
|
writeChromiumWrapper
|
|
54
|
-
} from "./chunk-
|
|
54
|
+
} from "./chunk-AJLGI7Y3.js";
|
|
55
55
|
import {
|
|
56
56
|
agentLogStream,
|
|
57
57
|
clearSessionHistory,
|
|
@@ -79,7 +79,7 @@ import {
|
|
|
79
79
|
sigtermFlushStreamLogs,
|
|
80
80
|
unregisterSession,
|
|
81
81
|
validateSession
|
|
82
|
-
} from "./chunk-
|
|
82
|
+
} from "./chunk-ON3LBL2Y.js";
|
|
83
83
|
import {
|
|
84
84
|
ACCOUNTS_DIR,
|
|
85
85
|
GREETING_DIRECTIVE,
|
|
@@ -119,10 +119,227 @@ import {
|
|
|
119
119
|
verifyAndGetConversationUpdatedAt,
|
|
120
120
|
verifyConversationOwnership,
|
|
121
121
|
writeAdminUserAndPerson
|
|
122
|
-
} from "./chunk-
|
|
122
|
+
} from "./chunk-PXQA2MA3.js";
|
|
123
123
|
|
|
124
|
-
// ../lib/graph-
|
|
124
|
+
// ../lib/graph-write/dist/audit.js
|
|
125
|
+
var require_audit = __commonJS({
|
|
126
|
+
"../lib/graph-write/dist/audit.js"(exports) {
|
|
127
|
+
"use strict";
|
|
128
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
129
|
+
exports.auditCypherWrite = auditCypherWrite;
|
|
130
|
+
exports.formatAuditLine = formatAuditLine;
|
|
131
|
+
var EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
132
|
+
var CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
|
|
133
|
+
var PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
|
|
134
|
+
function stripStringLiterals(cypher) {
|
|
135
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
136
|
+
}
|
|
137
|
+
function extractEdgeTypes(cleaned) {
|
|
138
|
+
const out = /* @__PURE__ */ new Set();
|
|
139
|
+
for (const m of cleaned.matchAll(EDGE_PATTERN)) {
|
|
140
|
+
for (const t of m[1].split("|")) {
|
|
141
|
+
const clean = t.trim();
|
|
142
|
+
if (clean)
|
|
143
|
+
out.add(clean);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return out;
|
|
147
|
+
}
|
|
148
|
+
function countCreateOrMergeNodes(cleaned) {
|
|
149
|
+
const matches = cleaned.match(CREATE_OR_MERGE_NODE);
|
|
150
|
+
return matches ? matches.length : 0;
|
|
151
|
+
}
|
|
152
|
+
function countProvenanceStamps(cleaned) {
|
|
153
|
+
const matches = cleaned.match(PROVENANCE_TOKEN);
|
|
154
|
+
return matches ? matches.length : 0;
|
|
155
|
+
}
|
|
156
|
+
function auditCypherWrite(input) {
|
|
157
|
+
const warnings = [];
|
|
158
|
+
const cleaned = stripStringLiterals(input.cypher);
|
|
159
|
+
const referencedTypes = extractEdgeTypes(cleaned);
|
|
160
|
+
for (const t of referencedTypes) {
|
|
161
|
+
if (!input.schema.relationshipTypes.has(t)) {
|
|
162
|
+
warnings.push({ kind: "unknown-type-warning", type: t });
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
if (input.nodesCreated > 0) {
|
|
166
|
+
const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
|
|
167
|
+
if (createOrMergeNodes > 0) {
|
|
168
|
+
const stamps = countProvenanceStamps(cleaned);
|
|
169
|
+
if (stamps < createOrMergeNodes) {
|
|
170
|
+
warnings.push({
|
|
171
|
+
kind: "missing-provenance-warning",
|
|
172
|
+
created: createOrMergeNodes,
|
|
173
|
+
stamped: stamps
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
if (input.orphanIds.length > 0) {
|
|
179
|
+
warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
|
|
180
|
+
}
|
|
181
|
+
return warnings;
|
|
182
|
+
}
|
|
183
|
+
function formatAuditLine(line) {
|
|
184
|
+
const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
|
|
185
|
+
switch (line.kind) {
|
|
186
|
+
case "accepted":
|
|
187
|
+
return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
|
|
188
|
+
case "orphan-warning":
|
|
189
|
+
return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
|
|
190
|
+
case "unknown-type-warning":
|
|
191
|
+
return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
|
|
192
|
+
case "missing-provenance-warning":
|
|
193
|
+
return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
// ../lib/graph-write/dist/index.js
|
|
125
200
|
var require_dist = __commonJS({
|
|
201
|
+
"../lib/graph-write/dist/index.js"(exports) {
|
|
202
|
+
"use strict";
|
|
203
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
204
|
+
if (k2 === void 0) k2 = k;
|
|
205
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
206
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
207
|
+
desc = { enumerable: true, get: function() {
|
|
208
|
+
return m[k];
|
|
209
|
+
} };
|
|
210
|
+
}
|
|
211
|
+
Object.defineProperty(o, k2, desc);
|
|
212
|
+
}) : (function(o, m, k, k2) {
|
|
213
|
+
if (k2 === void 0) k2 = k;
|
|
214
|
+
o[k2] = m[k];
|
|
215
|
+
}));
|
|
216
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
217
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
218
|
+
};
|
|
219
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
220
|
+
exports.ACTION_PROVENANCE_LABELS = void 0;
|
|
221
|
+
exports.stampCreatedBy = stampCreatedBy;
|
|
222
|
+
exports.writeNodeWithEdges = writeNodeWithEdges2;
|
|
223
|
+
__exportStar(require_audit(), exports);
|
|
224
|
+
exports.ACTION_PROVENANCE_LABELS = /* @__PURE__ */ new Set([
|
|
225
|
+
"Person",
|
|
226
|
+
"UserProfile",
|
|
227
|
+
"AdminUser",
|
|
228
|
+
"Organization",
|
|
229
|
+
"LocalBusiness",
|
|
230
|
+
"CloudflareTunnel",
|
|
231
|
+
"CloudflareHostname"
|
|
232
|
+
]);
|
|
233
|
+
function requiresActionProvenance(labels) {
|
|
234
|
+
for (const label of labels) {
|
|
235
|
+
if (exports.ACTION_PROVENANCE_LABELS.has(label))
|
|
236
|
+
return true;
|
|
237
|
+
}
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
function findProducedFromTaskCandidates(relationships) {
|
|
241
|
+
return relationships.filter((r) => r.type === "PRODUCED" && r.direction === "incoming");
|
|
242
|
+
}
|
|
243
|
+
function stampCreatedBy(props, createdBy) {
|
|
244
|
+
return {
|
|
245
|
+
...props,
|
|
246
|
+
createdByAgent: createdBy.agent ?? "unknown",
|
|
247
|
+
createdBySession: createdBy.session ?? "unknown",
|
|
248
|
+
createdByTool: createdBy.tool ?? null,
|
|
249
|
+
createdBySource: createdBy.source ?? null
|
|
250
|
+
};
|
|
251
|
+
}
|
|
252
|
+
async function writeNodeWithEdges2(params) {
|
|
253
|
+
const { session, labels, props, relationships, createdBy } = params;
|
|
254
|
+
const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
|
|
255
|
+
const labelCsv = labels.join(",");
|
|
256
|
+
const reviewDigestActionTool = typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
|
|
257
|
+
if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
|
|
258
|
+
const actionToolField = reviewDigestActionTool ? "review-digest-compose" : "n/a";
|
|
259
|
+
process.stderr.write(`[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}
|
|
260
|
+
`);
|
|
261
|
+
throw new Error("Write doctrine violated: review-detector feature removed (Task 884) \u2014 `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed.");
|
|
262
|
+
}
|
|
263
|
+
if (!relationships || relationships.length < 1) {
|
|
264
|
+
process.stderr.write(`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}
|
|
265
|
+
`);
|
|
266
|
+
throw new Error("Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine).");
|
|
267
|
+
}
|
|
268
|
+
const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
|
|
269
|
+
const nodeProps = stampCreatedBy(props, createdBy);
|
|
270
|
+
return await session.executeWrite(async (tx) => {
|
|
271
|
+
const targetIds = relationships.map((r) => r.targetNodeId);
|
|
272
|
+
const check = await tx.run(`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN elementId(t) AS id, labels(t) AS labels`, { ids: targetIds });
|
|
273
|
+
const labelsByTarget = /* @__PURE__ */ new Map();
|
|
274
|
+
for (const rec of check.records) {
|
|
275
|
+
labelsByTarget.set(rec.get("id"), rec.get("labels"));
|
|
276
|
+
}
|
|
277
|
+
const found = labelsByTarget.size;
|
|
278
|
+
const uniqueRequested = new Set(targetIds).size;
|
|
279
|
+
if (found !== uniqueRequested) {
|
|
280
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}
|
|
281
|
+
`);
|
|
282
|
+
throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
|
|
283
|
+
}
|
|
284
|
+
let producedByTaskId = null;
|
|
285
|
+
if (requiresActionProvenance(labels) && (createdBy.agent ?? "") !== "system") {
|
|
286
|
+
const candidates = findProducedFromTaskCandidates(relationships);
|
|
287
|
+
const taskCandidates = candidates.filter((r) => {
|
|
288
|
+
const lbls = labelsByTarget.get(r.targetNodeId);
|
|
289
|
+
return Array.isArray(lbls) && lbls.includes("Task");
|
|
290
|
+
});
|
|
291
|
+
if (taskCandidates.length === 0) {
|
|
292
|
+
process.stderr.write(`[graph-write] reject reason=missing-action-provenance labels=${labelCsv} agent=${agentLabel}
|
|
293
|
+
`);
|
|
294
|
+
throw new Error(`Process provenance doctrine violated: write to ${labelCsv} requires an inbound :PRODUCED edge from a :Task (createdBy.agent='${agentLabel}'). See .docs/neo4j.md (Process provenance doctrine).`);
|
|
295
|
+
}
|
|
296
|
+
producedByTaskId = taskCandidates[0].targetNodeId;
|
|
297
|
+
}
|
|
298
|
+
let nodeRes;
|
|
299
|
+
try {
|
|
300
|
+
nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
|
|
301
|
+
} catch (err) {
|
|
302
|
+
const code = err?.code ?? "";
|
|
303
|
+
if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
|
|
304
|
+
const accountIdProp = nodeProps.accountId;
|
|
305
|
+
const userIdProp = nodeProps.userId;
|
|
306
|
+
const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
|
|
307
|
+
const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
|
|
308
|
+
process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}
|
|
309
|
+
`);
|
|
310
|
+
}
|
|
311
|
+
throw err;
|
|
312
|
+
}
|
|
313
|
+
const nodeId = nodeRes.records[0].get("nodeId");
|
|
314
|
+
const nodeLabels = nodeRes.records[0].get("nodeLabels");
|
|
315
|
+
let edgesCreated = 0;
|
|
316
|
+
for (const rel of relationships) {
|
|
317
|
+
const type = rel.type.replace(/`/g, "");
|
|
318
|
+
const q = rel.direction === "outgoing" ? `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (a)-[:\`${type}\`]->(b)` : `MATCH (a), (b) WHERE elementId(a) = $from AND elementId(b) = $to CREATE (b)-[:\`${type}\`]->(a)`;
|
|
319
|
+
const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
|
|
320
|
+
const created = r.summary.counters.updates().relationshipsCreated;
|
|
321
|
+
if (created === 0) {
|
|
322
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}
|
|
323
|
+
`);
|
|
324
|
+
throw new Error(`Write doctrine violated: relationship CREATE to target ${rel.targetNodeId} produced 0 edges (target likely deleted concurrently after pre-check). Transaction rolled back.`);
|
|
325
|
+
}
|
|
326
|
+
edgesCreated += created;
|
|
327
|
+
}
|
|
328
|
+
if (edgesCreated !== relationships.length) {
|
|
329
|
+
process.stderr.write(`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}
|
|
330
|
+
`);
|
|
331
|
+
throw new Error(`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`);
|
|
332
|
+
}
|
|
333
|
+
process.stderr.write(`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedByTask=${producedByTaskId ?? "none"}
|
|
334
|
+
`);
|
|
335
|
+
return { nodeId, labels: nodeLabels, edgesCreated };
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// ../lib/graph-trash/dist/index.js
|
|
342
|
+
var require_dist2 = __commonJS({
|
|
126
343
|
"../lib/graph-trash/dist/index.js"(exports) {
|
|
127
344
|
"use strict";
|
|
128
345
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -618,15 +835,15 @@ var serveStatic = (options = { root: "" }) => {
|
|
|
618
835
|
};
|
|
619
836
|
|
|
620
837
|
// server/index.ts
|
|
621
|
-
import { readFileSync as
|
|
622
|
-
import { resolve as
|
|
838
|
+
import { readFileSync as readFileSync16, existsSync as existsSync22, watchFile } from "fs";
|
|
839
|
+
import { resolve as resolve21, join as join9, basename as basename5 } from "path";
|
|
623
840
|
import { homedir as homedir2 } from "os";
|
|
624
841
|
|
|
625
842
|
// app/lib/agent-slug-pattern.ts
|
|
626
843
|
var AGENT_SLUG_PATTERN = /^\/([a-z][a-z0-9-]{2,49})$/;
|
|
627
844
|
|
|
628
845
|
// server/routes/health.ts
|
|
629
|
-
import { existsSync as
|
|
846
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
630
847
|
import { createConnection } from "net";
|
|
631
848
|
|
|
632
849
|
// app/lib/network.ts
|
|
@@ -647,1562 +864,6 @@ function getLanIp() {
|
|
|
647
864
|
return fallback;
|
|
648
865
|
}
|
|
649
866
|
|
|
650
|
-
// app/lib/review-detector/boot.ts
|
|
651
|
-
import { basename as basename2 } from "path";
|
|
652
|
-
|
|
653
|
-
// app/lib/review-detector/rules.ts
|
|
654
|
-
import { readFileSync, writeFileSync, existsSync as existsSync2, statSync as statSync2, mkdirSync, renameSync } from "fs";
|
|
655
|
-
import { resolve, dirname } from "path";
|
|
656
|
-
var DEFAULT_SCAN_INTERVAL_MS = 5e3;
|
|
657
|
-
var RATE_LIMIT_PATTERN = "rate[- ]?limit(?:ed| reached| hit)|(?:HTTP|status)[^a-z]{0,3}429|too many requests";
|
|
658
|
-
var RATE_LIMIT_PATTERN_V1 = "\\b429\\b|rate.?limit|too.?many.?requests";
|
|
659
|
-
var VALID_TYPES = /* @__PURE__ */ new Set([
|
|
660
|
-
"reconnect-loop",
|
|
661
|
-
"repeated-error",
|
|
662
|
-
"silent-catch",
|
|
663
|
-
"file-write-storm",
|
|
664
|
-
"stale-log",
|
|
665
|
-
"rate-limit",
|
|
666
|
-
"absent-followup"
|
|
667
|
-
]);
|
|
668
|
-
var MAX_FOLLOWUP_WINDOW_MS = 6e5;
|
|
669
|
-
var VALID_SOURCES = /* @__PURE__ */ new Set([
|
|
670
|
-
"any",
|
|
671
|
-
"server",
|
|
672
|
-
"vnc",
|
|
673
|
-
"system",
|
|
674
|
-
"error",
|
|
675
|
-
"session",
|
|
676
|
-
"public",
|
|
677
|
-
"mcp",
|
|
678
|
-
"cloudflared",
|
|
679
|
-
"config-dir"
|
|
680
|
-
]);
|
|
681
|
-
var VALID_SCOPES = /* @__PURE__ */ new Set(["global", "session"]);
|
|
682
|
-
function defaultRules() {
|
|
683
|
-
return [
|
|
684
|
-
{
|
|
685
|
-
id: "whatsapp-reconnect-loop",
|
|
686
|
-
name: "WhatsApp Baileys reconnect loop",
|
|
687
|
-
type: "reconnect-loop",
|
|
688
|
-
logSource: "server",
|
|
689
|
-
pattern: "\\[whatsapp:baileys\\] ERROR",
|
|
690
|
-
thresholdCount: 5,
|
|
691
|
-
thresholdWindowMinutes: 10,
|
|
692
|
-
suggestedAction: "Check Baileys init-queries error. Run `/investigate` on the WhatsApp subsystem, or pause WhatsApp and re-pair the account from chat."
|
|
693
|
-
},
|
|
694
|
-
{
|
|
695
|
-
id: "repeated-error-generic",
|
|
696
|
-
name: "Repeated error signature",
|
|
697
|
-
type: "repeated-error",
|
|
698
|
-
// Match generic ERROR lines with a bracketed prefix. Fingerprinting by
|
|
699
|
-
// first 80 chars happens in the evaluator.
|
|
700
|
-
logSource: "server",
|
|
701
|
-
pattern: "\\] ERROR ",
|
|
702
|
-
thresholdCount: 20,
|
|
703
|
-
thresholdWindowMinutes: 60,
|
|
704
|
-
suggestedAction: "Repeated error burst. Tail the relevant log via `logs-read` and identify the upstream cause."
|
|
705
|
-
},
|
|
706
|
-
{
|
|
707
|
-
id: "silent-catch-fingerprint",
|
|
708
|
-
name: "Silent catch-block fingerprint",
|
|
709
|
-
type: "silent-catch",
|
|
710
|
-
// Patterns that historically indicated a silent catch. Every match is
|
|
711
|
-
// worth an alert — never suppressed by frequency.
|
|
712
|
-
logSource: "any",
|
|
713
|
-
pattern: "(UnhandledPromiseRejection|catch.*ignored|swallowed error|silent failure)",
|
|
714
|
-
thresholdCount: 0,
|
|
715
|
-
thresholdWindowMinutes: 0,
|
|
716
|
-
suggestedAction: "A silent-failure fingerprint appeared in logs. Read the surrounding context and turn the catch into loud failure per CLAUDE.md rule 'Observability Is Non-Negotiable'."
|
|
717
|
-
},
|
|
718
|
-
{
|
|
719
|
-
id: "credentials-write-storm",
|
|
720
|
-
name: "Credentials directory write storm",
|
|
721
|
-
type: "file-write-storm",
|
|
722
|
-
logSource: "config-dir",
|
|
723
|
-
pattern: "",
|
|
724
|
-
watchPath: "credentials",
|
|
725
|
-
thresholdCount: 5,
|
|
726
|
-
thresholdWindowMinutes: 5,
|
|
727
|
-
suggestedAction: "The credentials directory is being rewritten repeatedly. This usually means a subsystem is stuck in an auth-retry loop. Check which file is being rewritten and trace the caller."
|
|
728
|
-
},
|
|
729
|
-
// The stale-log rule type is fully supported by the evaluator but the
|
|
730
|
-
// default seed does not ship an instance of it. Choosing the right file
|
|
731
|
-
// to watch is subsystem-specific (e.g. a plugin-specific log that stops
|
|
732
|
-
// when the plugin dies); server.log is the wrong target because it is
|
|
733
|
-
// written continuously by the detector's own cycle events, so it never
|
|
734
|
-
// goes stale. Users can add a targeted stale-log rule via
|
|
735
|
-
// `review-rules-add` when they know which file matters for their
|
|
736
|
-
// subsystem.
|
|
737
|
-
{
|
|
738
|
-
id: "http-rate-limit-429",
|
|
739
|
-
name: "HTTP 429 rate-limit hit",
|
|
740
|
-
type: "rate-limit",
|
|
741
|
-
logSource: "any",
|
|
742
|
-
pattern: RATE_LIMIT_PATTERN,
|
|
743
|
-
thresholdCount: 0,
|
|
744
|
-
thresholdWindowMinutes: 0,
|
|
745
|
-
suggestedAction: "An external API returned 429 (rate-limited). Identify the caller and add backoff, or check the quota on the relevant API key."
|
|
746
|
-
},
|
|
747
|
-
{
|
|
748
|
-
id: "approval-bypass-detected",
|
|
749
|
-
name: "Approval gating bypass",
|
|
750
|
-
type: "repeated-error",
|
|
751
|
-
logSource: "error",
|
|
752
|
-
pattern: "\\[persist\\].*(?:email-send|email-reply|whatsapp-send|whatsapp-send-document|message|contact-erase).*approval=auto-executed",
|
|
753
|
-
thresholdCount: 0,
|
|
754
|
-
thresholdWindowMinutes: 0,
|
|
755
|
-
suggestedAction: "An external-facing tool executed with auto-executed approval state. Check account.json approvalPolicy \u2014 if this tool should require review, the gating hook may have been bypassed or the policy was changed."
|
|
756
|
-
},
|
|
757
|
-
{
|
|
758
|
-
// Task 530: catches the bridgeai-style class where a single conversation
|
|
759
|
-
// sees the same tool error repeatedly and the agent silently falls back.
|
|
760
|
-
// Session-scoped so cross-conversation coincidence doesn't trigger it.
|
|
761
|
-
id: "tool-result-recurring-errors",
|
|
762
|
-
name: "Tool result errors recurring in a conversation",
|
|
763
|
-
type: "repeated-error",
|
|
764
|
-
logSource: "system",
|
|
765
|
-
pattern: "\\[tool-result\\].*error=true",
|
|
766
|
-
thresholdCount: 2,
|
|
767
|
-
thresholdWindowMinutes: 5,
|
|
768
|
-
scope: "session",
|
|
769
|
-
suggestedAction: 'The same conversation has logged multiple tool failures. Use the admin `logs-read` MCP tool with `type: "system"` (or the `logs-read.sh` script with the conversationId) to retrieve the adjacent [tool-failure-diag] lines and identify whether the cause is DNS, TCP, HTTP, or the tool\'s internal pipeline \u2014 then adapt the next attempt to match. Never retry the same tool against the same target without diagnostic-grounded reasoning.'
|
|
770
|
-
},
|
|
771
|
-
{
|
|
772
|
-
// Task 532: closes the "60-second black-box tool wait" class. Fires when
|
|
773
|
-
// a tool hits the 30-second mark of the mid-flight heartbeat. The
|
|
774
|
-
// pattern anchors on elapsed=30s specifically (the tool-wait tick emits
|
|
775
|
-
// one line per 5s per tool, so matching every tick would be noisy) and
|
|
776
|
-
// excludes tools whose long runtime is expected: `Task`/`Agent` subagent
|
|
777
|
-
// dispatch, `Bash` with explicit long timeouts.
|
|
778
|
-
id: "tool-wait-long-stall",
|
|
779
|
-
name: "Tool wait exceeds 30 seconds (possible stall)",
|
|
780
|
-
type: "repeated-error",
|
|
781
|
-
logSource: "system",
|
|
782
|
-
pattern: "\\[tool-wait\\][^\\n]*name=(?!Task\\b|Agent\\b|Bash\\b)[A-Za-z0-9_]+[^\\n]*elapsed=30s",
|
|
783
|
-
thresholdCount: 0,
|
|
784
|
-
thresholdWindowMinutes: 0,
|
|
785
|
-
scope: "session",
|
|
786
|
-
suggestedAction: "A tool call has been pending for 30 seconds without a result. Read the adjacent [tool-wait-diag] and [tool-wait-proc] lines in the conversation's stream log to determine whether the network remained healthy, the subprocess held active sockets, and the HTTP request reached the wire. If diag shows a healthy network but the subprocess has no [subproc-stderr] UNDICI/HTTP activity during the wait window, the tool's internal pipeline is stalled \u2014 do not retry the same request against the same target without a change in approach."
|
|
787
|
-
},
|
|
788
|
-
{
|
|
789
|
-
// Task 536: detect agents ignoring the WEBFETCH_CANNOT_READ_JS_SPA
|
|
790
|
-
// structured failure. A single SPA short-circuit per conversation is
|
|
791
|
-
// expected — the hook is doing its job. Two or more in the same
|
|
792
|
-
// conversation within 5 minutes means either (a) the agent retried
|
|
793
|
-
// WebFetch on the same SPA URL despite the directive, or (b) the
|
|
794
|
-
// owner is asking about multiple SPA URLs in one session and the
|
|
795
|
-
// pattern needs surfacing as a recurring class. Both signal that the
|
|
796
|
-
// IDENTITY.md "Tool Failure Discipline" guidance is not landing in the
|
|
797
|
-
// prompt — revise the copy rather than add mechanical enforcement.
|
|
798
|
-
id: "webfetch-spa-short-circuit-recurring",
|
|
799
|
-
name: "WebFetch JS-SPA short-circuit fired repeatedly in conversation",
|
|
800
|
-
type: "repeated-error",
|
|
801
|
-
logSource: "system",
|
|
802
|
-
pattern: "WEBFETCH_CANNOT_READ_JS_SPA",
|
|
803
|
-
thresholdCount: 2,
|
|
804
|
-
thresholdWindowMinutes: 5,
|
|
805
|
-
scope: "session",
|
|
806
|
-
suggestedAction: "The WebFetch SPA preflight has fired more than once in this conversation. Either the agent is ignoring the loud-failure directive (retrying WebFetch after seeing WEBFETCH_CANNOT_READ_JS_SPA), or multiple SPA URLs are being asked about. Read the conversation's stream log for the [tool-use] / [tool-result] sequence around each occurrence \u2014 if the agent dispatched WebFetch on the same URL or substituted Playwright silently, revisit the IDENTITY.md `Tool Failure Discipline` paragraph that names structured-error handling."
|
|
807
|
-
},
|
|
808
|
-
{
|
|
809
|
-
// Task 538: fires when a [spawn] line appears in a conversation's stream
|
|
810
|
-
// log but no subprocess-lifecycle marker follows within 10s. The three
|
|
811
|
-
// acceptable followups are Task 535's contract — at least one must be
|
|
812
|
-
// emitted immediately at every spawn site. Their absence means
|
|
813
|
-
// `teeProcStderrToStreamLog` regressed, the markers drifted, or the
|
|
814
|
-
// spawn site was added without wiring them up. Session scope so a single
|
|
815
|
-
// broken conversation fires exactly once, not N times for every spawn.
|
|
816
|
-
id: "subproc-tee-silent-spawn",
|
|
817
|
-
name: "Subprocess spawn without a stderr-tee lifecycle marker",
|
|
818
|
-
type: "absent-followup",
|
|
819
|
-
logSource: "system",
|
|
820
|
-
pattern: "\\[spawn\\] pid=\\d+",
|
|
821
|
-
followupPattern: "\\[subproc-stderr-tee-attached\\]|\\[subproc-debug-unavailable\\]|\\[subproc-stderr-skip\\]",
|
|
822
|
-
followupWindowMs: 1e4,
|
|
823
|
-
thresholdCount: 0,
|
|
824
|
-
thresholdWindowMinutes: 0,
|
|
825
|
-
scope: "session",
|
|
826
|
-
suggestedAction: "The main-subprocess tee infrastructure has regressed \u2014 a spawn produced no lifecycle marker. Re-check `teeProcStderrToStreamLog` is invoked at the spawn site and that `[subproc-debug-unavailable]` or `[subproc-stderr-skip]` is written immediately when the tee cannot attach (Task 535 contract)."
|
|
827
|
-
},
|
|
828
|
-
{
|
|
829
|
-
// Task 533: surface every Cloudflare-plugin refusal. The plugin emits
|
|
830
|
-
// exactly one [cloudflare:refuse] line per refusal with a structured
|
|
831
|
-
// reason field; any single occurrence on a previously-clean device
|
|
832
|
-
// means the bound Cloudflare account does not match the operator's
|
|
833
|
-
// intent (or the post-flight FQDN drifted) and the operator needs to
|
|
834
|
-
// act in the dashboard.
|
|
835
|
-
id: "cloudflare-refuse",
|
|
836
|
-
name: "Cloudflare plugin refusal",
|
|
837
|
-
type: "silent-catch",
|
|
838
|
-
logSource: "any",
|
|
839
|
-
pattern: "\\[cloudflare:refuse\\]|\\[cloudflare:post-flight-mismatch\\]",
|
|
840
|
-
thresholdCount: 0,
|
|
841
|
-
thresholdWindowMinutes: 0,
|
|
842
|
-
suggestedAction: "The Cloudflare plugin refused an operation. Read the refusal `reason` field in the adjacent log line. For `account-drift` or `unbound-device`, run `tunnel-login force=true` while the operator is signed into the correct Cloudflare account in the browser. For `hostname-zone-not-routable`, the domain is not on Cloudflare yet \u2014 guide the operator to add it via the Cloudflare dashboard. For `post-flight-fqdn-mismatch` or `bound-account-does-not-own-hostname`, the laptop is signed into the wrong Cloudflare account \u2014 guide the operator to switch accounts in the dashboard, then re-run `tunnel-login force=true`."
|
|
843
|
-
},
|
|
844
|
-
{
|
|
845
|
-
// Task 540: the single highest-priority refusal — surface it immediately
|
|
846
|
-
// and independently of the generic cloudflare-refuse rule so the admin
|
|
847
|
-
// agent sees it on the very next turn. This is the exact class that
|
|
848
|
-
// burned the operator for 8 days across 9+ sessions (Apr 11–18, 2026):
|
|
849
|
-
// tunnel running locally, dashboard serving the wrong account, nothing
|
|
850
|
-
// from the internet reaches the laptop, and no prior telemetry surfaced
|
|
851
|
-
// it in time for the agent to self-correct.
|
|
852
|
-
id: "cloudflare-bound-account-mismatch",
|
|
853
|
-
name: "Cloudflare bound account does not own the configured hostnames",
|
|
854
|
-
type: "silent-catch",
|
|
855
|
-
logSource: "any",
|
|
856
|
-
pattern: '"reason":"bound-account-does-not-own-hostname"',
|
|
857
|
-
thresholdCount: 0,
|
|
858
|
-
thresholdWindowMinutes: 0,
|
|
859
|
-
suggestedAction: "This laptop is signed into a Cloudflare account that does not own the hostnames the tunnel is configured to serve. Run `tunnel-status` to confirm, then tell the operator verbatim: 'The tunnel is running on this laptop but nothing from the internet is reaching it. The Cloudflare account this laptop is signed into doesn't own your domain. Open Cloudflare in your browser \u2014 is the account name in the top-left the one that owns your domain? If not, switch to the correct one, then tell me and I will re-sign-in.' When the operator confirms the correct account is selected, run `tunnel-login force=true`."
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
// Task 545: tunnel-login's terminal-failure class — cloudflared's
|
|
863
|
-
// login process died without writing cert.pem. Covers every reason
|
|
864
|
-
// the handler emits on the `failed` branch: either an unknown exit
|
|
865
|
-
// (`-without-cert`), an exit preceded by the courtesy browser-launch
|
|
866
|
-
// marker (`-with-marker`), auth URL never produced (`-timeout`), or
|
|
867
|
-
// crashed before producing it at all. Task 541's original pattern
|
|
868
|
-
// matched `reason=browser-launch-fetch-error` — Task 545 retired
|
|
869
|
-
// that reason because the marker alone is no longer terminal
|
|
870
|
-
// (cloudflared keeps its OAuth-callback loop alive after emitting
|
|
871
|
-
// it). Use this pattern for any new terminal reason the handler
|
|
872
|
-
// gains: extend the alternation rather than adding parallel rules.
|
|
873
|
-
id: "cloudflare-tunnel-login-failed",
|
|
874
|
-
name: "Cloudflare tunnel-login process terminated without writing cert",
|
|
875
|
-
type: "silent-catch",
|
|
876
|
-
logSource: "any",
|
|
877
|
-
pattern: "\\[cloudflare:tunnel-login:failed\\] reason=(login-process-exited-without-cert|login-process-exited-with-marker|auth-url-timeout|process-exited-before-auth-url)",
|
|
878
|
-
thresholdCount: 0,
|
|
879
|
-
thresholdWindowMinutes: 0,
|
|
880
|
-
suggestedAction: "The cloudflared login process died before cert.pem landed on disk. For `login-process-exited-*` reasons the tunnel-login tool detected the dead process on the next call and has already respawned it \u2014 relay the new sign-in URL and wait for the operator to authorize in the VNC browser. For `auth-url-timeout` / `process-exited-before-auth-url`, the tool's most recent call returned an error and did not respawn \u2014 call `tunnel-login` again to spawn a fresh attempt. Never open the Cloudflare dashboard in any other surface; the only auth path is the sign-in URL the tool produces."
|
|
881
|
-
},
|
|
882
|
-
{
|
|
883
|
-
// Task 545: non-terminal advisory. cloudflared's browser-launch
|
|
884
|
-
// subcommand failed (DISPLAY unreachable, xdg-open absent, etc.) but
|
|
885
|
-
// its OAuth-callback listener is still running — the login can still
|
|
886
|
-
// complete if a human opens the URL. This rule fires so the admin
|
|
887
|
-
// agent can relay "open the URL yourself" to the operator the moment
|
|
888
|
-
// the condition appears, rather than waiting for the operator to
|
|
889
|
-
// notice nothing is happening in their browser. Task 546 will
|
|
890
|
-
// obsolete the advisory by rendering auth URLs that auto-open in
|
|
891
|
-
// the VNC browser.
|
|
892
|
-
id: "cloudflare-tunnel-login-browser-launch-failed",
|
|
893
|
-
name: "cloudflared couldn't open the sign-in URL (login still live)",
|
|
894
|
-
type: "silent-catch",
|
|
895
|
-
logSource: "any",
|
|
896
|
-
pattern: "\\[cloudflare:tunnel-login:browser-launch-failed\\]",
|
|
897
|
-
thresholdCount: 0,
|
|
898
|
-
thresholdWindowMinutes: 0,
|
|
899
|
-
suggestedAction: "cloudflared's browser-launch courtesy failed on this laptop, but the sign-in URL is still live \u2014 the process is waiting for the OAuth callback. Tell the operator to open the sign-in URL in the VNC browser themselves and complete the authorization. The tunnel-login tool already includes the URL and the advisory in its most recent response; do not restart the login (that would invalidate the URL the operator is about to open)."
|
|
900
|
-
},
|
|
901
|
-
{
|
|
902
|
-
// Task 545: raw-log surface. cloudflared-login.log is now read by
|
|
903
|
-
// the review-detector's `cloudflared` source (see sources.ts). The
|
|
904
|
-
// literal "Failed to fetch resource" is emitted by cloudflared
|
|
905
|
-
// itself when its browser-launch subcommand can't reach a display.
|
|
906
|
-
// This rule catches the cloudflared-side event even if the MCP
|
|
907
|
-
// handler's classification drifts or the platform is restarting
|
|
908
|
-
// mid-login and the advisory log line is not yet written. Keeping
|
|
909
|
-
// this rule on a different source (log-line, not MCP stderr) makes
|
|
910
|
-
// the detection redundant in the "defence in depth" sense — if the
|
|
911
|
-
// MCP classification ever regresses, this still fires.
|
|
912
|
-
id: "cloudflared-login-browser-launch-failed-raw",
|
|
913
|
-
name: "cloudflared login \u2014 browser-launch fetch error in log",
|
|
914
|
-
type: "silent-catch",
|
|
915
|
-
logSource: "cloudflared",
|
|
916
|
-
pattern: "Failed to fetch resource",
|
|
917
|
-
thresholdCount: 0,
|
|
918
|
-
thresholdWindowMinutes: 0,
|
|
919
|
-
suggestedAction: "cloudflared-login.log shows the browser-launch courtesy failed. The sign-in URL from the last tunnel-login call is still live \u2014 the cloudflared process waits for the OAuth callback regardless of whether it could open the browser itself. Tell the operator to open the sign-in URL manually in the VNC browser on the device."
|
|
920
|
-
},
|
|
921
|
-
{
|
|
922
|
-
// Task 540: cloudflared.log is the one file most likely to carry the
|
|
923
|
-
// "tunnel is having real-world connectivity problems" signal — QUIC
|
|
924
|
-
// connection failures, connector drops, edge unreachability. Prior to
|
|
925
|
-
// this rule it was written but never read (the review-detector had no
|
|
926
|
-
// rule coverage for it). A single ERR line is worth surfacing; the
|
|
927
|
-
// tee'd output is typically noise-free.
|
|
928
|
-
// Task 862: setup-tunnel.sh emits `[script:setup-tunnel]
|
|
929
|
-
// step=onboarding-persist result=skipped reason=no-account-dir` via
|
|
930
|
-
// phase_line when ACCOUNT_DIR is unset. Pre-Task-862, the form-driven
|
|
931
|
-
// action runner threaded STREAM_LOG_PATH but not ACCOUNT_DIR, so the
|
|
932
|
-
// line landed in the agent's stream log (system source) and the user
|
|
933
|
-
// looped on currentStep=6 indefinitely.
|
|
934
|
-
// The agent-via-Bash path also threads STREAM_LOG_PATH; operator-SSH
|
|
935
|
-
// does not — that's the disambiguator. If this pattern reappears in
|
|
936
|
-
// a system log, a future invocation surface forgot to declare
|
|
937
|
-
// ACCOUNT_DIR. Fix at action-runner.ts WHITELIST['cloudflare-setup'],
|
|
938
|
-
// not in the script.
|
|
939
|
-
id: "cloudflare-setup-account-dir-missing",
|
|
940
|
-
name: "cloudflare-setup ran without ACCOUNT_DIR \u2014 onboarding step-7 will not persist",
|
|
941
|
-
type: "silent-catch",
|
|
942
|
-
logSource: "system",
|
|
943
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=skipped reason=no-account-dir",
|
|
944
|
-
thresholdCount: 0,
|
|
945
|
-
thresholdWindowMinutes: 0,
|
|
946
|
-
suggestedAction: "An invocation surface for setup-tunnel.sh failed to declare ACCOUNT_DIR. Without it the script's step-7 persist block (Task 562) skips, leaving OnboardingState.currentStep=6 forever. Inspect platform/ui/server/lib/action-runner.ts WHITELIST['cloudflare-setup'].build (Task 862 thread) and platform/ui/app/lib/claude-agent/spawn-env.ts buildSpawnEnv (Task 562 thread) \u2014 confirm ACCOUNT_DIR is in the env map for whichever surface the action-id prefix indicates. Do NOT change setup-tunnel.sh; the skipped branch is correct for operator-SSH reconfigure flows."
|
|
947
|
-
},
|
|
948
|
-
{
|
|
949
|
-
id: "cloudflared-edge-errors",
|
|
950
|
-
name: "cloudflared edge connectivity errors",
|
|
951
|
-
type: "silent-catch",
|
|
952
|
-
logSource: "cloudflared",
|
|
953
|
-
pattern: "^\\S+ ERR (Failed to refresh protocol|no more connections active|Failed to dial a quic connection)",
|
|
954
|
-
thresholdCount: 0,
|
|
955
|
-
thresholdWindowMinutes: 0,
|
|
956
|
-
suggestedAction: "cloudflared is reporting edge connectivity errors in its daemon log. Read the last 20 lines of `cloudflared.log` to see the surrounding context. Transient QUIC drops are normal; sustained failures (more than a handful in a minute) point to either a network issue on this laptop or an edge-side routing problem. Run `tunnel-status` to check whether end-to-end probing still succeeds."
|
|
957
|
-
},
|
|
958
|
-
{
|
|
959
|
-
// Task 543: fires when an agent turn contains an opposing-axis choice-fork
|
|
960
|
-
// question ("Want me to X, or Y?"). Every occurrence is a violation of the
|
|
961
|
-
// IDENTITY § Questions rule — Rule A (one-sided questions) catches them
|
|
962
|
-
// all, and the sharper sub-class (Rule B — no menu when a tool returned a
|
|
963
|
-
// deterministic signal) is isolated by Task 544's `preceded-by` tightening.
|
|
964
|
-
// logSource is "any" so the rule catches violations on both the admin
|
|
965
|
-
// stream (claude-agent-stream-*) and the public agent stream
|
|
966
|
-
// (public-agent-stream-*); the regex is agent-phrasing-specific and has
|
|
967
|
-
// not been observed in other log contexts. Session scope groups matches
|
|
968
|
-
// by conversationId so a single offending turn fires exactly once.
|
|
969
|
-
id: "agent-choice-fork",
|
|
970
|
-
name: "Agent emitted a choice-fork question",
|
|
971
|
-
type: "repeated-error",
|
|
972
|
-
logSource: "any",
|
|
973
|
-
pattern: "Want me to [^\\n]+, or [^\\n]+\\?",
|
|
974
|
-
thresholdCount: 1,
|
|
975
|
-
thresholdWindowMinutes: 60,
|
|
976
|
-
scope: "session",
|
|
977
|
-
suggestedAction: 'Agent emitted a choice-fork question ("Want me to X, or Y?") instead of a one-sided question or the prescribed action. Review Task 543 IDENTITY \xA7 Questions \u2014 the agent asked an opposing-axis question, or degraded a deterministic tool signal into a menu. The log sample shows the offending turn verbatim.'
|
|
978
|
-
},
|
|
979
|
-
{
|
|
980
|
-
// Task 546: fires when the operator clicks a device-bound URL affordance
|
|
981
|
-
// and the chat UI cannot drive the device browser — either CDP is
|
|
982
|
-
// unreachable, the navigation timed out, or CDP returned an error. The
|
|
983
|
-
// log line carries intent, hostname, and navigateResult so the admin
|
|
984
|
-
// agent can name the affected flow and hostname verbatim on its next
|
|
985
|
-
// turn. Every occurrence is worth surfacing (thresholdCount: 0) because
|
|
986
|
-
// this is the exact class of silent failure Task 546 exists to close:
|
|
987
|
-
// the operator clicked, nothing happened on the device, and if we don't
|
|
988
|
-
// review the click telemetry the agent has no way to know the flow is
|
|
989
|
-
// stuck.
|
|
990
|
-
id: "device-url-click-failed",
|
|
991
|
-
name: "Device-bound URL click failed to drive the VNC browser",
|
|
992
|
-
type: "silent-catch",
|
|
993
|
-
logSource: "server",
|
|
994
|
-
// Enumerate the NavigateResult union explicitly rather than relying
|
|
995
|
-
// on a (?!ok) negative lookahead anchored to a specific token order.
|
|
996
|
-
// If a new member is added to the union in cdp-client.ts, this rule
|
|
997
|
-
// must be updated in the same commit — the pattern is order-agnostic
|
|
998
|
-
// (browser= and navigateResult= can appear in either order) and the
|
|
999
|
-
// enumerated list compile-fails the source if it ever drifts from
|
|
1000
|
-
// the shared type in device-url-schema.ts.
|
|
1001
|
-
pattern: "\\[device-url:click\\][^\\n]*(?:browser=fallback|navigateResult=(?:timeout|cdp-unreachable|error))",
|
|
1002
|
-
thresholdCount: 0,
|
|
1003
|
-
thresholdWindowMinutes: 0,
|
|
1004
|
-
suggestedAction: "A device-bound URL click failed to drive Chromium on the device's VNC display. Identify the `intent` and `hostname` from the log line, then check the VNC surface: read `vnc-boot.log` and confirm Chromium on :99 is responding on CDP port 9222. If CDP is unreachable, the operator needs to restart the VNC stack; if CDP is reachable but navigation errored, the URL itself may be malformed upstream \u2014 grep the stream log for the originating `[device-url:render]` line."
|
|
1005
|
-
},
|
|
1006
|
-
{
|
|
1007
|
-
// Task 554: fires when an agent turn emits a synthesized localhost/127.0.0.1
|
|
1008
|
-
// `__remote-auth/setup` URL. The only legitimate source for that URL is
|
|
1009
|
-
// `remote-auth-status`, which emits it inside a `maxy-device-url` affordance
|
|
1010
|
-
// block with the device's real hostname. A synthesized localhost variant is
|
|
1011
|
-
// the exact failure mode Task 554 closed: the removed "direct the user to
|
|
1012
|
-
// /__remote-auth/setup" clause in the password-setter's tool description
|
|
1013
|
-
// invited URL synthesis from a partial path. Session scope groups matches
|
|
1014
|
-
// by conversationId so a single offending turn fires exactly once, even if
|
|
1015
|
-
// the URL appears multiple times in the streamed response.
|
|
1016
|
-
id: "invented-remote-auth-url",
|
|
1017
|
-
name: "Invented localhost/127.0.0.1 remote-auth setup URL",
|
|
1018
|
-
type: "silent-catch",
|
|
1019
|
-
logSource: "any",
|
|
1020
|
-
pattern: "http(s)?://(localhost|127\\.0\\.0\\.1)[:\\w/.-]*__remote-auth/setup",
|
|
1021
|
-
thresholdCount: 0,
|
|
1022
|
-
thresholdWindowMinutes: 0,
|
|
1023
|
-
scope: "session",
|
|
1024
|
-
suggestedAction: "[Task 554 regression] An assistant turn emitted a synthesized localhost/127.0.0.1 remote-auth setup URL. The only authority for the browser-setup URL is `remote-auth-status`, which emits a `maxy-device-url` affordance block with the device's real hostname. Review the offending turn, confirm the `remote-auth-set-password` tool description and onboarding SKILL.md step 3 still lack URL-synthesis language, and patch whichever prose surface invited the synthesis."
|
|
1025
|
-
},
|
|
1026
|
-
{
|
|
1027
|
-
// Task 553: fires when `anthropic-setup` auto-resets a revoked API key.
|
|
1028
|
-
// The auto-reset is silent to the agent (the tool falls through to
|
|
1029
|
-
// awaiting_signin in the same call), but it is operator-visible — the
|
|
1030
|
-
// user's previously-stored key was just deleted because Anthropic
|
|
1031
|
-
// rejected it. Surfacing the event lets the admin agent explain on the
|
|
1032
|
-
// next turn why the user is being asked to sign in again instead of
|
|
1033
|
-
// continuing normally. The matching string is the exact stable prefix
|
|
1034
|
-
// emitted by the state machine immediately before `deleteKey()` is
|
|
1035
|
-
// invoked.
|
|
1036
|
-
id: "anthropic-setup-auth-error-auto-reset",
|
|
1037
|
-
name: "Anthropic API key auto-reset on auth_error",
|
|
1038
|
-
type: "silent-catch",
|
|
1039
|
-
logSource: "any",
|
|
1040
|
-
pattern: "\\[anthropic-setup\\] auth_error",
|
|
1041
|
-
thresholdCount: 0,
|
|
1042
|
-
thresholdWindowMinutes: 0,
|
|
1043
|
-
suggestedAction: "The stored Anthropic API key was rejected by console.anthropic.com (invalid, revoked, or expired) and `anthropic-setup` auto-deleted it. On the next operator interaction, explain that the key was cleared and walk them through sign-in again via the onboarding skill \u2014 the tool already returned `awaiting_signin` with the correct `browser_evaluate` action on the same call."
|
|
1044
|
-
},
|
|
1045
|
-
{
|
|
1046
|
-
// Task 562: setup-tunnel.sh persists step-7 completion to a filesystem
|
|
1047
|
-
// flag before arming the service restart. The flag is consumed by the
|
|
1048
|
-
// next session's `loadOnboardingStep` and `getOnboardingState` calls,
|
|
1049
|
-
// so any runtime failure to write it means the next admin session
|
|
1050
|
-
// will re-ask the Cloudflare question the user just answered. Every
|
|
1051
|
-
// occurrence is loud-surface-worthy: the cause is either a permission
|
|
1052
|
-
// issue on the onboarding directory or a filesystem problem on the
|
|
1053
|
-
// device, and the recovery is deterministic (the operator can re-run
|
|
1054
|
-
// setup-tunnel.sh or call `onboarding-complete-step` explicitly).
|
|
1055
|
-
id: "setup-tunnel-onboarding-persist-failed",
|
|
1056
|
-
name: "setup-tunnel.sh failed to persist step-7 completion",
|
|
1057
|
-
type: "silent-catch",
|
|
1058
|
-
logSource: "any",
|
|
1059
|
-
pattern: "\\[script:setup-tunnel\\] step=onboarding-persist result=error",
|
|
1060
|
-
thresholdCount: 0,
|
|
1061
|
-
thresholdWindowMinutes: 0,
|
|
1062
|
-
suggestedAction: "[Task 562] setup-tunnel.sh could not persist step-7 completion before arming the service restart. Read the `reason` field on the failing phase line: `fs-flag-dir-failed` means `${ACCOUNT_DIR}/onboarding/` is not writable (chmod issue or disk full); `fs-flag-write-failed` means the flag file itself could not be written. Without the flag, the next admin session will re-ask the Cloudflare question the user just answered. Recovery: fix the permission/disk issue, then either re-run `~/setup-tunnel.sh` (the flag-write is idempotent) or call `onboarding-complete-step` with step 7 from the admin chat to mark step 7 complete explicitly."
|
|
1063
|
-
},
|
|
1064
|
-
{
|
|
1065
|
-
// Task 570: every `[llm-call] adminModel resolution FAILED:` line
|
|
1066
|
-
// is a workflow LLM step that could not resolve a model because
|
|
1067
|
-
// readAdminModel returned null. The stderr line carries `reason=`
|
|
1068
|
-
// (enoent/eacces/fs_error/parse_error/field_missing/
|
|
1069
|
-
// field_wrong_type/field_empty) so the admin agent can act on the
|
|
1070
|
-
// specific failure mode rather than reading the generic persisted
|
|
1071
|
-
// error and misdiagnosing. Every occurrence is worth surfacing —
|
|
1072
|
-
// the workflow already failed when this line was emitted.
|
|
1073
|
-
id: "workflow-admin-model-resolution-failed",
|
|
1074
|
-
name: "Workflow readAdminModel returned null (reason code in log)",
|
|
1075
|
-
type: "silent-catch",
|
|
1076
|
-
logSource: "any",
|
|
1077
|
-
pattern: "\\[llm-call\\] adminModel resolution FAILED:",
|
|
1078
|
-
thresholdCount: 0,
|
|
1079
|
-
thresholdWindowMinutes: 0,
|
|
1080
|
-
suggestedAction: "A workflow LLM step could not resolve the account's adminModel. Read the log line's `reason=` field: `enoent` means the account.json path is wrong or the account is unprovisioned \u2014 inspect `path=` and confirm it exists. `eacces` means permission drift \u2014 check file ownership. `parse_error` means the file is malformed, likely mid-write or truncated \u2014 read the file and repair. `field_missing` / `field_wrong_type` / `field_empty` mean the `adminModel` field needs to be set to a valid model ID. Never edit the persisted WorkflowRun.error string; fix the underlying file or permissions and re-run the workflow."
|
|
1081
|
-
},
|
|
1082
|
-
{
|
|
1083
|
-
// Task 561: fires when an admin-agent Bash tool call installs
|
|
1084
|
-
// `bind9-dnsutils` at runtime. Post-Task-561 the Maxy installer
|
|
1085
|
-
// (platform/scripts/setup.sh) provisions the package on every fresh
|
|
1086
|
-
// and upgrade install, so `dig` is always in PATH before
|
|
1087
|
-
// setup-tunnel.sh runs. An admin apt-install of bind9-dnsutils is
|
|
1088
|
-
// therefore evidence the installer regressed (or the host is on a
|
|
1089
|
-
// pre-Task-561 image); in either case the fix is to re-run the
|
|
1090
|
-
// installer, not to patch apt-state from chat. Session scope so a
|
|
1091
|
-
// single offending turn fires exactly once.
|
|
1092
|
-
id: "admin-agent-apt-bind9-install",
|
|
1093
|
-
name: "Admin agent installed bind9-dnsutils at runtime (installer regression)",
|
|
1094
|
-
type: "repeated-error",
|
|
1095
|
-
logSource: "any",
|
|
1096
|
-
pattern: "\\[tool-use\\][^\\n]*name=Bash[^\\n]*apt-get install[^\\n]*bind9-dnsutils",
|
|
1097
|
-
thresholdCount: 1,
|
|
1098
|
-
thresholdWindowMinutes: 60,
|
|
1099
|
-
scope: "session",
|
|
1100
|
-
suggestedAction: "[Task 561 regression] The admin agent ran `apt-get install bind9-dnsutils` \u2014 the exact workaround Task 561 eliminated. The Maxy installer provisions `bind9-dnsutils` in `platform/scripts/setup.sh` so `dig` is always in PATH before `setup-tunnel.sh` runs. Do not patch apt-state from chat \u2014 re-run the installer (`npx -y @rubytech/create-maxy`) and verify the installer's apt-get line still includes `bind9-dnsutils`. If the package is present in setup.sh but missing on the device, the installer did not re-run step 1 on the current image."
|
|
1101
|
-
}
|
|
1102
|
-
];
|
|
1103
|
-
}
|
|
1104
|
-
function rulesFilePath(configDir2) {
|
|
1105
|
-
return resolve(configDir2, "review-rules.json");
|
|
1106
|
-
}
|
|
1107
|
-
function ensureRulesFile(configDir2) {
|
|
1108
|
-
const path2 = rulesFilePath(configDir2);
|
|
1109
|
-
if (existsSync2(path2)) return { created: false, path: path2 };
|
|
1110
|
-
mkdirSync(dirname(path2), { recursive: true });
|
|
1111
|
-
const body = {
|
|
1112
|
-
scanIntervalMs: DEFAULT_SCAN_INTERVAL_MS,
|
|
1113
|
-
rules: defaultRules()
|
|
1114
|
-
};
|
|
1115
|
-
atomicWriteJson(path2, body);
|
|
1116
|
-
return { created: true, path: path2 };
|
|
1117
|
-
}
|
|
1118
|
-
function loadRules(configDir2) {
|
|
1119
|
-
const path2 = rulesFilePath(configDir2);
|
|
1120
|
-
if (!existsSync2(path2)) {
|
|
1121
|
-
throw new Error(`rules file missing at ${path2}`);
|
|
1122
|
-
}
|
|
1123
|
-
const raw = readFileSync(path2, "utf-8");
|
|
1124
|
-
let parsed;
|
|
1125
|
-
try {
|
|
1126
|
-
parsed = JSON.parse(raw);
|
|
1127
|
-
} catch (err) {
|
|
1128
|
-
throw new Error(`rules file ${path2} is not valid JSON: ${err instanceof Error ? err.message : String(err)}`);
|
|
1129
|
-
}
|
|
1130
|
-
return validateRulesFile(parsed, path2);
|
|
1131
|
-
}
|
|
1132
|
-
function rulesFileMtime(configDir2) {
|
|
1133
|
-
const path2 = rulesFilePath(configDir2);
|
|
1134
|
-
try {
|
|
1135
|
-
return statSync2(path2).mtimeMs;
|
|
1136
|
-
} catch {
|
|
1137
|
-
return null;
|
|
1138
|
-
}
|
|
1139
|
-
}
|
|
1140
|
-
function saveRules(configDir2, file) {
|
|
1141
|
-
validateRulesFile(file, rulesFilePath(configDir2));
|
|
1142
|
-
atomicWriteJson(rulesFilePath(configDir2), file);
|
|
1143
|
-
}
|
|
1144
|
-
function atomicWriteJson(path2, body) {
|
|
1145
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1146
|
-
writeFileSync(tmp, JSON.stringify(body, null, 2) + "\n", "utf-8");
|
|
1147
|
-
renameSync(tmp, path2);
|
|
1148
|
-
}
|
|
1149
|
-
function validateRulesFile(input, sourceLabel) {
|
|
1150
|
-
if (!input || typeof input !== "object") {
|
|
1151
|
-
throw new Error(`${sourceLabel}: top-level must be an object`);
|
|
1152
|
-
}
|
|
1153
|
-
const obj = input;
|
|
1154
|
-
const scanIntervalMs = obj.scanIntervalMs;
|
|
1155
|
-
if (typeof scanIntervalMs !== "number" || scanIntervalMs < 500 || scanIntervalMs > 3e5) {
|
|
1156
|
-
throw new Error(`${sourceLabel}: scanIntervalMs must be a number between 500 and 300000`);
|
|
1157
|
-
}
|
|
1158
|
-
const rulesRaw = obj.rules;
|
|
1159
|
-
if (!Array.isArray(rulesRaw)) {
|
|
1160
|
-
throw new Error(`${sourceLabel}: rules must be an array`);
|
|
1161
|
-
}
|
|
1162
|
-
const ids = /* @__PURE__ */ new Set();
|
|
1163
|
-
const rules = rulesRaw.map((r, i) => validateRule(r, `${sourceLabel}#rules[${i}]`, ids));
|
|
1164
|
-
return { scanIntervalMs, rules };
|
|
1165
|
-
}
|
|
1166
|
-
function addMissingDefaultRules(rulesFile) {
|
|
1167
|
-
const existingIds = new Set(rulesFile.rules.map((r) => r.id));
|
|
1168
|
-
const defaults = defaultRules();
|
|
1169
|
-
let mutated = false;
|
|
1170
|
-
for (const rule of defaults) {
|
|
1171
|
-
if (!existingIds.has(rule.id)) {
|
|
1172
|
-
rulesFile.rules.push(rule);
|
|
1173
|
-
mutated = true;
|
|
1174
|
-
}
|
|
1175
|
-
}
|
|
1176
|
-
return mutated;
|
|
1177
|
-
}
|
|
1178
|
-
function migrateRateLimitPattern(rulesFile) {
|
|
1179
|
-
const rule = rulesFile.rules.find((r) => r.id === "http-rate-limit-429");
|
|
1180
|
-
if (!rule) return false;
|
|
1181
|
-
if (rule.pattern !== RATE_LIMIT_PATTERN_V1) return false;
|
|
1182
|
-
rule.pattern = RATE_LIMIT_PATTERN;
|
|
1183
|
-
return true;
|
|
1184
|
-
}
|
|
1185
|
-
function validateRule(input, label, seenIds) {
|
|
1186
|
-
if (!input || typeof input !== "object") {
|
|
1187
|
-
throw new Error(`${label}: rule must be an object`);
|
|
1188
|
-
}
|
|
1189
|
-
const r = input;
|
|
1190
|
-
const id = r.id;
|
|
1191
|
-
if (typeof id !== "string" || id.length === 0) {
|
|
1192
|
-
throw new Error(`${label}: id must be a non-empty string`);
|
|
1193
|
-
}
|
|
1194
|
-
if (seenIds.has(id)) {
|
|
1195
|
-
throw new Error(`${label}: duplicate rule id "${id}"`);
|
|
1196
|
-
}
|
|
1197
|
-
seenIds.add(id);
|
|
1198
|
-
const name = r.name;
|
|
1199
|
-
if (typeof name !== "string" || name.length === 0) {
|
|
1200
|
-
throw new Error(`${label}: name must be a non-empty string`);
|
|
1201
|
-
}
|
|
1202
|
-
const type = r.type;
|
|
1203
|
-
if (typeof type !== "string" || !VALID_TYPES.has(type)) {
|
|
1204
|
-
throw new Error(`${label}: type must be one of ${[...VALID_TYPES].join(", ")}`);
|
|
1205
|
-
}
|
|
1206
|
-
const logSource = r.logSource;
|
|
1207
|
-
if (typeof logSource !== "string" || !VALID_SOURCES.has(logSource)) {
|
|
1208
|
-
throw new Error(`${label}: logSource must be one of ${[...VALID_SOURCES].join(", ")}`);
|
|
1209
|
-
}
|
|
1210
|
-
const pattern = r.pattern;
|
|
1211
|
-
if (typeof pattern !== "string") {
|
|
1212
|
-
throw new Error(`${label}: pattern must be a string (may be empty for stale-log/file-write-storm)`);
|
|
1213
|
-
}
|
|
1214
|
-
if (pattern.length > 0) {
|
|
1215
|
-
try {
|
|
1216
|
-
new RegExp(pattern);
|
|
1217
|
-
} catch (err) {
|
|
1218
|
-
throw new Error(`${label}: pattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1219
|
-
}
|
|
1220
|
-
}
|
|
1221
|
-
const thresholdCount = r.thresholdCount;
|
|
1222
|
-
if (typeof thresholdCount !== "number" || thresholdCount < 0) {
|
|
1223
|
-
throw new Error(`${label}: thresholdCount must be a non-negative number`);
|
|
1224
|
-
}
|
|
1225
|
-
const thresholdWindowMinutes = r.thresholdWindowMinutes;
|
|
1226
|
-
if (typeof thresholdWindowMinutes !== "number" || thresholdWindowMinutes < 0) {
|
|
1227
|
-
throw new Error(`${label}: thresholdWindowMinutes must be a non-negative number`);
|
|
1228
|
-
}
|
|
1229
|
-
const suggestedAction = r.suggestedAction;
|
|
1230
|
-
if (typeof suggestedAction !== "string" || suggestedAction.length === 0) {
|
|
1231
|
-
throw new Error(`${label}: suggestedAction must be a non-empty string`);
|
|
1232
|
-
}
|
|
1233
|
-
const rule = {
|
|
1234
|
-
id,
|
|
1235
|
-
name,
|
|
1236
|
-
type,
|
|
1237
|
-
logSource,
|
|
1238
|
-
pattern,
|
|
1239
|
-
thresholdCount,
|
|
1240
|
-
thresholdWindowMinutes,
|
|
1241
|
-
suggestedAction
|
|
1242
|
-
};
|
|
1243
|
-
if (typeof r.watchPath === "string") rule.watchPath = r.watchPath;
|
|
1244
|
-
if (typeof r.staleHours === "number") rule.staleHours = r.staleHours;
|
|
1245
|
-
if (typeof r.followupPattern === "string") {
|
|
1246
|
-
if (r.followupPattern.length > 0) {
|
|
1247
|
-
try {
|
|
1248
|
-
new RegExp(r.followupPattern);
|
|
1249
|
-
} catch (err) {
|
|
1250
|
-
throw new Error(`${label}: followupPattern is not a valid regex: ${err instanceof Error ? err.message : String(err)}`);
|
|
1251
|
-
}
|
|
1252
|
-
}
|
|
1253
|
-
rule.followupPattern = r.followupPattern;
|
|
1254
|
-
}
|
|
1255
|
-
if (typeof r.followupWindowMs === "number") rule.followupWindowMs = r.followupWindowMs;
|
|
1256
|
-
if (typeof r.suppressedUntil === "string") rule.suppressedUntil = r.suppressedUntil;
|
|
1257
|
-
if (r.scope !== void 0) {
|
|
1258
|
-
if (typeof r.scope !== "string" || !VALID_SCOPES.has(r.scope)) {
|
|
1259
|
-
throw new Error(`${label}: scope must be one of ${[...VALID_SCOPES].join(", ")}`);
|
|
1260
|
-
}
|
|
1261
|
-
rule.scope = r.scope;
|
|
1262
|
-
}
|
|
1263
|
-
if (rule.type === "file-write-storm" || rule.type === "stale-log") {
|
|
1264
|
-
if (!rule.watchPath) {
|
|
1265
|
-
throw new Error(`${label}: ${rule.type} rules require watchPath`);
|
|
1266
|
-
}
|
|
1267
|
-
}
|
|
1268
|
-
if (rule.type === "stale-log") {
|
|
1269
|
-
if (typeof rule.staleHours !== "number" || rule.staleHours <= 0) {
|
|
1270
|
-
throw new Error(`${label}: stale-log rules require a positive staleHours`);
|
|
1271
|
-
}
|
|
1272
|
-
}
|
|
1273
|
-
if (rule.type === "absent-followup") {
|
|
1274
|
-
if (rule.pattern.length === 0) {
|
|
1275
|
-
throw new Error(`${label}: absent-followup rules require a non-empty pattern`);
|
|
1276
|
-
}
|
|
1277
|
-
if (typeof rule.followupPattern !== "string" || rule.followupPattern.length === 0) {
|
|
1278
|
-
throw new Error(`${label}: absent-followup rules require a non-empty followupPattern`);
|
|
1279
|
-
}
|
|
1280
|
-
if (typeof rule.followupWindowMs !== "number" || rule.followupWindowMs <= 0 || rule.followupWindowMs > MAX_FOLLOWUP_WINDOW_MS) {
|
|
1281
|
-
throw new Error(`${label}: absent-followup rules require followupWindowMs in (0, ${MAX_FOLLOWUP_WINDOW_MS}]`);
|
|
1282
|
-
}
|
|
1283
|
-
}
|
|
1284
|
-
return rule;
|
|
1285
|
-
}
|
|
1286
|
-
|
|
1287
|
-
// app/lib/review-detector/sources.ts
|
|
1288
|
-
import { existsSync as existsSync3, readdirSync, statSync as statSync3, writeFileSync as writeFileSync2, renameSync as renameSync2, mkdirSync as mkdirSync2, openSync, readSync, closeSync, readFileSync as readFileSync2 } from "fs";
|
|
1289
|
-
import { resolve as resolve2, join as join2, basename, dirname as dirname2 } from "path";
|
|
1290
|
-
function tailStatePath(configDir2) {
|
|
1291
|
-
return resolve2(configDir2, "review-state.json");
|
|
1292
|
-
}
|
|
1293
|
-
function loadTailState(configDir2) {
|
|
1294
|
-
const path2 = tailStatePath(configDir2);
|
|
1295
|
-
if (!existsSync3(path2)) return {};
|
|
1296
|
-
try {
|
|
1297
|
-
const raw = readFileSync2(path2, "utf-8");
|
|
1298
|
-
const parsed = JSON.parse(raw);
|
|
1299
|
-
if (!parsed || typeof parsed !== "object") return {};
|
|
1300
|
-
const clean = {};
|
|
1301
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
1302
|
-
const entry = value;
|
|
1303
|
-
if (entry && typeof entry.offset === "number" && typeof entry.size === "number" && typeof entry.inode === "number") {
|
|
1304
|
-
clean[key] = entry;
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
return clean;
|
|
1308
|
-
} catch (err) {
|
|
1309
|
-
console.error(`[review] tail state corrupt at ${path2}, starting fresh: ${err instanceof Error ? err.message : String(err)}`);
|
|
1310
|
-
return {};
|
|
1311
|
-
}
|
|
1312
|
-
}
|
|
1313
|
-
function saveTailState(configDir2, state) {
|
|
1314
|
-
const path2 = tailStatePath(configDir2);
|
|
1315
|
-
mkdirSync2(dirname2(path2), { recursive: true });
|
|
1316
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1317
|
-
writeFileSync2(tmp, JSON.stringify(state, null, 2) + "\n", "utf-8");
|
|
1318
|
-
renameSync2(tmp, path2);
|
|
1319
|
-
}
|
|
1320
|
-
function discoverSourceFiles(configDir2, accountLogDir2, logicalSource) {
|
|
1321
|
-
if (logicalSource === "server") {
|
|
1322
|
-
const p = resolve2(configDir2, "logs", "server.log");
|
|
1323
|
-
return existsSync3(p) ? [{ logicalSource: "server", filepath: p }] : [];
|
|
1324
|
-
}
|
|
1325
|
-
if (logicalSource === "vnc") {
|
|
1326
|
-
const p = resolve2(configDir2, "logs", "vnc-boot.log");
|
|
1327
|
-
return existsSync3(p) ? [{ logicalSource: "vnc", filepath: p }] : [];
|
|
1328
|
-
}
|
|
1329
|
-
if (logicalSource === "cloudflared") {
|
|
1330
|
-
const files2 = [];
|
|
1331
|
-
const daemon = resolve2(configDir2, "logs", "cloudflared.log");
|
|
1332
|
-
if (existsSync3(daemon)) files2.push({ logicalSource: "cloudflared", filepath: daemon });
|
|
1333
|
-
const login = resolve2(configDir2, "logs", "cloudflared-login.log");
|
|
1334
|
-
if (existsSync3(login)) files2.push({ logicalSource: "cloudflared", filepath: login });
|
|
1335
|
-
return files2;
|
|
1336
|
-
}
|
|
1337
|
-
const prefix = {
|
|
1338
|
-
system: "claude-agent-stream-",
|
|
1339
|
-
error: "claude-agent-stderr-",
|
|
1340
|
-
session: "sse-events-",
|
|
1341
|
-
public: "public-agent-stream-",
|
|
1342
|
-
mcp: "mcp-"
|
|
1343
|
-
}[logicalSource];
|
|
1344
|
-
if (!existsSync3(accountLogDir2)) return [];
|
|
1345
|
-
const files = [];
|
|
1346
|
-
let scanned = 0;
|
|
1347
|
-
let skippedPrefixMismatch = 0;
|
|
1348
|
-
let skippedNotLog = 0;
|
|
1349
|
-
for (const entry of readdirSync(accountLogDir2)) {
|
|
1350
|
-
scanned += 1;
|
|
1351
|
-
const matchesPrefix = entry.startsWith(prefix);
|
|
1352
|
-
const isLog = entry.endsWith(".log");
|
|
1353
|
-
if (matchesPrefix && isLog) {
|
|
1354
|
-
files.push({ logicalSource, filepath: join2(accountLogDir2, entry) });
|
|
1355
|
-
} else if (!matchesPrefix) {
|
|
1356
|
-
skippedPrefixMismatch += 1;
|
|
1357
|
-
} else {
|
|
1358
|
-
skippedNotLog += 1;
|
|
1359
|
-
}
|
|
1360
|
-
}
|
|
1361
|
-
files.sort((a, b) => {
|
|
1362
|
-
try {
|
|
1363
|
-
return statSync3(b.filepath).mtimeMs - statSync3(a.filepath).mtimeMs;
|
|
1364
|
-
} catch {
|
|
1365
|
-
return a.filepath.localeCompare(b.filepath);
|
|
1366
|
-
}
|
|
1367
|
-
});
|
|
1368
|
-
if (skippedPrefixMismatch > 0 || skippedNotLog > 0) {
|
|
1369
|
-
console.error(`[review-scan-skip] dir=${accountLogDir2} source=${logicalSource} prefix=${prefix} scanned=${scanned} matched=${files.length} skipped_prefix=${skippedPrefixMismatch} skipped_non_log=${skippedNotLog}`);
|
|
1370
|
-
}
|
|
1371
|
-
return files;
|
|
1372
|
-
}
|
|
1373
|
-
function discoverAllSources(configDir2, accountLogDir2) {
|
|
1374
|
-
return [
|
|
1375
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "server"),
|
|
1376
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "vnc"),
|
|
1377
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "system"),
|
|
1378
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "error"),
|
|
1379
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "session"),
|
|
1380
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "public"),
|
|
1381
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "mcp"),
|
|
1382
|
-
...discoverSourceFiles(configDir2, accountLogDir2, "cloudflared")
|
|
1383
|
-
];
|
|
1384
|
-
}
|
|
1385
|
-
function readNewLines(filepath, prev) {
|
|
1386
|
-
if (!existsSync3(filepath)) return null;
|
|
1387
|
-
const stat7 = statSync3(filepath);
|
|
1388
|
-
const size = stat7.size;
|
|
1389
|
-
const inode = stat7.ino;
|
|
1390
|
-
let startOffset = 0;
|
|
1391
|
-
let rotated = false;
|
|
1392
|
-
let truncated = false;
|
|
1393
|
-
if (prev) {
|
|
1394
|
-
if (prev.inode !== inode) {
|
|
1395
|
-
rotated = true;
|
|
1396
|
-
startOffset = 0;
|
|
1397
|
-
} else if (size < prev.offset) {
|
|
1398
|
-
truncated = true;
|
|
1399
|
-
startOffset = 0;
|
|
1400
|
-
} else {
|
|
1401
|
-
startOffset = prev.offset;
|
|
1402
|
-
}
|
|
1403
|
-
}
|
|
1404
|
-
if (startOffset >= size) {
|
|
1405
|
-
return {
|
|
1406
|
-
lines: [],
|
|
1407
|
-
entry: { offset: size, size, inode },
|
|
1408
|
-
rotated,
|
|
1409
|
-
truncated
|
|
1410
|
-
};
|
|
1411
|
-
}
|
|
1412
|
-
const fd = openSync(filepath, "r");
|
|
1413
|
-
try {
|
|
1414
|
-
const bufSize = Math.max(0, size - startOffset);
|
|
1415
|
-
const buf = Buffer.alloc(bufSize);
|
|
1416
|
-
if (bufSize > 0) {
|
|
1417
|
-
readSync(fd, buf, 0, bufSize, startOffset);
|
|
1418
|
-
}
|
|
1419
|
-
const text = buf.toString("utf-8");
|
|
1420
|
-
const lines = text.length > 0 ? text.split("\n") : [];
|
|
1421
|
-
let newOffset = size;
|
|
1422
|
-
if (lines.length > 0 && !text.endsWith("\n")) {
|
|
1423
|
-
const partial = lines.pop();
|
|
1424
|
-
newOffset = size - Buffer.byteLength(partial, "utf-8");
|
|
1425
|
-
} else if (lines.length > 0 && text.endsWith("\n")) {
|
|
1426
|
-
if (lines[lines.length - 1] === "") lines.pop();
|
|
1427
|
-
}
|
|
1428
|
-
return {
|
|
1429
|
-
lines,
|
|
1430
|
-
entry: { offset: newOffset, size, inode },
|
|
1431
|
-
rotated,
|
|
1432
|
-
truncated
|
|
1433
|
-
};
|
|
1434
|
-
} finally {
|
|
1435
|
-
closeSync(fd);
|
|
1436
|
-
}
|
|
1437
|
-
}
|
|
1438
|
-
function countRecentWrites(dir, sinceMs) {
|
|
1439
|
-
if (!existsSync3(dir)) return 0;
|
|
1440
|
-
let count = 0;
|
|
1441
|
-
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
1442
|
-
if (!entry.isFile()) continue;
|
|
1443
|
-
try {
|
|
1444
|
-
const st = statSync3(join2(dir, entry.name));
|
|
1445
|
-
if (st.mtimeMs >= sinceMs) count += 1;
|
|
1446
|
-
} catch {
|
|
1447
|
-
}
|
|
1448
|
-
}
|
|
1449
|
-
return count;
|
|
1450
|
-
}
|
|
1451
|
-
function fileLastWriteMs(path2) {
|
|
1452
|
-
if (!existsSync3(path2)) return null;
|
|
1453
|
-
try {
|
|
1454
|
-
return statSync3(path2).mtimeMs;
|
|
1455
|
-
} catch {
|
|
1456
|
-
return null;
|
|
1457
|
-
}
|
|
1458
|
-
}
|
|
1459
|
-
function accountLogDir(accountDir) {
|
|
1460
|
-
return resolve2(accountDir, "logs");
|
|
1461
|
-
}
|
|
1462
|
-
function sourceKey(file) {
|
|
1463
|
-
return `${file.logicalSource}:${basename(file.filepath)}`;
|
|
1464
|
-
}
|
|
1465
|
-
|
|
1466
|
-
// app/lib/review-detector/writer.ts
|
|
1467
|
-
import { appendFileSync, existsSync as existsSync4, mkdirSync as mkdirSync3, readFileSync as readFileSync3, writeFileSync as writeFileSync3, renameSync as renameSync3, statSync as statSync4 } from "fs";
|
|
1468
|
-
import { resolve as resolve3, dirname as dirname3 } from "path";
|
|
1469
|
-
import { randomUUID } from "crypto";
|
|
1470
|
-
function reviewLogPath(configDir2) {
|
|
1471
|
-
return resolve3(configDir2, "logs", "review.log");
|
|
1472
|
-
}
|
|
1473
|
-
function pendingAlertsPath(configDir2) {
|
|
1474
|
-
return resolve3(configDir2, "review-pending-alerts.jsonl");
|
|
1475
|
-
}
|
|
1476
|
-
function reviewLog(configDir2, event) {
|
|
1477
|
-
const path2 = reviewLogPath(configDir2);
|
|
1478
|
-
try {
|
|
1479
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1480
|
-
const line = `${new Date(
|
|
1481
|
-
typeof event.ts === "number" ? event.ts : Date.now()
|
|
1482
|
-
).toISOString()} [review] ${JSON.stringify(event)}
|
|
1483
|
-
`;
|
|
1484
|
-
appendFileSync(path2, line, "utf-8");
|
|
1485
|
-
} catch (err) {
|
|
1486
|
-
console.error(`[review] failed to write review log at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1487
|
-
}
|
|
1488
|
-
}
|
|
1489
|
-
async function ensureReviewAlertIndex() {
|
|
1490
|
-
const session = getSession();
|
|
1491
|
-
try {
|
|
1492
|
-
await session.run(
|
|
1493
|
-
`CREATE INDEX review_alert_lookup IF NOT EXISTS
|
|
1494
|
-
FOR (a:ReviewAlert)
|
|
1495
|
-
ON (a.accountId, a.resolvedAt, a.lastMatchAt)`
|
|
1496
|
-
);
|
|
1497
|
-
} finally {
|
|
1498
|
-
await session.close();
|
|
1499
|
-
}
|
|
1500
|
-
}
|
|
1501
|
-
async function ensureReviewDigestSchedule(accountId) {
|
|
1502
|
-
const eventId = `review-digest-${accountId}`;
|
|
1503
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1504
|
-
const next = /* @__PURE__ */ new Date();
|
|
1505
|
-
if (next.getHours() >= 8) {
|
|
1506
|
-
next.setDate(next.getDate() + 1);
|
|
1507
|
-
}
|
|
1508
|
-
next.setHours(8, 0, 0, 0);
|
|
1509
|
-
const nextRun = next.toISOString();
|
|
1510
|
-
const session = getSession();
|
|
1511
|
-
try {
|
|
1512
|
-
await session.run(
|
|
1513
|
-
`MERGE (e:Event { eventId: $eventId })
|
|
1514
|
-
ON CREATE SET
|
|
1515
|
-
e.accountId = $accountId,
|
|
1516
|
-
e.name = 'Daily review digest',
|
|
1517
|
-
e.description = 'Task 385 \u2014 composes the review cadence digest from the last 24 hours of review.log and active ReviewAlert records. Scheduled via check-due-events.',
|
|
1518
|
-
e.startDate = $now,
|
|
1519
|
-
e.eventStatus = 'scheduled',
|
|
1520
|
-
e.recurrence = '0 8 * * *',
|
|
1521
|
-
e.nextRun = datetime($nextRun),
|
|
1522
|
-
e.sourcePlugin = 'admin',
|
|
1523
|
-
e.actionPlugin = 'admin',
|
|
1524
|
-
e.actionTool = 'review-digest-compose',
|
|
1525
|
-
e.actionArgs = '{}',
|
|
1526
|
-
e.createdAt = $now,
|
|
1527
|
-
e.updatedAt = $now
|
|
1528
|
-
ON MATCH SET
|
|
1529
|
-
e.actionPlugin = 'admin',
|
|
1530
|
-
e.actionTool = 'review-digest-compose',
|
|
1531
|
-
e.updatedAt = $now`,
|
|
1532
|
-
{ eventId, accountId, now, nextRun }
|
|
1533
|
-
);
|
|
1534
|
-
} finally {
|
|
1535
|
-
await session.close();
|
|
1536
|
-
}
|
|
1537
|
-
}
|
|
1538
|
-
async function countActiveReviewAlerts(accountId) {
|
|
1539
|
-
const session = getSession();
|
|
1540
|
-
try {
|
|
1541
|
-
const result = await session.run(
|
|
1542
|
-
`MATCH (a:ReviewAlert {accountId: $accountId})
|
|
1543
|
-
WHERE a.resolvedAt IS NULL
|
|
1544
|
-
AND (a.suppressedUntil IS NULL OR a.suppressedUntil < datetime())
|
|
1545
|
-
RETURN count(a) AS n`,
|
|
1546
|
-
{ accountId }
|
|
1547
|
-
);
|
|
1548
|
-
const n = result.records[0]?.get("n");
|
|
1549
|
-
if (typeof n === "number") return n;
|
|
1550
|
-
if (n && typeof n.toNumber === "function") {
|
|
1551
|
-
return n.toNumber();
|
|
1552
|
-
}
|
|
1553
|
-
return 0;
|
|
1554
|
-
} finally {
|
|
1555
|
-
await session.close();
|
|
1556
|
-
}
|
|
1557
|
-
}
|
|
1558
|
-
async function upsertReviewAlert(accountId, match) {
|
|
1559
|
-
const session = getSession();
|
|
1560
|
-
try {
|
|
1561
|
-
await session.run(
|
|
1562
|
-
`MERGE (a:ReviewAlert { ruleId: $ruleId, accountId: $accountId })
|
|
1563
|
-
ON CREATE SET
|
|
1564
|
-
a.alertId = $alertId,
|
|
1565
|
-
a.ruleName = $ruleName,
|
|
1566
|
-
a.firstMatchAt = datetime($matchedAt),
|
|
1567
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1568
|
-
a.cumulativeMatchCount = 1,
|
|
1569
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1570
|
-
a.suggestedAction = $suggestedAction,
|
|
1571
|
-
a.suppressedUntil = null,
|
|
1572
|
-
a.resolvedAt = null
|
|
1573
|
-
ON MATCH SET
|
|
1574
|
-
a.lastMatchAt = datetime($matchedAt),
|
|
1575
|
-
a.cumulativeMatchCount = a.cumulativeMatchCount + 1,
|
|
1576
|
-
a.sampleEvidence = $sampleEvidence,
|
|
1577
|
-
a.suggestedAction = $suggestedAction,
|
|
1578
|
-
a.resolvedAt = null`,
|
|
1579
|
-
{
|
|
1580
|
-
ruleId: match.ruleId,
|
|
1581
|
-
accountId,
|
|
1582
|
-
alertId: randomUUID(),
|
|
1583
|
-
ruleName: match.ruleName,
|
|
1584
|
-
matchedAt: new Date(match.matchedAt).toISOString(),
|
|
1585
|
-
sampleEvidence: match.sampleEvidence,
|
|
1586
|
-
suggestedAction: match.suggestedAction
|
|
1587
|
-
}
|
|
1588
|
-
);
|
|
1589
|
-
} finally {
|
|
1590
|
-
await session.close();
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
function queueAlert(configDir2, accountId, match) {
|
|
1594
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1595
|
-
try {
|
|
1596
|
-
mkdirSync3(dirname3(path2), { recursive: true });
|
|
1597
|
-
const line = JSON.stringify({ accountId, match }) + "\n";
|
|
1598
|
-
appendFileSync(path2, line, "utf-8");
|
|
1599
|
-
} catch (err) {
|
|
1600
|
-
console.error(`[review] failed to queue alert at ${path2}: ${err instanceof Error ? err.message : String(err)}`);
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
async function drainPendingAlerts(configDir2) {
|
|
1604
|
-
const path2 = pendingAlertsPath(configDir2);
|
|
1605
|
-
if (!existsSync4(path2)) return { drained: 0, remaining: 0 };
|
|
1606
|
-
const raw = readFileSync3(path2, "utf-8");
|
|
1607
|
-
const lines = raw.split("\n").filter((l) => l.trim().length > 0);
|
|
1608
|
-
if (lines.length === 0) return { drained: 0, remaining: 0 };
|
|
1609
|
-
const remaining = [];
|
|
1610
|
-
let drained = 0;
|
|
1611
|
-
for (const line of lines) {
|
|
1612
|
-
let entry = null;
|
|
1613
|
-
try {
|
|
1614
|
-
entry = JSON.parse(line);
|
|
1615
|
-
} catch {
|
|
1616
|
-
continue;
|
|
1617
|
-
}
|
|
1618
|
-
try {
|
|
1619
|
-
await upsertReviewAlert(entry.accountId, entry.match);
|
|
1620
|
-
drained += 1;
|
|
1621
|
-
} catch {
|
|
1622
|
-
remaining.push(line);
|
|
1623
|
-
}
|
|
1624
|
-
}
|
|
1625
|
-
const tmp = `${path2}.tmp.${process.pid}.${Date.now()}`;
|
|
1626
|
-
if (remaining.length > 0) {
|
|
1627
|
-
writeFileSync3(tmp, remaining.join("\n") + "\n", "utf-8");
|
|
1628
|
-
} else {
|
|
1629
|
-
writeFileSync3(tmp, "", "utf-8");
|
|
1630
|
-
}
|
|
1631
|
-
renameSync3(tmp, path2);
|
|
1632
|
-
return { drained, remaining: remaining.length };
|
|
1633
|
-
}
|
|
1634
|
-
|
|
1635
|
-
// app/lib/review-detector/boot.ts
|
|
1636
|
-
async function bootDetector() {
|
|
1637
|
-
const account = resolveAccount();
|
|
1638
|
-
if (!account) {
|
|
1639
|
-
console.error("[review] boot: no account resolved \u2014 detector not starting (Phase 0 expects exactly one account)");
|
|
1640
|
-
return null;
|
|
1641
|
-
}
|
|
1642
|
-
const configDir2 = MAXY_DIR;
|
|
1643
|
-
const accountId = account.accountId;
|
|
1644
|
-
const accountDir = account.accountDir;
|
|
1645
|
-
const ensured = ensureRulesFile(configDir2);
|
|
1646
|
-
if (ensured.created) {
|
|
1647
|
-
reviewLog(configDir2, { event: "rules-defaults-created", path: ensured.path });
|
|
1648
|
-
}
|
|
1649
|
-
let rulesFile;
|
|
1650
|
-
try {
|
|
1651
|
-
rulesFile = loadRules(configDir2);
|
|
1652
|
-
} catch (err) {
|
|
1653
|
-
reviewLog(configDir2, {
|
|
1654
|
-
event: "boot-failed",
|
|
1655
|
-
reason: "rules-invalid",
|
|
1656
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1657
|
-
});
|
|
1658
|
-
console.error(`[review] boot: rules file invalid \u2014 ${err instanceof Error ? err.message : String(err)}`);
|
|
1659
|
-
return null;
|
|
1660
|
-
}
|
|
1661
|
-
if (addMissingDefaultRules(rulesFile)) {
|
|
1662
|
-
saveRules(configDir2, rulesFile);
|
|
1663
|
-
reviewLog(configDir2, { event: "rules-updated", update: "added-missing-defaults" });
|
|
1664
|
-
console.error("[review] boot: added missing default rules to existing rules file");
|
|
1665
|
-
}
|
|
1666
|
-
if (migrateRateLimitPattern(rulesFile)) {
|
|
1667
|
-
saveRules(configDir2, rulesFile);
|
|
1668
|
-
reviewLog(configDir2, { event: "rules-migrated", migration: "rate-limit-pattern-v2" });
|
|
1669
|
-
console.error("[review] boot: migrated http-rate-limit-429 pattern to v2 (Task 408)");
|
|
1670
|
-
}
|
|
1671
|
-
try {
|
|
1672
|
-
await ensureReviewAlertIndex();
|
|
1673
|
-
reviewLog(configDir2, { event: "neo4j-index-ensured" });
|
|
1674
|
-
} catch (err) {
|
|
1675
|
-
reviewLog(configDir2, {
|
|
1676
|
-
event: "neo4j-index-failed",
|
|
1677
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1678
|
-
});
|
|
1679
|
-
console.error(`[review] boot: ensureReviewAlertIndex failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1680
|
-
}
|
|
1681
|
-
try {
|
|
1682
|
-
await ensureReviewDigestSchedule(accountId);
|
|
1683
|
-
reviewLog(configDir2, { event: "digest-schedule-ensured" });
|
|
1684
|
-
} catch (err) {
|
|
1685
|
-
reviewLog(configDir2, {
|
|
1686
|
-
event: "digest-schedule-failed",
|
|
1687
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1688
|
-
});
|
|
1689
|
-
console.error(`[review] boot: ensureReviewDigestSchedule failed (continuing): ${err instanceof Error ? err.message : String(err)}`);
|
|
1690
|
-
}
|
|
1691
|
-
const tailState = loadTailState(configDir2);
|
|
1692
|
-
const logDir = accountLogDir(accountDir);
|
|
1693
|
-
const initialSources = discoverAllSources(configDir2, logDir);
|
|
1694
|
-
const snapshot = {
|
|
1695
|
-
state: "running",
|
|
1696
|
-
startedAt: Date.now(),
|
|
1697
|
-
lastScanAt: null,
|
|
1698
|
-
lastScanDurationMs: null,
|
|
1699
|
-
scanCycles: 0,
|
|
1700
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1701
|
-
sourcesTracked: initialSources.length,
|
|
1702
|
-
activeAlerts: 0,
|
|
1703
|
-
lastError: null
|
|
1704
|
-
};
|
|
1705
|
-
reviewLog(configDir2, {
|
|
1706
|
-
event: "detector-started",
|
|
1707
|
-
configDir: basename2(configDir2),
|
|
1708
|
-
accountId: accountId.slice(0, 8),
|
|
1709
|
-
rulesLoaded: rulesFile.rules.length,
|
|
1710
|
-
sourcesDiscovered: initialSources.length,
|
|
1711
|
-
tailStateEntries: Object.keys(tailState).length,
|
|
1712
|
-
scanIntervalMs: rulesFile.scanIntervalMs
|
|
1713
|
-
});
|
|
1714
|
-
const runtime = {
|
|
1715
|
-
configDir: configDir2,
|
|
1716
|
-
accountId,
|
|
1717
|
-
accountDir,
|
|
1718
|
-
rulesFile,
|
|
1719
|
-
rulesFileMtimeMs: rulesFileMtime(configDir2) ?? Date.now(),
|
|
1720
|
-
ruleState: /* @__PURE__ */ new Map(),
|
|
1721
|
-
tailState,
|
|
1722
|
-
snapshot,
|
|
1723
|
-
stopped: false
|
|
1724
|
-
};
|
|
1725
|
-
return runtime;
|
|
1726
|
-
}
|
|
1727
|
-
|
|
1728
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1729
|
-
import { resolve as resolve4 } from "path";
|
|
1730
|
-
|
|
1731
|
-
// app/lib/review-detector/evaluator.ts
|
|
1732
|
-
var SAMPLE_MAX_CHARS = 500;
|
|
1733
|
-
var CONV_ID_REGEX = /conversationId=([a-f0-9]{8})/;
|
|
1734
|
-
function scopeKeyFor(rule, line) {
|
|
1735
|
-
if (rule.scope !== "session") return "";
|
|
1736
|
-
const m = CONV_ID_REGEX.exec(line);
|
|
1737
|
-
return m ? m[1] : "";
|
|
1738
|
-
}
|
|
1739
|
-
var compiledRegexCache = /* @__PURE__ */ new Map();
|
|
1740
|
-
function compileRegex(pattern) {
|
|
1741
|
-
let re = compiledRegexCache.get(pattern);
|
|
1742
|
-
if (re === void 0) {
|
|
1743
|
-
re = new RegExp(pattern, "i");
|
|
1744
|
-
compiledRegexCache.set(pattern, re);
|
|
1745
|
-
}
|
|
1746
|
-
return re;
|
|
1747
|
-
}
|
|
1748
|
-
function isSuppressed(rule, nowMs) {
|
|
1749
|
-
if (!rule.suppressedUntil) return false;
|
|
1750
|
-
const until = Date.parse(rule.suppressedUntil);
|
|
1751
|
-
if (Number.isNaN(until)) return false;
|
|
1752
|
-
return nowMs < until;
|
|
1753
|
-
}
|
|
1754
|
-
function toSample(line) {
|
|
1755
|
-
if (line.length <= SAMPLE_MAX_CHARS) return line;
|
|
1756
|
-
return line.slice(0, SAMPLE_MAX_CHARS) + "\u2026";
|
|
1757
|
-
}
|
|
1758
|
-
function newRuleState() {
|
|
1759
|
-
return {
|
|
1760
|
-
matchTimestamps: [],
|
|
1761
|
-
matchTimestampsByScope: /* @__PURE__ */ new Map(),
|
|
1762
|
-
lastAlertAt: null,
|
|
1763
|
-
cumulativeSinceLastAlert: 0,
|
|
1764
|
-
lastSeenAt: null,
|
|
1765
|
-
pendingFollowups: []
|
|
1766
|
-
};
|
|
1767
|
-
}
|
|
1768
|
-
function evaluateTextRule(rule, lines, state, nowMs) {
|
|
1769
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1770
|
-
if (rule.pattern.length === 0) return { match: null, state };
|
|
1771
|
-
const regex = compileRegex(rule.pattern);
|
|
1772
|
-
const matchesByScope = /* @__PURE__ */ new Map();
|
|
1773
|
-
let firstSample = null;
|
|
1774
|
-
for (const line of lines) {
|
|
1775
|
-
if (!regex.test(line)) continue;
|
|
1776
|
-
if (!firstSample) firstSample = line;
|
|
1777
|
-
const key = scopeKeyFor(rule, line);
|
|
1778
|
-
const existing = matchesByScope.get(key) ?? [];
|
|
1779
|
-
existing.push(line);
|
|
1780
|
-
matchesByScope.set(key, existing);
|
|
1781
|
-
}
|
|
1782
|
-
if (matchesByScope.size === 0) return { match: null, state };
|
|
1783
|
-
const windowStart = rule.thresholdWindowMinutes > 0 ? nowMs - rule.thresholdWindowMinutes * 6e4 : -Infinity;
|
|
1784
|
-
const updated = {
|
|
1785
|
-
...state,
|
|
1786
|
-
matchTimestamps: [...state.matchTimestamps],
|
|
1787
|
-
matchTimestampsByScope: new Map(state.matchTimestampsByScope ?? [])
|
|
1788
|
-
};
|
|
1789
|
-
let firingSample = null;
|
|
1790
|
-
let fires = false;
|
|
1791
|
-
const isCountZero = rule.thresholdCount === 0;
|
|
1792
|
-
if (rule.scope === "session") {
|
|
1793
|
-
for (const [key, hits] of matchesByScope) {
|
|
1794
|
-
const prior = updated.matchTimestampsByScope.get(key) ?? [];
|
|
1795
|
-
const merged = [...prior, ...hits.map(() => nowMs)].filter((t) => t >= windowStart);
|
|
1796
|
-
if (merged.length === 0) {
|
|
1797
|
-
updated.matchTimestampsByScope.delete(key);
|
|
1798
|
-
} else {
|
|
1799
|
-
updated.matchTimestampsByScope.set(key, merged);
|
|
1800
|
-
}
|
|
1801
|
-
if (!fires && (isCountZero || merged.length >= rule.thresholdCount)) {
|
|
1802
|
-
fires = true;
|
|
1803
|
-
firingSample = hits[0];
|
|
1804
|
-
}
|
|
1805
|
-
}
|
|
1806
|
-
} else {
|
|
1807
|
-
for (const hits of matchesByScope.values()) {
|
|
1808
|
-
for (const _ of hits) updated.matchTimestamps.push(nowMs);
|
|
1809
|
-
}
|
|
1810
|
-
updated.matchTimestamps = updated.matchTimestamps.filter((t) => t >= windowStart);
|
|
1811
|
-
fires = isCountZero || updated.matchTimestamps.length >= rule.thresholdCount;
|
|
1812
|
-
if (fires) firingSample = firstSample;
|
|
1813
|
-
}
|
|
1814
|
-
if (!fires) {
|
|
1815
|
-
return { match: null, state: updated };
|
|
1816
|
-
}
|
|
1817
|
-
const match = {
|
|
1818
|
-
ruleId: rule.id,
|
|
1819
|
-
ruleName: rule.name,
|
|
1820
|
-
matchedAt: nowMs,
|
|
1821
|
-
sampleEvidence: toSample(firingSample ?? firstSample ?? lines[0] ?? ""),
|
|
1822
|
-
suggestedAction: rule.suggestedAction
|
|
1823
|
-
};
|
|
1824
|
-
return { match, state: updated };
|
|
1825
|
-
}
|
|
1826
|
-
function evaluateFileWriteStormRule(rule, recentWriteCount, state, nowMs) {
|
|
1827
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1828
|
-
if (recentWriteCount < rule.thresholdCount) {
|
|
1829
|
-
return { match: null, state };
|
|
1830
|
-
}
|
|
1831
|
-
const match = {
|
|
1832
|
-
ruleId: rule.id,
|
|
1833
|
-
ruleName: rule.name,
|
|
1834
|
-
matchedAt: nowMs,
|
|
1835
|
-
sampleEvidence: `${recentWriteCount} file writes in ${rule.thresholdWindowMinutes}m at ${rule.watchPath}`,
|
|
1836
|
-
suggestedAction: rule.suggestedAction
|
|
1837
|
-
};
|
|
1838
|
-
return { match, state };
|
|
1839
|
-
}
|
|
1840
|
-
function evaluateStaleLogRule(rule, lastMtimeMs, state, nowMs) {
|
|
1841
|
-
if (isSuppressed(rule, nowMs)) return { match: null, state };
|
|
1842
|
-
let lastSeenAt = state.lastSeenAt;
|
|
1843
|
-
if (lastMtimeMs !== null) {
|
|
1844
|
-
lastSeenAt = Math.max(lastSeenAt ?? 0, lastMtimeMs);
|
|
1845
|
-
}
|
|
1846
|
-
const updated = { ...state, lastSeenAt };
|
|
1847
|
-
if (lastMtimeMs === null || lastSeenAt === null) {
|
|
1848
|
-
return { match: null, state: updated };
|
|
1849
|
-
}
|
|
1850
|
-
const staleMs = (rule.staleHours ?? 24) * 60 * 60 * 1e3;
|
|
1851
|
-
if (nowMs - lastSeenAt < staleMs) {
|
|
1852
|
-
return { match: null, state: updated };
|
|
1853
|
-
}
|
|
1854
|
-
const match = {
|
|
1855
|
-
ruleId: rule.id,
|
|
1856
|
-
ruleName: rule.name,
|
|
1857
|
-
matchedAt: nowMs,
|
|
1858
|
-
sampleEvidence: `last write at ${new Date(lastSeenAt).toISOString()} (${rule.watchPath})`,
|
|
1859
|
-
suggestedAction: rule.suggestedAction
|
|
1860
|
-
};
|
|
1861
|
-
return { match, state: updated };
|
|
1862
|
-
}
|
|
1863
|
-
function evaluateAbsentFollowupRule(rule, lines, state, nowMs) {
|
|
1864
|
-
if (isSuppressed(rule, nowMs)) return { matches: [], state };
|
|
1865
|
-
if (!rule.pattern || !rule.followupPattern || !rule.followupWindowMs) {
|
|
1866
|
-
return { matches: [], state };
|
|
1867
|
-
}
|
|
1868
|
-
const triggerRegex = compileRegex(rule.pattern);
|
|
1869
|
-
const followupRegex = compileRegex(rule.followupPattern);
|
|
1870
|
-
const pending = [...state.pendingFollowups ?? []];
|
|
1871
|
-
for (const line of lines) {
|
|
1872
|
-
if (triggerRegex.test(line)) {
|
|
1873
|
-
pending.push({
|
|
1874
|
-
scope: scopeKeyFor(rule, line),
|
|
1875
|
-
timestamp: nowMs,
|
|
1876
|
-
line,
|
|
1877
|
-
fulfilled: false
|
|
1878
|
-
});
|
|
1879
|
-
continue;
|
|
1880
|
-
}
|
|
1881
|
-
if (followupRegex.test(line)) {
|
|
1882
|
-
const scope = scopeKeyFor(rule, line);
|
|
1883
|
-
for (const entry of pending) {
|
|
1884
|
-
if (!entry.fulfilled && entry.scope === scope) {
|
|
1885
|
-
entry.fulfilled = true;
|
|
1886
|
-
break;
|
|
1887
|
-
}
|
|
1888
|
-
}
|
|
1889
|
-
}
|
|
1890
|
-
}
|
|
1891
|
-
const matches = [];
|
|
1892
|
-
const kept = [];
|
|
1893
|
-
for (const entry of pending) {
|
|
1894
|
-
const age = nowMs - entry.timestamp;
|
|
1895
|
-
if (age >= rule.followupWindowMs) {
|
|
1896
|
-
if (!entry.fulfilled) {
|
|
1897
|
-
matches.push({
|
|
1898
|
-
ruleId: rule.id,
|
|
1899
|
-
ruleName: rule.name,
|
|
1900
|
-
matchedAt: nowMs,
|
|
1901
|
-
sampleEvidence: toSample(entry.line),
|
|
1902
|
-
suggestedAction: rule.suggestedAction,
|
|
1903
|
-
missedForMs: age
|
|
1904
|
-
});
|
|
1905
|
-
}
|
|
1906
|
-
continue;
|
|
1907
|
-
}
|
|
1908
|
-
kept.push(entry);
|
|
1909
|
-
}
|
|
1910
|
-
return {
|
|
1911
|
-
matches,
|
|
1912
|
-
state: { ...state, pendingFollowups: kept }
|
|
1913
|
-
};
|
|
1914
|
-
}
|
|
1915
|
-
var ALERT_WINDOW_MS = 60 * 60 * 1e3;
|
|
1916
|
-
function rateLimitDecision(state, nowMs) {
|
|
1917
|
-
const since = state.lastAlertAt === null ? Infinity : nowMs - state.lastAlertAt;
|
|
1918
|
-
if (since >= ALERT_WINDOW_MS) {
|
|
1919
|
-
return {
|
|
1920
|
-
surface: true,
|
|
1921
|
-
state: { ...state, lastAlertAt: nowMs, cumulativeSinceLastAlert: 0 }
|
|
1922
|
-
};
|
|
1923
|
-
}
|
|
1924
|
-
return {
|
|
1925
|
-
surface: false,
|
|
1926
|
-
state: { ...state, cumulativeSinceLastAlert: state.cumulativeSinceLastAlert + 1 }
|
|
1927
|
-
};
|
|
1928
|
-
}
|
|
1929
|
-
|
|
1930
|
-
// app/lib/review-detector/scan-loop.ts
|
|
1931
|
-
async function runScanCycle(runtime) {
|
|
1932
|
-
const cycleStart = Date.now();
|
|
1933
|
-
runtime.snapshot.scanCycles += 1;
|
|
1934
|
-
try {
|
|
1935
|
-
const currentMtime = rulesFileMtime(runtime.configDir);
|
|
1936
|
-
if (currentMtime !== null && currentMtime !== runtime.rulesFileMtimeMs) {
|
|
1937
|
-
try {
|
|
1938
|
-
runtime.rulesFile = loadRules(runtime.configDir);
|
|
1939
|
-
runtime.rulesFileMtimeMs = currentMtime;
|
|
1940
|
-
runtime.snapshot.rulesLoaded = runtime.rulesFile.rules.length;
|
|
1941
|
-
reviewLog(runtime.configDir, {
|
|
1942
|
-
event: "rules-reloaded",
|
|
1943
|
-
rulesLoaded: runtime.rulesFile.rules.length,
|
|
1944
|
-
mtime: new Date(currentMtime).toISOString()
|
|
1945
|
-
});
|
|
1946
|
-
} catch (err) {
|
|
1947
|
-
reviewLog(runtime.configDir, {
|
|
1948
|
-
event: "rules-reload-failed",
|
|
1949
|
-
error: err instanceof Error ? err.message : String(err)
|
|
1950
|
-
});
|
|
1951
|
-
runtime.snapshot.state = "degraded";
|
|
1952
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
1953
|
-
}
|
|
1954
|
-
}
|
|
1955
|
-
const logDir = accountLogDir(runtime.accountDir);
|
|
1956
|
-
const files = discoverAllSources(runtime.configDir, logDir);
|
|
1957
|
-
runtime.snapshot.sourcesTracked = files.length;
|
|
1958
|
-
const linesBySource = /* @__PURE__ */ new Map();
|
|
1959
|
-
for (const file of files) {
|
|
1960
|
-
const key = sourceKey(file);
|
|
1961
|
-
const prev = runtime.tailState[key];
|
|
1962
|
-
const result = readNewLines(file.filepath, prev);
|
|
1963
|
-
if (result === null) {
|
|
1964
|
-
if (prev) {
|
|
1965
|
-
reviewLog(runtime.configDir, {
|
|
1966
|
-
event: "source-vanished",
|
|
1967
|
-
source: file.logicalSource,
|
|
1968
|
-
filepath: file.filepath
|
|
1969
|
-
});
|
|
1970
|
-
delete runtime.tailState[key];
|
|
1971
|
-
}
|
|
1972
|
-
continue;
|
|
1973
|
-
}
|
|
1974
|
-
if (result.rotated) {
|
|
1975
|
-
reviewLog(runtime.configDir, {
|
|
1976
|
-
event: "source-rotated",
|
|
1977
|
-
source: file.logicalSource,
|
|
1978
|
-
filepath: file.filepath
|
|
1979
|
-
});
|
|
1980
|
-
}
|
|
1981
|
-
if (result.truncated) {
|
|
1982
|
-
reviewLog(runtime.configDir, {
|
|
1983
|
-
event: "source-truncated",
|
|
1984
|
-
source: file.logicalSource,
|
|
1985
|
-
filepath: file.filepath
|
|
1986
|
-
});
|
|
1987
|
-
}
|
|
1988
|
-
runtime.tailState[key] = result.entry;
|
|
1989
|
-
if (result.lines.length > 0) {
|
|
1990
|
-
const bucket = linesBySource.get(file.logicalSource) ?? [];
|
|
1991
|
-
bucket.push(...result.lines);
|
|
1992
|
-
linesBySource.set(file.logicalSource, bucket);
|
|
1993
|
-
}
|
|
1994
|
-
}
|
|
1995
|
-
const matches = [];
|
|
1996
|
-
for (const rule of runtime.rulesFile.rules) {
|
|
1997
|
-
let state = runtime.ruleState.get(rule.id) ?? newRuleState();
|
|
1998
|
-
if (isSuppressed(rule, cycleStart)) {
|
|
1999
|
-
reviewLog(runtime.configDir, {
|
|
2000
|
-
event: "rule-suppressed",
|
|
2001
|
-
ruleId: rule.id,
|
|
2002
|
-
suppressedUntil: rule.suppressedUntil
|
|
2003
|
-
});
|
|
2004
|
-
runtime.ruleState.set(rule.id, state);
|
|
2005
|
-
continue;
|
|
2006
|
-
}
|
|
2007
|
-
let match = null;
|
|
2008
|
-
if (rule.type === "reconnect-loop" || rule.type === "repeated-error" || rule.type === "silent-catch" || rule.type === "rate-limit") {
|
|
2009
|
-
let inputLines = [];
|
|
2010
|
-
if (rule.logSource === "any") {
|
|
2011
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2012
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2013
|
-
}
|
|
2014
|
-
} else {
|
|
2015
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2016
|
-
}
|
|
2017
|
-
if (inputLines.length > 0) {
|
|
2018
|
-
const result = evaluateTextRule(rule, inputLines, state, cycleStart);
|
|
2019
|
-
state = result.state;
|
|
2020
|
-
match = result.match;
|
|
2021
|
-
}
|
|
2022
|
-
} else if (rule.type === "file-write-storm") {
|
|
2023
|
-
const dir = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2024
|
-
const sinceMs = cycleStart - rule.thresholdWindowMinutes * 6e4;
|
|
2025
|
-
const count = countRecentWrites(dir, sinceMs);
|
|
2026
|
-
const result = evaluateFileWriteStormRule(rule, count, state, cycleStart);
|
|
2027
|
-
state = result.state;
|
|
2028
|
-
match = result.match;
|
|
2029
|
-
} else if (rule.type === "stale-log") {
|
|
2030
|
-
const trackedPath = resolve4(runtime.configDir, rule.watchPath ?? "");
|
|
2031
|
-
const lastMs = fileLastWriteMs(trackedPath);
|
|
2032
|
-
const result = evaluateStaleLogRule(rule, lastMs, state, cycleStart);
|
|
2033
|
-
state = result.state;
|
|
2034
|
-
match = result.match;
|
|
2035
|
-
} else if (rule.type === "absent-followup") {
|
|
2036
|
-
let inputLines = [];
|
|
2037
|
-
if (rule.logSource === "any") {
|
|
2038
|
-
for (const [src, lines] of linesBySource.entries()) {
|
|
2039
|
-
if (src !== "config-dir") inputLines.push(...lines);
|
|
2040
|
-
}
|
|
2041
|
-
} else {
|
|
2042
|
-
inputLines = linesBySource.get(rule.logSource) ?? [];
|
|
2043
|
-
}
|
|
2044
|
-
const result = evaluateAbsentFollowupRule(rule, inputLines, state, cycleStart);
|
|
2045
|
-
state = result.state;
|
|
2046
|
-
for (const m of result.matches) {
|
|
2047
|
-
const safeTrigger = m.sampleEvidence.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
2048
|
-
console.error(
|
|
2049
|
-
`[review-detector] absent-followup rule=${rule.id} trigger="${safeTrigger}" missed_for_ms=${m.missedForMs ?? ""}`
|
|
2050
|
-
);
|
|
2051
|
-
matches.push(m);
|
|
2052
|
-
}
|
|
2053
|
-
}
|
|
2054
|
-
runtime.ruleState.set(rule.id, state);
|
|
2055
|
-
if (match) matches.push(match);
|
|
2056
|
-
}
|
|
2057
|
-
for (const match of matches) {
|
|
2058
|
-
const state = runtime.ruleState.get(match.ruleId) ?? newRuleState();
|
|
2059
|
-
const decision = rateLimitDecision(state, cycleStart);
|
|
2060
|
-
runtime.ruleState.set(match.ruleId, decision.state);
|
|
2061
|
-
if (!decision.surface) {
|
|
2062
|
-
reviewLog(runtime.configDir, {
|
|
2063
|
-
event: "rate-limit-deferred",
|
|
2064
|
-
ruleId: match.ruleId,
|
|
2065
|
-
cumulativeSinceLastAlert: decision.state.cumulativeSinceLastAlert
|
|
2066
|
-
});
|
|
2067
|
-
continue;
|
|
2068
|
-
}
|
|
2069
|
-
reviewLog(runtime.configDir, {
|
|
2070
|
-
event: "match",
|
|
2071
|
-
ruleId: match.ruleId,
|
|
2072
|
-
ruleName: match.ruleName,
|
|
2073
|
-
sampleEvidence: match.sampleEvidence,
|
|
2074
|
-
suggestedAction: match.suggestedAction
|
|
2075
|
-
});
|
|
2076
|
-
try {
|
|
2077
|
-
await upsertReviewAlert(runtime.accountId, match);
|
|
2078
|
-
reviewLog(runtime.configDir, { event: "alert-persisted", ruleId: match.ruleId });
|
|
2079
|
-
} catch (err) {
|
|
2080
|
-
queueAlert(runtime.configDir, runtime.accountId, match);
|
|
2081
|
-
reviewLog(runtime.configDir, {
|
|
2082
|
-
event: "alert-queued",
|
|
2083
|
-
ruleId: match.ruleId,
|
|
2084
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2085
|
-
});
|
|
2086
|
-
}
|
|
2087
|
-
}
|
|
2088
|
-
try {
|
|
2089
|
-
const drain = await drainPendingAlerts(runtime.configDir);
|
|
2090
|
-
if (drain.drained > 0 || drain.remaining > 0) {
|
|
2091
|
-
reviewLog(runtime.configDir, {
|
|
2092
|
-
event: "queue-drain",
|
|
2093
|
-
drained: drain.drained,
|
|
2094
|
-
remaining: drain.remaining
|
|
2095
|
-
});
|
|
2096
|
-
}
|
|
2097
|
-
} catch (err) {
|
|
2098
|
-
reviewLog(runtime.configDir, {
|
|
2099
|
-
event: "queue-drain-failed",
|
|
2100
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2101
|
-
});
|
|
2102
|
-
}
|
|
2103
|
-
try {
|
|
2104
|
-
runtime.snapshot.activeAlerts = await countActiveReviewAlerts(runtime.accountId);
|
|
2105
|
-
} catch (err) {
|
|
2106
|
-
reviewLog(runtime.configDir, {
|
|
2107
|
-
event: "active-alerts-count-failed",
|
|
2108
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2109
|
-
});
|
|
2110
|
-
}
|
|
2111
|
-
saveTailState(runtime.configDir, runtime.tailState);
|
|
2112
|
-
const cycleDuration = Date.now() - cycleStart;
|
|
2113
|
-
runtime.snapshot.lastScanAt = cycleStart;
|
|
2114
|
-
runtime.snapshot.lastScanDurationMs = cycleDuration;
|
|
2115
|
-
if (runtime.snapshot.state !== "degraded" && runtime.snapshot.state !== "failed") {
|
|
2116
|
-
runtime.snapshot.state = "running";
|
|
2117
|
-
}
|
|
2118
|
-
runtime.snapshot.lastError = null;
|
|
2119
|
-
reviewLog(runtime.configDir, {
|
|
2120
|
-
event: "cycle",
|
|
2121
|
-
cycles: runtime.snapshot.scanCycles,
|
|
2122
|
-
sources: files.length,
|
|
2123
|
-
rules: runtime.rulesFile.rules.length,
|
|
2124
|
-
matches: matches.length,
|
|
2125
|
-
durationMs: cycleDuration
|
|
2126
|
-
});
|
|
2127
|
-
} catch (err) {
|
|
2128
|
-
runtime.snapshot.state = "degraded";
|
|
2129
|
-
runtime.snapshot.lastError = err instanceof Error ? err.message : String(err);
|
|
2130
|
-
reviewLog(runtime.configDir, {
|
|
2131
|
-
event: "cycle-failed",
|
|
2132
|
-
error: err instanceof Error ? err.message : String(err)
|
|
2133
|
-
});
|
|
2134
|
-
}
|
|
2135
|
-
}
|
|
2136
|
-
function startScanLoop(runtime) {
|
|
2137
|
-
let inFlight = null;
|
|
2138
|
-
const interval = setInterval(async () => {
|
|
2139
|
-
if (runtime.stopped) return;
|
|
2140
|
-
if (inFlight) return;
|
|
2141
|
-
inFlight = runScanCycle(runtime);
|
|
2142
|
-
try {
|
|
2143
|
-
await inFlight;
|
|
2144
|
-
} finally {
|
|
2145
|
-
inFlight = null;
|
|
2146
|
-
}
|
|
2147
|
-
}, runtime.rulesFile.scanIntervalMs);
|
|
2148
|
-
inFlight = runScanCycle(runtime);
|
|
2149
|
-
return async () => {
|
|
2150
|
-
runtime.stopped = true;
|
|
2151
|
-
clearInterval(interval);
|
|
2152
|
-
if (inFlight) {
|
|
2153
|
-
try {
|
|
2154
|
-
await inFlight;
|
|
2155
|
-
} catch {
|
|
2156
|
-
}
|
|
2157
|
-
}
|
|
2158
|
-
runtime.snapshot.state = "stopped";
|
|
2159
|
-
reviewLog(runtime.configDir, { event: "detector-stopped", cycles: runtime.snapshot.scanCycles });
|
|
2160
|
-
};
|
|
2161
|
-
}
|
|
2162
|
-
|
|
2163
|
-
// app/lib/review-detector/index.ts
|
|
2164
|
-
var activeRuntime = null;
|
|
2165
|
-
var stopFn = null;
|
|
2166
|
-
async function startReviewDetector() {
|
|
2167
|
-
if (stopFn) {
|
|
2168
|
-
console.error("[review] startReviewDetector called twice \u2014 ignoring second call");
|
|
2169
|
-
return;
|
|
2170
|
-
}
|
|
2171
|
-
try {
|
|
2172
|
-
activeRuntime = await bootDetector();
|
|
2173
|
-
if (!activeRuntime) return;
|
|
2174
|
-
stopFn = startScanLoop(activeRuntime);
|
|
2175
|
-
} catch (err) {
|
|
2176
|
-
console.error(`[review] detector start failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2177
|
-
}
|
|
2178
|
-
}
|
|
2179
|
-
async function shutdownReviewDetector() {
|
|
2180
|
-
if (!stopFn) return;
|
|
2181
|
-
try {
|
|
2182
|
-
await stopFn();
|
|
2183
|
-
} catch (err) {
|
|
2184
|
-
console.error(`[review] detector shutdown error: ${err instanceof Error ? err.message : String(err)}`);
|
|
2185
|
-
}
|
|
2186
|
-
stopFn = null;
|
|
2187
|
-
activeRuntime = null;
|
|
2188
|
-
}
|
|
2189
|
-
function getReviewDetectorSnapshot() {
|
|
2190
|
-
if (!activeRuntime) {
|
|
2191
|
-
return {
|
|
2192
|
-
state: "stopped",
|
|
2193
|
-
startedAt: null,
|
|
2194
|
-
lastScanAt: null,
|
|
2195
|
-
lastScanDurationMs: null,
|
|
2196
|
-
scanCycles: 0,
|
|
2197
|
-
rulesLoaded: 0,
|
|
2198
|
-
sourcesTracked: 0,
|
|
2199
|
-
activeAlerts: 0,
|
|
2200
|
-
lastError: null
|
|
2201
|
-
};
|
|
2202
|
-
}
|
|
2203
|
-
return activeRuntime.snapshot;
|
|
2204
|
-
}
|
|
2205
|
-
|
|
2206
867
|
// app/lib/whatsapp/schema.ts
|
|
2207
868
|
import { z } from "zod";
|
|
2208
869
|
var DmPolicySchema = z.enum(["open", "allowlist", "disabled"]);
|
|
@@ -2267,20 +928,20 @@ var WhatsAppConfigSchema = z.object({
|
|
|
2267
928
|
});
|
|
2268
929
|
|
|
2269
930
|
// app/lib/whatsapp/config-persist.ts
|
|
2270
|
-
import { readFileSync
|
|
2271
|
-
import { resolve
|
|
931
|
+
import { readFileSync, writeFileSync, existsSync as existsSync2 } from "fs";
|
|
932
|
+
import { resolve, join as join2 } from "path";
|
|
2272
933
|
var TAG = "[whatsapp:config]";
|
|
2273
934
|
function configPath(accountDir) {
|
|
2274
|
-
return
|
|
935
|
+
return resolve(accountDir, "account.json");
|
|
2275
936
|
}
|
|
2276
937
|
function readConfig(accountDir) {
|
|
2277
938
|
const path2 = configPath(accountDir);
|
|
2278
|
-
if (!
|
|
2279
|
-
return JSON.parse(
|
|
939
|
+
if (!existsSync2(path2)) throw new Error(`account.json not found at ${path2}`);
|
|
940
|
+
return JSON.parse(readFileSync(path2, "utf-8"));
|
|
2280
941
|
}
|
|
2281
942
|
function writeConfig(accountDir, config) {
|
|
2282
943
|
const path2 = configPath(accountDir);
|
|
2283
|
-
|
|
944
|
+
writeFileSync(path2, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
2284
945
|
}
|
|
2285
946
|
function reloadManagerConfig(accountDir) {
|
|
2286
947
|
try {
|
|
@@ -2450,8 +1111,8 @@ function setPublicAgent(accountDir, slug) {
|
|
|
2450
1111
|
if (!trimmed) {
|
|
2451
1112
|
return { ok: false, error: "Agent slug cannot be empty." };
|
|
2452
1113
|
}
|
|
2453
|
-
const agentConfigPath =
|
|
2454
|
-
if (!
|
|
1114
|
+
const agentConfigPath = join2(accountDir, "agents", trimmed, "config.json");
|
|
1115
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2455
1116
|
return { ok: false, error: `Agent "${trimmed}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2456
1117
|
}
|
|
2457
1118
|
try {
|
|
@@ -2514,8 +1175,8 @@ function setGroupPublicAgent(accountDir, accountId, groupJid, slug) {
|
|
|
2514
1175
|
if (!trimmedSlug) return { ok: false, error: "Agent slug cannot be empty." };
|
|
2515
1176
|
if (!trimmedGroup) return { ok: false, error: "Group JID cannot be empty." };
|
|
2516
1177
|
if (!trimmedAccount) return { ok: false, error: "Account ID cannot be empty." };
|
|
2517
|
-
const agentConfigPath =
|
|
2518
|
-
if (!
|
|
1178
|
+
const agentConfigPath = join2(accountDir, "agents", trimmedSlug, "config.json");
|
|
1179
|
+
if (!existsSync2(agentConfigPath)) {
|
|
2519
1180
|
return { ok: false, error: `Agent "${trimmedSlug}" not found \u2014 no config.json at ${agentConfigPath}. Check the agent slug and try again.` };
|
|
2520
1181
|
}
|
|
2521
1182
|
try {
|
|
@@ -2744,7 +1405,7 @@ function listCredentialAccountIds(configDir2) {
|
|
|
2744
1405
|
}
|
|
2745
1406
|
|
|
2746
1407
|
// app/lib/whatsapp/session.ts
|
|
2747
|
-
import { randomUUID
|
|
1408
|
+
import { randomUUID } from "crypto";
|
|
2748
1409
|
import fsSync2 from "fs";
|
|
2749
1410
|
import fs2 from "fs/promises";
|
|
2750
1411
|
import { inspect } from "util";
|
|
@@ -2819,7 +1480,7 @@ var credsSaveQueue = Promise.resolve();
|
|
|
2819
1480
|
async function drainCredsSaveQueue(timeoutMs = 5e3) {
|
|
2820
1481
|
console.error(`${TAG3} draining credential save queue\u2026`);
|
|
2821
1482
|
const timer2 = new Promise(
|
|
2822
|
-
(
|
|
1483
|
+
(resolve22) => setTimeout(() => resolve22("timeout"), timeoutMs)
|
|
2823
1484
|
);
|
|
2824
1485
|
const result = await Promise.race([
|
|
2825
1486
|
credsSaveQueue.then(() => "drained"),
|
|
@@ -2947,11 +1608,11 @@ async function createWaSocket(opts) {
|
|
|
2947
1608
|
return sock;
|
|
2948
1609
|
}
|
|
2949
1610
|
async function waitForConnection(sock) {
|
|
2950
|
-
return new Promise((
|
|
1611
|
+
return new Promise((resolve22, reject) => {
|
|
2951
1612
|
const handler = (update) => {
|
|
2952
1613
|
if (update.connection === "open") {
|
|
2953
1614
|
sock.ev.off("connection.update", handler);
|
|
2954
|
-
|
|
1615
|
+
resolve22();
|
|
2955
1616
|
}
|
|
2956
1617
|
if (update.connection === "close") {
|
|
2957
1618
|
sock.ev.off("connection.update", handler);
|
|
@@ -3065,14 +1726,14 @@ ${inspected}`;
|
|
|
3065
1726
|
return inspect2(err, INSPECT_OPTS2);
|
|
3066
1727
|
}
|
|
3067
1728
|
function withTimeout(label, promise, timeoutMs) {
|
|
3068
|
-
return new Promise((
|
|
1729
|
+
return new Promise((resolve22, reject) => {
|
|
3069
1730
|
const timer2 = setTimeout(() => {
|
|
3070
1731
|
reject(new Error(`${label} timed out after ${timeoutMs}ms`));
|
|
3071
1732
|
}, timeoutMs);
|
|
3072
1733
|
promise.then(
|
|
3073
1734
|
(value) => {
|
|
3074
1735
|
clearTimeout(timer2);
|
|
3075
|
-
|
|
1736
|
+
resolve22(value);
|
|
3076
1737
|
},
|
|
3077
1738
|
(err) => {
|
|
3078
1739
|
clearTimeout(timer2);
|
|
@@ -3607,8 +2268,8 @@ async function persistWhatsAppMessage(input) {
|
|
|
3607
2268
|
const { givenName, familyName } = splitName(input.pushName);
|
|
3608
2269
|
const prev = sessionWriteLocks.get(input.sessionKey);
|
|
3609
2270
|
let release;
|
|
3610
|
-
const mine = new Promise((
|
|
3611
|
-
release =
|
|
2271
|
+
const mine = new Promise((resolve22) => {
|
|
2272
|
+
release = resolve22;
|
|
3612
2273
|
});
|
|
3613
2274
|
const chained = (prev ?? Promise.resolve()).then(() => mine);
|
|
3614
2275
|
sessionWriteLocks.set(input.sessionKey, chained);
|
|
@@ -3793,8 +2454,8 @@ async function ensureWhatsAppConversation(input) {
|
|
|
3793
2454
|
}
|
|
3794
2455
|
|
|
3795
2456
|
// app/lib/whatsapp/platform-account-id.ts
|
|
3796
|
-
import { readdirSync
|
|
3797
|
-
import { resolve as
|
|
2457
|
+
import { readdirSync, readFileSync as readFileSync2 } from "fs";
|
|
2458
|
+
import { resolve as resolve2 } from "path";
|
|
3798
2459
|
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
3799
2460
|
var cached = null;
|
|
3800
2461
|
var cachedAccountsDir = null;
|
|
@@ -3818,7 +2479,7 @@ function resolvePlatformAccountId(accountsDir = ACCOUNTS_DIR) {
|
|
|
3818
2479
|
function enumerateValidAccountIds(accountsDir) {
|
|
3819
2480
|
let names;
|
|
3820
2481
|
try {
|
|
3821
|
-
names =
|
|
2482
|
+
names = readdirSync(accountsDir);
|
|
3822
2483
|
} catch (err) {
|
|
3823
2484
|
if (err.code === "ENOENT") return [];
|
|
3824
2485
|
throw err;
|
|
@@ -3826,9 +2487,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3826
2487
|
const valid = [];
|
|
3827
2488
|
for (const name of names) {
|
|
3828
2489
|
if (!UUID_RE.test(name)) continue;
|
|
3829
|
-
const configPath2 =
|
|
2490
|
+
const configPath2 = resolve2(accountsDir, name, "account.json");
|
|
3830
2491
|
try {
|
|
3831
|
-
JSON.parse(
|
|
2492
|
+
JSON.parse(readFileSync2(configPath2, "utf-8"));
|
|
3832
2493
|
valid.push(name);
|
|
3833
2494
|
} catch (err) {
|
|
3834
2495
|
const code = err.code;
|
|
@@ -3839,9 +2500,9 @@ function enumerateValidAccountIds(accountsDir) {
|
|
|
3839
2500
|
}
|
|
3840
2501
|
|
|
3841
2502
|
// app/lib/whatsapp/inbound/media.ts
|
|
3842
|
-
import { randomUUID as
|
|
2503
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
3843
2504
|
import { writeFile, mkdir } from "fs/promises";
|
|
3844
|
-
import { join as
|
|
2505
|
+
import { join as join3 } from "path";
|
|
3845
2506
|
import {
|
|
3846
2507
|
downloadMediaMessage,
|
|
3847
2508
|
downloadContentFromMessage,
|
|
@@ -3926,8 +2587,8 @@ async function downloadInboundMedia(msg, sock, opts) {
|
|
|
3926
2587
|
}
|
|
3927
2588
|
await mkdir(MEDIA_DIR, { recursive: true });
|
|
3928
2589
|
const ext = mimeToExt(mimetype ?? "application/octet-stream");
|
|
3929
|
-
const filename = `${
|
|
3930
|
-
const filePath =
|
|
2590
|
+
const filename = `${randomUUID2()}.${ext}`;
|
|
2591
|
+
const filePath = join3(MEDIA_DIR, filename);
|
|
3931
2592
|
await writeFile(filePath, buffer);
|
|
3932
2593
|
const sizeKB = (buffer.length / 1024).toFixed(0);
|
|
3933
2594
|
console.error(`${TAG9} media downloaded type=${mimetype ?? "unknown"} size=${sizeKB}KB path=${filePath}`);
|
|
@@ -4615,11 +3276,11 @@ async function connectWithReconnect(conn) {
|
|
|
4615
3276
|
console.error(
|
|
4616
3277
|
`${TAG13} reconnecting account=${conn.accountId} in ${delay}ms (attempt ${decision.nextAttempts}/${maxAttempts})`
|
|
4617
3278
|
);
|
|
4618
|
-
await new Promise((
|
|
4619
|
-
const timer2 = setTimeout(
|
|
3279
|
+
await new Promise((resolve22) => {
|
|
3280
|
+
const timer2 = setTimeout(resolve22, delay);
|
|
4620
3281
|
conn.abortController.signal.addEventListener("abort", () => {
|
|
4621
3282
|
clearTimeout(timer2);
|
|
4622
|
-
|
|
3283
|
+
resolve22();
|
|
4623
3284
|
}, { once: true });
|
|
4624
3285
|
});
|
|
4625
3286
|
}
|
|
@@ -4627,16 +3288,16 @@ async function connectWithReconnect(conn) {
|
|
|
4627
3288
|
}
|
|
4628
3289
|
}
|
|
4629
3290
|
function waitForDisconnectEvent(conn) {
|
|
4630
|
-
return new Promise((
|
|
3291
|
+
return new Promise((resolve22) => {
|
|
4631
3292
|
if (!conn.sock) {
|
|
4632
|
-
|
|
3293
|
+
resolve22();
|
|
4633
3294
|
return;
|
|
4634
3295
|
}
|
|
4635
3296
|
const sock = conn.sock;
|
|
4636
3297
|
const handler = (update) => {
|
|
4637
3298
|
if (update.connection === "close") {
|
|
4638
3299
|
sock.ev.off("connection.update", handler);
|
|
4639
|
-
|
|
3300
|
+
resolve22();
|
|
4640
3301
|
}
|
|
4641
3302
|
};
|
|
4642
3303
|
sock.ev.on("connection.update", handler);
|
|
@@ -4897,8 +3558,8 @@ async function handleInboundMessage(conn, msg) {
|
|
|
4897
3558
|
const conversationKey = isGroup ? remoteJid : senderPhone;
|
|
4898
3559
|
const debounceKey = `${conn.accountId}:${conversationKey}:${senderPhone}`;
|
|
4899
3560
|
let resolvePending;
|
|
4900
|
-
const sttPending = new Promise((
|
|
4901
|
-
resolvePending =
|
|
3561
|
+
const sttPending = new Promise((resolve22) => {
|
|
3562
|
+
resolvePending = resolve22;
|
|
4902
3563
|
});
|
|
4903
3564
|
if (conn.debouncer) conn.debouncer.registerPending(debounceKey, sttPending);
|
|
4904
3565
|
try {
|
|
@@ -5019,20 +3680,20 @@ async function probeApiKey() {
|
|
|
5019
3680
|
return result.status;
|
|
5020
3681
|
}
|
|
5021
3682
|
function checkPort(port2, timeoutMs = 500) {
|
|
5022
|
-
return new Promise((
|
|
3683
|
+
return new Promise((resolve22) => {
|
|
5023
3684
|
const socket = createConnection(port2, "127.0.0.1");
|
|
5024
3685
|
socket.setTimeout(timeoutMs);
|
|
5025
3686
|
socket.once("connect", () => {
|
|
5026
3687
|
socket.destroy();
|
|
5027
|
-
|
|
3688
|
+
resolve22(true);
|
|
5028
3689
|
});
|
|
5029
3690
|
socket.once("error", () => {
|
|
5030
3691
|
socket.destroy();
|
|
5031
|
-
|
|
3692
|
+
resolve22(false);
|
|
5032
3693
|
});
|
|
5033
3694
|
socket.once("timeout", () => {
|
|
5034
3695
|
socket.destroy();
|
|
5035
|
-
|
|
3696
|
+
resolve22(false);
|
|
5036
3697
|
});
|
|
5037
3698
|
});
|
|
5038
3699
|
}
|
|
@@ -5041,8 +3702,8 @@ app.get("/", async (c) => {
|
|
|
5041
3702
|
const browserTransport = resolveBrowserTransport(c.req.raw, c.env?.incoming?.socket?.remoteAddress);
|
|
5042
3703
|
let pinConfigured = false;
|
|
5043
3704
|
try {
|
|
5044
|
-
if (
|
|
5045
|
-
const raw =
|
|
3705
|
+
if (existsSync3(USERS_FILE)) {
|
|
3706
|
+
const raw = readFileSync3(USERS_FILE, "utf-8").trim();
|
|
5046
3707
|
if (raw) {
|
|
5047
3708
|
const users = JSON.parse(raw);
|
|
5048
3709
|
pinConfigured = Array.isArray(users) && users.length > 0;
|
|
@@ -5061,7 +3722,7 @@ app.get("/", async (c) => {
|
|
|
5061
3722
|
const vncRunning = await checkPort(6080);
|
|
5062
3723
|
let apiKeyConfigured = false;
|
|
5063
3724
|
try {
|
|
5064
|
-
apiKeyConfigured =
|
|
3725
|
+
apiKeyConfigured = existsSync3(keyFilePath());
|
|
5065
3726
|
} catch {
|
|
5066
3727
|
}
|
|
5067
3728
|
let apiKeyStatus = "missing";
|
|
@@ -5094,7 +3755,6 @@ app.get("/", async (c) => {
|
|
|
5094
3755
|
const step = await loadOnboardingStep(account.accountId);
|
|
5095
3756
|
if (step !== null) onboardingComplete = step >= 6;
|
|
5096
3757
|
}
|
|
5097
|
-
const reviewDetector = getReviewDetectorSnapshot();
|
|
5098
3758
|
return c.json({
|
|
5099
3759
|
pin_configured: pinConfigured,
|
|
5100
3760
|
claude_authenticated: claudeAuthenticated,
|
|
@@ -5110,31 +3770,20 @@ app.get("/", async (c) => {
|
|
|
5110
3770
|
any_connected: whatsappAnyConnected,
|
|
5111
3771
|
any_stuck: whatsappAnyStuck,
|
|
5112
3772
|
accounts: whatsappAccounts
|
|
5113
|
-
},
|
|
5114
|
-
review_detector: {
|
|
5115
|
-
state: reviewDetector.state,
|
|
5116
|
-
started_at: reviewDetector.startedAt,
|
|
5117
|
-
last_scan_at: reviewDetector.lastScanAt,
|
|
5118
|
-
last_scan_duration_ms: reviewDetector.lastScanDurationMs,
|
|
5119
|
-
scan_cycles: reviewDetector.scanCycles,
|
|
5120
|
-
rules_loaded: reviewDetector.rulesLoaded,
|
|
5121
|
-
sources_tracked: reviewDetector.sourcesTracked,
|
|
5122
|
-
active_alerts: reviewDetector.activeAlerts,
|
|
5123
|
-
last_error: reviewDetector.lastError
|
|
5124
3773
|
}
|
|
5125
3774
|
});
|
|
5126
3775
|
});
|
|
5127
3776
|
var health_default = app;
|
|
5128
3777
|
|
|
5129
3778
|
// server/routes/session.ts
|
|
5130
|
-
import { resolve as
|
|
5131
|
-
import { existsSync as
|
|
3779
|
+
import { resolve as resolve3 } from "path";
|
|
3780
|
+
import { existsSync as existsSync4, writeFileSync as writeFileSync2, mkdirSync } from "fs";
|
|
5132
3781
|
var UUID_RE2 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
5133
3782
|
function writeBrandingCache(accountId, agentSlug, branding) {
|
|
5134
3783
|
try {
|
|
5135
|
-
const cacheDir =
|
|
5136
|
-
|
|
5137
|
-
|
|
3784
|
+
const cacheDir = resolve3(MAXY_DIR, "branding-cache", accountId);
|
|
3785
|
+
mkdirSync(cacheDir, { recursive: true });
|
|
3786
|
+
writeFileSync2(resolve3(cacheDir, `${agentSlug}.json`), JSON.stringify(branding), "utf-8");
|
|
5138
3787
|
} catch (err) {
|
|
5139
3788
|
console.error(`[branding] cache write failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
5140
3789
|
}
|
|
@@ -5204,9 +3853,9 @@ app2.post("/", async (c) => {
|
|
|
5204
3853
|
}
|
|
5205
3854
|
let agentConfig = null;
|
|
5206
3855
|
if (account) {
|
|
5207
|
-
const agentDir =
|
|
5208
|
-
const agentConfigPath =
|
|
5209
|
-
if (!
|
|
3856
|
+
const agentDir = resolve3(account.accountDir, "agents", agentSlug);
|
|
3857
|
+
const agentConfigPath = resolve3(agentDir, "config.json");
|
|
3858
|
+
if (!existsSync4(agentDir) || !existsSync4(agentConfigPath)) {
|
|
5210
3859
|
return c.json({ error: "Agent not found" }, 404);
|
|
5211
3860
|
}
|
|
5212
3861
|
agentConfig = resolveAgentConfig(account.accountDir, agentSlug);
|
|
@@ -5456,12 +4105,12 @@ ${raw}`;
|
|
|
5456
4105
|
}
|
|
5457
4106
|
|
|
5458
4107
|
// app/lib/attachments.ts
|
|
5459
|
-
import { randomUUID as
|
|
4108
|
+
import { randomUUID as randomUUID3 } from "crypto";
|
|
5460
4109
|
import { mkdir as mkdir2, readFile, stat as stat2, writeFile as writeFile2 } from "fs/promises";
|
|
5461
4110
|
import { realpathSync } from "fs";
|
|
5462
|
-
import { resolve as
|
|
5463
|
-
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
5464
|
-
var ATTACHMENTS_ROOT =
|
|
4111
|
+
import { resolve as resolve4, extname, basename } from "path";
|
|
4112
|
+
var PLATFORM_ROOT2 = process.env.MAXY_PLATFORM_ROOT ?? resolve4(process.cwd(), "../platform");
|
|
4113
|
+
var ATTACHMENTS_ROOT = resolve4(PLATFORM_ROOT2, "..", "data/uploads");
|
|
5465
4114
|
var SUPPORTED_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5466
4115
|
"image/jpeg",
|
|
5467
4116
|
"image/png",
|
|
@@ -5487,12 +4136,12 @@ function assertSupportedMime(mimeType) {
|
|
|
5487
4136
|
}
|
|
5488
4137
|
}
|
|
5489
4138
|
async function writeAttachment(scope, filename, mimeType, sizeBytes, buffer) {
|
|
5490
|
-
const attachmentId =
|
|
5491
|
-
const dir =
|
|
4139
|
+
const attachmentId = randomUUID3();
|
|
4140
|
+
const dir = resolve4(ATTACHMENTS_ROOT, scope, attachmentId);
|
|
5492
4141
|
await mkdir2(dir, { recursive: true });
|
|
5493
4142
|
const ext = extname(filename) || "";
|
|
5494
|
-
const storagePath =
|
|
5495
|
-
const metaPath =
|
|
4143
|
+
const storagePath = resolve4(dir, `${attachmentId}${ext}`);
|
|
4144
|
+
const metaPath = resolve4(dir, `${attachmentId}.meta.json`);
|
|
5496
4145
|
const meta = {
|
|
5497
4146
|
attachmentId,
|
|
5498
4147
|
scope,
|
|
@@ -5566,7 +4215,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5566
4215
|
`File exceeds the 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB).`
|
|
5567
4216
|
);
|
|
5568
4217
|
}
|
|
5569
|
-
const filename =
|
|
4218
|
+
const filename = basename(filePath);
|
|
5570
4219
|
const mimeType = detectMimeType(filePath);
|
|
5571
4220
|
const buffer = await readFile(filePath);
|
|
5572
4221
|
return writeAttachment(accountId, filename, mimeType, fileStat.size, buffer);
|
|
@@ -5575,7 +4224,7 @@ async function storeGeneratedFile(accountId, filePath) {
|
|
|
5575
4224
|
// app/lib/stt/voice-note.ts
|
|
5576
4225
|
import { writeFile as writeFile3, mkdtemp, rm } from "fs/promises";
|
|
5577
4226
|
import { tmpdir } from "os";
|
|
5578
|
-
import { join as
|
|
4227
|
+
import { join as join4 } from "path";
|
|
5579
4228
|
var TAG14 = "[voice]";
|
|
5580
4229
|
var AUDIO_MIME_TYPES = /* @__PURE__ */ new Set([
|
|
5581
4230
|
"audio/ogg",
|
|
@@ -5613,9 +4262,9 @@ async function transcribeVoiceNote(file, source) {
|
|
|
5613
4262
|
let tempDir;
|
|
5614
4263
|
let tempPath;
|
|
5615
4264
|
try {
|
|
5616
|
-
tempDir = await mkdtemp(
|
|
4265
|
+
tempDir = await mkdtemp(join4(tmpdir(), "voice-"));
|
|
5617
4266
|
const ext = audioExtension(mimeType);
|
|
5618
|
-
tempPath =
|
|
4267
|
+
tempPath = join4(tempDir, `recording${ext}`);
|
|
5619
4268
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
5620
4269
|
await writeFile3(tempPath, buffer);
|
|
5621
4270
|
} catch (err) {
|
|
@@ -6170,16 +4819,16 @@ var group_default = app4;
|
|
|
6170
4819
|
|
|
6171
4820
|
// app/lib/access-gate.ts
|
|
6172
4821
|
import neo4j from "neo4j-driver";
|
|
6173
|
-
import { readFileSync as
|
|
6174
|
-
import { resolve as
|
|
6175
|
-
import { randomUUID as
|
|
6176
|
-
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ??
|
|
4822
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
4823
|
+
import { resolve as resolve5 } from "path";
|
|
4824
|
+
import { randomUUID as randomUUID4, randomInt } from "crypto";
|
|
4825
|
+
var PLATFORM_ROOT3 = process.env.MAXY_PLATFORM_ROOT ?? resolve5(process.cwd(), "..");
|
|
6177
4826
|
var driver = null;
|
|
6178
4827
|
function readPassword() {
|
|
6179
4828
|
if (process.env.NEO4J_PASSWORD) return process.env.NEO4J_PASSWORD;
|
|
6180
|
-
const passwordFile =
|
|
4829
|
+
const passwordFile = resolve5(PLATFORM_ROOT3, "config/.neo4j-password");
|
|
6181
4830
|
try {
|
|
6182
|
-
return
|
|
4831
|
+
return readFileSync4(passwordFile, "utf-8").trim();
|
|
6183
4832
|
} catch {
|
|
6184
4833
|
throw new Error(
|
|
6185
4834
|
`Neo4j password not found. Expected at ${passwordFile} or in NEO4J_PASSWORD env var.`
|
|
@@ -6428,7 +5077,7 @@ async function setGrantPassword(grantId, passwordHash) {
|
|
|
6428
5077
|
}
|
|
6429
5078
|
}
|
|
6430
5079
|
async function generateNewMagicToken(grantId) {
|
|
6431
|
-
const token =
|
|
5080
|
+
const token = randomUUID4();
|
|
6432
5081
|
const session = getSession2();
|
|
6433
5082
|
try {
|
|
6434
5083
|
await session.run(
|
|
@@ -6490,19 +5139,19 @@ async function findActiveGrantByContact(contactValue, agentSlug, accountId) {
|
|
|
6490
5139
|
}
|
|
6491
5140
|
|
|
6492
5141
|
// app/lib/brevo-sms.ts
|
|
6493
|
-
import { readFileSync as
|
|
6494
|
-
import { dirname
|
|
6495
|
-
import { resolve as
|
|
6496
|
-
var BREVO_API_KEY_FILE =
|
|
5142
|
+
import { readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync2, existsSync as existsSync5, chmodSync } from "fs";
|
|
5143
|
+
import { dirname } from "path";
|
|
5144
|
+
import { resolve as resolve6 } from "path";
|
|
5145
|
+
var BREVO_API_KEY_FILE = resolve6(MAXY_DIR, ".brevo-api-key");
|
|
6497
5146
|
var BREVO_API_URL = "https://api.brevo.com/v3/transactionalSMS/sms";
|
|
6498
5147
|
var BREVO_TIMEOUT_MS = 1e4;
|
|
6499
5148
|
var BREVO_SENDER = "Maxy";
|
|
6500
5149
|
var platformRoot = process.env.MAXY_PLATFORM_ROOT;
|
|
6501
5150
|
if (platformRoot) {
|
|
6502
5151
|
try {
|
|
6503
|
-
const brandPath =
|
|
6504
|
-
if (
|
|
6505
|
-
const brand = JSON.parse(
|
|
5152
|
+
const brandPath = resolve6(platformRoot, "config", "brand.json");
|
|
5153
|
+
if (existsSync5(brandPath)) {
|
|
5154
|
+
const brand = JSON.parse(readFileSync5(brandPath, "utf-8"));
|
|
6506
5155
|
if (brand.productName) BREVO_SENDER = brand.productName;
|
|
6507
5156
|
}
|
|
6508
5157
|
} catch {
|
|
@@ -6510,7 +5159,7 @@ if (platformRoot) {
|
|
|
6510
5159
|
}
|
|
6511
5160
|
function readBrevoApiKey() {
|
|
6512
5161
|
try {
|
|
6513
|
-
const key =
|
|
5162
|
+
const key = readFileSync5(BREVO_API_KEY_FILE, "utf-8").trim();
|
|
6514
5163
|
if (!key) {
|
|
6515
5164
|
throw new Error(`Brevo API key file is empty: ${BREVO_API_KEY_FILE}`);
|
|
6516
5165
|
}
|
|
@@ -6525,7 +5174,7 @@ function readBrevoApiKey() {
|
|
|
6525
5174
|
}
|
|
6526
5175
|
}
|
|
6527
5176
|
function hasBrevoApiKey() {
|
|
6528
|
-
return
|
|
5177
|
+
return existsSync5(BREVO_API_KEY_FILE);
|
|
6529
5178
|
}
|
|
6530
5179
|
async function sendSms(recipient, content, opts) {
|
|
6531
5180
|
let apiKey;
|
|
@@ -6941,7 +5590,7 @@ app5.post("/send-otp", async (c) => {
|
|
|
6941
5590
|
var access_default = app5;
|
|
6942
5591
|
|
|
6943
5592
|
// server/routes/telegram.ts
|
|
6944
|
-
import { existsSync as
|
|
5593
|
+
import { existsSync as existsSync6, readFileSync as readFileSync6 } from "fs";
|
|
6945
5594
|
import { timingSafeEqual } from "crypto";
|
|
6946
5595
|
|
|
6947
5596
|
// app/lib/telegram/access-control.ts
|
|
@@ -6978,8 +5627,8 @@ var TELEGRAM_API = "https://api.telegram.org";
|
|
|
6978
5627
|
function getWebhookSecret(botType) {
|
|
6979
5628
|
const filePath = botType === "admin" ? TELEGRAM_ADMIN_WEBHOOK_SECRET_FILE : TELEGRAM_WEBHOOK_SECRET_FILE;
|
|
6980
5629
|
try {
|
|
6981
|
-
if (!
|
|
6982
|
-
const secret =
|
|
5630
|
+
if (!existsSync6(filePath)) return null;
|
|
5631
|
+
const secret = readFileSync6(filePath, "utf-8").trim();
|
|
6983
5632
|
return secret || null;
|
|
6984
5633
|
} catch {
|
|
6985
5634
|
return null;
|
|
@@ -7137,12 +5786,12 @@ app6.post("/webhook", async (c) => {
|
|
|
7137
5786
|
var telegram_default = app6;
|
|
7138
5787
|
|
|
7139
5788
|
// server/routes/whatsapp.ts
|
|
7140
|
-
import { join as
|
|
5789
|
+
import { join as join5, resolve as resolve7, basename as basename2 } from "path";
|
|
7141
5790
|
import { readFile as readFile2, stat as stat3 } from "fs/promises";
|
|
7142
|
-
import { realpathSync as realpathSync2, readdirSync as
|
|
5791
|
+
import { realpathSync as realpathSync2, readdirSync as readdirSync2, readFileSync as readFileSync7, existsSync as existsSync7 } from "fs";
|
|
7143
5792
|
|
|
7144
5793
|
// app/lib/whatsapp/login.ts
|
|
7145
|
-
import { randomUUID as
|
|
5794
|
+
import { randomUUID as randomUUID5 } from "crypto";
|
|
7146
5795
|
var TAG17 = "[whatsapp:login]";
|
|
7147
5796
|
var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
|
|
7148
5797
|
var activeLogins = /* @__PURE__ */ new Map();
|
|
@@ -7245,8 +5894,8 @@ async function startLogin(opts) {
|
|
|
7245
5894
|
resetActiveLogin(accountId);
|
|
7246
5895
|
let resolveQr = null;
|
|
7247
5896
|
let rejectQr = null;
|
|
7248
|
-
const qrPromise = new Promise((
|
|
7249
|
-
resolveQr =
|
|
5897
|
+
const qrPromise = new Promise((resolve22, reject) => {
|
|
5898
|
+
resolveQr = resolve22;
|
|
7250
5899
|
rejectQr = reject;
|
|
7251
5900
|
});
|
|
7252
5901
|
const qrTimer = setTimeout(
|
|
@@ -7281,7 +5930,7 @@ async function startLogin(opts) {
|
|
|
7281
5930
|
const login = {
|
|
7282
5931
|
accountId,
|
|
7283
5932
|
authDir,
|
|
7284
|
-
id:
|
|
5933
|
+
id: randomUUID5(),
|
|
7285
5934
|
sock,
|
|
7286
5935
|
startedAt: Date.now(),
|
|
7287
5936
|
connected: false
|
|
@@ -7480,7 +6129,7 @@ app7.post("/login/start", async (c) => {
|
|
|
7480
6129
|
const body = await c.req.json().catch(() => ({}));
|
|
7481
6130
|
const accountId = validateAccountId(body.accountId);
|
|
7482
6131
|
const force = body.force ?? false;
|
|
7483
|
-
const authDir =
|
|
6132
|
+
const authDir = join5(MAXY_DIR, "credentials", "whatsapp", accountId);
|
|
7484
6133
|
const result = await startLogin({ accountId, authDir, force });
|
|
7485
6134
|
console.error(`${TAG18} login/start result account=${accountId} hasQr=${!!result.qrRaw}${result.selfPhone ? ` phone=${result.selfPhone}` : ""}`);
|
|
7486
6135
|
return c.json(result);
|
|
@@ -7640,17 +6289,17 @@ app7.post("/config", async (c) => {
|
|
|
7640
6289
|
return c.json(result, result.ok ? 200 : 400);
|
|
7641
6290
|
}
|
|
7642
6291
|
case "list-public-agents": {
|
|
7643
|
-
const agentsDir =
|
|
6292
|
+
const agentsDir = resolve7(account.accountDir, "agents");
|
|
7644
6293
|
const agents = [];
|
|
7645
|
-
if (
|
|
6294
|
+
if (existsSync7(agentsDir)) {
|
|
7646
6295
|
try {
|
|
7647
|
-
const entries =
|
|
6296
|
+
const entries = readdirSync2(agentsDir, { withFileTypes: true });
|
|
7648
6297
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
7649
6298
|
if (!entry.isDirectory() || entry.name === "admin") continue;
|
|
7650
|
-
const configPath2 =
|
|
7651
|
-
if (!
|
|
6299
|
+
const configPath2 = resolve7(agentsDir, entry.name, "config.json");
|
|
6300
|
+
if (!existsSync7(configPath2)) continue;
|
|
7652
6301
|
try {
|
|
7653
|
-
const config = JSON.parse(
|
|
6302
|
+
const config = JSON.parse(readFileSync7(configPath2, "utf-8"));
|
|
7654
6303
|
agents.push({ slug: entry.name, displayName: config.displayName ?? entry.name });
|
|
7655
6304
|
} catch {
|
|
7656
6305
|
console.error(`${TAG18} config action=list-public-agents error="failed to parse config.json for agent ${entry.name}" \u2014 skipping`);
|
|
@@ -7725,7 +6374,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7725
6374
|
if (!maxyAccountId || !PLATFORM_ROOT4) {
|
|
7726
6375
|
return c.json({ error: "Cannot validate file path: missing account or platform context" }, 400);
|
|
7727
6376
|
}
|
|
7728
|
-
const accountDir =
|
|
6377
|
+
const accountDir = resolve7(PLATFORM_ROOT4, "..", "data/accounts", maxyAccountId);
|
|
7729
6378
|
let resolvedPath;
|
|
7730
6379
|
try {
|
|
7731
6380
|
resolvedPath = realpathSync2(filePath);
|
|
@@ -7749,7 +6398,7 @@ app7.post("/send-document", async (c) => {
|
|
|
7749
6398
|
return c.json({ error: `File exceeds 20 MB limit (${(fileStat.size / 1024 / 1024).toFixed(1)} MB)` }, 400);
|
|
7750
6399
|
}
|
|
7751
6400
|
const buffer = Buffer.from(await readFile2(resolvedPath));
|
|
7752
|
-
const filename =
|
|
6401
|
+
const filename = basename2(resolvedPath);
|
|
7753
6402
|
const mimetype = detectMimeType(resolvedPath);
|
|
7754
6403
|
const sock = getSocket(accountId);
|
|
7755
6404
|
if (!sock) {
|
|
@@ -7949,16 +6598,16 @@ var whatsapp_default = app7;
|
|
|
7949
6598
|
|
|
7950
6599
|
// server/routes/onboarding.ts
|
|
7951
6600
|
import { spawn, execFileSync } from "child_process";
|
|
7952
|
-
import { openSync
|
|
7953
|
-
import { resolve as
|
|
7954
|
-
import { createHash, randomUUID as
|
|
6601
|
+
import { openSync, closeSync, writeFileSync as writeFileSync4, writeSync, existsSync as existsSync8, mkdirSync as mkdirSync3, readFileSync as readFileSync8, unlinkSync } from "fs";
|
|
6602
|
+
import { resolve as resolve8, dirname as dirname2 } from "path";
|
|
6603
|
+
import { createHash, randomUUID as randomUUID6 } from "crypto";
|
|
7955
6604
|
var PLATFORM_ROOT5 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
7956
6605
|
function hashPin(pin) {
|
|
7957
6606
|
return createHash("sha256").update(pin).digest("hex");
|
|
7958
6607
|
}
|
|
7959
6608
|
function readUsersFile() {
|
|
7960
|
-
if (!
|
|
7961
|
-
const raw =
|
|
6609
|
+
if (!existsSync8(USERS_FILE)) return null;
|
|
6610
|
+
const raw = readFileSync8(USERS_FILE, "utf-8").trim();
|
|
7962
6611
|
if (!raw) return [];
|
|
7963
6612
|
return JSON.parse(raw);
|
|
7964
6613
|
}
|
|
@@ -8024,11 +6673,11 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8024
6673
|
if (!vncReady) return c.json({ error: "VNC display failed to start" }, 500);
|
|
8025
6674
|
}
|
|
8026
6675
|
await ensureCdp(transport);
|
|
8027
|
-
|
|
6676
|
+
writeFileSync4(logPath("claude-auth"), "");
|
|
8028
6677
|
const chromiumWrapper = writeChromiumWrapper();
|
|
8029
6678
|
const x11Env = buildX11Env(chromiumWrapper, transport);
|
|
8030
6679
|
vncLog("claude-auth", { action: "start", transport });
|
|
8031
|
-
const claudeAuthLogFd =
|
|
6680
|
+
const claudeAuthLogFd = openSync(logPath("claude-auth"), "a");
|
|
8032
6681
|
const claudeProc = spawn("claude", ["auth", "login"], {
|
|
8033
6682
|
env: x11Env,
|
|
8034
6683
|
stdio: ["ignore", "pipe", "pipe"]
|
|
@@ -8037,7 +6686,7 @@ app8.post("/claude-auth", async (c) => {
|
|
|
8037
6686
|
const onClaudeOutput = (chunk) => writeSync(claudeAuthLogFd, chunk);
|
|
8038
6687
|
claudeProc.stdout?.on("data", onClaudeOutput);
|
|
8039
6688
|
claudeProc.stderr?.on("data", onClaudeOutput);
|
|
8040
|
-
claudeProc.once("close", () =>
|
|
6689
|
+
claudeProc.once("close", () => closeSync(claudeAuthLogFd));
|
|
8041
6690
|
await waitForAuthPage(2e4);
|
|
8042
6691
|
return c.json({ started: true, transport });
|
|
8043
6692
|
});
|
|
@@ -8068,22 +6717,22 @@ app8.post("/set-pin", async (c) => {
|
|
|
8068
6717
|
const hash = hashPin(body.pin);
|
|
8069
6718
|
const account = resolveAccount();
|
|
8070
6719
|
const existingOwnerUserId = account?.config.admins?.find((a) => a.role === "owner")?.userId;
|
|
8071
|
-
const userId = existingOwnerUserId ??
|
|
6720
|
+
const userId = existingOwnerUserId ?? randomUUID6();
|
|
8072
6721
|
if (existingOwnerUserId) {
|
|
8073
6722
|
console.log(`[set-pin] reusing existing owner userId=${userId.slice(0, 8)}\u2026 (change-PIN preserves identity)`);
|
|
8074
6723
|
} else {
|
|
8075
6724
|
console.log(`[set-pin] minted new userId=${userId.slice(0, 8)}\u2026 (first-time install)`);
|
|
8076
6725
|
}
|
|
8077
|
-
|
|
8078
|
-
|
|
6726
|
+
mkdirSync3(dirname2(USERS_FILE), { recursive: true });
|
|
6727
|
+
writeFileSync4(USERS_FILE, JSON.stringify([{ userId, pin: hash }]), { mode: 384 });
|
|
8079
6728
|
console.log(`[set-pin] wrote users.json: userId=${userId.slice(0, 8)}\u2026 hash=${hash.slice(0, 8)}\u2026`);
|
|
8080
6729
|
if (account) {
|
|
8081
6730
|
try {
|
|
8082
|
-
const config = JSON.parse(
|
|
6731
|
+
const config = JSON.parse(readFileSync8(`${account.accountDir}/account.json`, "utf-8"));
|
|
8083
6732
|
if (!config.admins) config.admins = [];
|
|
8084
6733
|
if (!config.admins.some((a) => a.userId === userId)) {
|
|
8085
6734
|
config.admins.push({ userId, role: "owner" });
|
|
8086
|
-
|
|
6735
|
+
writeFileSync4(`${account.accountDir}/account.json`, JSON.stringify(config, null, 2) + "\n");
|
|
8087
6736
|
console.log(`[set-pin] added userId=${userId.slice(0, 8)}\u2026 to account.json admins`);
|
|
8088
6737
|
}
|
|
8089
6738
|
} catch (err) {
|
|
@@ -8136,7 +6785,7 @@ app8.delete("/set-pin", async (c) => {
|
|
|
8136
6785
|
unlinkSync(USERS_FILE);
|
|
8137
6786
|
console.log(`[set-pin] cleared users.json (last entry removed): userId=${matchedUser.userId.slice(0, 8)}\u2026`);
|
|
8138
6787
|
} else {
|
|
8139
|
-
|
|
6788
|
+
writeFileSync4(USERS_FILE, JSON.stringify(remaining), { mode: 384 });
|
|
8140
6789
|
console.log(`[set-pin] removed entry from users.json: userId=${matchedUser.userId.slice(0, 8)}\u2026 remaining=${remaining.length}`);
|
|
8141
6790
|
}
|
|
8142
6791
|
return c.json({ ok: true });
|
|
@@ -8155,19 +6804,19 @@ app8.post("/skip", async (c) => {
|
|
|
8155
6804
|
}
|
|
8156
6805
|
const { accountId, accountDir } = account;
|
|
8157
6806
|
let agentName = "Maxy";
|
|
8158
|
-
const brandPath = PLATFORM_ROOT5 ?
|
|
8159
|
-
if (brandPath &&
|
|
6807
|
+
const brandPath = PLATFORM_ROOT5 ? resolve8(PLATFORM_ROOT5, "config", "brand.json") : "";
|
|
6808
|
+
if (brandPath && existsSync8(brandPath)) {
|
|
8160
6809
|
try {
|
|
8161
|
-
const brand = JSON.parse(
|
|
6810
|
+
const brand = JSON.parse(readFileSync8(brandPath, "utf-8"));
|
|
8162
6811
|
if (brand.productName) agentName = brand.productName;
|
|
8163
6812
|
} catch (err) {
|
|
8164
6813
|
console.error(`[onboarding-skip] brand.json read failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8165
6814
|
}
|
|
8166
6815
|
}
|
|
8167
|
-
const soulPath =
|
|
6816
|
+
const soulPath = resolve8(accountDir, "agents", "admin", "SOUL.md");
|
|
8168
6817
|
try {
|
|
8169
|
-
|
|
8170
|
-
|
|
6818
|
+
mkdirSync3(dirname2(soulPath), { recursive: true });
|
|
6819
|
+
writeFileSync4(soulPath, `You are ${agentName}, an AI operations manager.
|
|
8171
6820
|
`);
|
|
8172
6821
|
console.log(`[onboarding-skip] wrote SOUL.md: ${soulPath}`);
|
|
8173
6822
|
} catch (err) {
|
|
@@ -8211,9 +6860,9 @@ app8.post("/skip", async (c) => {
|
|
|
8211
6860
|
var onboarding_default = app8;
|
|
8212
6861
|
|
|
8213
6862
|
// server/routes/client-error.ts
|
|
8214
|
-
import { appendFileSync
|
|
8215
|
-
import { join as
|
|
8216
|
-
var CLIENT_ERRORS_LOG =
|
|
6863
|
+
import { appendFileSync, existsSync as existsSync9, renameSync, statSync as statSync2 } from "fs";
|
|
6864
|
+
import { join as join6 } from "path";
|
|
6865
|
+
var CLIENT_ERRORS_LOG = join6(LOG_DIR, "client-errors.log");
|
|
8217
6866
|
var MAX_LOG_SIZE = 10 * 1024 * 1024;
|
|
8218
6867
|
var MAX_BODY_SIZE = 8 * 1024;
|
|
8219
6868
|
var MAX_STACK_LEN = 2e3;
|
|
@@ -8256,10 +6905,10 @@ function stackHead(stack) {
|
|
|
8256
6905
|
}
|
|
8257
6906
|
function rotateIfNeeded() {
|
|
8258
6907
|
try {
|
|
8259
|
-
if (!
|
|
8260
|
-
const stats =
|
|
6908
|
+
if (!existsSync9(CLIENT_ERRORS_LOG)) return;
|
|
6909
|
+
const stats = statSync2(CLIENT_ERRORS_LOG);
|
|
8261
6910
|
if (stats.size < MAX_LOG_SIZE) return;
|
|
8262
|
-
|
|
6911
|
+
renameSync(CLIENT_ERRORS_LOG, CLIENT_ERRORS_LOG + ".1");
|
|
8263
6912
|
} catch (err) {
|
|
8264
6913
|
console.error(`[client-error] log rotation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8265
6914
|
}
|
|
@@ -8360,7 +7009,7 @@ app9.post("/", async (c) => {
|
|
|
8360
7009
|
tag: typeof body.tag === "string" ? truncate(body.tag, 32) : void 0,
|
|
8361
7010
|
status: typeof body.status === "number" ? body.status : void 0
|
|
8362
7011
|
};
|
|
8363
|
-
|
|
7012
|
+
appendFileSync(CLIENT_ERRORS_LOG, JSON.stringify(payload) + "\n", "utf-8");
|
|
8364
7013
|
} catch (err) {
|
|
8365
7014
|
console.error(`[client-error] append failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8366
7015
|
}
|
|
@@ -8370,15 +7019,15 @@ app9.post("/", async (c) => {
|
|
|
8370
7019
|
var client_error_default = app9;
|
|
8371
7020
|
|
|
8372
7021
|
// server/routes/admin/session.ts
|
|
8373
|
-
import { readFileSync as
|
|
7022
|
+
import { readFileSync as readFileSync9, writeFileSync as writeFileSync5, existsSync as existsSync10 } from "fs";
|
|
8374
7023
|
import { createHash as createHash2 } from "crypto";
|
|
8375
7024
|
var deprecationLogged = /* @__PURE__ */ new Set();
|
|
8376
7025
|
function hashPin2(pin) {
|
|
8377
7026
|
return createHash2("sha256").update(pin).digest("hex");
|
|
8378
7027
|
}
|
|
8379
7028
|
function readUsersFile2() {
|
|
8380
|
-
if (!
|
|
8381
|
-
const raw =
|
|
7029
|
+
if (!existsSync10(USERS_FILE)) return null;
|
|
7030
|
+
const raw = readFileSync9(USERS_FILE, "utf-8").trim();
|
|
8382
7031
|
if (!raw) return [];
|
|
8383
7032
|
return JSON.parse(raw);
|
|
8384
7033
|
}
|
|
@@ -8395,7 +7044,7 @@ function stripLegacyNameField(users) {
|
|
|
8395
7044
|
}
|
|
8396
7045
|
}
|
|
8397
7046
|
try {
|
|
8398
|
-
|
|
7047
|
+
writeFileSync5(USERS_FILE, JSON.stringify(users), { mode: 384 });
|
|
8399
7048
|
} catch (err) {
|
|
8400
7049
|
console.error(`[admin-identity] users-json strip failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
8401
7050
|
}
|
|
@@ -8552,13 +7201,13 @@ app10.post("/", async (c) => {
|
|
|
8552
7201
|
var session_default2 = app10;
|
|
8553
7202
|
|
|
8554
7203
|
// server/routes/admin/chat.ts
|
|
8555
|
-
import { resolve as
|
|
8556
|
-
import { appendFileSync as
|
|
7204
|
+
import { resolve as resolve9 } from "path";
|
|
7205
|
+
import { appendFileSync as appendFileSync3 } from "fs";
|
|
8557
7206
|
|
|
8558
7207
|
// app/lib/script-stream-tailer.ts
|
|
8559
7208
|
import * as childProcess from "child_process";
|
|
8560
|
-
import { appendFileSync as
|
|
8561
|
-
import { dirname as
|
|
7209
|
+
import { appendFileSync as appendFileSync2, createReadStream as createReadStream2, mkdirSync as mkdirSync4, statSync as statSync3 } from "fs";
|
|
7210
|
+
import { dirname as dirname3 } from "path";
|
|
8562
7211
|
import { StringDecoder } from "string_decoder";
|
|
8563
7212
|
var SCRIPT_STREAM_RE = /^\[([^\]]+)\] \[script:([a-z][a-z0-9-]*)((?::[a-z0-9:_-]+)?)\] (.*)$/;
|
|
8564
7213
|
function parseLine(line) {
|
|
@@ -8576,7 +7225,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8576
7225
|
const { path: path2, onEvent, onError } = opts;
|
|
8577
7226
|
let offset;
|
|
8578
7227
|
try {
|
|
8579
|
-
offset =
|
|
7228
|
+
offset = statSync3(path2).size;
|
|
8580
7229
|
} catch {
|
|
8581
7230
|
offset = 0;
|
|
8582
7231
|
}
|
|
@@ -8595,7 +7244,7 @@ function startScriptStreamTailer(opts) {
|
|
|
8595
7244
|
try {
|
|
8596
7245
|
let size;
|
|
8597
7246
|
try {
|
|
8598
|
-
size =
|
|
7247
|
+
size = statSync3(path2).size;
|
|
8599
7248
|
} catch {
|
|
8600
7249
|
return;
|
|
8601
7250
|
}
|
|
@@ -8667,8 +7316,8 @@ function writeRouteMilestone(streamLogPath, scope, line) {
|
|
|
8667
7316
|
}
|
|
8668
7317
|
const ts = (/* @__PURE__ */ new Date()).toISOString();
|
|
8669
7318
|
try {
|
|
8670
|
-
|
|
8671
|
-
|
|
7319
|
+
mkdirSync4(dirname3(streamLogPath), { recursive: true });
|
|
7320
|
+
appendFileSync2(streamLogPath, `[${ts}] [script:${scope}] ${line}
|
|
8672
7321
|
`);
|
|
8673
7322
|
} catch (err) {
|
|
8674
7323
|
console.error(
|
|
@@ -8843,7 +7492,7 @@ var app11 = new Hono();
|
|
|
8843
7492
|
app11.post("/cancel", requireAdminSession, async (c) => {
|
|
8844
7493
|
const session_key = c.var.sessionKey;
|
|
8845
7494
|
try {
|
|
8846
|
-
const { interruptClient: interruptClient2 } = await import("./client-pool-
|
|
7495
|
+
const { interruptClient: interruptClient2 } = await import("./client-pool-GBY5I2KQ.js");
|
|
8847
7496
|
await interruptClient2(session_key);
|
|
8848
7497
|
return c.json({ ok: true });
|
|
8849
7498
|
} catch (err) {
|
|
@@ -9007,10 +7656,10 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9007
7656
|
function resolveTeeStreamLogPath() {
|
|
9008
7657
|
const liveConvId = getConversationIdForSession(session_key);
|
|
9009
7658
|
const key = liveConvId ?? preflushStreamLogKey(session_key);
|
|
9010
|
-
return
|
|
7659
|
+
return resolve9(account.accountDir, "logs", `claude-agent-stream-${key}.log`);
|
|
9011
7660
|
}
|
|
9012
7661
|
try {
|
|
9013
|
-
|
|
7662
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [chat-route-version=task606-tee-path-resolve] sessionKey=${session_key.slice(0, 12)}\u2026
|
|
9014
7663
|
`);
|
|
9015
7664
|
} catch {
|
|
9016
7665
|
}
|
|
@@ -9019,7 +7668,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9019
7668
|
incoming.on("close", () => {
|
|
9020
7669
|
const tsClose = (/* @__PURE__ */ new Date()).toISOString();
|
|
9021
7670
|
try {
|
|
9022
|
-
|
|
7671
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${tsClose}] [incoming-close] sessionKey=${session_key.slice(0, 12)}\u2026 complete=${incoming.complete}
|
|
9023
7672
|
`);
|
|
9024
7673
|
} catch {
|
|
9025
7674
|
}
|
|
@@ -9027,7 +7676,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9027
7676
|
console.error(`[${tsClose}] [incoming-close] DISCONNECT sessionKey=${session_key.slice(0, 12)}\u2026`);
|
|
9028
7677
|
interruptClient(session_key).catch((err) => {
|
|
9029
7678
|
try {
|
|
9030
|
-
|
|
7679
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] interrupt-failed: ${err instanceof Error ? err.message : String(err)}
|
|
9031
7680
|
`);
|
|
9032
7681
|
} catch {
|
|
9033
7682
|
}
|
|
@@ -9036,7 +7685,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9036
7685
|
});
|
|
9037
7686
|
} else {
|
|
9038
7687
|
try {
|
|
9039
|
-
|
|
7688
|
+
appendFileSync3(resolveTeeStreamLogPath(), `[${(/* @__PURE__ */ new Date()).toISOString()}] [incoming-close] UNAVAILABLE \u2014 c.env.incoming is not an EventEmitter
|
|
9040
7689
|
`);
|
|
9041
7690
|
} catch {
|
|
9042
7691
|
}
|
|
@@ -9048,7 +7697,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9048
7697
|
} catch {
|
|
9049
7698
|
}
|
|
9050
7699
|
try {
|
|
9051
|
-
|
|
7700
|
+
appendFileSync3(resolveTeeStreamLogPath(), line);
|
|
9052
7701
|
} catch {
|
|
9053
7702
|
}
|
|
9054
7703
|
return true;
|
|
@@ -9109,7 +7758,7 @@ app11.post("/", requireAdminSession, async (c) => {
|
|
|
9109
7758
|
try {
|
|
9110
7759
|
registerAdminSSE(sseEntry);
|
|
9111
7760
|
if (sseConvId) {
|
|
9112
|
-
const streamLogPath =
|
|
7761
|
+
const streamLogPath = resolve9(account.accountDir, "logs", `claude-agent-stream-${sseConvId}.log`);
|
|
9113
7762
|
tailer = startScriptStreamTailer({
|
|
9114
7763
|
path: streamLogPath,
|
|
9115
7764
|
onEvent: (event) => {
|
|
@@ -9238,22 +7887,22 @@ app12.post("/", requireAdminSession, async (c) => {
|
|
|
9238
7887
|
var compact_default = app12;
|
|
9239
7888
|
|
|
9240
7889
|
// server/routes/admin/logs.ts
|
|
9241
|
-
import { existsSync as
|
|
9242
|
-
import { resolve as
|
|
7890
|
+
import { existsSync as existsSync12, readdirSync as readdirSync3, readFileSync as readFileSync10, statSync as statSync4 } from "fs";
|
|
7891
|
+
import { resolve as resolve10, basename as basename3 } from "path";
|
|
9243
7892
|
|
|
9244
7893
|
// app/lib/logs-read-resolve.ts
|
|
9245
|
-
import { existsSync as
|
|
9246
|
-
import { join as
|
|
7894
|
+
import { existsSync as existsSync11 } from "fs";
|
|
7895
|
+
import { join as join7 } from "path";
|
|
9247
7896
|
function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
9248
7897
|
const tried = [fullFilename, preflushFilename];
|
|
9249
7898
|
const hits = [];
|
|
9250
7899
|
const stalePreflushPaths = [];
|
|
9251
7900
|
for (const dir of logDirs) {
|
|
9252
|
-
const fullPath =
|
|
9253
|
-
if (
|
|
7901
|
+
const fullPath = join7(dir, fullFilename);
|
|
7902
|
+
if (existsSync11(fullPath)) {
|
|
9254
7903
|
hits.push({ path: fullPath, shape: "full", dir });
|
|
9255
|
-
const preflushSibling =
|
|
9256
|
-
if (
|
|
7904
|
+
const preflushSibling = join7(dir, preflushFilename);
|
|
7905
|
+
if (existsSync11(preflushSibling)) {
|
|
9257
7906
|
stalePreflushPaths.push(preflushSibling);
|
|
9258
7907
|
}
|
|
9259
7908
|
}
|
|
@@ -9262,8 +7911,8 @@ function resolveConversationLogPaths(fullFilename, preflushFilename, logDirs) {
|
|
|
9262
7911
|
return { hits, stalePreflushPaths, tried };
|
|
9263
7912
|
}
|
|
9264
7913
|
for (const dir of logDirs) {
|
|
9265
|
-
const preflushPath =
|
|
9266
|
-
if (
|
|
7914
|
+
const preflushPath = join7(dir, preflushFilename);
|
|
7915
|
+
if (existsSync11(preflushPath)) {
|
|
9267
7916
|
hits.push({ path: preflushPath, shape: "preflush", dir });
|
|
9268
7917
|
}
|
|
9269
7918
|
}
|
|
@@ -9283,19 +7932,19 @@ app13.get("/", async (c) => {
|
|
|
9283
7932
|
const sessionKeyParam = c.req.query("sessionKey");
|
|
9284
7933
|
const download = c.req.query("download") === "1";
|
|
9285
7934
|
const account = resolveAccount();
|
|
9286
|
-
const
|
|
7935
|
+
const accountLogDir = account ? resolve10(account.accountDir, "logs") : null;
|
|
9287
7936
|
const logDirs = [];
|
|
9288
|
-
if (
|
|
7937
|
+
if (accountLogDir) logDirs.push(accountLogDir);
|
|
9289
7938
|
logDirs.push(LOG_DIR);
|
|
9290
7939
|
if (fileParam) {
|
|
9291
|
-
const safe =
|
|
7940
|
+
const safe = basename3(fileParam);
|
|
9292
7941
|
const searched = [];
|
|
9293
7942
|
for (const dir of logDirs) {
|
|
9294
|
-
const filePath =
|
|
7943
|
+
const filePath = resolve10(dir, safe);
|
|
9295
7944
|
searched.push(filePath);
|
|
9296
7945
|
try {
|
|
9297
|
-
const buffer =
|
|
9298
|
-
const onDiskBytes =
|
|
7946
|
+
const buffer = readFileSync10(filePath);
|
|
7947
|
+
const onDiskBytes = statSync4(filePath).size;
|
|
9299
7948
|
const headers = {
|
|
9300
7949
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9301
7950
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9367,9 +8016,9 @@ app13.get("/", async (c) => {
|
|
|
9367
8016
|
const hit = hits[0];
|
|
9368
8017
|
console.info(`[admin/logs] resolved sessionKey=${sessionKeySlice} conversationId=${conversationIdSlice} shape=${hit.shape} stalePreflushCount=${stalePreflushCount}`);
|
|
9369
8018
|
try {
|
|
9370
|
-
const filename =
|
|
8019
|
+
const filename = basename3(hit.path);
|
|
9371
8020
|
if (stalePreflushCount > 0 && !download) {
|
|
9372
|
-
const content =
|
|
8021
|
+
const content = readFileSync10(hit.path, "utf-8");
|
|
9373
8022
|
return c.json({
|
|
9374
8023
|
log: content,
|
|
9375
8024
|
filename,
|
|
@@ -9377,8 +8026,8 @@ app13.get("/", async (c) => {
|
|
|
9377
8026
|
warnings: stalePreflushPaths.map((path2) => ({ kind: "stale-preflush", path: path2 }))
|
|
9378
8027
|
});
|
|
9379
8028
|
}
|
|
9380
|
-
const buffer =
|
|
9381
|
-
const onDiskBytes =
|
|
8029
|
+
const buffer = readFileSync10(hit.path);
|
|
8030
|
+
const onDiskBytes = statSync4(hit.path).size;
|
|
9382
8031
|
const headers = {
|
|
9383
8032
|
"Content-Type": "text/plain; charset=utf-8",
|
|
9384
8033
|
"Content-Length": String(buffer.byteLength)
|
|
@@ -9411,19 +8060,19 @@ app13.get("/", async (c) => {
|
|
|
9411
8060
|
const seen = /* @__PURE__ */ new Set();
|
|
9412
8061
|
const logs = {};
|
|
9413
8062
|
for (const dir of logDirs) {
|
|
9414
|
-
if (!
|
|
8063
|
+
if (!existsSync12(dir)) continue;
|
|
9415
8064
|
let files;
|
|
9416
8065
|
try {
|
|
9417
|
-
files =
|
|
8066
|
+
files = readdirSync3(dir).filter((f) => f.endsWith(".log"));
|
|
9418
8067
|
} catch (err) {
|
|
9419
8068
|
const reason = err instanceof Error ? err.message : String(err);
|
|
9420
8069
|
console.warn(`[admin/logs] readdir-fail dir=${dir} reason=${reason}`);
|
|
9421
8070
|
continue;
|
|
9422
8071
|
}
|
|
9423
|
-
files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime:
|
|
8072
|
+
files.filter((f) => !seen.has(f)).map((f) => ({ name: f, mtime: statSync4(resolve10(dir, f)).mtimeMs })).sort((a, b) => b.mtime - a.mtime).forEach(({ name }) => {
|
|
9424
8073
|
seen.add(name);
|
|
9425
8074
|
try {
|
|
9426
|
-
const content =
|
|
8075
|
+
const content = readFileSync10(resolve10(dir, name));
|
|
9427
8076
|
const tail = content.length > TAIL_BYTES ? content.subarray(content.length - TAIL_BYTES).toString("utf-8") : content.toString("utf-8");
|
|
9428
8077
|
logs[name] = tail.trim() || "(empty)";
|
|
9429
8078
|
} catch (err) {
|
|
@@ -9462,8 +8111,8 @@ var claude_info_default = app14;
|
|
|
9462
8111
|
|
|
9463
8112
|
// server/routes/admin/attachment.ts
|
|
9464
8113
|
import { readFile as readFile3, readdir } from "fs/promises";
|
|
9465
|
-
import { existsSync as
|
|
9466
|
-
import { resolve as
|
|
8114
|
+
import { existsSync as existsSync13 } from "fs";
|
|
8115
|
+
import { resolve as resolve11 } from "path";
|
|
9467
8116
|
var app15 = new Hono();
|
|
9468
8117
|
app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
9469
8118
|
const attachmentId = c.req.param("attachmentId");
|
|
@@ -9475,12 +8124,12 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9475
8124
|
if (!/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(attachmentId)) {
|
|
9476
8125
|
return new Response("Not found", { status: 404 });
|
|
9477
8126
|
}
|
|
9478
|
-
const dir =
|
|
9479
|
-
if (!
|
|
8127
|
+
const dir = resolve11(ATTACHMENTS_ROOT, accountId, attachmentId);
|
|
8128
|
+
if (!existsSync13(dir)) {
|
|
9480
8129
|
return new Response("Not found", { status: 404 });
|
|
9481
8130
|
}
|
|
9482
|
-
const metaPath =
|
|
9483
|
-
if (!
|
|
8131
|
+
const metaPath = resolve11(dir, `${attachmentId}.meta.json`);
|
|
8132
|
+
if (!existsSync13(metaPath)) {
|
|
9484
8133
|
return new Response("Not found", { status: 404 });
|
|
9485
8134
|
}
|
|
9486
8135
|
let meta;
|
|
@@ -9494,7 +8143,7 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9494
8143
|
if (!dataFile) {
|
|
9495
8144
|
return new Response("Not found", { status: 404 });
|
|
9496
8145
|
}
|
|
9497
|
-
const filePath =
|
|
8146
|
+
const filePath = resolve11(dir, dataFile);
|
|
9498
8147
|
const buffer = await readFile3(filePath);
|
|
9499
8148
|
return new Response(new Uint8Array(buffer), {
|
|
9500
8149
|
headers: {
|
|
@@ -9507,24 +8156,24 @@ app15.get("/:attachmentId", requireAdminSession, async (c) => {
|
|
|
9507
8156
|
var attachment_default = app15;
|
|
9508
8157
|
|
|
9509
8158
|
// server/routes/admin/agents.ts
|
|
9510
|
-
import { resolve as
|
|
9511
|
-
import { readdirSync as
|
|
8159
|
+
import { resolve as resolve12 } from "path";
|
|
8160
|
+
import { readdirSync as readdirSync4, readFileSync as readFileSync11, existsSync as existsSync14, rmSync } from "fs";
|
|
9512
8161
|
var app16 = new Hono();
|
|
9513
8162
|
app16.get("/", (c) => {
|
|
9514
8163
|
const account = resolveAccount();
|
|
9515
8164
|
if (!account) return c.json({ agents: [] });
|
|
9516
|
-
const agentsDir =
|
|
9517
|
-
if (!
|
|
8165
|
+
const agentsDir = resolve12(account.accountDir, "agents");
|
|
8166
|
+
if (!existsSync14(agentsDir)) return c.json({ agents: [] });
|
|
9518
8167
|
const agents = [];
|
|
9519
8168
|
try {
|
|
9520
|
-
const entries =
|
|
8169
|
+
const entries = readdirSync4(agentsDir, { withFileTypes: true });
|
|
9521
8170
|
for (const entry of entries.sort((a, b) => a.name.localeCompare(b.name))) {
|
|
9522
8171
|
if (!entry.isDirectory()) continue;
|
|
9523
8172
|
if (entry.name === "admin") continue;
|
|
9524
|
-
const configPath2 =
|
|
9525
|
-
if (!
|
|
8173
|
+
const configPath2 = resolve12(agentsDir, entry.name, "config.json");
|
|
8174
|
+
if (!existsSync14(configPath2)) continue;
|
|
9526
8175
|
try {
|
|
9527
|
-
const config = JSON.parse(
|
|
8176
|
+
const config = JSON.parse(readFileSync11(configPath2, "utf-8"));
|
|
9528
8177
|
agents.push({
|
|
9529
8178
|
slug: entry.name,
|
|
9530
8179
|
displayName: config.displayName ?? entry.name,
|
|
@@ -9550,8 +8199,8 @@ app16.delete("/:slug", async (c) => {
|
|
|
9550
8199
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9551
8200
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9552
8201
|
}
|
|
9553
|
-
const agentDir =
|
|
9554
|
-
if (!
|
|
8202
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
8203
|
+
if (!existsSync14(agentDir)) {
|
|
9555
8204
|
return c.json({ error: "Agent not found" }, 404);
|
|
9556
8205
|
}
|
|
9557
8206
|
try {
|
|
@@ -9580,8 +8229,8 @@ app16.post("/:slug/project", async (c) => {
|
|
|
9580
8229
|
if (slug.includes("/") || slug.includes("..") || slug.includes("\\")) {
|
|
9581
8230
|
return c.json({ error: "Invalid agent slug" }, 400);
|
|
9582
8231
|
}
|
|
9583
|
-
const agentDir =
|
|
9584
|
-
if (!
|
|
8232
|
+
const agentDir = resolve12(account.accountDir, "agents", slug);
|
|
8233
|
+
if (!existsSync14(agentDir)) {
|
|
9585
8234
|
return c.json({ error: "Agent not found on disk" }, 404);
|
|
9586
8235
|
}
|
|
9587
8236
|
try {
|
|
@@ -9597,7 +8246,7 @@ var agents_default = app16;
|
|
|
9597
8246
|
// server/routes/admin/sessions.ts
|
|
9598
8247
|
import crypto2 from "crypto";
|
|
9599
8248
|
import { resolve as resolvePath } from "path";
|
|
9600
|
-
import { appendFileSync as
|
|
8249
|
+
import { appendFileSync as appendFileSync4, existsSync as existsSync15 } from "fs";
|
|
9601
8250
|
function validateAndShapeAttachments(raws, conversationAccountId, conversationId, messageId, streamLogPath) {
|
|
9602
8251
|
const chips = [];
|
|
9603
8252
|
let valid = 0;
|
|
@@ -9606,11 +8255,11 @@ function validateAndShapeAttachments(raws, conversationAccountId, conversationId
|
|
|
9606
8255
|
let reason = null;
|
|
9607
8256
|
if (!a.attachmentId || !a.filename || !a.mimeType || !a.storagePath) reason = "schema-fail";
|
|
9608
8257
|
else if (a.accountId !== conversationAccountId) reason = "account-mismatch";
|
|
9609
|
-
else if (!
|
|
8258
|
+
else if (!existsSync15(a.storagePath)) reason = "missing-file";
|
|
9610
8259
|
if (reason) {
|
|
9611
8260
|
invalid++;
|
|
9612
8261
|
try {
|
|
9613
|
-
|
|
8262
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} attachmentId=${(a.attachmentId || "").slice(0, 8)} reason=${reason}
|
|
9614
8263
|
`);
|
|
9615
8264
|
} catch {
|
|
9616
8265
|
}
|
|
@@ -9673,7 +8322,7 @@ function reconstructAssistantEvents(content, components, conversationId, message
|
|
|
9673
8322
|
const line = `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate-invalid] conversationId=${conversationId.slice(0, 8)} messageId=${messageId.slice(0, 8)} name=${c.name || "<unnamed>"} reason=${invalidReason}
|
|
9674
8323
|
`;
|
|
9675
8324
|
try {
|
|
9676
|
-
|
|
8325
|
+
appendFileSync4(streamLogPath, line);
|
|
9677
8326
|
} catch {
|
|
9678
8327
|
}
|
|
9679
8328
|
continue;
|
|
@@ -9876,14 +8525,14 @@ app17.post("/:id/resume", async (c) => {
|
|
|
9876
8525
|
const userMessageCount = rehydrated.filter((m) => m.role !== "assistant").length;
|
|
9877
8526
|
const reason = bridged ? "post-restart" : "page-refresh";
|
|
9878
8527
|
try {
|
|
9879
|
-
|
|
8528
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [admin-resume] reason=${reason} sessionKey=${sessionKey.slice(0, 8)} conversationId=${conversationId.slice(0, 8)} ${tag} loadedMessages=${messages.length} componentCount=${totalComponents} userAttachmentCount=${totalAttachments}
|
|
9880
8529
|
`);
|
|
9881
8530
|
if (totalComponents > 0) {
|
|
9882
|
-
|
|
8531
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [component-rehydrate] conversationId=${conversationId.slice(0, 8)} count=${totalComponents} valid=${totalValid} invalid=${totalInvalid} textRuns=${textRuns}
|
|
9883
8532
|
`);
|
|
9884
8533
|
}
|
|
9885
8534
|
if (totalAttachments > 0 || totalAttachmentInvalid > 0) {
|
|
9886
|
-
|
|
8535
|
+
appendFileSync4(streamLogPath, `[${(/* @__PURE__ */ new Date()).toISOString()}] [attachment-rehydrate] conversationId=${conversationId.slice(0, 8)} userMessages=${userMessageCount} attachments=${totalAttachments} invalid=${totalAttachmentInvalid}
|
|
9887
8536
|
`);
|
|
9888
8537
|
}
|
|
9889
8538
|
} catch {
|
|
@@ -10147,8 +8796,8 @@ var events_default = app20;
|
|
|
10147
8796
|
|
|
10148
8797
|
// server/routes/admin/cloudflare.ts
|
|
10149
8798
|
import { homedir } from "os";
|
|
10150
|
-
import { resolve as
|
|
10151
|
-
import { readFileSync as
|
|
8799
|
+
import { resolve as resolve14 } from "path";
|
|
8800
|
+
import { readFileSync as readFileSync14 } from "fs";
|
|
10152
8801
|
|
|
10153
8802
|
// app/lib/dns-label.ts
|
|
10154
8803
|
var VALID_LABEL = /^[a-z0-9]([a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
@@ -10164,14 +8813,14 @@ function isValidDomain(value) {
|
|
|
10164
8813
|
}
|
|
10165
8814
|
|
|
10166
8815
|
// app/lib/alias-domains.ts
|
|
10167
|
-
import { existsSync as
|
|
10168
|
-
import { dirname as
|
|
10169
|
-
import { resolve as
|
|
10170
|
-
var ALIAS_DOMAINS_PATH =
|
|
8816
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync5, readFileSync as readFileSync12, writeFileSync as writeFileSync6 } from "fs";
|
|
8817
|
+
import { dirname as dirname4 } from "path";
|
|
8818
|
+
import { resolve as resolve13 } from "path";
|
|
8819
|
+
var ALIAS_DOMAINS_PATH = resolve13(MAXY_DIR, "alias-domains.json");
|
|
10171
8820
|
function readExisting() {
|
|
10172
|
-
if (!
|
|
8821
|
+
if (!existsSync16(ALIAS_DOMAINS_PATH)) return /* @__PURE__ */ new Set();
|
|
10173
8822
|
try {
|
|
10174
|
-
const parsed = JSON.parse(
|
|
8823
|
+
const parsed = JSON.parse(readFileSync12(ALIAS_DOMAINS_PATH, "utf-8"));
|
|
10175
8824
|
if (!Array.isArray(parsed)) return /* @__PURE__ */ new Set();
|
|
10176
8825
|
return new Set(parsed.filter((h) => typeof h === "string"));
|
|
10177
8826
|
} catch {
|
|
@@ -10182,18 +8831,226 @@ function addAliasDomain(hostname2) {
|
|
|
10182
8831
|
const existing = readExisting();
|
|
10183
8832
|
if (existing.has(hostname2)) return;
|
|
10184
8833
|
existing.add(hostname2);
|
|
10185
|
-
|
|
10186
|
-
|
|
8834
|
+
mkdirSync5(dirname4(ALIAS_DOMAINS_PATH), { recursive: true });
|
|
8835
|
+
writeFileSync6(ALIAS_DOMAINS_PATH, JSON.stringify([...existing], null, 2) + "\n", "utf-8");
|
|
8836
|
+
}
|
|
8837
|
+
|
|
8838
|
+
// app/lib/cloudflare-task-tracker.ts
|
|
8839
|
+
import { readFileSync as readFileSync13, existsSync as existsSync17 } from "fs";
|
|
8840
|
+
import { randomUUID as randomUUID7 } from "crypto";
|
|
8841
|
+
var import_dist = __toESM(require_dist(), 1);
|
|
8842
|
+
var CREATED_BY_AGENT = "cloudflare-setup-endpoint";
|
|
8843
|
+
var TASK_KIND = "cloudflare-tunnel-login";
|
|
8844
|
+
async function openCloudflareTask(params) {
|
|
8845
|
+
const { accountId, conversationKey, inputsProvided } = params;
|
|
8846
|
+
const taskId = randomUUID7();
|
|
8847
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8848
|
+
const session = getSession();
|
|
8849
|
+
try {
|
|
8850
|
+
const conv = await session.run(
|
|
8851
|
+
`MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
8852
|
+
{ conversationKey, accountId }
|
|
8853
|
+
);
|
|
8854
|
+
if (conv.records.length === 0) {
|
|
8855
|
+
throw new Error(
|
|
8856
|
+
`cloudflare-task-tracker: no Conversation with sessionKey=${conversationKey.slice(-8)} for accountId \u2014 refusing to create an orphan Task`
|
|
8857
|
+
);
|
|
8858
|
+
}
|
|
8859
|
+
const conversationElementId = conv.records[0].get("id");
|
|
8860
|
+
const props = {
|
|
8861
|
+
taskId,
|
|
8862
|
+
accountId,
|
|
8863
|
+
name: "Cloudflare tunnel login + setup",
|
|
8864
|
+
description: `Deterministic cloudflare setup invoked by /api/admin/cloudflare/setup. inputsProvided=[${inputsProvided.join(", ")}]`,
|
|
8865
|
+
status: "running",
|
|
8866
|
+
priority: "normal",
|
|
8867
|
+
kind: TASK_KIND,
|
|
8868
|
+
inputsProvided,
|
|
8869
|
+
startedAt: now,
|
|
8870
|
+
createdAt: now,
|
|
8871
|
+
updatedAt: now
|
|
8872
|
+
};
|
|
8873
|
+
const relationships = [
|
|
8874
|
+
{
|
|
8875
|
+
type: "RAISED_DURING",
|
|
8876
|
+
direction: "outgoing",
|
|
8877
|
+
targetNodeId: conversationElementId
|
|
8878
|
+
}
|
|
8879
|
+
];
|
|
8880
|
+
const result = await (0, import_dist.writeNodeWithEdges)({
|
|
8881
|
+
session,
|
|
8882
|
+
labels: ["Task"],
|
|
8883
|
+
props,
|
|
8884
|
+
relationships,
|
|
8885
|
+
createdBy: {
|
|
8886
|
+
agent: CREATED_BY_AGENT,
|
|
8887
|
+
session: conversationKey,
|
|
8888
|
+
tool: "cloudflare-setup-endpoint"
|
|
8889
|
+
}
|
|
8890
|
+
});
|
|
8891
|
+
process.stderr.write(
|
|
8892
|
+
`[task] action-start kind=${TASK_KIND} taskId=${taskId} raisedDuring=${conversationKey.slice(-8)}
|
|
8893
|
+
`
|
|
8894
|
+
);
|
|
8895
|
+
return { taskId, taskElementId: result.nodeId };
|
|
8896
|
+
} finally {
|
|
8897
|
+
await session.close();
|
|
8898
|
+
}
|
|
8899
|
+
}
|
|
8900
|
+
async function appendCloudflareSteps(taskId, accountId, streamLogPath) {
|
|
8901
|
+
if (!existsSync17(streamLogPath)) return [];
|
|
8902
|
+
let content;
|
|
8903
|
+
try {
|
|
8904
|
+
content = readFileSync13(streamLogPath, "utf-8");
|
|
8905
|
+
} catch {
|
|
8906
|
+
return [];
|
|
8907
|
+
}
|
|
8908
|
+
const steps = [];
|
|
8909
|
+
for (const line of content.split(/\r?\n/)) {
|
|
8910
|
+
const m = line.match(/\bphase_line\s+setup-tunnel\s+step=(\S+)/);
|
|
8911
|
+
if (m) steps.push(m[1]);
|
|
8912
|
+
}
|
|
8913
|
+
if (steps.length === 0) return [];
|
|
8914
|
+
const session = getSession();
|
|
8915
|
+
try {
|
|
8916
|
+
await session.run(
|
|
8917
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
8918
|
+
SET t.steps = coalesce(t.steps, []) + $steps,
|
|
8919
|
+
t.updatedAt = $updatedAt`,
|
|
8920
|
+
{ taskId, accountId, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
8921
|
+
);
|
|
8922
|
+
for (const step of steps) {
|
|
8923
|
+
process.stderr.write(
|
|
8924
|
+
`[task] action-step kind=${TASK_KIND} taskId=${taskId} step=${step}
|
|
8925
|
+
`
|
|
8926
|
+
);
|
|
8927
|
+
}
|
|
8928
|
+
return steps;
|
|
8929
|
+
} finally {
|
|
8930
|
+
await session.close();
|
|
8931
|
+
}
|
|
8932
|
+
}
|
|
8933
|
+
async function completeCloudflareTask(params) {
|
|
8934
|
+
const { taskId, taskElementId, accountId, conversationKey, tunnelId, tunnelName, hostnames, status, errorMessage } = params;
|
|
8935
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
8936
|
+
if (status === "failed" && (!errorMessage || errorMessage.trim().length === 0)) {
|
|
8937
|
+
throw new Error(
|
|
8938
|
+
"cloudflare-task-tracker: errorMessage is required when status='failed' (Task 885 process-provenance contract)."
|
|
8939
|
+
);
|
|
8940
|
+
}
|
|
8941
|
+
const session = getSession();
|
|
8942
|
+
try {
|
|
8943
|
+
if (status === "completed" && tunnelId && tunnelName) {
|
|
8944
|
+
const conv = await session.run(
|
|
8945
|
+
`MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
8946
|
+
{ conversationKey, accountId }
|
|
8947
|
+
);
|
|
8948
|
+
const conversationElementId = conv.records.length > 0 ? conv.records[0].get("id") : null;
|
|
8949
|
+
const tunnelProps = {
|
|
8950
|
+
accountId,
|
|
8951
|
+
tunnelId,
|
|
8952
|
+
tunnelName,
|
|
8953
|
+
createdAt: now,
|
|
8954
|
+
updatedAt: now
|
|
8955
|
+
};
|
|
8956
|
+
const tunnelRels = [
|
|
8957
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId }
|
|
8958
|
+
];
|
|
8959
|
+
if (conversationElementId) {
|
|
8960
|
+
tunnelRels.push({
|
|
8961
|
+
type: "RAISED_DURING",
|
|
8962
|
+
direction: "outgoing",
|
|
8963
|
+
targetNodeId: conversationElementId
|
|
8964
|
+
});
|
|
8965
|
+
}
|
|
8966
|
+
const tunnelWrite = await (0, import_dist.writeNodeWithEdges)({
|
|
8967
|
+
session,
|
|
8968
|
+
labels: ["CloudflareTunnel"],
|
|
8969
|
+
props: tunnelProps,
|
|
8970
|
+
relationships: tunnelRels,
|
|
8971
|
+
createdBy: {
|
|
8972
|
+
agent: CREATED_BY_AGENT,
|
|
8973
|
+
session: conversationKey,
|
|
8974
|
+
tool: "cloudflare-setup-endpoint"
|
|
8975
|
+
}
|
|
8976
|
+
});
|
|
8977
|
+
for (const h of hostnames ?? []) {
|
|
8978
|
+
const hostRels = [
|
|
8979
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId },
|
|
8980
|
+
{
|
|
8981
|
+
type: "ROUTES_TO",
|
|
8982
|
+
direction: "outgoing",
|
|
8983
|
+
targetNodeId: tunnelWrite.nodeId
|
|
8984
|
+
}
|
|
8985
|
+
];
|
|
8986
|
+
await (0, import_dist.writeNodeWithEdges)({
|
|
8987
|
+
session,
|
|
8988
|
+
labels: ["CloudflareHostname"],
|
|
8989
|
+
props: {
|
|
8990
|
+
accountId,
|
|
8991
|
+
hostnameValue: h.hostnameValue,
|
|
8992
|
+
tunnelId,
|
|
8993
|
+
isApex: h.isApex,
|
|
8994
|
+
createdAt: now,
|
|
8995
|
+
updatedAt: now
|
|
8996
|
+
},
|
|
8997
|
+
relationships: hostRels,
|
|
8998
|
+
createdBy: {
|
|
8999
|
+
agent: CREATED_BY_AGENT,
|
|
9000
|
+
session: conversationKey,
|
|
9001
|
+
tool: "cloudflare-setup-endpoint"
|
|
9002
|
+
}
|
|
9003
|
+
});
|
|
9004
|
+
}
|
|
9005
|
+
}
|
|
9006
|
+
const setClauses = [
|
|
9007
|
+
"t.status = $status",
|
|
9008
|
+
"t.completedAt = $now",
|
|
9009
|
+
"t.updatedAt = $now"
|
|
9010
|
+
];
|
|
9011
|
+
const queryParams = { taskId, accountId, status, now };
|
|
9012
|
+
if (status === "failed" && errorMessage) {
|
|
9013
|
+
setClauses.push("t.errorMessage = $errorMessage");
|
|
9014
|
+
queryParams.errorMessage = errorMessage;
|
|
9015
|
+
}
|
|
9016
|
+
const updateRes = await session.run(
|
|
9017
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
9018
|
+
SET ${setClauses.join(", ")}
|
|
9019
|
+
RETURN size(coalesce(t.steps, [])) AS stepsCount`,
|
|
9020
|
+
queryParams
|
|
9021
|
+
);
|
|
9022
|
+
const stepsCount = updateRes.records[0]?.get("stepsCount")?.toNumber?.() ?? 0;
|
|
9023
|
+
process.stderr.write(
|
|
9024
|
+
`[task] action-done kind=${TASK_KIND} taskId=${taskId} status=${status} stepsCount=${stepsCount}
|
|
9025
|
+
`
|
|
9026
|
+
);
|
|
9027
|
+
} finally {
|
|
9028
|
+
await session.close();
|
|
9029
|
+
}
|
|
9030
|
+
}
|
|
9031
|
+
function readTunnelState(brandConfigDir) {
|
|
9032
|
+
const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
|
|
9033
|
+
if (!existsSync17(statePath)) return null;
|
|
9034
|
+
try {
|
|
9035
|
+
const parsed = JSON.parse(readFileSync13(statePath, "utf-8"));
|
|
9036
|
+
const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
|
|
9037
|
+
const tunnelName = typeof parsed.tunnelName === "string" ? parsed.tunnelName : null;
|
|
9038
|
+
const domain = typeof parsed.domain === "string" ? parsed.domain : null;
|
|
9039
|
+
if (!tunnelId || !tunnelName || !domain) return null;
|
|
9040
|
+
return { tunnelId, tunnelName, domain };
|
|
9041
|
+
} catch {
|
|
9042
|
+
return null;
|
|
9043
|
+
}
|
|
10187
9044
|
}
|
|
10188
9045
|
|
|
10189
9046
|
// server/routes/admin/cloudflare.ts
|
|
10190
9047
|
var SETUP_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
10191
9048
|
var DOMAINS_TIMEOUT_MS = 40 * 1e3;
|
|
10192
9049
|
function loadBrandInfo() {
|
|
10193
|
-
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10194
|
-
const brandPath =
|
|
9050
|
+
const platformRoot2 = process.env.MAXY_PLATFORM_ROOT ?? resolve14(process.cwd(), "..");
|
|
9051
|
+
const brandPath = resolve14(platformRoot2, "config", "brand.json");
|
|
10195
9052
|
try {
|
|
10196
|
-
const parsed = JSON.parse(
|
|
9053
|
+
const parsed = JSON.parse(readFileSync14(brandPath, "utf-8"));
|
|
10197
9054
|
const hostname2 = typeof parsed.hostname === "string" && parsed.hostname ? parsed.hostname : "maxy";
|
|
10198
9055
|
const configDir2 = typeof parsed.configDir === "string" && parsed.configDir ? parsed.configDir : ".maxy";
|
|
10199
9056
|
return { hostname: hostname2, configDir: configDir2 };
|
|
@@ -10296,7 +9153,7 @@ app21.get("/domains", requireAdminSession, async (c) => {
|
|
|
10296
9153
|
streamLogPath = streamLogPathFor(accountId, correlationId).streamLogPath;
|
|
10297
9154
|
log(`phase=stream-log-resolved path=${streamLogPath}`);
|
|
10298
9155
|
const brand = loadBrandInfo();
|
|
10299
|
-
const scriptPath =
|
|
9156
|
+
const scriptPath = resolve14(homedir(), "list-cf-domains.sh");
|
|
10300
9157
|
const result = await runFormSpawn({
|
|
10301
9158
|
scriptPath,
|
|
10302
9159
|
args: [brand.hostname],
|
|
@@ -10435,6 +9292,17 @@ app21.post("/setup", requireAdminSession, async (c) => {
|
|
|
10435
9292
|
if (await isActionActive("cloudflare-setup")) {
|
|
10436
9293
|
return err("request", "Another Cloudflare setup is already running. Wait for it to finish before starting a new one.");
|
|
10437
9294
|
}
|
|
9295
|
+
let cloudflareTask;
|
|
9296
|
+
try {
|
|
9297
|
+
cloudflareTask = await openCloudflareTask({
|
|
9298
|
+
accountId,
|
|
9299
|
+
conversationKey: sessionKey,
|
|
9300
|
+
inputsProvided: ["adminLabel", "adminDomain", publicFqdn ? "publicLabel" : null, apex ? "apex" : null, "password"].filter((s) => typeof s === "string")
|
|
9301
|
+
});
|
|
9302
|
+
log(`phase=task-opened taskId=${cloudflareTask.taskId}`);
|
|
9303
|
+
} catch (e) {
|
|
9304
|
+
return err("script", `Failed to open process-provenance Task record: ${e instanceof Error ? e.message : String(e)}`);
|
|
9305
|
+
}
|
|
10438
9306
|
let actionId;
|
|
10439
9307
|
let unit;
|
|
10440
9308
|
try {
|
|
@@ -10447,8 +9315,27 @@ app21.post("/setup", requireAdminSession, async (c) => {
|
|
|
10447
9315
|
log(`phase=action-launched id=${actionId} unit=${unit}`);
|
|
10448
9316
|
void (async () => {
|
|
10449
9317
|
const status = await waitForExit(unit, SETUP_TIMEOUT_MS);
|
|
9318
|
+
try {
|
|
9319
|
+
const appendedSteps = await appendCloudflareSteps(cloudflareTask.taskId, accountId, streamLogPath);
|
|
9320
|
+
log(`phase=task-steps-appended count=${appendedSteps.length}`);
|
|
9321
|
+
} catch (e) {
|
|
9322
|
+
logErr(`phase=task-steps-append-failed reason="${e instanceof Error ? e.message : String(e)}"`);
|
|
9323
|
+
}
|
|
10450
9324
|
if (!status || status.execMainStatus !== 0) {
|
|
10451
9325
|
logErr(`phase=post-exit-skipped reason=${status ? `exit=${status.execMainStatus}` : "unit-gone"} id=${actionId}`);
|
|
9326
|
+
try {
|
|
9327
|
+
const exitReason = status ? `script exited with status ${status.execMainStatus}` : "systemd unit vanished before exit recorded";
|
|
9328
|
+
await completeCloudflareTask({
|
|
9329
|
+
taskId: cloudflareTask.taskId,
|
|
9330
|
+
taskElementId: cloudflareTask.taskElementId,
|
|
9331
|
+
accountId,
|
|
9332
|
+
conversationKey: sessionKey,
|
|
9333
|
+
status: "failed",
|
|
9334
|
+
errorMessage: exitReason
|
|
9335
|
+
});
|
|
9336
|
+
} catch (e) {
|
|
9337
|
+
logErr(`phase=task-close-failed status=failed reason="${e instanceof Error ? e.message : String(e)}"`);
|
|
9338
|
+
}
|
|
10452
9339
|
return;
|
|
10453
9340
|
}
|
|
10454
9341
|
const candidates = [publicFqdn, apex].filter((h) => typeof h === "string" && h.length > 0);
|
|
@@ -10468,6 +9355,30 @@ app21.post("/setup", requireAdminSession, async (c) => {
|
|
|
10468
9355
|
logErr(`phase=alias-domain-write host=${host} result=error reason="${e instanceof Error ? e.message : String(e)}"`);
|
|
10469
9356
|
}
|
|
10470
9357
|
}
|
|
9358
|
+
try {
|
|
9359
|
+
const tunnelState = readTunnelState(brand.configDir);
|
|
9360
|
+
const allHostnames = [adminFqdn, publicFqdn, apex].filter(
|
|
9361
|
+
(h) => typeof h === "string" && h.length > 0
|
|
9362
|
+
);
|
|
9363
|
+
const hostnameRecords = allHostnames.map((value) => ({
|
|
9364
|
+
hostnameValue: value,
|
|
9365
|
+
// Apex heuristic: exactly one dot in the FQDN. Mirrors setup-tunnel.sh:332.
|
|
9366
|
+
isApex: (value.match(/\./g)?.length ?? 0) === 1
|
|
9367
|
+
}));
|
|
9368
|
+
await completeCloudflareTask({
|
|
9369
|
+
taskId: cloudflareTask.taskId,
|
|
9370
|
+
taskElementId: cloudflareTask.taskElementId,
|
|
9371
|
+
accountId,
|
|
9372
|
+
conversationKey: sessionKey,
|
|
9373
|
+
tunnelId: tunnelState?.tunnelId,
|
|
9374
|
+
tunnelName: tunnelState?.tunnelName,
|
|
9375
|
+
hostnames: tunnelState ? hostnameRecords : void 0,
|
|
9376
|
+
status: "completed"
|
|
9377
|
+
});
|
|
9378
|
+
log(`phase=task-closed status=completed tunnelId=${tunnelState?.tunnelId ?? "absent"} hostnames=${allHostnames.length}`);
|
|
9379
|
+
} catch (e) {
|
|
9380
|
+
logErr(`phase=task-close-failed status=completed reason="${e instanceof Error ? e.message : String(e)}"`);
|
|
9381
|
+
}
|
|
10471
9382
|
log(`phase=done action=${actionId}`);
|
|
10472
9383
|
})().catch((e) => logErr(`post-exit handler threw: ${e}`));
|
|
10473
9384
|
const total = Date.now() - started;
|
|
@@ -10495,17 +9406,17 @@ var cloudflare_default = app21;
|
|
|
10495
9406
|
import { createReadStream as createReadStream3 } from "fs";
|
|
10496
9407
|
import { readdir as readdir2, readFile as readFile4, stat as stat4, mkdir as mkdir3, writeFile as writeFile4, unlink as unlink2 } from "fs/promises";
|
|
10497
9408
|
import { realpathSync as realpathSync4 } from "fs";
|
|
10498
|
-
import { basename as
|
|
9409
|
+
import { basename as basename4, dirname as dirname5, join as join8, resolve as resolve16, sep as sep2 } from "path";
|
|
10499
9410
|
import { Readable as Readable2 } from "stream";
|
|
10500
9411
|
|
|
10501
9412
|
// app/lib/data-path.ts
|
|
10502
9413
|
import { realpathSync as realpathSync3 } from "fs";
|
|
10503
|
-
import { resolve as
|
|
10504
|
-
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ??
|
|
10505
|
-
var DATA_ROOT =
|
|
9414
|
+
import { resolve as resolve15, normalize, sep, relative } from "path";
|
|
9415
|
+
var PLATFORM_ROOT6 = process.env.MAXY_PLATFORM_ROOT ?? resolve15(process.cwd(), "../platform");
|
|
9416
|
+
var DATA_ROOT = resolve15(PLATFORM_ROOT6, "..", "data");
|
|
10506
9417
|
function resolveDataPath(raw) {
|
|
10507
9418
|
const cleaned = normalize("/" + (raw ?? "").replace(/\\/g, "/")).replace(/^\/+/, "");
|
|
10508
|
-
const absolute =
|
|
9419
|
+
const absolute = resolve15(DATA_ROOT, cleaned);
|
|
10509
9420
|
let dataRootReal;
|
|
10510
9421
|
try {
|
|
10511
9422
|
dataRootReal = realpathSync3(DATA_ROOT);
|
|
@@ -10853,7 +9764,7 @@ async function cascadeDeleteDocument(params) {
|
|
|
10853
9764
|
var UUID_RE4 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
10854
9765
|
async function readMeta(absDir, baseName) {
|
|
10855
9766
|
try {
|
|
10856
|
-
const raw = await readFile4(
|
|
9767
|
+
const raw = await readFile4(join8(absDir, `${baseName}.meta.json`), "utf8");
|
|
10857
9768
|
const parsed = JSON.parse(raw);
|
|
10858
9769
|
if (typeof parsed?.filename === "string") {
|
|
10859
9770
|
return { filename: parsed.filename, mimeType: typeof parsed.mimeType === "string" ? parsed.mimeType : void 0 };
|
|
@@ -10864,7 +9775,7 @@ async function readMeta(absDir, baseName) {
|
|
|
10864
9775
|
}
|
|
10865
9776
|
async function readAccountNames() {
|
|
10866
9777
|
const map = /* @__PURE__ */ new Map();
|
|
10867
|
-
const accountsDir =
|
|
9778
|
+
const accountsDir = resolve16(DATA_ROOT, "accounts");
|
|
10868
9779
|
let names;
|
|
10869
9780
|
try {
|
|
10870
9781
|
names = await readdir2(accountsDir);
|
|
@@ -10873,7 +9784,7 @@ async function readAccountNames() {
|
|
|
10873
9784
|
}
|
|
10874
9785
|
for (const name of names) {
|
|
10875
9786
|
if (!UUID_RE4.test(name)) continue;
|
|
10876
|
-
const configPath2 =
|
|
9787
|
+
const configPath2 = resolve16(accountsDir, name, "account.json");
|
|
10877
9788
|
try {
|
|
10878
9789
|
const raw = await readFile4(configPath2, "utf8");
|
|
10879
9790
|
const parsed = JSON.parse(raw);
|
|
@@ -10891,7 +9802,7 @@ async function readAccountNames() {
|
|
|
10891
9802
|
}
|
|
10892
9803
|
async function enrich(absolute, entry, accountNames) {
|
|
10893
9804
|
if (entry.kind === "directory" && UUID_RE4.test(entry.name)) {
|
|
10894
|
-
const meta = await readMeta(
|
|
9805
|
+
const meta = await readMeta(join8(absolute, entry.name), entry.name);
|
|
10895
9806
|
if (meta?.filename) {
|
|
10896
9807
|
entry.displayName = meta.filename;
|
|
10897
9808
|
entry.mimeType = meta.mimeType;
|
|
@@ -10950,7 +9861,7 @@ app22.get("/", requireAdminSession, async (c) => {
|
|
|
10950
9861
|
continue;
|
|
10951
9862
|
}
|
|
10952
9863
|
try {
|
|
10953
|
-
const entryPath =
|
|
9864
|
+
const entryPath = join8(absolute, name);
|
|
10954
9865
|
const s = await stat4(entryPath);
|
|
10955
9866
|
entries.push({
|
|
10956
9867
|
name,
|
|
@@ -11005,7 +9916,7 @@ app22.get("/download", requireAdminSession, async (c) => {
|
|
|
11005
9916
|
if (!info.isFile()) {
|
|
11006
9917
|
return c.json({ error: "Path is not a file" }, 400);
|
|
11007
9918
|
}
|
|
11008
|
-
const filename =
|
|
9919
|
+
const filename = basename4(absolute);
|
|
11009
9920
|
const mimeType = detectMimeType(absolute);
|
|
11010
9921
|
const nodeStream = createReadStream3(absolute);
|
|
11011
9922
|
const webStream = Readable2.toWeb(nodeStream);
|
|
@@ -11062,10 +9973,10 @@ app22.post("/upload", requireAdminSession, async (c) => {
|
|
|
11062
9973
|
error: `Unsupported file type: "${file.type}". Supported: ${[...SUPPORTED_MIME_TYPES].join(", ")}.`
|
|
11063
9974
|
}, 422);
|
|
11064
9975
|
}
|
|
11065
|
-
const safeName =
|
|
9976
|
+
const safeName = basename4(file.name).replace(/[\0/\\]/g, "_");
|
|
11066
9977
|
const finalName = `${Date.now()}-${safeName}`;
|
|
11067
|
-
const destDir =
|
|
11068
|
-
const destPath =
|
|
9978
|
+
const destDir = resolve16(DATA_ROOT, "uploads", accountId);
|
|
9979
|
+
const destPath = resolve16(destDir, finalName);
|
|
11069
9980
|
try {
|
|
11070
9981
|
await mkdir3(destDir, { recursive: true });
|
|
11071
9982
|
const dataRootReal = realpathSync4(DATA_ROOT);
|
|
@@ -11107,7 +10018,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11107
10018
|
return c.json({ error: resolution.error }, resolution.status);
|
|
11108
10019
|
}
|
|
11109
10020
|
const { absolute, relative: relPath2 } = resolution;
|
|
11110
|
-
const base =
|
|
10021
|
+
const base = basename4(absolute);
|
|
11111
10022
|
const segments = relPath2.split("/").filter(Boolean);
|
|
11112
10023
|
if (base === "account.json" || segments.includes(".git")) {
|
|
11113
10024
|
console.error(`[data] file-delete blocked path="${relPath2}" reason="protected"`);
|
|
@@ -11123,7 +10034,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11123
10034
|
}
|
|
11124
10035
|
const dot = base.lastIndexOf(".");
|
|
11125
10036
|
const stem = dot === -1 ? base : base.slice(0, dot);
|
|
11126
|
-
const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ?
|
|
10037
|
+
const sidecarPath = UUID_RE4.test(stem) && base !== `${stem}.meta.json` ? join8(dirname5(absolute), `${stem}.meta.json`) : null;
|
|
11127
10038
|
await unlink2(absolute);
|
|
11128
10039
|
if (sidecarPath) {
|
|
11129
10040
|
try {
|
|
@@ -11160,7 +10071,7 @@ app22.delete("/", requireAdminSession, async (c) => {
|
|
|
11160
10071
|
var files_default = app22;
|
|
11161
10072
|
|
|
11162
10073
|
// ../lib/graph-search/src/index.ts
|
|
11163
|
-
var
|
|
10074
|
+
var import_dist2 = __toESM(require_dist2());
|
|
11164
10075
|
import { int } from "neo4j-driver";
|
|
11165
10076
|
var VECTOR_WEIGHT = 0.7;
|
|
11166
10077
|
var BM25_WEIGHT = 0.3;
|
|
@@ -11215,7 +10126,7 @@ async function bm25Only(session, params) {
|
|
|
11215
10126
|
${scopeClause}
|
|
11216
10127
|
${agentClause}
|
|
11217
10128
|
${labelClause}
|
|
11218
|
-
AND ${(0,
|
|
10129
|
+
AND ${(0, import_dist2.notTrashed)("node")}
|
|
11219
10130
|
${kwClause}
|
|
11220
10131
|
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
11221
10132
|
ORDER BY score DESC
|
|
@@ -11305,7 +10216,7 @@ async function hybrid(session, embed2, params) {
|
|
|
11305
10216
|
WHERE node.accountId = $accountId
|
|
11306
10217
|
${scopeClause}
|
|
11307
10218
|
${agentClause}
|
|
11308
|
-
AND ${(0,
|
|
10219
|
+
AND ${(0, import_dist2.notTrashed)("node")}
|
|
11309
10220
|
${keywordClause}
|
|
11310
10221
|
RETURN node, score, labels(node) AS nodeLabels, elementId(node) AS nodeId
|
|
11311
10222
|
ORDER BY score DESC
|
|
@@ -11366,7 +10277,7 @@ async function hybrid(session, embed2, params) {
|
|
|
11366
10277
|
const propResult = await session.run(
|
|
11367
10278
|
`MATCH (node)
|
|
11368
10279
|
WHERE node.accountId = $accountId
|
|
11369
|
-
AND ${(0,
|
|
10280
|
+
AND ${(0, import_dist2.notTrashed)("node")}
|
|
11370
10281
|
AND node.keywords IS NOT NULL
|
|
11371
10282
|
AND ANY(kw IN $kwSubs WHERE ANY(nk IN node.keywords WHERE toLower(nk) = kw))
|
|
11372
10283
|
${propScopeClause}
|
|
@@ -11423,7 +10334,7 @@ async function hybrid(session, embed2, params) {
|
|
|
11423
10334
|
`UNWIND $nodeIds AS nid
|
|
11424
10335
|
MATCH (n)-[r]-(related)
|
|
11425
10336
|
WHERE elementId(n) = nid
|
|
11426
|
-
AND ${(0,
|
|
10337
|
+
AND ${(0, import_dist2.notTrashed)("related")}
|
|
11427
10338
|
${expandScopeClause}
|
|
11428
10339
|
${expandAgentClause}
|
|
11429
10340
|
WITH nid, n, r, related
|
|
@@ -11674,11 +10585,6 @@ var GRAPH_LABEL_COLOURS = {
|
|
|
11674
10585
|
// confusion doesn't apply, but kept distinguishable for the legend)
|
|
11675
10586
|
Email: "#6F7F4A",
|
|
11676
10587
|
EmailAccount: "#91A063",
|
|
11677
|
-
// Review signals (Task 626 — previously written by review-detector but
|
|
11678
|
-
// unregistered here, producing an `unknown label` 400 whenever the
|
|
11679
|
-
// filter popover advertised the label. Muted brick — still reads as
|
|
11680
|
-
// alert against the cream background but doesn't shout fire-engine red.)
|
|
11681
|
-
ReviewAlert: "#A85C5C",
|
|
11682
10588
|
// Public-agent projection (Task 837) — burnished bronze sits between
|
|
11683
10589
|
// people-terracotta and email-moss but shares neither hue, signalling
|
|
11684
10590
|
// "operator-defined persona that traverses to KnowledgeDocuments,
|
|
@@ -12603,8 +11509,8 @@ var adherence_default = app30;
|
|
|
12603
11509
|
// server/routes/admin/sidebar-artefacts.ts
|
|
12604
11510
|
import neo4j3 from "neo4j-driver";
|
|
12605
11511
|
import { readFile as readFile5, readdir as readdir3, stat as stat5 } from "fs/promises";
|
|
12606
|
-
import { resolve as
|
|
12607
|
-
import { existsSync as
|
|
11512
|
+
import { resolve as resolve17, relative as relative2, isAbsolute } from "path";
|
|
11513
|
+
import { existsSync as existsSync18 } from "fs";
|
|
12608
11514
|
var LIMIT = 50;
|
|
12609
11515
|
var TEXT_MIME_PREFIXES = ["text/", "application/json", "application/markdown"];
|
|
12610
11516
|
var ADMIN_AGENT_FILES = ["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"];
|
|
@@ -12620,7 +11526,7 @@ app31.get("/", requireAdminSession, async (c) => {
|
|
|
12620
11526
|
if (docs === null) {
|
|
12621
11527
|
return c.json({ error: "Failed to load artefacts" }, 500);
|
|
12622
11528
|
}
|
|
12623
|
-
const accountDir =
|
|
11529
|
+
const accountDir = resolve17(ACCOUNTS_DIR, accountId);
|
|
12624
11530
|
const agents = await fetchAgentTemplateRows(accountDir);
|
|
12625
11531
|
const artefacts = [...docs, ...agents].sort(
|
|
12626
11532
|
(a, b) => (b.updatedAt ?? "").localeCompare(a.updatedAt ?? "")
|
|
@@ -12683,8 +11589,8 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12683
11589
|
logSkip(displayName, "non-text-mime", mimeType);
|
|
12684
11590
|
return { content: "", skipReason: "non-text-mime" };
|
|
12685
11591
|
}
|
|
12686
|
-
const accountDir =
|
|
12687
|
-
const dir =
|
|
11592
|
+
const accountDir = resolve17(ATTACHMENTS_ROOT, accountId);
|
|
11593
|
+
const dir = resolve17(accountDir, attachmentId);
|
|
12688
11594
|
try {
|
|
12689
11595
|
validateFilePathInAccount(dir, accountDir);
|
|
12690
11596
|
} catch {
|
|
@@ -12698,7 +11604,7 @@ async function readArtefactContent(accountId, attachmentId, mimeType, displayNam
|
|
|
12698
11604
|
logSkip(displayName, "missing-on-disk", mimeType);
|
|
12699
11605
|
return { content: "", skipReason: "missing-on-disk" };
|
|
12700
11606
|
}
|
|
12701
|
-
return { content: await readFile5(
|
|
11607
|
+
return { content: await readFile5(resolve17(dir, dataFile), "utf-8"), skipReason: null };
|
|
12702
11608
|
} catch (err) {
|
|
12703
11609
|
const message = err instanceof Error ? err.message : String(err);
|
|
12704
11610
|
console.error(`[admin/sidebar-artefacts] read-failed attachmentId=${attachmentId.slice(0, 8)} error="${message}"`);
|
|
@@ -12714,8 +11620,8 @@ function logSkip(name, reason, mimeType) {
|
|
|
12714
11620
|
async function fetchAgentTemplateRows(accountDir) {
|
|
12715
11621
|
const rows = [];
|
|
12716
11622
|
for (const filename of ADMIN_AGENT_FILES) {
|
|
12717
|
-
const overridePath =
|
|
12718
|
-
const bundledPath =
|
|
11623
|
+
const overridePath = resolve17(accountDir, "agents", "admin", filename);
|
|
11624
|
+
const bundledPath = resolve17(PLATFORM_ROOT, "templates", "agents", "admin", filename);
|
|
12719
11625
|
const labelStem = filename.replace(/\.md$/, "");
|
|
12720
11626
|
const row = await readAgentTemplateRow({
|
|
12721
11627
|
id: `agent-template:admin:${filename}`,
|
|
@@ -12729,12 +11635,12 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
12729
11635
|
});
|
|
12730
11636
|
if (row) rows.push(row);
|
|
12731
11637
|
}
|
|
12732
|
-
const overrideDir =
|
|
12733
|
-
const bundledDir =
|
|
11638
|
+
const overrideDir = resolve17(accountDir, "specialists", "agents");
|
|
11639
|
+
const bundledDir = resolve17(PLATFORM_ROOT, "templates", "specialists", "agents");
|
|
12734
11640
|
const specialistNames = await unionSpecialistFilenames(overrideDir, bundledDir);
|
|
12735
11641
|
for (const filename of specialistNames) {
|
|
12736
|
-
const overridePath =
|
|
12737
|
-
const bundledPath =
|
|
11642
|
+
const overridePath = resolve17(overrideDir, filename);
|
|
11643
|
+
const bundledPath = resolve17(bundledDir, filename);
|
|
12738
11644
|
const row = await readAgentTemplateRow({
|
|
12739
11645
|
id: `agent-template:specialist:${filename}`,
|
|
12740
11646
|
displayName: filename.replace(/\.md$/, ""),
|
|
@@ -12752,7 +11658,7 @@ async function fetchAgentTemplateRows(accountDir) {
|
|
|
12752
11658
|
async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
12753
11659
|
const names = /* @__PURE__ */ new Set();
|
|
12754
11660
|
for (const dir of [overrideDir, bundledDir]) {
|
|
12755
|
-
if (!
|
|
11661
|
+
if (!existsSync18(dir)) continue;
|
|
12756
11662
|
try {
|
|
12757
11663
|
const entries = await readdir3(dir);
|
|
12758
11664
|
for (const entry of entries) {
|
|
@@ -12767,7 +11673,7 @@ async function unionSpecialistFilenames(overrideDir, bundledDir) {
|
|
|
12767
11673
|
}
|
|
12768
11674
|
async function readAgentTemplateRow(inp) {
|
|
12769
11675
|
let chosenPath = null;
|
|
12770
|
-
if (
|
|
11676
|
+
if (existsSync18(inp.overridePath)) {
|
|
12771
11677
|
try {
|
|
12772
11678
|
validateFilePathInAccount(inp.overridePath, inp.overrideRoot);
|
|
12773
11679
|
chosenPath = inp.overridePath;
|
|
@@ -12778,7 +11684,7 @@ async function readAgentTemplateRow(inp) {
|
|
|
12778
11684
|
);
|
|
12779
11685
|
return null;
|
|
12780
11686
|
}
|
|
12781
|
-
} else if (
|
|
11687
|
+
} else if (existsSync18(inp.bundledPath)) {
|
|
12782
11688
|
if (!isWithin(inp.bundledPath, inp.bundledRoot)) {
|
|
12783
11689
|
console.error(
|
|
12784
11690
|
`[admin/sidebar-artefacts] agent-template-read-failed agent=${inp.displayName} kind=${inp.logName} error="bundled path outside PLATFORM_ROOT"`
|
|
@@ -12819,8 +11725,8 @@ var sidebar_artefacts_default = app31;
|
|
|
12819
11725
|
|
|
12820
11726
|
// server/routes/admin/sidebar-artefact-save.ts
|
|
12821
11727
|
import { mkdir as mkdir4, readdir as readdir4, stat as stat6, writeFile as writeFile5 } from "fs/promises";
|
|
12822
|
-
import { resolve as
|
|
12823
|
-
import { existsSync as
|
|
11728
|
+
import { resolve as resolve18 } from "path";
|
|
11729
|
+
import { existsSync as existsSync19 } from "fs";
|
|
12824
11730
|
var ADMIN_AGENT_FILES2 = /* @__PURE__ */ new Set(["IDENTITY.md", "SOUL.md", "KNOWLEDGE.md"]);
|
|
12825
11731
|
var UUID_RE5 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12826
11732
|
var app32 = new Hono();
|
|
@@ -12832,7 +11738,7 @@ app32.post("/", requireAdminSession, async (c) => {
|
|
|
12832
11738
|
if (!body || typeof body.id !== "string" || typeof body.content !== "string") {
|
|
12833
11739
|
return c.json({ error: "id and content required" }, 400);
|
|
12834
11740
|
}
|
|
12835
|
-
const accountDir =
|
|
11741
|
+
const accountDir = resolve18(ACCOUNTS_DIR, accountId);
|
|
12836
11742
|
const resolved = await resolveSavePath(body.id, accountId, accountDir);
|
|
12837
11743
|
if (resolved.kind === "reject") {
|
|
12838
11744
|
console.error(
|
|
@@ -12873,22 +11779,22 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
12873
11779
|
if (role !== "admin" || !ADMIN_AGENT_FILES2.has(filename)) {
|
|
12874
11780
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
12875
11781
|
}
|
|
12876
|
-
const parent =
|
|
11782
|
+
const parent = resolve18(accountDir, "agents", "admin");
|
|
12877
11783
|
await mkdir4(parent, { recursive: true });
|
|
12878
11784
|
try {
|
|
12879
11785
|
validateFilePathInAccount(parent, accountDir);
|
|
12880
11786
|
} catch {
|
|
12881
11787
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
12882
11788
|
}
|
|
12883
|
-
return { kind: "admin-template", path:
|
|
11789
|
+
return { kind: "admin-template", path: resolve18(parent, filename) };
|
|
12884
11790
|
}
|
|
12885
11791
|
if (UUID_RE5.test(id)) {
|
|
12886
|
-
const dir =
|
|
12887
|
-
if (!
|
|
11792
|
+
const dir = resolve18(ATTACHMENTS_ROOT, accountId, id);
|
|
11793
|
+
if (!existsSync19(dir)) {
|
|
12888
11794
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
12889
11795
|
}
|
|
12890
11796
|
try {
|
|
12891
|
-
validateFilePathInAccount(dir,
|
|
11797
|
+
validateFilePathInAccount(dir, resolve18(ATTACHMENTS_ROOT, accountId));
|
|
12892
11798
|
} catch {
|
|
12893
11799
|
return { kind: "reject", status: 400, reason: "containment-rejected" };
|
|
12894
11800
|
}
|
|
@@ -12897,7 +11803,7 @@ async function resolveSavePath(id, accountId, accountDir) {
|
|
|
12897
11803
|
if (!dataFile) {
|
|
12898
11804
|
return { kind: "reject", status: 400, reason: "not-found" };
|
|
12899
11805
|
}
|
|
12900
|
-
return { kind: "knowledge-doc", path:
|
|
11806
|
+
return { kind: "knowledge-doc", path: resolve18(dir, dataFile) };
|
|
12901
11807
|
}
|
|
12902
11808
|
return { kind: "reject", status: 400, reason: "invalid-id" };
|
|
12903
11809
|
}
|
|
@@ -12908,8 +11814,8 @@ var sidebar_artefact_save_default = app32;
|
|
|
12908
11814
|
|
|
12909
11815
|
// server/routes/admin/sidebar-artefact-content.ts
|
|
12910
11816
|
import { readFile as readFile6, readdir as readdir5 } from "fs/promises";
|
|
12911
|
-
import { existsSync as
|
|
12912
|
-
import { resolve as
|
|
11817
|
+
import { existsSync as existsSync20 } from "fs";
|
|
11818
|
+
import { resolve as resolve19 } from "path";
|
|
12913
11819
|
var UUID_RE6 = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
12914
11820
|
var app33 = new Hono();
|
|
12915
11821
|
app33.get("/", requireAdminSession, async (c) => {
|
|
@@ -12921,14 +11827,14 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
12921
11827
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12922
11828
|
return new Response("Not found", { status: 404 });
|
|
12923
11829
|
}
|
|
12924
|
-
const dir =
|
|
12925
|
-
if (!
|
|
11830
|
+
const dir = resolve19(ATTACHMENTS_ROOT, accountId, id);
|
|
11831
|
+
if (!existsSync20(dir)) {
|
|
12926
11832
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12927
11833
|
return new Response("Not found", { status: 404 });
|
|
12928
11834
|
}
|
|
12929
11835
|
let meta;
|
|
12930
11836
|
try {
|
|
12931
|
-
meta = JSON.parse(await readFile6(
|
|
11837
|
+
meta = JSON.parse(await readFile6(resolve19(dir, `${id}.meta.json`), "utf-8"));
|
|
12932
11838
|
} catch {
|
|
12933
11839
|
console.error(`[admin/sidebar-artefact-content] not-found id=${id.slice(0, 8)}`);
|
|
12934
11840
|
return new Response("Not found", { status: 404 });
|
|
@@ -12940,7 +11846,7 @@ app33.get("/", requireAdminSession, async (c) => {
|
|
|
12940
11846
|
return new Response("Not found", { status: 404 });
|
|
12941
11847
|
}
|
|
12942
11848
|
const start = Date.now();
|
|
12943
|
-
const buffer = await readFile6(
|
|
11849
|
+
const buffer = await readFile6(resolve19(dir, dataFile));
|
|
12944
11850
|
const ms = Date.now() - start;
|
|
12945
11851
|
console.log(
|
|
12946
11852
|
`[admin/sidebar-artefact-content] account=${accountId} id=${id.slice(0, 8)} mime=${meta.mimeType} bytes=${buffer.length} ms=${ms}`
|
|
@@ -12984,8 +11890,8 @@ app34.route("/sidebar-artefact-content", sidebar_artefact_content_default);
|
|
|
12984
11890
|
var admin_default = app34;
|
|
12985
11891
|
|
|
12986
11892
|
// server/routes/sites.ts
|
|
12987
|
-
import { existsSync as
|
|
12988
|
-
import { resolve as
|
|
11893
|
+
import { existsSync as existsSync21, readFileSync as readFileSync15, realpathSync as realpathSync5, statSync as statSync5 } from "fs";
|
|
11894
|
+
import { resolve as resolve20 } from "path";
|
|
12989
11895
|
var SAFE_SEG_RE = /^[a-z0-9_][a-z0-9_.-]{0,99}$/i;
|
|
12990
11896
|
var MIME = {
|
|
12991
11897
|
".html": "text/html; charset=utf-8",
|
|
@@ -13043,28 +11949,28 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13043
11949
|
}
|
|
13044
11950
|
segments.push(seg);
|
|
13045
11951
|
}
|
|
13046
|
-
const rootDir =
|
|
13047
|
-
let filePath = segments.length === 0 ? rootDir :
|
|
11952
|
+
const rootDir = resolve20(account.accountDir, "sites");
|
|
11953
|
+
let filePath = segments.length === 0 ? rootDir : resolve20(rootDir, ...segments);
|
|
13048
11954
|
if (filePath !== rootDir && !filePath.startsWith(rootDir + "/")) {
|
|
13049
11955
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13050
11956
|
return c.text("Forbidden", 403);
|
|
13051
11957
|
}
|
|
13052
11958
|
let stat7;
|
|
13053
11959
|
try {
|
|
13054
|
-
stat7 =
|
|
11960
|
+
stat7 = existsSync21(filePath) ? statSync5(filePath) : null;
|
|
13055
11961
|
} catch {
|
|
13056
11962
|
stat7 = null;
|
|
13057
11963
|
}
|
|
13058
11964
|
if (stat7?.isDirectory()) {
|
|
13059
|
-
filePath =
|
|
11965
|
+
filePath = resolve20(filePath, "index.html");
|
|
13060
11966
|
} else if (stat7 === null && isDirRequest) {
|
|
13061
|
-
filePath =
|
|
11967
|
+
filePath = resolve20(filePath, "index.html");
|
|
13062
11968
|
}
|
|
13063
11969
|
if (!filePath.startsWith(rootDir + "/")) {
|
|
13064
11970
|
console.error(`[sites] path-traversal-rejected path=${reqPath} reason=escape status=403`);
|
|
13065
11971
|
return c.text("Forbidden", 403);
|
|
13066
11972
|
}
|
|
13067
|
-
if (!
|
|
11973
|
+
if (!existsSync21(filePath)) {
|
|
13068
11974
|
console.error(`[sites] not-found path=${reqPath} status=404`);
|
|
13069
11975
|
return c.text("Not found", 404);
|
|
13070
11976
|
}
|
|
@@ -13083,7 +11989,7 @@ app35.get("/:rel{.*}", (c) => {
|
|
|
13083
11989
|
}
|
|
13084
11990
|
let body;
|
|
13085
11991
|
try {
|
|
13086
|
-
body =
|
|
11992
|
+
body = readFileSync15(realPath);
|
|
13087
11993
|
} catch (err) {
|
|
13088
11994
|
const code = err?.code;
|
|
13089
11995
|
if (code === "EISDIR") {
|
|
@@ -13215,14 +12121,14 @@ function clientFrom(c) {
|
|
|
13215
12121
|
);
|
|
13216
12122
|
}
|
|
13217
12123
|
var PLATFORM_ROOT7 = process.env.MAXY_PLATFORM_ROOT || "";
|
|
13218
|
-
var BRAND_JSON_PATH = PLATFORM_ROOT7 ?
|
|
12124
|
+
var BRAND_JSON_PATH = PLATFORM_ROOT7 ? join9(PLATFORM_ROOT7, "config", "brand.json") : "";
|
|
13219
12125
|
var BRAND = { productName: "Maxy", hostname: "maxy", configDir: ".maxy", domain: "getmaxy.com" };
|
|
13220
|
-
if (BRAND_JSON_PATH && !
|
|
12126
|
+
if (BRAND_JSON_PATH && !existsSync22(BRAND_JSON_PATH)) {
|
|
13221
12127
|
console.error(`[brand] WARNING: brand.json not found at ${BRAND_JSON_PATH} \u2014 using Maxy defaults`);
|
|
13222
12128
|
}
|
|
13223
|
-
if (BRAND_JSON_PATH &&
|
|
12129
|
+
if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
|
|
13224
12130
|
try {
|
|
13225
|
-
const parsed = JSON.parse(
|
|
12131
|
+
const parsed = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
|
|
13226
12132
|
BRAND = { ...BRAND, ...parsed };
|
|
13227
12133
|
} catch (err) {
|
|
13228
12134
|
console.error(`[brand] Failed to parse brand.json: ${err.message}`);
|
|
@@ -13241,11 +12147,11 @@ var brandLoginOpts = {
|
|
|
13241
12147
|
bodyFont: BRAND.defaultFonts?.body,
|
|
13242
12148
|
logoContainsName: !!BRAND.logoContainsName
|
|
13243
12149
|
};
|
|
13244
|
-
var ALIAS_DOMAINS_PATH2 =
|
|
12150
|
+
var ALIAS_DOMAINS_PATH2 = join9(homedir2(), BRAND.configDir, "alias-domains.json");
|
|
13245
12151
|
function loadAliasDomains() {
|
|
13246
12152
|
try {
|
|
13247
|
-
if (!
|
|
13248
|
-
const parsed = JSON.parse(
|
|
12153
|
+
if (!existsSync22(ALIAS_DOMAINS_PATH2)) return null;
|
|
12154
|
+
const parsed = JSON.parse(readFileSync16(ALIAS_DOMAINS_PATH2, "utf-8"));
|
|
13249
12155
|
if (!Array.isArray(parsed)) {
|
|
13250
12156
|
console.error("[alias-domains] malformed alias-domains.json \u2014 expected array");
|
|
13251
12157
|
return null;
|
|
@@ -13585,20 +12491,20 @@ app36.get("/agent-assets/:slug/:filename", (c) => {
|
|
|
13585
12491
|
console.error(`[agent-assets] no-account slug=${slug} file=${filename}`);
|
|
13586
12492
|
return c.text("Not found", 404);
|
|
13587
12493
|
}
|
|
13588
|
-
const filePath =
|
|
13589
|
-
const expectedDir =
|
|
12494
|
+
const filePath = resolve21(account.accountDir, "agents", slug, "assets", filename);
|
|
12495
|
+
const expectedDir = resolve21(account.accountDir, "agents", slug, "assets");
|
|
13590
12496
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13591
12497
|
console.error(`[agent-assets] path-traversal-rejected slug=${slug} file=${filename}`);
|
|
13592
12498
|
return c.text("Forbidden", 403);
|
|
13593
12499
|
}
|
|
13594
|
-
if (!
|
|
12500
|
+
if (!existsSync22(filePath)) {
|
|
13595
12501
|
console.error(`[agent-assets] serve slug=${slug} file=${filename} status=404`);
|
|
13596
12502
|
return c.text("Not found", 404);
|
|
13597
12503
|
}
|
|
13598
12504
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13599
12505
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13600
12506
|
console.log(`[agent-assets] serve slug=${slug} file=${filename} status=200`);
|
|
13601
|
-
const body =
|
|
12507
|
+
const body = readFileSync16(filePath);
|
|
13602
12508
|
return c.body(body, 200, {
|
|
13603
12509
|
"Content-Type": contentType,
|
|
13604
12510
|
"Cache-Control": "public, max-age=3600"
|
|
@@ -13615,20 +12521,20 @@ app36.get("/generated/:filename", (c) => {
|
|
|
13615
12521
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13616
12522
|
return c.text("Not found", 404);
|
|
13617
12523
|
}
|
|
13618
|
-
const filePath =
|
|
13619
|
-
const expectedDir =
|
|
12524
|
+
const filePath = resolve21(account.accountDir, "generated", filename);
|
|
12525
|
+
const expectedDir = resolve21(account.accountDir, "generated");
|
|
13620
12526
|
if (!filePath.startsWith(expectedDir + "/")) {
|
|
13621
12527
|
console.error(`[generated] serve file=${filename} status=403`);
|
|
13622
12528
|
return c.text("Forbidden", 403);
|
|
13623
12529
|
}
|
|
13624
|
-
if (!
|
|
12530
|
+
if (!existsSync22(filePath)) {
|
|
13625
12531
|
console.error(`[generated] serve file=${filename} status=404`);
|
|
13626
12532
|
return c.text("Not found", 404);
|
|
13627
12533
|
}
|
|
13628
12534
|
const ext = "." + filename.split(".").pop()?.toLowerCase();
|
|
13629
12535
|
const contentType = IMAGE_MIME[ext] || "application/octet-stream";
|
|
13630
12536
|
console.log(`[generated] serve file=${filename} status=200`);
|
|
13631
|
-
const body =
|
|
12537
|
+
const body = readFileSync16(filePath);
|
|
13632
12538
|
return c.body(body, 200, {
|
|
13633
12539
|
"Content-Type": contentType,
|
|
13634
12540
|
"Cache-Control": "public, max-age=86400"
|
|
@@ -13638,9 +12544,9 @@ app36.route("/sites", sites_default);
|
|
|
13638
12544
|
var htmlCache = /* @__PURE__ */ new Map();
|
|
13639
12545
|
var brandLogoPath = "/brand/maxy-monochrome.png";
|
|
13640
12546
|
var brandIconPath = "/brand/maxy-monochrome.png";
|
|
13641
|
-
if (BRAND_JSON_PATH &&
|
|
12547
|
+
if (BRAND_JSON_PATH && existsSync22(BRAND_JSON_PATH)) {
|
|
13642
12548
|
try {
|
|
13643
|
-
const fullBrand = JSON.parse(
|
|
12549
|
+
const fullBrand = JSON.parse(readFileSync16(BRAND_JSON_PATH, "utf-8"));
|
|
13644
12550
|
if (fullBrand.assets?.logo) brandLogoPath = `/brand/${fullBrand.assets.logo}`;
|
|
13645
12551
|
brandIconPath = fullBrand.assets?.icon ? `/brand/${fullBrand.assets.icon}` : brandLogoPath;
|
|
13646
12552
|
} catch {
|
|
@@ -13657,9 +12563,9 @@ var brandScript = `<script>window.__BRAND__=${JSON.stringify({
|
|
|
13657
12563
|
function readInstalledVersion() {
|
|
13658
12564
|
try {
|
|
13659
12565
|
if (!PLATFORM_ROOT7) return "unknown";
|
|
13660
|
-
const versionFile =
|
|
13661
|
-
if (!
|
|
13662
|
-
const content =
|
|
12566
|
+
const versionFile = join9(PLATFORM_ROOT7, "config", `.${BRAND.hostname}-version`);
|
|
12567
|
+
if (!existsSync22(versionFile)) return "unknown";
|
|
12568
|
+
const content = readFileSync16(versionFile, "utf-8").trim();
|
|
13663
12569
|
return content || "unknown";
|
|
13664
12570
|
} catch {
|
|
13665
12571
|
return "unknown";
|
|
@@ -13700,7 +12606,7 @@ var clientErrorReporterScript = `<script>
|
|
|
13700
12606
|
function cachedHtml(file) {
|
|
13701
12607
|
let html = htmlCache.get(file);
|
|
13702
12608
|
if (!html) {
|
|
13703
|
-
html =
|
|
12609
|
+
html = readFileSync16(resolve21(process.cwd(), "public", file), "utf-8");
|
|
13704
12610
|
const productNameEsc = escapeHtml(BRAND.productName);
|
|
13705
12611
|
html = html.replace(/<title>([^<]*)<\/title>/, (_match, inner) => `<title>${escapeHtml(inner).replace(/Maxy/g, productNameEsc)}</title>`);
|
|
13706
12612
|
html = html.replace('href="/favicon.ico"', `href="${escapeHtml(brandFaviconPath)}"`);
|
|
@@ -13716,26 +12622,26 @@ ${clientErrorReporterScript}
|
|
|
13716
12622
|
}
|
|
13717
12623
|
var brandedHtmlCache = /* @__PURE__ */ new Map();
|
|
13718
12624
|
function loadBrandingCache(agentSlug) {
|
|
13719
|
-
const configDir2 =
|
|
12625
|
+
const configDir2 = join9(homedir2(), BRAND.configDir);
|
|
13720
12626
|
try {
|
|
13721
|
-
const accountJsonPath =
|
|
13722
|
-
if (!
|
|
13723
|
-
const account = JSON.parse(
|
|
12627
|
+
const accountJsonPath = join9(configDir2, "account.json");
|
|
12628
|
+
if (!existsSync22(accountJsonPath)) return null;
|
|
12629
|
+
const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
|
|
13724
12630
|
const accountId = account.accountId;
|
|
13725
12631
|
if (!accountId) return null;
|
|
13726
|
-
const cachePath =
|
|
13727
|
-
if (!
|
|
13728
|
-
return JSON.parse(
|
|
12632
|
+
const cachePath = join9(configDir2, "branding-cache", accountId, `${agentSlug}.json`);
|
|
12633
|
+
if (!existsSync22(cachePath)) return null;
|
|
12634
|
+
return JSON.parse(readFileSync16(cachePath, "utf-8"));
|
|
13729
12635
|
} catch {
|
|
13730
12636
|
return null;
|
|
13731
12637
|
}
|
|
13732
12638
|
}
|
|
13733
12639
|
function resolveDefaultSlug() {
|
|
13734
12640
|
try {
|
|
13735
|
-
const configDir2 =
|
|
13736
|
-
const accountJsonPath =
|
|
13737
|
-
if (!
|
|
13738
|
-
const account = JSON.parse(
|
|
12641
|
+
const configDir2 = join9(homedir2(), BRAND.configDir);
|
|
12642
|
+
const accountJsonPath = join9(configDir2, "account.json");
|
|
12643
|
+
if (!existsSync22(accountJsonPath)) return null;
|
|
12644
|
+
const account = JSON.parse(readFileSync16(accountJsonPath, "utf-8"));
|
|
13739
12645
|
return account.defaultAgent || null;
|
|
13740
12646
|
} catch {
|
|
13741
12647
|
return null;
|
|
@@ -13808,7 +12714,7 @@ app36.use("/vnc-popout.html", logViewerFetch);
|
|
|
13808
12714
|
app36.get("/vnc-popout.html", (c) => {
|
|
13809
12715
|
let html = htmlCache.get("vnc-popout.html");
|
|
13810
12716
|
if (!html) {
|
|
13811
|
-
html =
|
|
12717
|
+
html = readFileSync16(resolve21(process.cwd(), "public", "vnc-popout.html"), "utf-8");
|
|
13812
12718
|
const name = escapeHtml(BRAND.productName);
|
|
13813
12719
|
html = html.replace("<title>Browser \u2014 Maxy</title>", `<title>${name}</title>`);
|
|
13814
12720
|
html = html.replace("</head>", ` ${brandScript}
|
|
@@ -13898,8 +12804,8 @@ try {
|
|
|
13898
12804
|
(async () => {
|
|
13899
12805
|
try {
|
|
13900
12806
|
let userId = "";
|
|
13901
|
-
if (
|
|
13902
|
-
const users = JSON.parse(
|
|
12807
|
+
if (existsSync22(USERS_FILE)) {
|
|
12808
|
+
const users = JSON.parse(readFileSync16(USERS_FILE, "utf-8").trim() || "[]");
|
|
13903
12809
|
userId = users[0]?.userId ?? "";
|
|
13904
12810
|
}
|
|
13905
12811
|
await backfillNullUserIdConversations(userId);
|
|
@@ -13914,15 +12820,8 @@ try {
|
|
|
13914
12820
|
console.error(`[migration] runBootMigrations rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
13915
12821
|
}
|
|
13916
12822
|
})();
|
|
13917
|
-
(async () => {
|
|
13918
|
-
try {
|
|
13919
|
-
await startReviewDetector();
|
|
13920
|
-
} catch (err) {
|
|
13921
|
-
console.error(`[review] startReviewDetector rejected: ${err instanceof Error ? err.message : String(err)}`);
|
|
13922
|
-
}
|
|
13923
|
-
})();
|
|
13924
12823
|
startGraphHealthTimer();
|
|
13925
|
-
var configDirForWhatsApp =
|
|
12824
|
+
var configDirForWhatsApp = basename5(MAXY_DIR) || ".maxy";
|
|
13926
12825
|
var bootAccount = resolveAccount();
|
|
13927
12826
|
var bootAccountConfig = bootAccount?.config;
|
|
13928
12827
|
var bootPublicAgent = bootAccount ? resolvePublicAgent(bootAccount.accountDir, { accountId: bootAccount.accountId })?.slug ?? null : null;
|
|
@@ -13966,7 +12865,7 @@ if (bootAccountConfig?.whatsapp) {
|
|
|
13966
12865
|
}
|
|
13967
12866
|
init({
|
|
13968
12867
|
configDir: configDirForWhatsApp,
|
|
13969
|
-
platformRoot:
|
|
12868
|
+
platformRoot: resolve21(process.env.MAXY_PLATFORM_ROOT ?? join9(__dirname, "..")),
|
|
13970
12869
|
accountConfig: bootAccountConfig,
|
|
13971
12870
|
onMessage: async (msg) => {
|
|
13972
12871
|
try {
|
|
@@ -14103,11 +13002,6 @@ process.on("SIGTERM", async () => {
|
|
|
14103
13002
|
} catch (err) {
|
|
14104
13003
|
console.error(`[server] shutdown error: ${String(err)}`);
|
|
14105
13004
|
}
|
|
14106
|
-
try {
|
|
14107
|
-
await shutdownReviewDetector();
|
|
14108
|
-
} catch (err) {
|
|
14109
|
-
console.error(`[server] review detector shutdown error: ${String(err)}`);
|
|
14110
|
-
}
|
|
14111
13005
|
console.error("[server] graceful shutdown complete \u2014 exiting");
|
|
14112
13006
|
process.exit(0);
|
|
14113
13007
|
});
|