@rubytech/create-maxy 1.0.743 → 1.0.745

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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts +2 -0
  3. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts.map +1 -0
  4. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js +97 -0
  5. package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js.map +1 -0
  6. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts +37 -0
  7. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts.map +1 -0
  8. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js +333 -0
  9. package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js.map +1 -0
  10. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts +71 -0
  11. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts.map +1 -0
  12. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js +168 -0
  13. package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js.map +1 -0
  14. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +13 -1
  15. package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -1
  16. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +70 -3
  17. package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -1
  18. package/payload/platform/lib/graph-mcp/dist/index.js +297 -11
  19. package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
  20. package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +3 -6
  21. package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
  22. package/payload/platform/lib/graph-mcp/dist/schema-cache.js +30 -7
  23. package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
  24. package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate-write.test.ts +150 -0
  25. package/payload/platform/lib/graph-mcp/src/cypher-rewrite-stamp.ts +349 -0
  26. package/payload/platform/lib/graph-mcp/src/cypher-shim-write.ts +240 -0
  27. package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +95 -3
  28. package/payload/platform/lib/graph-mcp/src/index.ts +415 -18
  29. package/payload/platform/lib/graph-mcp/src/schema-cache.ts +37 -7
  30. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts +2 -0
  31. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts.map +1 -0
  32. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js +147 -0
  33. package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js.map +1 -0
  34. package/payload/platform/lib/graph-write/dist/audit.d.ts +84 -0
  35. package/payload/platform/lib/graph-write/dist/audit.d.ts.map +1 -0
  36. package/payload/platform/lib/graph-write/dist/audit.js +129 -0
  37. package/payload/platform/lib/graph-write/dist/audit.js.map +1 -0
  38. package/payload/platform/lib/graph-write/dist/index.d.ts +1 -0
  39. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  40. package/payload/platform/lib/graph-write/dist/index.js +18 -22
  41. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  42. package/payload/platform/lib/graph-write/src/__tests__/audit.test.ts +162 -0
  43. package/payload/platform/lib/graph-write/src/audit.ts +182 -0
  44. package/payload/platform/lib/graph-write/src/index.ts +5 -0
  45. package/payload/platform/package.json +2 -2
  46. package/payload/platform/plugins/docs/references/deployment.md +2 -1
  47. package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
  48. package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
  49. package/payload/platform/templates/specialists/agents/database-operator.md +20 -6
  50. package/payload/server/chunk-2T4RRIJK.js +9462 -0
  51. package/payload/server/chunk-SPTD7L7Z.js +9474 -0
  52. package/payload/server/maxy-edge.js +94 -16
  53. package/payload/server/public/assets/{graph-DhNy70eS.js → graph-BoSJpLG3.js} +1 -1
  54. package/payload/server/public/graph.html +1 -1
  55. package/payload/server/server.js +1 -1
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Pure helpers for the shim-owned write_neo4j_cypher path (Task 797).
3
+ * Extracted from index.ts so the executeWrite tx body, response synthesis,
4
+ * and value serialization can be unit-tested without the shim's top-level
5
+ * process bootstrap (which spawns uvx and opens stdin/stdout pipes).
6
+ *
7
+ * The shim's index.ts wires these up: rewriter → shared driver → session.
8
+ * executeWrite(tx => runWriteTxBody(...)) → response synth → stdout.
9
+ */
10
+
11
+ export const ORPHAN_CHECK_CYPHER =
12
+ "MATCH (n) WHERE n.createdBySession = $__autoSession " +
13
+ "AND n.createdAt >= $__autoStartTimestamp " +
14
+ "AND NOT (n)--() " +
15
+ "RETURN collect(elementId(n)) AS orphanIds, " +
16
+ "collect(distinct labels(n)) AS labelSets";
17
+
18
+ export interface GraphTx {
19
+ run(
20
+ cypher: string,
21
+ params?: Record<string, unknown>,
22
+ ): Promise<{
23
+ records: Array<{
24
+ keys: readonly string[];
25
+ get: (k: string | number) => unknown;
26
+ }>;
27
+ summary: {
28
+ counters: {
29
+ updates(): {
30
+ nodesCreated: number;
31
+ relationshipsCreated: number;
32
+ propertiesSet: number;
33
+ };
34
+ };
35
+ };
36
+ }>;
37
+ }
38
+
39
+ export interface GraphSession {
40
+ executeWrite<T>(work: (tx: GraphTx) => Promise<T>): Promise<T>;
41
+ close(): Promise<void>;
42
+ }
43
+
44
+ export interface GraphDriver {
45
+ session(): GraphSession;
46
+ }
47
+
48
+ export class OrphanRollbackError extends Error {
49
+ constructor(
50
+ public readonly orphanIds: string[],
51
+ public readonly sampleLabels: string[],
52
+ ) {
53
+ super(`orphan rollback: ${orphanIds.length} unattached node(s)`);
54
+ }
55
+ }
56
+
57
+ export interface AutoContext {
58
+ agent: string;
59
+ session: string;
60
+ }
61
+
62
+ export interface WriteTxOutcome {
63
+ nodesCreated: number;
64
+ relsCreated: number;
65
+ propertiesSet: number;
66
+ serializedRecords: Array<Record<string, unknown>>;
67
+ }
68
+
69
+ export function serializeValue(v: unknown): unknown {
70
+ if (v === null || v === undefined) return v;
71
+ if (typeof v === "string" || typeof v === "boolean" || typeof v === "number") {
72
+ return v;
73
+ }
74
+ if (Array.isArray(v)) return v.map(serializeValue);
75
+ if (typeof v === "object") {
76
+ const obj = v as Record<string, unknown>;
77
+ if (
78
+ "low" in obj &&
79
+ "high" in obj &&
80
+ typeof obj.low === "number" &&
81
+ typeof obj.high === "number" &&
82
+ Object.keys(obj).length === 2
83
+ ) {
84
+ if (obj.high === 0 || obj.high === -1) return obj.low;
85
+ return String(obj);
86
+ }
87
+ if ("properties" in obj && "labels" in obj) {
88
+ return {
89
+ labels: obj.labels,
90
+ elementId: obj.elementId,
91
+ properties: serializeValue(obj.properties),
92
+ };
93
+ }
94
+ if ("properties" in obj && "type" in obj && "elementId" in obj) {
95
+ return {
96
+ type: obj.type,
97
+ elementId: obj.elementId,
98
+ properties: serializeValue(obj.properties),
99
+ };
100
+ }
101
+ if (
102
+ obj.constructor &&
103
+ obj.constructor.name &&
104
+ obj.constructor.name !== "Object" &&
105
+ obj.constructor.name !== "Array"
106
+ ) {
107
+ return String(obj);
108
+ }
109
+ const out: Record<string, unknown> = {};
110
+ for (const [k, val] of Object.entries(obj)) {
111
+ out[k] = serializeValue(val);
112
+ }
113
+ return out;
114
+ }
115
+ return v;
116
+ }
117
+
118
+ /**
119
+ * Body of the executeWrite tx. Runs the rewritten cypher, captures counters,
120
+ * runs the orphan check, throws OrphanRollbackError if any orphans remain
121
+ * (executeWrite catches the throw and rolls back the entire tx).
122
+ *
123
+ * Caller is responsible for: (a) opening the session, (b) wrapping this in
124
+ * `session.executeWrite(tx => runWriteTxBody(tx, ...))`, (c) calling close()
125
+ * in finally, (d) translating the outcome / OrphanRollbackError into the
126
+ * synthesised MCP response and audit log lines.
127
+ */
128
+ export async function runWriteTxBody(
129
+ tx: GraphTx,
130
+ rewrittenCypher: string,
131
+ operatorParams: Record<string, unknown>,
132
+ autoCtx: AutoContext,
133
+ ): Promise<WriteTxOutcome> {
134
+ const tsRes = await tx.run("RETURN datetime() AS now");
135
+ const startTs = tsRes.records[0].get("now");
136
+
137
+ const merged: Record<string, unknown> = {
138
+ ...operatorParams,
139
+ __autoStartTimestamp: startTs,
140
+ __autoAgent: autoCtx.agent,
141
+ __autoSession: autoCtx.session,
142
+ };
143
+
144
+ const writeRes = await tx.run(rewrittenCypher, merged);
145
+ const counters = writeRes.summary.counters.updates();
146
+ const nodesCreated = counters.nodesCreated;
147
+ const relsCreated = counters.relationshipsCreated;
148
+ const propertiesSet = counters.propertiesSet;
149
+ const keys =
150
+ writeRes.records.length > 0 ? writeRes.records[0].keys.map(String) : [];
151
+ const serializedRecords = writeRes.records.map((rec) => {
152
+ const out: Record<string, unknown> = {};
153
+ for (const k of keys) {
154
+ out[k] = serializeValue(rec.get(k));
155
+ }
156
+ return out;
157
+ });
158
+
159
+ const orphanRes = await tx.run(ORPHAN_CHECK_CYPHER, merged);
160
+ const orphanIds = (orphanRes.records[0].get("orphanIds") as string[]) ?? [];
161
+ const labelSets =
162
+ (orphanRes.records[0].get("labelSets") as string[][]) ?? [];
163
+ if (orphanIds.length > 0) {
164
+ const flatLabels = Array.from(new Set(labelSets.flat()));
165
+ throw new OrphanRollbackError(orphanIds, flatLabels);
166
+ }
167
+
168
+ return { nodesCreated, relsCreated, propertiesSet, serializedRecords };
169
+ }
170
+
171
+ export interface WriteSynthInput {
172
+ nodesCreated: number;
173
+ relsCreated: number;
174
+ propertiesSet: number;
175
+ records: Array<Record<string, unknown>>;
176
+ }
177
+
178
+ export function synthesiseWriteResponse(
179
+ id: string | number,
180
+ w: WriteSynthInput,
181
+ ): string {
182
+ const parts: string[] = [];
183
+ if (w.records.length > 0) {
184
+ parts.push(
185
+ `${w.records.length} record${w.records.length === 1 ? "" : "s"} returned`,
186
+ );
187
+ }
188
+ parts.push(`${w.nodesCreated} nodes created`);
189
+ parts.push(`${w.relsCreated} relationships created`);
190
+ if (w.propertiesSet > 0) parts.push(`${w.propertiesSet} properties set`);
191
+ if (w.records.length > 0) {
192
+ parts.push("");
193
+ parts.push(JSON.stringify(w.records, null, 2));
194
+ }
195
+ return JSON.stringify({
196
+ jsonrpc: "2.0",
197
+ id,
198
+ result: {
199
+ content: [{ type: "text", text: parts.join("\n") }],
200
+ },
201
+ });
202
+ }
203
+
204
+ export function synthesiseOrphanRollback(
205
+ id: string | number,
206
+ orphanCount: number,
207
+ sampleLabels: string[],
208
+ ): string {
209
+ const labelLine =
210
+ sampleLabels.length > 0
211
+ ? sampleLabels.slice(0, 5).join(", ")
212
+ : "(unknown — orphans had no labels)";
213
+ const text =
214
+ `orphan rollback — ${orphanCount} node(s) created without an edge in the same transaction.\n` +
215
+ `Sample labels: ${labelLine}.\n\n` +
216
+ `The transaction was aborted; nothing was persisted. Anchor each new node to its semantic parent in the same statement (Graph Stewardship Doctrine Rule 1 in database-operator.md).`;
217
+ return JSON.stringify({
218
+ jsonrpc: "2.0",
219
+ id,
220
+ result: {
221
+ content: [{ type: "text", text }],
222
+ isError: true,
223
+ },
224
+ });
225
+ }
226
+
227
+ export function synthesiseShimError(
228
+ id: string | number,
229
+ prefix: string,
230
+ errMsg: string,
231
+ ): string {
232
+ return JSON.stringify({
233
+ jsonrpc: "2.0",
234
+ id,
235
+ result: {
236
+ content: [{ type: "text", text: `${prefix}: ${errMsg}` }],
237
+ isError: true,
238
+ },
239
+ });
240
+ }
@@ -31,13 +31,75 @@ export interface UnknownToken {
31
31
  hint: string;
32
32
  }
33
33
 
34
+ export type ForbiddenKind =
35
+ | "drop-database"
36
+ | "create-index"
37
+ | "create-constraint"
38
+ | "call-dbms"
39
+ | "call-db-create";
40
+
41
+ export interface ForbiddenPattern {
42
+ kind: ForbiddenKind;
43
+ match: string;
44
+ hint: string;
45
+ }
46
+
34
47
  export interface ValidationResult {
35
48
  ok: boolean;
36
49
  unknown: UnknownToken[];
50
+ forbidden: ForbiddenPattern[];
37
51
  labelTokens: string[];
38
52
  edgeTokens: string[];
39
53
  }
40
54
 
55
+ export interface ValidateOptions {
56
+ /** "read" (default) skips DDL/admin forbidden checks. "write" applies them. */
57
+ mode?: "read" | "write";
58
+ }
59
+
60
+ // DDL / admin patterns that the database-operator's raw write surface must
61
+ // reject. The graph MCP shim is the only enforcement point — Neo4j Community
62
+ // has no per-tool ACL, so the shim filters by syntactic shape before
63
+ // forwarding the JSON-RPC line to the upstream cypher driver. Each pattern
64
+ // is applied to a string-literal-stripped cypher body so that prose inside
65
+ // quoted strings cannot trip a false positive.
66
+ //
67
+ // CALL apoc.* is intentionally NOT forbidden — apoc.refactor.mergeNodes is
68
+ // the documented dedup primitive. CALL db.labels() and other read-only db.*
69
+ // calls are allowed; only db.create* (createLabel, createProperty,
70
+ // createRelationshipType) is forbidden because those are schema-mutating.
71
+ const FORBIDDEN_PATTERNS: ReadonlyArray<{
72
+ kind: ForbiddenKind;
73
+ pattern: RegExp;
74
+ hint: string;
75
+ }> = [
76
+ {
77
+ kind: "drop-database",
78
+ pattern: /\bDROP\s+(?:DATABASE|ALIAS|USER|ROLE)\b/i,
79
+ hint: "DROP DATABASE/USER/ROLE/ALIAS is admin DDL — not permitted on the operator surface.",
80
+ },
81
+ {
82
+ kind: "create-index",
83
+ pattern: /\bCREATE\s+(?:OR\s+REPLACE\s+)?(?:RANGE\s+|TEXT\s+|POINT\s+|LOOKUP\s+|FULLTEXT\s+|BTREE\s+|VECTOR\s+)?INDEX\b/i,
84
+ hint: "Index DDL is admin-owned (seed-neo4j.sh applies indexes). Not permitted on the operator surface.",
85
+ },
86
+ {
87
+ kind: "create-constraint",
88
+ pattern: /\bCREATE\s+(?:OR\s+REPLACE\s+)?CONSTRAINT\b/i,
89
+ hint: "Constraint DDL is admin-owned (seed-neo4j.sh applies constraints). Not permitted on the operator surface.",
90
+ },
91
+ {
92
+ kind: "call-dbms",
93
+ pattern: /\bCALL\s+dbms\./i,
94
+ hint: "CALL dbms.* is admin/security surface. Not permitted on the operator surface.",
95
+ },
96
+ {
97
+ kind: "call-db-create",
98
+ pattern: /\bCALL\s+db\.create/i,
99
+ hint: "CALL db.create* mutates schema (label/property/type registration). Not permitted on the operator surface.",
100
+ },
101
+ ];
102
+
41
103
  // Bracket content containing a type reference. Examples this matches:
42
104
  // [:PART_OF]
43
105
  // [r:PART_OF]
@@ -52,7 +114,10 @@ const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[
52
114
  // the edge pattern. The [A-Z] anchor excludes lowercase map keys.
53
115
  const LABEL_PATTERN = /:([A-Z][A-Za-z0-9_]*)/g;
54
116
 
55
- function stripStringLiterals(cypher: string): string {
117
+ // Exported so the post-write audit (graph-write/audit.ts) can reuse the same
118
+ // literal-stripping pass before its own static parsing — avoids duplicate
119
+ // false-positive treatment across the two surfaces.
120
+ export function stripStringLiterals(cypher: string): string {
56
121
  // Replace single- and double-quoted literals with empty quotes. Preserves
57
122
  // positional structure without retaining content that could match the
58
123
  // label regex (e.g. ':SomeLabel' inside a string).
@@ -118,19 +183,40 @@ function hintFor(
118
183
  return `${didYouMean}Unknown ${kind} '${token}' — not in the current Neo4j schema. See .docs/neo4j.md for the canonical taxonomy.`;
119
184
  }
120
185
 
186
+ function detectForbidden(cypher: string): ForbiddenPattern[] {
187
+ // Strip literals so a property value like 'CREATE INDEX archive' cannot
188
+ // trip a false rejection. The shared `stripStringLiterals` keeps this
189
+ // treatment uniform with token extraction and audit static parsing.
190
+ const cleaned = stripStringLiterals(cypher);
191
+ const out: ForbiddenPattern[] = [];
192
+ for (const { kind, pattern, hint } of FORBIDDEN_PATTERNS) {
193
+ const found = cleaned.match(pattern);
194
+ if (found) {
195
+ out.push({ kind, match: found[0], hint });
196
+ }
197
+ }
198
+ return out;
199
+ }
200
+
121
201
  export function validate(
122
202
  cypher: string,
123
203
  snapshot: SchemaSnapshot,
204
+ options: ValidateOptions = {},
124
205
  ): ValidationResult {
206
+ const mode = options.mode ?? "read";
207
+ const forbidden = mode === "write" ? detectForbidden(cypher) : [];
125
208
  const { labels, edges } = extractTokens(cypher);
126
209
  // Fail-open when the snapshot is empty. An empty snapshot means "schema
127
210
  // cache not loaded" (boot race, Neo4j unreachable). Rejecting every token
128
211
  // would wedge the admin agent; letting it through preserves the observable
129
212
  // `validated=false` signal on the existing [graph-query] line.
213
+ // Forbidden DDL/admin patterns still apply — those are syntactic, not
214
+ // schema-derived.
130
215
  if (snapshot.labels.size === 0 && snapshot.relationshipTypes.size === 0) {
131
216
  return {
132
- ok: true,
217
+ ok: forbidden.length === 0,
133
218
  unknown: [],
219
+ forbidden,
134
220
  labelTokens: [...labels],
135
221
  edgeTokens: [...edges],
136
222
  };
@@ -148,9 +234,15 @@ export function validate(
148
234
  unknown.push({ token, kind: "relationship", nearest, hint: hintFor(token, "relationship", nearest) });
149
235
  }
150
236
  }
237
+ // Write-mode caller treats `unknown` as a soft warning (audit), not a
238
+ // hard reject — operators legitimately introduce new labels via REMOVE
239
+ // n:Old SET n:New. Read-mode preserves prior behaviour: any unknown
240
+ // → ok=false.
241
+ const tokensOk = mode === "write" ? true : unknown.length === 0;
151
242
  return {
152
- ok: unknown.length === 0,
243
+ ok: tokensOk && forbidden.length === 0,
153
244
  unknown,
245
+ forbidden,
154
246
  labelTokens: [...labels],
155
247
  edgeTokens: [...edges],
156
248
  };