@rubytech/create-realagent 1.0.817 → 1.0.819
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/src/__tests__/fulltext-coverage.test.ts +1 -1
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts +2 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js +168 -0
- package/payload/platform/lib/graph-write/dist/__tests__/action-provenance-gate.test.js.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +30 -2
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +111 -3
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -1
- package/payload/platform/lib/graph-write/src/__tests__/action-provenance-gate.test.ts +191 -0
- package/payload/platform/lib/graph-write/src/index.ts +112 -6
- package/payload/platform/neo4j/edge-annotations.json +0 -8
- package/payload/platform/neo4j/migrations/004-prune-alien-accounts.ts +3 -4
- package/payload/platform/neo4j/migrations/005-removed-review-feature.ts +102 -0
- package/payload/platform/neo4j/schema.cypher +25 -22
- package/payload/platform/plugins/admin/PLUGIN.md +1 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js +6 -44
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +24 -6
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.d.ts.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js +2 -3
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/cloudflared.js.map +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js +1 -1
- package/payload/platform/plugins/cloudflare/mcp/dist/lib/setup-orchestrator.js.map +1 -1
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +1 -1
- package/payload/platform/plugins/memory/PLUGIN.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +7 -2
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js +1 -1
- package/payload/platform/plugins/memory/mcp/dist/lib/graph-write-gate.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +8 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +26 -2
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/schema-base.md +8 -0
- package/payload/platform/plugins/tasks/PLUGIN.md +2 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js +10 -5
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +27 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -2
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts +20 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js +46 -6
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-update.js.map +1 -1
- package/payload/platform/scripts/logs-read.sh +8 -38
- package/payload/server/chunk-AJLGI7Y3.js +10067 -0
- package/payload/server/chunk-ON3LBL2Y.js +1114 -0
- package/payload/server/chunk-PXQA2MA3.js +2518 -0
- package/payload/server/client-pool-GBY5I2KQ.js +31 -0
- package/payload/server/maxy-edge.js +3 -3
- package/payload/server/neo4j-migrations-STCKDWAL.js +364 -0
- package/payload/server/public/assets/{admin-2w0XSMC6.js → admin-CdVYoqKD.js} +1 -1
- package/payload/server/public/assets/{graph-C4-jEPDE.js → graph-DeH6ulGh.js} +1 -1
- package/payload/server/public/assets/{page-zuI00fuC.js → page-WIAWD2Oi.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +790 -1896
|
@@ -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
|
|
20
|
-
* - unresolved target id
|
|
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
|
|
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
|
-
|
|
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
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
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,
|
|
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.
|
|
1183
|
-
type: z.enum(["agent-stream", "system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc"
|
|
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,
|
|
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
|
|
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" &&
|
|
1280
|
+
const searchPrefixes = type && type !== "heartbeat" && type !== "server" && type !== "vnc" && prefixes[type]
|
|
1282
1281
|
? { [type]: prefixes[type] }
|
|
1283
|
-
: (type === "server" || type === "vnc"
|
|
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();
|