@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.
- package/package.json +1 -1
- package/payload/platform/lib/admin-access-password/__tests__/index.test.ts +31 -0
- package/payload/platform/lib/admin-access-password/dist/index.d.ts +8 -0
- package/payload/platform/lib/admin-access-password/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admin-access-password/dist/index.js +5 -1
- package/payload/platform/lib/admin-access-password/dist/index.js.map +1 -1
- package/payload/platform/lib/admin-access-password/src/index.ts +14 -1
- package/payload/platform/lib/admins-write/__tests__/index.test.ts +27 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts +8 -0
- package/payload/platform/lib/admins-write/dist/index.d.ts.map +1 -1
- package/payload/platform/lib/admins-write/dist/index.js +5 -1
- package/payload/platform/lib/admins-write/dist/index.js.map +1 -1
- package/payload/platform/lib/admins-write/src/index.ts +14 -1
- package/payload/platform/plugins/admin/.claude-plugin/plugin.json +1 -1
- package/payload/platform/plugins/admin/PLUGIN.md +4 -2
- package/payload/platform/plugins/admin/mcp/dist/credential-identity.d.ts +23 -0
- package/payload/platform/plugins/admin/mcp/dist/credential-identity.d.ts.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/credential-identity.js +35 -0
- package/payload/platform/plugins/admin/mcp/dist/credential-identity.js.map +1 -0
- package/payload/platform/plugins/admin/mcp/dist/index.js +110 -57
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts +22 -0
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.d.ts.map +1 -1
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js +62 -2
- package/payload/platform/plugins/admin/mcp/dist/tools/account-lifecycle.js.map +1 -1
- package/payload/platform/plugins/admin/skills/platform-architecture/SKILL.md +7 -1
- package/payload/platform/plugins/docs/references/admin-identity-gate.md +40 -0
- package/payload/platform/plugins/docs/references/internals.md +4 -0
- package/payload/platform/plugins/docs/references/plugins-guide.md +2 -0
- package/payload/platform/plugins/graph-viewer/.claude-plugin/plugin.json +13 -0
- package/payload/platform/plugins/graph-viewer/PLUGIN.md +1 -1
- package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/index.js +193 -0
- package/payload/platform/plugins/graph-viewer/lib/mcp-spawn-tee/package.json +3 -0
- package/payload/platform/plugins/linkedin-import/skills/linkedin-import/references/profile.md +1 -1
- package/payload/platform/scripts/remediate-glsmith-identity.sh +500 -0
- package/payload/platform/scripts/seed-neo4j.sh +77 -12
- package/payload/platform/services/claude-session-manager/dist/http-server.d.ts.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/http-server.js +20 -4
- package/payload/platform/services/claude-session-manager/dist/http-server.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/identity-audit.d.ts +51 -0
- package/payload/platform/services/claude-session-manager/dist/identity-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/identity-audit.js +103 -0
- package/payload/platform/services/claude-session-manager/dist/identity-audit.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/index.js +69 -0
- package/payload/platform/services/claude-session-manager/dist/index.js.map +1 -1
- package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.d.ts +25 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.js +42 -0
- package/payload/platform/services/claude-session-manager/dist/mcp-user-binding.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.d.ts +44 -0
- package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.js +127 -0
- package/payload/platform/services/claude-session-manager/dist/redundant-operator-seat-audit.js.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts +57 -0
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.d.ts.map +1 -0
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js +147 -0
- package/payload/platform/services/claude-session-manager/dist/tool-registration-drift.js.map +1 -0
- package/payload/server/{chunk-LMLVOZ6G.js → chunk-UGPQCVIW.js} +11 -38
- package/payload/server/maxy-edge.js +1 -1
- 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
|
-
|
|
870
|
-
|
|
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 =
|
|
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,
|
|
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
|
|
1027
|
-
//
|
|
1028
|
-
//
|
|
1029
|
-
//
|
|
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(
|
|
1053
|
+
coalesce(p.givenName, '') AS givenName`, { userId });
|
|
1033
1054
|
const verifiedAccountId = verify.records[0]?.get("accountId") || "";
|
|
1034
|
-
const
|
|
1035
|
-
if (!verifiedAccountId || !
|
|
1036
|
-
throw new Error(`post-write assertion failed: AdminUser userId=${userIdShort} accountId=${verifiedAccountId || "(null)"}
|
|
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 =
|
|
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
|
|
1100
|
-
//
|
|
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})
|
|
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
|
|
1108
|
-
|
|
1109
|
-
|
|
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
|
|
1170
|
-
//
|
|
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
|
-
|
|
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
|
|
1181
|
-
|
|
1182
|
-
|
|
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
|
-
|
|
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 =
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
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
|
|
2120
|
-
|
|
2121
|
-
|
|
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:
|
|
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.")
|
|
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:
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
3309
|
-
|
|
3310
|
-
|
|
3311
|
-
|
|
3312
|
-
|
|
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 {
|