@rubytech/create-maxy 1.0.796 → 1.0.797
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/plugins/admin/PLUGIN.md +4 -0
- package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +9 -5
- package/payload/platform/plugins/admin/mcp/dist/index.js +54 -18
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/docs/references/settings.md +6 -0
- package/payload/platform/scripts/logs-read.sh +17 -12
- package/payload/platform/templates/agents/admin/IDENTITY.md +6 -0
- package/payload/server/public/assets/{admin-StzFnTQB.js → admin-Cz8hUAqx.js} +1 -1
- package/payload/server/public/assets/{graph-BOKpKqLw.js → graph-CBu0rtrP.js} +1 -1
- package/payload/server/public/assets/{page-DItB4skl.js → page-BNM63zsb.js} +1 -1
- package/payload/server/public/graph.html +2 -2
- package/payload/server/public/index.html +2 -2
- package/payload/server/server.js +17 -6
package/package.json
CHANGED
|
@@ -51,6 +51,10 @@ Platform management tools for both the admin and public agents. The `plugin-read
|
|
|
51
51
|
|
|
52
52
|
Tools are available via the `admin` MCP server.
|
|
53
53
|
|
|
54
|
+
**Three-store admin auth invariant (Task 850).** `admin-add` writes to all three identity stores (`users.json` PIN auth, `account.json` `admins[]` role, Neo4j `:AdminUser`/`:Person` graph identity) with per-leg `[admin-auth-store]` log lines and returns `is_error: true` on any leg failure naming what's already written. `admin-update-pin` writes `users.json` only and emits the same line. Direct `Edit`/`Write` on `account.json` is blocked at the `pre-tool-use` hook — mutations go through `account-update`, `plugin-toggle-enabled`, or the `admin-*` tools. See `.docs/agents.md` § "Three-store admin auth invariant" for the full contract.
|
|
55
|
+
|
|
56
|
+
`logs-read { type: "agent-stream" }` (Task 850) is the canonical name for the per-conversation tool-use/tool-result archive previously called `system`; both names work and the legacy alias is preserved.
|
|
57
|
+
|
|
54
58
|
## Skills
|
|
55
59
|
|
|
56
60
|
| Task | When to use | Reference |
|
|
@@ -47,11 +47,15 @@ if [ "$AGENT_TYPE" = "admin" ]; then
|
|
|
47
47
|
echo "[entitlement] tool-deny: tool=${TOOL_NAME} path=${FILE_PATH} field=entitlement-file" >&2
|
|
48
48
|
exit 2
|
|
49
49
|
;;
|
|
50
|
-
# account.json (Task 831) — agent must use the account-update
|
|
51
|
-
# (or plugin-toggle-enabled for enabledPlugins changes), which
|
|
52
|
-
# editable fields server-side. Direct Edit/Write bypasses
|
|
53
|
-
# deny unconditionally including cwd-relative paths.
|
|
54
|
-
*/
|
|
50
|
+
# account.json (Task 831 + Task 850) — agent must use the account-update
|
|
51
|
+
# MCP tool (or plugin-toggle-enabled for enabledPlugins changes), which
|
|
52
|
+
# whitelists editable fields server-side. Direct Edit/Write bypasses
|
|
53
|
+
# that whitelist; deny unconditionally including cwd-relative paths.
|
|
54
|
+
# Task 850 adds */account.json and *account.json as a backstop so any
|
|
55
|
+
# layout the named patterns miss is still caught — the Adam Mackay
|
|
56
|
+
# incident showed a tier upgrade via raw Edit on account.json reach
|
|
57
|
+
# the disk, exactly the failure mode this hook exists to prevent.
|
|
58
|
+
*/data/accounts/*/account.json|*/config/accounts/*/account.json|data/accounts/*/account.json|config/accounts/*/account.json|*/account.json|*account.json|account.json)
|
|
55
59
|
echo "Blocked: Admin agent cannot edit account.json directly. Use the account-update or plugin-toggle-enabled MCP tools — they whitelist editable fields server-side and exclude tier and purchasedPlugins by design." >&2
|
|
56
60
|
echo "[entitlement] tool-deny: tool=${TOOL_NAME} path=${FILE_PATH} field=account-json" >&2
|
|
57
61
|
exit 2
|
|
@@ -621,7 +621,7 @@ server.tool("plugin-toggle-enabled", "Enable or disable a plugin in this account
|
|
|
621
621
|
// ===================================================================
|
|
622
622
|
// Admin user management tools
|
|
623
623
|
// ===================================================================
|
|
624
|
-
server.tool("admin-add", "Add a new admin user to this account. Creates a device-level user entry (users.json) and adds them to this account's admins list (account.json). PIN must be at least 4 digits. If no PIN is provided, a unique 4-digit PIN is generated. Returns the userId and PIN to share with the new admin.", {
|
|
624
|
+
server.tool("admin-add", "Add a new admin user to this account. Creates a device-level user entry (users.json) and adds them to this account's admins list (account.json). PIN must be at least 4 digits. If no PIN is provided, a unique 4-digit PIN is generated. Returns the userId and PIN to share with the new admin.\n\nIMPORTANT — retry behaviour: if the user already stated a specific PIN earlier in the conversation and a first admin-add call failed (e.g. tier cap reached, PIN collision), every retry MUST re-pass that PIN as the `pin` parameter. Omitting `pin` on the retry auto-generates a different 4-digit PIN, silently substituting what the user asked for.", {
|
|
625
625
|
name: z.string().describe("Display name for the new admin (stored on the AdminUser node in Neo4j)."),
|
|
626
626
|
pin: z.string().optional().describe("Optional PIN (minimum 4 digits). If omitted, a unique 4-digit PIN is generated."),
|
|
627
627
|
}, async ({ name, pin: rawPin }) => {
|
|
@@ -679,14 +679,25 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
|
|
|
679
679
|
}
|
|
680
680
|
const pinHash = hashPin(plaintextPin);
|
|
681
681
|
const userId = crypto.randomUUID();
|
|
682
|
+
// Three-store admin auth invariant (Task 850): users.json (device-level
|
|
683
|
+
// PIN auth), account.json admins[] (account-level role), Neo4j AdminUser
|
|
684
|
+
// (display + graph identity). Each leg emits a [admin-auth-store] line so
|
|
685
|
+
// a future incident can grep one log to know which leg failed; any leg
|
|
686
|
+
// failing makes the tool result is_error: true with a cause line citing
|
|
687
|
+
// what's already been written. No automatic rollback — the cause line is
|
|
688
|
+
// the manual-recovery diagnostic.
|
|
689
|
+
const userIdShort = userId.slice(0, 8);
|
|
682
690
|
// 1. Write to users.json (device-level). Auth fields only — `name` lives
|
|
683
691
|
// on the AdminUser node in Neo4j (Task 829).
|
|
684
692
|
users.push({ userId, pin: pinHash });
|
|
685
693
|
try {
|
|
686
694
|
writeUsersJson(users);
|
|
695
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=ok store=users`);
|
|
687
696
|
}
|
|
688
697
|
catch (err) {
|
|
689
|
-
|
|
698
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
699
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=fail store=users error=${errMsg}`);
|
|
700
|
+
return { content: [{ type: "text", text: `${TAG} Failed to write users.json: ${errMsg}` }], isError: true };
|
|
690
701
|
}
|
|
691
702
|
// 2. Write to account.json (account-level)
|
|
692
703
|
try {
|
|
@@ -698,10 +709,12 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
|
|
|
698
709
|
const configPath = join(getAccountDir(), "account.json");
|
|
699
710
|
await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
700
711
|
}
|
|
712
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=ok store=account`);
|
|
701
713
|
}
|
|
702
714
|
catch (err) {
|
|
703
|
-
|
|
704
|
-
|
|
715
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
716
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=fail store=account error=${errMsg}`);
|
|
717
|
+
return { content: [{ type: "text", text: `${TAG} users.json updated; account.json write FAILED — manual reconciliation needed: ${errMsg}` }], isError: true };
|
|
705
718
|
}
|
|
706
719
|
// 3. Write to Neo4j (graph-level): AdminUser + Person + OWNS atomically,
|
|
707
720
|
// plus ADMIN_OF edge to the LocalBusiness for this account.
|
|
@@ -710,7 +723,12 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
|
|
|
710
723
|
// in platform/ui/app/lib/neo4j-store.ts (case-insensitive exact match
|
|
711
724
|
// on givenName + familyName; partial-name ambiguity does NOT match —
|
|
712
725
|
// rationalisation is a separate agent-mediated concern).
|
|
713
|
-
|
|
726
|
+
// Task 850 — Neo4j-leg failure now returns is_error: true with the
|
|
727
|
+
// [admin-auth-store] line; previously it set a soft warning string and
|
|
728
|
+
// returned success, which is what hid the Adam Mackay incident from
|
|
729
|
+
// the recovery agent. The user is still functional via users.json +
|
|
730
|
+
// account.json, but the graph state is divergent and the operator
|
|
731
|
+
// must know.
|
|
714
732
|
let personReused = false;
|
|
715
733
|
try {
|
|
716
734
|
const session = getSession();
|
|
@@ -759,18 +777,25 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
|
|
|
759
777
|
finally {
|
|
760
778
|
await session.close();
|
|
761
779
|
}
|
|
780
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=ok store=neo4j personReused=${personReused}`);
|
|
762
781
|
}
|
|
763
782
|
catch (err) {
|
|
764
783
|
const errMsg = err instanceof Error ? err.message : String(err);
|
|
765
|
-
console.error(
|
|
766
|
-
|
|
784
|
+
console.error(`[admin-auth-store] action=add userId=${userIdShort} result=fail store=neo4j error=${errMsg}`);
|
|
785
|
+
return {
|
|
786
|
+
content: [{
|
|
787
|
+
type: "text",
|
|
788
|
+
text: `${TAG} users.json + account.json updated for userId ${userId} (PIN: ${plaintextPin}); Neo4j sync FAILED — manual reconciliation needed: ${errMsg}`,
|
|
789
|
+
}],
|
|
790
|
+
isError: true,
|
|
791
|
+
};
|
|
767
792
|
}
|
|
768
|
-
console.error(`${TAG} [admin-identity] adminuser-bound userId=${
|
|
793
|
+
console.error(`${TAG} [admin-identity] adminuser-bound userId=${userIdShort} name=${name.trim()} personReused=${personReused}`);
|
|
769
794
|
console.error(`${TAG} admin added: userId=${userId} userName=${name.trim()} accountId=${ACCOUNT_ID} role=admin addedBy=${callerUserId ?? "unknown"}`);
|
|
770
795
|
return {
|
|
771
796
|
content: [{
|
|
772
797
|
type: "text",
|
|
773
|
-
text: `Admin added successfully.\n\n- **Name:** ${name.trim()}\n- **userId:** ${userId}\n- **PIN:** ${plaintextPin}\n- **Role:** admin\n\nShare the PIN with ${name.trim()} so they can log in
|
|
798
|
+
text: `Admin added successfully.\n\n- **Name:** ${name.trim()}\n- **userId:** ${userId}\n- **PIN:** ${plaintextPin}\n- **Role:** admin\n\nShare the PIN with ${name.trim()} so they can log in.`,
|
|
774
799
|
}],
|
|
775
800
|
};
|
|
776
801
|
});
|
|
@@ -912,18 +937,22 @@ server.tool("admin-update-pin", "Update an existing admin user's PIN. Defaults t
|
|
|
912
937
|
users = readUsersJson();
|
|
913
938
|
}
|
|
914
939
|
catch (err) {
|
|
940
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
915
941
|
console.error(`${TAG} userId=${userIdLabel} result=user-not-found reason=users-json-read-failed`);
|
|
916
|
-
|
|
942
|
+
console.error(`[admin-auth-store] action=update-pin userId=${userIdLabel} result=fail store=users error=${errMsg}`);
|
|
943
|
+
return { content: [{ type: "text", text: `${TAG} ${errMsg}` }], isError: true };
|
|
917
944
|
}
|
|
918
945
|
const targetIndex = users.findIndex(u => u.userId === userId);
|
|
919
946
|
if (targetIndex === -1) {
|
|
920
947
|
console.error(`${TAG} userId=${userIdLabel} result=user-not-found`);
|
|
948
|
+
console.error(`[admin-auth-store] action=update-pin userId=${userIdLabel} result=fail store=users error=user-not-found`);
|
|
921
949
|
return { content: [{ type: "text", text: `${TAG} User ${userId} not found in users.json.` }], isError: true };
|
|
922
950
|
}
|
|
923
951
|
const newHash = hashPin(newPin);
|
|
924
952
|
const collidesWithOther = users.some((u, i) => i !== targetIndex && u.pin === newHash);
|
|
925
953
|
if (collidesWithOther) {
|
|
926
954
|
console.error(`${TAG} userId=${userIdLabel} result=collision`);
|
|
955
|
+
console.error(`[admin-auth-store] action=update-pin userId=${userIdLabel} result=fail store=users error=pin-collision`);
|
|
927
956
|
return { content: [{ type: "text", text: `${TAG} That PIN is already in use by another user. Choose a different PIN.` }], isError: true };
|
|
928
957
|
}
|
|
929
958
|
users[targetIndex].pin = newHash;
|
|
@@ -931,9 +960,12 @@ server.tool("admin-update-pin", "Update an existing admin user's PIN. Defaults t
|
|
|
931
960
|
writeUsersJson(users);
|
|
932
961
|
}
|
|
933
962
|
catch (err) {
|
|
934
|
-
|
|
963
|
+
const errMsg = err instanceof Error ? err.message : String(err);
|
|
964
|
+
console.error(`[admin-auth-store] action=update-pin userId=${userIdLabel} result=fail store=users error=${errMsg}`);
|
|
965
|
+
return { content: [{ type: "text", text: `${TAG} Failed to write users.json: ${errMsg}` }], isError: true };
|
|
935
966
|
}
|
|
936
967
|
console.error(`${TAG} userId=${userIdLabel} result=ok`);
|
|
968
|
+
console.error(`[admin-auth-store] action=update-pin userId=${userIdLabel} result=ok store=users`);
|
|
937
969
|
const self = userId === callerUserId;
|
|
938
970
|
return {
|
|
939
971
|
content: [{ type: "text", text: `PIN updated${self ? "" : ` for userId ${userId}`}. The new PIN takes effect on the next login.` }],
|
|
@@ -1147,8 +1179,8 @@ server.tool("agent-list", "List all public (non-admin) agents with their full co
|
|
|
1147
1179
|
};
|
|
1148
1180
|
}
|
|
1149
1181
|
});
|
|
1150
|
-
server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=
|
|
1151
|
-
type: z.enum(["system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc", "review"]).optional(),
|
|
1182
|
+
server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=agent-stream/error/session/public) are now per-conversation — pass `conversationId` to retrieve a single conversation's log from first [spawn] to final [process-exit]. type=agent-stream: per-conversation tool-use/tool-result archive (every `[tool-use]` and `[tool-result]` pair with full input + output JSON, plus raw Claude stream-json, agent events, and MCP server stderr via tee). USE THIS when investigating what an agent ACTUALLY did with its tools — server.log only carries `[persist] tool-call persisted` markers, not bodies. (`type=system` is a backwards-compatible alias for the same archive.) type=session: SSE events sent to client. type=error: Claude subprocess stderr (raw — NODE_DEBUG HTTP/NET/UNDICI traces land in agent-stream via the stream tee, not here). type=heartbeat: platform event dispatcher (check-due-events cron). type=public: public agent diagnostic log. type=server: platform server log. type=mcp: MCP server stderr (per-plugin raw). type=vnc: VNC browser viewer lifecycle. type=review: log-review detector decisions. sessionKey: grep legacy sessionKey-tagged lines across all logs (useful for pre-Task-532 artefacts). When conversationId is provided, reads the single per-conversation file for the requested type (or dumps all type files for that conversationId if type is omitted).", {
|
|
1183
|
+
type: z.enum(["agent-stream", "system", "session", "error", "heartbeat", "public", "server", "mcp", "vnc", "review"]).optional(),
|
|
1152
1184
|
lines: z.number().optional(),
|
|
1153
1185
|
sessionKey: z.string().optional(),
|
|
1154
1186
|
conversationId: z.string().optional(),
|
|
@@ -1181,16 +1213,17 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
1181
1213
|
return { content: [{ type: "text", text: `Log directory does not exist: ${LOG_DIR}` }] };
|
|
1182
1214
|
}
|
|
1183
1215
|
const prefixMap = {
|
|
1184
|
-
|
|
1216
|
+
"agent-stream": "claude-agent-stream",
|
|
1217
|
+
system: "claude-agent-stream", // Task 850 — backwards-compatible alias for agent-stream
|
|
1185
1218
|
error: "claude-agent-stderr",
|
|
1186
1219
|
session: "sse-events",
|
|
1187
1220
|
public: "public-agent-stream",
|
|
1188
1221
|
};
|
|
1189
|
-
const resolvedType = type ?? "
|
|
1222
|
+
const resolvedType = type ?? "agent-stream";
|
|
1190
1223
|
const prefix = prefixMap[resolvedType];
|
|
1191
1224
|
if (!prefix) {
|
|
1192
1225
|
return {
|
|
1193
|
-
content: [{ type: "text", text: `type=${resolvedType} is not per-conversation. Valid per-conversation types:
|
|
1226
|
+
content: [{ type: "text", text: `type=${resolvedType} is not per-conversation. Valid per-conversation types: agent-stream, error, session, public. For platform-scoped types (server, vnc, review, heartbeat, mcp) omit conversationId.` }],
|
|
1194
1227
|
isError: true,
|
|
1195
1228
|
};
|
|
1196
1229
|
}
|
|
@@ -1234,7 +1267,8 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
1234
1267
|
// --- Session-key filtered mode: grep across log files ---
|
|
1235
1268
|
if (sessionKey) {
|
|
1236
1269
|
const prefixes = {
|
|
1237
|
-
|
|
1270
|
+
"agent-stream": "claude-agent-stream-",
|
|
1271
|
+
system: "claude-agent-stream-", // Task 850 — backwards-compatible alias
|
|
1238
1272
|
error: "claude-agent-stderr-",
|
|
1239
1273
|
session: "sse-events-",
|
|
1240
1274
|
public: "public-agent-stream-",
|
|
@@ -1332,7 +1366,7 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
1332
1366
|
return { content: [{ type: "text", text: `# Session timeline: ${sessionKey}\n\n${sections.join("\n\n")}` }] };
|
|
1333
1367
|
}
|
|
1334
1368
|
// --- Standard mode: tail most recent file of the requested type ---
|
|
1335
|
-
const resolvedType = type ?? "
|
|
1369
|
+
const resolvedType = type ?? "agent-stream";
|
|
1336
1370
|
// Heartbeat log is a single fixed file, not prefix-based
|
|
1337
1371
|
if (resolvedType === "heartbeat") {
|
|
1338
1372
|
const logFile = resolve(LOG_DIR, "check-due-events.log");
|
|
@@ -1381,6 +1415,8 @@ server.tool("logs-read", "Read recent logs. Task 532: the stream logs (type=syst
|
|
|
1381
1415
|
}).toString();
|
|
1382
1416
|
return { content: [{ type: "text", text: `# review.log\n\n${result}` }] };
|
|
1383
1417
|
}
|
|
1418
|
+
// Task 850 — agent-stream and the legacy system alias both map to
|
|
1419
|
+
// claude-agent-stream-. The fall-through default also covers them.
|
|
1384
1420
|
const prefix = resolvedType === "error" ? "claude-agent-stderr-"
|
|
1385
1421
|
: resolvedType === "session" ? "sse-events-"
|
|
1386
1422
|
: resolvedType === "public" ? "public-agent-stream-"
|