@rubytech/create-realagent 1.0.818 → 1.0.820
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-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 +28 -5
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/graph-write/dist/index.js +97 -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 +90 -9
- package/payload/platform/neo4j/schema.cypher +24 -0
- package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +25 -8
- package/payload/platform/plugins/cloudflare/PLUGIN.md +1 -1
- package/payload/platform/plugins/cloudflare/scripts/setup-tunnel.sh +70 -20
- package/payload/platform/plugins/docs/references/cloudflare.md +1 -1
- package/payload/platform/plugins/docs/references/graph.md +20 -0
- package/payload/platform/plugins/docs/references/internals.md +16 -0
- package/payload/platform/plugins/email/PLUGIN.md +2 -0
- package/payload/platform/plugins/memory/PLUGIN.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +15 -4
- package/payload/platform/plugins/memory/mcp/dist/index.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/mcp/dist/tools/profile-update.d.ts +20 -0
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.js +40 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/profile-update.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/server/chunk-5OG7TUQL.js +315 -0
- package/payload/server/chunk-CZGOB575.js +593 -0
- package/payload/server/chunk-NUXYHO6N.js +10079 -0
- package/payload/server/chunk-SALVIGXH.js +1116 -0
- package/payload/server/chunk-ZT6LKDTP.js +2238 -0
- package/payload/server/client-pool-IQU6H43X.js +32 -0
- package/payload/server/cloudflare-task-tracker-Q4X5BYR7.js +17 -0
- package/payload/server/maxy-edge.js +4 -3
- package/payload/server/neo4j-migrations-CYIKMSEO.js +366 -0
- package/payload/server/public/assets/admin-BoGPEBe_.js +352 -0
- package/payload/server/public/assets/{graph-DeH6ulGh.js → graph-LLMJa4Ch.js} +1 -1
- package/payload/server/public/assets/{page-WIAWD2Oi.js → page-DoaF3DB0.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +276 -16
- package/payload/server/public/assets/admin-CdVYoqKD.js +0 -352
|
@@ -13,14 +13,24 @@ 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
|
|
21
|
-
* -
|
|
22
|
-
*
|
|
23
|
-
*
|
|
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
|
|
24
34
|
* re-introducing them. The vitest tombstone grep is the primary fence
|
|
25
35
|
* for raw `session.run("MERGE …")` callers that bypass this primitive.)
|
|
26
36
|
*
|
|
@@ -31,6 +41,43 @@ export * from "./audit.js";
|
|
|
31
41
|
* this field for access control — it is forensic, not a security boundary.
|
|
32
42
|
*/
|
|
33
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
|
+
|
|
34
81
|
import type { Session } from "neo4j-driver";
|
|
35
82
|
|
|
36
83
|
export interface GraphRelationship {
|
|
@@ -128,10 +175,16 @@ export async function writeNodeWithEdges(
|
|
|
128
175
|
return await session.executeWrite(async (tx) => {
|
|
129
176
|
const targetIds = relationships.map((r) => r.targetNodeId);
|
|
130
177
|
const check = await tx.run(
|
|
131
|
-
`UNWIND $ids AS id MATCH (t) WHERE elementId(t) = id RETURN
|
|
132
|
-
{ 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 },
|
|
133
180
|
);
|
|
134
|
-
|
|
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;
|
|
135
188
|
const uniqueRequested = new Set(targetIds).size;
|
|
136
189
|
if (found !== uniqueRequested) {
|
|
137
190
|
process.stderr.write(
|
|
@@ -142,6 +195,34 @@ export async function writeNodeWithEdges(
|
|
|
142
195
|
);
|
|
143
196
|
}
|
|
144
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
|
+
|
|
145
226
|
let nodeRes;
|
|
146
227
|
try {
|
|
147
228
|
nodeRes = await tx.run(
|
|
@@ -212,7 +293,7 @@ export async function writeNodeWithEdges(
|
|
|
212
293
|
}
|
|
213
294
|
|
|
214
295
|
process.stderr.write(
|
|
215
|
-
`[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`
|
|
216
297
|
);
|
|
217
298
|
|
|
218
299
|
return { nodeId, labels: nodeLabels, edgesCreated };
|
|
@@ -1018,3 +1018,27 @@ FOR (n:Section) ON (n.createdBySession);
|
|
|
1018
1018
|
|
|
1019
1019
|
CREATE INDEX task_created_by_session IF NOT EXISTS
|
|
1020
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);
|
|
@@ -201,21 +201,38 @@ Pin the operator's persona and bootstrap the graph nodes that satisfy the graph-
|
|
|
201
201
|
|
|
202
202
|
**Wait for the user's submission.** If the user picks "Other" or types free text instead of selecting, ask them which of the two personas best describes them and re-render the select. Do not proceed without one of the two documented modes — the agent must not improvise a third path. If the user pivots off-topic mid-flow, answer their question briefly and re-render the select; step 9 stays incomplete until they pick a mode.
|
|
203
203
|
|
|
204
|
-
**Call `onboarding-step9-mode` with the chosen mode before any graph write or skill invocation.** The tool emits the diagnostic log line and returns the deterministic next-action prose.
|
|
204
|
+
**Call `onboarding-step9-mode` with the chosen mode before any graph write or skill invocation.** The tool emits the diagnostic log line and returns the deterministic next-action prose.
|
|
205
|
+
|
|
206
|
+
**Open the action record with `task-create` (Task 885 process-provenance doctrine).** Before any graph write or skill invocation, call:
|
|
207
|
+
|
|
208
|
+
```
|
|
209
|
+
task-create
|
|
210
|
+
name: "Establish operator owner — onboarding step 9"
|
|
211
|
+
description: "<one-line summary of the chosen mode and what entities will be produced>"
|
|
212
|
+
status: "running"
|
|
213
|
+
kind: "onboarding-establish-owner"
|
|
214
|
+
inputsProvided: ["mode"]
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
The returned `taskId` is the action-provenance handle for this step — every subsequent `memory-write` for an action-provenance-gated label (`Person`, `UserProfile`, `AdminUser`, `Organization`, `LocalBusiness`) MUST pass it as `producedByTaskId` so the inbound `:PRODUCED` edge from the Task is composed into the write. The Task is auto-linked to the current `AdminConversation` via `RAISED_DURING` (this is what makes `MATCH (c:AdminConversation)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(entity)` traversable from the conversation that initiated onboarding).
|
|
218
|
+
|
|
219
|
+
Then branch on the mode.
|
|
205
220
|
|
|
206
221
|
### `business-owner`
|
|
207
222
|
|
|
208
|
-
Invoke the `business-profile` skill.
|
|
223
|
+
Invoke the `business-profile` skill, passing the `taskId` so the skill can thread it as `producedByTaskId` into every `memory-write` it issues. The skill follows its first-run path: create the `AdminUser` node, create the `LocalBusiness` node, create the `Organization` node, collect identity + address + whichever additional domains (hours, services, FAQs, brand assets) the user provides. When `business-profile` reports that the required nodes exist in the graph, call `task-update(appendStep:"business-profile-complete")` then write the `HAS_PROFILE` edge from the personal-profile `Person` to the operator's `UserProfile` via `memory-update` (one `memory-search` to resolve both elementIds; the `UserProfile` already exists from step 6 onwards via the lazy-create in `loadUserProfile`). Then call `task-complete(taskId)` and `onboarding-complete-step` with step 9. Do not mark step 9 complete before the required nodes + the HAS_PROFILE edge exist — the gate's precondition must be real, not just recorded.
|
|
209
224
|
|
|
210
225
|
### `personal`
|
|
211
226
|
|
|
212
|
-
Personal mode does not register a `LocalBusiness`. The `AdminUser` and personal-profile `Person` nodes were written deterministically at PIN setup time (Task 830 — `writeAdminUserAndPerson`), so this step only enriches the existing Person with
|
|
227
|
+
Personal mode does not register a `LocalBusiness`. The `AdminUser` and personal-profile `Person` nodes were written deterministically at PIN setup time (Task 830 — `writeAdminUserAndPerson`, run as `createdBy.agent === 'system'` and therefore exempt from the action-provenance gate), so this step only enriches the existing Person with operator-identity fields and links it to the `UserProfile`:
|
|
213
228
|
|
|
214
|
-
1. **Ask the user for their email** in one short conversational message — {{productName}}
|
|
215
|
-
2. **
|
|
216
|
-
3. **
|
|
217
|
-
4. **
|
|
229
|
+
1. **Ask the user for their email AND phone number** in one short conversational message — {{productName}} stores both on the personal-profile Person for downstream features (notifications, contact-method matching, identity-coverage signal that the agent uses to detect missing identity in future turns). The user may decline either; record what they provide. (Operator-identity fix: the system-prompt's "Identity coverage" block surfaces missing identity fields on every turn, so a partial answer becomes a follow-up prompt rather than a silent gap.)
|
|
230
|
+
2. **Attach the identity to Person.** Call `profile-update` with `personFields: { email: "<value>", telephone: "<value>" }` (omit either key the user declined). The tool resolves the personal-profile Person via `(au:AdminUser {userId:$you})-[:OWNS]->(p:Person)` server-side and writes the canonical `email`/`telephone` fields. Use canonical `telephone` — `phone` is the schema synonym, not the canonical name; `profile-update` rejects `phone` rather than silently rewriting it. The tool throws if the OWNS edge is missing rather than silently no-oping (the PIN-setup path is the only place that edge is created — a missing edge means PIN setup never ran or was rolled back).
|
|
231
|
+
3. **Append the step.** Call `task-update(appendStep:"identity-attached")`.
|
|
232
|
+
4. **Link the personal-profile `Person` to the `UserProfile`.** Call `memory-search` to resolve the `UserProfile` elementId for the operator (the lazy `loadUserProfile` write created it on the first admin session). Then call `memory-update` on the Person to add the `HAS_PROFILE` edge to the UserProfile. (`HAS_PROFILE` from `:Person` is a sibling pattern to the existing `AdminUser→HAS_PROFILE→UserProfile`; both are valid sources for the same edge type. See [schema-base.md Relationship Patterns](../../../memory/references/schema-base.md).)
|
|
233
|
+
5. **Close the action record.** Call `task-update(appendStep:"profile-linked")` then `task-complete(taskId)`.
|
|
234
|
+
6. **Mark step 9 complete.** Call `onboarding-complete-step` with step 9.
|
|
218
235
|
|
|
219
236
|
After step 9 completes in personal mode, tell the user that {{productName}} is configured for personal use — their employer (if any) is not registered here. If they later become the operator for a business of their own, they can ask {{productName}} to set up a business profile, which invokes the `business-profile` skill directly.
|
|
220
237
|
|
|
221
|
-
If the user declines to bootstrap during step 9 in any mode, leave step 9 incomplete. The next session will resume here
|
|
238
|
+
If the user declines to bootstrap during step 9 in any mode, leave step 9 incomplete AND call `task-update(taskId, status:"failed", errorMessage:"<one-line reason>")` so the action record reflects the abandonment instead of dangling in `running` forever. The next session will resume here with a fresh `task-create` (the prior failed Task stays in the graph as the audit record). Any attempt to write user-domain data will surface `Write blocked (no-admin-user)` or `Write blocked (no-local-business)` via the gate, pulling the agent back into this step.
|
|
@@ -30,7 +30,7 @@ The plugin registers no agent-facing MCP tools. Every Cloudflare operation is dr
|
|
|
30
30
|
|
|
31
31
|
| Script | Purpose |
|
|
32
32
|
|---|---|
|
|
33
|
-
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel
|
|
33
|
+
| [`scripts/setup-tunnel.sh`](scripts/setup-tunnel.sh) | Autonomous end-to-end setup: OAuth login, tunnel resolve (operator-supplied identity), DNS route, config + state, service restart, post-restart verification. Invocation: `~/setup-tunnel.sh <brand> <port> <admin-hostname> [<public-hostname>] [<apex-hostname>]`. Required env: `STREAM_LOG_PATH`, `ACCOUNT_DIR`, AND exactly one of `TUNNEL_ID` (operator selected an existing tunnel from `/api/admin/cloudflare/tunnels`) or `TUNNEL_NAME` (operator typed a name to create) per the operator-selected-tunnel fix. The pre-fix derivation `${BRAND}-$(hostname -s)` is removed — the operator's logged-in Cloudflare account is the source of truth for which tunnel exists. Apex hostnames print an `ACTION REQUIRED` block for the dashboard record the CLI cannot create. Step 1 (wrappers faithfully relay third-party CLI) spawns `cloudflared tunnel login`, extracts the argotunnel URL from its stdout, mechanically opens it on the Pi VNC chromium (`DISPLAY=${DISPLAY:-:99} /usr/bin/chromium <url> &`), then polls for `~/.cloudflared/cert.pem` while the operator clicks the zone row + Authorize on the VNC. 180 s budget with a 2-second `step=oauth-login result=awaiting-cert` heartbeat. No CDP auto-click, no DOM matcher. |
|
|
34
34
|
| [`scripts/reset-tunnel.sh`](scripts/reset-tunnel.sh) | Deletes every tunnel on the brand's CF account and wipes `${CFG_DIR}`. Does not touch the platform service, stray CNAMEs, or token-mode connectors — those require dashboard cleanup or `pkill`. Invocation: `~/reset-tunnel.sh <brand>`. No polling blocks — every long-wait is bounded by `cloudflared`'s network round-trip, so no heartbeat contract applies. |
|
|
35
35
|
|
|
36
36
|
### Skills
|
|
@@ -274,32 +274,82 @@ if [ ! -f "${CFG_DIR}/cert.pem" ]; then
|
|
|
274
274
|
fi
|
|
275
275
|
|
|
276
276
|
# --------------------------------------------------------------------------
|
|
277
|
-
# Step 2+3:
|
|
278
|
-
#
|
|
279
|
-
#
|
|
280
|
-
#
|
|
281
|
-
#
|
|
277
|
+
# Step 2+3: Resolve the tunnel identity from operator input (Task 886 §B).
|
|
278
|
+
#
|
|
279
|
+
# Pre-Task-886 the script derived TUNNEL_NAME locally as "${BRAND}-$(hostname
|
|
280
|
+
# -s)" and reused-or-created. That breaks the same doctrine as pre-Task-589
|
|
281
|
+
# zone selection: the local hostname has no authority over which tunnel the
|
|
282
|
+
# operator's logged-in Cloudflare account holds. A renamed device produced a
|
|
283
|
+
# new tunnel; existing CNAMEs continued to point at the old one; the chat
|
|
284
|
+
# said "Done. tunnel=maxy-maxytest" while the operator's `maxytest` hostname
|
|
285
|
+
# kept resolving via a stale orphan.
|
|
286
|
+
#
|
|
287
|
+
# New contract: the form (rendered via /api/admin/cloudflare/tunnels list) is
|
|
288
|
+
# the source of truth. The endpoint passes exactly one of:
|
|
289
|
+
# TUNNEL_ID — operator selected an existing tunnel from the list
|
|
290
|
+
# TUNNEL_NAME — operator typed a name to create a new tunnel
|
|
291
|
+
# The endpoint validates the exactly-one constraint; the script enforces it
|
|
292
|
+
# again here as defence in depth. Setting both, or neither, is a misuse.
|
|
282
293
|
# --------------------------------------------------------------------------
|
|
283
294
|
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
295
|
+
if [ -n "${TUNNEL_ID:-}" ] && [ -n "${TUNNEL_NAME:-}" ]; then
|
|
296
|
+
phase_line setup-tunnel step=tunnel-resolve result=error reason=both-set
|
|
297
|
+
echo "ERROR: TUNNEL_ID and TUNNEL_NAME are mutually exclusive — pass exactly one." >&2
|
|
298
|
+
exit 1
|
|
299
|
+
fi
|
|
300
|
+
if [ -z "${TUNNEL_ID:-}" ] && [ -z "${TUNNEL_NAME:-}" ]; then
|
|
301
|
+
phase_line setup-tunnel step=tunnel-resolve result=error reason=neither-set
|
|
302
|
+
echo "ERROR: TUNNEL_ID (selected) or TUNNEL_NAME (explicit-create) is required." >&2
|
|
303
|
+
echo " The form derives one of these from operator input via" >&2
|
|
304
|
+
echo " /api/admin/cloudflare/tunnels — re-run the form, do not" >&2
|
|
305
|
+
echo " invoke this script directly without one of the env vars." >&2
|
|
306
|
+
exit 1
|
|
307
|
+
fi
|
|
308
|
+
|
|
309
|
+
if [ -n "${TUNNEL_ID:-}" ]; then
|
|
310
|
+
# Operator-selected branch. Resolve the name back from `tunnel list` for
|
|
311
|
+
# the log line + tunnel.state file. A missing row here means the operator
|
|
312
|
+
# selected a tunnel that has since been deleted from another surface
|
|
313
|
+
# (rare but possible) — fail loudly so the operator sees the cause
|
|
314
|
+
# rather than getting a silent fallback to "create new".
|
|
315
|
+
TUNNEL_NAME="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json 2>/dev/null \
|
|
316
|
+
| jq -r --arg I "${TUNNEL_ID}" '.[]? | select(.id == $I) | .name' | head -1)"
|
|
317
|
+
if [ -z "${TUNNEL_NAME}" ] || [ "${TUNNEL_NAME}" = "null" ]; then
|
|
318
|
+
phase_line setup-tunnel step=tunnel-resolve result=error \
|
|
319
|
+
reason=selected-tunnel-not-found tunnel_id="${TUNNEL_ID}"
|
|
320
|
+
echo "ERROR: TUNNEL_ID ${TUNNEL_ID} is not on the logged-in Cloudflare account." >&2
|
|
321
|
+
echo " It may have been deleted from the dashboard since the form loaded." >&2
|
|
322
|
+
echo " Re-render the form to refresh the tunnel list." >&2
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
TUNNEL_SOURCE="operator-selected"
|
|
326
|
+
else
|
|
327
|
+
# Operator-create branch. Refuse to silently reuse an existing tunnel of
|
|
328
|
+
# the same name — that recreates the pre-Task-886 silent-collision bug.
|
|
329
|
+
# The operator should have picked it from the list.
|
|
330
|
+
EXISTING_ID="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json 2>/dev/null \
|
|
331
|
+
| jq -r --arg N "${TUNNEL_NAME}" '.[]? | select(.name == $N) | .id' | head -1)"
|
|
332
|
+
if [ -n "${EXISTING_ID}" ] && [ "${EXISTING_ID}" != "null" ]; then
|
|
333
|
+
phase_line setup-tunnel step=tunnel-resolve result=error \
|
|
334
|
+
reason=name-already-exists tunnel_name="${TUNNEL_NAME}" tunnel_id="${EXISTING_ID}"
|
|
335
|
+
echo "ERROR: a tunnel named ${TUNNEL_NAME} already exists (id=${EXISTING_ID})." >&2
|
|
336
|
+
echo " Re-render the form and select it from the list, or pick a different name." >&2
|
|
337
|
+
exit 1
|
|
338
|
+
fi
|
|
289
339
|
cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel create "${TUNNEL_NAME}"
|
|
290
340
|
TUNNEL_ID="$(cloudflared --origincert "${CFG_DIR}/cert.pem" tunnel list --output json \
|
|
291
341
|
| jq -r --arg N "${TUNNEL_NAME}" '.[]? | select(.name == $N) | .id' | head -1)"
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
342
|
+
if [ -z "${TUNNEL_ID}" ] || [ "${TUNNEL_ID}" = "null" ]; then
|
|
343
|
+
phase_line setup-tunnel step=tunnel-resolve result=error \
|
|
344
|
+
reason=create-then-uuid-missing tunnel_name="${TUNNEL_NAME}"
|
|
345
|
+
echo "ERROR: created tunnel ${TUNNEL_NAME} but its UUID is missing from tunnel list." >&2
|
|
346
|
+
exit 1
|
|
347
|
+
fi
|
|
348
|
+
TUNNEL_SOURCE="operator-created"
|
|
299
349
|
fi
|
|
300
|
-
phase_line setup-tunnel step=tunnel-resolve
|
|
301
|
-
tunnel_id="${TUNNEL_ID}"
|
|
302
|
-
echo "tunnel: ${TUNNEL_NAME} (${TUNNEL_ID})"
|
|
350
|
+
phase_line setup-tunnel step=tunnel-resolve source="${TUNNEL_SOURCE}" \
|
|
351
|
+
tunnel_id="${TUNNEL_ID}" tunnel_name="${TUNNEL_NAME}"
|
|
352
|
+
echo "tunnel: ${TUNNEL_NAME} (${TUNNEL_ID}) [${TUNNEL_SOURCE}]"
|
|
303
353
|
|
|
304
354
|
# --------------------------------------------------------------------------
|
|
305
355
|
# Step 3b: Zone pre-flight. Before routing DNS, verify every non-apex
|
|
@@ -25,7 +25,7 @@ Ask the agent to set up Cloudflare. The agent first confirms the domain is alrea
|
|
|
25
25
|
When you submit, the `/api/admin/cloudflare/setup` endpoint runs — in strict order — `setRemotePassword`, launches a `cloudflare-setup` action (earlier platform fixes: `systemd-run --user` transient unit wrapping `setup-tunnel.sh <brand> <port> <hostname...>`), and registers a post-exit handler to write alias-domains for every non-`public.*` public or apex hostname (so e.g. `chat.yourdomain.com` is classified as public by `isPublicHost`). The script runs end-to-end:
|
|
26
26
|
|
|
27
27
|
- `cloudflared tunnel login` — OAuth browser sign-in. The VNC browser opens the Cloudflare authorize page; pick the account that owns your domain, click Authorize. `cert.pem` lands.
|
|
28
|
-
- Tunnel
|
|
28
|
+
- Tunnel resolution from operator-supplied identity (operator-selected-tunnel fix). The form populates a tunnel-select dropdown from `GET /api/admin/cloudflare/tunnels` (which calls `cloudflared tunnel list --output json` on your logged-in account). You either pick an existing tunnel from the list or type a name to create a new one. The form posts EXACTLY ONE of `{tunnelId, tunnelName}`; the script enforces the same constraint as defence in depth. Pre-fix the script derived `${BRAND}-$(hostname -s)` locally; that broke the operator-state-is-authoritative doctrine and silently created orphan tunnels whenever the device hostname changed. Stream log emits `step=tunnel-resolve source=operator-selected|operator-created tunnel_id=… tunnel_name=…` once the UUID is known.
|
|
29
29
|
- **Zone pre-flight** — for every non-apex hostname the script queries `1.1.1.1` for the registrable parent's NS records and refuses the whole run if they don't point at Cloudflare. Stream log: `step=zone-preflight result=ok|error zones_on_account=… missing_parent_for=…`. Catches "domain not on Cloudflare"; does not catch "domain on a different Cloudflare account than `cert.pem` is bound to" — that case surfaces later via `tunnel-status`.
|
|
30
30
|
- `cloudflared tunnel route dns` for each subdomain hostname. Apex hostnames cannot be routed this way — the script prints an **ACTION REQUIRED** block naming the exact dashboard record to add or edit. Stream log emits `step=route-dns hostname=… tunnel_id=…` before the call and `step=route-dns hostname=… result=ok|apex-skip|error` after; on error the bounded cloudflared stderr (≤400 chars) rides in the same phase line. **The script does not parse cloudflared's stdout** — exit code is the sole decision signal, so all three legitimate cloudflared output shapes (new record, overwrite, idempotent "already configured") are treated as success.
|
|
31
31
|
- `config.yml` and `tunnel.state` written under `${CFG_DIR}`.
|
|
@@ -31,6 +31,26 @@ readable when you zoom out to see a large subgraph.
|
|
|
31
31
|
Tier transitions are debounced so spinning the scroll wheel does not cause
|
|
32
32
|
label flicker; labels only rewrite once zoom settles on a new tier.
|
|
33
33
|
|
|
34
|
+
## Cluster-expand on Conversation/Message clicks (cluster-integrity fix)
|
|
35
|
+
|
|
36
|
+
Clicking a Conversation node OR any Message node pulls the WHOLE
|
|
37
|
+
conversation cluster onto the canvas: the Conversation node itself plus
|
|
38
|
+
every Message belonging to it (via `PART_OF`), capped at 200 messages
|
|
39
|
+
for layout reasons. The arrow chain along the conversation (the `NEXT`
|
|
40
|
+
edges) renders for free because the inter-node relationship pass picks
|
|
41
|
+
up edges where both endpoints are in the visible window.
|
|
42
|
+
|
|
43
|
+
Pre-fix, clicking a middle Message expanded only its prev+next
|
|
44
|
+
neighbours; the head, tail, and Conversation node dropped off, visually
|
|
45
|
+
disintegrating the conversation. The new behaviour keeps the cluster
|
|
46
|
+
intact across click navigation. `PART_OF` edges are now rendered between
|
|
47
|
+
visible Conversation/Message pairs (previously suppressed because they
|
|
48
|
+
"added no information when the Conversation node wasn't on canvas" — an
|
|
49
|
+
assumption that broke the moment the cluster-expand put it there).
|
|
50
|
+
|
|
51
|
+
The breadcrumb above the canvas tracks each pivot — every entry except
|
|
52
|
+
the last is clickable to pop the view-stack back to that point.
|
|
53
|
+
|
|
34
54
|
## Tooltips and side panel
|
|
35
55
|
|
|
36
56
|
Hovering a node still shows the full 5-line tooltip (display name, labels,
|
|
@@ -467,6 +467,22 @@ grep '[persist] tool-call persisted' server.log | tail -10
|
|
|
467
467
|
|
|
468
468
|
Each log entry includes the tool name and a truncated conversation ID for correlation.
|
|
469
469
|
|
|
470
|
+
## Process provenance — durable actions emit Tasks
|
|
471
|
+
|
|
472
|
+
Every durable action — onboarding, cloudflare tunnel-login, brand publish, future deterministic flows — emits a `:Task {kind:"<flow>"}` node carrying the action's lifecycle and a `:PRODUCED` edge to every entity the action created. This makes the graph traversable from the originating Conversation to every entity created during it via `(c)<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e)` — answering "what did this turn produce" in one Cypher hop.
|
|
473
|
+
|
|
474
|
+
The doctrine is enforced at the storage primitive: writes to `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` MUST include exactly one inbound `:PRODUCED` edge whose source is a `:Task`. Bootstrap writes (PIN-setup, schema migrations, lazy `loadUserProfile`) are exempt via `createdBy.agent === 'system'`.
|
|
475
|
+
|
|
476
|
+
Two surfaces emit the lifecycle: agent-driven actions call `task-create`/`task-update`/`task-complete` over MCP (`task-create` accepts `kind`, `inputsProvided` — names only, never values — and `raisedDuringConversationKey` to resolve the `RAISED_DURING` edge). Shell-driven actions wrap their script invocation in [platform/ui/app/lib/cloudflare-task-tracker.ts](../../../ui/app/lib/cloudflare-task-tracker.ts) (cloudflare is the first; installer / brand-publish / OAuth-login deferred). Both surfaces emit the same `[task] action-start|step|done` log lines so operators can grep one channel uniformly.
|
|
477
|
+
|
|
478
|
+
`memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that Task is composed into the write's relationships before the gate runs — the typical agent-side pattern is to call `task-create` at the start of a flow, capture `taskId`, and pass it as `producedByTaskId` on every subsequent `memory-write` for an action-provenance-gated label. The gate verifies Task and write share the same `accountId`; mismatch is rejected loud.
|
|
479
|
+
|
|
480
|
+
Operator audit cyphers:
|
|
481
|
+
- "What entities did this conversation's actions produce?" — `MATCH (c:AdminConversation {conversationId:$id})<-[:RAISED_DURING]-(t:Task)-[:PRODUCED]->(e) RETURN labels(e), e.name, t.kind, t.status`
|
|
482
|
+
- "What cloudflare resources did this onboarding produce?" — `MATCH (t:Task {kind:'cloudflare-tunnel-login', status:'completed'})-[:PRODUCED]->(r) RETURN t.taskId, r.tunnelId, r.hostnameValue ORDER BY t.completedAt DESC`
|
|
483
|
+
|
|
484
|
+
See `.docs/neo4j.md § Process provenance doctrine` for the full enforcement contract, observability surface, and out-of-scope deferrals.
|
|
485
|
+
|
|
470
486
|
## Context compaction
|
|
471
487
|
|
|
472
488
|
When an admin turn crosses 75% of the model's context window, {{productName}} runs a silent compaction turn that asks the agent to call the `session-compact` MCP tool with a structured briefing (what you asked for, what was done, decisions made, work-in-progress, things you've shared about yourself). The briefing is written to Neo4j; the next admin turn injects it back into the system prompt, so continuity survives across the compaction boundary without re-sending the full transcript.
|
|
@@ -20,6 +20,8 @@ metadata: {"platform":{}}
|
|
|
20
20
|
|
|
21
21
|
Manages the agent's own dedicated email account — IMAP for reading, SMTP for sending. Admin agent only.
|
|
22
22
|
|
|
23
|
+
**Identity boundary (operator-identity fix).** This plugin manages the AGENT's email channel: an `:EmailAccount` node with IMAP/SMTP credentials, polling state, and the agent's `agentAddress`. It is NOT the operator-identity store. The operator's own email lives on the OWNS-bound `:Person` (`Person.email`) and is captured via `profile-update` with `personFields`. Confusing the two surfaces (writing the operator's personal email to `EmailAccount.agentAddress`, or vice versa) breaks routing AND identity coverage. See [.docs/agents.md § Coverage-driven elicitation bias] for the operator-identity contract.
|
|
24
|
+
|
|
23
25
|
## Capabilities
|
|
24
26
|
|
|
25
27
|
- **Setup:** `email-setup` — collect credentials via rendered `form`, connect IMAP/SMTP. Supports alias addresses (`agentAddress`).
|
|
@@ -59,6 +59,12 @@ Ranking is ephemeral and contextual — nothing is written back to the graph. Wh
|
|
|
59
59
|
|
|
60
60
|
Use `expandHops: 0` for listing and inventory queries (returns node properties only — compact output). Use `expandHops: 1` (default) for deep context queries where related nodes add value. Queries that combine a high `limit` with `expandHops: 1` on node types that have many relationships (e.g. KnowledgeDocument with HAS_SECTION) are most likely to trigger trimming.
|
|
61
61
|
|
|
62
|
+
## Process provenance — `producedByTaskId`
|
|
63
|
+
|
|
64
|
+
`memory-write` accepts an optional `producedByTaskId` parameter. When set, an inbound `:PRODUCED` edge from that `:Task` node is composed into the write's `relationships` array before the storage gate runs. The gate verifies the Task and the new node share the same `accountId`; mismatch is rejected loud.
|
|
65
|
+
|
|
66
|
+
Writes targeting `:Person`, `:UserProfile`, `:AdminUser`, `:Organization`, `:LocalBusiness`, `:CloudflareTunnel`, or `:CloudflareHostname` REQUIRE this thread (or `createdBy.agent === 'system'` for bootstrap paths). The typical pattern: call `task-create` at the start of a flow with `kind` and `raisedDuringConversationKey`; capture the returned `taskId`; pass it as `producedByTaskId` on every subsequent `memory-write` for an action-provenance-gated label. See `.docs/neo4j.md § Process provenance doctrine`.
|
|
67
|
+
|
|
62
68
|
## Graph Hygiene
|
|
63
69
|
|
|
64
70
|
Graph hygiene is **agent-directed, case by case** — no autonomous rule engine, no cron. The Neo4j graph degrades into a landfill if pollution from prior agent errors (wrong-account hostnames, fabricated identifiers, raw monologue dumps) is allowed to compound. Cleanup happens when the admin agent observes a match against the operator-curated deny-list (or the user directly asks) and applies discretion using `memory-update`, `memory-delete`, or `conversation-memory-expunge`.
|
|
@@ -415,7 +415,7 @@ server.tool("memory-rank", "Retrieve entities from the knowledge graph and rank
|
|
|
415
415
|
}
|
|
416
416
|
});
|
|
417
417
|
if (!readOnly) {
|
|
418
|
-
server.tool("memory-write", "Create a new node in the knowledge graph with automatic embedding computation. Write doctrine (Task 673): every node must be created with at least one relationship. Call memory-search first to find target elementIds.", {
|
|
418
|
+
server.tool("memory-write", "Create a new node in the knowledge graph with automatic embedding computation. Write doctrine (Task 673): every node must be created with at least one relationship. Call memory-search first to find target elementIds. Process-provenance (Task 885): writes targeting :Person, :UserProfile, :AdminUser, :Organization, :LocalBusiness, :CloudflareTunnel, or :CloudflareHostname require an inbound :PRODUCED edge from a :Task — pass the active Task's elementId as `producedByTaskId` and the edge will be composed automatically.", {
|
|
419
419
|
labels: z
|
|
420
420
|
.array(z.string())
|
|
421
421
|
.describe("Node labels (e.g. ['Person'], ['Question'], ['DefinedTerm'])"),
|
|
@@ -433,7 +433,11 @@ if (!readOnly) {
|
|
|
433
433
|
}))
|
|
434
434
|
.min(1)
|
|
435
435
|
.describe("Relationships to create with existing nodes. At least one is required — a node without an adjacency is noise, not knowledge."),
|
|
436
|
-
|
|
436
|
+
producedByTaskId: z
|
|
437
|
+
.string()
|
|
438
|
+
.optional()
|
|
439
|
+
.describe("Active Task elementId (Task 885). When set, an inbound :PRODUCED edge from this Task is composed into relationships before the write — required for action-provenance-gated labels (Person, UserProfile, AdminUser, Organization, LocalBusiness, CloudflareTunnel, CloudflareHostname). Task must share the same accountId; mismatch is rejected."),
|
|
440
|
+
}, async ({ labels, properties, scope, relationships, producedByTaskId }) => {
|
|
437
441
|
try {
|
|
438
442
|
const result = await memoryWrite({
|
|
439
443
|
labels,
|
|
@@ -447,6 +451,7 @@ if (!readOnly) {
|
|
|
447
451
|
tool: "memory-write",
|
|
448
452
|
},
|
|
449
453
|
validator,
|
|
454
|
+
producedByTaskId,
|
|
450
455
|
});
|
|
451
456
|
return {
|
|
452
457
|
content: [
|
|
@@ -1582,7 +1587,7 @@ if (!readOnly) {
|
|
|
1582
1587
|
await dbSession.close();
|
|
1583
1588
|
}
|
|
1584
1589
|
});
|
|
1585
|
-
server.tool("profile-update", "Create, update, or reinforce a user preference
|
|
1590
|
+
server.tool("profile-update", "Create, update, or reinforce a user preference; set top-level UserProfile fields (timezone, locale, givenName, role, expertise) via profileFields; AND set operator-identity fields (email, telephone) on the operator's personal-profile Person via personFields (Task 886 §D — Person is the canonical operator-identity store; UserProfile carries preferences and behavioural state only). Supports modes: reinforce (re-observed, boost confidence), update (value changed), contradict (value contradicts prior), merge (combine overlapping preferences).", {
|
|
1586
1591
|
category: z.enum(["communication", "scheduling", "decision", "workflow", "content", "interaction"])
|
|
1587
1592
|
.describe("Preference category"),
|
|
1588
1593
|
key: z.string().describe("Preference identifier (e.g. 'response_length', 'meeting_time')"),
|
|
@@ -1595,9 +1600,14 @@ if (!readOnly) {
|
|
|
1595
1600
|
.describe("Conversation ID for evidence linking"),
|
|
1596
1601
|
profileFields: z.record(z.string(), z.unknown()).optional()
|
|
1597
1602
|
.describe("Top-level UserProfile fields to update (e.g. timezone, locale, role)"),
|
|
1603
|
+
personFields: z.object({
|
|
1604
|
+
email: z.string().optional(),
|
|
1605
|
+
telephone: z.string().optional(),
|
|
1606
|
+
}).optional()
|
|
1607
|
+
.describe("Operator-identity fields written to the OWNS-bound Person (Task 886 §D). Use canonical `telephone` (NOT `phone` — that is the schema synonym, not the canonical name). Tool throws if the AdminUser-OWNS-Person edge is missing rather than silently no-op."),
|
|
1598
1608
|
mergeSourceIds: z.array(z.string()).optional()
|
|
1599
1609
|
.describe("For mode 'merge': preferenceIds of sources to combine into this preference"),
|
|
1600
|
-
}, async ({ category, key, value, source, mode, conversationId, profileFields, mergeSourceIds }) => {
|
|
1610
|
+
}, async ({ category, key, value, source, mode, conversationId, profileFields, personFields, mergeSourceIds }) => {
|
|
1601
1611
|
try {
|
|
1602
1612
|
if (!userId) {
|
|
1603
1613
|
return { content: [{ type: "text", text: "profile-update requires an authenticated admin session with userId" }], isError: true };
|
|
@@ -1612,6 +1622,7 @@ if (!readOnly) {
|
|
|
1612
1622
|
mode,
|
|
1613
1623
|
conversationId,
|
|
1614
1624
|
profileFields: profileFields,
|
|
1625
|
+
personFields,
|
|
1615
1626
|
mergeSourceIds,
|
|
1616
1627
|
});
|
|
1617
1628
|
return {
|