nodal-agents 0.7.0 → 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 (165) hide show
  1. package/cli.js +335 -35
  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/meta/_journal.json +28 -0
  7. package/package.json +1 -1
  8. package/runner.js +47123 -35332
  9. package/web/.next/BUILD_ID +1 -1
  10. package/web/.next/app-path-routes-manifest.json +4 -4
  11. package/web/.next/build-manifest.json +2 -2
  12. package/web/.next/prerender-manifest.json +3 -3
  13. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js +3 -3
  14. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page.js.nft.json +1 -1
  15. package/web/.next/server/app/(dashboard)/agents/[id]/edit/page_client-reference-manifest.js +1 -1
  16. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js +2 -2
  17. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page.js.nft.json +1 -1
  18. package/web/.next/server/app/(dashboard)/agents/[id]/telegram/page_client-reference-manifest.js +1 -1
  19. package/web/.next/server/app/(dashboard)/agents/page.js +2 -2
  20. package/web/.next/server/app/(dashboard)/agents/page.js.nft.json +1 -1
  21. package/web/.next/server/app/(dashboard)/agents/page_client-reference-manifest.js +1 -1
  22. package/web/.next/server/app/(dashboard)/approvals/page.js +2 -2
  23. package/web/.next/server/app/(dashboard)/approvals/page.js.nft.json +1 -1
  24. package/web/.next/server/app/(dashboard)/approvals/page_client-reference-manifest.js +1 -1
  25. package/web/.next/server/app/(dashboard)/automations/page.js +2 -2
  26. package/web/.next/server/app/(dashboard)/automations/page.js.nft.json +1 -1
  27. package/web/.next/server/app/(dashboard)/automations/page_client-reference-manifest.js +1 -1
  28. package/web/.next/server/app/(dashboard)/chat/page.js +2 -2
  29. package/web/.next/server/app/(dashboard)/chat/page.js.nft.json +1 -1
  30. package/web/.next/server/app/(dashboard)/chat/page_client-reference-manifest.js +1 -1
  31. package/web/.next/server/app/(dashboard)/connectors/page.js +1 -1
  32. package/web/.next/server/app/(dashboard)/connectors/page.js.nft.json +1 -1
  33. package/web/.next/server/app/(dashboard)/connectors/page_client-reference-manifest.js +1 -1
  34. package/web/.next/server/app/(dashboard)/credentials/page.js +1 -1
  35. package/web/.next/server/app/(dashboard)/credentials/page.js.nft.json +1 -1
  36. package/web/.next/server/app/(dashboard)/credentials/page_client-reference-manifest.js +1 -1
  37. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js +2 -2
  38. package/web/.next/server/app/(dashboard)/jobs/[id]/page.js.nft.json +1 -1
  39. package/web/.next/server/app/(dashboard)/jobs/[id]/page_client-reference-manifest.js +1 -1
  40. package/web/.next/server/app/(dashboard)/jobs/page.js +2 -2
  41. package/web/.next/server/app/(dashboard)/jobs/page.js.nft.json +1 -1
  42. package/web/.next/server/app/(dashboard)/jobs/page_client-reference-manifest.js +1 -1
  43. package/web/.next/server/app/(dashboard)/learned-skills/page.js +2 -2
  44. package/web/.next/server/app/(dashboard)/learned-skills/page.js.nft.json +1 -1
  45. package/web/.next/server/app/(dashboard)/learned-skills/page_client-reference-manifest.js +1 -1
  46. package/web/.next/server/app/(dashboard)/llm-providers/page.js +1 -1
  47. package/web/.next/server/app/(dashboard)/llm-providers/page.js.nft.json +1 -1
  48. package/web/.next/server/app/(dashboard)/llm-providers/page_client-reference-manifest.js +1 -1
  49. package/web/.next/server/app/(dashboard)/logs/page.js +2 -2
  50. package/web/.next/server/app/(dashboard)/logs/page.js.nft.json +1 -1
  51. package/web/.next/server/app/(dashboard)/logs/page_client-reference-manifest.js +1 -1
  52. package/web/.next/server/app/(dashboard)/mcp/page.js +2 -2
  53. package/web/.next/server/app/(dashboard)/mcp/page.js.nft.json +1 -1
  54. package/web/.next/server/app/(dashboard)/mcp/page_client-reference-manifest.js +1 -1
  55. package/web/.next/server/app/(dashboard)/memories/page.js +2 -2
  56. package/web/.next/server/app/(dashboard)/memories/page.js.nft.json +1 -1
  57. package/web/.next/server/app/(dashboard)/memories/page_client-reference-manifest.js +1 -1
  58. package/web/.next/server/app/(dashboard)/page.js +2 -2
  59. package/web/.next/server/app/(dashboard)/page.js.nft.json +1 -1
  60. package/web/.next/server/app/(dashboard)/page_client-reference-manifest.js +1 -1
  61. package/web/.next/server/app/(dashboard)/settings/page.js +2 -2
  62. package/web/.next/server/app/(dashboard)/settings/page.js.nft.json +1 -1
  63. package/web/.next/server/app/(dashboard)/settings/page_client-reference-manifest.js +1 -1
  64. package/web/.next/server/app/(dashboard)/settings/root-context/page.js +2 -2
  65. package/web/.next/server/app/(dashboard)/settings/root-context/page.js.nft.json +1 -1
  66. package/web/.next/server/app/(dashboard)/settings/root-context/page_client-reference-manifest.js +1 -1
  67. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js +2 -2
  68. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page.js.nft.json +1 -1
  69. package/web/.next/server/app/(dashboard)/skills/[id]/edit/page_client-reference-manifest.js +1 -1
  70. package/web/.next/server/app/(dashboard)/skills/new/page.js +2 -2
  71. package/web/.next/server/app/(dashboard)/skills/new/page.js.nft.json +1 -1
  72. package/web/.next/server/app/(dashboard)/skills/new/page_client-reference-manifest.js +1 -1
  73. package/web/.next/server/app/(dashboard)/skills/page.js +2 -2
  74. package/web/.next/server/app/(dashboard)/skills/page.js.nft.json +1 -1
  75. package/web/.next/server/app/(dashboard)/skills/page_client-reference-manifest.js +1 -1
  76. package/web/.next/server/app/_global-error/page_client-reference-manifest.js +1 -1
  77. package/web/.next/server/app/_global-error.html +1 -1
  78. package/web/.next/server/app/_global-error.rsc +2 -2
  79. package/web/.next/server/app/_global-error.segments/_full.segment.rsc +2 -2
  80. package/web/.next/server/app/_global-error.segments/_global-error/__PAGE__.segment.rsc +1 -1
  81. package/web/.next/server/app/_global-error.segments/_global-error.segment.rsc +1 -1
  82. package/web/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
  83. package/web/.next/server/app/_global-error.segments/_index.segment.rsc +2 -2
  84. package/web/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
  85. package/web/.next/server/app/_not-found/page_client-reference-manifest.js +1 -1
  86. package/web/.next/server/app/_not-found.html +1 -1
  87. package/web/.next/server/app/_not-found.rsc +3 -3
  88. package/web/.next/server/app/_not-found.segments/_full.segment.rsc +3 -3
  89. package/web/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
  90. package/web/.next/server/app/_not-found.segments/_index.segment.rsc +3 -3
  91. package/web/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +2 -2
  92. package/web/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
  93. package/web/.next/server/app/_not-found.segments/_tree.segment.rsc +2 -2
  94. package/web/.next/server/app/api/auth/[...all]/route.js +1 -1
  95. package/web/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
  96. package/web/.next/server/app/api/oauth/[provider]/callback/route.js +1 -1
  97. package/web/.next/server/app/api/oauth/[provider]/callback/route.js.nft.json +1 -1
  98. package/web/.next/server/app/api/oauth/[provider]/start/route.js +1 -1
  99. package/web/.next/server/app/api/oauth/[provider]/start/route.js.nft.json +1 -1
  100. package/web/.next/server/app/login/page_client-reference-manifest.js +1 -1
  101. package/web/.next/server/app/onboarding/page.js +205 -185
  102. package/web/.next/server/app/onboarding/page_client-reference-manifest.js +1 -1
  103. package/web/.next/server/app-paths-manifest.json +4 -4
  104. package/web/.next/server/chunks/2713.js +47 -27
  105. package/web/.next/server/chunks/3223.js +1 -1
  106. package/web/.next/server/chunks/4574.js +1 -1
  107. package/web/.next/server/chunks/5515.js +1 -0
  108. package/web/.next/server/chunks/6468.js +1 -0
  109. package/web/.next/server/chunks/8410.js +1 -0
  110. package/web/.next/server/chunks/{4488.js → 8810.js} +2 -2
  111. package/web/.next/server/chunks/{3154.js → 960.js} +10 -10
  112. package/web/.next/server/middleware-build-manifest.js +1 -1
  113. package/web/.next/server/pages/404.html +1 -1
  114. package/web/.next/server/pages/500.html +1 -1
  115. package/web/.next/server/server-reference-manifest.js +1 -1
  116. package/web/.next/server/server-reference-manifest.json +1 -1
  117. package/web/.next/static/chunks/4964-c6711d3d479b6085.js +1 -0
  118. package/web/.next/static/chunks/{9836-6830cb9d49ad261f.js → 9836-177f6ca2f15ff703.js} +1 -1
  119. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-cfce874f4acede5e.js +6 -0
  120. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-9249c2839f551ba1.js +1 -0
  121. package/web/.next/static/chunks/app/(dashboard)/agents/page-7d784d737acedccf.js +1 -0
  122. package/web/.next/static/chunks/app/(dashboard)/approvals/{page-9a01cb723a6ccff0.js → page-b3a838ecbc94a030.js} +1 -1
  123. package/web/.next/static/chunks/app/(dashboard)/automations/page-b1b548de4f52696c.js +1 -0
  124. package/web/.next/static/chunks/app/(dashboard)/chat/{page-a6cc7a58986c0340.js → page-bcf6dcc73b370eb0.js} +1 -1
  125. package/web/.next/static/chunks/app/(dashboard)/connectors/{page-21d6c042800c8cd3.js → page-2c6f913cc6aea4b7.js} +1 -1
  126. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-43f033251eddcc4b.js +1 -0
  127. package/web/.next/static/chunks/app/(dashboard)/jobs/page-1f892f950bb30403.js +1 -0
  128. package/web/.next/static/chunks/app/(dashboard)/layout-e184673151ff8a05.js +1 -0
  129. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-f873bb6f8539e47e.js +1 -0
  130. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-64adda30176f4ccb.js +1 -0
  131. package/web/.next/static/chunks/app/(dashboard)/logs/page-79cfd9141c39d8ef.js +1 -0
  132. package/web/.next/static/chunks/app/(dashboard)/mcp/page-d276d7bab25286a1.js +1 -0
  133. package/web/.next/static/chunks/app/(dashboard)/memories/page-c66720494a340503.js +1 -0
  134. package/web/.next/static/chunks/app/(dashboard)/page-3649204dd27a0ad0.js +1 -0
  135. package/web/.next/static/chunks/app/(dashboard)/settings/page-1cab404d2eccfd80.js +1 -0
  136. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-b5898c5444f88c9a.js +1 -0
  137. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-82d91fc6b6acc161.js +1 -0
  138. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-58b5ed04583ca7d4.js +1 -0
  139. package/web/.next/static/chunks/app/(dashboard)/skills/page-312bd4ae8ddc8114.js +1 -0
  140. package/web/.next/static/chunks/app/onboarding/{page-a4116e2f430eaff8.js → page-f2f04878e81fdf23.js} +2 -2
  141. package/web/.next/static/css/{eff119dd24f92093.css → 4d808b91d7b58fce.css} +1 -1
  142. package/web/.next/server/chunks/4626.js +0 -1
  143. package/web/.next/server/chunks/8971.js +0 -1
  144. package/web/.next/server/chunks/9043.js +0 -1
  145. package/web/.next/static/chunks/4964-08171c654e41b0a4.js +0 -1
  146. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/edit/page-fa5717f0d0450659.js +0 -6
  147. package/web/.next/static/chunks/app/(dashboard)/agents/[id]/telegram/page-73012222871598aa.js +0 -1
  148. package/web/.next/static/chunks/app/(dashboard)/agents/page-7e9616b28c098e9e.js +0 -1
  149. package/web/.next/static/chunks/app/(dashboard)/automations/page-85c985cd687406ed.js +0 -1
  150. package/web/.next/static/chunks/app/(dashboard)/jobs/[id]/page-f6b993a88c70a2fe.js +0 -1
  151. package/web/.next/static/chunks/app/(dashboard)/jobs/page-d0d6b9db1b68451f.js +0 -1
  152. package/web/.next/static/chunks/app/(dashboard)/layout-d2ac2646967496bb.js +0 -1
  153. package/web/.next/static/chunks/app/(dashboard)/learned-skills/page-ec6312f6dfd7ee23.js +0 -1
  154. package/web/.next/static/chunks/app/(dashboard)/llm-providers/page-1fb26463eddf509c.js +0 -1
  155. package/web/.next/static/chunks/app/(dashboard)/logs/page-0bca733315e8a208.js +0 -1
  156. package/web/.next/static/chunks/app/(dashboard)/mcp/page-ff56ecf2f65f5466.js +0 -1
  157. package/web/.next/static/chunks/app/(dashboard)/memories/page-fe4d9438c93750d6.js +0 -1
  158. package/web/.next/static/chunks/app/(dashboard)/page-9402fe2931216f4b.js +0 -1
  159. package/web/.next/static/chunks/app/(dashboard)/settings/page-995662f10ebff365.js +0 -1
  160. package/web/.next/static/chunks/app/(dashboard)/settings/root-context/page-785b6ce3e4c5890b.js +0 -1
  161. package/web/.next/static/chunks/app/(dashboard)/skills/[id]/edit/page-2106201c4df8b5a4.js +0 -1
  162. package/web/.next/static/chunks/app/(dashboard)/skills/new/page-0135bcb86ab60dc2.js +0 -1
  163. package/web/.next/static/chunks/app/(dashboard)/skills/page-54e939d40c8b5b6d.js +0 -1
  164. /package/web/.next/static/{KFac7DQ7gdp9XPQ2bnb-2 → SgnP_MouhUmw0bBf-lUmD}/_buildManifest.js +0 -0
  165. /package/web/.next/static/{KFac7DQ7gdp9XPQ2bnb-2 → 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;
@@ -11883,6 +11926,13 @@ var init_root_agent = __esm({
11883
11926
  }
11884
11927
  });
11885
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
+
11886
11936
  // ../../packages/shared/src/connector-catalog.ts
11887
11937
  var init_connector_catalog = __esm({
11888
11938
  "../../packages/shared/src/connector-catalog.ts"() {
@@ -11914,7 +11964,10 @@ var init_model_catalog = __esm({
11914
11964
  // deepseek-chat is the production alias for DeepSeek V3. Standard tool
11915
11965
  // calling with forced tool_choice supported.
11916
11966
  capabilities: { tools: true, forcedToolChoice: true },
11917
- 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 }
11918
11971
  },
11919
11972
  {
11920
11973
  modelId: "deepseek-reasoner",
@@ -11924,7 +11977,8 @@ var init_model_catalog = __esm({
11924
11977
  // reasoning:true). Also requires reasoning_content to be echoed back on
11925
11978
  // assistant messages with tool_calls (the deepseek fetch shim handles this).
11926
11979
  capabilities: { tools: true, forcedToolChoice: true, reasoning: true },
11927
- contextWindow: 128e3
11980
+ contextWindow: 128e3,
11981
+ pricing: { inputPerMillionUsd: 0.55, outputPerMillionUsd: 2.19 }
11928
11982
  }
11929
11983
  ],
11930
11984
  // ─── Native MiniMax (api.minimax.io/anthropic) ───────────────────────────────
@@ -11937,6 +11991,11 @@ var init_model_catalog = __esm({
11937
11991
  // forced tool_choice with a 400/404 (observed on M3 via OpenRouter); the
11938
11992
  // runtime completion floor covers it. Context windows are docs-sourced where
11939
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.
11940
11999
  minimax: [
11941
12000
  {
11942
12001
  modelId: "MiniMax-M3",
@@ -12220,6 +12279,7 @@ var init_src2 = __esm({
12220
12279
  init_operation();
12221
12280
  init_providers();
12222
12281
  init_root_agent();
12282
+ init_system_prompt_cache();
12223
12283
  init_connector_catalog();
12224
12284
  init_community_skill_catalog();
12225
12285
  init_model_catalog();
@@ -12370,7 +12430,13 @@ var init_entities = __esm({
12370
12430
  check(
12371
12431
  "entity_members_role_check",
12372
12432
  sql`${table.role} IN ('owner', 'admin', 'member', 'viewer')`
12373
- )
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)
12374
12440
  ]
12375
12441
  );
12376
12442
  }
@@ -12422,7 +12488,7 @@ var init_agents = __esm({
12422
12488
  id: uuid("id").primaryKey().defaultRandom(),
12423
12489
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12424
12490
  name: text("name").notNull(),
12425
- slug: text("slug").notNull().unique(),
12491
+ slug: text("slug").notNull(),
12426
12492
  personality: text("personality").notNull(),
12427
12493
  model: text("model").default("claude-sonnet-4-6-20260217"),
12428
12494
  llmKeyId: uuid("llm_key_id").references(() => entityLlmKeys.id, { onDelete: "set null" }),
@@ -12472,7 +12538,17 @@ var init_agents = __esm({
12472
12538
  "agents_orchestrator_mode_check",
12473
12539
  sql`${table.orchestratorMode} IN ('router', 'planner') OR ${table.orchestratorMode} IS NULL`
12474
12540
  ),
12475
- 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()
12476
12552
  ]
12477
12553
  );
12478
12554
  agentAssignments = pgTable(
@@ -12485,7 +12561,18 @@ var init_agents = __esm({
12485
12561
  instructions: text("instructions"),
12486
12562
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
12487
12563
  },
12488
- () => []
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
+ ]
12489
12576
  );
12490
12577
  agentBudgets = pgTable(
12491
12578
  "agent_budgets",
@@ -12614,8 +12701,19 @@ var init_jobs = __esm({
12614
12701
  sql`${table.createdAt} DESC`
12615
12702
  ),
12616
12703
  index("idx_agent_jobs_parent_job_id").on(table.parentJobId),
12617
- 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).
12618
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`),
12619
12717
  check(
12620
12718
  "agent_jobs_status_check",
12621
12719
  sql`${table.status} IN ('pending','processing','completed','failed','awaiting_approval','awaiting_delegation','cancelled')`
@@ -12678,6 +12776,10 @@ var init_tasks = __esm({
12678
12776
  index("idx_agent_tasks_orchestrator_status").on(table.orchestratorId, table.status),
12679
12777
  index("idx_agent_tasks_status").on(table.status),
12680
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),
12681
12783
  check(
12682
12784
  "agent_tasks_status_check",
12683
12785
  sql`${table.status} IN ('todo','in_progress','done','cancelled','blocked')`
@@ -12862,7 +12964,16 @@ var init_approvals = __esm({
12862
12964
  check(
12863
12965
  "approval_rules_action_check",
12864
12966
  sql`${table.action} IN ('auto_approve','require_approval','block')`
12865
- )
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()
12866
12977
  ]
12867
12978
  );
12868
12979
  }
@@ -12994,8 +13105,8 @@ var init_skills = __esm({
12994
13105
  {
12995
13106
  id: uuid("id").primaryKey().defaultRandom(),
12996
13107
  entityId: uuid("entity_id").references(() => entities.id, { onDelete: "cascade" }),
12997
- name: text("name").notNull().unique(),
12998
- slug: text("slug").notNull().unique(),
13108
+ name: text("name").notNull(),
13109
+ slug: text("slug").notNull(),
12999
13110
  content: text("content").notNull(),
13000
13111
  active: boolean("active").default(true),
13001
13112
  description: text("description"),
@@ -13035,7 +13146,20 @@ var init_skills = __esm({
13035
13146
  (table) => [
13036
13147
  index("idx_agent_skills_entity_id").on(table.entityId),
13037
13148
  index("idx_skills_active").on(table.active, table.slug),
13038
- 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()
13039
13163
  ]
13040
13164
  );
13041
13165
  skillVersions = pgTable(
@@ -13092,7 +13216,14 @@ var init_skills = __esm({
13092
13216
  filesWritable: boolean("files_writable").notNull().default(false),
13093
13217
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
13094
13218
  },
13095
- () => []
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
+ ]
13096
13227
  );
13097
13228
  }
13098
13229
  });
@@ -13224,7 +13355,14 @@ var init_mcp = __esm({
13224
13355
  createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13225
13356
  updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13226
13357
  },
13227
- (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
+ ]
13228
13366
  );
13229
13367
  }
13230
13368
  });
@@ -13279,7 +13417,14 @@ var init_misc = __esm({
13279
13417
  check(
13280
13418
  "agent_plugins_hook_check",
13281
13419
  sql`${table.hook} IN ('pre_task','post_task','pre_tool','post_tool','on_memory_save')`
13282
- )
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()
13283
13428
  ]
13284
13429
  );
13285
13430
  rateLimits = pgTable("rate_limits", {
@@ -13451,16 +13596,65 @@ var init_chat_messages = __esm({
13451
13596
  });
13452
13597
 
13453
13598
  // ../../packages/db/src/schema/app-settings.ts
13454
- var appSettings;
13599
+ var appSettings, entitySettings;
13455
13600
  var init_app_settings = __esm({
13456
13601
  "../../packages/db/src/schema/app-settings.ts"() {
13457
13602
  "use strict";
13458
13603
  init_pg_core();
13604
+ init_entities();
13459
13605
  appSettings = pgTable("app_settings", {
13460
13606
  key: text("key").primaryKey(),
13461
13607
  value: text("value").default("").notNull(),
13462
13608
  updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull()
13463
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
+ );
13464
13658
  }
13465
13659
  });
13466
13660
 
@@ -13512,12 +13706,14 @@ __export(schema_exports, {
13512
13706
  entities: () => entities,
13513
13707
  entityLlmKeys: () => entityLlmKeys,
13514
13708
  entityMembers: () => entityMembers,
13709
+ entitySettings: () => entitySettings,
13515
13710
  mcpConnections: () => mcpConnections,
13516
13711
  mcpServers: () => mcpServers,
13517
13712
  rateLimits: () => rateLimits,
13518
13713
  sessions: () => sessions,
13519
13714
  skillConnectors: () => skillConnectors,
13520
13715
  skillVersions: () => skillVersions,
13716
+ telegramAllowedChats: () => telegramAllowedChats,
13521
13717
  toolCalls: () => toolCalls,
13522
13718
  userProfiles: () => userProfiles,
13523
13719
  users: () => users,
@@ -13549,6 +13745,7 @@ var init_schema2 = __esm({
13549
13745
  init_agent_workspaces();
13550
13746
  init_chat_messages();
13551
13747
  init_app_settings();
13748
+ init_telegram_allowed_chats();
13552
13749
  }
13553
13750
  });
13554
13751
 
@@ -13557,7 +13754,29 @@ function createClient(connectionString, options = {}) {
13557
13754
  const pool = src_default(connectionString, {
13558
13755
  max: options.max ?? 10,
13559
13756
  // Fail fast on connection issues rather than hanging
13560
- 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
+ }
13561
13780
  });
13562
13781
  const db = drizzle(pool, { schema: schema_exports });
13563
13782
  return {
@@ -13584,12 +13803,23 @@ var init_transaction = __esm({
13584
13803
 
13585
13804
  // ../../packages/secrets/src/index.ts
13586
13805
  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";
13806
+ import { chmodSync as chmodSync2, existsSync as existsSync5, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "fs";
13588
13807
  import { homedir as homedir2 } from "os";
13589
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;
13590
13819
  var init_src3 = __esm({
13591
13820
  "../../packages/secrets/src/index.ts"() {
13592
13821
  "use strict";
13822
+ VERSION = "v1";
13593
13823
  }
13594
13824
  });
13595
13825
 
@@ -13605,6 +13835,29 @@ var init_credentials2 = __esm({
13605
13835
  }
13606
13836
  });
13607
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
+
13608
13861
  // ../../packages/db/src/repos/agents.ts
13609
13862
  var init_agents2 = __esm({
13610
13863
  "../../packages/db/src/repos/agents.ts"() {
@@ -13648,6 +13901,7 @@ var init_src4 = __esm({
13648
13901
  init_transaction();
13649
13902
  init_schema2();
13650
13903
  init_credentials2();
13904
+ init_master_key_guard();
13651
13905
  init_agents2();
13652
13906
  init_skills2();
13653
13907
  init_retention();
@@ -13707,11 +13961,20 @@ var init_local_auth = __esm({
13707
13961
  }
13708
13962
  });
13709
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
+
13710
13972
  // ../../packages/auth/src/providers/bearer-token.ts
13711
13973
  var init_bearer_token = __esm({
13712
13974
  "../../packages/auth/src/providers/bearer-token.ts"() {
13713
13975
  "use strict";
13714
13976
  init_local_trust();
13977
+ init_constant_time();
13715
13978
  }
13716
13979
  });
13717
13980
 
@@ -13750,6 +14013,7 @@ var init_seed = __esm({
13750
14013
  });
13751
14014
 
13752
14015
  // src/lib/env.ts
14016
+ import { randomBytes as randomBytes3 } from "crypto";
13753
14017
  function buildEnvForRunner(config, databaseUrl) {
13754
14018
  const bind = config.bind === "loopback" ? "127.0.0.1" : "0.0.0.0";
13755
14019
  const env = {
@@ -13773,7 +14037,19 @@ function buildEnvForRunner(config, databaseUrl) {
13773
14037
  return env;
13774
14038
  }
13775
14039
  function resolveAuthMode(config) {
13776
- 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
+ }
13777
14053
  return config.bind === "lan" ? "local-auth" : "local-trust";
13778
14054
  }
13779
14055
  function buildEnvForWeb(config, databaseUrl) {
@@ -13797,8 +14073,16 @@ function buildEnvForWeb(config, databaseUrl) {
13797
14073
  // The web itself doesn't bind on this — Next.js listens on 0.0.0.0 anyway.
13798
14074
  BIND: bind,
13799
14075
  NODE_ENV: "production",
13800
- // AUTH_SECRET is required by better-auth in local-auth mode; harmless in local-trust.
13801
- 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"),
13802
14086
  // WORKER_SECRET — same value as the runner — so sendTaskAction can sign
13803
14087
  // the POST /api/worker call. Without this the runner returns 403 and the
13804
14088
  // job stays pending forever (cron only scans task-board, not API jobs).
@@ -14081,7 +14365,7 @@ __export(up_exports, {
14081
14365
  import chalk from "chalk";
14082
14366
  import ora from "ora";
14083
14367
  import open from "open";
14084
- import { randomBytes as randomBytes3 } from "crypto";
14368
+ import { randomBytes as randomBytes4 } from "crypto";
14085
14369
  async function killSilent(child) {
14086
14370
  await killProcessTree(child);
14087
14371
  }
@@ -14106,8 +14390,12 @@ async function runUp(opts = {}) {
14106
14390
  const defaultConfig = {
14107
14391
  // No `llm` section: the browser onboarding sets it via createLlmKeyAction.
14108
14392
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14109
- workerSecret: randomBytes3(32).toString("hex"),
14110
- 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"),
14111
14399
  // loopback derives auth: local-trust (no login/signup). Leave `auth`
14112
14400
  // unset so resolveAuthMode falls back to the loopback → local-trust map.
14113
14401
  bind: "loopback"
@@ -14115,6 +14403,7 @@ async function runUp(opts = {}) {
14115
14403
  writeConfig(defaultConfig);
14116
14404
  config = defaultConfig;
14117
14405
  }
14406
+ resolveAuthMode(config);
14118
14407
  const orphans = [];
14119
14408
  for (const [name, port] of [
14120
14409
  ["web", config.ports.web],
@@ -14220,6 +14509,17 @@ async function runUp(opts = {}) {
14220
14509
  await pg.stop();
14221
14510
  throw err;
14222
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
+ }
14223
14523
  const authMode = resolveAuthMode(config);
14224
14524
  if (authMode === "local-trust") {
14225
14525
  const seedSpinner = ora("Seeding default user and agent\u2026").start();
@@ -14269,13 +14569,13 @@ async function runUp(opts = {}) {
14269
14569
  }
14270
14570
  void (async () => {
14271
14571
  try {
14272
- 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));
14273
14573
  const installed = getInstalledVersion2();
14274
14574
  const latest = await Promise.race([
14275
14575
  getLatestVersion2(),
14276
14576
  new Promise((resolve2) => setTimeout(() => resolve2(null), 3e3))
14277
14577
  ]);
14278
- if (latest !== null && latest !== installed) {
14578
+ if (latest !== null && isNewerVersion2(latest, installed)) {
14279
14579
  console.log(chalk.cyan(` \u2139 v${latest} available \u2014 run \`nodal-agents update\``));
14280
14580
  }
14281
14581
  } catch {
@@ -14431,9 +14731,9 @@ __export(init_exports, {
14431
14731
  import chalk2 from "chalk";
14432
14732
  import prompts from "prompts";
14433
14733
  import ora2 from "ora";
14434
- import { randomBytes as randomBytes4 } from "crypto";
14734
+ import { randomBytes as randomBytes5 } from "crypto";
14435
14735
  function generateSecret(bytes = 32) {
14436
- return randomBytes4(bytes).toString("hex");
14736
+ return randomBytes5(bytes).toString("hex");
14437
14737
  }
14438
14738
  async function runInit(options = {}) {
14439
14739
  const existing = readConfig();
@@ -14454,7 +14754,7 @@ async function runInit(options = {}) {
14454
14754
  const config2 = {
14455
14755
  ports: { web: 3e3, runner: 3001, postgres: 25432 },
14456
14756
  workerSecret: generateSecret(32),
14457
- serverActionsKey: randomBytes4(32).toString("base64"),
14757
+ serverActionsKey: randomBytes5(32).toString("base64"),
14458
14758
  bind: "lan",
14459
14759
  auth: { mode: "local-auth" }
14460
14760
  };