@rubytech/create-realagent 1.0.865 → 1.0.867
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/dist/index.d.ts +51 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-search/dist/index.js +77 -7
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/bm25-strong-bypass-threshold.test.ts +126 -0
- package/payload/platform/lib/graph-search/src/__tests__/vector-threshold.test.ts +170 -0
- package/payload/platform/lib/graph-search/src/index.ts +129 -9
- package/payload/platform/plugins/admin/skills/publish-site/SKILL.md +2 -0
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +2 -0
- package/payload/platform/templates/agents/admin/IDENTITY.md +2 -1
- package/payload/platform/templates/specialists/agents/content-producer.md +17 -3
- package/payload/platform/templates/specialists/agents/database-operator.md +1 -1
- package/payload/server/chunk-DHSBEMWW.js +11319 -0
- package/payload/server/chunk-FHNFKJZN.js +2143 -0
- package/payload/server/chunk-ND23BDBM.js +11312 -0
- package/payload/server/chunk-TOLLHW7W.js +1155 -0
- package/payload/server/chunk-UXLZ5Z3Y.js +667 -0
- package/payload/server/client-pool-2IUOSYDF.js +34 -0
- package/payload/server/cloudflare-task-tracker-OCFIVXEJ.js +20 -0
- package/payload/server/maxy-edge.js +5 -6
- package/payload/server/public/assets/{Checkbox-BySsatDO.js → Checkbox-B9hff9s8.js} +1 -1
- package/payload/server/public/assets/{admin-CCML_l4E.js → admin-Cpi6L_g7.js} +3 -3
- package/payload/server/public/assets/data-Da6iYRW1.js +1 -0
- package/payload/server/public/assets/graph-BHq-JYwV.js +1 -0
- package/payload/server/public/assets/{useAdminFetch-B3MO55eB.js → graph-labels-ChinGFwI.js} +1 -1
- package/payload/server/public/assets/{jsx-runtime-O5ef8xK8.css → jsx-runtime-CVA1ZrPS.css} +1 -1
- package/payload/server/public/assets/page-DqPf65sS.js +50 -0
- package/payload/server/public/assets/page-OVrxtgOZ.js +1 -0
- package/payload/server/public/assets/{public-DRrf63wm.js → public-CJN5KAiK.js} +1 -1
- package/payload/server/public/assets/{useVoiceRecorder-CR8gcELb.js → useVoiceRecorder-DyVx7e7a.js} +1 -1
- package/payload/server/public/data.html +5 -5
- package/payload/server/public/graph.html +6 -6
- package/payload/server/public/index.html +8 -8
- package/payload/server/public/public.html +5 -5
- package/payload/server/server.js +211 -165
- package/payload/server/public/assets/data-BuuqlV4L.js +0 -1
- package/payload/server/public/assets/graph-CtVITeok.js +0 -1
- package/payload/server/public/assets/page-Ddc_nKh8.js +0 -1
- package/payload/server/public/assets/page-IQBQoOdT.js +0 -50
- /package/payload/server/public/assets/{jsx-runtime-DnY0498s.js → jsx-runtime-nxP_2eNo.js} +0 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getSession
|
|
3
|
+
} from "./chunk-FHNFKJZN.js";
|
|
4
|
+
import {
|
|
5
|
+
__commonJS,
|
|
6
|
+
__toESM
|
|
7
|
+
} from "./chunk-JSBRDJBE.js";
|
|
8
|
+
|
|
9
|
+
// ../lib/graph-write/dist/audit.js
|
|
10
|
+
var require_audit = __commonJS({
|
|
11
|
+
"../lib/graph-write/dist/audit.js"(exports) {
|
|
12
|
+
"use strict";
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
14
|
+
exports.auditCypherWrite = auditCypherWrite;
|
|
15
|
+
exports.formatAuditLine = formatAuditLine;
|
|
16
|
+
var EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
17
|
+
var CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
|
|
18
|
+
var PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
|
|
19
|
+
function stripStringLiterals(cypher) {
|
|
20
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
21
|
+
}
|
|
22
|
+
function extractEdgeTypes(cleaned) {
|
|
23
|
+
const out = /* @__PURE__ */ new Set();
|
|
24
|
+
for (const m of cleaned.matchAll(EDGE_PATTERN)) {
|
|
25
|
+
for (const t of m[1].split("|")) {
|
|
26
|
+
const clean = t.trim();
|
|
27
|
+
if (clean)
|
|
28
|
+
out.add(clean);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return out;
|
|
32
|
+
}
|
|
33
|
+
function countCreateOrMergeNodes(cleaned) {
|
|
34
|
+
const matches = cleaned.match(CREATE_OR_MERGE_NODE);
|
|
35
|
+
return matches ? matches.length : 0;
|
|
36
|
+
}
|
|
37
|
+
function countProvenanceStamps(cleaned) {
|
|
38
|
+
const matches = cleaned.match(PROVENANCE_TOKEN);
|
|
39
|
+
return matches ? matches.length : 0;
|
|
40
|
+
}
|
|
41
|
+
function auditCypherWrite(input) {
|
|
42
|
+
const warnings = [];
|
|
43
|
+
const cleaned = stripStringLiterals(input.cypher);
|
|
44
|
+
const referencedTypes = extractEdgeTypes(cleaned);
|
|
45
|
+
for (const t of referencedTypes) {
|
|
46
|
+
if (!input.schema.relationshipTypes.has(t)) {
|
|
47
|
+
warnings.push({ kind: "unknown-type-warning", type: t });
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (input.nodesCreated > 0) {
|
|
51
|
+
const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
|
|
52
|
+
if (createOrMergeNodes > 0) {
|
|
53
|
+
const stamps = countProvenanceStamps(cleaned);
|
|
54
|
+
if (stamps < createOrMergeNodes) {
|
|
55
|
+
warnings.push({
|
|
56
|
+
kind: "missing-provenance-warning",
|
|
57
|
+
created: createOrMergeNodes,
|
|
58
|
+
stamped: stamps
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
if (input.orphanIds.length > 0) {
|
|
64
|
+
warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
|
|
65
|
+
}
|
|
66
|
+
return warnings;
|
|
67
|
+
}
|
|
68
|
+
function formatAuditLine(line) {
|
|
69
|
+
const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
|
|
70
|
+
switch (line.kind) {
|
|
71
|
+
case "accepted":
|
|
72
|
+
return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
|
|
73
|
+
case "orphan-warning":
|
|
74
|
+
return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
|
|
75
|
+
case "unknown-type-warning":
|
|
76
|
+
return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
|
|
77
|
+
case "missing-provenance-warning":
|
|
78
|
+
return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
// ../lib/graph-write/dist/index.js
|
|
85
|
+
var require_dist = __commonJS({
|
|
86
|
+
"../lib/graph-write/dist/index.js"(exports) {
|
|
87
|
+
"use strict";
|
|
88
|
+
var __createBinding = exports && exports.__createBinding || (Object.create ? (function(o, m, k, k2) {
|
|
89
|
+
if (k2 === void 0) k2 = k;
|
|
90
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
91
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
92
|
+
desc = { enumerable: true, get: function() {
|
|
93
|
+
return m[k];
|
|
94
|
+
} };
|
|
95
|
+
}
|
|
96
|
+
Object.defineProperty(o, k2, desc);
|
|
97
|
+
}) : (function(o, m, k, k2) {
|
|
98
|
+
if (k2 === void 0) k2 = k;
|
|
99
|
+
o[k2] = m[k];
|
|
100
|
+
}));
|
|
101
|
+
var __exportStar = exports && exports.__exportStar || function(m, exports2) {
|
|
102
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports2, p)) __createBinding(exports2, m, p);
|
|
103
|
+
};
|
|
104
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
105
|
+
exports.ACTION_PROVENANCE_LABELS = void 0;
|
|
106
|
+
exports.stampCreatedBy = stampCreatedBy;
|
|
107
|
+
exports.writeNodeWithEdges = writeNodeWithEdges2;
|
|
108
|
+
__exportStar(require_audit(), exports);
|
|
109
|
+
exports.ACTION_PROVENANCE_LABELS = /* @__PURE__ */ new Set([
|
|
110
|
+
"Person",
|
|
111
|
+
"UserProfile",
|
|
112
|
+
"AdminUser",
|
|
113
|
+
"Organization",
|
|
114
|
+
"LocalBusiness",
|
|
115
|
+
"CloudflareTunnel",
|
|
116
|
+
"CloudflareHostname"
|
|
117
|
+
]);
|
|
118
|
+
function requiresActionProvenance(labels) {
|
|
119
|
+
for (const label of labels) {
|
|
120
|
+
if (exports.ACTION_PROVENANCE_LABELS.has(label))
|
|
121
|
+
return true;
|
|
122
|
+
}
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
function findProducedFromTaskCandidates(relationships) {
|
|
126
|
+
return relationships.filter((r) => r.type === "PRODUCED" && r.direction === "incoming");
|
|
127
|
+
}
|
|
128
|
+
function stampCreatedBy(props, createdBy) {
|
|
129
|
+
return {
|
|
130
|
+
...props,
|
|
131
|
+
createdByAgent: createdBy.agent ?? "unknown",
|
|
132
|
+
createdBySession: createdBy.session ?? "unknown",
|
|
133
|
+
createdByTool: createdBy.tool ?? null,
|
|
134
|
+
createdBySource: createdBy.source ?? null
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
async function writeNodeWithEdges2(params) {
|
|
138
|
+
const { session, labels, props, relationships, createdBy } = params;
|
|
139
|
+
const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
|
|
140
|
+
const labelCsv = labels.join(",");
|
|
141
|
+
const isSystemBootstrap = (createdBy.agent ?? "") === "system";
|
|
142
|
+
if (!isSystemBootstrap) {
|
|
143
|
+
const accountId = props.accountId;
|
|
144
|
+
const expectedAccountId = params.expectedAccountId ?? process.env.ACCOUNT_ID;
|
|
145
|
+
if (typeof accountId !== "string" || !expectedAccountId || accountId !== expectedAccountId) {
|
|
146
|
+
const slice = typeof accountId === "string" ? accountId.slice(0, 8) : "missing";
|
|
147
|
+
process.stderr.write(`[graph-write] reject reason=invalid-account-id accountId=${slice} writer=${agentLabel}
|
|
148
|
+
`);
|
|
149
|
+
throw new Error(`Write doctrine violated: invalid-account-id (${slice}) \u2014 accountId must equal ACCOUNT_ID set by the spawning process. See .docs/neo4j.md "Account isolation invariant".`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
const reviewDigestActionTool = typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
|
|
153
|
+
if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
|
|
154
|
+
const actionToolField = reviewDigestActionTool ? "review-digest-compose" : "n/a";
|
|
155
|
+
process.stderr.write(`[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}
|
|
156
|
+
`);
|
|
157
|
+
throw new Error("Write doctrine violated: review-detector feature removed (Task 884) \u2014 `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed.");
|
|
158
|
+
}
|
|
159
|
+
if (!relationships || relationships.length < 1) {
|
|
160
|
+
process.stderr.write(`[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}
|
|
161
|
+
`);
|
|
162
|
+
throw new Error("Write doctrine violated: a node must be created with at least one relationship. See .docs/neo4j.md (Write doctrine).");
|
|
163
|
+
}
|
|
164
|
+
const labelStr = labels.map((l) => `\`${l.replace(/`/g, "")}\``).join(":");
|
|
165
|
+
const nodeProps = stampCreatedBy(props, createdBy);
|
|
166
|
+
return await session.executeWrite(async (tx) => {
|
|
167
|
+
const targetIds = relationships.map((r) => r.targetNodeId);
|
|
168
|
+
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 });
|
|
169
|
+
const labelsByTarget = /* @__PURE__ */ new Map();
|
|
170
|
+
for (const rec of check.records) {
|
|
171
|
+
labelsByTarget.set(rec.get("id"), rec.get("labels"));
|
|
172
|
+
}
|
|
173
|
+
const found = labelsByTarget.size;
|
|
174
|
+
const uniqueRequested = new Set(targetIds).size;
|
|
175
|
+
if (found !== uniqueRequested) {
|
|
176
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target labels=${labelCsv} agent=${agentLabel} requested=${uniqueRequested} found=${found}
|
|
177
|
+
`);
|
|
178
|
+
throw new Error(`Write doctrine violated: ${uniqueRequested - found} of ${uniqueRequested} relationship target(s) did not resolve (elementId mismatch). No node created.`);
|
|
179
|
+
}
|
|
180
|
+
let producedByTaskId = null;
|
|
181
|
+
if (requiresActionProvenance(labels) && (createdBy.agent ?? "") !== "system") {
|
|
182
|
+
const candidates = findProducedFromTaskCandidates(relationships);
|
|
183
|
+
const taskCandidates = candidates.filter((r) => {
|
|
184
|
+
const lbls = labelsByTarget.get(r.targetNodeId);
|
|
185
|
+
return Array.isArray(lbls) && lbls.includes("Task");
|
|
186
|
+
});
|
|
187
|
+
if (taskCandidates.length === 0) {
|
|
188
|
+
process.stderr.write(`[graph-write] reject reason=missing-action-provenance labels=${labelCsv} agent=${agentLabel}
|
|
189
|
+
`);
|
|
190
|
+
throw new Error(`missing-action-provenance: write to ${labelCsv} requires an inbound :PRODUCED edge from a :Task (createdBy.agent='${agentLabel}'). Pass producedByTaskId on the write \u2014 call task-create at the start of the flow and thread the returned taskId through every subsequent memory-write for one of these labels.`);
|
|
191
|
+
}
|
|
192
|
+
producedByTaskId = taskCandidates[0].targetNodeId;
|
|
193
|
+
}
|
|
194
|
+
let nodeRes;
|
|
195
|
+
try {
|
|
196
|
+
nodeRes = await tx.run(`CREATE (n:${labelStr} $props) RETURN elementId(n) AS nodeId, labels(n) AS nodeLabels`, { props: nodeProps });
|
|
197
|
+
} catch (err) {
|
|
198
|
+
const code = err?.code ?? "";
|
|
199
|
+
if (code === "Neo.ClientError.Schema.ConstraintValidationFailed" && labels.includes("UserProfile")) {
|
|
200
|
+
const accountIdProp = nodeProps.accountId;
|
|
201
|
+
const userIdProp = nodeProps.userId;
|
|
202
|
+
const acctSlice = typeof accountIdProp === "string" ? accountIdProp.slice(0, 8) : "unknown";
|
|
203
|
+
const userSlice = typeof userIdProp === "string" ? userIdProp.slice(0, 8) : "unknown";
|
|
204
|
+
process.stderr.write(`[graph-write] reject reason=user-profile-uniqueness-violation accountId=${acctSlice} userId=${userSlice} writer=${agentLabel}
|
|
205
|
+
`);
|
|
206
|
+
}
|
|
207
|
+
throw err;
|
|
208
|
+
}
|
|
209
|
+
const nodeId = nodeRes.records[0].get("nodeId");
|
|
210
|
+
const nodeLabels = nodeRes.records[0].get("nodeLabels");
|
|
211
|
+
let edgesCreated = 0;
|
|
212
|
+
for (const rel of relationships) {
|
|
213
|
+
const type = rel.type.replace(/`/g, "");
|
|
214
|
+
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)`;
|
|
215
|
+
const r = await tx.run(q, { from: nodeId, to: rel.targetNodeId });
|
|
216
|
+
const created = r.summary.counters.updates().relationshipsCreated;
|
|
217
|
+
if (created === 0) {
|
|
218
|
+
process.stderr.write(`[graph-write] reject reason=unresolved-target-on-create labels=${labelCsv} agent=${agentLabel} relType=${rel.type} targetId=${rel.targetNodeId}
|
|
219
|
+
`);
|
|
220
|
+
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.`);
|
|
221
|
+
}
|
|
222
|
+
edgesCreated += created;
|
|
223
|
+
}
|
|
224
|
+
if (edgesCreated !== relationships.length) {
|
|
225
|
+
process.stderr.write(`[graph-write] reject reason=edge-count-mismatch labels=${labelCsv} agent=${agentLabel} requested=${relationships.length} created=${edgesCreated}
|
|
226
|
+
`);
|
|
227
|
+
throw new Error(`Write doctrine violated: expected ${relationships.length} edges, created ${edgesCreated}. Transaction rolled back.`);
|
|
228
|
+
}
|
|
229
|
+
process.stderr.write(`[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedByTask=${producedByTaskId ?? "none"}
|
|
230
|
+
`);
|
|
231
|
+
return { nodeId, labels: nodeLabels, edgesCreated };
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// ../lib/task-secrets/dist/index.js
|
|
238
|
+
var require_dist2 = __commonJS({
|
|
239
|
+
"../lib/task-secrets/dist/index.js"(exports) {
|
|
240
|
+
"use strict";
|
|
241
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
242
|
+
exports.redactSecrets = redactSecrets2;
|
|
243
|
+
function redactSecrets2(payload, schema) {
|
|
244
|
+
if (!payload) {
|
|
245
|
+
return { redacted: {}, droppedFields: 0, droppedFieldNames: [] };
|
|
246
|
+
}
|
|
247
|
+
const secrets = new Set(schema?.secretFields ?? []);
|
|
248
|
+
const redacted = {};
|
|
249
|
+
const dropped = [];
|
|
250
|
+
for (const [key, value] of Object.entries(payload)) {
|
|
251
|
+
if (secrets.has(key)) {
|
|
252
|
+
dropped.push(key);
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
redacted[key] = value;
|
|
256
|
+
}
|
|
257
|
+
dropped.sort();
|
|
258
|
+
return { redacted, droppedFields: dropped.length, droppedFieldNames: dropped };
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
// app/lib/cloudflare-task-tracker.ts
|
|
264
|
+
import { readFileSync, existsSync } from "fs";
|
|
265
|
+
import { randomUUID } from "crypto";
|
|
266
|
+
var import_dist = __toESM(require_dist(), 1);
|
|
267
|
+
var import_dist2 = __toESM(require_dist2(), 1);
|
|
268
|
+
var CREATED_BY_AGENT = "cloudflare-setup-endpoint";
|
|
269
|
+
var TASK_KIND = "cloudflare-tunnel-login";
|
|
270
|
+
async function openCloudflareTask(params) {
|
|
271
|
+
const { accountId, conversationKey, inputsProvided, inputs, inputSchema, messageId } = params;
|
|
272
|
+
const taskId = randomUUID();
|
|
273
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
274
|
+
const session = getSession();
|
|
275
|
+
try {
|
|
276
|
+
const conv = await session.run(
|
|
277
|
+
`MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
278
|
+
{ conversationKey, accountId }
|
|
279
|
+
);
|
|
280
|
+
if (conv.records.length === 0) {
|
|
281
|
+
throw new Error(
|
|
282
|
+
`cloudflare-task-tracker: no Conversation with sessionKey=${conversationKey.slice(-8)} for accountId \u2014 refusing to create an orphan Task`
|
|
283
|
+
);
|
|
284
|
+
}
|
|
285
|
+
const conversationElementId = conv.records[0].get("id");
|
|
286
|
+
let messageElementId = null;
|
|
287
|
+
let raisedDuringTag = `raisedDuringConversation=${conversationKey.slice(-8)}`;
|
|
288
|
+
if (messageId) {
|
|
289
|
+
const convIdRow = await session.run(
|
|
290
|
+
`MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN c.conversationId AS conversationId LIMIT 1`,
|
|
291
|
+
{ conversationKey, accountId }
|
|
292
|
+
);
|
|
293
|
+
const conversationId = convIdRow.records.length > 0 ? convIdRow.records[0].get("conversationId") : null;
|
|
294
|
+
if (conversationId) {
|
|
295
|
+
const msgRow = await session.run(
|
|
296
|
+
`MATCH (m:Message {messageId: $messageId, accountId: $accountId, conversationId: $conversationId}) RETURN elementId(m) AS id LIMIT 1`,
|
|
297
|
+
{ messageId, accountId, conversationId }
|
|
298
|
+
);
|
|
299
|
+
if (msgRow.records.length > 0) {
|
|
300
|
+
messageElementId = msgRow.records[0].get("id");
|
|
301
|
+
raisedDuringTag = `raisedDuringMessage=${messageId.slice(0, 8)}`;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
const adminLabelStr = typeof inputs?.adminLabel === "string" ? inputs.adminLabel : "?";
|
|
306
|
+
const adminDomainStr = typeof inputs?.adminDomain === "string" ? inputs.adminDomain : "?";
|
|
307
|
+
const publicLabelStr = typeof inputs?.publicLabel === "string" ? inputs.publicLabel : "";
|
|
308
|
+
const publicDomainStr = typeof inputs?.publicDomain === "string" ? inputs.publicDomain : "";
|
|
309
|
+
const publicSegment = publicDomainStr ? `, public=${publicLabelStr}.${publicDomainStr}` : "";
|
|
310
|
+
const description = `Cloudflare setup for admin=${adminLabelStr}.${adminDomainStr}${publicSegment}`;
|
|
311
|
+
const props = {
|
|
312
|
+
taskId,
|
|
313
|
+
accountId,
|
|
314
|
+
name: "Cloudflare tunnel login + setup",
|
|
315
|
+
description,
|
|
316
|
+
status: "running",
|
|
317
|
+
priority: "normal",
|
|
318
|
+
kind: TASK_KIND,
|
|
319
|
+
inputsProvided,
|
|
320
|
+
startedAt: now,
|
|
321
|
+
createdAt: now,
|
|
322
|
+
updatedAt: now
|
|
323
|
+
};
|
|
324
|
+
if (inputs) {
|
|
325
|
+
if (!inputSchema) {
|
|
326
|
+
process.stderr.write(
|
|
327
|
+
`[task] redact-no-schema kind=${TASK_KIND} taskId=${taskId} fieldsCount=${Object.keys(inputs).length}
|
|
328
|
+
`
|
|
329
|
+
);
|
|
330
|
+
}
|
|
331
|
+
const { redacted, droppedFields, droppedFieldNames } = (0, import_dist2.redactSecrets)(inputs, inputSchema);
|
|
332
|
+
for (const [key, value] of Object.entries(redacted)) {
|
|
333
|
+
if (value === void 0 || value === null || value === "") continue;
|
|
334
|
+
props[`inputs.${key}`] = value;
|
|
335
|
+
}
|
|
336
|
+
if (droppedFields > 0) {
|
|
337
|
+
process.stderr.write(
|
|
338
|
+
`[task] redacted kind=${TASK_KIND} taskId=${taskId} droppedFields=${droppedFields} fields=${droppedFieldNames.join(",")}
|
|
339
|
+
`
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
const relationships = [
|
|
344
|
+
{
|
|
345
|
+
type: "RAISED_DURING",
|
|
346
|
+
direction: "outgoing",
|
|
347
|
+
targetNodeId: messageElementId ?? conversationElementId
|
|
348
|
+
}
|
|
349
|
+
];
|
|
350
|
+
const result = await (0, import_dist.writeNodeWithEdges)({
|
|
351
|
+
session,
|
|
352
|
+
labels: ["Task"],
|
|
353
|
+
props,
|
|
354
|
+
relationships,
|
|
355
|
+
createdBy: {
|
|
356
|
+
agent: CREATED_BY_AGENT,
|
|
357
|
+
session: conversationKey,
|
|
358
|
+
tool: "cloudflare-setup-endpoint"
|
|
359
|
+
}
|
|
360
|
+
});
|
|
361
|
+
process.stderr.write(
|
|
362
|
+
`[task] action-start kind=${TASK_KIND} taskId=${taskId} ${raisedDuringTag}
|
|
363
|
+
`
|
|
364
|
+
);
|
|
365
|
+
return { taskId, taskElementId: result.nodeId };
|
|
366
|
+
} finally {
|
|
367
|
+
await session.close();
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
async function appendCloudflareSteps(taskId, accountId, streamLogPath) {
|
|
371
|
+
if (!existsSync(streamLogPath)) return [];
|
|
372
|
+
let content;
|
|
373
|
+
try {
|
|
374
|
+
content = readFileSync(streamLogPath, "utf-8");
|
|
375
|
+
} catch {
|
|
376
|
+
return [];
|
|
377
|
+
}
|
|
378
|
+
const steps = [];
|
|
379
|
+
for (const line of content.split(/\r?\n/)) {
|
|
380
|
+
const m = line.match(/\bphase_line\s+setup-tunnel\s+step=(\S+)/);
|
|
381
|
+
if (m) steps.push(m[1]);
|
|
382
|
+
}
|
|
383
|
+
if (steps.length === 0) return [];
|
|
384
|
+
const session = getSession();
|
|
385
|
+
try {
|
|
386
|
+
await session.run(
|
|
387
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
388
|
+
SET t.steps = coalesce(t.steps, []) + $steps,
|
|
389
|
+
t.updatedAt = $updatedAt`,
|
|
390
|
+
{ taskId, accountId, steps, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
391
|
+
);
|
|
392
|
+
for (const step of steps) {
|
|
393
|
+
process.stderr.write(
|
|
394
|
+
`[task] action-step kind=${TASK_KIND} taskId=${taskId} step=${step}
|
|
395
|
+
`
|
|
396
|
+
);
|
|
397
|
+
}
|
|
398
|
+
return steps;
|
|
399
|
+
} finally {
|
|
400
|
+
await session.close();
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
async function completeCloudflareTask(params) {
|
|
404
|
+
const { taskId, taskElementId, accountId, conversationKey, tunnelId, tunnelName, hostnames, status, errorMessage } = params;
|
|
405
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
406
|
+
if (status === "failed" && (!errorMessage || errorMessage.trim().length === 0)) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"cloudflare-task-tracker: errorMessage is required when status='failed' (Task 885 process-provenance contract)."
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const session = getSession();
|
|
412
|
+
try {
|
|
413
|
+
if (status === "completed" && tunnelId && tunnelName) {
|
|
414
|
+
const conv = await session.run(
|
|
415
|
+
`MATCH (c:Conversation {sessionKey: $conversationKey, accountId: $accountId}) RETURN elementId(c) AS id LIMIT 1`,
|
|
416
|
+
{ conversationKey, accountId }
|
|
417
|
+
);
|
|
418
|
+
const conversationElementId = conv.records.length > 0 ? conv.records[0].get("id") : null;
|
|
419
|
+
const tunnelProps = {
|
|
420
|
+
accountId,
|
|
421
|
+
tunnelId,
|
|
422
|
+
tunnelName,
|
|
423
|
+
createdAt: now,
|
|
424
|
+
updatedAt: now
|
|
425
|
+
};
|
|
426
|
+
const tunnelRels = [
|
|
427
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId }
|
|
428
|
+
];
|
|
429
|
+
if (conversationElementId) {
|
|
430
|
+
tunnelRels.push({
|
|
431
|
+
type: "RAISED_DURING",
|
|
432
|
+
direction: "outgoing",
|
|
433
|
+
targetNodeId: conversationElementId
|
|
434
|
+
});
|
|
435
|
+
}
|
|
436
|
+
const tunnelWrite = await (0, import_dist.writeNodeWithEdges)({
|
|
437
|
+
session,
|
|
438
|
+
labels: ["CloudflareTunnel"],
|
|
439
|
+
props: tunnelProps,
|
|
440
|
+
relationships: tunnelRels,
|
|
441
|
+
createdBy: {
|
|
442
|
+
agent: CREATED_BY_AGENT,
|
|
443
|
+
session: conversationKey,
|
|
444
|
+
tool: "cloudflare-setup-endpoint"
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
for (const h of hostnames ?? []) {
|
|
448
|
+
const hostRels = [
|
|
449
|
+
{ type: "PRODUCED", direction: "incoming", targetNodeId: taskElementId },
|
|
450
|
+
{
|
|
451
|
+
type: "ROUTES_TO",
|
|
452
|
+
direction: "outgoing",
|
|
453
|
+
targetNodeId: tunnelWrite.nodeId
|
|
454
|
+
}
|
|
455
|
+
];
|
|
456
|
+
await (0, import_dist.writeNodeWithEdges)({
|
|
457
|
+
session,
|
|
458
|
+
labels: ["CloudflareHostname"],
|
|
459
|
+
props: {
|
|
460
|
+
accountId,
|
|
461
|
+
hostnameValue: h.hostnameValue,
|
|
462
|
+
tunnelId,
|
|
463
|
+
isApex: h.isApex,
|
|
464
|
+
createdAt: now,
|
|
465
|
+
updatedAt: now
|
|
466
|
+
},
|
|
467
|
+
relationships: hostRels,
|
|
468
|
+
createdBy: {
|
|
469
|
+
agent: CREATED_BY_AGENT,
|
|
470
|
+
session: conversationKey,
|
|
471
|
+
tool: "cloudflare-setup-endpoint"
|
|
472
|
+
}
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
const setClauses = [
|
|
477
|
+
"t.status = $status",
|
|
478
|
+
"t.completedAt = $now",
|
|
479
|
+
"t.updatedAt = $now"
|
|
480
|
+
];
|
|
481
|
+
const queryParams = { taskId, accountId, status, now };
|
|
482
|
+
if (status === "failed" && errorMessage) {
|
|
483
|
+
setClauses.push("t.errorMessage = $errorMessage");
|
|
484
|
+
queryParams.errorMessage = errorMessage;
|
|
485
|
+
}
|
|
486
|
+
if (status === "completed") {
|
|
487
|
+
setClauses.push("t.errorMessage = null");
|
|
488
|
+
if (tunnelId) {
|
|
489
|
+
setClauses.push("t.tunnelId = $tunnelId");
|
|
490
|
+
queryParams.tunnelId = tunnelId;
|
|
491
|
+
}
|
|
492
|
+
if (tunnelName) {
|
|
493
|
+
setClauses.push("t.tunnelName = $tunnelName");
|
|
494
|
+
queryParams.tunnelName = tunnelName;
|
|
495
|
+
}
|
|
496
|
+
if (hostnames && hostnames.length > 0) {
|
|
497
|
+
setClauses.push("t.hostnames = $hostnamesList");
|
|
498
|
+
queryParams.hostnamesList = hostnames.map((h) => h.hostnameValue);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const updateRes = await session.run(
|
|
502
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
503
|
+
SET ${setClauses.join(", ")}
|
|
504
|
+
RETURN size(coalesce(t.steps, [])) AS stepsCount, count(t) AS affected`,
|
|
505
|
+
queryParams
|
|
506
|
+
);
|
|
507
|
+
const stepsCount = updateRes.records[0]?.get("stepsCount")?.toNumber?.() ?? 0;
|
|
508
|
+
const affected = updateRes.records[0]?.get("affected")?.toNumber?.() ?? 0;
|
|
509
|
+
if (affected !== 1) {
|
|
510
|
+
throw new Error(
|
|
511
|
+
`cloudflare-task-tracker: completeCloudflareTask MATCH count=${affected} for taskId=${taskId} \u2014 Task missing or accountId mismatch`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
process.stderr.write(
|
|
515
|
+
`[task] action-done kind=${TASK_KIND} taskId=${taskId} status=${status} stepsCount=${stepsCount}
|
|
516
|
+
`
|
|
517
|
+
);
|
|
518
|
+
} finally {
|
|
519
|
+
await session.close();
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function readTunnelState(brandConfigDir) {
|
|
523
|
+
const statePath = `${process.env.HOME ?? ""}/${brandConfigDir}/cloudflared/tunnel.state`;
|
|
524
|
+
if (!existsSync(statePath)) return null;
|
|
525
|
+
try {
|
|
526
|
+
const parsed = JSON.parse(readFileSync(statePath, "utf-8"));
|
|
527
|
+
const tunnelId = typeof parsed.tunnelId === "string" ? parsed.tunnelId : null;
|
|
528
|
+
const tunnelName = typeof parsed.tunnelName === "string" ? parsed.tunnelName : null;
|
|
529
|
+
const domain = typeof parsed.domain === "string" ? parsed.domain : null;
|
|
530
|
+
if (!tunnelId || !tunnelName || !domain) return null;
|
|
531
|
+
return { tunnelId, tunnelName, domain };
|
|
532
|
+
} catch {
|
|
533
|
+
return null;
|
|
534
|
+
}
|
|
535
|
+
}
|
|
536
|
+
var CLOUDFLARE_TASK_DIAGNOSTICS = {
|
|
537
|
+
scriptExitedNonzero: "script-exited-nonzero",
|
|
538
|
+
noTunnelStateOnDisk: "no-tunnel-state-on-disk",
|
|
539
|
+
endpointDiedPreReconcile: "endpoint-died-pre-reconcile"
|
|
540
|
+
};
|
|
541
|
+
function resolveUnitGoneVerdict(args) {
|
|
542
|
+
const { tunnelState, expected } = args;
|
|
543
|
+
const tunnelStateFound = tunnelState !== null;
|
|
544
|
+
const nameSet = typeof expected.tunnelName === "string" && expected.tunnelName.length > 0;
|
|
545
|
+
const idSet = typeof expected.tunnelId === "string" && expected.tunnelId.length > 0;
|
|
546
|
+
const identityMatch = tunnelState !== null && (nameSet && tunnelState.tunnelName === expected.tunnelName || idSet && tunnelState.tunnelId === expected.tunnelId);
|
|
547
|
+
if (tunnelStateFound && identityMatch) {
|
|
548
|
+
return { kind: "close-completed", tunnelStateFound: true, identityMatch: true };
|
|
549
|
+
}
|
|
550
|
+
return { kind: "close-failed", tunnelStateFound, identityMatch };
|
|
551
|
+
}
|
|
552
|
+
var RECONCILE_DELAY_BUDGET_MS = 9e4;
|
|
553
|
+
var RECONCILE_HARD_AGE_MS = 60 * 60 * 1e3;
|
|
554
|
+
async function recoverRunningCloudflareTasks(accountId, brandConfigDir, conversationKeyForState) {
|
|
555
|
+
const session = getSession();
|
|
556
|
+
try {
|
|
557
|
+
const cutoff = new Date(Date.now() - RECONCILE_DELAY_BUDGET_MS).toISOString();
|
|
558
|
+
const stale = await session.run(
|
|
559
|
+
`MATCH (t:Task {accountId: $accountId, kind: $kind, status: 'running'})
|
|
560
|
+
WHERE t.startedAt < $cutoff
|
|
561
|
+
RETURN t.taskId AS taskId, elementId(t) AS taskElementId, t.startedAt AS startedAt
|
|
562
|
+
ORDER BY t.startedAt ASC`,
|
|
563
|
+
{ accountId, kind: TASK_KIND, cutoff }
|
|
564
|
+
);
|
|
565
|
+
const tunnelState = readTunnelState(brandConfigDir);
|
|
566
|
+
const tunnelStateFound = tunnelState !== null;
|
|
567
|
+
const resolved = [];
|
|
568
|
+
for (const record of stale.records) {
|
|
569
|
+
const taskId = record.get("taskId");
|
|
570
|
+
const taskElementId = record.get("taskElementId");
|
|
571
|
+
const startedAt = record.get("startedAt");
|
|
572
|
+
const ageMs = Date.now() - new Date(startedAt).getTime();
|
|
573
|
+
try {
|
|
574
|
+
if (tunnelStateFound && tunnelState) {
|
|
575
|
+
const tunnelExistsRow = await session.run(
|
|
576
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:PRODUCED]->(tn:CloudflareTunnel {tunnelId: $tunnelId})
|
|
577
|
+
RETURN count(tn) AS existsCount`,
|
|
578
|
+
{ taskId, accountId, tunnelId: tunnelState.tunnelId }
|
|
579
|
+
);
|
|
580
|
+
const existsCountRaw = tunnelExistsRow.records[0]?.get("existsCount");
|
|
581
|
+
const tunnelAlreadyExists = (typeof existsCountRaw === "number" ? existsCountRaw : existsCountRaw?.toNumber?.() ?? 0) > 0;
|
|
582
|
+
if (tunnelAlreadyExists) {
|
|
583
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
584
|
+
const closeRes = await session.run(
|
|
585
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})
|
|
586
|
+
SET t.status = 'completed', t.completedAt = $now, t.updatedAt = $now,
|
|
587
|
+
t.errorMessage = null,
|
|
588
|
+
t.tunnelId = $tunnelId, t.tunnelName = $tunnelName
|
|
589
|
+
RETURN count(t) AS affected`,
|
|
590
|
+
{ taskId, accountId, now, tunnelId: tunnelState.tunnelId, tunnelName: tunnelState.tunnelName }
|
|
591
|
+
);
|
|
592
|
+
const aff = closeRes.records[0]?.get("affected");
|
|
593
|
+
const affN = typeof aff === "number" ? aff : aff?.toNumber?.() ?? 0;
|
|
594
|
+
if (affN !== 1) {
|
|
595
|
+
throw new Error(`reconciler close-out MATCH count=${affN} for taskId=${taskId}`);
|
|
596
|
+
}
|
|
597
|
+
resolved.push({ taskId, resolution: "completed-existing-tunnel" });
|
|
598
|
+
process.stderr.write(
|
|
599
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=true resolution=completed
|
|
600
|
+
`
|
|
601
|
+
);
|
|
602
|
+
continue;
|
|
603
|
+
}
|
|
604
|
+
const convRow = await session.run(
|
|
605
|
+
`MATCH (t:Task {taskId: $taskId, accountId: $accountId})-[:RAISED_DURING]->(target)
|
|
606
|
+
OPTIONAL MATCH (target)-[:PART_OF]->(c:Conversation)
|
|
607
|
+
RETURN coalesce(c.sessionKey, target.sessionKey) AS sessionKey LIMIT 1`,
|
|
608
|
+
{ taskId, accountId }
|
|
609
|
+
);
|
|
610
|
+
const resolvedConversationKey = convRow.records[0]?.get("sessionKey");
|
|
611
|
+
await completeCloudflareTask({
|
|
612
|
+
taskId,
|
|
613
|
+
taskElementId,
|
|
614
|
+
accountId,
|
|
615
|
+
// Empty string when the edge resolution found no Conversation
|
|
616
|
+
// (rare — would require a malformed Task with no RAISED_DURING).
|
|
617
|
+
// The live close-out will produce a Tunnel without provenance
|
|
618
|
+
// edges in that case; better than silently dropping the close-out.
|
|
619
|
+
conversationKey: resolvedConversationKey ?? conversationKeyForState ?? "",
|
|
620
|
+
tunnelId: tunnelState.tunnelId,
|
|
621
|
+
tunnelName: tunnelState.tunnelName,
|
|
622
|
+
hostnames: void 0,
|
|
623
|
+
status: "completed"
|
|
624
|
+
});
|
|
625
|
+
resolved.push({ taskId, resolution: "completed" });
|
|
626
|
+
process.stderr.write(
|
|
627
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=true tunnel-already-written=false resolution=completed
|
|
628
|
+
`
|
|
629
|
+
);
|
|
630
|
+
} else {
|
|
631
|
+
const diagnostic = ageMs > RECONCILE_HARD_AGE_MS ? CLOUDFLARE_TASK_DIAGNOSTICS.noTunnelStateOnDisk : CLOUDFLARE_TASK_DIAGNOSTICS.endpointDiedPreReconcile;
|
|
632
|
+
await completeCloudflareTask({
|
|
633
|
+
taskId,
|
|
634
|
+
taskElementId,
|
|
635
|
+
accountId,
|
|
636
|
+
conversationKey: conversationKeyForState ?? "",
|
|
637
|
+
status: "failed",
|
|
638
|
+
errorMessage: diagnostic
|
|
639
|
+
});
|
|
640
|
+
resolved.push({ taskId, resolution: `failed-${diagnostic}` });
|
|
641
|
+
process.stderr.write(
|
|
642
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} age=${Math.round(ageMs / 1e3)}s tunnel-state-found=false resolution=failed errorMessage=${diagnostic}
|
|
643
|
+
`
|
|
644
|
+
);
|
|
645
|
+
}
|
|
646
|
+
} catch (err) {
|
|
647
|
+
process.stderr.write(
|
|
648
|
+
`[task] action-recover kind=${TASK_KIND} taskId=${taskId} resolution=error reason="${err instanceof Error ? err.message : String(err)}"
|
|
649
|
+
`
|
|
650
|
+
);
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
return { scanned: stale.records.length, resolved };
|
|
654
|
+
} finally {
|
|
655
|
+
await session.close();
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
export {
|
|
660
|
+
openCloudflareTask,
|
|
661
|
+
appendCloudflareSteps,
|
|
662
|
+
completeCloudflareTask,
|
|
663
|
+
readTunnelState,
|
|
664
|
+
CLOUDFLARE_TASK_DIAGNOSTICS,
|
|
665
|
+
resolveUnitGoneVerdict,
|
|
666
|
+
recoverRunningCloudflareTasks
|
|
667
|
+
};
|