@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.
Files changed (61) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
  3. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts +2 -0
  4. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts.map +1 -0
  5. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +168 -0
  6. package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -0
  7. package/payload/platform/lib/graph-write/dist/index.d.ts +30 -2
  8. package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
  9. package/payload/platform/lib/graph-write/dist/index.js +111 -3
  10. package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
  11. package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +191 -0
  12. package/payload/platform/lib/graph-write/src/index.ts +112 -6
  13. package/payload/platform/neo4j/edge-annotations.json +0 -8
  14. package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
  15. package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
  16. package/payload/platform/neo4j/schema.cypher +25 -22
  17. package/payload/platform/plugins/admin/PLUGIN.md +1 -8
  18. package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
  19. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  20. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +24 -6
  21. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
  22. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
  23. package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
  24. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
  25. package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
  26. package/payload/platform/plugins/docs/references/internals.md +16 -0
  27. package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
  28. package/payload/platform/plugins/memory/PLUGIN.md +6 -0
  29. package/payload/platform/plugins/memory/mcp/dist/index.js +7 -2
  30. package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
  31. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
  32. package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
  33. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +8 -0
  34. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
  35. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +26 -2
  36. package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
  37. package/payload/platform/plugins/memory/references/schema-base.md +8 -0
  38. package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
  39. package/payload/platform/plugins/tasks/mcp/dist/index.js +10 -5
  40. package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
  41. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +27 -1
  42. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
  43. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -2
  44. package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
  45. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts +20 -1
  46. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts.map +1 -1
  47. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +46 -6
  48. package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
  49. package/payload/platform/scripts/logs-read.sh +8 -38
  50. package/payload/server/chunk-AJLGI7Y3.js +10067 -0
  51. package/payload/server/chunk-ON3LBL2Y.js +1114 -0
  52. package/payload/server/chunk-PXQA2MA3.js +2518 -0
  53. package/payload/server/client-pool-GBY5I2KQ.js +31 -0
  54. package/payload/server/maxy-edge.js +3 -3
  55. package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
  56. package/payload/server/public/assets/{admin-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
  57. package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
  58. package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
  59. package/payload/server/public/graph.html +2 -2
  60. package/payload/server/public/index.html +2 -2
  61. package/payload/server/server.js +790 -1896
@@ -13,11 +13,26 @@ export * from "./audit.js";
13
13
  * maps, and flat props are queryable — `MATCH (n) WHERE n.createdBySession
14
14
  * = $id RETURN n` is the forensic entry point).
15
15
  *
16
+ * Process provenance doctrine (Task 885): writes targeting any label in
17
+ * `ACTION_PROVENANCE_LABELS` (Person, UserProfile, AdminUser, Organization,
18
+ * LocalBusiness, CloudflareTunnel, CloudflareHostname) must include exactly
19
+ * one inbound `:PRODUCED` edge whose source is a `:Task` node. This makes
20
+ * every durable LLM-tool-driven entity creation traversable from the Task
21
+ * node that produced it, and forward from Task→Conversation via
22
+ * `:RAISED_DURING`. Bootstrap writes (`createdBy.agent === 'system'`) are
23
+ * exempt — installer / migration / lazy-create paths run as system writers.
24
+ *
16
25
  * Rejection paths (every one emits a stderr log the admin server pipes to
17
26
  * server.log, so orphan pressure is visible per-write not just in the
18
27
  * hourly [graph-health] signal):
19
- * - zero relationships → `[graph-write] reject reason=zero-relationships`
20
- * - unresolved target id → `[graph-write] reject reason=unresolved-target`
28
+ * - zero relationships → `[graph-write] reject reason=zero-relationships`
29
+ * - unresolved target id → `[graph-write] reject reason=unresolved-target`
30
+ * - missing action provenance → `[graph-write] reject reason=missing-action-provenance`
31
+ * - removed-feature write → `[graph-write] reject reason=removed-feature`
32
+ * (`:ReviewAlert` and `:Event {actionTool:"review-digest-compose"}` are
33
+ * deleted features; the gate catches doctrine-compliant writers
34
+ * re-introducing them. The vitest tombstone grep is the primary fence
35
+ * for raw `session.run("MERGE …")` callers that bypass this primitive.)
21
36
  *
22
37
  * The `createdBy.agent` field is advisory, not authoritative — it is sourced
23
38
  * from the MCP server's AGENT_SLUG env var, which is set by the trusted
@@ -26,6 +41,43 @@ export * from "./audit.js";
26
41
  * this field for access control — it is forensic, not a security boundary.
27
42
  */
28
43
 
44
+ /**
45
+ * Labels that require an inbound `:PRODUCED` edge from a `:Task` (Task 885).
46
+ * Bootstrap writes with `createdBy.agent === 'system'` are exempt — that
47
+ * carve-out covers PIN-setup `writeAdminUserAndPerson`, schema migrations,
48
+ * and the lazy `loadUserProfile` MERGE path that bypasses this primitive
49
+ * entirely (raw Cypher in `platform/ui/app/lib/neo4j-store.ts`).
50
+ *
51
+ * The set is intentionally not exhaustive — labels added here become a
52
+ * blocking gate for every LLM-tool write of that label. Add a label only
53
+ * after every legitimate writer of it can either (a) carry a Task-PRODUCED
54
+ * edge, or (b) declare itself as `agent: 'system'`.
55
+ */
56
+ export const ACTION_PROVENANCE_LABELS: ReadonlySet<string> = new Set([
57
+ "Person",
58
+ "UserProfile",
59
+ "AdminUser",
60
+ "Organization",
61
+ "LocalBusiness",
62
+ "CloudflareTunnel",
63
+ "CloudflareHostname",
64
+ ]);
65
+
66
+ function requiresActionProvenance(labels: readonly string[]): boolean {
67
+ for (const label of labels) {
68
+ if (ACTION_PROVENANCE_LABELS.has(label)) return true;
69
+ }
70
+ return false;
71
+ }
72
+
73
+ function findProducedFromTaskCandidates(
74
+ relationships: readonly GraphRelationship[],
75
+ ): GraphRelationship[] {
76
+ return relationships.filter(
77
+ (r) => r.type === "PRODUCED" && r.direction === "incoming",
78
+ );
79
+ }
80
+
29
81
  import type { Session } from "neo4j-driver";
30
82
 
31
83
  export interface GraphRelationship {
@@ -88,6 +140,26 @@ export async function writeNodeWithEdges(
88
140
  const agentLabel = createdBy.agent ?? createdBy.source ?? "unknown";
89
141
  const labelCsv = labels.join(",");
90
142
 
143
+ // Task 884: review-detector / review-digest feature is removed entirely.
144
+ // Reject any re-introduction via the doctrine surface. The actionTool check
145
+ // is property-keyed (not label-keyed) so a future scheduled-Event writer
146
+ // re-introducing the daily digest by tool name is caught here even though
147
+ // `:Event` is otherwise a valid label. Fires before the zero-relationships
148
+ // check because the dead-feature rejection is structurally more fundamental.
149
+ const reviewDigestActionTool =
150
+ typeof props.actionTool === "string" && props.actionTool === "review-digest-compose";
151
+ if (labels.includes("ReviewAlert") || reviewDigestActionTool) {
152
+ const actionToolField = reviewDigestActionTool
153
+ ? "review-digest-compose"
154
+ : "n/a";
155
+ process.stderr.write(
156
+ `[graph-write] reject reason=removed-feature labels=${labelCsv} actionTool=${actionToolField} agent=${agentLabel}\n`
157
+ );
158
+ throw new Error(
159
+ "Write doctrine violated: review-detector feature removed (Task 884) — `:ReviewAlert` and `:Event {actionTool:'review-digest-compose'}` writes are not allowed."
160
+ );
161
+ }
162
+
91
163
  if (!relationships || relationships.length < 1) {
92
164
  process.stderr.write(
93
165
  `[graph-write] reject reason=zero-relationships labels=${labelCsv} agent=${agentLabel}\n`
@@ -103,10 +175,16 @@ export async function writeNodeWithEdges(
103
175
  return await session.executeWrite(async (tx) => {
104
176
  const targetIds = relationships.map((r) => r.targetNodeId);
105
177
  const check = await tx.run(
106
- `UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN count(DISTINCT t) AS found`,
107
- { ids: targetIds }
178
+ `UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN elementId(t) AS id, labels(t) AS labels`,
179
+ { ids: targetIds },
108
180
  );
109
- const found = check.records[0].get("found").toNumber();
181
+
182
+ const labelsByTarget = new Map<string, string[]>();
183
+ for (const rec of check.records) {
184
+ labelsByTarget.set(rec.get("id") as string, rec.get("labels") as string[]);
185
+ }
186
+
187
+ const found = labelsByTarget.size;
110
188
  const uniqueRequested = new Set(targetIds).size;
111
189
  if (found !== uniqueRequested) {
112
190
  process.stderr.write(
@@ -117,6 +195,34 @@ export async function writeNodeWithEdges(
117
195
  );
118
196
  }
119
197
 
198
+ // Process provenance gate (Task 885). Labels in
199
+ // ACTION_PROVENANCE_LABELS require a `:Task` source on at least one
200
+ // inbound :PRODUCED edge so every durable LLM-tool entity write is
201
+ // traversable from the Task that produced it. Bootstrap writes
202
+ // (createdBy.agent === 'system') are exempt — installer + migration
203
+ // paths predate any Task. The check runs INSIDE the transaction so
204
+ // the labels-by-target map is consistent with target resolution.
205
+ let producedByTaskId: string | null = null;
206
+ if (
207
+ requiresActionProvenance(labels) &&
208
+ (createdBy.agent ?? "") !== "system"
209
+ ) {
210
+ const candidates = findProducedFromTaskCandidates(relationships);
211
+ const taskCandidates = candidates.filter((r) => {
212
+ const lbls = labelsByTarget.get(r.targetNodeId);
213
+ return Array.isArray(lbls) && lbls.includes("Task");
214
+ });
215
+ if (taskCandidates.length === 0) {
216
+ process.stderr.write(
217
+ `[graph-write] reject reason=missing-action-provenance labels=${labelCsv} agent=${agentLabel}\n`
218
+ );
219
+ throw new Error(
220
+ `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).`
221
+ );
222
+ }
223
+ producedByTaskId = taskCandidates[0].targetNodeId;
224
+ }
225
+
120
226
  let nodeRes;
121
227
  try {
122
228
  nodeRes = await tx.run(
@@ -187,7 +293,7 @@ export async function writeNodeWithEdges(
187
293
  }
188
294
 
189
295
  process.stderr.write(
190
- `[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"}\n`
296
+ `[graph-write] accepted labels=${labelCsv} edges=${edgesCreated} createdByAgent=${createdBy.agent ?? "unknown"} createdByTool=${createdBy.tool ?? createdBy.source ?? "unknown"} producedByTask=${producedByTaskId ?? "none"}\n`
191
297
  );
192
298
 
193
299
  return { nodeId, labels: nodeLabels, edgesCreated };
@@ -96,14 +96,6 @@
96
96
  "direction": "(Email)-[:RECEIVED_BY]->(Person)",
97
97
  "note": "Email recipient."
98
98
  },
99
- "RAISED_BY": {
100
- "direction": "(ReviewAlert)-[:RAISED_BY]->(*)",
101
- "note": "Alert provenance."
102
- },
103
- "AFFECTS": {
104
- "direction": "(ReviewAlert)-[:AFFECTS]->(*)",
105
- "note": "Alert impact surface."
106
- },
107
99
  "BLOCKS": {
108
100
  "direction": "(Task)-[:BLOCKS]->(Task)",
109
101
  "note": "Task dependency."
@@ -5,10 +5,9 @@
5
5
  * `${DATA_ROOT}/accounts/<uuid>/account.json`. Idempotent — silent when
6
6
  * the graph is already clean.
7
7
  *
8
- * Why a backstop, not a writer fix: the leaked nodes were written by the
9
- * `review-digest-compose` writer, which has since been removed in favour
10
- * of a coming gbrain rewrite. With no live writer to fix, this is the
11
- * surface that catches future writer drift before the next gbrain ships.
8
+ * Why a backstop, not a writer fix: the leaked nodes were written by a
9
+ * since-removed writer. With no live writer to fix, this is the surface
10
+ * that catches future writer drift.
12
11
  *
13
12
  * Hard guard: refuses to run when the on-disk account set is empty
14
13
  * (corrupt-install scenario). Refusing to wipe the graph is louder than
@@ -0,0 +1,102 @@
1
+ /**
2
+ * Migration 005 — Remove review-detector / review-digest feature artefacts (Task 884).
3
+ *
4
+ * The review-detector subsystem was wired into boot, MERGEd a daily `:Event`
5
+ * with `actionTool="review-digest-compose"` on every start, and aggregated
6
+ * `:ReviewAlert` nodes via rule matches. The feature is deleted entirely;
7
+ * this migration is the boot-sweep that removes the residue from existing
8
+ * installs. Idempotent — once the graph is clean, both queries DETACH DELETE
9
+ * zero rows and the log line records `events=0 alerts=0`.
10
+ *
11
+ * Filesystem artefacts (`review.log`, `review-rules.json`, `review-state.json`,
12
+ * `review-pending-alerts.jsonl`) are also removed best-effort — ENOENT is the
13
+ * idempotent no-op; any other errno is logged and ignored so a sticky permission
14
+ * problem does not block boot.
15
+ *
16
+ * Same structural-type pattern as 004-prune-alien-accounts.ts: the runner sits
17
+ * outside `platform/ui/` so depending on the workspace dedupe state the
18
+ * `neo4j-driver` import resolves to a different node_modules copy, and TS
19
+ * treats two same-shape types from different paths as nominally distinct.
20
+ */
21
+
22
+ import { resolve } from "node:path";
23
+ import { unlinkSync } from "node:fs";
24
+ import { homedir } from "node:os";
25
+ import { basename, dirname } from "node:path";
26
+
27
+ type Neo4jDriverLike = {
28
+ session(): {
29
+ run(
30
+ cypher: string,
31
+ params?: Record<string, unknown>,
32
+ ): Promise<{ records: Array<{ get(key: string): unknown }> }>;
33
+ close(): Promise<void>;
34
+ };
35
+ };
36
+
37
+ export async function removeReviewFeature(
38
+ driver: Neo4jDriverLike,
39
+ platformRoot: string,
40
+ ): Promise<void> {
41
+ const session = driver.session();
42
+ let eventsDeleted = 0;
43
+ let alertsDeleted = 0;
44
+
45
+ try {
46
+ const eventResult = await session.run(
47
+ `MATCH (e:Event)
48
+ WHERE e.actionTool = "review-digest-compose"
49
+ OR e.eventId STARTS WITH "review-digest-"
50
+ DETACH DELETE e
51
+ RETURN count(e) AS deleted`,
52
+ );
53
+ eventsDeleted = toNumber(eventResult.records[0]?.get("deleted"));
54
+
55
+ const alertResult = await session.run(
56
+ `MATCH (a:ReviewAlert)
57
+ DETACH DELETE a
58
+ RETURN count(a) AS deleted`,
59
+ );
60
+ alertsDeleted = toNumber(alertResult.records[0]?.get("deleted"));
61
+ } finally {
62
+ await session.close();
63
+ }
64
+
65
+ // Best-effort filesystem cleanup. The configDir convention is
66
+ // `~/.<installDirName>` where installDir = dirname(platformRoot)
67
+ // (e.g. `~/maxy/platform` → `~/.maxy`).
68
+ const installDirName = basename(dirname(platformRoot));
69
+ const configDir = resolve(homedir(), `.${installDirName}`);
70
+ const artefacts = [
71
+ resolve(configDir, "logs", "review.log"),
72
+ resolve(configDir, "review-rules.json"),
73
+ resolve(configDir, "review-state.json"),
74
+ resolve(configDir, "review-pending-alerts.jsonl"),
75
+ ];
76
+ for (const path of artefacts) {
77
+ try {
78
+ unlinkSync(path);
79
+ } catch (err) {
80
+ const code = (err as NodeJS.ErrnoException).code;
81
+ if (code === "ENOENT") continue;
82
+ console.error(
83
+ `[migration] removed-review-feature unlink-skip path=${path} reason=${code ?? "unknown"}`,
84
+ );
85
+ }
86
+ }
87
+
88
+ console.error(
89
+ `[migration] removed-review-feature events=${eventsDeleted} alerts=${alertsDeleted}`,
90
+ );
91
+ }
92
+
93
+ function toNumber(v: unknown): number {
94
+ if (typeof v === "number") return v;
95
+ if (
96
+ v &&
97
+ typeof (v as { toNumber?: () => number }).toNumber === "function"
98
+ ) {
99
+ return (v as { toNumber: () => number }).toNumber();
100
+ }
101
+ return 0;
102
+ }
@@ -284,7 +284,6 @@ OPTIONS {
284
284
  // - Workflows: Workflow, WorkflowStep, WorkflowRun, StepResult
285
285
  // - Onboarding: OnboardingState
286
286
  // - Email: Email, EmailAccount
287
- // - Review signals: ReviewAlert
288
287
  // - CV/career sublabels: Position, Credential
289
288
  // - Public agents: Agent (Task 837 — projection of file-based public agents)
290
289
  //
@@ -314,7 +313,7 @@ FOR (n:LocalBusiness|Service|PriceSpecification|OpeningHoursSpecification|Organi
314
313
  |Conversation|AdminConversation|PublicConversation|Message|UserMessage|AssistantMessage|ToolCall
315
314
  |Task|Project|Event
316
315
  |Workflow|WorkflowStep|WorkflowRun|StepResult
317
- |OnboardingState|Email|EmailAccount|ReviewAlert
316
+ |OnboardingState|Email|EmailAccount
318
317
  |Position|Credential|Agent)
319
318
  ON EACH [n.name, n.firstName, n.lastName, n.givenName, n.familyName,
320
319
  n.title, n.currentTitle, n.summary, n.body, n.content, n.text, n.description, n.headline, n.abstract,
@@ -832,26 +831,6 @@ FOR (au:AdminUser) REQUIRE au.userId IS UNIQUE;
832
831
  CREATE INDEX graph_preference_account_user IF NOT EXISTS
833
832
  FOR (p:GraphPreference) ON (p.accountId, p.userId);
834
833
 
835
- // ----------------------------------------------------------
836
- // ReviewAlert — review-detector rule-match aggregation
837
- //
838
- // One alert per (accountId, ruleId); subsequent rule matches bump
839
- // lastMatchAt and cumulativeMatchCount via ON MATCH. Written by
840
- // platform/ui/app/lib/review-detector/writer.ts, read by the admin
841
- // chat's alert-surfacing tools. Listed in FILTER_EXCLUDED_LABELS
842
- // (graph-labels.ts, Task 626) so it never surfaces as a /graph filter
843
- // row, but registered in GRAPH_LABEL_COLOURS so neighbourhood-mode
844
- // drilldown / search hits can still render it.
845
- //
846
- // Composite MERGE key (accountId, ruleId) — no UNIQUE constraint for
847
- // the same reason as GraphPreference: the composite MERGE is
848
- // idempotent and a composite constraint would add schema surface
849
- // for no behavioural gain.
850
- // ----------------------------------------------------------
851
-
852
- CREATE INDEX review_alert_account_rule IF NOT EXISTS
853
- FOR (a:ReviewAlert) ON (a.accountId, a.ruleId);
854
-
855
834
  // ----------------------------------------------------------
856
835
  // ToolCall — durable audit trail for agent tool invocations
857
836
  //
@@ -1039,3 +1018,27 @@ FOR (n:Section) ON (n.createdBySession);
1039
1018
 
1040
1019
  CREATE INDEX task_created_by_session IF NOT EXISTS
1041
1020
  FOR (n:Task) ON (n.createdBySession);
1021
+
1022
+ // ----------------------------------------------------------
1023
+ // CloudflareTunnel / CloudflareHostname (Task 885) — graph audit of
1024
+ // the tunnel-login deterministic flow. Written by the
1025
+ // /api/admin/cloudflare/setup endpoint after setup-tunnel.sh exits 0,
1026
+ // linked via (Task {kind:'cloudflare-tunnel-login'})-[:PRODUCED]->.
1027
+ // CloudflareHostname carries the routed FQDN; CloudflareTunnel
1028
+ // carries the connector identity. Source of truth remains the on-disk
1029
+ // cert.pem + tunnel.state + config.yml — these nodes are the graph
1030
+ // projection so operators can MATCH from a Conversation to "which
1031
+ // cloudflare resources did this onboarding produce?"
1032
+ // ----------------------------------------------------------
1033
+
1034
+ CREATE CONSTRAINT cloudflare_tunnel_id_unique IF NOT EXISTS
1035
+ FOR (t:CloudflareTunnel) REQUIRE t.tunnelId IS UNIQUE;
1036
+
1037
+ CREATE INDEX cloudflare_tunnel_account IF NOT EXISTS
1038
+ FOR (t:CloudflareTunnel) ON (t.accountId);
1039
+
1040
+ CREATE CONSTRAINT cloudflare_hostname_unique IF NOT EXISTS
1041
+ FOR (h:CloudflareHostname) REQUIRE (h.accountId, h.hostnameValue) IS UNIQUE;
1042
+
1043
+ CREATE INDEX cloudflare_hostname_account IF NOT EXISTS
1044
+ FOR (h:CloudflareHostname) ON (h.accountId);
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  name: admin
3
- description: "Platform administration plugin. Provides system-status, brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, render-component, session-reset, session-resume, file-attach, wifi, review-cadence tools (review-rules-list, review-rules-suppress, review-rules-unsuppress, review-rules-add, review-rules-remove, review-alerts-recent), adherence-read (attention-weighted adherence ledger), and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
3
+ description: "Platform administration plugin. Provides system-status, brand-settings, account-manage, account-update, admin-add, admin-remove, admin-list, admin-update-pin, agent-list, agent-config-read, logs-read, plugin-read, render-component, session-reset, session-resume, file-attach, wifi, adherence-read (attention-weighted adherence ledger), and action-approval tools (action-pending, action-approve, action-reject, action-edit) for managing the Maxy platform."
4
4
  tools:
5
5
  - system-status
6
6
  - brand-settings
@@ -18,12 +18,6 @@ tools:
18
18
  - session-reset
19
19
  - session-resume
20
20
  - file-attach
21
- - review-rules-list
22
- - review-rules-suppress
23
- - review-rules-unsuppress
24
- - review-rules-add
25
- - review-rules-remove
26
- - review-alerts-recent
27
21
  - adherence-read
28
22
  - action-pending
29
23
  - action-approve
@@ -40,7 +34,6 @@ hidden:
40
34
  - onboarding-step9-mode
41
35
  - qr-generate
42
36
  - wifi
43
- - review-digest-compose
44
37
  always: true
45
38
  embed: false
46
39
  ---
@@ -17,7 +17,6 @@ import { homedir, hostname as osHostname } from "node:os";
17
17
  import QRCode from "qrcode";
18
18
  import { getSession, closeDriver } from "./lib/neo4j.js";
19
19
  import { getOnboardingState, completeOnboardingStep } from "./lib/onboarding.js";
20
- import { registerReviewTools } from "./lib/review-tools.js";
21
20
  const server = new McpServer({
22
21
  name: "admin",
23
22
  version: "0.1.0",
@@ -1179,8 +1178,8 @@ server.tool("agent-list", "List all public (non-admin) agents with their full co
1179
1178
  };
1180
1179
  }
1181
1180
  });
1182
- server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agent-stream/error/session/public) are now per-conversation — pass `conversationId` to retrieve a single conversation's log from first [spawn] to final [process-exit]. type=agent-stream: per-conversation tool-use/tool-result archive (every `[tool-use]` and `[tool-result]` pair with full input + output JSON, plus raw Claude stream-json, agent events, and MCP server stderr via tee). USE THIS when investigating what an agent ACTUALLY did with its tools — server.log only carries `[persist] tool-call persisted` markers, not bodies. (`type=system` is a backwards-compatible alias for the same archive.) type=session: SSE events sent to client. type=error: Claude subprocess stderr (raw — NODE_DEBUG HTTP/NET/UNDICI traces land in agent-stream via the stream tee, not here). type=heartbeat: platform event dispatcher (check-due-events cron). type=public: public agent diagnostic log. type=server: platform server log. type=mcp: MCP server stderr (per-plugin raw). type=vnc: VNC browser viewer lifecycle. type=review: log-review detector decisions. sessionKey: grep legacy sessionKey-tagged lines across all logs (useful for pre-Task-532 artefacts). When conversationId is provided, reads the single per-conversation file for the requested type (or dumps all type files for that conversationId if type is omitted).", {
1183
- type: z.enum(["agent-stream", "system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc", "review"]).optional(),
1181
+ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agent-stream/error/session/public) are now per-conversation — pass `conversationId` to retrieve a single conversation's log from first [spawn] to final [process-exit]. type=agent-stream: per-conversation tool-use/tool-result archive (every `[tool-use]` and `[tool-result]` pair with full input + output JSON, plus raw Claude stream-json, agent events, and MCP server stderr via tee). USE THIS when investigating what an agent ACTUALLY did with its tools — server.log only carries `[persist] tool-call persisted` markers, not bodies. (`type=system` is a backwards-compatible alias for the same archive.) type=session: SSE events sent to client. type=error: Claude subprocess stderr (raw — NODE_DEBUG HTTP/NET/UNDICI traces land in agent-stream via the stream tee, not here). type=heartbeat: platform event dispatcher (check-due-events cron). type=public: public agent diagnostic log. type=server: platform server log. type=mcp: MCP server stderr (per-plugin raw). type=vnc: VNC browser viewer lifecycle. sessionKey: grep legacy sessionKey-tagged lines across all logs (useful for pre-Task-532 artefacts). When conversationId is provided, reads the single per-conversation file for the requested type (or dumps all type files for that conversationId if type is omitted).", {
1182
+ type: z.enum(["agent-stream", "system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc"]).optional(),
1184
1183
  lines: z.number().optional(),
1185
1184
  sessionKey: z.string().optional(),
1186
1185
  conversationId: z.string().optional(),
@@ -1223,7 +1222,7 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agen
1223
1222
  const prefix = prefixMap[resolvedType];
1224
1223
  if (!prefix) {
1225
1224
  return {
1226
- content: [{ type: "text", text: `type=${resolvedType} is not per-conversation. Valid per-conversation types: agent-stream, error, session, public. For platform-scoped types (server, vnc, review, heartbeat, mcp) omit conversationId.` }],
1225
+ content: [{ type: "text", text: `type=${resolvedType} is not per-conversation. Valid per-conversation types: agent-stream, error, session, public. For platform-scoped types (server, vnc, heartbeat, mcp) omit conversationId.` }],
1227
1226
  isError: true,
1228
1227
  };
1229
1228
  }
@@ -1276,11 +1275,11 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agen
1276
1275
  };
1277
1276
  // When a type is explicitly provided, search only that type's files.
1278
1277
  // When type is omitted, search all prefix-based log types (excludes heartbeat — no session context).
1279
- // server, vnc, and review are single-file (configDir-scoped), so they have no prefix search;
1278
+ // server and vnc are single-file (configDir-scoped), so they have no prefix search;
1280
1279
  // their matches are produced by dedicated blocks below.
1281
- const searchPrefixes = type && type !== "heartbeat" && type !== "server" && type !== "vnc" && type !== "review" && prefixes[type]
1280
+ const searchPrefixes = type && type !== "heartbeat" && type !== "server" && type !== "vnc" && prefixes[type]
1282
1281
  ? { [type]: prefixes[type] }
1283
- : (type === "server" || type === "vnc" || type === "review") ? {} : prefixes;
1282
+ : (type === "server" || type === "vnc") ? {} : prefixes;
1284
1283
  const allFiles = readdirSync(LOG_DIR).filter(f => f.endsWith(".log"));
1285
1284
  const sections = [];
1286
1285
  // Search account-scoped prefix-based log files
@@ -1341,25 +1340,6 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agen
1341
1340
  }
1342
1341
  }
1343
1342
  }
1344
- // Search review.log (platform-scoped, in configDir) — the
1345
- // proactive log-review detector's self-log (Task 385).
1346
- const includeReview = !type || type === "review";
1347
- if (includeReview) {
1348
- const reviewLog = resolve(CONFIG_DIR, "logs", "review.log");
1349
- if (existsSync(reviewLog)) {
1350
- try {
1351
- const result = execFileSync("grep", ["-F", sessionKey, reviewLog], { timeout: 5000 }).toString().trim();
1352
- if (result) {
1353
- sections.push(`## review.log (review)\n${result}`);
1354
- }
1355
- }
1356
- catch (grepErr) {
1357
- const exitCode = grepErr.status;
1358
- if (exitCode !== 1)
1359
- throw grepErr;
1360
- }
1361
- }
1362
- }
1363
1343
  if (sections.length === 0) {
1364
1344
  return { content: [{ type: "text", text: `No log lines found for session key "${sessionKey}".` }] };
1365
1345
  }
@@ -1401,20 +1381,6 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agen
1401
1381
  }).toString();
1402
1382
  return { content: [{ type: "text", text: `# vnc-boot.log\n\n${result}` }] };
1403
1383
  }
1404
- // Review log is a single platform-scoped file written by the in-process
1405
- // review detector (Task 385). Every scan cycle, rule match, suppression,
1406
- // rate-limit decision, admin-tool rule mutation, and watchdog event is
1407
- // here. Use this to answer "did the detector see X?" questions.
1408
- if (resolvedType === "review") {
1409
- const reviewLogFile = resolve(CONFIG_DIR, "logs", "review.log");
1410
- if (!existsSync(reviewLogFile)) {
1411
- return { content: [{ type: "text", text: `No review log found at ${reviewLogFile} (detector may not have started yet)` }] };
1412
- }
1413
- const result = execFileSync("tail", ["-n", String(lines), reviewLogFile], {
1414
- timeout: 5000,
1415
- }).toString();
1416
- return { content: [{ type: "text", text: `# review.log\n\n${result}` }] };
1417
- }
1418
1384
  // Task 850 — agent-stream and the legacy system alias both map to
1419
1385
  // claude-agent-stream-. The fall-through default also covers them.
1420
1386
  const prefix = resolvedType === "error" ? "claude-agent-stderr-"
@@ -3229,10 +3195,6 @@ server.tool("adherence-read", "Read the attention-weighted adherence ledger for
3229
3195
  };
3230
3196
  }
3231
3197
  });
3232
- // Review cadence tools (Task 385) — list, suppress, add, remove, etc.
3233
- // Registered as the final tool bundle because they depend on CONFIG_DIR
3234
- // and ACCOUNT_ID being resolved above.
3235
- registerReviewTools(server, { configDir: CONFIG_DIR, accountId: ACCOUNT_ID });
3236
3198
  // Cleanup on exit (SIGTERM for systemd service stops on Pi)
3237
3199
  const cleanup = async () => {
3238
3200
  await closeDriver();