nodal-agents 0.7.0 → 0.7.5

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 (174) hide show
  1. package/cli.js +453 -37
  2. package/migrations/0054_audit2_db_integrity.sql +52 -0
  3. package/migrations/0055_entity_settings.sql +22 -0
  4. package/migrations/0056_agents_skills_entity_scoped_unique.sql +23 -0
  5. package/migrations/0057_telegram_allowed_chats.sql +34 -0
  6. package/migrations/0058_entity_llm_keys_context_window.sql +6 -0
  7. package/migrations/0059_agent_jobs_conversation_id.sql +10 -0
  8. package/migrations/meta/_journal.json +42 -0
  9. package/package.json +1 -1
  10. package/runner.js +47940 -35285
  11. package/web/.next/BUILD_ID +1 -1
  12. package/web/.next/app-path-routes-manifest.json +7 -7
  13. package/web/.next/build-manifest.json +2 -2
  14. package/web/.next/prerender-manifest.json +3 -3
  15. package/web/.next/routes-manifest.json +32 -1
  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 +1 -1
  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 +1 -1
  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 +1 -1
  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 +2 -2
  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 +122 -94
  105. package/web/.next/server/app/onboarding/page.js.nft.json +1 -1
  106. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  107. package/web/.next/server/app-paths-manifest.json +7 -7
  108. package/web/.next/server/chunks/2043.js +1 -0
  109. package/web/.next/server/chunks/2046.js +1 -0
  110. package/web/.next/server/chunks/2713.js +56 -28
  111. package/web/.next/server/chunks/4479.js +1 -0
  112. package/web/.next/server/chunks/4574.js +1 -1
  113. package/web/.next/server/chunks/{4488.js → 5744.js} +5 -4
  114. package/web/.next/server/chunks/{8464.js → 7388.js} +1 -2
  115. package/web/.next/server/chunks/7560.js +1 -0
  116. package/web/.next/server/chunks/{3154.js → 960.js} +10 -10
  117. package/web/.next/server/middleware-build-manifest.js +1 -1
  118. package/web/.next/server/pages/404.html +1 -1
  119. package/web/.next/server/pages/500.html +1 -1
  120. package/web/.next/server/server-reference-manifest.js +1 -1
  121. package/web/.next/server/server-reference-manifest.json +1 -1
  122. package/web/.next/static/chunks/3114-d846062bc8434b58.js +1 -0
  123. package/web/.next/static/chunks/4893-730f4c62f76c6c48.js +1 -0
  124. package/web/.next/static/chunks/8097-bad68cd78aa59a10.js +1 -0
  125. package/web/.next/static/chunks/{9836-6830cb9d49ad261f.js → 9836-a69e19544b669a71.js} +1 -1
  126. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-18973089c62eb12e.js +6 -0
  127. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-7913483b3bfd7dda.js +1 -0
  128. package/web/.next/static/chunks/app/(dashboard)/agents/page-eb7b72d4d7bfde01.js +1 -0
  129. package/web/.next/static/chunks/app/(dashboard)/approvals/{page-9a01cb723a6ccff0.js → page-dc8dc54ed07bc81a.js} +1 -1
  130. package/web/.next/static/chunks/app/(dashboard)/automations/page-1034613ecd525509.js +1 -0
  131. package/web/.next/static/chunks/app/(dashboard)/chat/page-4d0e2fd060f2aafb.js +1 -0
  132. package/web/.next/static/chunks/app/(dashboard)/connectors/{page-21d6c042800c8cd3.js → page-8bad5bc43f197471.js} +1 -1
  133. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-116a001adade3a1e.js +1 -0
  134. package/web/.next/static/chunks/app/(dashboard)/jobs/page-0fc661a4e7588bdb.js +1 -0
  135. package/web/.next/static/chunks/app/(dashboard)/layout-9068b164d3ae4853.js +1 -0
  136. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-01f1b68b334bf442.js +1 -0
  137. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-99fd3e7b631131e8.js +1 -0
  138. package/web/.next/static/chunks/app/(dashboard)/logs/{page-0bca733315e8a208.js → page-94babd54bdd0d1af.js} +1 -1
  139. package/web/.next/static/chunks/app/(dashboard)/mcp/page-2d190bdd49b7b4b4.js +1 -0
  140. package/web/.next/static/chunks/app/(dashboard)/memories/page-ac872edd26efe323.js +1 -0
  141. package/web/.next/static/chunks/app/(dashboard)/page-8cd3f1d4d072ac25.js +1 -0
  142. package/web/.next/static/chunks/app/(dashboard)/settings/page-aed8fc039205ccd7.js +1 -0
  143. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-a2eb786f9d897813.js +1 -0
  144. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-7f87e2c66d1ceea4.js +1 -0
  145. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-54734a6e0675887a.js +1 -0
  146. package/web/.next/static/chunks/app/(dashboard)/skills/{page-54e939d40c8b5b6d.js → page-cae26ae45b9b11e5.js} +1 -1
  147. package/web/.next/static/chunks/app/onboarding/{page-a4116e2f430eaff8.js → page-e63eee720a95baac.js} +2 -2
  148. package/web/.next/static/css/{eff119dd24f92093.css → 289a2e89c0d093fd.css} +1 -1
  149. package/web/.next/server/chunks/3223.js +0 -1
  150. package/web/.next/server/chunks/4626.js +0 -1
  151. package/web/.next/server/chunks/8971.js +0 -1
  152. package/web/.next/server/chunks/9043.js +0 -1
  153. package/web/.next/static/chunks/4964-08171c654e41b0a4.js +0 -1
  154. package/web/.next/static/chunks/609-a0c06f3592427d58.js +0 -1
  155. package/web/.next/static/chunks/7704-7ba1c3d45662ffc8.js +0 -1
  156. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-fa5717f0d0450659.js +0 -6
  157. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-73012222871598aa.js +0 -1
  158. package/web/.next/static/chunks/app/(dashboard)/agents/page-7e9616b28c098e9e.js +0 -1
  159. package/web/.next/static/chunks/app/(dashboard)/automations/page-85c985cd687406ed.js +0 -1
  160. package/web/.next/static/chunks/app/(dashboard)/chat/page-a6cc7a58986c0340.js +0 -1
  161. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-f6b993a88c70a2fe.js +0 -1
  162. package/web/.next/static/chunks/app/(dashboard)/jobs/page-d0d6b9db1b68451f.js +0 -1
  163. package/web/.next/static/chunks/app/(dashboard)/layout-d2ac2646967496bb.js +0 -1
  164. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-ec6312f6dfd7ee23.js +0 -1
  165. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-1fb26463eddf509c.js +0 -1
  166. package/web/.next/static/chunks/app/(dashboard)/mcp/page-ff56ecf2f65f5466.js +0 -1
  167. package/web/.next/static/chunks/app/(dashboard)/memories/page-fe4d9438c93750d6.js +0 -1
  168. package/web/.next/static/chunks/app/(dashboard)/page-9402fe2931216f4b.js +0 -1
  169. package/web/.next/static/chunks/app/(dashboard)/settings/page-995662f10ebff365.js +0 -1
  170. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-785b6ce3e4c5890b.js +0 -1
  171. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-2106201c4df8b5a4.js +0 -1
  172. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-0135bcb86ab60dc2.js +0 -1
  173. /package/web/.next/static/{KFac7DQ7gdp9XPQ2bnb-2 → XF1rXMlGkoyAu_GI5theh}/_buildManifest.js +0 -0
  174. /package/web/.next/static/{KFac7DQ7gdp9XPQ2bnb-2 → XF1rXMlGkoyAu_GI5theh}/_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,8 +74,22 @@ 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";
78
+ import { execFileSync } from "child_process";
63
79
  import { randomBytes } from "crypto";
80
+ function restrictFileToOwner(path) {
81
+ try {
82
+ chmodSync(path, 384);
83
+ } catch {
84
+ }
85
+ if (process.platform !== "win32") return;
86
+ const user = process.env["USERNAME"] || process.env["USER"];
87
+ if (!user) return;
88
+ try {
89
+ execFileSync("icacls", [path, "/inheritance:r", "/grant:r", `${user}:F`], { stdio: "ignore" });
90
+ } catch {
91
+ }
92
+ }
64
93
  function ensureConfigDir() {
65
94
  for (const dir of [CONFIG_DIR, PID_DIR, LOG_DIR, PG_DATA_DIR]) {
66
95
  if (!existsSync2(dir)) {
@@ -81,11 +110,22 @@ function readConfig(configFile = CONFIG_FILE) {
81
110
  parsed = { ...parsed, serverActionsKey: randomBytes(32).toString("base64") };
82
111
  writeConfig(parsed, configFile);
83
112
  }
113
+ if (!parsed.authSecret) {
114
+ parsed = { ...parsed, authSecret: randomBytes(32).toString("base64") };
115
+ writeConfig(parsed, configFile);
116
+ }
117
+ if (configFile === CONFIG_FILE) {
118
+ try {
119
+ chmodSync(configFile, 384);
120
+ } catch {
121
+ }
122
+ }
84
123
  return parsed;
85
124
  }
86
125
  function writeConfig(config, configFile = CONFIG_FILE) {
87
126
  if (configFile === CONFIG_FILE) ensureConfigDir();
88
- writeFileSync(configFile, JSON.stringify(config, null, 2), "utf-8");
127
+ writeFileSync(configFile, JSON.stringify(config, null, 2), { encoding: "utf-8", mode: 384 });
128
+ restrictFileToOwner(configFile);
89
129
  }
90
130
  var ConfigSchema, CONFIG_DIR, CONFIG_FILE, PID_DIR, LOG_DIR, PG_DATA_DIR;
91
131
  var init_config = __esm({
@@ -133,6 +173,18 @@ var init_config = __esm({
133
173
  * files; `readConfig` auto-mints + persists when absent.
134
174
  */
135
175
  serverActionsKey: z.string().length(44).optional(),
176
+ /**
177
+ * Base64-encoded 32-byte AES key used as better-auth's AUTH_SECRET (session
178
+ * cookie signing) for the web process. Deliberately SEPARATE from
179
+ * `workerSecret` (M-4): `workerSecret` is the runner's auth frontier — a
180
+ * leak of it must not also let an attacker forge a signed session cookie.
181
+ *
182
+ * Optional in the schema for back-compat with pre-existing config.json
183
+ * files; `readConfig` auto-mints + persists when absent. On an existing
184
+ * install this invalidates any active better-auth sessions ONCE (users
185
+ * re-login) — an acceptable one-time cost for closing the amplification.
186
+ */
187
+ authSecret: z.string().length(44).optional(),
136
188
  bind: z.enum(["loopback", "lan"]).default("loopback"),
137
189
  bearerToken: z.string().optional(),
138
190
  /**
@@ -6037,6 +6089,12 @@ var init_policies = __esm({
6037
6089
  });
6038
6090
 
6039
6091
  // ../../node_modules/.pnpm/drizzle-orm@0.45.2_@electri_96bd1661c4535b1384d37581a38c7c74/node_modules/drizzle-orm/pg-core/primary-keys.js
6092
+ function primaryKey(...config) {
6093
+ if (config[0].columns) {
6094
+ return new PrimaryKeyBuilder(config[0].columns, config[0].name);
6095
+ }
6096
+ return new PrimaryKeyBuilder(config);
6097
+ }
6040
6098
  var PrimaryKeyBuilder, PrimaryKey;
6041
6099
  var init_primary_keys = __esm({
6042
6100
  "../../node_modules/.pnpm/drizzle-orm@0.45.2_@electri_96bd1661c4535b1384d37581a38c7c74/node_modules/drizzle-orm/pg-core/primary-keys.js"() {
@@ -6400,19 +6458,19 @@ function extractTablesRelationalConfig(schema, configHelpers) {
6400
6458
  const relations2 = value.config(
6401
6459
  configHelpers(value.table)
6402
6460
  );
6403
- let primaryKey;
6461
+ let primaryKey2;
6404
6462
  for (const [relationName, relation] of Object.entries(relations2)) {
6405
6463
  if (tableName) {
6406
6464
  const tableConfig = tablesConfig[tableName];
6407
6465
  tableConfig.relations[relationName] = relation;
6408
- if (primaryKey) {
6409
- tableConfig.primaryKey.push(...primaryKey);
6466
+ if (primaryKey2) {
6467
+ tableConfig.primaryKey.push(...primaryKey2);
6410
6468
  }
6411
6469
  } else {
6412
6470
  if (!(dbName in relationsBuffer)) {
6413
6471
  relationsBuffer[dbName] = {
6414
6472
  relations: {},
6415
- primaryKey
6473
+ primaryKey: primaryKey2
6416
6474
  };
6417
6475
  }
6418
6476
  relationsBuffer[dbName].relations[relationName] = relation;
@@ -11231,6 +11289,10 @@ var init_job = __esm({
11231
11289
  task: z7.string().min(1),
11232
11290
  original_task: z7.string().nullable(),
11233
11291
  chat_id: z7.string().nullable(),
11292
+ // Groups jobs from the same conversational thread (migration 0059) — see
11293
+ // apps/runner/src/job/conversation-id.ts for the stamping rule. Null for
11294
+ // non-conversational jobs (cron/schedule/webhook with no parent).
11295
+ conversation_id: z7.string().guid().nullable(),
11234
11296
  system_prompt: z7.string().nullable(),
11235
11297
  messages: z7.array(z7.record(z7.string(), z7.unknown())),
11236
11298
  tools_used: z7.array(z7.string()),
@@ -11271,6 +11333,7 @@ var init_job = __esm({
11271
11333
  status: JobStatusSchema.default("pending"),
11272
11334
  original_task: z7.string().nullable().optional(),
11273
11335
  chat_id: z7.string().nullable().optional(),
11336
+ conversation_id: z7.string().guid().nullable().optional(),
11274
11337
  system_prompt: z7.string().nullable().optional(),
11275
11338
  request_id: z7.string().nullable().optional(),
11276
11339
  parent_job_id: z7.string().guid().nullable().optional()
@@ -11278,6 +11341,38 @@ var init_job = __esm({
11278
11341
  }
11279
11342
  });
11280
11343
 
11344
+ // ../../packages/shared/src/entities/job-classification.ts
11345
+ var CHAT_DELIVERY_TOOLS, CHAT_CONTEXT_READ_TOOLS, CHAT_ALLOWED_TOOLS;
11346
+ var init_job_classification = __esm({
11347
+ "../../packages/shared/src/entities/job-classification.ts"() {
11348
+ "use strict";
11349
+ CHAT_DELIVERY_TOOLS = [
11350
+ "telegram_send_message",
11351
+ "send_image",
11352
+ "send_file",
11353
+ "send_video",
11354
+ "send_audio",
11355
+ "send_voice",
11356
+ "return_result",
11357
+ "dashboard_publish"
11358
+ ];
11359
+ CHAT_CONTEXT_READ_TOOLS = [
11360
+ "query_memory",
11361
+ "search_history",
11362
+ "skill_view",
11363
+ "list_models",
11364
+ "list_schedules",
11365
+ "file_read",
11366
+ "file_list",
11367
+ "file_search"
11368
+ ];
11369
+ CHAT_ALLOWED_TOOLS = /* @__PURE__ */ new Set([
11370
+ ...CHAT_DELIVERY_TOOLS,
11371
+ ...CHAT_CONTEXT_READ_TOOLS
11372
+ ]);
11373
+ }
11374
+ });
11375
+
11281
11376
  // ../../packages/shared/src/entities/task.ts
11282
11377
  import { z as z8 } from "zod";
11283
11378
  var TaskDescriptionSchema, TaskTitleSchema, AgentTaskSchema, AgentTaskInsertSchema;
@@ -11383,7 +11478,9 @@ var init_connector = __esm({
11383
11478
  name: z10.string().min(1).max(120),
11384
11479
  slug: z10.string().min(1).max(80).regex(/^[a-z0-9-]+$/),
11385
11480
  base_url: z10.string().nullable(),
11386
- // api_key stored encrypted in DB (pgp_sym_encrypt); raw value never returned
11481
+ // audit#2026-07-07 SEC-5: this comment used to claim pgp_sym_encrypt (pgcrypto),
11482
+ // which is not used anywhere. Real encryption is application-level AES-256-GCM
11483
+ // via @nodal-agents/secrets before the value ever reaches the DB; raw value never returned.
11387
11484
  api_key: z10.string().nullable(),
11388
11485
  active: z10.boolean(),
11389
11486
  auth_type: ConnectorAuthTypeSchema,
@@ -11883,6 +11980,13 @@ var init_root_agent = __esm({
11883
11980
  }
11884
11981
  });
11885
11982
 
11983
+ // ../../packages/shared/src/system-prompt-cache.ts
11984
+ var init_system_prompt_cache = __esm({
11985
+ "../../packages/shared/src/system-prompt-cache.ts"() {
11986
+ "use strict";
11987
+ }
11988
+ });
11989
+
11886
11990
  // ../../packages/shared/src/connector-catalog.ts
11887
11991
  var init_connector_catalog = __esm({
11888
11992
  "../../packages/shared/src/connector-catalog.ts"() {
@@ -11914,7 +12018,10 @@ var init_model_catalog = __esm({
11914
12018
  // deepseek-chat is the production alias for DeepSeek V3. Standard tool
11915
12019
  // calling with forced tool_choice supported.
11916
12020
  capabilities: { tools: true, forcedToolChoice: true },
11917
- contextWindow: 128e3
12021
+ contextWindow: 128e3,
12022
+ // Standard (cache-miss) rate per DeepSeek's public pricing page —
12023
+ // verify against api-docs.deepseek.com/quick_start/pricing if this drifts.
12024
+ pricing: { inputPerMillionUsd: 0.27, outputPerMillionUsd: 1.1 }
11918
12025
  },
11919
12026
  {
11920
12027
  modelId: "deepseek-reasoner",
@@ -11924,7 +12031,8 @@ var init_model_catalog = __esm({
11924
12031
  // reasoning:true). Also requires reasoning_content to be echoed back on
11925
12032
  // assistant messages with tool_calls (the deepseek fetch shim handles this).
11926
12033
  capabilities: { tools: true, forcedToolChoice: true, reasoning: true },
11927
- contextWindow: 128e3
12034
+ contextWindow: 128e3,
12035
+ pricing: { inputPerMillionUsd: 0.55, outputPerMillionUsd: 2.19 }
11928
12036
  }
11929
12037
  ],
11930
12038
  // ─── Native MiniMax (api.minimax.io/anthropic) ───────────────────────────────
@@ -11937,6 +12045,11 @@ var init_model_catalog = __esm({
11937
12045
  // forced tool_choice with a 400/404 (observed on M3 via OpenRouter); the
11938
12046
  // runtime completion floor covers it. Context windows are docs-sourced where
11939
12047
  // available, else a conservative 200K until a live probe confirms.
12048
+ // No `pricing` yet (unlike deepseek above) — no confidently-current MiniMax
12049
+ // rate card verified at write time. Left as documented debt (Fix #21): the
12050
+ // derived-cost fallback returns 0 for these until pricing is added, same as
12051
+ // every other unpriced entry in this catalog; Guard 1a (token budget) is the
12052
+ // backstop meanwhile.
11940
12053
  minimax: [
11941
12054
  {
11942
12055
  modelId: "MiniMax-M3",
@@ -12197,6 +12310,28 @@ var init_timezone = __esm({
12197
12310
  }
12198
12311
  });
12199
12312
 
12313
+ // ../../packages/shared/src/redact-secrets.ts
12314
+ var init_redact_secrets = __esm({
12315
+ "../../packages/shared/src/redact-secrets.ts"() {
12316
+ "use strict";
12317
+ }
12318
+ });
12319
+
12320
+ // ../../packages/shared/src/catastrophic-command.ts
12321
+ var init_catastrophic_command = __esm({
12322
+ "../../packages/shared/src/catastrophic-command.ts"() {
12323
+ "use strict";
12324
+ }
12325
+ });
12326
+
12327
+ // ../../packages/shared/src/approval-impact.ts
12328
+ var init_approval_impact = __esm({
12329
+ "../../packages/shared/src/approval-impact.ts"() {
12330
+ "use strict";
12331
+ init_catastrophic_command();
12332
+ }
12333
+ });
12334
+
12200
12335
  // ../../packages/shared/src/index.ts
12201
12336
  var init_src2 = __esm({
12202
12337
  "../../packages/shared/src/index.ts"() {
@@ -12207,6 +12342,7 @@ var init_src2 = __esm({
12207
12342
  init_user();
12208
12343
  init_agent();
12209
12344
  init_job();
12345
+ init_job_classification();
12210
12346
  init_task();
12211
12347
  init_credential();
12212
12348
  init_connector();
@@ -12220,11 +12356,15 @@ var init_src2 = __esm({
12220
12356
  init_operation();
12221
12357
  init_providers();
12222
12358
  init_root_agent();
12359
+ init_system_prompt_cache();
12223
12360
  init_connector_catalog();
12224
12361
  init_community_skill_catalog();
12225
12362
  init_model_catalog();
12226
12363
  init_mcp_catalog();
12227
12364
  init_timezone();
12365
+ init_redact_secrets();
12366
+ init_approval_impact();
12367
+ init_catastrophic_command();
12228
12368
  }
12229
12369
  });
12230
12370
 
@@ -12370,7 +12510,13 @@ var init_entities = __esm({
12370
12510
  check(
12371
12511
  "entity_members_role_check",
12372
12512
  sql`${table.role} IN ('owner', 'admin', 'member', 'viewer')`
12373
- )
12513
+ ),
12514
+ // R5 (audit #2 follow-up, found while sweeping helpers.ts for the SAME
12515
+ // class of phantom constraint): the pglite test DDL already assumed a
12516
+ // user can only be a member of an entity once — this table had no such
12517
+ // constraint in the real schema/migrations. Both columns are NOT NULL, so
12518
+ // a plain UNIQUE is correct (no NULLS NOT DISTINCT needed).
12519
+ uniqueIndex("entity_members_entity_user_unique").on(table.entityId, table.userId)
12374
12520
  ]
12375
12521
  );
12376
12522
  }
@@ -12398,6 +12544,11 @@ var init_llm_keys = __esm({
12398
12544
  apiKeyLast4: text("api_key_last4").notNull().default(""),
12399
12545
  baseUrl: text("base_url"),
12400
12546
  nickname: text("nickname"),
12547
+ // Real context window (tokens) for a custom/local model the catalog can't
12548
+ // know (É-3). Auto-detected from the endpoint at save time or set by the
12549
+ // user; NULL ⇒ fall back to the catalogued value or DEFAULT_CONTEXT_WINDOW.
12550
+ // Used window-relative to trigger compaction before the model overflows.
12551
+ contextWindow: integer("context_window"),
12401
12552
  isActive: boolean("is_active").notNull().default(true),
12402
12553
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
12403
12554
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
@@ -12422,7 +12573,7 @@ var init_agents = __esm({
12422
12573
  id: uuid("id").primaryKey().defaultRandom(),
12423
12574
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12424
12575
  name: text("name").notNull(),
12425
- slug: text("slug").notNull().unique(),
12576
+ slug: text("slug").notNull(),
12426
12577
  personality: text("personality").notNull(),
12427
12578
  model: text("model").default("claude-sonnet-4-6-20260217"),
12428
12579
  llmKeyId: uuid("llm_key_id").references(() => entityLlmKeys.id, { onDelete: "set null" }),
@@ -12472,7 +12623,17 @@ var init_agents = __esm({
12472
12623
  "agents_orchestrator_mode_check",
12473
12624
  sql`${table.orchestratorMode} IN ('router', 'planner') OR ${table.orchestratorMode} IS NULL`
12474
12625
  ),
12475
- check("agents_max_tokens_per_job_check", sql`${table.maxTokensPerJob} >= 0`)
12626
+ check("agents_max_tokens_per_job_check", sql`${table.maxTokensPerJob} >= 0`),
12627
+ // F-6 (audit #2): slug was UNIQUE GLOBALLY, so a 2nd workspace/entity
12628
+ // installing the same community skill's companion agent (or any agent
12629
+ // sharing a slug with another entity's agent) would crash the insert —
12630
+ // and, in multi-user mode, let one entity enumerate/squat another's
12631
+ // slugs. Scoped to (entity_id, slug): every real insert always sets
12632
+ // entityId (createAgentRepo takes it as a required param — no production
12633
+ // path leaves it NULL), but the column itself is nullable, so
12634
+ // NULLS NOT DISTINCT closes the same NULL-entity gap as DB-1/agent_plugins
12635
+ // (multiple (NULL, 'x') rows would otherwise all satisfy a plain UNIQUE).
12636
+ unique("agents_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct()
12476
12637
  ]
12477
12638
  );
12478
12639
  agentAssignments = pgTable(
@@ -12485,7 +12646,18 @@ var init_agents = __esm({
12485
12646
  instructions: text("instructions"),
12486
12647
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
12487
12648
  },
12488
- () => []
12649
+ (table) => [
12650
+ // F-18 (audit #2): no index beyond the PK meant updateAgentAction's
12651
+ // delete-then-insert (apps/web/src/lib/actions.ts) could race and leave the
12652
+ // same sub-agent attached twice under one orchestrator (duplicated in the
12653
+ // team-block). The unique index's leading column (orchestrator_id) also
12654
+ // serves the hot read path (team-block.ts, assign-tools.ts, detach-agent.ts
12655
+ // all look up by orchestratorId alone) — no separate index needed.
12656
+ unique("agent_assignments_orchestrator_sub_agent_unique").on(
12657
+ table.orchestratorId,
12658
+ table.subAgentId
12659
+ )
12660
+ ]
12489
12661
  );
12490
12662
  agentBudgets = pgTable(
12491
12663
  "agent_budgets",
@@ -12532,6 +12704,18 @@ var init_jobs = __esm({
12532
12704
  task: text("task").notNull(),
12533
12705
  originalTask: text("original_task"),
12534
12706
  chatId: text("chat_id"),
12707
+ /**
12708
+ * Groups jobs from the same conversational thread for the Jobs page UI
12709
+ * (migration 0059). Purely a reporting/grouping concern — the runtime
12710
+ * execution path (delegation, cron, task-board) never reads this column.
12711
+ * NULL for non-conversational jobs (cron/schedule/webhook with no parent).
12712
+ * Stamped at creation by apps/runner/src/job/conversation-id.ts, reusing
12713
+ * the exact idle-reset session boundary thread-history.ts already uses
12714
+ * for chat continuity — see that file for the gap rule. Delegated/
12715
+ * task-board children inherit their creator's value; nothing else
12716
+ * mutates it after insert.
12717
+ */
12718
+ conversationId: uuid("conversation_id"),
12535
12719
  systemPrompt: text("system_prompt"),
12536
12720
  messages: jsonb("messages").default(sql`'[]'::jsonb`),
12537
12721
  /**
@@ -12614,8 +12798,22 @@ var init_jobs = __esm({
12614
12798
  sql`${table.createdAt} DESC`
12615
12799
  ),
12616
12800
  index("idx_agent_jobs_parent_job_id").on(table.parentJobId),
12617
- index("idx_jobs_parent").on(table.parentJobId),
12801
+ // Migration 0059: the Jobs page groups conversational jobs by
12802
+ // (entity_id, conversation_id) — this index serves that lookup directly.
12803
+ index("idx_agent_jobs_entity_conversation").on(table.entityId, table.conversationId),
12804
+ // idx_jobs_parent (F-20, audit #2) was an exact duplicate of
12805
+ // idx_agent_jobs_parent_job_id above — same column, same order, both from
12806
+ // migration 0000. Dropped in migration 0054; kept this one (matches the
12807
+ // idx_agent_jobs_* naming convention used everywhere else on this table).
12618
12808
  index("idx_jobs_status").on(table.status, table.createdAt),
12809
+ // DB-3 (audit #2): findUndeliveredRootJobIds (cron/deliver-results.ts)
12810
+ // joins on this column filtered to IS NULL every tick; the cron recovery
12811
+ // scans (findPendingJobsToRecover, resetOrphanedJobs, failStalePendingJobs
12812
+ // in cron/reset-orphans.ts) also filter by status without an entity_id,
12813
+ // which idx_jobs_status (kept above) still serves — completed rows vastly
12814
+ // outnumber the still-open ones, so a partial index keyed on the open set
12815
+ // stays small regardless of table growth.
12816
+ index("idx_agent_jobs_completed_at_null").on(table.completedAt).where(sql`${table.completedAt} IS NULL`),
12619
12817
  check(
12620
12818
  "agent_jobs_status_check",
12621
12819
  sql`${table.status} IN ('pending','processing','completed','failed','awaiting_approval','awaiting_delegation','cancelled')`
@@ -12678,6 +12876,10 @@ var init_tasks = __esm({
12678
12876
  index("idx_agent_tasks_orchestrator_status").on(table.orchestratorId, table.status),
12679
12877
  index("idx_agent_tasks_status").on(table.status),
12680
12878
  index("idx_agent_tasks_assigned").on(table.assignedAgentId),
12879
+ // DB-3 (audit #2): findUndeliveredRootJobIds (apps/runner/src/cron/
12880
+ // deliver-results.ts) scans `WHERE root_job_id IS NOT NULL` on every cron
12881
+ // tick and had no index to support it — full scan on the hottest task table.
12882
+ index("idx_agent_tasks_root_job_id").on(table.rootJobId),
12681
12883
  check(
12682
12884
  "agent_tasks_status_check",
12683
12885
  sql`${table.status} IN ('todo','in_progress','done','cancelled','blocked')`
@@ -12742,7 +12944,10 @@ var init_connectors = __esm({
12742
12944
  name: text("name").notNull(),
12743
12945
  slug: text("slug").notNull(),
12744
12946
  baseUrl: text("base_url"),
12745
- // Encrypted with pgp_sym_encrypt in DB raw value never returned in select
12947
+ // audit#2026-07-07 SEC-5: this comment used to claim pgp_sym_encrypt (pgcrypto),
12948
+ // which is not used anywhere. Real encryption is application-level AES-256-GCM
12949
+ // via @nodal-agents/secrets before the value ever reaches this column — the DB
12950
+ // column itself just holds ciphertext as opaque text. Raw value never returned in select.
12746
12951
  apiKey: text("api_key"),
12747
12952
  active: boolean("active").default(true),
12748
12953
  authType: text("auth_type").notNull().default("api_key"),
@@ -12862,7 +13067,16 @@ var init_approvals = __esm({
12862
13067
  check(
12863
13068
  "approval_rules_action_check",
12864
13069
  sql`${table.action} IN ('auto_approve','require_approval','block')`
12865
- )
13070
+ ),
13071
+ // DB-1 (audit #2): one canonical rule per (entity, agent-or-null, tool) —
13072
+ // without this, two concurrent setAgentApprovalRuleAction calls (or any
13073
+ // direct insert) can leave two rows for the same scope with DIVERGENT
13074
+ // actions, and matchApprovalRule's `.find()` picks whichever the SELECT
13075
+ // happens to return first — a non-deterministic approval gate. agentId IS
13076
+ // NULL marks an entity-wide rule (e.g. the run_command LAN master-switch),
13077
+ // so a plain UNIQUE would treat two NULL rows as distinct and let the
13078
+ // duplicate back in; NULLS NOT DISTINCT (PG15+) closes that gap.
13079
+ unique("approval_rules_entity_agent_tool_unique").on(table.entityId, table.agentId, table.toolName).nullsNotDistinct()
12866
13080
  ]
12867
13081
  );
12868
13082
  }
@@ -12994,8 +13208,8 @@ var init_skills = __esm({
12994
13208
  {
12995
13209
  id: uuid("id").primaryKey().defaultRandom(),
12996
13210
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12997
- name: text("name").notNull().unique(),
12998
- slug: text("slug").notNull().unique(),
13211
+ name: text("name").notNull(),
13212
+ slug: text("slug").notNull(),
12999
13213
  content: text("content").notNull(),
13000
13214
  active: boolean("active").default(true),
13001
13215
  description: text("description"),
@@ -13035,7 +13249,20 @@ var init_skills = __esm({
13035
13249
  (table) => [
13036
13250
  index("idx_agent_skills_entity_id").on(table.entityId),
13037
13251
  index("idx_skills_active").on(table.active, table.slug),
13038
- index("idx_agent_skills_is_community").on(table.isCommunity)
13252
+ index("idx_agent_skills_is_community").on(table.isCommunity),
13253
+ // F-6 (audit #2): slug AND name were UNIQUE GLOBALLY — a 2nd workspace/
13254
+ // entity installing the same community skill (e.g. slug 'comfyui') or
13255
+ // creating a skill with the same display name crashed the insert, and in
13256
+ // multi-user mode let one entity squat/enumerate another's slugs or
13257
+ // names. Scoped to (entity_id, slug) / (entity_id, name): every real
13258
+ // insert always sets entityId (createSkillRepo takes it as a required
13259
+ // param; the system-skill seeder and community-skill installer both pass
13260
+ // it too — no production path leaves it NULL), but the column itself is
13261
+ // nullable, so NULLS NOT DISTINCT closes the same NULL-entity gap as
13262
+ // DB-1/agent_plugins (multiple (NULL, 'x') rows would otherwise all
13263
+ // satisfy a plain UNIQUE).
13264
+ unique("agent_skills_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct(),
13265
+ unique("agent_skills_entity_name_unique").on(table.entityId, table.name).nullsNotDistinct()
13039
13266
  ]
13040
13267
  );
13041
13268
  skillVersions = pgTable(
@@ -13092,7 +13319,14 @@ var init_skills = __esm({
13092
13319
  filesWritable: boolean("files_writable").notNull().default(false),
13093
13320
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
13094
13321
  },
13095
- () => []
13322
+ (table) => [
13323
+ // DB-2 (audit #2): the table had no index beyond the PK, so assignSkillRepo's
13324
+ // check-then-insert (packages/db/src/repos/skills.ts) could race and leave
13325
+ // the same skill assigned twice to one agent. The unique index's leading
13326
+ // column (agent_id) also serves the hot read path (execute.ts loads all
13327
+ // skills for an agent) — no separate index needed on top of it.
13328
+ unique("agent_skill_assignments_agent_skill_unique").on(table.agentId, table.skillId)
13329
+ ]
13096
13330
  );
13097
13331
  }
13098
13332
  });
@@ -13224,7 +13458,14 @@ var init_mcp = __esm({
13224
13458
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13225
13459
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13226
13460
  },
13227
- (table) => [index("idx_mcp_connections_entity").on(table.entityId)]
13461
+ (table) => [
13462
+ index("idx_mcp_connections_entity").on(table.entityId),
13463
+ // R5 (audit #2 follow-up): the pglite test DDL (packages/db/src/tests/
13464
+ // helpers.ts) already assumed a per-entity unique slug here — same class
13465
+ // of phantom-constraint bug as DB-2/F-18. entityId is NOT NULL on this
13466
+ // table, so a plain UNIQUE (no NULLS NOT DISTINCT needed) is correct.
13467
+ uniqueIndex("mcp_connections_entity_slug_unique").on(table.entityId, table.slug)
13468
+ ]
13228
13469
  );
13229
13470
  }
13230
13471
  });
@@ -13279,7 +13520,14 @@ var init_misc = __esm({
13279
13520
  check(
13280
13521
  "agent_plugins_hook_check",
13281
13522
  sql`${table.hook} IN ('pre_task','post_task','pre_tool','post_tool','on_memory_save')`
13282
- )
13523
+ ),
13524
+ // R5 (audit #2 follow-up): the pglite test DDL (packages/db/src/tests/
13525
+ // helpers.ts) already assumed a per-entity unique slug here — same class
13526
+ // of phantom-constraint bug as DB-2/F-18 (test mock ahead of the real
13527
+ // schema/migration). entityId is nullable (no `.notNull()` above), so
13528
+ // NULLS NOT DISTINCT closes the same NULL-entity gap as DB-1's
13529
+ // approval_rules constraint.
13530
+ unique("agent_plugins_entity_slug_unique").on(table.entityId, table.slug).nullsNotDistinct()
13283
13531
  ]
13284
13532
  );
13285
13533
  rateLimits = pgTable("rate_limits", {
@@ -13451,16 +13699,65 @@ var init_chat_messages = __esm({
13451
13699
  });
13452
13700
 
13453
13701
  // ../../packages/db/src/schema/app-settings.ts
13454
- var appSettings;
13702
+ var appSettings, entitySettings;
13455
13703
  var init_app_settings = __esm({
13456
13704
  "../../packages/db/src/schema/app-settings.ts"() {
13457
13705
  "use strict";
13458
13706
  init_pg_core();
13707
+ init_entities();
13459
13708
  appSettings = pgTable("app_settings", {
13460
13709
  key: text("key").primaryKey(),
13461
13710
  value: text("value").default("").notNull(),
13462
13711
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
13463
13712
  });
13713
+ entitySettings = pgTable(
13714
+ "entity_settings",
13715
+ {
13716
+ entityId: uuid("entity_id").notNull().references(() => entities.id, { onDelete: "cascade" }),
13717
+ key: text("key").notNull(),
13718
+ value: text("value").default("").notNull(),
13719
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
13720
+ },
13721
+ (table) => [primaryKey({ columns: [table.entityId, table.key] })]
13722
+ );
13723
+ }
13724
+ });
13725
+
13726
+ // ../../packages/db/src/schema/telegram-allowed-chats.ts
13727
+ var telegramAllowedChats;
13728
+ var init_telegram_allowed_chats = __esm({
13729
+ "../../packages/db/src/schema/telegram-allowed-chats.ts"() {
13730
+ "use strict";
13731
+ init_pg_core();
13732
+ init_drizzle_orm();
13733
+ init_entities();
13734
+ init_agents();
13735
+ telegramAllowedChats = pgTable(
13736
+ "telegram_allowed_chats",
13737
+ {
13738
+ id: uuid("id").primaryKey().defaultRandom(),
13739
+ entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
13740
+ /** The bot the chat is talking to (the RECEIVING agent). */
13741
+ agentId: uuid("agent_id").notNull().references(() => agents.id, { onDelete: "cascade" }),
13742
+ /** Telegram chat id (private chat, group, or supergroup) as a string. */
13743
+ chatId: text("chat_id").notNull(),
13744
+ /** 'owner' = the bot's controller; 'member' = an approved additional chat. */
13745
+ role: text("role").notNull().default("member"),
13746
+ /** 'active' = may create jobs; 'pending' = awaiting the owner's confirmation. */
13747
+ status: text("status").notNull().default("pending"),
13748
+ /** Display name of the requester, for the owner's confirmation card. */
13749
+ requesterName: text("requester_name"),
13750
+ createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
13751
+ updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow()
13752
+ },
13753
+ (table) => [
13754
+ unique("telegram_allowed_chats_agent_chat_unique").on(table.agentId, table.chatId),
13755
+ index("idx_telegram_allowed_chats_agent").on(table.agentId),
13756
+ index("idx_telegram_allowed_chats_entity").on(table.entityId),
13757
+ check("telegram_allowed_chats_role_check", sql`${table.role} IN ('owner','member')`),
13758
+ check("telegram_allowed_chats_status_check", sql`${table.status} IN ('active','pending')`)
13759
+ ]
13760
+ );
13464
13761
  }
13465
13762
  });
13466
13763
 
@@ -13512,12 +13809,14 @@ __export(schema_exports, {
13512
13809
  entities: () => entities,
13513
13810
  entityLlmKeys: () => entityLlmKeys,
13514
13811
  entityMembers: () => entityMembers,
13812
+ entitySettings: () => entitySettings,
13515
13813
  mcpConnections: () => mcpConnections,
13516
13814
  mcpServers: () => mcpServers,
13517
13815
  rateLimits: () => rateLimits,
13518
13816
  sessions: () => sessions,
13519
13817
  skillConnectors: () => skillConnectors,
13520
13818
  skillVersions: () => skillVersions,
13819
+ telegramAllowedChats: () => telegramAllowedChats,
13521
13820
  toolCalls: () => toolCalls,
13522
13821
  userProfiles: () => userProfiles,
13523
13822
  users: () => users,
@@ -13549,6 +13848,7 @@ var init_schema2 = __esm({
13549
13848
  init_agent_workspaces();
13550
13849
  init_chat_messages();
13551
13850
  init_app_settings();
13851
+ init_telegram_allowed_chats();
13552
13852
  }
13553
13853
  });
13554
13854
 
@@ -13557,7 +13857,29 @@ function createClient(connectionString, options = {}) {
13557
13857
  const pool = src_default(connectionString, {
13558
13858
  max: options.max ?? 10,
13559
13859
  // Fail fast on connection issues rather than hanging
13560
- connect_timeout: 10
13860
+ connect_timeout: 10,
13861
+ // R2 (audit2 concurrency review): bound the two ways a query on this pool
13862
+ // can hang indefinitely with no client-side timeout at all. Sent as
13863
+ // Postgres startup/session parameters (postgres.js `connection` option),
13864
+ // so every connection in the pool gets them — no per-query opt-in needed.
13865
+ // - lock_timeout: the vector identified for the cron watchdog (R1) — a
13866
+ // query blocked waiting on a row lock previously waited forever.
13867
+ // 30s is comfortably longer than any legitimate lock wait in this
13868
+ // app's write patterns (single-row updates/inserts, no long-held
13869
+ // locks by design).
13870
+ // - idle_in_transaction_session_timeout: a transaction left open with
13871
+ // no further statements (BEGIN without COMMIT/ROLLBACK) is always a
13872
+ // bug, never a legitimate long-running operation — 60s is generous.
13873
+ // Deliberately NOT setting statement_timeout here: a global cap would
13874
+ // also apply to legitimate long-running operations (a large backfill, a
13875
+ // slow migration-adjacent query) and risk cutting them off mid-flight.
13876
+ // migrate.ts uses its own separate `postgres()` connection (not this
13877
+ // factory) for running migrations, so these timeouts don't touch
13878
+ // migrations at all either way.
13879
+ connection: {
13880
+ lock_timeout: 3e4,
13881
+ idle_in_transaction_session_timeout: 6e4
13882
+ }
13561
13883
  });
13562
13884
  const db = drizzle(pool, { schema: schema_exports });
13563
13885
  return {
@@ -13584,12 +13906,24 @@ var init_transaction = __esm({
13584
13906
 
13585
13907
  // ../../packages/secrets/src/index.ts
13586
13908
  import { createCipheriv, createDecipheriv, randomBytes as randomBytes2 } from "crypto";
13587
- import { chmodSync, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13909
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13910
+ import { execFileSync as execFileSync2 } from "child_process";
13588
13911
  import { homedir as homedir2 } from "os";
13589
13912
  import { dirname as dirname3, join as join5 } from "path";
13913
+ function masterKeyPath() {
13914
+ return join5(homedir2(), ".nodalai", "secrets.key");
13915
+ }
13916
+ function masterKeyFileMissing(path = masterKeyPath()) {
13917
+ return !existsSync5(path);
13918
+ }
13919
+ function isEncrypted(value) {
13920
+ return value.startsWith(`enc:${VERSION}:`);
13921
+ }
13922
+ var VERSION;
13590
13923
  var init_src3 = __esm({
13591
13924
  "../../packages/secrets/src/index.ts"() {
13592
13925
  "use strict";
13926
+ VERSION = "v1";
13593
13927
  }
13594
13928
  });
13595
13929
 
@@ -13605,6 +13939,41 @@ var init_credentials2 = __esm({
13605
13939
  }
13606
13940
  });
13607
13941
 
13942
+ // ../../packages/db/src/queries/master-key-guard.ts
13943
+ async function assertMasterKeyRestorable(db, options = {}) {
13944
+ if (!masterKeyFileMissing(options.keyPath)) return;
13945
+ const [credRow] = await db.select({ id: credentials.id }).from(credentials).limit(1);
13946
+ if (credRow) throw missingKeyError("a stored OAuth credential");
13947
+ const llmKeyRows = await db.select({ apiKey: entityLlmKeys.apiKey }).from(entityLlmKeys);
13948
+ const hasEncryptedLlmKey = llmKeyRows.some((r) => r.apiKey !== "" && isEncrypted(r.apiKey));
13949
+ if (hasEncryptedLlmKey) throw missingKeyError("a stored LLM API key");
13950
+ const connectorRows = await db.select({ apiKey: connectors.apiKey }).from(connectors);
13951
+ const hasEncryptedConnector = connectorRows.some((r) => !!r.apiKey && isEncrypted(r.apiKey));
13952
+ if (hasEncryptedConnector) throw missingKeyError("a stored connector API key");
13953
+ const mcpRows = await db.select({ apiKey: mcpServers.apiKey, envVars: mcpServers.envVars }).from(mcpServers);
13954
+ const hasEncryptedMcp = mcpRows.some((r) => {
13955
+ if (r.apiKey && isEncrypted(r.apiKey)) return true;
13956
+ const env = r.envVars ?? {};
13957
+ return Object.values(env).some((v) => typeof v === "string" && isEncrypted(v));
13958
+ });
13959
+ if (hasEncryptedMcp) throw missingKeyError("a stored MCP server secret");
13960
+ }
13961
+ function missingKeyError(what) {
13962
+ return new Error(
13963
+ `~/.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.`
13964
+ );
13965
+ }
13966
+ var init_master_key_guard = __esm({
13967
+ "../../packages/db/src/queries/master-key-guard.ts"() {
13968
+ "use strict";
13969
+ init_credentials();
13970
+ init_llm_keys();
13971
+ init_connectors();
13972
+ init_mcp();
13973
+ init_src3();
13974
+ }
13975
+ });
13976
+
13608
13977
  // ../../packages/db/src/repos/agents.ts
13609
13978
  var init_agents2 = __esm({
13610
13979
  "../../packages/db/src/repos/agents.ts"() {
@@ -13648,6 +14017,7 @@ var init_src4 = __esm({
13648
14017
  init_transaction();
13649
14018
  init_schema2();
13650
14019
  init_credentials2();
14020
+ init_master_key_guard();
13651
14021
  init_agents2();
13652
14022
  init_skills2();
13653
14023
  init_retention();
@@ -13707,11 +14077,20 @@ var init_local_auth = __esm({
13707
14077
  }
13708
14078
  });
13709
14079
 
14080
+ // ../../packages/auth/src/lib/constant-time.ts
14081
+ import { createHash, timingSafeEqual } from "crypto";
14082
+ var init_constant_time = __esm({
14083
+ "../../packages/auth/src/lib/constant-time.ts"() {
14084
+ "use strict";
14085
+ }
14086
+ });
14087
+
13710
14088
  // ../../packages/auth/src/providers/bearer-token.ts
13711
14089
  var init_bearer_token = __esm({
13712
14090
  "../../packages/auth/src/providers/bearer-token.ts"() {
13713
14091
  "use strict";
13714
14092
  init_local_trust();
14093
+ init_constant_time();
13715
14094
  }
13716
14095
  });
13717
14096
 
@@ -13750,6 +14129,7 @@ var init_seed = __esm({
13750
14129
  });
13751
14130
 
13752
14131
  // src/lib/env.ts
14132
+ import { randomBytes as randomBytes3 } from "crypto";
13753
14133
  function buildEnvForRunner(config, databaseUrl) {
13754
14134
  const bind = config.bind === "loopback" ? "127.0.0.1" : "0.0.0.0";
13755
14135
  const env = {
@@ -13773,7 +14153,19 @@ function buildEnvForRunner(config, databaseUrl) {
13773
14153
  return env;
13774
14154
  }
13775
14155
  function resolveAuthMode(config) {
13776
- if (config.auth?.mode) return config.auth.mode;
14156
+ if (config.bearerToken) {
14157
+ throw new Error(
14158
+ '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.'
14159
+ );
14160
+ }
14161
+ if (config.auth?.mode) {
14162
+ if (config.auth.mode === "local-trust" && config.bind === "lan") {
14163
+ throw new Error(
14164
+ '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.'
14165
+ );
14166
+ }
14167
+ return config.auth.mode;
14168
+ }
13777
14169
  return config.bind === "lan" ? "local-auth" : "local-trust";
13778
14170
  }
13779
14171
  function buildEnvForWeb(config, databaseUrl) {
@@ -13797,8 +14189,16 @@ function buildEnvForWeb(config, databaseUrl) {
13797
14189
  // The web itself doesn't bind on this — Next.js listens on 0.0.0.0 anyway.
13798
14190
  BIND: bind,
13799
14191
  NODE_ENV: "production",
13800
- // AUTH_SECRET is required by better-auth in local-auth mode; harmless in local-trust.
13801
- AUTH_SECRET: config.workerSecret,
14192
+ // AUTH_SECRET is required by better-auth in local-auth mode; harmless in
14193
+ // local-trust. M-4: deliberately SEPARATE from workerSecret — the two
14194
+ // used to share a value, so a leak of workerSecret (the runner's auth
14195
+ // frontier) would also let an attacker forge a signed better-auth session
14196
+ // cookie. readConfig() auto-mints authSecret, so this is set in every
14197
+ // normal boot path; the random fallback below only guards a caller that
14198
+ // somehow bypasses readConfig (ephemeral — would invalidate sessions on
14199
+ // every restart, which is a symptom to fix at the config layer, not a
14200
+ // reason to crash boot or fall back to workerSecret).
14201
+ AUTH_SECRET: config.authSecret ?? randomBytes3(32).toString("base64"),
13802
14202
  // WORKER_SECRET — same value as the runner — so sendTaskAction can sign
13803
14203
  // the POST /api/worker call. Without this the runner returns 403 and the
13804
14204
  // job stays pending forever (cron only scans task-board, not API jobs).
@@ -14081,7 +14481,7 @@ __export(up_exports, {
14081
14481
  import chalk from "chalk";
14082
14482
  import ora from "ora";
14083
14483
  import open from "open";
14084
- import { randomBytes as randomBytes3 } from "crypto";
14484
+ import { randomBytes as randomBytes4 } from "crypto";
14085
14485
  async function killSilent(child) {
14086
14486
  await killProcessTree(child);
14087
14487
  }
@@ -14106,8 +14506,12 @@ async function runUp(opts = {}) {
14106
14506
  const defaultConfig = {
14107
14507
  // No `llm` section: the browser onboarding sets it via createLlmKeyAction.
14108
14508
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14109
- workerSecret: randomBytes3(32).toString("hex"),
14110
- serverActionsKey: randomBytes3(32).toString("base64"),
14509
+ workerSecret: randomBytes4(32).toString("hex"),
14510
+ serverActionsKey: randomBytes4(32).toString("base64"),
14511
+ // M-4: authSecret is minted here (distinct from workerSecret) rather
14512
+ // than left for readConfig()'s auto-mint, because this in-memory
14513
+ // `config` is used directly below without a re-read.
14514
+ authSecret: randomBytes4(32).toString("base64"),
14111
14515
  // loopback derives auth: local-trust (no login/signup). Leave `auth`
14112
14516
  // unset so resolveAuthMode falls back to the loopback → local-trust map.
14113
14517
  bind: "loopback"
@@ -14115,6 +14519,7 @@ async function runUp(opts = {}) {
14115
14519
  writeConfig(defaultConfig);
14116
14520
  config = defaultConfig;
14117
14521
  }
14522
+ resolveAuthMode(config);
14118
14523
  const orphans = [];
14119
14524
  for (const [name, port] of [
14120
14525
  ["web", config.ports.web],
@@ -14220,6 +14625,17 @@ async function runUp(opts = {}) {
14220
14625
  await pg.stop();
14221
14626
  throw err;
14222
14627
  }
14628
+ {
14629
+ const { db, close } = createClient(databaseUrl, { max: 1 });
14630
+ try {
14631
+ await assertMasterKeyRestorable(db);
14632
+ } catch (err) {
14633
+ await close();
14634
+ await pg.stop();
14635
+ throw err;
14636
+ }
14637
+ await close();
14638
+ }
14223
14639
  const authMode = resolveAuthMode(config);
14224
14640
  if (authMode === "local-trust") {
14225
14641
  const seedSpinner = ora("Seeding default user and agent\u2026").start();
@@ -14269,13 +14685,13 @@ async function runUp(opts = {}) {
14269
14685
  }
14270
14686
  void (async () => {
14271
14687
  try {
14272
- const { getInstalledVersion: getInstalledVersion2, getLatestVersion: getLatestVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
14688
+ const { getInstalledVersion: getInstalledVersion2, getLatestVersion: getLatestVersion2, isNewerVersion: isNewerVersion2 } = await Promise.resolve().then(() => (init_version(), version_exports));
14273
14689
  const installed = getInstalledVersion2();
14274
14690
  const latest = await Promise.race([
14275
14691
  getLatestVersion2(),
14276
14692
  new Promise((resolve2) => setTimeout(() => resolve2(null), 3e3))
14277
14693
  ]);
14278
- if (latest !== null && latest !== installed) {
14694
+ if (latest !== null && isNewerVersion2(latest, installed)) {
14279
14695
  console.log(chalk.cyan(` \u2139 v${latest} available \u2014 run \`nodal-agents update\``));
14280
14696
  }
14281
14697
  } catch {
@@ -14431,9 +14847,9 @@ __export(init_exports, {
14431
14847
  import chalk2 from "chalk";
14432
14848
  import prompts from "prompts";
14433
14849
  import ora2 from "ora";
14434
- import { randomBytes as randomBytes4 } from "crypto";
14850
+ import { randomBytes as randomBytes5 } from "crypto";
14435
14851
  function generateSecret(bytes = 32) {
14436
- return randomBytes4(bytes).toString("hex");
14852
+ return randomBytes5(bytes).toString("hex");
14437
14853
  }
14438
14854
  async function runInit(options = {}) {
14439
14855
  const existing = readConfig();
@@ -14454,7 +14870,7 @@ async function runInit(options = {}) {
14454
14870
  const config2 = {
14455
14871
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14456
14872
  workerSecret: generateSecret(32),
14457
- serverActionsKey: randomBytes4(32).toString("base64"),
14873
+ serverActionsKey: randomBytes5(32).toString("base64"),
14458
14874
  bind: "lan",
14459
14875
  auth: { mode: "local-auth" }
14460
14876
  };