@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.
- package/package.json +1 -1
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts +2 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js +97 -0
- package/payload/platform/lib/graph-mcp/dist/__tests__/cypher-validate-write.test.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts +37 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js +333 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-rewrite-stamp.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts +71 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.d.ts.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js +168 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-shim-write.js.map +1 -0
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts +13 -1
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js +70 -3
- package/payload/platform/lib/graph-mcp/dist/cypher-validate.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/index.js +297 -11
- package/payload/platform/lib/graph-mcp/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts +3 -6
- package/payload/platform/lib/graph-mcp/dist/schema-cache.d.ts.map +1 -1
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js +30 -7
- package/payload/platform/lib/graph-mcp/dist/schema-cache.js.map +1 -1
- package/payload/platform/lib/graph-mcp/src/__tests__/cypher-validate-write.test.ts +150 -0
- package/payload/platform/lib/graph-mcp/src/cypher-rewrite-stamp.ts +349 -0
- package/payload/platform/lib/graph-mcp/src/cypher-shim-write.ts +240 -0
- package/payload/platform/lib/graph-mcp/src/cypher-validate.ts +95 -3
- package/payload/platform/lib/graph-mcp/src/index.ts +415 -18
- package/payload/platform/lib/graph-mcp/src/schema-cache.ts +37 -7
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts +2 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js +147 -0
- package/payload/platform/lib/graph-write/dist/__tests__/audit.test.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/audit.d.ts +84 -0
- package/payload/platform/lib/graph-write/dist/audit.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/audit.js +129 -0
- package/payload/platform/lib/graph-write/dist/audit.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +18 -22
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/audit.test.ts +162 -0
- package/payload/platform/lib/graph-write/src/audit.ts +182 -0
- package/payload/platform/lib/graph-write/src/index.ts +5 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/docs/references/deployment.md +2 -1
- package/payload/platform/plugins/docs/references/memory-guide.md +2 -0
- package/payload/platform/plugins/docs/references/troubleshooting.md +16 -0
- package/payload/platform/templates/specialists/agents/database-operator.md +20 -6
- package/payload/server/chunk-2T4RRIJK.js +9462 -0
- package/payload/server/chunk-SPTD7L7Z.js +9474 -0
- package/payload/server/maxy-edge.js +94 -16
- package/payload/server/public/assets/{graph-DhNy70eS.js → graph-BoSJpLG3.js} +1 -1
- package/payload/server/public/graph.html +1 -1
- package/payload/server/server.js +1 -1
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-write audit for the database-operator's raw `write_neo4j_cypher` tool
|
|
3
|
+
* (Task 796). Sibling to `[graph-write] accepted` for the wrapped writers
|
|
4
|
+
* (`memory-write`, `contact-create`, etc. — Task 673) — same module so the
|
|
5
|
+
* `[graph-*]` log family stays uniform across both write surfaces.
|
|
6
|
+
*
|
|
7
|
+
* Design posture:
|
|
8
|
+
* - Static parse first. Pre-flight regex on the cypher text catches missing
|
|
9
|
+
* provenance stamps and unknown edge types cheaply, no driver round-trip.
|
|
10
|
+
* - Dynamic orphan detection is the caller's responsibility — the graph-mcp
|
|
11
|
+
* shim runs the detection query against Neo4j after the upstream commits
|
|
12
|
+
* and passes the resulting elementId list as `orphanIds`. Keeping the
|
|
13
|
+
* module pure (no neo4j-driver import) makes it unit-testable without a
|
|
14
|
+
* live database.
|
|
15
|
+
* - Audit is observational. Warnings never block the write. The
|
|
16
|
+
* prompt-level Graph Stewardship Doctrine in
|
|
17
|
+
* [database-operator.md](../../../templates/specialists/agents/database-operator.md)
|
|
18
|
+
* names the discipline; the audit is the verification stream.
|
|
19
|
+
*
|
|
20
|
+
* String literals are stripped before pattern checks so that property values
|
|
21
|
+
* containing words like `'createdByAgent in my bio'` cannot trip a false
|
|
22
|
+
* provenance count, and edge-like patterns inside quoted strings cannot
|
|
23
|
+
* trip a false unknown-type warning. The regex shape is duplicated from
|
|
24
|
+
* [cypher-validate.ts](../../graph-mcp/src/cypher-validate.ts) intentionally:
|
|
25
|
+
* graph-write does not depend on graph-mcp, and a one-line regex helper
|
|
26
|
+
* doesn't justify a third package. Drift in either copy is a regression
|
|
27
|
+
* surfaced by both the validator tests and the audit tests.
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
export interface SchemaSnapshot {
|
|
31
|
+
readonly labels: ReadonlySet<string>;
|
|
32
|
+
readonly relationshipTypes: ReadonlySet<string>;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface CypherWriteAuditInput {
|
|
36
|
+
cypher: string;
|
|
37
|
+
schema: SchemaSnapshot;
|
|
38
|
+
agentName: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
/** Counter from upstream response — drives missing-provenance arithmetic. */
|
|
41
|
+
nodesCreated: number;
|
|
42
|
+
/** Counter from upstream response — informational only. */
|
|
43
|
+
relsCreated: number;
|
|
44
|
+
/**
|
|
45
|
+
* elementIds of nodes the post-write orphan query identified. Caller scopes
|
|
46
|
+
* the query to (createdBySession, createdAt >= writeStartTimestamp) so this
|
|
47
|
+
* list cannot include nodes from prior writes in the same session. Empty
|
|
48
|
+
* array = no orphans (or no scope to check).
|
|
49
|
+
*/
|
|
50
|
+
orphanIds: string[];
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export type AuditWarning =
|
|
54
|
+
| { kind: "orphan-warning"; orphanIds: string[] }
|
|
55
|
+
| { kind: "unknown-type-warning"; type: string }
|
|
56
|
+
| { kind: "missing-provenance-warning"; created: number; stamped: number };
|
|
57
|
+
|
|
58
|
+
export type AuditLine =
|
|
59
|
+
| {
|
|
60
|
+
kind: "accepted";
|
|
61
|
+
cypherPrefix: string;
|
|
62
|
+
nodesCreated: number;
|
|
63
|
+
relsCreated: number;
|
|
64
|
+
agentName: string;
|
|
65
|
+
sessionId: string;
|
|
66
|
+
}
|
|
67
|
+
| { kind: "orphan-warning"; cypherPrefix: string; orphanIds: string[] }
|
|
68
|
+
| { kind: "unknown-type-warning"; cypherPrefix: string; type: string }
|
|
69
|
+
| {
|
|
70
|
+
kind: "missing-provenance-warning";
|
|
71
|
+
cypherPrefix: string;
|
|
72
|
+
created: number;
|
|
73
|
+
stamped: number;
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
// Mirrors cypher-validate's pattern. Captures TYPE(|TYPE)* alternation; the
|
|
77
|
+
// audit splits on `|` to enumerate every referenced type.
|
|
78
|
+
const EDGE_PATTERN = /\[[^\]]*?:([A-Z_][A-Za-z0-9_]*(?:\|[A-Z_][A-Za-z0-9_]*)*)[^\]]*?\]/g;
|
|
79
|
+
|
|
80
|
+
// Counts CREATE (n:Label) and MERGE (n:Label) statements that introduce a
|
|
81
|
+
// node. The trailing `[A-Z]` requires at least one label (so `CREATE INDEX`
|
|
82
|
+
// and bare `CREATE (a)-[:R]->(b)` shapes don't trip this — bare patterns
|
|
83
|
+
// reference a previously-MATCHed alias and don't introduce new nodes).
|
|
84
|
+
const CREATE_OR_MERGE_NODE = /\b(?:CREATE|MERGE)\s*\(\s*[A-Za-z_][A-Za-z0-9_]*\s*:\s*[A-Z]/g;
|
|
85
|
+
|
|
86
|
+
// Stamp tokens — at least one of these MUST appear per created node for the
|
|
87
|
+
// write to satisfy provenance discipline. The audit counts occurrences;
|
|
88
|
+
// stamp-count >= node-count is the pass criterion.
|
|
89
|
+
const PROVENANCE_TOKEN = /\bcreatedBy(?:Agent|Tool|Session|Source)\b/g;
|
|
90
|
+
|
|
91
|
+
function stripStringLiterals(cypher: string): string {
|
|
92
|
+
// Identical regex to cypher-validate.ts. Replaces single- and double-
|
|
93
|
+
// quoted literals with empty quotes so e.g. 'CREATE (n:Person)' inside a
|
|
94
|
+
// property value cannot inflate the create-count.
|
|
95
|
+
return cypher.replace(/'[^']*'|"[^"]*"/g, '""');
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function extractEdgeTypes(cleaned: string): Set<string> {
|
|
99
|
+
const out = new Set<string>();
|
|
100
|
+
// Use String.matchAll for stateless iteration — no shared regex lastIndex.
|
|
101
|
+
for (const m of cleaned.matchAll(EDGE_PATTERN)) {
|
|
102
|
+
for (const t of m[1].split("|")) {
|
|
103
|
+
const clean = t.trim();
|
|
104
|
+
if (clean) out.add(clean);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
return out;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function countCreateOrMergeNodes(cleaned: string): number {
|
|
111
|
+
const matches = cleaned.match(CREATE_OR_MERGE_NODE);
|
|
112
|
+
return matches ? matches.length : 0;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function countProvenanceStamps(cleaned: string): number {
|
|
116
|
+
const matches = cleaned.match(PROVENANCE_TOKEN);
|
|
117
|
+
return matches ? matches.length : 0;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function auditCypherWrite(input: CypherWriteAuditInput): AuditWarning[] {
|
|
121
|
+
const warnings: AuditWarning[] = [];
|
|
122
|
+
const cleaned = stripStringLiterals(input.cypher);
|
|
123
|
+
|
|
124
|
+
// 1. Unknown edge type — cypher names a type not in the live ontology.
|
|
125
|
+
// Enumerate every referenced type; emit one warning per unknown.
|
|
126
|
+
const referencedTypes = extractEdgeTypes(cleaned);
|
|
127
|
+
for (const t of referencedTypes) {
|
|
128
|
+
if (!input.schema.relationshipTypes.has(t)) {
|
|
129
|
+
warnings.push({ kind: "unknown-type-warning", type: t });
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Missing provenance — count CREATE/MERGE-with-label statements vs
|
|
134
|
+
// `createdBy*` token occurrences. Stamps may live in inline maps
|
|
135
|
+
// (`{createdByAgent: $agent}`) or SET clauses; both shapes contribute
|
|
136
|
+
// to the token count. The arithmetic is precision-imprecise — a
|
|
137
|
+
// multi-stamp single CREATE inflates `stamped`, a multi-CREATE single
|
|
138
|
+
// SET undercounts — but the directional signal (zero stamps for many
|
|
139
|
+
// creates) catches the failure mode this audit exists to surface.
|
|
140
|
+
//
|
|
141
|
+
// nodesCreated=0 short-circuits the check: the upstream counter is
|
|
142
|
+
// ground truth for whether any node was actually persisted. An
|
|
143
|
+
// idempotent MERGE that matched an existing node has a static
|
|
144
|
+
// create-count of 1 but contributed no new node — no provenance was
|
|
145
|
+
// needed, and warning here would be a false positive.
|
|
146
|
+
if (input.nodesCreated > 0) {
|
|
147
|
+
const createOrMergeNodes = countCreateOrMergeNodes(cleaned);
|
|
148
|
+
if (createOrMergeNodes > 0) {
|
|
149
|
+
const stamps = countProvenanceStamps(cleaned);
|
|
150
|
+
if (stamps < createOrMergeNodes) {
|
|
151
|
+
warnings.push({
|
|
152
|
+
kind: "missing-provenance-warning",
|
|
153
|
+
created: createOrMergeNodes,
|
|
154
|
+
stamped: stamps,
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// 3. Orphans — caller-supplied. The dynamic detection happens at the
|
|
161
|
+
// graph-mcp shim layer (which has the Neo4j driver); the audit module
|
|
162
|
+
// only renders the warning shape.
|
|
163
|
+
if (input.orphanIds.length > 0) {
|
|
164
|
+
warnings.push({ kind: "orphan-warning", orphanIds: input.orphanIds });
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
return warnings;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
export function formatAuditLine(line: AuditLine): string {
|
|
171
|
+
const prefixField = `query="${line.cypherPrefix.replace(/"/g, "'")}"`;
|
|
172
|
+
switch (line.kind) {
|
|
173
|
+
case "accepted":
|
|
174
|
+
return `[graph-cypher-write] accepted ${prefixField} nodesCreated=${line.nodesCreated} relsCreated=${line.relsCreated} agentName=${line.agentName} sessionId=${line.sessionId}`;
|
|
175
|
+
case "orphan-warning":
|
|
176
|
+
return `[graph-cypher-write] orphan-warning ${prefixField} orphanIds=${line.orphanIds.join(",")}`;
|
|
177
|
+
case "unknown-type-warning":
|
|
178
|
+
return `[graph-cypher-write] unknown-type-warning ${prefixField} type=${line.type}`;
|
|
179
|
+
case "missing-provenance-warning":
|
|
180
|
+
return `[graph-cypher-write] missing-provenance-warning ${prefixField} created=${line.created} stamped=${line.stamped}`;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// Task 796: re-export the post-write audit so consumers (graph-mcp shim)
|
|
2
|
+
// import from the same package as `writeNodeWithEdges`. Keeps the
|
|
3
|
+
// `[graph-*]` log family colocated.
|
|
4
|
+
export * from "./audit.js";
|
|
5
|
+
|
|
1
6
|
/**
|
|
2
7
|
* Write doctrine (Task 673): a node without at least one adjacency is noise,
|
|
3
8
|
* not knowledge. `writeNodeWithEdges` is the single primitive every graph
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
"plugins/*/mcp"
|
|
7
7
|
],
|
|
8
8
|
"scripts": {
|
|
9
|
-
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-
|
|
10
|
-
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-
|
|
9
|
+
"build": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json && NODE_OPTIONS='--max-old-space-size=8192' tsc -b plugins/*/mcp/tsconfig.json",
|
|
10
|
+
"build:lib": "tsc -p lib/models/tsconfig.json && tsc -p lib/anthropic-key/tsconfig.json && tsc -p lib/oauth-llm/tsconfig.json && tsc -p lib/mcp-stderr-tee/tsconfig.json && tsc -p lib/mcp-spawn-tee/tsconfig.json && tsc -p lib/graph-write/tsconfig.json && tsc -p lib/graph-mcp/tsconfig.json && tsc -p lib/graph-trash/tsconfig.json && tsc -p lib/graph-search/tsconfig.json && tsc -p lib/screening-patterns/tsconfig.json && tsc -p lib/device-url/tsconfig.json && tsc -p lib/brand-templating/tsconfig.json",
|
|
11
11
|
"build:memory": "tsc -p plugins/memory/mcp/tsconfig.json",
|
|
12
12
|
"build:contacts": "tsc -p plugins/contacts/mcp/tsconfig.json",
|
|
13
13
|
"build:telegram": "tsc -p plugins/telegram/mcp/tsconfig.json",
|
|
@@ -100,7 +100,8 @@ systemctl --user daemon-reload
|
|
|
100
100
|
A single Pi or laptop can host more than one brand (for example Maxy and Real Agent) side by side. Each brand runs as its own service on its own port, with its own install directory and its own data. Installing one brand does not touch the other.
|
|
101
101
|
|
|
102
102
|
- **Separate:** each brand has its own install folder (`~/maxy/`, `~/realagent/`), its own config folder (`~/.maxy/`, `~/.realagent/`), its own web port, its own Cloudflare tunnel state, its own edge systemd unit (`maxy-edge.service` vs `realagent-edge.service`), and by default its own Neo4j database (Maxy on bolt port 7687, Real Agent on 7688). Action runner units are transient and per-invocation, not per-brand, so no naming conflict is possible.
|
|
103
|
-
- **Brand-isolated Neo4j (Task 787):** when a brand provisions a dedicated Neo4j instance (any port other than 7687), the installer stops and disables the apt-package's system `neo4j.service` after enabling the brand-dedicated unit, so only one Neo4j process holds the shared `/var/lib/neo4j/run/` PID file. The seed step receives the brand-correct `NEO4J_URI` and `NEO4J_PASSWORD` as explicit environment variables — the seed script no longer carries a `bolt://localhost:7687` default. A failed dedicated start aborts the install loudly with a journalctl tail; there is no silent fallback to the system instance. Stop/disable targets the literal `neo4j.service` only, so
|
|
103
|
+
- **Brand-isolated Neo4j (Task 787):** when a brand provisions a dedicated Neo4j instance (any port other than 7687), the installer stops and disables the apt-package's system `neo4j.service` after enabling the brand-dedicated unit, so only one Neo4j process holds the shared `/var/lib/neo4j/run/` PID file. The seed step receives the brand-correct `NEO4J_URI` and `NEO4J_PASSWORD` as explicit environment variables — the seed script no longer carries a `bolt://localhost:7687` default. A failed dedicated start aborts the install loudly with a journalctl tail; there is no silent fallback to the system instance. Stop/disable targets the literal `neo4j.service` only, so peer brands running their own `neo4j-{brand}.service` are unaffected.
|
|
104
|
+
- **Multi-brand-host caveat (Task 800):** on a host where the default brand keeps using the shared system `neo4j.service` per Task 659 V3 (e.g. a laptop running Maxy on default port 7687 alongside a branded build on a dedicated port), the Task 787 disable above kills the default brand's database when a branded installer runs. Symptom: default-brand admin server keeps running but bolt port 7687 has no listener; the branded install log contains `[neo4j] disabling system unit (brand-dedicated active on port <branded-port>)` immediately followed by `Stopping neo4j.service` in the systemd journal. Data on disk is intact (`disable` only removes the WantedBy symlink); recovery is `sudo systemctl enable neo4j && sudo systemctl start neo4j`. Task 800 adds a peer-brand guard so the disable is skipped when any `~/.<brand>/.env` pins `NEO4J_URI=bolt://localhost:7687`. Until Task 800 lands, repeat the recovery after each branded `create-{brand}` upgrade on a multi-brand host.
|
|
104
105
|
- **Shared:** both brands share the system Chromium/VNC stack, the Ollama model server, and the `cloudflared` command itself. Browser automation is serialised — one admin session at a time across both brands.
|
|
105
106
|
|
|
106
107
|
To install a second brand on a device that already runs the first, just run the other installer. No flags needed for isolation:
|
|
@@ -106,6 +106,8 @@ Every new node in {{productName}}'s graph is created with at least one connectio
|
|
|
106
106
|
|
|
107
107
|
Every node also carries a provenance stamp — which agent wrote it, in which session, via which tool. You never see these fields, but they are how operators trace unusual growth back to the code path that produced it, and why your graph stays clean over time.
|
|
108
108
|
|
|
109
|
+
**Two write surfaces, one substrate.** General agents write through schema-aware helpers — {{productName}} can record a new contact, a new commitment, a new preference without ever typing a database query, and the helper enforces the connection-and-provenance rule above structurally. The graph-steward role (the specialist {{productName}} dispatches when you ask for graph hygiene — "merge those two duplicate contacts," "wire those four tasks to the meeting," "rename the legacy label across the graph") additionally has a raw Cypher write tool for the multi-step operations the helpers cannot express. The steward role internalises the same connection-and-provenance discipline in its prompt; a post-write audit emits a warning on every breach so the same rules apply to both surfaces. Both paths feed the same hourly orphan trend and the same forensic provenance fields — read-side, you cannot tell the two apart, and that is the point.
|
|
110
|
+
|
|
109
111
|
## Privacy
|
|
110
112
|
|
|
111
113
|
All memory is stored on your local Raspberry Pi. The Neo4j database never leaves your network. {{productName}} does not sync memory to any cloud service or third party.
|
|
@@ -121,6 +121,22 @@ If the initial Cloudflare login fails during setup, {{productName}} will fall ba
|
|
|
121
121
|
|
|
122
122
|
---
|
|
123
123
|
|
|
124
|
+
## "Bad Gateway" or holding page during an upgrade
|
|
125
|
+
|
|
126
|
+
Task 795 — `maxy-edge.service` (always-on front door) classifies upstream errors and serves a brand-aware response. There are two distinct user-visible shapes; the right one depends on what failed.
|
|
127
|
+
|
|
128
|
+
**Branded holding page ("{{productName}} is starting") for ~10 s during an upgrade — this is expected and self-healing.** The edge process binds the public port immediately, but `maxy.service` (the upstream UI) takes ~10 s after restart to apply the neo4j schema and mount its 11 routes. Any browser navigation that lands during that window gets a self-contained HTML holding page that polls `/api/health` and reloads automatically once the upstream binds. No operator action required. The diagnostic line in `~/.maxy/logs/edge.log` is `[edge] upstream http error path=… err=connect ECONNREFUSED 127.0.0.1:<UPSTREAM_PORT> err-class=econnrefused-coldstart upstream=…` and disappears as soon as upstream binds.
|
|
129
|
+
|
|
130
|
+
**Branded plain-text 502 ("Bad Gateway ({{productName}} unavailable)") — real upstream failure, not cold-start.** Any error class other than `ECONNREFUSED` (timeouts, resets, host-unreachable) returns the existing 502 path. The diagnostic line carries `err-class=other`. Read the log with `tail -200 ~/.maxy/logs/edge.log | rg 'err-class=other'` and check `~/.maxy/logs/server.log` for upstream stack traces — the upstream itself is the source.
|
|
131
|
+
|
|
132
|
+
**Continuous `err-class=econnrefused-coldstart` for >30 s past the last `[edge] listening` line** indicates the upstream never binds — the upgrade or boot has stalled. Recover via `sudo systemctl --user status maxy.service` and check the action runner log per the next section. Permanent-failure UI escalation (turning the holding page into an error after N seconds) is intentionally deferred.
|
|
133
|
+
|
|
134
|
+
**The literal string `maxy-ui` should never appear in `edge.log` or in any user-visible 502 body**, regardless of brand. If it does, the edge is running pre-Task-795 code — re-bundle and re-publish.
|
|
135
|
+
|
|
136
|
+
**Verifying the holding page locally:** `curl -sS -H 'Accept: text/html' http://127.0.0.1:<EDGE_PORT>/` while `maxy.service` is stopped should return HTML containing the brand `productName`. The `Accept: text/html` header is required — non-html clients (default `curl`, `fetch()`, XHR) get the branded plain-text 502 instead, so the holding page's own `/api/health` polls don't break themselves during cold-start.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
124
140
|
## Action runner — upgrade or Cloudflare setup appears stuck
|
|
125
141
|
|
|
126
142
|
Task 664 replaced the ttyd/xterm admin terminal with a detached action runner. Upgrades and Cloudflare setup now run under transient `systemd-run --user` units whose stdout+stderr land in a persisted per-action log, streamed to the browser via SSE. Task 666 moved the four routes that serve the modal (`/api/admin/actions/*`, `/api/admin/version`) onto `maxy-edge.service`, so the log panel's stream survives a mid-run restart of `maxy.service` without reconnecting.
|
|
@@ -3,7 +3,7 @@ name: database-operator
|
|
|
3
3
|
description: "Document and archive ingestion and ad-hoc graph operations — running the universal `document-ingest` skill for any unstructured document (PDF, text, transcript, web page, audio, video) and per-source archive-import skills (LinkedIn Basic Data Export today; CRM-type seed archives as each plugin ships), plus operator-driven graph hygiene (prune orphans, deduplicate entities, add edges, normalise labels). Delegate when the operator uploads any document, drops an archive directory into chat, or asks for any graph operation that is not a routine per-turn write."
|
|
4
4
|
summary: "Ingests every unstructured document and external archive into your graph (LinkedIn today; other CRM sources in future) and handles ad-hoc graph tidy-ups on request. For example, when you upload a CV, a pricing guide, or a contract; when you drop a LinkedIn export folder into chat; or when you ask to prune orphan nodes, merge duplicate people, or add edges between entities."
|
|
5
5
|
model: claude-sonnet-4-6
|
|
6
|
-
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
6
|
+
tools: Read, Bash, Glob, Grep, mcp__graph__maxy-graph-read_neo4j_cypher, mcp__graph__maxy-graph-write_neo4j_cypher, mcp__graph__maxy-graph-get_neo4j_schema, mcp__memory__memory-write, mcp__memory__memory-update, mcp__memory__memory-delete, mcp__memory__memory-search, mcp__memory__memory-rank, mcp__memory__memory-reindex, mcp__memory__memory-find-candidates, mcp__memory__memory-ingest, mcp__memory__memory-ingest-extract, mcp__memory__memory-ingest-web, mcp__memory__memory-classify, mcp__memory__memory-archive-write, mcp__memory__graph-prune-denylist-list, mcp__memory__graph-prune-denylist-add, mcp__memory__graph-prune-denylist-remove, mcp__contacts__contact-create, mcp__contacts__contact-update, mcp__contacts__contact-lookup, mcp__contacts__contact-list, mcp__admin__file-attach, mcp__admin__plugin-read
|
|
7
7
|
---
|
|
8
8
|
|
|
9
9
|
# Database Operator
|
|
@@ -124,6 +124,20 @@ Future CRM-type seed plugins (HubSpot, Salesforce, Pipedrive, iCloud contacts, G
|
|
|
124
124
|
|
|
125
125
|
When the admin delegates a graph operation — pruning, dedup, edge addition, label normalisation, schema-drift tidy — follow this discipline.
|
|
126
126
|
|
|
127
|
+
You wield two write surfaces. Wrapped writers (`memory-write`, `memory-update`, `memory-delete`, `memory-find-candidates`, `contact-create`, etc.) are schema-aware helpers that enforce orphan-prevention and provenance structurally — the right surface for single-node creates, contact ingestion, soft-delete sweeps. Raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher` is the right surface for the writes wrapped helpers cannot express: edges between two pre-existing nodes, multi-statement hygiene (`DETACH DELETE` orphans, `apoc.refactor.mergeNodes` duplicates, `REMOVE :OldLabel SET :NewLabel`), and any operation that scopes a transaction across multiple matched node sets. The graph is the account's knowledge substrate; raw Cypher is power tooling for the role responsible for that substrate.
|
|
128
|
+
|
|
129
|
+
### Graph stewardship doctrine — raw Cypher writes
|
|
130
|
+
|
|
131
|
+
Two rules govern every raw Cypher write you author. They require LLM judgement — the structural gate cannot make these calls for you. Two further rules (provenance, orphan-prevention) are now structurally enforced by the shim and need no prose discipline.
|
|
132
|
+
|
|
133
|
+
**1. Every node has ≥1 typed edge in the same transaction.** A bare `CREATE (n:Person {name: 'Ian'})` is data, not knowledge — retrieval finds the node but it carries no context. Anchor every node creation to the relationship that explains it: `MATCH (anchor) WHERE … CREATE (anchor)-[:TYPE]->(n:Label {…})`, or `MERGE` the node and its edge in the same statement. *Why:* a graph that accumulates islands defeats relationship-traversal queries — the value of the graph is in the edges, not the rows.
|
|
134
|
+
|
|
135
|
+
**2. Every edge type is in the live ontology.** Inventing types fragments retrieval — `KNOWS` ≠ `knows` ≠ `HAS_KNOWN`. Call `mcp__graph__maxy-graph-get_neo4j_schema` before authoring any write whose edge type you are not certain about; if no fitting type exists, stop and ask the admin agent for ontology guidance — never coin a synonym. *Why:* edge typology compounds over time. A synonym today blocks every future query that expected the canonical type, and the only fix is a label-rewrite Cypher pass that touches the same edge from both sides.
|
|
136
|
+
|
|
137
|
+
**Structural enforcement (Task 797).** The shim auto-stamps `createdAt`, `createdByAgent`, `createdByTool`, `createdBySession` on every `CREATE`/`MERGE` alias before forwarding to Neo4j — you do not write these properties yourself. The shim runs the cypher inside a managed `executeWrite` and self-audits for unattached nodes before committing; if any node you created has zero edges in the same transaction, the entire transaction rolls back and you receive a structured error naming the orphan label(s). Treat the rollback as a hard failure (do not retry the same cypher); your job is to author atomic CREATE/MERGE-with-edge statements per Rule 1, not to write defensive WITH/MATCH/RETURN audits or hand-written SET clauses for `createdBy*` fields. The `[graph-cypher-write]` audit lines (`auto-stamp applied`, `accepted`, `orphan-rollback`, `orphan-warning`, `missing-provenance-warning`, `unknown-type-warning`) name what the structural enforcement saw — they are observation surfaces, not duties.
|
|
138
|
+
|
|
139
|
+
The two rules together replace the LOUD-FAIL improvisation pattern that prior versions of this prompt prescribed when a wrapped writer lacked an edge-between-existing-nodes path. You no longer loud-fail on missing graph-write tools — you have them. You loud-fail on credentials, on out-of-surface tools (a skill prescribing a non-graph MCP token you do not hold), and on dispatched skills whose prerequisites are unmet — exactly as the LOUD-FAIL prerogative names.
|
|
140
|
+
|
|
127
141
|
### Before writing any Cypher
|
|
128
142
|
|
|
129
143
|
1. **Consult the SCHEMA block.** Your MCP tool set includes `maxy-graph-get_neo4j_schema` for live schema snapshots. Call it before authoring any write Cypher you are not certain about. The upstream cypher-mcp validator rejects writes with unknown labels or edges; catch the mismatch upfront, not at the rejection.
|
|
@@ -148,14 +162,14 @@ When two nodes represent the same real-world entity (two `:Person` rows for the
|
|
|
148
162
|
|
|
149
163
|
1. Read both via `memory-search` or direct `maxy-graph-read_neo4j_cypher`.
|
|
150
164
|
2. Decide which is the survivor (usually the older `createdAt`, or the one with richer properties). State the choice.
|
|
151
|
-
3.
|
|
152
|
-
4.
|
|
153
|
-
5. `memory-delete` the duplicate with a single-node call — the operator named this merge explicitly, no filter-token needed.
|
|
165
|
+
3. Property reconciliation: `memory-update` for property copies, or `write_neo4j_cypher` with `apoc.refactor.mergeNodes([survivor, duplicate])` when the merge is multi-edge (apoc reparents every relationship onto the survivor in one transaction). Stamp `mergedAt`, `mergedFromAgent`, `mergedFromSession` per the stewardship doctrine.
|
|
166
|
+
4. For property-only merges that leave edges unchanged, follow up with `memory-update` on the survivor and `memory-delete` on the duplicate. The duplicate delete is single-node, no filter-token needed (the operator named this merge explicitly).
|
|
154
167
|
|
|
155
168
|
### Edge addition, label normalisation, prune-denylist
|
|
156
169
|
|
|
157
|
-
- **Edge addition** — `
|
|
158
|
-
- **
|
|
170
|
+
- **Edge addition between two existing nodes** — raw Cypher via `mcp__graph__maxy-graph-write_neo4j_cypher`: `MATCH (a:LabelA {…}), (b:LabelB {…}) MERGE (a)-[r:TYPE]->(b) SET r.createdAt = datetime(), r.createdByAgent = $agent, r.createdByTool = 'graph-cypher-write', r.createdBySession = $sessionId`. The wrapped `memory-write` requires a new node payload — for edge-only writes, raw Cypher is the surface.
|
|
171
|
+
- **Edge addition that creates a new node** — `memory-write` with a `relationships` payload naming the exact edge type. Validator rejects unknown edge types structurally; re-read the schema before authoring.
|
|
172
|
+
- **Label normalisation** — `write_neo4j_cypher` with `MATCH (n:OldLabel) WHERE … REMOVE n:OldLabel SET n:NewLabel SET n.relabelledAt = datetime(), n.relabelledByAgent = $agent`. Bulk renames are schema changes — always confirm the owner / scope with the admin before executing across the whole graph.
|
|
159
173
|
- **Prune denylist** — when the operator wants to preserve specific nodes from future prune passes (current or scheduled-autonomous), use `graph-prune-denylist-add/remove/list` to manage the registry.
|
|
160
174
|
|
|
161
175
|
---
|