@rubytech/create-maxy-code 0.1.437 → 0.1.439

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 (60) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +31 -0
  3. package/payload/platform/lib/admin-access-password/dist/index.d.ts +8 -0
  4. package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
  5. package/payload/platform/lib/admin-access-password/dist/index.js +5 -1
  6. package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
  7. package/payload/platform/lib/admin-access-password/src/index.ts +14 -1
  8. package/payload/platform/lib/admins-write/__tests__/index.test.ts +27 -0
  9. package/payload/platform/lib/admins-write/dist/index.d.ts +8 -0
  10. package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
  11. package/payload/platform/lib/admins-write/dist/index.js +5 -1
  12. package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
  13. package/payload/platform/lib/admins-write/src/index.ts +14 -1
  14. package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
  15. package/payload/platform/plugins/admin/PLUGIN.md +4 -2
  16. package/payload/platform/plugins/admin/mcp/dist/credential-identity.d.ts +23 -0
  17. package/payload/platform/plugins/admin/mcp/dist/credential-identity.d.ts.map +1 -0
  18. package/payload/platform/plugins/admin/mcp/dist/credential-identity.js +35 -0
  19. package/payload/platform/plugins/admin/mcp/dist/credential-identity.js.map +1 -0
  20. package/payload/platform/plugins/admin/mcp/dist/index.js +110 -57
  21. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  22. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +22 -0
  23. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -1
  24. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +62 -2
  25. package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -1
  26. package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -1
  27. package/payload/platform/plugins/docs/references/admin-identity-gate.md +40 -0
  28. package/payload/platform/plugins/docs/references/internals.md +4 -0
  29. package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
  30. package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +13 -0
  31. package/payload/platform/plugins/graph-viewer/PLUGIN.md +1 -1
  32. package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +193 -0
  33. package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/package.json +3 -0
  34. package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +1 -1
  35. package/payload/platform/scripts/remediate-glsmith-identity.sh +500 -0
  36. package/payload/platform/scripts/seed-neo4j.sh +77 -12
  37. package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
  38. package/payload/platform/services/claude-session-manager/dist/http-server.js +20 -4
  39. package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
  40. package/payload/platform/services/claude-session-manager/dist/identity-audit.d.ts +51 -0
  41. package/payload/platform/services/claude-session-manager/dist/identity-audit.d.ts.map +1 -0
  42. package/payload/platform/services/claude-session-manager/dist/identity-audit.js +103 -0
  43. package/payload/platform/services/claude-session-manager/dist/identity-audit.js.map +1 -0
  44. package/payload/platform/services/claude-session-manager/dist/index.js +69 -0
  45. package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
  46. package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.d.ts +25 -0
  47. package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.d.ts.map +1 -0
  48. package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.js +42 -0
  49. package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.js.map +1 -0
  50. package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.d.ts +44 -0
  51. package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.d.ts.map +1 -0
  52. package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.js +127 -0
  53. package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.js.map +1 -0
  54. package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts +57 -0
  55. package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts.map +1 -0
  56. package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js +147 -0
  57. package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js.map +1 -0
  58. package/payload/server/{chunk-LMLVOZ6G.js → chunk-UGPQCVIW.js} +11 -38
  59. package/payload/server/maxy-edge.js +1 -1
  60. package/payload/server/server.js +15 -16
@@ -10,6 +10,7 @@ import { execFile, execFileSync } from "node:child_process";
10
10
  import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs";
11
11
  import { writeAdminEntry, removeAdminFromAccount, logUsersAudit, formatAuditRowIds } from "../../../../lib/admins-write/dist/index.js";
12
12
  import { setAccessPassword as setAccessPasswordHash, clearAccessPassword as clearAccessPasswordHash, accessPasswordCollides } from "../../../../lib/admin-access-password/dist/index.js";
13
+ import { resolveCredentialTarget } from "./credential-identity.js";
13
14
  import { embed } from "../../../../lib/embed-client/dist/index.js";
14
15
  import { requirePortEnv } from "../../../../lib/require-port-env/dist/index.js";
15
16
  import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
@@ -27,7 +28,7 @@ import { publishSite } from "./tools/publish-site.js";
27
28
  import { resolveCapabilitiesHere } from "./tools/capabilities-here.js";
28
29
  import { runCatalogueCensus } from "./lib/catalogue-census.js";
29
30
  import { lifelineTool } from "../../../../lib/mcp-lifeline/dist/index.js";
30
- import { listAccountsFromDir, isHouseAccount, archiveAccountDir, provisionClientAccount, findHouseAccount, getChannelRoutingAccountId, setChannelRoutingAccountId, seedAccountGraphRoot, readHouseAdminUserIds, setManagingAdmin, findClientAccountsManagedBy, reassignManagingAdmin, ACCOUNT_NODE_COUNT_CYPHER, ACCOUNT_PURGE_CYPHER, } from "./tools/account-lifecycle.js";
31
+ import { listAccountsFromDir, isHouseAccount, archiveAccountDir, provisionClientAccount, findHouseAccount, getChannelRoutingAccountId, setChannelRoutingAccountId, seedAccountGraphRoot, isPlaceholderOwnerName, refuseClientAdminAdd, readHouseAdminUserIds, setManagingAdmin, findClientAccountsManagedBy, reassignManagingAdmin, ACCOUNT_NODE_COUNT_CYPHER, ACCOUNT_PURGE_CYPHER, } from "./tools/account-lifecycle.js";
31
32
  function refusalGuidance(kind) {
32
33
  switch (kind) {
33
34
  case "unsafe-slug":
@@ -53,6 +54,15 @@ const PLATFORM_ROOT = process.env.PLATFORM_ROOT ?? resolve(import.meta.dirname,
53
54
  // isolation in metadata-only mode.
54
55
  const ACCOUNT_ID = process.env.ACCOUNT_ID ?? null;
55
56
  process.stderr.write(`[admin] boot accountId=${ACCOUNT_ID ?? "null"}\n`);
57
+ /** Task 1573 — the verified caller identity for credential tools: the session's
58
+ * authenticated admin, carried as process.env.USER_ID, with empty treated as
59
+ * "no verified identity". Post Task 1573 the rc-spawn env stamps this from the
60
+ * authenticated admin (or empty), never the install owner via fallback, so a
61
+ * credential tool can trust it as the operating identity. */
62
+ function verifiedCaller() {
63
+ const u = process.env.USER_ID;
64
+ return u && u.length > 0 ? u : undefined;
65
+ }
56
66
  function refuseNoAccount(toolName) {
57
67
  process.stderr.write(`[admin] tool=${toolName} refuse reason=no-account-context\n`);
58
68
  return {
@@ -866,8 +876,20 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
866
876
  if (!ACCOUNT_ID)
867
877
  return refuseNoAccount("admin-add");
868
878
  const TAG = "[admin]";
869
- if (!name.trim()) {
870
- return { content: [{ type: "text", text: `${TAG} Name is required.` }], isError: true };
879
+ // Task 1568 — a client sub-account carries one owner identity and no operator
880
+ // seats. Since Task 999 admin access is install-wide, so a seat here grants
881
+ // nothing (canAccessAdmin never reads admins[]). Refuse before any store
882
+ // write; the house account is where operators hold PIN logins and admins[].
883
+ const clientRefusal = refuseClientAdminAdd(isHouseAccount(ACCOUNTS_ROOT, ACCOUNT_ID), ACCOUNT_ID);
884
+ if (clientRefusal) {
885
+ process.stderr.write(`${clientRefusal.logLine}\n`);
886
+ return { content: [{ type: "text", text: `${TAG} ${clientRefusal.message}` }], isError: true };
887
+ }
888
+ // Task 1567 — the same name-required invariant as the owner-seat seeds: a
889
+ // blank name or the literal "Owner" placeholder is refused, never persisted.
890
+ if (isPlaceholderOwnerName(name)) {
891
+ process.stderr.write(`[admin-add] op=refused reason=no-name\n`);
892
+ return { content: [{ type: "text", text: `${TAG} A real name is required (it cannot be blank or the placeholder "Owner").` }], isError: true };
871
893
  }
872
894
  let users;
873
895
  try {
@@ -896,7 +918,7 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
896
918
  return { content: [{ type: "text", text: `${TAG} Unable to verify admin limit — cannot add admin.` }], isError: true };
897
919
  }
898
920
  // Resolve the calling user's identity from the session environment
899
- const callerUserId = process.env.USER_ID;
921
+ const callerUserId = verifiedCaller(); // Task 1573 — empty USER_ID -> "unknown" actor, not ''
900
922
  // PIN: use provided or generate. Constraint: minimum 4 digits, no upper bound.
901
923
  let plaintextPin;
902
924
  if (rawPin) {
@@ -985,10 +1007,8 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
985
1007
  // pruned silently, costing the admin pin during onboarding.
986
1008
  const result = await session.run(`MERGE (au:AdminUser {userId: $userId})
987
1009
  ON CREATE SET au.accountId = $accountId,
988
- au.name = $name,
989
1010
  au.createdAt = $createdAt
990
1011
  ON MATCH SET au.accountId = COALESCE(au.accountId, $accountId),
991
- au.name = $name,
992
1012
  au.updatedAt = $createdAt
993
1013
  WITH au
994
1014
  MATCH (b:LocalBusiness {accountId: $accountId})
@@ -1019,21 +1039,22 @@ eagerTool(server, "admin-add", "Add a new admin user to this account. Creates a
1019
1039
  MERGE (au)-[:OWNS]->(newPerson)
1020
1040
  RETURN false AS reused
1021
1041
  }
1022
- RETURN reused`, { userId, name: trimmedName, createdAt, accountId: ACCOUNT_ID, givenName, familyName, personEmbedding });
1042
+ RETURN reused`, { userId, createdAt, accountId: ACCOUNT_ID, givenName, familyName, personEmbedding });
1023
1043
  if (result.records.length > 0) {
1024
1044
  personReused = result.records[0].get("reused");
1025
1045
  }
1026
- // post-write assertion mirroring the one in neo4j-store.ts.
1027
- // MATCH back the AdminUser we just wrote and verify every required
1028
- // field landed. Loud-fail the tool call if accountId or name is null
1029
- // so a Cypher regression that drops the field is grep-detectable.
1046
+ // Task 1570 — post-write assertion on the canonical invariants: the
1047
+ // AdminUser has its accountId, and an OWNS-Person in its own account with
1048
+ // a non-empty givenName (the sole name source). A Cypher regression that
1049
+ // drops the Person or its name is grep-detectable and loud-fails the tool.
1030
1050
  const verify = await session.run(`MATCH (au:AdminUser {userId: $userId})
1051
+ OPTIONAL MATCH (au)-[:OWNS]->(p:Person) WHERE p.accountId = au.accountId
1031
1052
  RETURN coalesce(au.accountId, '') AS accountId,
1032
- coalesce(au.name, '') AS name`, { userId });
1053
+ coalesce(p.givenName, '') AS givenName`, { userId });
1033
1054
  const verifiedAccountId = verify.records[0]?.get("accountId") || "";
1034
- const verifiedName = verify.records[0]?.get("name") || "";
1035
- if (!verifiedAccountId || !verifiedName) {
1036
- throw new Error(`post-write assertion failed: AdminUser userId=${userIdShort} accountId=${verifiedAccountId || "(null)"} name=${verifiedName || "(null)"} — required fields missing after MERGE`);
1055
+ const verifiedGivenName = (verify.records[0]?.get("givenName") || "").trim();
1056
+ if (!verifiedAccountId || !verifiedGivenName) {
1057
+ throw new Error(`post-write assertion failed: AdminUser userId=${userIdShort} accountId=${verifiedAccountId || "(null)"} owned-Person givenName=${verifiedGivenName || "(null)"} — required fields missing after MERGE`);
1037
1058
  }
1038
1059
  }
1039
1060
  finally {
@@ -1067,7 +1088,7 @@ eagerTool(server, "admin-remove", "Remove an admin from this account. Removes th
1067
1088
  if (!ACCOUNT_ID)
1068
1089
  return refuseNoAccount("admin-remove");
1069
1090
  const TAG = "[admin]";
1070
- const callerUserId = process.env.USER_ID;
1091
+ const callerUserId = verifiedCaller(); // Task 1573 — empty USER_ID -> "unknown" actor, not ''
1071
1092
  // Read account.json and validate
1072
1093
  let config;
1073
1094
  let admins;
@@ -1096,17 +1117,22 @@ eagerTool(server, "admin-remove", "Remove an admin from this account. Removes th
1096
1117
  return { content: [{ type: "text", text: `${TAG} Cannot remove ${userId}: they are the managing admin of ${managed.length} client sub-account(s): ${managed.join(", ")}. Reassign each to another house admin with account_reassign_admin, then retry.` }], isError: true };
1097
1118
  }
1098
1119
  }
1099
- // Resolve the admin's name from Neo4j (canonical) for the confirmation
1100
- // message. Best-effort fall back to userId if the graph is unreachable.
1120
+ // Resolve the admin's name from the owned Person (Task 1570 the canonical
1121
+ // and only name source, keyed on the AdminUser's own account) for the
1122
+ // confirmation message. Best-effort — fall back to userId if the graph is
1123
+ // unreachable or the owned Person is absent/nameless.
1101
1124
  let removedName = userId;
1102
1125
  try {
1103
1126
  const session = getSession();
1104
1127
  try {
1105
- const result = await session.run(`MATCH (au:AdminUser {userId: $userId}) RETURN au.name AS name LIMIT 1`, { userId });
1128
+ const result = await session.run(`MATCH (au:AdminUser {userId: $userId})-[:OWNS]->(p:Person)
1129
+ WHERE p.accountId = au.accountId
1130
+ RETURN p.givenName AS givenName, p.familyName AS familyName LIMIT 1`, { userId });
1106
1131
  if (result.records.length > 0) {
1107
- const name = result.records[0].get("name");
1108
- if (name && name.trim())
1109
- removedName = name.trim();
1132
+ const givenName = result.records[0].get("givenName")?.trim() ?? "";
1133
+ const familyName = result.records[0].get("familyName")?.trim() ?? "";
1134
+ if (givenName)
1135
+ removedName = familyName ? `${givenName} ${familyName}` : givenName;
1110
1136
  }
1111
1137
  }
1112
1138
  finally {
@@ -1166,20 +1192,23 @@ eagerTool(server, "admin-list", "List all admins for this account with their nam
1166
1192
  if (admins.length === 0) {
1167
1193
  return { content: [{ type: "text", text: `${TAG} No admins configured for this account.` }] };
1168
1194
  }
1169
- // Enrich with names from Neo4j AdminUser (canonical). Best
1170
- // effort render "(unknown)" for any userId without a graph entry.
1195
+ // Enrich with names from each admin's owned Person (Task 1570 — the
1196
+ // canonical and only name source, keyed on the AdminUser's own account).
1197
+ // Best effort — render "(unknown)" for any userId with no named owned Person.
1171
1198
  const userMap = new Map();
1172
1199
  try {
1173
1200
  const session = getSession();
1174
1201
  try {
1175
1202
  const result = await session.run(`UNWIND $userIds AS uid
1176
- MATCH (au:AdminUser {userId: uid})
1177
- RETURN au.userId AS userId, au.name AS name`, { userIds: admins.map(a => a.userId) });
1203
+ MATCH (au:AdminUser {userId: uid})-[:OWNS]->(p:Person)
1204
+ WHERE p.accountId = au.accountId
1205
+ RETURN uid AS userId, p.givenName AS givenName, p.familyName AS familyName`, { userIds: admins.map(a => a.userId) });
1178
1206
  for (const record of result.records) {
1179
1207
  const uid = record.get("userId");
1180
- const name = record.get("name");
1181
- if (name && name.trim())
1182
- userMap.set(uid, name.trim());
1208
+ const givenName = record.get("givenName")?.trim() ?? "";
1209
+ const familyName = record.get("familyName")?.trim() ?? "";
1210
+ if (givenName)
1211
+ userMap.set(uid, familyName ? `${givenName} ${familyName}` : givenName);
1183
1212
  }
1184
1213
  }
1185
1214
  finally {
@@ -1198,17 +1227,21 @@ eagerTool(server, "admin-list", "List all admins for this account with their nam
1198
1227
  eagerTool(server, "admin-update-pin", "Update an existing admin user's PIN. Defaults to the calling admin if no userId is given. PIN must be at least 4 digits and unique across all users on the device. PINs are device-level: updating another admin's PIN does not require shared account membership — any admin on the device can rotate any other admin's PIN, matching the existing trust model used by admin-remove.", {
1199
1228
  userId: z.string().optional().describe("The userId of the admin whose PIN to update. Defaults to the caller (the admin invoking this tool)."),
1200
1229
  newPin: z.string().describe("The new PIN. Minimum 4 digits, no upper bound."),
1201
- }, async ({ userId: targetUserId, newPin }) => {
1230
+ confirm: z.boolean().optional().describe("Required (true) only when userId names a DIFFERENT admin than the caller — confirms a deliberate cross-identity PIN change."),
1231
+ }, async ({ userId: targetUserId, newPin, confirm }) => {
1202
1232
  if (!ACCOUNT_ID)
1203
1233
  return refuseNoAccount("admin-update-pin");
1204
1234
  const TAG = "[admin-update-pin]";
1205
- const callerUserId = process.env.USER_ID;
1206
- const userId = targetUserId ?? callerUserId;
1207
- const userIdLabel = userId ? userId.slice(0, 8) : "unknown";
1208
- if (!userId) {
1209
- console.error(`${TAG} userId=${userIdLabel} result=user-not-found reason=no-caller-context`);
1210
- return { content: [{ type: "text", text: `${TAG} No userId supplied and no caller context — cannot update.` }], isError: true };
1211
- }
1235
+ const callerUserId = verifiedCaller();
1236
+ // Task 1573 no ambient owner fallback. Self resolves to the verified
1237
+ // caller; a cross-identity write needs an explicit userId + confirm.
1238
+ const resolved = resolveCredentialTarget({ explicitUserId: targetUserId, callerUserId, confirm });
1239
+ if (!resolved.ok) {
1240
+ console.error(`${TAG} result=refused reason=${resolved.error.split(":")[0]}`);
1241
+ return { content: [{ type: "text", text: `${TAG} ${resolved.error}` }], isError: true };
1242
+ }
1243
+ const userId = resolved.target;
1244
+ const userIdLabel = userId.slice(0, 8);
1212
1245
  if (newPin.length < 4) {
1213
1246
  console.error(`${TAG} userId=${userIdLabel} result=too-short`);
1214
1247
  return { content: [{ type: "text", text: `${TAG} PIN must be at least 4 digits.` }], isError: true };
@@ -1252,7 +1285,7 @@ eagerTool(server, "admin-update-pin", "Update an existing admin user's PIN. Defa
1252
1285
  // (a pin rotation); the audited field is the pin.
1253
1286
  {
1254
1287
  const rows = formatAuditRowIds(users.map(u => u.userId));
1255
- logUsersAudit({ actor: callerUserId ?? "unknown", session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG }, { action: "set-pin", field: "pin", rowsBefore: rows, rowsAfter: rows });
1288
+ logUsersAudit({ actor: callerUserId ?? "unknown", target: userId, explicitUserId: resolved.isCrossIdentity, session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG }, { action: "set-pin", field: "pin", rowsBefore: rows, rowsAfter: rows });
1256
1289
  }
1257
1290
  const self = userId === callerUserId;
1258
1291
  return {
@@ -2113,13 +2146,18 @@ lifelineTool(server, "remote-auth-set-password", "Set an admin's remote access p
2113
2146
  "Protects the admin interface when exposed over the tunnel — has no effect on the public endpoint or the tunnel itself.", {
2114
2147
  password: z.string(),
2115
2148
  userId: z.string().optional().describe("The admin to set the password for. Defaults to the calling admin. Use admin-list to find userIds."),
2116
- }, async ({ password, userId }) => {
2149
+ confirm: z.boolean().optional().describe("Required (true) only when userId names a DIFFERENT admin than the caller — confirms a deliberate cross-identity password change."),
2150
+ }, async ({ password, userId, confirm }) => {
2117
2151
  if (!ACCOUNT_ID)
2118
2152
  return refuseNoAccount("remote-auth-set-password");
2119
- const targetUserId = userId ?? process.env.USER_ID;
2120
- if (!targetUserId) {
2121
- return { content: [{ type: "text", text: "No target admin: pass userId, or run as an identified admin." }], isError: true };
2122
- }
2153
+ const callerUserId = verifiedCaller();
2154
+ // Task 1573 — no ambient owner fallback. Self resolves to the verified
2155
+ // caller; a cross-identity write needs an explicit userId + confirm.
2156
+ const resolved = resolveCredentialTarget({ explicitUserId: userId, callerUserId, confirm });
2157
+ if (!resolved.ok) {
2158
+ return { content: [{ type: "text", text: resolved.error }], isError: true };
2159
+ }
2160
+ const targetUserId = resolved.target;
2123
2161
  let users;
2124
2162
  try {
2125
2163
  users = readUsersJson();
@@ -2148,8 +2186,8 @@ lifelineTool(server, "remote-auth-set-password", "Set an admin's remote access p
2148
2186
  return { content: [{ type: "text", text: `That password is already in use by another admin. Choose a different one.` }], isError: true };
2149
2187
  }
2150
2188
  try {
2151
- await setAccessPasswordHash(targetUserId, password, USERS_FILE, { actor: process.env.USER_ID ?? "unknown", session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG });
2152
- console.error(`[admin] remote-access-password set userId=${targetUserId.slice(0, 8)}`);
2189
+ await setAccessPasswordHash(targetUserId, password, USERS_FILE, { actor: callerUserId ?? "unknown", target: targetUserId, explicitUserId: resolved.isCrossIdentity, session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG });
2190
+ console.error(`[admin] remote-access-password set userId=${targetUserId.slice(0, 8)} actor=${(callerUserId ?? "unknown").slice(0, 8)} explicitUserId=${resolved.isCrossIdentity}`);
2153
2191
  return { content: [{ type: "text", text: `Remote access password set for userId ${targetUserId}.` }] };
2154
2192
  }
2155
2193
  catch (err) {
@@ -2160,9 +2198,17 @@ lifelineTool(server, "remote-auth-set-password", "Set an admin's remote access p
2160
2198
  }
2161
2199
  });
2162
2200
  lifelineTool(server, "remote-auth-revoke-password", "Revoke an admin's remote access password so they can no longer obtain a NEW remote session (an already-open session lapses within 24h). " +
2163
- "Does not affect any other admin's password and does not touch their PIN. Pass userId (from admin-list).", { userId: z.string().describe("The admin whose remote access password to revoke. Use admin-list to find userIds.") }, async ({ userId }) => {
2201
+ "Does not affect any other admin's password and does not touch their PIN. Pass userId (from admin-list).", { userId: z.string().describe("The admin whose remote access password to revoke. Use admin-list to find userIds."),
2202
+ confirm: z.boolean().optional().describe("Required (true) when userId names a DIFFERENT admin than the caller — confirms a deliberate cross-identity revoke.") }, async ({ userId, confirm }) => {
2164
2203
  if (!ACCOUNT_ID)
2165
2204
  return refuseNoAccount("remote-auth-revoke-password");
2205
+ const callerUserId = verifiedCaller();
2206
+ // Task 1573 — actor is the verified caller, never ambient env; a revoke of
2207
+ // another admin's password is a cross-identity write needing confirm.
2208
+ const resolved = resolveCredentialTarget({ explicitUserId: userId, callerUserId, confirm });
2209
+ if (!resolved.ok) {
2210
+ return { content: [{ type: "text", text: resolved.error }], isError: true };
2211
+ }
2166
2212
  let users;
2167
2213
  try {
2168
2214
  users = readUsersJson();
@@ -2182,8 +2228,8 @@ lifelineTool(server, "remote-auth-revoke-password", "Revoke an admin's remote ac
2182
2228
  return { content: [{ type: "text", text: "Refusing to revoke the last remaining remote access password — the install would have no remote access. Set another admin's password first." }], isError: true };
2183
2229
  }
2184
2230
  try {
2185
- clearAccessPasswordHash(userId, USERS_FILE, { actor: process.env.USER_ID ?? "unknown", session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG });
2186
- console.error(`[admin] remote-access-password revoked userId=${userId.slice(0, 8)}`);
2231
+ clearAccessPasswordHash(userId, USERS_FILE, { actor: callerUserId ?? "unknown", target: userId, explicitUserId: resolved.isCrossIdentity, session: process.env.SESSION_NODE_ID, logFile: USERS_AUDIT_LOG });
2232
+ console.error(`[admin] remote-access-password revoked userId=${userId.slice(0, 8)} actor=${(callerUserId ?? "unknown").slice(0, 8)} explicitUserId=${resolved.isCrossIdentity}`);
2187
2233
  return { content: [{ type: "text", text: `Remote access password revoked for userId ${userId}.` }] };
2188
2234
  }
2189
2235
  catch (err) {
@@ -3267,9 +3313,10 @@ eagerTool(server, "account_list", "List every sub-account on this install (one h
3267
3313
  content: [{ type: "text", text: accounts.length ? lines.join("\n") : "No accounts found." }],
3268
3314
  };
3269
3315
  });
3270
- eagerTool(server, "account_create", "Provision a new CLIENT sub-account on this install (managed service). REQUIRES a managing admin: pass `adminUserId`, the userId of the house team member who will manage this client (get the choices from admin-list; ask the operator which one before creating). Mints a UUID, scaffolds the account directory (agents, specialists, settings, account.json with role:\"client\" and managingAdminUserId), seeds default plugins, and seeds the account's graph root (a :LocalBusiness plus its owner :AdminUser) so you can populate it immediately. Refuses if no adminUserId is given or if it is not a current house admin. Returns the new accountId. Operator-only; never creates the house sub-account (that is the install itself).", {
3316
+ eagerTool(server, "account_create", "Provision a new CLIENT sub-account on this install (managed service). REQUIRES two things: `adminUserId`, the userId of the house team member who will manage this client (get the choices from admin-list; ask the operator which one before creating); and `ownerName`, the real name of the client business's owner (ask the operator for it — never invent it or reuse the house operator's name). Mints a UUID, scaffolds the account directory (agents, specialists, settings, account.json with role:\"client\" and managingAdminUserId), seeds default plugins, and seeds the account's graph root (a :LocalBusiness plus its owner :AdminUser and owner :Person carrying ownerName) so you can populate it immediately. Refuses if adminUserId is missing/not a house admin, or if ownerName is missing/blank/the placeholder \"Owner\". Returns the new accountId. Operator-only; never creates the house sub-account (that is the install itself).", {
3271
3317
  adminUserId: z.string().describe("The userId of the house admin who will manage this client sub-account. Must be a current house admin (see admin-list). Required — creation is refused without it."),
3272
- }, async ({ adminUserId }) => {
3318
+ ownerName: z.string().describe("The real name of the client business's owner. Applied to both the owner AdminUser and owner Person for the new sub-account. Required — creation is refused when missing, blank, or the placeholder \"Owner\". Ask the operator; never reuse the house operator's name."),
3319
+ }, async ({ adminUserId, ownerName }) => {
3273
3320
  if (!ACCOUNT_ID)
3274
3321
  return refuseNoAccount("account_create");
3275
3322
  // Task 1500 — every client sub-account must carry a responsible house admin.
@@ -3279,6 +3326,14 @@ eagerTool(server, "account_create", "Provision a new CLIENT sub-account on this
3279
3326
  process.stderr.write(`[account-lifecycle] op=create-refused reason=no-admin adminUserId=-\n`);
3280
3327
  return { content: [{ type: "text", text: `account_create refused: name the managing house admin (adminUserId). Call admin-list to see the choices and ask the operator which one manages this client.` }], isError: true };
3281
3328
  }
3329
+ // Task 1567 — the business owner's name is a hard precondition of the
3330
+ // sub-account. Refuse before any scaffold so a nameless create leaves no
3331
+ // partial account directory or rootless graph behind. The same predicate
3332
+ // guards the seed (seedAccountGraphRoot) as a defence in depth.
3333
+ if (isPlaceholderOwnerName(ownerName)) {
3334
+ process.stderr.write(`[account-create] op=refused reason=no-owner-name\n`);
3335
+ return { content: [{ type: "text", text: `account_create refused: give the client business owner's real name (ownerName). It cannot be blank or the placeholder "Owner". Ask the operator whose business this is.` }], isError: true };
3336
+ }
3282
3337
  const houseAdminUserIds = readHouseAdminUserIds(ACCOUNTS_ROOT);
3283
3338
  if (!houseAdminUserIds.includes(adminUserId)) {
3284
3339
  process.stderr.write(`[account-lifecycle] op=create-refused reason=unknown-admin adminUserId=${adminUserId.slice(0, 8)}\n`);
@@ -3305,13 +3360,11 @@ eagerTool(server, "account_create", "Provision a new CLIENT sub-account on this
3305
3360
  // MERGE-keyed on its own userId at accountId=house, so reusing it would
3306
3361
  // leave the client's account-scoped AdminUser MATCH empty.
3307
3362
  const ownerUserId = randomUUID();
3308
- let ownerName = "Owner";
3309
- try {
3310
- const users = JSON.parse(readFileSync(USERS_FILE, "utf-8"));
3311
- if (Array.isArray(users) && typeof users[0]?.name === "string")
3312
- ownerName = users[0].name;
3313
- }
3314
- catch { /* no users.json — default "Owner" */ }
3363
+ // Task 1567 — ownerName is the validated business-owner name from the tool
3364
+ // input (refused above if blank/placeholder). It is applied to both the
3365
+ // owner AdminUser and the owner Person in seedAccountGraphRoot. It is never
3366
+ // derived from the house operator's users.json that seeded the wrong
3367
+ // person's name and defaulted to the "Owner" placeholder.
3315
3368
  const seedSession = getSession();
3316
3369
  let rootSeeded = false;
3317
3370
  try {