nodal-agents 0.6.8 → 0.7.2

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 (169) hide show
  1. package/cli.js +359 -35
  2. package/migrations/0051_memory_search_fts.sql +2 -0
  3. package/migrations/0052_entities_memory_curation.sql +1 -0
  4. package/migrations/0053_memory_importance_locked.sql +1 -0
  5. package/migrations/0054_audit2_db_integrity.sql +52 -0
  6. package/migrations/0055_entity_settings.sql +22 -0
  7. package/migrations/0056_agents_skills_entity_scoped_unique.sql +23 -0
  8. package/migrations/0057_telegram_allowed_chats.sql +34 -0
  9. package/migrations/meta/_journal.json +49 -0
  10. package/package.json +1 -1
  11. package/runner.js +47144 -35235
  12. package/web/.next/BUILD_ID +1 -1
  13. package/web/.next/app-path-routes-manifest.json +3 -3
  14. package/web/.next/build-manifest.json +2 -2
  15. package/web/.next/prerender-manifest.json +3 -3
  16. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js +3 -3
  17. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js.nft.json +1 -1
  18. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page_client-reference-manifest.js +1 -1
  19. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js +2 -2
  20. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js.nft.json +1 -1
  21. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page_client-reference-manifest.js +1 -1
  22. package/web/.next/server/app/(dashboard)/agents/page.js +2 -2
  23. package/web/.next/server/app/(dashboard)/agents/page.js.nft.json +1 -1
  24. package/web/.next/server/app/(dashboard)/agents/page_client-reference-manifest.js +1 -1
  25. package/web/.next/server/app/(dashboard)/approvals/page.js +2 -2
  26. package/web/.next/server/app/(dashboard)/approvals/page.js.nft.json +1 -1
  27. package/web/.next/server/app/(dashboard)/approvals/page_client-reference-manifest.js +1 -1
  28. package/web/.next/server/app/(dashboard)/automations/page.js +2 -2
  29. package/web/.next/server/app/(dashboard)/automations/page.js.nft.json +1 -1
  30. package/web/.next/server/app/(dashboard)/automations/page_client-reference-manifest.js +1 -1
  31. package/web/.next/server/app/(dashboard)/chat/page.js +2 -2
  32. package/web/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  33. package/web/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  34. package/web/.next/server/app/(dashboard)/connectors/page.js +2 -2
  35. package/web/.next/server/app/(dashboard)/connectors/page.js.nft.json +1 -1
  36. package/web/.next/server/app/(dashboard)/connectors/page_client-reference-manifest.js +1 -1
  37. package/web/.next/server/app/(dashboard)/credentials/page.js +1 -1
  38. package/web/.next/server/app/(dashboard)/credentials/page.js.nft.json +1 -1
  39. package/web/.next/server/app/(dashboard)/credentials/page_client-reference-manifest.js +1 -1
  40. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js +2 -2
  41. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js.nft.json +1 -1
  42. package/web/.next/server/app/(dashboard)/jobs/[id]/page_client-reference-manifest.js +1 -1
  43. package/web/.next/server/app/(dashboard)/jobs/page.js +2 -2
  44. package/web/.next/server/app/(dashboard)/jobs/page.js.nft.json +1 -1
  45. package/web/.next/server/app/(dashboard)/jobs/page_client-reference-manifest.js +1 -1
  46. package/web/.next/server/app/(dashboard)/learned-skills/page.js +2 -2
  47. package/web/.next/server/app/(dashboard)/learned-skills/page.js.nft.json +1 -1
  48. package/web/.next/server/app/(dashboard)/learned-skills/page_client-reference-manifest.js +1 -1
  49. package/web/.next/server/app/(dashboard)/llm-providers/page.js +2 -2
  50. package/web/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  51. package/web/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  52. package/web/.next/server/app/(dashboard)/logs/page.js +2 -2
  53. package/web/.next/server/app/(dashboard)/logs/page.js.nft.json +1 -1
  54. package/web/.next/server/app/(dashboard)/logs/page_client-reference-manifest.js +1 -1
  55. package/web/.next/server/app/(dashboard)/mcp/page.js +2 -2
  56. package/web/.next/server/app/(dashboard)/mcp/page.js.nft.json +1 -1
  57. package/web/.next/server/app/(dashboard)/mcp/page_client-reference-manifest.js +1 -1
  58. package/web/.next/server/app/(dashboard)/memories/page.js +2 -2
  59. package/web/.next/server/app/(dashboard)/memories/page.js.nft.json +1 -1
  60. package/web/.next/server/app/(dashboard)/memories/page_client-reference-manifest.js +1 -1
  61. package/web/.next/server/app/(dashboard)/page.js +3 -3
  62. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  63. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  64. package/web/.next/server/app/(dashboard)/settings/page.js +2 -2
  65. package/web/.next/server/app/(dashboard)/settings/page.js.nft.json +1 -1
  66. package/web/.next/server/app/(dashboard)/settings/page_client-reference-manifest.js +1 -1
  67. package/web/.next/server/app/(dashboard)/settings/root-context/page.js +2 -2
  68. package/web/.next/server/app/(dashboard)/settings/root-context/page.js.nft.json +1 -1
  69. package/web/.next/server/app/(dashboard)/settings/root-context/page_client-reference-manifest.js +1 -1
  70. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js +2 -2
  71. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js.nft.json +1 -1
  72. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page_client-reference-manifest.js +1 -1
  73. package/web/.next/server/app/(dashboard)/skills/new/page.js +2 -2
  74. package/web/.next/server/app/(dashboard)/skills/new/page.js.nft.json +1 -1
  75. package/web/.next/server/app/(dashboard)/skills/new/page_client-reference-manifest.js +1 -1
  76. package/web/.next/server/app/(dashboard)/skills/page.js +2 -2
  77. package/web/.next/server/app/(dashboard)/skills/page.js.nft.json +1 -1
  78. package/web/.next/server/app/(dashboard)/skills/page_client-reference-manifest.js +1 -1
  79. package/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  80. package/web/.next/server/app/_global-error.html +1 -1
  81. package/web/.next/server/app/_global-error.rsc +2 -2
  82. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  83. package/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  84. package/web/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  85. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  86. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +2 -2
  87. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  88. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  89. package/web/.next/server/app/_not-found.html +1 -1
  90. package/web/.next/server/app/_not-found.rsc +3 -3
  91. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  92. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  93. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  94. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  95. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  96. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  97. package/web/.next/server/app/api/auth/[...all]/route.js +1 -1
  98. package/web/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
  99. package/web/.next/server/app/api/oauth/[provider]/callback/route.js +1 -1
  100. package/web/.next/server/app/api/oauth/[provider]/callback/route.js.nft.json +1 -1
  101. package/web/.next/server/app/api/oauth/[provider]/start/route.js +1 -1
  102. package/web/.next/server/app/api/oauth/[provider]/start/route.js.nft.json +1 -1
  103. package/web/.next/server/app/login/page_client-reference-manifest.js +1 -1
  104. package/web/.next/server/app/onboarding/page.js +214 -190
  105. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  106. package/web/.next/server/app-paths-manifest.json +3 -3
  107. package/web/.next/server/chunks/2713.js +47 -23
  108. package/web/.next/server/chunks/3223.js +1 -1
  109. package/web/.next/server/chunks/4574.js +1 -1
  110. package/web/.next/server/chunks/5515.js +1 -0
  111. package/web/.next/server/chunks/6468.js +1 -0
  112. package/web/.next/server/chunks/8410.js +1 -0
  113. package/web/.next/server/chunks/{4488.js → 8810.js} +2 -2
  114. package/web/.next/server/chunks/{3154.js → 960.js} +10 -10
  115. package/web/.next/server/middleware-build-manifest.js +1 -1
  116. package/web/.next/server/pages/404.html +1 -1
  117. package/web/.next/server/pages/500.html +1 -1
  118. package/web/.next/server/server-reference-manifest.js +1 -1
  119. package/web/.next/server/server-reference-manifest.json +1 -1
  120. package/web/.next/static/chunks/4964-c6711d3d479b6085.js +1 -0
  121. package/web/.next/static/chunks/{9836-6a7e70d2572d9671.js → 9836-177f6ca2f15ff703.js} +1 -1
  122. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-cfce874f4acede5e.js +6 -0
  123. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-9249c2839f551ba1.js +1 -0
  124. package/web/.next/static/chunks/app/(dashboard)/agents/page-7d784d737acedccf.js +1 -0
  125. package/web/.next/static/chunks/app/(dashboard)/approvals/{page-6435e07096efe1ef.js → page-b3a838ecbc94a030.js} +1 -1
  126. package/web/.next/static/chunks/app/(dashboard)/automations/page-b1b548de4f52696c.js +1 -0
  127. package/web/.next/static/chunks/app/(dashboard)/chat/page-bcf6dcc73b370eb0.js +1 -0
  128. package/web/.next/static/chunks/app/(dashboard)/connectors/page-2c6f913cc6aea4b7.js +1 -0
  129. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-43f033251eddcc4b.js +1 -0
  130. package/web/.next/static/chunks/app/(dashboard)/jobs/page-1f892f950bb30403.js +1 -0
  131. package/web/.next/static/chunks/app/(dashboard)/layout-e184673151ff8a05.js +1 -0
  132. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-f873bb6f8539e47e.js +1 -0
  133. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-64adda30176f4ccb.js +1 -0
  134. package/web/.next/static/chunks/app/(dashboard)/logs/{page-f9efb4dd80bf104a.js → page-79cfd9141c39d8ef.js} +1 -1
  135. package/web/.next/static/chunks/app/(dashboard)/mcp/page-d276d7bab25286a1.js +1 -0
  136. package/web/.next/static/chunks/app/(dashboard)/memories/page-c66720494a340503.js +1 -0
  137. package/web/.next/static/chunks/app/(dashboard)/page-3649204dd27a0ad0.js +1 -0
  138. package/web/.next/static/chunks/app/(dashboard)/settings/page-1cab404d2eccfd80.js +1 -0
  139. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-b5898c5444f88c9a.js +1 -0
  140. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/{page-d20237d23a440118.js → page-82d91fc6b6acc161.js} +1 -1
  141. package/web/.next/static/chunks/app/(dashboard)/skills/new/{page-8454b37bed789c4e.js → page-58b5ed04583ca7d4.js} +1 -1
  142. package/web/.next/static/chunks/app/(dashboard)/skills/page-312bd4ae8ddc8114.js +1 -0
  143. package/web/.next/static/chunks/app/onboarding/page-f2f04878e81fdf23.js +26 -0
  144. package/web/.next/static/css/4d808b91d7b58fce.css +3 -0
  145. package/web/.next/server/chunks/2053.js +0 -1
  146. package/web/.next/server/chunks/5290.js +0 -1
  147. package/web/.next/server/chunks/9282.js +0 -1
  148. package/web/.next/static/chunks/4964-09a4dc14acc324f3.js +0 -1
  149. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-1172f97f44c6b953.js +0 -6
  150. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-a858be2a0b9d5d42.js +0 -1
  151. package/web/.next/static/chunks/app/(dashboard)/agents/page-637555c8b299e4d7.js +0 -1
  152. package/web/.next/static/chunks/app/(dashboard)/automations/page-416f63bbd11d1a93.js +0 -1
  153. package/web/.next/static/chunks/app/(dashboard)/chat/page-53507d81c2ceee4c.js +0 -1
  154. package/web/.next/static/chunks/app/(dashboard)/connectors/page-f6a4d52ef7ea8c7f.js +0 -1
  155. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-acb7d1383384f27b.js +0 -1
  156. package/web/.next/static/chunks/app/(dashboard)/jobs/page-b82e456c240416ad.js +0 -1
  157. package/web/.next/static/chunks/app/(dashboard)/layout-d0662737b23eaebb.js +0 -1
  158. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-3c67b5a3869a9224.js +0 -1
  159. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-294135b03955dd45.js +0 -1
  160. package/web/.next/static/chunks/app/(dashboard)/mcp/page-482dc02cf953c197.js +0 -1
  161. package/web/.next/static/chunks/app/(dashboard)/memories/page-ce432008a8a4c118.js +0 -1
  162. package/web/.next/static/chunks/app/(dashboard)/page-d76a7350bf5bea10.js +0 -1
  163. package/web/.next/static/chunks/app/(dashboard)/settings/page-80cb948b3c0cef7d.js +0 -1
  164. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-8e7dabbb399e369d.js +0 -1
  165. package/web/.next/static/chunks/app/(dashboard)/skills/page-af8d329facf31c8f.js +0 -1
  166. package/web/.next/static/chunks/app/onboarding/page-3543225eab5f4029.js +0 -26
  167. package/web/.next/static/css/b06a60c5d9b30bc8.css +0 -3
  168. /package/web/.next/static/{RORb5LJdSgtd7i6KVr0zl → SgnP_MouhUmw0bBf-lUmD}/_buildManifest.js +0 -0
  169. /package/web/.next/static/{RORb5LJdSgtd7i6KVr0zl → SgnP_MouhUmw0bBf-lUmD}/_ssgManifest.js +0 -0
package/cli.js CHANGED
@@ -13,7 +13,8 @@ var __export = (target, all) => {
13
13
  var version_exports = {};
14
14
  __export(version_exports, {
15
15
  getInstalledVersion: () => getInstalledVersion,
16
- getLatestVersion: () => getLatestVersion
16
+ getLatestVersion: () => getLatestVersion,
17
+ isNewerVersion: () => isNewerVersion
17
18
  });
18
19
  import { createRequire } from "module";
19
20
  import { fileURLToPath } from "url";
@@ -49,6 +50,20 @@ async function getLatestVersion() {
49
50
  return null;
50
51
  }
51
52
  }
53
+ function parseVersionTriple(v) {
54
+ const match = /^(\d+)\.(\d+)\.(\d+)/.exec(v);
55
+ if (!match) return null;
56
+ return [Number(match[1]), Number(match[2]), Number(match[3])];
57
+ }
58
+ function isNewerVersion(candidate, current) {
59
+ const a = parseVersionTriple(candidate);
60
+ const b2 = parseVersionTriple(current);
61
+ if (!a || !b2) return false;
62
+ for (let i = 0; i < 3; i++) {
63
+ if (a[i] !== b2[i]) return a[i] > b2[i];
64
+ }
65
+ return false;
66
+ }
52
67
  var init_version = __esm({
53
68
  "src/lib/version.ts"() {
54
69
  "use strict";
@@ -59,7 +74,7 @@ var init_version = __esm({
59
74
  import { z } from "zod";
60
75
  import { homedir } from "os";
61
76
  import { join as join2 } from "path";
62
- import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync2 } from "fs";
77
+ import { readFileSync, writeFileSync, mkdirSync, existsSync as existsSync2, chmodSync } from "fs";
63
78
  import { randomBytes } from "crypto";
64
79
  function ensureConfigDir() {
65
80
  for (const dir of [CONFIG_DIR, PID_DIR, LOG_DIR, PG_DATA_DIR]) {
@@ -81,11 +96,21 @@ function readConfig(configFile = CONFIG_FILE) {
81
96
  parsed = { ...parsed, serverActionsKey: randomBytes(32).toString("base64") };
82
97
  writeConfig(parsed, configFile);
83
98
  }
99
+ if (!parsed.authSecret) {
100
+ parsed = { ...parsed, authSecret: randomBytes(32).toString("base64") };
101
+ writeConfig(parsed, configFile);
102
+ }
103
+ if (configFile === CONFIG_FILE) {
104
+ try {
105
+ chmodSync(configFile, 384);
106
+ } catch {
107
+ }
108
+ }
84
109
  return parsed;
85
110
  }
86
111
  function writeConfig(config, configFile = CONFIG_FILE) {
87
112
  if (configFile === CONFIG_FILE) ensureConfigDir();
88
- writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
113
+ writeFileSync(configFile, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
89
114
  }
90
115
  var ConfigSchema, CONFIG_DIR, CONFIG_FILE, PID_DIR, LOG_DIR, PG_DATA_DIR;
91
116
  var init_config = __esm({
@@ -133,6 +158,18 @@ var init_config = __esm({
133
158
  * files; `readConfig` auto-mints + persists when absent.
134
159
  */
135
160
  serverActionsKey: z.string().length(44).optional(),
161
+ /**
162
+ * Base64-encoded 32-byte AES key used as better-auth's AUTH_SECRET (session
163
+ * cookie signing) for the web process. Deliberately SEPARATE from
164
+ * `workerSecret` (M-4): `workerSecret` is the runner's auth frontier — a
165
+ * leak of it must not also let an attacker forge a signed session cookie.
166
+ *
167
+ * Optional in the schema for back-compat with pre-existing config.json
168
+ * files; `readConfig` auto-mints + persists when absent. On an existing
169
+ * install this invalidates any active better-auth sessions ONCE (users
170
+ * re-login) — an acceptable one-time cost for closing the amplification.
171
+ */
172
+ authSecret: z.string().length(44).optional(),
136
173
  bind: z.enum(["loopback", "lan"]).default("loopback"),
137
174
  bearerToken: z.string().optional(),
138
175
  /**
@@ -6037,6 +6074,12 @@ var init_policies = __esm({
6037
6074
  });
6038
6075
 
6039
6076
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@electri_96bd1661c4535b1384d37581a38c7c74/node_modules/drizzle-orm/pg-core/primary-keys.js
6077
+ function primaryKey(...config) {
6078
+ if (config[0].columns) {
6079
+ return new PrimaryKeyBuilder(config[0].columns, config[0].name);
6080
+ }
6081
+ return new PrimaryKeyBuilder(config);
6082
+ }
6040
6083
  var PrimaryKeyBuilder, PrimaryKey;
6041
6084
  var init_primary_keys = __esm({
6042
6085
  "../../node_modules/.pnpm/drizzle-orm@0.45.2_@electri_96bd1661c4535b1384d37581a38c7c74/node_modules/drizzle-orm/pg-core/primary-keys.js"() {
@@ -6400,19 +6443,19 @@ function extractTablesRelationalConfig(schema, configHelpers) {
6400
6443
  const relations2 = value.config(
6401
6444
  configHelpers(value.table)
6402
6445
  );
6403
- let primaryKey;
6446
+ let primaryKey2;
6404
6447
  for (const [relationName, relation] of Object.entries(relations2)) {
6405
6448
  if (tableName) {
6406
6449
  const tableConfig = tablesConfig[tableName];
6407
6450
  tableConfig.relations[relationName] = relation;
6408
- if (primaryKey) {
6409
- tableConfig.primaryKey.push(...primaryKey);
6451
+ if (primaryKey2) {
6452
+ tableConfig.primaryKey.push(...primaryKey2);
6410
6453
  }
6411
6454
  } else {
6412
6455
  if (!(dbName in relationsBuffer)) {
6413
6456
  relationsBuffer[dbName] = {
6414
6457
  relations: {},
6415
- primaryKey
6458
+ primaryKey: primaryKey2
6416
6459
  };
6417
6460
  }
6418
6461
  relationsBuffer[dbName].relations[relationName] = relation;
@@ -11511,6 +11554,10 @@ var init_memory = __esm({
11511
11554
  valid_to: z13.string().datetime().nullable(),
11512
11555
  fact_hash: z13.string().nullable(),
11513
11556
  archived: z13.boolean(),
11557
+ // User override lock (migration 0053) — when true, the curator refuses to
11558
+ // re-score importance for this fact. Defaulted so rows/objects predating
11559
+ // the column still parse.
11560
+ importance_locked: z13.boolean().default(false),
11514
11561
  last_accessed_at: z13.string().datetime().nullable(),
11515
11562
  access_count: z13.number().int().min(0),
11516
11563
  created_at: z13.string().datetime(),
@@ -11522,6 +11569,7 @@ var init_memory = __esm({
11522
11569
  updated_at: true,
11523
11570
  fact_hash: true,
11524
11571
  archived: true,
11572
+ importance_locked: true,
11525
11573
  last_accessed_at: true,
11526
11574
  access_count: true,
11527
11575
  valid_from: true,
@@ -11878,6 +11926,13 @@ var init_root_agent = __esm({
11878
11926
  }
11879
11927
  });
11880
11928
 
11929
+ // ../../packages/shared/src/system-prompt-cache.ts
11930
+ var init_system_prompt_cache = __esm({
11931
+ "../../packages/shared/src/system-prompt-cache.ts"() {
11932
+ "use strict";
11933
+ }
11934
+ });
11935
+
11881
11936
  // ../../packages/shared/src/connector-catalog.ts
11882
11937
  var init_connector_catalog = __esm({
11883
11938
  "../../packages/shared/src/connector-catalog.ts"() {
@@ -11909,7 +11964,10 @@ var init_model_catalog = __esm({
11909
11964
  // deepseek-chat is the production alias for DeepSeek V3. Standard tool
11910
11965
  // calling with forced tool_choice supported.
11911
11966
  capabilities: { tools: true, forcedToolChoice: true },
11912
- contextWindow: 128e3
11967
+ contextWindow: 128e3,
11968
+ // Standard (cache-miss) rate per DeepSeek's public pricing page —
11969
+ // verify against api-docs.deepseek.com/quick_start/pricing if this drifts.
11970
+ pricing: { inputPerMillionUsd: 0.27, outputPerMillionUsd: 1.1 }
11913
11971
  },
11914
11972
  {
11915
11973
  modelId: "deepseek-reasoner",
@@ -11919,7 +11977,8 @@ var init_model_catalog = __esm({
11919
11977
  // reasoning:true). Also requires reasoning_content to be echoed back on
11920
11978
  // assistant messages with tool_calls (the deepseek fetch shim handles this).
11921
11979
  capabilities: { tools: true, forcedToolChoice: true, reasoning: true },
11922
- contextWindow: 128e3
11980
+ contextWindow: 128e3,
11981
+ pricing: { inputPerMillionUsd: 0.55, outputPerMillionUsd: 2.19 }
11923
11982
  }
11924
11983
  ],
11925
11984
  // ─── Native MiniMax (api.minimax.io/anthropic) ───────────────────────────────
@@ -11932,6 +11991,11 @@ var init_model_catalog = __esm({
11932
11991
  // forced tool_choice with a 400/404 (observed on M3 via OpenRouter); the
11933
11992
  // runtime completion floor covers it. Context windows are docs-sourced where
11934
11993
  // available, else a conservative 200K until a live probe confirms.
11994
+ // No `pricing` yet (unlike deepseek above) — no confidently-current MiniMax
11995
+ // rate card verified at write time. Left as documented debt (Fix #21): the
11996
+ // derived-cost fallback returns 0 for these until pricing is added, same as
11997
+ // every other unpriced entry in this catalog; Guard 1a (token budget) is the
11998
+ // backstop meanwhile.
11935
11999
  minimax: [
11936
12000
  {
11937
12001
  modelId: "MiniMax-M3",
@@ -12215,6 +12279,7 @@ var init_src2 = __esm({
12215
12279
  init_operation();
12216
12280
  init_providers();
12217
12281
  init_root_agent();
12282
+ init_system_prompt_cache();
12218
12283
  init_connector_catalog();
12219
12284
  init_community_skill_catalog();
12220
12285
  init_model_catalog();
@@ -12330,6 +12395,10 @@ var init_entities = __esm({
12330
12395
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
12331
12396
  lastCuratorRunAt: timestamp("last_curator_run_at", { withTimezone: true }),
12332
12397
  reflectionEnabled: boolean("reflection_enabled").notNull().default(false),
12398
+ // Decoupled from reflection_enabled (which gates the skill-learning loop,
12399
+ // opt-in). Gates the memory curator (Phase 2 LLM pass) — ON by default so
12400
+ // memory curation runs even for entities that never opted into reflection.
12401
+ memoryCurationEnabled: boolean("memory_curation_enabled").notNull().default(true),
12333
12402
  // Controls whether agent-authored skills are auto-assigned to the authoring
12334
12403
  // agent ('auto') or queued for the entity owner to approve ('approval').
12335
12404
  skillAssignmentMode: text("skill_assignment_mode").notNull().default("approval").$type(),
@@ -12361,7 +12430,13 @@ var init_entities = __esm({
12361
12430
  check(
12362
12431
  "entity_members_role_check",
12363
12432
  sql`${table.role} IN ('owner', 'admin', 'member', 'viewer')`
12364
- )
12433
+ ),
12434
+ // R5 (audit #2 follow-up, found while sweeping helpers.ts for the SAME
12435
+ // class of phantom constraint): the pglite test DDL already assumed a
12436
+ // user can only be a member of an entity once — this table had no such
12437
+ // constraint in the real schema/migrations. Both columns are NOT NULL, so
12438
+ // a plain UNIQUE is correct (no NULLS NOT DISTINCT needed).
12439
+ uniqueIndex("entity_members_entity_user_unique").on(table.entityId, table.userId)
12365
12440
  ]
12366
12441
  );
12367
12442
  }
@@ -12413,7 +12488,7 @@ var init_agents = __esm({
12413
12488
  id: uuid("id").primaryKey().defaultRandom(),
12414
12489
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12415
12490
  name: text("name").notNull(),
12416
- slug: text("slug").notNull().unique(),
12491
+ slug: text("slug").notNull(),
12417
12492
  personality: text("personality").notNull(),
12418
12493
  model: text("model").default("claude-sonnet-4-6-20260217"),
12419
12494
  llmKeyId: uuid("llm_key_id").references(() => entityLlmKeys.id, { onDelete: "set null" }),
@@ -12463,7 +12538,17 @@ var init_agents = __esm({
12463
12538
  "agents_orchestrator_mode_check",
12464
12539
  sql`${table.orchestratorMode} IN ('router', 'planner') OR ${table.orchestratorMode} IS NULL`
12465
12540
  ),
12466
- check("agents_max_tokens_per_job_check", sql`${table.maxTokensPerJob} >= 0`)
12541
+ check("agents_max_tokens_per_job_check", sql`${table.maxTokensPerJob} >= 0`),
12542
+ // F-6 (audit #2): slug was UNIQUE GLOBALLY, so a 2nd workspace/entity
12543
+ // installing the same community skill's companion agent (or any agent
12544
+ // sharing a slug with another entity's agent) would crash the insert —
12545
+ // and, in multi-user mode, let one entity enumerate/squat another's
12546
+ // slugs. Scoped to (entity_id, slug): every real insert always sets
12547
+ // entityId (createAgentRepo takes it as a required param — no production
12548
+ // path leaves it NULL), but the column itself is nullable, so
12549
+ // NULLS NOT DISTINCT closes the same NULL-entity gap as DB-1/agent_plugins
12550
+ // (multiple (NULL, 'x') rows would otherwise all satisfy a plain UNIQUE).
12551
+ unique("agents_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct()
12467
12552
  ]
12468
12553
  );
12469
12554
  agentAssignments = pgTable(
@@ -12476,7 +12561,18 @@ var init_agents = __esm({
12476
12561
  instructions: text("instructions"),
12477
12562
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
12478
12563
  },
12479
- () => []
12564
+ (table) => [
12565
+ // F-18 (audit #2): no index beyond the PK meant updateAgentAction's
12566
+ // delete-then-insert (apps/web/src/lib/actions.ts) could race and leave the
12567
+ // same sub-agent attached twice under one orchestrator (duplicated in the
12568
+ // team-block). The unique index's leading column (orchestrator_id) also
12569
+ // serves the hot read path (team-block.ts, assign-tools.ts, detach-agent.ts
12570
+ // all look up by orchestratorId alone) — no separate index needed.
12571
+ unique("agent_assignments_orchestrator_sub_agent_unique").on(
12572
+ table.orchestratorId,
12573
+ table.subAgentId
12574
+ )
12575
+ ]
12480
12576
  );
12481
12577
  agentBudgets = pgTable(
12482
12578
  "agent_budgets",
@@ -12605,8 +12701,19 @@ var init_jobs = __esm({
12605
12701
  sql`${table.createdAt} DESC`
12606
12702
  ),
12607
12703
  index("idx_agent_jobs_parent_job_id").on(table.parentJobId),
12608
- index("idx_jobs_parent").on(table.parentJobId),
12704
+ // idx_jobs_parent (F-20, audit #2) was an exact duplicate of
12705
+ // idx_agent_jobs_parent_job_id above — same column, same order, both from
12706
+ // migration 0000. Dropped in migration 0054; kept this one (matches the
12707
+ // idx_agent_jobs_* naming convention used everywhere else on this table).
12609
12708
  index("idx_jobs_status").on(table.status, table.createdAt),
12709
+ // DB-3 (audit #2): findUndeliveredRootJobIds (cron/deliver-results.ts)
12710
+ // joins on this column filtered to IS NULL every tick; the cron recovery
12711
+ // scans (findPendingJobsToRecover, resetOrphanedJobs, failStalePendingJobs
12712
+ // in cron/reset-orphans.ts) also filter by status without an entity_id,
12713
+ // which idx_jobs_status (kept above) still serves — completed rows vastly
12714
+ // outnumber the still-open ones, so a partial index keyed on the open set
12715
+ // stays small regardless of table growth.
12716
+ index("idx_agent_jobs_completed_at_null").on(table.completedAt).where(sql`${table.completedAt} IS NULL`),
12610
12717
  check(
12611
12718
  "agent_jobs_status_check",
12612
12719
  sql`${table.status} IN ('pending','processing','completed','failed','awaiting_approval','awaiting_delegation','cancelled')`
@@ -12669,6 +12776,10 @@ var init_tasks = __esm({
12669
12776
  index("idx_agent_tasks_orchestrator_status").on(table.orchestratorId, table.status),
12670
12777
  index("idx_agent_tasks_status").on(table.status),
12671
12778
  index("idx_agent_tasks_assigned").on(table.assignedAgentId),
12779
+ // DB-3 (audit #2): findUndeliveredRootJobIds (apps/runner/src/cron/
12780
+ // deliver-results.ts) scans `WHERE root_job_id IS NOT NULL` on every cron
12781
+ // tick and had no index to support it — full scan on the hottest task table.
12782
+ index("idx_agent_tasks_root_job_id").on(table.rootJobId),
12672
12783
  check(
12673
12784
  "agent_tasks_status_check",
12674
12785
  sql`${table.status} IN ('todo','in_progress','done','cancelled','blocked')`
@@ -12853,7 +12964,16 @@ var init_approvals = __esm({
12853
12964
  check(
12854
12965
  "approval_rules_action_check",
12855
12966
  sql`${table.action} IN ('auto_approve','require_approval','block')`
12856
- )
12967
+ ),
12968
+ // DB-1 (audit #2): one canonical rule per (entity, agent-or-null, tool) —
12969
+ // without this, two concurrent setAgentApprovalRuleAction calls (or any
12970
+ // direct insert) can leave two rows for the same scope with DIVERGENT
12971
+ // actions, and matchApprovalRule's `.find()` picks whichever the SELECT
12972
+ // happens to return first — a non-deterministic approval gate. agentId IS
12973
+ // NULL marks an entity-wide rule (e.g. the run_command LAN master-switch),
12974
+ // so a plain UNIQUE would treat two NULL rows as distinct and let the
12975
+ // duplicate back in; NULLS NOT DISTINCT (PG15+) closes that gap.
12976
+ unique("approval_rules_entity_agent_tool_unique").on(table.entityId, table.agentId, table.toolName).nullsNotDistinct()
12857
12977
  ]
12858
12978
  );
12859
12979
  }
@@ -12886,6 +13006,15 @@ var init_memory2 = __esm({
12886
13006
  id: uuid("id").primaryKey().defaultRandom(),
12887
13007
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12888
13008
  agentId: uuid("agent_id").references(() => agents.id, { onDelete: "cascade" }),
13009
+ /**
13010
+ * Full-text source. A generated `search_tsv tsvector` column + GIN index
13011
+ * (raw SQL, migration 0051 — not expressible in the Drizzle schema builder,
13012
+ * same pattern as agent_jobs.search_tsv / migration 0050) makes this fact
13013
+ * rankable by relevance. Powers memory injection (selectMemoriesForInjection)
13014
+ * and the `query_memory` builtin (keywordSearchMemories) via ts_rank.
13015
+ * Config is `'english'` (stemming — "tokens" matches a "token" query),
13016
+ * unlike agent_jobs.search_tsv which stays `'simple'`.
13017
+ */
12889
13018
  fact: text("fact").notNull(),
12890
13019
  category: text("category").default("context"),
12891
13020
  importance: integer("importance").default(3),
@@ -12897,6 +13026,12 @@ var init_memory2 = __esm({
12897
13026
  validTo: timestamp("valid_to", { withTimezone: true }),
12898
13027
  factHash: text("fact_hash"),
12899
13028
  archived: boolean("archived").default(false),
13029
+ /**
13030
+ * User override lock (migration 0053). When true, the curator's
13031
+ * usage-driven re-scoring MUST refuse to touch importance — a user-pinned
13032
+ * value wins over the agent's guess and the curator's correction.
13033
+ */
13034
+ importanceLocked: boolean("importance_locked").notNull().default(false),
12900
13035
  lastAccessedAt: timestamp("last_accessed_at", { withTimezone: true }).defaultNow(),
12901
13036
  accessCount: integer("access_count").default(0),
12902
13037
  createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
@@ -12970,8 +13105,8 @@ var init_skills = __esm({
12970
13105
  {
12971
13106
  id: uuid("id").primaryKey().defaultRandom(),
12972
13107
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12973
- name: text("name").notNull().unique(),
12974
- slug: text("slug").notNull().unique(),
13108
+ name: text("name").notNull(),
13109
+ slug: text("slug").notNull(),
12975
13110
  content: text("content").notNull(),
12976
13111
  active: boolean("active").default(true),
12977
13112
  description: text("description"),
@@ -13011,7 +13146,20 @@ var init_skills = __esm({
13011
13146
  (table) => [
13012
13147
  index("idx_agent_skills_entity_id").on(table.entityId),
13013
13148
  index("idx_skills_active").on(table.active, table.slug),
13014
- index("idx_agent_skills_is_community").on(table.isCommunity)
13149
+ index("idx_agent_skills_is_community").on(table.isCommunity),
13150
+ // F-6 (audit #2): slug AND name were UNIQUE GLOBALLY — a 2nd workspace/
13151
+ // entity installing the same community skill (e.g. slug 'comfyui') or
13152
+ // creating a skill with the same display name crashed the insert, and in
13153
+ // multi-user mode let one entity squat/enumerate another's slugs or
13154
+ // names. Scoped to (entity_id, slug) / (entity_id, name): every real
13155
+ // insert always sets entityId (createSkillRepo takes it as a required
13156
+ // param; the system-skill seeder and community-skill installer both pass
13157
+ // it too — no production path leaves it NULL), but the column itself is
13158
+ // nullable, so NULLS NOT DISTINCT closes the same NULL-entity gap as
13159
+ // DB-1/agent_plugins (multiple (NULL, 'x') rows would otherwise all
13160
+ // satisfy a plain UNIQUE).
13161
+ unique("agent_skills_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct(),
13162
+ unique("agent_skills_entity_name_unique").on(table.entityId, table.name).nullsNotDistinct()
13015
13163
  ]
13016
13164
  );
13017
13165
  skillVersions = pgTable(
@@ -13068,7 +13216,14 @@ var init_skills = __esm({
13068
13216
  filesWritable: boolean("files_writable").notNull().default(false),
13069
13217
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
13070
13218
  },
13071
- () => []
13219
+ (table) => [
13220
+ // DB-2 (audit #2): the table had no index beyond the PK, so assignSkillRepo's
13221
+ // check-then-insert (packages/db/src/repos/skills.ts) could race and leave
13222
+ // the same skill assigned twice to one agent. The unique index's leading
13223
+ // column (agent_id) also serves the hot read path (execute.ts loads all
13224
+ // skills for an agent) — no separate index needed on top of it.
13225
+ unique("agent_skill_assignments_agent_skill_unique").on(table.agentId, table.skillId)
13226
+ ]
13072
13227
  );
13073
13228
  }
13074
13229
  });
@@ -13200,7 +13355,14 @@ var init_mcp = __esm({
13200
13355
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13201
13356
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13202
13357
  },
13203
- (table) => [index("idx_mcp_connections_entity").on(table.entityId)]
13358
+ (table) => [
13359
+ index("idx_mcp_connections_entity").on(table.entityId),
13360
+ // R5 (audit #2 follow-up): the pglite test DDL (packages/db/src/tests/
13361
+ // helpers.ts) already assumed a per-entity unique slug here — same class
13362
+ // of phantom-constraint bug as DB-2/F-18. entityId is NOT NULL on this
13363
+ // table, so a plain UNIQUE (no NULLS NOT DISTINCT needed) is correct.
13364
+ uniqueIndex("mcp_connections_entity_slug_unique").on(table.entityId, table.slug)
13365
+ ]
13204
13366
  );
13205
13367
  }
13206
13368
  });
@@ -13255,7 +13417,14 @@ var init_misc = __esm({
13255
13417
  check(
13256
13418
  "agent_plugins_hook_check",
13257
13419
  sql`${table.hook} IN ('pre_task','post_task','pre_tool','post_tool','on_memory_save')`
13258
- )
13420
+ ),
13421
+ // R5 (audit #2 follow-up): the pglite test DDL (packages/db/src/tests/
13422
+ // helpers.ts) already assumed a per-entity unique slug here — same class
13423
+ // of phantom-constraint bug as DB-2/F-18 (test mock ahead of the real
13424
+ // schema/migration). entityId is nullable (no `.notNull()` above), so
13425
+ // NULLS NOT DISTINCT closes the same NULL-entity gap as DB-1's
13426
+ // approval_rules constraint.
13427
+ unique("agent_plugins_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct()
13259
13428
  ]
13260
13429
  );
13261
13430
  rateLimits = pgTable("rate_limits", {
@@ -13427,16 +13596,65 @@ var init_chat_messages = __esm({
13427
13596
  });
13428
13597
 
13429
13598
  // ../../packages/db/src/schema/app-settings.ts
13430
- var appSettings;
13599
+ var appSettings, entitySettings;
13431
13600
  var init_app_settings = __esm({
13432
13601
  "../../packages/db/src/schema/app-settings.ts"() {
13433
13602
  "use strict";
13434
13603
  init_pg_core();
13604
+ init_entities();
13435
13605
  appSettings = pgTable("app_settings", {
13436
13606
  key: text("key").primaryKey(),
13437
13607
  value: text("value").default("").notNull(),
13438
13608
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
13439
13609
  });
13610
+ entitySettings = pgTable(
13611
+ "entity_settings",
13612
+ {
13613
+ entityId: uuid("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
13614
+ key: text("key").notNull(),
13615
+ value: text("value").default("").notNull(),
13616
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
13617
+ },
13618
+ (table) => [primaryKey({ columns: [table.entityId, table.key] })]
13619
+ );
13620
+ }
13621
+ });
13622
+
13623
+ // ../../packages/db/src/schema/telegram-allowed-chats.ts
13624
+ var telegramAllowedChats;
13625
+ var init_telegram_allowed_chats = __esm({
13626
+ "../../packages/db/src/schema/telegram-allowed-chats.ts"() {
13627
+ "use strict";
13628
+ init_pg_core();
13629
+ init_drizzle_orm();
13630
+ init_entities();
13631
+ init_agents();
13632
+ telegramAllowedChats = pgTable(
13633
+ "telegram_allowed_chats",
13634
+ {
13635
+ id: uuid("id").primaryKey().defaultRandom(),
13636
+ entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
13637
+ /** The bot the chat is talking to (the RECEIVING agent). */
13638
+ agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
13639
+ /** Telegram chat id (private chat, group, or supergroup) as a string. */
13640
+ chatId: text("chat_id").notNull(),
13641
+ /** 'owner' = the bot's controller; 'member' = an approved additional chat. */
13642
+ role: text("role").notNull().default("member"),
13643
+ /** 'active' = may create jobs; 'pending' = awaiting the owner's confirmation. */
13644
+ status: text("status").notNull().default("pending"),
13645
+ /** Display name of the requester, for the owner's confirmation card. */
13646
+ requesterName: text("requester_name"),
13647
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
13648
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow()
13649
+ },
13650
+ (table) => [
13651
+ unique("telegram_allowed_chats_agent_chat_unique").on(table.agentId, table.chatId),
13652
+ index("idx_telegram_allowed_chats_agent").on(table.agentId),
13653
+ index("idx_telegram_allowed_chats_entity").on(table.entityId),
13654
+ check("telegram_allowed_chats_role_check", sql`${table.role} IN ('owner','member')`),
13655
+ check("telegram_allowed_chats_status_check", sql`${table.status} IN ('active','pending')`)
13656
+ ]
13657
+ );
13440
13658
  }
13441
13659
  });
13442
13660
 
@@ -13488,12 +13706,14 @@ __export(schema_exports, {
13488
13706
  entities: () => entities,
13489
13707
  entityLlmKeys: () => entityLlmKeys,
13490
13708
  entityMembers: () => entityMembers,
13709
+ entitySettings: () => entitySettings,
13491
13710
  mcpConnections: () => mcpConnections,
13492
13711
  mcpServers: () => mcpServers,
13493
13712
  rateLimits: () => rateLimits,
13494
13713
  sessions: () => sessions,
13495
13714
  skillConnectors: () => skillConnectors,
13496
13715
  skillVersions: () => skillVersions,
13716
+ telegramAllowedChats: () => telegramAllowedChats,
13497
13717
  toolCalls: () => toolCalls,
13498
13718
  userProfiles: () => userProfiles,
13499
13719
  users: () => users,
@@ -13525,6 +13745,7 @@ var init_schema2 = __esm({
13525
13745
  init_agent_workspaces();
13526
13746
  init_chat_messages();
13527
13747
  init_app_settings();
13748
+ init_telegram_allowed_chats();
13528
13749
  }
13529
13750
  });
13530
13751
 
@@ -13533,7 +13754,29 @@ function createClient(connectionString, options = {}) {
13533
13754
  const pool = src_default(connectionString, {
13534
13755
  max: options.max ?? 10,
13535
13756
  // Fail fast on connection issues rather than hanging
13536
- connect_timeout: 10
13757
+ connect_timeout: 10,
13758
+ // R2 (audit2 concurrency review): bound the two ways a query on this pool
13759
+ // can hang indefinitely with no client-side timeout at all. Sent as
13760
+ // Postgres startup/session parameters (postgres.js `connection` option),
13761
+ // so every connection in the pool gets them — no per-query opt-in needed.
13762
+ // - lock_timeout: the vector identified for the cron watchdog (R1) — a
13763
+ // query blocked waiting on a row lock previously waited forever.
13764
+ // 30s is comfortably longer than any legitimate lock wait in this
13765
+ // app's write patterns (single-row updates/inserts, no long-held
13766
+ // locks by design).
13767
+ // - idle_in_transaction_session_timeout: a transaction left open with
13768
+ // no further statements (BEGIN without COMMIT/ROLLBACK) is always a
13769
+ // bug, never a legitimate long-running operation — 60s is generous.
13770
+ // Deliberately NOT setting statement_timeout here: a global cap would
13771
+ // also apply to legitimate long-running operations (a large backfill, a
13772
+ // slow migration-adjacent query) and risk cutting them off mid-flight.
13773
+ // migrate.ts uses its own separate `postgres()` connection (not this
13774
+ // factory) for running migrations, so these timeouts don't touch
13775
+ // migrations at all either way.
13776
+ connection: {
13777
+ lock_timeout: 3e4,
13778
+ idle_in_transaction_session_timeout: 6e4
13779
+ }
13537
13780
  });
13538
13781
  const db = drizzle(pool, { schema: schema_exports });
13539
13782
  return {
@@ -13560,12 +13803,23 @@ var init_transaction = __esm({
13560
13803
 
13561
13804
  // ../../packages/secrets/src/index.ts
13562
13805
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
13563
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13806
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13564
13807
  import { homedir as homedir2 } from "os";
13565
13808
  import { dirname as dirname3, join as join5 } from "path";
13809
+ function masterKeyPath() {
13810
+ return join5(homedir2(), ".nodalai", "secrets.key");
13811
+ }
13812
+ function masterKeyFileMissing(path = masterKeyPath()) {
13813
+ return !existsSync5(path);
13814
+ }
13815
+ function isEncrypted(value) {
13816
+ return value.startsWith(`enc:${VERSION}:`);
13817
+ }
13818
+ var VERSION;
13566
13819
  var init_src3 = __esm({
13567
13820
  "../../packages/secrets/src/index.ts"() {
13568
13821
  "use strict";
13822
+ VERSION = "v1";
13569
13823
  }
13570
13824
  });
13571
13825
 
@@ -13581,6 +13835,29 @@ var init_credentials2 = __esm({
13581
13835
  }
13582
13836
  });
13583
13837
 
13838
+ // ../../packages/db/src/queries/master-key-guard.ts
13839
+ async function assertMasterKeyRestorable(db, options = {}) {
13840
+ if (!masterKeyFileMissing(options.keyPath)) return;
13841
+ const [credRow] = await db.select({ id: credentials.id }).from(credentials).limit(1);
13842
+ if (credRow) throw missingKeyError("a stored OAuth credential");
13843
+ const llmKeyRows = await db.select({ apiKey: entityLlmKeys.apiKey }).from(entityLlmKeys);
13844
+ const hasEncryptedLlmKey = llmKeyRows.some((r) => r.apiKey !== "" && isEncrypted(r.apiKey));
13845
+ if (hasEncryptedLlmKey) throw missingKeyError("a stored LLM API key");
13846
+ }
13847
+ function missingKeyError(what) {
13848
+ return new Error(
13849
+ `~/.nodalai/secrets.key is missing, but the database already has ${what} encrypted with it. Generating a NEW key here would make every existing credential / LLM key permanently undecryptable, with no further warning. Restore the original secrets.key from a backup before running \`nodal-agents up\` again. If you intend to start fresh, delete the affected rows (or wipe the database) first, then retry.`
13850
+ );
13851
+ }
13852
+ var init_master_key_guard = __esm({
13853
+ "../../packages/db/src/queries/master-key-guard.ts"() {
13854
+ "use strict";
13855
+ init_credentials();
13856
+ init_llm_keys();
13857
+ init_src3();
13858
+ }
13859
+ });
13860
+
13584
13861
  // ../../packages/db/src/repos/agents.ts
13585
13862
  var init_agents2 = __esm({
13586
13863
  "../../packages/db/src/repos/agents.ts"() {
@@ -13624,6 +13901,7 @@ var init_src4 = __esm({
13624
13901
  init_transaction();
13625
13902
  init_schema2();
13626
13903
  init_credentials2();
13904
+ init_master_key_guard();
13627
13905
  init_agents2();
13628
13906
  init_skills2();
13629
13907
  init_retention();
@@ -13683,11 +13961,20 @@ var init_local_auth = __esm({
13683
13961
  }
13684
13962
  });
13685
13963
 
13964
+ // ../../packages/auth/src/lib/constant-time.ts
13965
+ import { createHash, timingSafeEqual } from "crypto";
13966
+ var init_constant_time = __esm({
13967
+ "../../packages/auth/src/lib/constant-time.ts"() {
13968
+ "use strict";
13969
+ }
13970
+ });
13971
+
13686
13972
  // ../../packages/auth/src/providers/bearer-token.ts
13687
13973
  var init_bearer_token = __esm({
13688
13974
  "../../packages/auth/src/providers/bearer-token.ts"() {
13689
13975
  "use strict";
13690
13976
  init_local_trust();
13977
+ init_constant_time();
13691
13978
  }
13692
13979
  });
13693
13980
 
@@ -13726,6 +14013,7 @@ var init_seed = __esm({
13726
14013
  });
13727
14014
 
13728
14015
  // src/lib/env.ts
14016
+ import { randomBytes as randomBytes3 } from "crypto";
13729
14017
  function buildEnvForRunner(config, databaseUrl) {
13730
14018
  const bind = config.bind === "loopback" ? "127.0.0.1" : "0.0.0.0";
13731
14019
  const env = {
@@ -13749,7 +14037,19 @@ function buildEnvForRunner(config, databaseUrl) {
13749
14037
  return env;
13750
14038
  }
13751
14039
  function resolveAuthMode(config) {
13752
- if (config.auth?.mode) return config.auth.mode;
14040
+ if (config.bearerToken) {
14041
+ throw new Error(
14042
+ 'Invalid config: "bearerToken" is set in config.json, which the docs describe as activating AUTH_MODE=bearer-token \u2014 but that mode is not fully wired end-to-end yet (the web dashboard\'s auth provider still falls back to no-auth for bearer-token mode; see apps/web/src/lib/server.ts getAuthProvider). Booting anyway would silently run in local-trust or local-auth instead \u2014 NOT the bearer-token protection you configured. Refused. Remove "bearerToken" from config.json until this is wired, or use `bind`/`auth.mode` for local-trust/local-auth.'
14043
+ );
14044
+ }
14045
+ if (config.auth?.mode) {
14046
+ if (config.auth.mode === "local-trust" && config.bind === "lan") {
14047
+ throw new Error(
14048
+ 'Invalid config: auth.mode="local-trust" with bind="lan" would expose every runner route unauthenticated on the network (including run_command auto-approve). Refused. Use auth.mode="local-auth" for a LAN bind, or bind="loopback" for local-trust.'
14049
+ );
14050
+ }
14051
+ return config.auth.mode;
14052
+ }
13753
14053
  return config.bind === "lan" ? "local-auth" : "local-trust";
13754
14054
  }
13755
14055
  function buildEnvForWeb(config, databaseUrl) {
@@ -13773,8 +14073,16 @@ function buildEnvForWeb(config, databaseUrl) {
13773
14073
  // The web itself doesn't bind on this — Next.js listens on 0.0.0.0 anyway.
13774
14074
  BIND: bind,
13775
14075
  NODE_ENV: "production",
13776
- // AUTH_SECRET is required by better-auth in local-auth mode; harmless in local-trust.
13777
- AUTH_SECRET: config.workerSecret,
14076
+ // AUTH_SECRET is required by better-auth in local-auth mode; harmless in
14077
+ // local-trust. M-4: deliberately SEPARATE from workerSecret — the two
14078
+ // used to share a value, so a leak of workerSecret (the runner's auth
14079
+ // frontier) would also let an attacker forge a signed better-auth session
14080
+ // cookie. readConfig() auto-mints authSecret, so this is set in every
14081
+ // normal boot path; the random fallback below only guards a caller that
14082
+ // somehow bypasses readConfig (ephemeral — would invalidate sessions on
14083
+ // every restart, which is a symptom to fix at the config layer, not a
14084
+ // reason to crash boot or fall back to workerSecret).
14085
+ AUTH_SECRET: config.authSecret ?? randomBytes3(32).toString("base64"),
13778
14086
  // WORKER_SECRET — same value as the runner — so sendTaskAction can sign
13779
14087
  // the POST /api/worker call. Without this the runner returns 403 and the
13780
14088
  // job stays pending forever (cron only scans task-board, not API jobs).
@@ -14057,7 +14365,7 @@ __export(up_exports, {
14057
14365
  import chalk from "chalk";
14058
14366
  import ora from "ora";
14059
14367
  import open from "open";
14060
- import { randomBytes as randomBytes3 } from "crypto";
14368
+ import { randomBytes as randomBytes4 } from "crypto";
14061
14369
  async function killSilent(child) {
14062
14370
  await killProcessTree(child);
14063
14371
  }
@@ -14082,8 +14390,12 @@ async function runUp(opts = {}) {
14082
14390
  const defaultConfig = {
14083
14391
  // No `llm` section: the browser onboarding sets it via createLlmKeyAction.
14084
14392
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14085
- workerSecret: randomBytes3(32).toString("hex"),
14086
- serverActionsKey: randomBytes3(32).toString("base64"),
14393
+ workerSecret: randomBytes4(32).toString("hex"),
14394
+ serverActionsKey: randomBytes4(32).toString("base64"),
14395
+ // M-4: authSecret is minted here (distinct from workerSecret) rather
14396
+ // than left for readConfig()'s auto-mint, because this in-memory
14397
+ // `config` is used directly below without a re-read.
14398
+ authSecret: randomBytes4(32).toString("base64"),
14087
14399
  // loopback derives auth: local-trust (no login/signup). Leave `auth`
14088
14400
  // unset so resolveAuthMode falls back to the loopback → local-trust map.
14089
14401
  bind: "loopback"
@@ -14091,6 +14403,7 @@ async function runUp(opts = {}) {
14091
14403
  writeConfig(defaultConfig);
14092
14404
  config = defaultConfig;
14093
14405
  }
14406
+ resolveAuthMode(config);
14094
14407
  const orphans = [];
14095
14408
  for (const [name, port] of [
14096
14409
  ["web", config.ports.web],
@@ -14196,6 +14509,17 @@ async function runUp(opts = {}) {
14196
14509
  await pg.stop();
14197
14510
  throw err;
14198
14511
  }
14512
+ {
14513
+ const { db, close } = createClient(databaseUrl, { max: 1 });
14514
+ try {
14515
+ await assertMasterKeyRestorable(db);
14516
+ } catch (err) {
14517
+ await close();
14518
+ await pg.stop();
14519
+ throw err;
14520
+ }
14521
+ await close();
14522
+ }
14199
14523
  const authMode = resolveAuthMode(config);
14200
14524
  if (authMode === "local-trust") {
14201
14525
  const seedSpinner = ora("Seeding default user and agent\u2026").start();
@@ -14245,13 +14569,13 @@ async function runUp(opts = {}) {
14245
14569
  }
14246
14570
  void (async () => {
14247
14571
  try {
14248
- const { getInstalledVersion: getInstalledVersion2, getLatestVersion: getLatestVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
14572
+ const { getInstalledVersion: getInstalledVersion2, getLatestVersion: getLatestVersion2, isNewerVersion: isNewerVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
14249
14573
  const installed = getInstalledVersion2();
14250
14574
  const latest = await Promise.race([
14251
14575
  getLatestVersion2(),
14252
14576
  new Promise((resolve2) => setTimeout(() => resolve2(null), 3e3))
14253
14577
  ]);
14254
- if (latest !== null && latest !== installed) {
14578
+ if (latest !== null && isNewerVersion2(latest, installed)) {
14255
14579
  console.log(chalk.cyan(` \u2139 v${latest} available \u2014 run \`nodal-agents update\``));
14256
14580
  }
14257
14581
  } catch {
@@ -14407,9 +14731,9 @@ __export(init_exports, {
14407
14731
  import chalk2 from "chalk";
14408
14732
  import prompts from "prompts";
14409
14733
  import ora2 from "ora";
14410
- import { randomBytes as randomBytes4 } from "crypto";
14734
+ import { randomBytes as randomBytes5 } from "crypto";
14411
14735
  function generateSecret(bytes = 32) {
14412
- return randomBytes4(bytes).toString("hex");
14736
+ return randomBytes5(bytes).toString("hex");
14413
14737
  }
14414
14738
  async function runInit(options = {}) {
14415
14739
  const existing = readConfig();
@@ -14430,7 +14754,7 @@ async function runInit(options = {}) {
14430
14754
  const config2 = {
14431
14755
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14432
14756
  workerSecret: generateSecret(32),
14433
- serverActionsKey: randomBytes4(32).toString("base64"),
14757
+ serverActionsKey: randomBytes5(32).toString("base64"),
14434
14758
  bind: "lan",
14435
14759
  auth: { mode: "local-auth" }
14436
14760
  };