@rubytech/create-realagent 1.0.772 → 1.0.775

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 (45) hide show
  1. package/package.json +1 -1
  2. package/payload/platform/config/brand.json +1 -0
  3. package/payload/platform/lib/entitlement/PUBKEY-HASH.txt +1 -0
  4. package/payload/platform/lib/entitlement/dist/canonicalize.d.ts +26 -0
  5. package/payload/platform/lib/entitlement/dist/canonicalize.d.ts.map +1 -0
  6. package/payload/platform/lib/entitlement/dist/canonicalize.js +54 -0
  7. package/payload/platform/lib/entitlement/dist/canonicalize.js.map +1 -0
  8. package/payload/platform/lib/entitlement/dist/index.d.ts +76 -0
  9. package/payload/platform/lib/entitlement/dist/index.d.ts.map +1 -0
  10. package/payload/platform/lib/entitlement/dist/index.js +293 -0
  11. package/payload/platform/lib/entitlement/dist/index.js.map +1 -0
  12. package/payload/platform/lib/entitlement/rubytech-pubkey.pem +3 -0
  13. package/payload/platform/package.json +2 -2
  14. package/payload/platform/plugins/admin/hooks/pre-tool-use.sh +32 -0
  15. package/payload/platform/plugins/admin/mcp/dist/index.js +140 -10
  16. package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
  17. package/payload/platform/plugins/admin/skills/business-profile/SKILL.md +5 -6
  18. package/payload/platform/plugins/admin/skills/onboarding/SKILL.md +5 -6
  19. package/payload/platform/plugins/admin/skills/plugin-management/SKILL.md +10 -5
  20. package/payload/platform/scripts/generate-entitlement-fixture.mjs +152 -0
  21. package/payload/server/chunk-2HBD6IRL.js +3242 -0
  22. package/payload/server/chunk-MIP54X7Q.js +3244 -0
  23. package/payload/server/chunk-PIMJJCOQ.js +9563 -0
  24. package/payload/server/chunk-TM3EQSID.js +9800 -0
  25. package/payload/server/client-pool-4MZN42GG.js +28 -0
  26. package/payload/server/client-pool-U3A5YUO7.js +28 -0
  27. package/payload/server/maxy-edge.js +2 -2
  28. package/payload/server/public/assets/{Checkbox-DEE8t2QO.js → Checkbox-C_KxaLc-.js} +1 -1
  29. package/payload/server/public/assets/{admin-CFttroHB.js → admin-xbKPR6ZI.js} +30 -30
  30. package/payload/server/public/assets/data-D23IzpJ2.js +1 -0
  31. package/payload/server/public/assets/graph-D2AS9zFS.js +1 -0
  32. package/payload/server/public/assets/{jsx-runtime-DSbkOE76.css → jsx-runtime-BZtBxBng.css} +1 -1
  33. package/payload/server/public/assets/{page-YUT5e7hL.js → page-CjTfZ3O6.js} +2 -2
  34. package/payload/server/public/assets/{page-ZATk95ZG.js → page-DEWgk_nR.js} +1 -1
  35. package/payload/server/public/assets/{public-BLi3J8KU.js → public-CehiL-qZ.js} +1 -1
  36. package/payload/server/public/assets/{share-2-DS7Pnkkq.js → share-2-BG1VXt3z.js} +1 -1
  37. package/payload/server/public/assets/{useVoiceRecorder-pEHqS1ib.js → useVoiceRecorder-1Dvb-yHn.js} +1 -1
  38. package/payload/server/public/data.html +5 -5
  39. package/payload/server/public/graph.html +6 -6
  40. package/payload/server/public/index.html +8 -8
  41. package/payload/server/public/public.html +5 -5
  42. package/payload/server/server.js +52 -17
  43. package/payload/server/public/assets/data-ryPag-T-.js +0 -1
  44. package/payload/server/public/assets/graph-BPnH-UZB.js +0 -1
  45. /package/payload/server/public/assets/{jsx-runtime-DeNudFNA.js → jsx-runtime-DrneHL3t.js} +0 -0
@@ -10,6 +10,7 @@ import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSyn
10
10
  import { writeKey, validateKey, hasKey, keyFilePath, deleteKey } from "../../../../lib/anthropic-key/dist/index.js";
11
11
  import { deviceUrlBlock } from "../../../../lib/device-url/dist/index.js";
12
12
  import { substituteBrandPlaceholders } from "../../../../lib/brand-templating/dist/index.js";
13
+ import { resolveEntitlement } from "../../../../lib/entitlement/dist/index.js";
13
14
  import { createHash, randomInt, randomUUID } from "node:crypto";
14
15
  import { createConnection } from "node:net";
15
16
  import { homedir, hostname as osHostname } from "node:os";
@@ -30,7 +31,10 @@ const PLATFORM_PORT = process.env.PLATFORM_PORT;
30
31
  if (!PLATFORM_PORT) {
31
32
  throw new Error("PLATFORM_PORT environment variable is required — set by getMcpServers() in claude-agent.ts");
32
33
  }
33
- // Brand-aware config — reads configDir and productName from brand.json stamped at install time.
34
+ // Brand-aware config — reads configDir, productName, and commercialMode from
35
+ // brand.json stamped at install time. commercialMode (Task 831) gates the
36
+ // entitlement verifier: false (default) preserves personal-mode installs;
37
+ // true requires a Rubytech-signed entitlement.json or the install runs locked.
34
38
  // No fallback: if brand.json is missing or incomplete, the platform wasn't properly installed.
35
39
  function resolveBrandConfig() {
36
40
  const brandPath = resolve(PLATFORM_ROOT, "config", "brand.json");
@@ -45,7 +49,11 @@ function resolveBrandConfig() {
45
49
  if (!brand.productName) {
46
50
  throw new Error(`brand.json at ${brandPath} is missing the productName field`);
47
51
  }
48
- return { configDir: brand.configDir, productName: brand.productName };
52
+ return {
53
+ configDir: brand.configDir,
54
+ productName: brand.productName,
55
+ commercialMode: brand.commercialMode === true,
56
+ };
49
57
  }
50
58
  catch (err) {
51
59
  if (err instanceof SyntaxError) {
@@ -57,6 +65,27 @@ function resolveBrandConfig() {
57
65
  const BRAND_CONFIG = resolveBrandConfig();
58
66
  const CONFIG_DIR = resolve(homedir(), BRAND_CONFIG.configDir);
59
67
  const BRAND_NAME = BRAND_CONFIG.productName;
68
+ // Entitlement input shape for the verifier. configDir is rooted under $HOME
69
+ // (where entitlement.json is delivered post-purchase). platformRoot is needed
70
+ // because the verifier runs in two bundle contexts (CJS dist for MCP, ESM tsup
71
+ // bundle for UI) — caller passes the root rather than the verifier guessing.
72
+ const ENTITLEMENT_BRAND = {
73
+ configDir: CONFIG_DIR,
74
+ platformRoot: PLATFORM_ROOT,
75
+ commercialMode: BRAND_CONFIG.commercialMode,
76
+ };
77
+ /** Resolve current effective entitlement. Memoized inside the verifier. */
78
+ async function currentEntitlement() {
79
+ const config = await readAccountConfig();
80
+ return resolveEntitlement(ENTITLEMENT_BRAND, {
81
+ accountId: typeof config.accountId === "string" ? config.accountId : "",
82
+ customerEmail: typeof config.customerEmail === "string" ? config.customerEmail : undefined,
83
+ tier: typeof config.tier === "string" ? config.tier : undefined,
84
+ purchasedPlugins: Array.isArray(config.purchasedPlugins)
85
+ ? config.purchasedPlugins
86
+ : undefined,
87
+ });
88
+ }
60
89
  const REMOTE_PASSWORD_FILE = resolve(CONFIG_DIR, ".remote-password");
61
90
  // Resolve account directory
62
91
  function getAccountDir() {
@@ -542,6 +571,53 @@ server.tool("account-update", "Update a user-configurable setting in account.jso
542
571
  };
543
572
  }
544
573
  });
574
+ // Plugin enable/disable: deterministic write to account.json.enabledPlugins.
575
+ // The pre-tool-use hook denies the agent's direct Edit on account.json (Task 831),
576
+ // so the agent can no longer toggle enablement by hand-editing the file. This
577
+ // tool is the legitimate path: validates the plugin name, refuses core plugins,
578
+ // confirms the plugin directory exists, and atomically updates the array.
579
+ // Entitlement-bearing fields (tier, purchasedPlugins) are NOT writable here.
580
+ server.tool("plugin-toggle-enabled", "Enable or disable a plugin in this account by adding/removing its name from account.json's enabledPlugins array. Validates the plugin exists under platform/plugins/, refuses to disable core plugins (admin, memory, docs, cloudflare, anthropic), and writes atomically. Takes effect on next session start. Does NOT change purchasedPlugins or tier — those derive from the signed entitlement payload.", {
581
+ pluginName: z.string().describe("Plugin slug (lowercase a-z0-9-)."),
582
+ action: z.enum(["enable", "disable"]).describe("enable adds to enabledPlugins; disable removes."),
583
+ }, async ({ pluginName, action }) => {
584
+ const TAG = "[admin:plugin-toggle-enabled]";
585
+ if (!VALID_PLUGIN_NAME.test(pluginName)) {
586
+ return { content: [{ type: "text", text: `${TAG} Invalid plugin name "${pluginName}". Must match ${VALID_PLUGIN_NAME}.` }], isError: true };
587
+ }
588
+ if (CORE_PLUGINS.includes(pluginName)) {
589
+ return { content: [{ type: "text", text: `${TAG} "${pluginName}" is a core plugin and cannot be toggled.` }], isError: true };
590
+ }
591
+ const pluginDir = resolve(PLUGINS_DIR, pluginName);
592
+ if (!existsSync(pluginDir) || !existsSync(join(pluginDir, "PLUGIN.md"))) {
593
+ return { content: [{ type: "text", text: `${TAG} Plugin "${pluginName}" not installed at ${pluginDir} (no PLUGIN.md). Install via premium-deliver or platform release.` }], isError: true };
594
+ }
595
+ try {
596
+ const configPath = join(getAccountDir(), "account.json");
597
+ const config = await readAccountConfig();
598
+ const current = Array.isArray(config.enabledPlugins) ? config.enabledPlugins : [];
599
+ let next;
600
+ if (action === "enable") {
601
+ if (current.includes(pluginName)) {
602
+ return { content: [{ type: "text", text: `${TAG} "${pluginName}" is already enabled.` }] };
603
+ }
604
+ next = [...current, pluginName];
605
+ }
606
+ else {
607
+ if (!current.includes(pluginName)) {
608
+ return { content: [{ type: "text", text: `${TAG} "${pluginName}" is not enabled — nothing to disable.` }] };
609
+ }
610
+ next = current.filter((n) => n !== pluginName);
611
+ }
612
+ config.enabledPlugins = next;
613
+ await writeFile(configPath, JSON.stringify(config, null, 2) + "\n", "utf-8");
614
+ console.error(`${TAG} ${action}d plugin=${pluginName} path=${configPath}`);
615
+ return { content: [{ type: "text", text: `Plugin "${pluginName}" ${action}d. Takes effect on next session.` }] };
616
+ }
617
+ catch (err) {
618
+ return { content: [{ type: "text", text: `${TAG} Failed: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
619
+ }
620
+ });
545
621
  // ===================================================================
546
622
  // Admin user management tools
547
623
  // ===================================================================
@@ -560,11 +636,15 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
560
636
  catch (err) {
561
637
  return { content: [{ type: "text", text: `${TAG} ${err instanceof Error ? err.message : String(err)}` }], isError: true };
562
638
  }
563
- // Enforce per-tier admin limit
639
+ // Enforce per-tier admin limit. Effective tier comes from the signed
640
+ // entitlement payload (Task 831) — editing account.json.tier directly
641
+ // has no effect; the verifier uses the signed value and emits a
642
+ // [entitlement] tampered: line if disk and signed diverge.
564
643
  try {
565
644
  const config = await readAccountConfig();
566
645
  const currentAdmins = (config.admins ?? []);
567
- const tier = typeof config.tier === "string" ? config.tier : "";
646
+ const entitlement = await currentEntitlement();
647
+ const tier = entitlement.tier;
568
648
  const maxAdmins = MAX_ADMINS_BY_TIER[tier] ?? MAX_ADMINS_DEFAULT;
569
649
  if (currentAdmins.length >= maxAdmins) {
570
650
  return { content: [{ type: "text", text: `${TAG} Admin limit reached (${maxAdmins} for ${tier || "this"} tier). Remove an existing admin before adding a new one.` }], isError: true };
@@ -623,20 +703,58 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
623
703
  console.error(`${TAG} account.json write failed: ${err instanceof Error ? err.message : String(err)}`);
624
704
  return { content: [{ type: "text", text: `${TAG} User created in users.json but failed to add to account: ${err instanceof Error ? err.message : String(err)}` }], isError: true };
625
705
  }
626
- // 3. Write to Neo4j (graph-level) partial failure is a warning, not an error
706
+ // 3. Write to Neo4j (graph-level): AdminUser + Person + OWNS atomically,
707
+ // plus ADMIN_OF edge to the LocalBusiness for this account.
708
+ // Task 830 — deterministic identity creation: never delegate node
709
+ // creation to the LLM. Person reuse rule mirrors writeAdminUserAndPerson
710
+ // in platform/ui/app/lib/neo4j-store.ts (case-insensitive exact match
711
+ // on givenName + familyName; partial-name ambiguity does NOT match —
712
+ // rationalisation is a separate agent-mediated concern).
627
713
  let neo4jWarning = "";
714
+ let personReused = false;
628
715
  try {
629
716
  const session = getSession();
630
717
  try {
631
718
  const createdAt = new Date().toISOString();
632
719
  const trimmedName = name.trim();
633
- await session.run(`MERGE (au:AdminUser {userId: $userId})
720
+ const firstSpace = trimmedName.search(/\s/);
721
+ const givenName = firstSpace === -1 ? trimmedName : trimmedName.slice(0, firstSpace).trim();
722
+ const familyName = firstSpace === -1 ? null : (trimmedName.slice(firstSpace + 1).trim() || null);
723
+ const result = await session.run(`MERGE (au:AdminUser {userId: $userId})
634
724
  ON CREATE SET au.name = $name, au.createdAt = $createdAt
635
- ON MATCH SET au.name = $name
725
+ ON MATCH SET au.name = $name, au.updatedAt = $createdAt
636
726
  WITH au
637
727
  MATCH (b:LocalBusiness {accountId: $accountId})
638
728
  MERGE (au)-[r:ADMIN_OF]->(b)
639
- ON CREATE SET r.role = 'admin', r.grantedAt = $createdAt`, { userId, name: trimmedName, createdAt, accountId: ACCOUNT_ID });
729
+ ON CREATE SET r.role = 'admin', r.grantedAt = $createdAt
730
+ WITH au
731
+ OPTIONAL MATCH (existingPerson:Person {accountId: $accountId})
732
+ WHERE toLower(existingPerson.givenName) = toLower($givenName)
733
+ AND coalesce(toLower(existingPerson.familyName), '') = coalesce(toLower($familyName), '')
734
+ WITH au, existingPerson
735
+ CALL {
736
+ WITH au, existingPerson
737
+ WITH au, existingPerson WHERE existingPerson IS NOT NULL
738
+ MERGE (au)-[:OWNS]->(existingPerson)
739
+ RETURN true AS reused
740
+ UNION
741
+ WITH au, existingPerson
742
+ WITH au WHERE existingPerson IS NULL
743
+ CREATE (newPerson:Person {
744
+ accountId: $accountId,
745
+ givenName: $givenName,
746
+ familyName: $familyName,
747
+ role: 'admin-personal',
748
+ scope: 'admin',
749
+ createdAt: $createdAt
750
+ })
751
+ MERGE (au)-[:OWNS]->(newPerson)
752
+ RETURN false AS reused
753
+ }
754
+ RETURN reused`, { userId, name: trimmedName, createdAt, accountId: ACCOUNT_ID, givenName, familyName });
755
+ if (result.records.length > 0) {
756
+ personReused = result.records[0].get("reused");
757
+ }
640
758
  }
641
759
  finally {
642
760
  await session.close();
@@ -647,6 +765,7 @@ server.tool("admin-add", "Add a new admin user to this account. Creates a device
647
765
  console.error(`${TAG} Neo4j sync failed during admin-add: userId=${userId} error=${errMsg} — files written, graph pending`);
648
766
  neo4jWarning = ` Note: Neo4j sync failed (${errMsg}) — the admin user is functional but the graph will need reconciliation on next seed.`;
649
767
  }
768
+ console.error(`${TAG} [admin-identity] adminuser-bound userId=${userId.slice(0, 8)} name=${name.trim()} personReused=${personReused}`);
650
769
  console.error(`${TAG} admin added: userId=${userId} userName=${name.trim()} accountId=${ACCOUNT_ID} role=admin addedBy=${callerUserId ?? "unknown"}`);
651
770
  return {
652
771
  content: [{
@@ -2140,7 +2259,9 @@ server.tool("premium-list", "List available premium plugins and their delivery s
2140
2259
  };
2141
2260
  }
2142
2261
  const config = await readAccountConfig();
2143
- const purchased = Array.isArray(config.purchasedPlugins) ? config.purchasedPlugins : [];
2262
+ // Effective purchasedPlugins from signed entitlement (Task 831).
2263
+ const entitlement = await currentEntitlement();
2264
+ const purchased = entitlement.purchasedPlugins;
2144
2265
  const enabled = Array.isArray(config.enabledPlugins) ? config.enabledPlugins : [];
2145
2266
  const entries = readdirSync(STAGING_ROOT, { withFileTypes: true })
2146
2267
  .filter(e => e.isDirectory())
@@ -2245,7 +2366,16 @@ server.tool("premium-deliver", "Deliver a purchased premium plugin. Copies plugi
2245
2366
  isError: true,
2246
2367
  };
2247
2368
  }
2248
- const purchased = Array.isArray(config.purchasedPlugins) ? config.purchasedPlugins : [];
2369
+ // Effective purchasedPlugins from signed entitlement (Task 831). The
2370
+ // raw account.json value is ignored on commercial installs; verifier
2371
+ // returns the signed list (or [] on anonymous-fallback).
2372
+ const entitlement = await resolveEntitlement(ENTITLEMENT_BRAND, {
2373
+ accountId: typeof config.accountId === "string" ? config.accountId : "",
2374
+ customerEmail: typeof config.customerEmail === "string" ? config.customerEmail : undefined,
2375
+ tier: typeof config.tier === "string" ? config.tier : undefined,
2376
+ purchasedPlugins: Array.isArray(config.purchasedPlugins) ? config.purchasedPlugins : undefined,
2377
+ });
2378
+ const purchased = entitlement.purchasedPlugins;
2249
2379
  // --- Check purchase status ---
2250
2380
  if (!purchased.includes(pluginName)) {
2251
2381
  return {