@agent-native/core 0.8.1 → 0.9.1

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 (181) hide show
  1. package/dist/agent/run-manager.d.ts +10 -0
  2. package/dist/agent/run-manager.d.ts.map +1 -1
  3. package/dist/agent/run-manager.js +77 -4
  4. package/dist/agent/run-manager.js.map +1 -1
  5. package/dist/agent/run-store.d.ts +4 -1
  6. package/dist/agent/run-store.d.ts.map +1 -1
  7. package/dist/agent/run-store.js +6 -5
  8. package/dist/agent/run-store.js.map +1 -1
  9. package/dist/cli/create.d.ts +26 -1
  10. package/dist/cli/create.d.ts.map +1 -1
  11. package/dist/cli/create.js +62 -18
  12. package/dist/cli/create.js.map +1 -1
  13. package/dist/cli/index.js +177 -22
  14. package/dist/cli/index.js.map +1 -1
  15. package/dist/cli/workspace-dev.js +66 -5
  16. package/dist/cli/workspace-dev.js.map +1 -1
  17. package/dist/client/AgentPanel.d.ts +2 -0
  18. package/dist/client/AgentPanel.d.ts.map +1 -1
  19. package/dist/client/AgentPanel.js +4 -3
  20. package/dist/client/AgentPanel.js.map +1 -1
  21. package/dist/client/AssistantChat.d.ts.map +1 -1
  22. package/dist/client/AssistantChat.js +38 -84
  23. package/dist/client/AssistantChat.js.map +1 -1
  24. package/dist/client/agent-chat-adapter.d.ts.map +1 -1
  25. package/dist/client/agent-chat-adapter.js +122 -15
  26. package/dist/client/agent-chat-adapter.js.map +1 -1
  27. package/dist/client/analytics.d.ts +14 -0
  28. package/dist/client/analytics.d.ts.map +1 -1
  29. package/dist/client/analytics.js +34 -0
  30. package/dist/client/analytics.js.map +1 -1
  31. package/dist/client/components/PresenceBar.d.ts.map +1 -1
  32. package/dist/client/components/PresenceBar.js +21 -15
  33. package/dist/client/components/PresenceBar.js.map +1 -1
  34. package/dist/client/composer/ComposerPlusMenu.d.ts.map +1 -1
  35. package/dist/client/composer/ComposerPlusMenu.js +12 -11
  36. package/dist/client/composer/ComposerPlusMenu.js.map +1 -1
  37. package/dist/client/composer/TiptapComposer.d.ts.map +1 -1
  38. package/dist/client/composer/TiptapComposer.js +5 -4
  39. package/dist/client/composer/TiptapComposer.js.map +1 -1
  40. package/dist/client/composer/VoiceButton.d.ts.map +1 -1
  41. package/dist/client/composer/VoiceButton.js +9 -8
  42. package/dist/client/composer/VoiceButton.js.map +1 -1
  43. package/dist/client/dev-overlay/DevOverlay.d.ts.map +1 -1
  44. package/dist/client/dev-overlay/DevOverlay.js +4 -3
  45. package/dist/client/dev-overlay/DevOverlay.js.map +1 -1
  46. package/dist/client/extensions/EmbeddedExtension.d.ts.map +1 -1
  47. package/dist/client/extensions/EmbeddedExtension.js +2 -1
  48. package/dist/client/extensions/EmbeddedExtension.js.map +1 -1
  49. package/dist/client/extensions/ExtensionEditor.d.ts.map +1 -1
  50. package/dist/client/extensions/ExtensionEditor.js +2 -1
  51. package/dist/client/extensions/ExtensionEditor.js.map +1 -1
  52. package/dist/client/extensions/ExtensionSlot.d.ts.map +1 -1
  53. package/dist/client/extensions/ExtensionSlot.js +2 -1
  54. package/dist/client/extensions/ExtensionSlot.js.map +1 -1
  55. package/dist/client/extensions/ExtensionViewer.d.ts.map +1 -1
  56. package/dist/client/extensions/ExtensionViewer.js +4 -3
  57. package/dist/client/extensions/ExtensionViewer.js.map +1 -1
  58. package/dist/client/extensions/ExtensionsSidebarSection.d.ts.map +1 -1
  59. package/dist/client/extensions/ExtensionsSidebarSection.js +10 -9
  60. package/dist/client/extensions/ExtensionsSidebarSection.js.map +1 -1
  61. package/dist/client/index.d.ts +3 -1
  62. package/dist/client/index.d.ts.map +1 -1
  63. package/dist/client/index.js +3 -1
  64. package/dist/client/index.js.map +1 -1
  65. package/dist/client/integrations/IntegrationCard.d.ts.map +1 -1
  66. package/dist/client/integrations/IntegrationCard.js +2 -1
  67. package/dist/client/integrations/IntegrationCard.js.map +1 -1
  68. package/dist/client/integrations/IntegrationsPanel.d.ts.map +1 -1
  69. package/dist/client/integrations/IntegrationsPanel.js +3 -2
  70. package/dist/client/integrations/IntegrationsPanel.js.map +1 -1
  71. package/dist/client/onboarding/OnboardingPanel.d.ts.map +1 -1
  72. package/dist/client/onboarding/OnboardingPanel.js +3 -2
  73. package/dist/client/onboarding/OnboardingPanel.js.map +1 -1
  74. package/dist/client/onboarding/SetupButton.d.ts.map +1 -1
  75. package/dist/client/onboarding/SetupButton.js +14 -13
  76. package/dist/client/onboarding/SetupButton.js.map +1 -1
  77. package/dist/client/org/InvitationBanner.d.ts +8 -2
  78. package/dist/client/org/InvitationBanner.d.ts.map +1 -1
  79. package/dist/client/org/InvitationBanner.js +27 -6
  80. package/dist/client/org/InvitationBanner.js.map +1 -1
  81. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  82. package/dist/client/org/OrgSwitcher.js +29 -5
  83. package/dist/client/org/OrgSwitcher.js.map +1 -1
  84. package/dist/client/org/TeamPage.d.ts.map +1 -1
  85. package/dist/client/org/TeamPage.js +7 -6
  86. package/dist/client/org/TeamPage.js.map +1 -1
  87. package/dist/client/resources/ResourceEditor.d.ts.map +1 -1
  88. package/dist/client/resources/ResourceEditor.js +2 -1
  89. package/dist/client/resources/ResourceEditor.js.map +1 -1
  90. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  91. package/dist/client/resources/ResourcesPanel.js +9 -9
  92. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  93. package/dist/client/settings/AgentsSection.d.ts.map +1 -1
  94. package/dist/client/settings/AgentsSection.js +8 -7
  95. package/dist/client/settings/AgentsSection.js.map +1 -1
  96. package/dist/client/settings/AutomationsSection.d.ts.map +1 -1
  97. package/dist/client/settings/AutomationsSection.js +4 -3
  98. package/dist/client/settings/AutomationsSection.js.map +1 -1
  99. package/dist/client/settings/SecretsSection.d.ts.map +1 -1
  100. package/dist/client/settings/SecretsSection.js +2 -1
  101. package/dist/client/settings/SecretsSection.js.map +1 -1
  102. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  103. package/dist/client/settings/SettingsPanel.js +3 -2
  104. package/dist/client/settings/SettingsPanel.js.map +1 -1
  105. package/dist/client/settings/index.d.ts +1 -1
  106. package/dist/client/settings/index.d.ts.map +1 -1
  107. package/dist/client/settings/index.js.map +1 -1
  108. package/dist/client/sse-event-processor.d.ts.map +1 -1
  109. package/dist/client/sse-event-processor.js +45 -4
  110. package/dist/client/sse-event-processor.js.map +1 -1
  111. package/dist/client/theme.d.ts +4 -0
  112. package/dist/client/theme.d.ts.map +1 -0
  113. package/dist/client/theme.js +13 -0
  114. package/dist/client/theme.js.map +1 -0
  115. package/dist/client/use-session.d.ts.map +1 -1
  116. package/dist/client/use-session.js +14 -2
  117. package/dist/client/use-session.js.map +1 -1
  118. package/dist/collab/client.d.ts +1 -0
  119. package/dist/collab/client.d.ts.map +1 -1
  120. package/dist/collab/client.js +18 -1
  121. package/dist/collab/client.js.map +1 -1
  122. package/dist/org/auto-join-domain.d.ts +28 -0
  123. package/dist/org/auto-join-domain.d.ts.map +1 -0
  124. package/dist/org/auto-join-domain.js +92 -0
  125. package/dist/org/auto-join-domain.js.map +1 -0
  126. package/dist/org/index.d.ts +2 -0
  127. package/dist/org/index.d.ts.map +1 -1
  128. package/dist/org/index.js +1 -0
  129. package/dist/org/index.js.map +1 -1
  130. package/dist/scripts/db/exec.d.ts.map +1 -1
  131. package/dist/scripts/db/exec.js +27 -1
  132. package/dist/scripts/db/exec.js.map +1 -1
  133. package/dist/scripts/db/index.d.ts.map +1 -1
  134. package/dist/scripts/db/index.js +1 -0
  135. package/dist/scripts/db/index.js.map +1 -1
  136. package/dist/scripts/db/reset-dev-owner.d.ts +27 -0
  137. package/dist/scripts/db/reset-dev-owner.d.ts.map +1 -0
  138. package/dist/scripts/db/reset-dev-owner.js +225 -0
  139. package/dist/scripts/db/reset-dev-owner.js.map +1 -0
  140. package/dist/scripts/db/scoping.d.ts.map +1 -1
  141. package/dist/scripts/db/scoping.js +15 -30
  142. package/dist/scripts/db/scoping.js.map +1 -1
  143. package/dist/scripts/dev-session.d.ts +46 -0
  144. package/dist/scripts/dev-session.d.ts.map +1 -0
  145. package/dist/scripts/dev-session.js +81 -0
  146. package/dist/scripts/dev-session.js.map +1 -0
  147. package/dist/scripts/runner.d.ts.map +1 -1
  148. package/dist/scripts/runner.js +21 -0
  149. package/dist/scripts/runner.js.map +1 -1
  150. package/dist/secrets/register.d.ts +1 -1
  151. package/dist/secrets/register.d.ts.map +1 -1
  152. package/dist/secrets/register.js +4 -2
  153. package/dist/secrets/register.js.map +1 -1
  154. package/dist/secrets/routes.d.ts.map +1 -1
  155. package/dist/secrets/routes.js +32 -0
  156. package/dist/secrets/routes.js.map +1 -1
  157. package/dist/server/better-auth-instance.d.ts.map +1 -1
  158. package/dist/server/better-auth-instance.js +11 -0
  159. package/dist/server/better-auth-instance.js.map +1 -1
  160. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  161. package/dist/server/core-routes-plugin.js +56 -13
  162. package/dist/server/core-routes-plugin.js.map +1 -1
  163. package/dist/server/credential-provider.d.ts +47 -4
  164. package/dist/server/credential-provider.d.ts.map +1 -1
  165. package/dist/server/credential-provider.js +105 -29
  166. package/dist/server/credential-provider.js.map +1 -1
  167. package/dist/server/design-token-utils.d.ts +13 -2
  168. package/dist/server/design-token-utils.d.ts.map +1 -1
  169. package/dist/server/design-token-utils.js +48 -16
  170. package/dist/server/design-token-utils.js.map +1 -1
  171. package/dist/server/onboarding-html.d.ts.map +1 -1
  172. package/dist/server/onboarding-html.js +97 -8
  173. package/dist/server/onboarding-html.js.map +1 -1
  174. package/dist/templates/default/app/root.tsx +4 -0
  175. package/dist/templates/default/app/routes/_index.tsx +3 -2
  176. package/dist/vite/client.d.ts.map +1 -1
  177. package/dist/vite/client.js +76 -34
  178. package/dist/vite/client.js.map +1 -1
  179. package/package.json +1 -1
  180. package/src/templates/default/app/root.tsx +4 -0
  181. package/src/templates/default/app/routes/_index.tsx +3 -2
@@ -0,0 +1,225 @@
1
+ /**
2
+ * Core script: db-reset-dev-owner
3
+ *
4
+ * One-shot fix for local DBs that accumulated rows owned by the dev
5
+ * sentinel `local@localhost`. Pre-changes-53, db-exec / db-query /
6
+ * db-patch silently fell back to that owner when no real identity was
7
+ * present, so any data created via CLI runs (or by older versions of
8
+ * the runner) landed under the sentinel and is now invisible to the
9
+ * actual signed-in user.
10
+ *
11
+ * This script discovers every ownable table (those with an
12
+ * `owner_email` column), then re-points each `local@localhost` row to
13
+ * the email passed via `--to`. Optionally restricted to a single table
14
+ * with `--table`.
15
+ *
16
+ * Local-dev-only safety: refuses to run when `NODE_ENV=production` or
17
+ * when targeting a non-`file:` SQLite URL (no Postgres / Turso /
18
+ * shared-DB writes).
19
+ *
20
+ * Usage:
21
+ * pnpm action db-reset-dev-owner --to matthew@builder.io
22
+ * pnpm action db-reset-dev-owner --to matthew@builder.io --dry-run
23
+ * pnpm action db-reset-dev-owner --to matthew@builder.io --table decks
24
+ * pnpm action db-reset-dev-owner --to matthew@builder.io --db ./data/app.db
25
+ */
26
+ import path from "path";
27
+ import { createClient } from "@libsql/client";
28
+ import { getDatabaseUrl, getDatabaseAuthToken } from "../../db/client.js";
29
+ import { parseArgs } from "../utils.js";
30
+ const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — script intentionally targets these rows
31
+ function isPostgresUrl(url) {
32
+ return url.startsWith("postgres://") || url.startsWith("postgresql://");
33
+ }
34
+ function parseScriptArgs(args) {
35
+ const parsed = parseArgs(args);
36
+ if (parsed.help === "true")
37
+ return null;
38
+ const to = parsed.to?.trim();
39
+ if (!to || !to.includes("@")) {
40
+ console.error("Error: --to <email> is required and must look like an email address.");
41
+ return null;
42
+ }
43
+ if (to === DEV_FALLBACK_EMAIL) {
44
+ console.error(`Error: --to cannot be ${DEV_FALLBACK_EMAIL} (that's the sentinel we're fixing).`);
45
+ return null;
46
+ }
47
+ return {
48
+ to,
49
+ table: parsed.table?.trim() || undefined,
50
+ dryRun: parsed["dry-run"] === "true",
51
+ dbPath: parsed.db?.trim() || undefined,
52
+ };
53
+ }
54
+ function printHelp() {
55
+ console.log(`Usage: pnpm action db-reset-dev-owner --to <email> [options]
56
+
57
+ Reassigns rows owned by '${DEV_FALLBACK_EMAIL}' to the given email across
58
+ every table that has an 'owner_email' column. Use this once when an old
59
+ local DB still has rows that the new (post-changes-53) scoping won't show
60
+ to the actual signed-in user.
61
+
62
+ Required:
63
+ --to <email> Target email — usually the address you sign in with locally
64
+
65
+ Options:
66
+ --table <name> Only reset one table (default: every ownable table)
67
+ --dry-run Print what would change without writing
68
+ --db <path> SQLite database path (default: DATABASE_URL or ./data/app.db)
69
+ --help Show this help message
70
+
71
+ Refuses to run when NODE_ENV=production or against a non-local DB URL.`);
72
+ }
73
+ export default async function dbResetDevOwner(args) {
74
+ if (args.includes("--help") || args.length === 0) {
75
+ printHelp();
76
+ return;
77
+ }
78
+ const parsed = parseScriptArgs(args);
79
+ if (!parsed) {
80
+ // parseScriptArgs already printed the error; exit non-zero.
81
+ throw new Error("invalid arguments");
82
+ }
83
+ if (process.env.NODE_ENV === "production") {
84
+ console.error("Error: refusing to run db-reset-dev-owner with NODE_ENV=production.");
85
+ process.exit(1);
86
+ }
87
+ // Resolve target DB URL — same precedence as wipe-leaked-builder-keys.
88
+ let url;
89
+ if (parsed.dbPath) {
90
+ url = "file:" + path.resolve(parsed.dbPath);
91
+ }
92
+ else if (getDatabaseUrl()) {
93
+ url = getDatabaseUrl();
94
+ }
95
+ else {
96
+ url = "file:" + path.resolve(process.cwd(), "data", "app.db");
97
+ }
98
+ const isPostgres = isPostgresUrl(url);
99
+ const isLocalSqlite = url.startsWith("file:");
100
+ if (!isPostgres && !isLocalSqlite) {
101
+ console.error(`Error: refusing to run against shared DB URL ${url}. ` +
102
+ "This script is only for local SQLite files.");
103
+ process.exit(1);
104
+ }
105
+ if (isPostgres && process.env.AN_ALLOW_PG_DEV_OWNER_RESET !== "1") {
106
+ console.error("Error: refusing to run against a Postgres DB. Set " +
107
+ "AN_ALLOW_PG_DEV_OWNER_RESET=1 to override (only do this on a " +
108
+ "local Postgres you fully own — never on Neon/prod).");
109
+ process.exit(1);
110
+ }
111
+ const dbLabel = isLocalSqlite
112
+ ? url.slice("file:".length)
113
+ : (() => {
114
+ try {
115
+ return new URL(url).host || url;
116
+ }
117
+ catch {
118
+ return url;
119
+ }
120
+ })();
121
+ console.log(`[reset-dev-owner] target: ${dbLabel}` +
122
+ `${parsed.dryRun ? " (dry-run)" : ""}`);
123
+ console.log(`[reset-dev-owner] reassigning '${DEV_FALLBACK_EMAIL}' → '${parsed.to}'`);
124
+ if (isPostgres) {
125
+ await runPostgres(url, parsed);
126
+ }
127
+ else {
128
+ await runSqlite(url, parsed);
129
+ }
130
+ }
131
+ async function runSqlite(url, args) {
132
+ const client = createClient({ url, authToken: getDatabaseAuthToken() });
133
+ try {
134
+ const tables = args.table
135
+ ? [args.table]
136
+ : await discoverSqliteOwnerTables(client);
137
+ if (tables.length === 0) {
138
+ console.log("[reset-dev-owner] no tables with owner_email column — nothing to do.");
139
+ return;
140
+ }
141
+ let totalUpdated = 0;
142
+ for (const table of tables) {
143
+ const escaped = table.replace(/"/g, '""');
144
+ const countRes = await client.execute({
145
+ sql: `SELECT COUNT(*) AS c FROM "${escaped}" WHERE owner_email = ?`,
146
+ args: [DEV_FALLBACK_EMAIL],
147
+ });
148
+ const count = Number(countRes.rows[0]?.c ?? 0);
149
+ if (count === 0) {
150
+ console.log(` ${table}: 0 rows`);
151
+ continue;
152
+ }
153
+ console.log(` ${table}: ${count} row(s)${args.dryRun ? " (dry-run)" : ""}`);
154
+ if (args.dryRun)
155
+ continue;
156
+ const updateRes = await client.execute({
157
+ sql: `UPDATE "${escaped}" SET owner_email = ? WHERE owner_email = ?`,
158
+ args: [args.to, DEV_FALLBACK_EMAIL],
159
+ });
160
+ totalUpdated += updateRes.rowsAffected;
161
+ }
162
+ console.log(args.dryRun
163
+ ? `[reset-dev-owner] dry-run complete.`
164
+ : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`);
165
+ }
166
+ finally {
167
+ client.close();
168
+ }
169
+ }
170
+ async function runPostgres(url, args) {
171
+ const { default: pg } = await import("postgres");
172
+ const sql = pg(url);
173
+ try {
174
+ const tables = args.table
175
+ ? [args.table]
176
+ : await discoverPostgresOwnerTables(sql);
177
+ if (tables.length === 0) {
178
+ console.log("[reset-dev-owner] no tables with owner_email column — nothing to do.");
179
+ return;
180
+ }
181
+ let totalUpdated = 0;
182
+ for (const table of tables) {
183
+ const countRes = (await sql.unsafe(`SELECT COUNT(*)::int AS c FROM "${table.replace(/"/g, '""')}" WHERE owner_email = $1`, [DEV_FALLBACK_EMAIL]));
184
+ const count = countRes[0]?.c ?? 0;
185
+ if (count === 0) {
186
+ console.log(` ${table}: 0 rows`);
187
+ continue;
188
+ }
189
+ console.log(` ${table}: ${count} row(s)${args.dryRun ? " (dry-run)" : ""}`);
190
+ if (args.dryRun)
191
+ continue;
192
+ const updateRes = (await sql.unsafe(`UPDATE "${table.replace(/"/g, '""')}" SET owner_email = $1 WHERE owner_email = $2`, [args.to, DEV_FALLBACK_EMAIL]));
193
+ totalUpdated += updateRes.count ?? 0;
194
+ }
195
+ console.log(args.dryRun
196
+ ? `[reset-dev-owner] dry-run complete.`
197
+ : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`);
198
+ }
199
+ finally {
200
+ await sql.end();
201
+ }
202
+ }
203
+ async function discoverSqliteOwnerTables(client) {
204
+ const tablesRes = await client.execute(`SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`);
205
+ const out = [];
206
+ for (const row of tablesRes.rows) {
207
+ const table = (row.name ?? row[0]);
208
+ const escaped = table.replace(/"/g, '""');
209
+ const colsRes = await client.execute(`PRAGMA table_info("${escaped}")`);
210
+ const hasOwner = colsRes.rows.some((r) => (r.name ?? r[1]) === "owner_email");
211
+ if (hasOwner)
212
+ out.push(table);
213
+ }
214
+ return out;
215
+ }
216
+ async function discoverPostgresOwnerTables(sql) {
217
+ const rows = (await sql `
218
+ SELECT table_name
219
+ FROM information_schema.columns
220
+ WHERE table_schema = 'public' AND column_name = 'owner_email'
221
+ ORDER BY table_name
222
+ `);
223
+ return Array.from(rows).map((r) => r.table_name);
224
+ }
225
+ //# sourceMappingURL=reset-dev-owner.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"reset-dev-owner.js","sourceRoot":"","sources":["../../../src/scripts/db/reset-dev-owner.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Core script: db-reset-dev-owner\n *\n * One-shot fix for local DBs that accumulated rows owned by the dev\n * sentinel `local@localhost`. Pre-changes-53, db-exec / db-query /\n * db-patch silently fell back to that owner when no real identity was\n * present, so any data created via CLI runs (or by older versions of\n * the runner) landed under the sentinel and is now invisible to the\n * actual signed-in user.\n *\n * This script discovers every ownable table (those with an\n * `owner_email` column), then re-points each `local@localhost` row to\n * the email passed via `--to`. Optionally restricted to a single table\n * with `--table`.\n *\n * Local-dev-only safety: refuses to run when `NODE_ENV=production` or\n * when targeting a non-`file:` SQLite URL (no Postgres / Turso /\n * shared-DB writes).\n *\n * Usage:\n * pnpm action db-reset-dev-owner --to matthew@builder.io\n * pnpm action db-reset-dev-owner --to matthew@builder.io --dry-run\n * pnpm action db-reset-dev-owner --to matthew@builder.io --table decks\n * pnpm action db-reset-dev-owner --to matthew@builder.io --db ./data/app.db\n */\n\nimport path from \"path\";\nimport { createClient } from \"@libsql/client\";\nimport { getDatabaseUrl, getDatabaseAuthToken } from \"../../db/client.js\";\nimport { parseArgs } from \"../utils.js\";\n\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — script intentionally targets these rows\n\nfunction isPostgresUrl(url: string): boolean {\n return url.startsWith(\"postgres://\") || url.startsWith(\"postgresql://\");\n}\n\ninterface Args {\n to: string;\n table?: string;\n dryRun: boolean;\n dbPath?: string;\n}\n\nfunction parseScriptArgs(args: string[]): Args | null {\n const parsed = parseArgs(args);\n if (parsed.help === \"true\") return null;\n\n const to = parsed.to?.trim();\n if (!to || !to.includes(\"@\")) {\n console.error(\n \"Error: --to <email> is required and must look like an email address.\",\n );\n return null;\n }\n if (to === DEV_FALLBACK_EMAIL) {\n console.error(\n `Error: --to cannot be ${DEV_FALLBACK_EMAIL} (that's the sentinel we're fixing).`,\n );\n return null;\n }\n\n return {\n to,\n table: parsed.table?.trim() || undefined,\n dryRun: parsed[\"dry-run\"] === \"true\",\n dbPath: parsed.db?.trim() || undefined,\n };\n}\n\nfunction printHelp(): void {\n console.log(`Usage: pnpm action db-reset-dev-owner --to <email> [options]\n\nReassigns rows owned by '${DEV_FALLBACK_EMAIL}' to the given email across\nevery table that has an 'owner_email' column. Use this once when an old\nlocal DB still has rows that the new (post-changes-53) scoping won't show\nto the actual signed-in user.\n\nRequired:\n --to <email> Target email — usually the address you sign in with locally\n\nOptions:\n --table <name> Only reset one table (default: every ownable table)\n --dry-run Print what would change without writing\n --db <path> SQLite database path (default: DATABASE_URL or ./data/app.db)\n --help Show this help message\n\nRefuses to run when NODE_ENV=production or against a non-local DB URL.`);\n}\n\nexport default async function dbResetDevOwner(args: string[]): Promise<void> {\n if (args.includes(\"--help\") || args.length === 0) {\n printHelp();\n return;\n }\n\n const parsed = parseScriptArgs(args);\n if (!parsed) {\n // parseScriptArgs already printed the error; exit non-zero.\n throw new Error(\"invalid arguments\");\n }\n\n if (process.env.NODE_ENV === \"production\") {\n console.error(\n \"Error: refusing to run db-reset-dev-owner with NODE_ENV=production.\",\n );\n process.exit(1);\n }\n\n // Resolve target DB URL — same precedence as wipe-leaked-builder-keys.\n let url: string;\n if (parsed.dbPath) {\n url = \"file:\" + path.resolve(parsed.dbPath);\n } else if (getDatabaseUrl()) {\n url = getDatabaseUrl();\n } else {\n url = \"file:\" + path.resolve(process.cwd(), \"data\", \"app.db\");\n }\n\n const isPostgres = isPostgresUrl(url);\n const isLocalSqlite = url.startsWith(\"file:\");\n\n if (!isPostgres && !isLocalSqlite) {\n console.error(\n `Error: refusing to run against shared DB URL ${url}. ` +\n \"This script is only for local SQLite files.\",\n );\n process.exit(1);\n }\n if (isPostgres && process.env.AN_ALLOW_PG_DEV_OWNER_RESET !== \"1\") {\n console.error(\n \"Error: refusing to run against a Postgres DB. Set \" +\n \"AN_ALLOW_PG_DEV_OWNER_RESET=1 to override (only do this on a \" +\n \"local Postgres you fully own — never on Neon/prod).\",\n );\n process.exit(1);\n }\n\n const dbLabel = isLocalSqlite\n ? url.slice(\"file:\".length)\n : (() => {\n try {\n return new URL(url).host || url;\n } catch {\n return url;\n }\n })();\n\n console.log(\n `[reset-dev-owner] target: ${dbLabel}` +\n `${parsed.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n console.log(\n `[reset-dev-owner] reassigning '${DEV_FALLBACK_EMAIL}' → '${parsed.to}'`,\n );\n\n if (isPostgres) {\n await runPostgres(url, parsed);\n } else {\n await runSqlite(url, parsed);\n }\n}\n\nasync function runSqlite(url: string, args: Args): Promise<void> {\n const client = createClient({ url, authToken: getDatabaseAuthToken() });\n try {\n const tables = args.table\n ? [args.table]\n : await discoverSqliteOwnerTables(client);\n\n if (tables.length === 0) {\n console.log(\n \"[reset-dev-owner] no tables with owner_email column — nothing to do.\",\n );\n return;\n }\n\n let totalUpdated = 0;\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const countRes = await client.execute({\n sql: `SELECT COUNT(*) AS c FROM \"${escaped}\" WHERE owner_email = ?`,\n args: [DEV_FALLBACK_EMAIL],\n });\n const count = Number((countRes.rows[0] as any)?.c ?? 0);\n if (count === 0) {\n console.log(` ${table}: 0 rows`);\n continue;\n }\n console.log(\n ` ${table}: ${count} row(s)${args.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n if (args.dryRun) continue;\n const updateRes = await client.execute({\n sql: `UPDATE \"${escaped}\" SET owner_email = ? WHERE owner_email = ?`,\n args: [args.to, DEV_FALLBACK_EMAIL],\n });\n totalUpdated += updateRes.rowsAffected;\n }\n\n console.log(\n args.dryRun\n ? `[reset-dev-owner] dry-run complete.`\n : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`,\n );\n } finally {\n client.close();\n }\n}\n\nasync function runPostgres(url: string, args: Args): Promise<void> {\n const { default: pg } = await import(\"postgres\");\n const sql = pg(url);\n try {\n const tables = args.table\n ? [args.table]\n : await discoverPostgresOwnerTables(sql);\n\n if (tables.length === 0) {\n console.log(\n \"[reset-dev-owner] no tables with owner_email column — nothing to do.\",\n );\n return;\n }\n\n let totalUpdated = 0;\n for (const table of tables) {\n const countRes = (await sql.unsafe(\n `SELECT COUNT(*)::int AS c FROM \"${table.replace(/\"/g, '\"\"')}\" WHERE owner_email = $1`,\n [DEV_FALLBACK_EMAIL],\n )) as unknown as Array<{ c: number }>;\n const count = countRes[0]?.c ?? 0;\n if (count === 0) {\n console.log(` ${table}: 0 rows`);\n continue;\n }\n console.log(\n ` ${table}: ${count} row(s)${args.dryRun ? \" (dry-run)\" : \"\"}`,\n );\n if (args.dryRun) continue;\n const updateRes = (await sql.unsafe(\n `UPDATE \"${table.replace(/\"/g, '\"\"')}\" SET owner_email = $1 WHERE owner_email = $2`,\n [args.to, DEV_FALLBACK_EMAIL],\n )) as unknown as { count?: number };\n totalUpdated += updateRes.count ?? 0;\n }\n\n console.log(\n args.dryRun\n ? `[reset-dev-owner] dry-run complete.`\n : `[reset-dev-owner] reassigned ${totalUpdated} row(s) across ${tables.length} table(s).`,\n );\n } finally {\n await sql.end();\n }\n}\n\nasync function discoverSqliteOwnerTables(client: any): Promise<string[]> {\n const tablesRes = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const out: string[] = [];\n for (const row of tablesRes.rows) {\n const table = (row.name ?? row[0]) as string;\n const escaped = table.replace(/\"/g, '\"\"');\n const colsRes = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n const hasOwner = colsRes.rows.some(\n (r: any) => (r.name ?? r[1]) === \"owner_email\",\n );\n if (hasOwner) out.push(table);\n }\n return out;\n}\n\nasync function discoverPostgresOwnerTables(sql: any): Promise<string[]> {\n const rows = (await sql`\n SELECT table_name\n FROM information_schema.columns\n WHERE table_schema = 'public' AND column_name = 'owner_email'\n ORDER BY table_name\n `) as unknown as Array<{ table_name: string }>;\n return Array.from(rows).map((r) => r.table_name);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAsLH,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,8DAA8D;IAC9D,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,GAAG,GACT,OAAO,CAAC,cAAc,CAAC,CA4CzB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC,CA2C7E"}
1
+ {"version":3,"file":"scoping.d.ts","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AA6LH,MAAM,WAAW,cAAc;IAC7B,yEAAyE;IACzE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,sEAAsE;IACtE,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,iCAAiC;IACjC,MAAM,EAAE,OAAO,CAAC;IAChB,gEAAgE;IAChE,SAAS,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,4DAA4D;IAC5D,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,mEAAmE;IACnE,gBAAgB,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;IAC9B,8DAA8D;IAC9D,WAAW,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,KAAK,EAAE,GAAG,GACT,OAAO,CAAC,cAAc,CAAC,CAmCzB;AAED;;;GAGG;AACH,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,GAAG,GAAG,OAAO,CAAC,cAAc,CAAC,CA8B7E"}
@@ -28,8 +28,15 @@ const ORG_COLUMN = "org_id";
28
28
  const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant
29
29
  function getUserEmail() {
30
30
  const userEmail = getRequestUserEmail() || null;
31
- if (userEmail === DEV_FALLBACK_EMAIL) {
32
- throw new Error("DB script scoping requires a real user identity; refusing to run with local@localhost.");
31
+ if (!userEmail || userEmail === DEV_FALLBACK_EMAIL) {
32
+ throw new Error("db-exec / db-query / db-patch require an authenticated user identity. " +
33
+ "Easiest fix: open the app at http://localhost:3000 and sign in — " +
34
+ "the CLI then auto-loads your session. Otherwise set " +
35
+ "AGENT_USER_EMAIL=<email> in the env, or invoke through an HTTP " +
36
+ "action that runs under runWithRequestContext. Refusing to run unscoped — " +
37
+ "an unscoped UPDATE/DELETE would touch every user's rows, and an " +
38
+ "unscoped INSERT would land with the dev sentinel owner and be invisible " +
39
+ "to the UI.");
33
40
  }
34
41
  return userEmail;
35
42
  }
@@ -149,21 +156,12 @@ function buildScopedTables(allColumns, userEmail, orgId, isPostgres) {
149
156
  * Returns setup/teardown SQL to run before/after the user's query.
150
157
  */
151
158
  export async function buildScopingPostgres(pgSql) {
152
- const inactive = {
153
- setup: [],
154
- teardown: [],
155
- active: false,
156
- userEmail: null,
157
- orgId: null,
158
- ownerEmailTables: new Set(),
159
- orgIdTables: new Set(),
160
- };
161
- // Scoping is always active when there is a request user (dev, preview, and
162
- // prod). Previously this short-circuited outside production, which created
163
- // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).
159
+ // getUserEmail() throws when there is no authenticated user (no request
160
+ // context AND no AGENT_USER_EMAIL env) or when it resolves to the dev
161
+ // sentinel `local@localhost`. We let that throw propagate: the script
162
+ // refuses to run unscoped rather than silently writing rows that the UI
163
+ // then can't see, or running an UPDATE/DELETE across every user's data.
164
164
  const userEmail = getUserEmail();
165
- if (!userEmail)
166
- return inactive;
167
165
  const orgId = getOrgId();
168
166
  const allColumns = await discoverColumnsPostgres(pgSql);
169
167
  const scoped = buildScopedTables(allColumns, userEmail, orgId, true);
@@ -197,21 +195,8 @@ export async function buildScopingPostgres(pgSql) {
197
195
  * Returns setup/teardown SQL to run before/after the user's query.
198
196
  */
199
197
  export async function buildScopingSqlite(client) {
200
- const inactive = {
201
- setup: [],
202
- teardown: [],
203
- active: false,
204
- userEmail: null,
205
- orgId: null,
206
- ownerEmailTables: new Set(),
207
- orgIdTables: new Set(),
208
- };
209
- // Scoping is always active when there is a request user (dev, preview, and
210
- // prod). Previously this short-circuited outside production, which created
211
- // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).
198
+ // See buildScopingPostgres: getUserEmail() throws on no user / dev sentinel.
212
199
  const userEmail = getUserEmail();
213
- if (!userEmail)
214
- return inactive;
215
200
  const orgId = getOrgId();
216
201
  const allColumns = await discoverColumnsSqlite(client);
217
202
  const scoped = buildScopedTables(allColumns, userEmail, orgId, false);
@@ -1 +1 @@
1
- {"version":3,"file":"scoping.js","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-user and per-org data scoping for db-query / db-exec.\n *\n * In production mode, creates temporary views that shadow real tables so\n * that raw SQL only sees the current user's (and org's) data.\n *\n * Convention:\n * - Template tables use an `owner_email` column for user scoping.\n * - Template tables use an `org_id` column for org scoping.\n * - Core tables have their own scoping patterns (key prefix, session_id, etc.).\n * - When both columns are present, both WHERE clauses are applied (AND).\n *\n * Temp views take precedence over real tables in both SQLite and Postgres,\n * so the user's SQL runs unmodified against the filtered views.\n */\n\n// Core tables with non-standard scoping (not owner_email).\n// Map of table name → { column, mode }.\nconst CORE_TABLE_SCOPING: Record<\n string,\n { column: string; mode: \"prefix\" | \"exact\" }\n> = {\n settings: { column: \"key\", mode: \"prefix\" }, // keys like u:<email>:<key>\n application_state: { column: \"session_id\", mode: \"exact\" },\n oauth_tokens: { column: \"owner\", mode: \"exact\" },\n sessions: { column: \"email\", mode: \"exact\" },\n};\n\n// The conventional column names for user/org ownership in template tables.\nimport {\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../../server/request-context.js\";\n\nconst OWNER_COLUMN = \"owner_email\";\nconst ORG_COLUMN = \"org_id\";\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant\n\ninterface ScopedTable {\n name: string;\n viewSql: string;\n}\n\nfunction getUserEmail(): string | null {\n const userEmail = getRequestUserEmail() || null;\n if (userEmail === DEV_FALLBACK_EMAIL) {\n throw new Error(\n \"DB script scoping requires a real user identity; refusing to run with local@localhost.\",\n );\n }\n return userEmail;\n}\n\nfunction getOrgId(): string | null {\n return getRequestOrgId() || null;\n}\n\n// ─── Schema introspection ───────────────────────────────────────────────────\n\ninterface TableColumn {\n table: string;\n column: string;\n}\n\nasync function discoverColumnsPostgres(pgSql: any): Promise<TableColumn[]> {\n const rows: any[] = await pgSql`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position\n `;\n return rows.map((r) => ({ table: r.table_name, column: r.column_name }));\n}\n\nasync function discoverColumnsSqlite(client: any): Promise<TableColumn[]> {\n const tablesResult = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const tables = tablesResult.rows.map((r: any) => (r.name ?? r[0]) as string);\n\n const result: TableColumn[] = [];\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const colsResult = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n for (const row of colsResult.rows) {\n result.push({\n table,\n column: (row.name ?? row[1]) as string,\n });\n }\n }\n return result;\n}\n\n// ─── View generation ────────────────────────────────────────────────────────\n\n/** Escape a string for safe inclusion in a SQL single-quoted literal. */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nfunction buildScopedTables(\n allColumns: TableColumn[],\n userEmail: string,\n orgId: string | null,\n isPostgres: boolean,\n): ScopedTable[] {\n // Group columns by table\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n\n const scoped: ScopedTable[] = [];\n const qualifiedPrefix = isPostgres ? \"public.\" : \"main.\";\n const safeEmail = escapeSqlString(userEmail);\n const safeOrgId = orgId ? escapeSqlString(orgId) : null;\n\n // WITH CHECK OPTION ensures INSERTs/UPDATEs through the auto-updatable view\n // can't write rows that violate the WHERE filter. Without it, an attacker\n // could `INSERT INTO recordings (..., owner_email) VALUES (..., 'victim@x')`\n // through the view and the row would land in the base table under the\n // victim's identity. SQLite views are not auto-updatable in the same way\n // (they require triggers), so this clause is a no-op there but harmless.\n const checkOption = isPostgres ? \" WITH LOCAL CHECK OPTION\" : \"\";\n\n for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n let whereSql: string;\n if (coreScoping.mode === \"prefix\") {\n // settings: key starts with u:<email>:\n // Escape \\, % and _ in the email so LIKE treats them literally.\n const likeEmail = safeEmail\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/%/g, \"\\\\%\")\n .replace(/_/g, \"\\\\_\");\n const prefix = `u:${likeEmail}:`;\n whereSql = `\"${coreScoping.column}\" LIKE '${prefix}%' ESCAPE '\\\\'`;\n } else {\n whereSql = `\"${coreScoping.column}\" = '${safeEmail}'`;\n }\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n });\n continue;\n }\n\n if (\n table === \"tool_data\" &&\n columns.includes(\"scope\") &&\n columns.includes(OWNER_COLUMN) &&\n columns.includes(ORG_COLUMN)\n ) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})${checkOption}`,\n });\n continue;\n }\n\n // Build WHERE clauses for owner_email and org_id\n const clauses: string[] = [];\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n clauses.push(`\"${OWNER_COLUMN}\" = '${safeEmail}'`);\n }\n if (hasOrg && safeOrgId) {\n clauses.push(`\"${ORG_COLUMN}\" = '${safeOrgId}'`);\n }\n\n if (clauses.length > 0) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${clauses.join(\" AND \")}${checkOption}`,\n });\n }\n }\n\n return scoped;\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────────\n\nexport interface ScopingContext {\n /** SQL statements to run before the user's query (create temp views). */\n setup: string[];\n /** SQL statements to run after the user's query (drop temp views). */\n teardown: string[];\n /** Whether scoping is active. */\n active: boolean;\n /** The current user email (for INSERT injection in db-exec). */\n userEmail: string | null;\n /** The current org ID (for INSERT injection in db-exec). */\n orgId: string | null;\n /** Tables that have owner_email columns (for INSERT injection). */\n ownerEmailTables: Set<string>;\n /** Tables that have org_id columns (for INSERT injection). */\n orgIdTables: Set<string>;\n}\n\n/**\n * Build scoping context for a Postgres connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingPostgres(\n pgSql: any,\n): Promise<ScopingContext> {\n const inactive: ScopingContext = {\n setup: [],\n teardown: [],\n active: false,\n userEmail: null,\n orgId: null,\n ownerEmailTables: new Set(),\n orgIdTables: new Set(),\n };\n\n // Scoping is always active when there is a request user (dev, preview, and\n // prod). Previously this short-circuited outside production, which created\n // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).\n const userEmail = getUserEmail();\n if (!userEmail) return inactive;\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsPostgres(pgSql);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, true);\n\n // Track which tables have owner_email / org_id for INSERT injection\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map((s) => `DROP VIEW IF EXISTS pg_temp.\"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n };\n}\n\n/**\n * Build scoping context for a SQLite/libsql connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingSqlite(client: any): Promise<ScopingContext> {\n const inactive: ScopingContext = {\n setup: [],\n teardown: [],\n active: false,\n userEmail: null,\n orgId: null,\n ownerEmailTables: new Set(),\n orgIdTables: new Set(),\n };\n\n // Scoping is always active when there is a request user (dev, preview, and\n // prod). Previously this short-circuited outside production, which created\n // a cross-user read in dev mode. See audit 05-tools-sandbox.md (C3.d).\n const userEmail = getUserEmail();\n if (!userEmail) return inactive;\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsSqlite(client);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, false);\n\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map((s) => `DROP VIEW IF EXISTS \"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n };\n}\n"]}
1
+ {"version":3,"file":"scoping.js","sourceRoot":"","sources":["../../../src/scripts/db/scoping.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Per-user and per-org data scoping for db-query / db-exec.\n *\n * In production mode, creates temporary views that shadow real tables so\n * that raw SQL only sees the current user's (and org's) data.\n *\n * Convention:\n * - Template tables use an `owner_email` column for user scoping.\n * - Template tables use an `org_id` column for org scoping.\n * - Core tables have their own scoping patterns (key prefix, session_id, etc.).\n * - When both columns are present, both WHERE clauses are applied (AND).\n *\n * Temp views take precedence over real tables in both SQLite and Postgres,\n * so the user's SQL runs unmodified against the filtered views.\n */\n\n// Core tables with non-standard scoping (not owner_email).\n// Map of table name → { column, mode }.\nconst CORE_TABLE_SCOPING: Record<\n string,\n { column: string; mode: \"prefix\" | \"exact\" }\n> = {\n settings: { column: \"key\", mode: \"prefix\" }, // keys like u:<email>:<key>\n application_state: { column: \"session_id\", mode: \"exact\" },\n oauth_tokens: { column: \"owner\", mode: \"exact\" },\n sessions: { column: \"email\", mode: \"exact\" },\n};\n\n// The conventional column names for user/org ownership in template tables.\nimport {\n getRequestUserEmail,\n getRequestOrgId,\n} from \"../../server/request-context.js\";\n\nconst OWNER_COLUMN = \"owner_email\";\nconst ORG_COLUMN = \"org_id\";\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel is rejected below so DB scripts cannot silently scope to the dev fallback tenant\n\ninterface ScopedTable {\n name: string;\n viewSql: string;\n}\n\nfunction getUserEmail(): string {\n const userEmail = getRequestUserEmail() || null;\n if (!userEmail || userEmail === DEV_FALLBACK_EMAIL) {\n throw new Error(\n \"db-exec / db-query / db-patch require an authenticated user identity. \" +\n \"Easiest fix: open the app at http://localhost:3000 and sign in — \" +\n \"the CLI then auto-loads your session. Otherwise set \" +\n \"AGENT_USER_EMAIL=<email> in the env, or invoke through an HTTP \" +\n \"action that runs under runWithRequestContext. Refusing to run unscoped — \" +\n \"an unscoped UPDATE/DELETE would touch every user's rows, and an \" +\n \"unscoped INSERT would land with the dev sentinel owner and be invisible \" +\n \"to the UI.\",\n );\n }\n return userEmail;\n}\n\nfunction getOrgId(): string | null {\n return getRequestOrgId() || null;\n}\n\n// ─── Schema introspection ───────────────────────────────────────────────────\n\ninterface TableColumn {\n table: string;\n column: string;\n}\n\nasync function discoverColumnsPostgres(pgSql: any): Promise<TableColumn[]> {\n const rows: any[] = await pgSql`\n SELECT table_name, column_name\n FROM information_schema.columns\n WHERE table_schema = 'public'\n ORDER BY table_name, ordinal_position\n `;\n return rows.map((r) => ({ table: r.table_name, column: r.column_name }));\n}\n\nasync function discoverColumnsSqlite(client: any): Promise<TableColumn[]> {\n const tablesResult = await client.execute(\n `SELECT name FROM sqlite_master WHERE type='table' AND name NOT LIKE 'sqlite_%'`,\n );\n const tables = tablesResult.rows.map((r: any) => (r.name ?? r[0]) as string);\n\n const result: TableColumn[] = [];\n for (const table of tables) {\n const escaped = table.replace(/\"/g, '\"\"');\n const colsResult = await client.execute(`PRAGMA table_info(\"${escaped}\")`);\n for (const row of colsResult.rows) {\n result.push({\n table,\n column: (row.name ?? row[1]) as string,\n });\n }\n }\n return result;\n}\n\n// ─── View generation ────────────────────────────────────────────────────────\n\n/** Escape a string for safe inclusion in a SQL single-quoted literal. */\nfunction escapeSqlString(value: string): string {\n return value.replace(/'/g, \"''\");\n}\n\nfunction buildScopedTables(\n allColumns: TableColumn[],\n userEmail: string,\n orgId: string | null,\n isPostgres: boolean,\n): ScopedTable[] {\n // Group columns by table\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n\n const scoped: ScopedTable[] = [];\n const qualifiedPrefix = isPostgres ? \"public.\" : \"main.\";\n const safeEmail = escapeSqlString(userEmail);\n const safeOrgId = orgId ? escapeSqlString(orgId) : null;\n\n // WITH CHECK OPTION ensures INSERTs/UPDATEs through the auto-updatable view\n // can't write rows that violate the WHERE filter. Without it, an attacker\n // could `INSERT INTO recordings (..., owner_email) VALUES (..., 'victim@x')`\n // through the view and the row would land in the base table under the\n // victim's identity. SQLite views are not auto-updatable in the same way\n // (they require triggers), so this clause is a no-op there but harmless.\n const checkOption = isPostgres ? \" WITH LOCAL CHECK OPTION\" : \"\";\n\n for (const [table, columns] of columnsByTable) {\n // Check core table scoping\n const coreScoping = CORE_TABLE_SCOPING[table];\n if (coreScoping) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n let whereSql: string;\n if (coreScoping.mode === \"prefix\") {\n // settings: key starts with u:<email>:\n // Escape \\, % and _ in the email so LIKE treats them literally.\n const likeEmail = safeEmail\n .replace(/\\\\/g, \"\\\\\\\\\")\n .replace(/%/g, \"\\\\%\")\n .replace(/_/g, \"\\\\_\");\n const prefix = `u:${likeEmail}:`;\n whereSql = `\"${coreScoping.column}\" LIKE '${prefix}%' ESCAPE '\\\\'`;\n } else {\n whereSql = `\"${coreScoping.column}\" = '${safeEmail}'`;\n }\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${whereSql}${checkOption}`,\n });\n continue;\n }\n\n if (\n table === \"tool_data\" &&\n columns.includes(\"scope\") &&\n columns.includes(OWNER_COLUMN) &&\n columns.includes(ORG_COLUMN)\n ) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n const orgClause = safeOrgId\n ? ` OR (\"scope\" = 'org' AND \"${ORG_COLUMN}\" = '${safeOrgId}')`\n : \"\";\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ((\"scope\" = 'user' AND \"${OWNER_COLUMN}\" = '${safeEmail}')${orgClause})${checkOption}`,\n });\n continue;\n }\n\n // Build WHERE clauses for owner_email and org_id\n const clauses: string[] = [];\n const hasOwner = columns.includes(OWNER_COLUMN);\n const hasOrg = columns.includes(ORG_COLUMN);\n\n if (hasOwner) {\n clauses.push(`\"${OWNER_COLUMN}\" = '${safeEmail}'`);\n }\n if (hasOrg && safeOrgId) {\n clauses.push(`\"${ORG_COLUMN}\" = '${safeOrgId}'`);\n }\n\n if (clauses.length > 0) {\n const realTable = `${qualifiedPrefix}\"${table}\"`;\n scoped.push({\n name: table,\n viewSql: `${isPostgres ? \"CREATE OR REPLACE TEMPORARY\" : \"CREATE TEMPORARY\"} VIEW \"${table}\" AS SELECT * FROM ${realTable} WHERE ${clauses.join(\" AND \")}${checkOption}`,\n });\n }\n }\n\n return scoped;\n}\n\n// ─── Public API ─────────────────────────────────────────────────────────────\n\nexport interface ScopingContext {\n /** SQL statements to run before the user's query (create temp views). */\n setup: string[];\n /** SQL statements to run after the user's query (drop temp views). */\n teardown: string[];\n /** Whether scoping is active. */\n active: boolean;\n /** The current user email (for INSERT injection in db-exec). */\n userEmail: string | null;\n /** The current org ID (for INSERT injection in db-exec). */\n orgId: string | null;\n /** Tables that have owner_email columns (for INSERT injection). */\n ownerEmailTables: Set<string>;\n /** Tables that have org_id columns (for INSERT injection). */\n orgIdTables: Set<string>;\n}\n\n/**\n * Build scoping context for a Postgres connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingPostgres(\n pgSql: any,\n): Promise<ScopingContext> {\n // getUserEmail() throws when there is no authenticated user (no request\n // context AND no AGENT_USER_EMAIL env) or when it resolves to the dev\n // sentinel `local@localhost`. We let that throw propagate: the script\n // refuses to run unscoped rather than silently writing rows that the UI\n // then can't see, or running an UPDATE/DELETE across every user's data.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsPostgres(pgSql);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, true);\n\n // Track which tables have owner_email / org_id for INSERT injection\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map((s) => `DROP VIEW IF EXISTS pg_temp.\"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n };\n}\n\n/**\n * Build scoping context for a SQLite/libsql connection.\n * Returns setup/teardown SQL to run before/after the user's query.\n */\nexport async function buildScopingSqlite(client: any): Promise<ScopingContext> {\n // See buildScopingPostgres: getUserEmail() throws on no user / dev sentinel.\n const userEmail = getUserEmail();\n\n const orgId = getOrgId();\n const allColumns = await discoverColumnsSqlite(client);\n const scoped = buildScopedTables(allColumns, userEmail, orgId, false);\n\n const columnsByTable = new Map<string, string[]>();\n for (const { table, column } of allColumns) {\n const cols = columnsByTable.get(table) || [];\n cols.push(column);\n columnsByTable.set(table, cols);\n }\n const ownerEmailTables = new Set<string>();\n const orgIdTables = new Set<string>();\n for (const [table, columns] of columnsByTable) {\n if (columns.includes(OWNER_COLUMN)) ownerEmailTables.add(table);\n if (columns.includes(ORG_COLUMN)) orgIdTables.add(table);\n }\n\n return {\n setup: scoped.map((s) => s.viewSql),\n teardown: scoped.map((s) => `DROP VIEW IF EXISTS \"${s.name}\"`),\n active: scoped.length > 0,\n userEmail,\n orgId,\n ownerEmailTables,\n orgIdTables,\n };\n}\n"]}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Dev-only session bootstrap for `pnpm action <name>` (and any other CLI
3
+ * caller of `runScript`).
4
+ *
5
+ * After changes-53, db-exec / db-query / db-patch refuse to run unless
6
+ * `getRequestUserEmail()` returns a real identity. In an HTTP request the
7
+ * Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`
8
+ * so scoping just works. CLI invocations have no such wrapper, so without
9
+ * this helper every db-* CLI run hands the user a stack trace.
10
+ *
11
+ * What this does: when the runner is about to dispatch, resolve a real
12
+ * email by reading the most-recent row from the legacy `sessions` table
13
+ * (the same table that `addSession()` writes from google-oauth.ts and the
14
+ * A2A receiver fallback already consults). The runner then wraps dispatch
15
+ * in `runWithRequestContext({ userEmail })` so the action sees a real
16
+ * identity.
17
+ *
18
+ * SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY
19
+ * created_at DESC LIMIT 1` query is unscoped — on a machine where
20
+ * multiple developers have signed in (or after a `pnpm action …` run
21
+ * from another team's app), this will bind to whoever signed in most
22
+ * recently across *all* sessions in the DB. If that is wrong, set
23
+ * `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env
24
+ * always wins. A `[dev-session]` log line is emitted so wrong-binding
25
+ * is easy to spot.
26
+ *
27
+ * Strict gating mirrors the A2A precedent in
28
+ * `server/agent-chat-plugin.ts` (search for "latest session"):
29
+ * - NODE_ENV !== "production".
30
+ * - AUTH_MODE unset or === "local" — don't auto-impersonate when an
31
+ * admin or hosted auth mode is in use.
32
+ *
33
+ * If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged
34
+ * — explicit env wins over any DB-derived guess (matches how
35
+ * `getRequestUserEmail()` itself behaves).
36
+ */
37
+ /**
38
+ * Resolve the local dev user's email for the current CLI invocation.
39
+ *
40
+ * Returns the resolved email, or `undefined` when no real identity is
41
+ * available. Callers should let the downstream "no authenticated user"
42
+ * error propagate — its message points the user at the two fixes
43
+ * (sign in via the running app, or set `AGENT_USER_EMAIL`).
44
+ */
45
+ export declare function resolveDevUserEmail(): Promise<string | undefined>;
46
+ //# sourceMappingURL=dev-session.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-session.d.ts","sourceRoot":"","sources":["../../src/scripts/dev-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAIH;;;;;;;GAOG;AACH,wBAAsB,mBAAmB,IAAI,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAkCvE"}
@@ -0,0 +1,81 @@
1
+ /**
2
+ * Dev-only session bootstrap for `pnpm action <name>` (and any other CLI
3
+ * caller of `runScript`).
4
+ *
5
+ * After changes-53, db-exec / db-query / db-patch refuse to run unless
6
+ * `getRequestUserEmail()` returns a real identity. In an HTTP request the
7
+ * Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`
8
+ * so scoping just works. CLI invocations have no such wrapper, so without
9
+ * this helper every db-* CLI run hands the user a stack trace.
10
+ *
11
+ * What this does: when the runner is about to dispatch, resolve a real
12
+ * email by reading the most-recent row from the legacy `sessions` table
13
+ * (the same table that `addSession()` writes from google-oauth.ts and the
14
+ * A2A receiver fallback already consults). The runner then wraps dispatch
15
+ * in `runWithRequestContext({ userEmail })` so the action sees a real
16
+ * identity.
17
+ *
18
+ * SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY
19
+ * created_at DESC LIMIT 1` query is unscoped — on a machine where
20
+ * multiple developers have signed in (or after a `pnpm action …` run
21
+ * from another team's app), this will bind to whoever signed in most
22
+ * recently across *all* sessions in the DB. If that is wrong, set
23
+ * `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env
24
+ * always wins. A `[dev-session]` log line is emitted so wrong-binding
25
+ * is easy to spot.
26
+ *
27
+ * Strict gating mirrors the A2A precedent in
28
+ * `server/agent-chat-plugin.ts` (search for "latest session"):
29
+ * - NODE_ENV !== "production".
30
+ * - AUTH_MODE unset or === "local" — don't auto-impersonate when an
31
+ * admin or hosted auth mode is in use.
32
+ *
33
+ * If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged
34
+ * — explicit env wins over any DB-derived guess (matches how
35
+ * `getRequestUserEmail()` itself behaves).
36
+ */
37
+ const DEV_FALLBACK_EMAIL = "local@localhost"; // guard:allow-localhost-fallback — sentinel intentionally rejected so the resolver doesn't return it
38
+ /**
39
+ * Resolve the local dev user's email for the current CLI invocation.
40
+ *
41
+ * Returns the resolved email, or `undefined` when no real identity is
42
+ * available. Callers should let the downstream "no authenticated user"
43
+ * error propagate — its message points the user at the two fixes
44
+ * (sign in via the running app, or set `AGENT_USER_EMAIL`).
45
+ */
46
+ export async function resolveDevUserEmail() {
47
+ const explicit = process.env.AGENT_USER_EMAIL;
48
+ if (explicit)
49
+ return explicit;
50
+ // Hard refusal: this helper must never source identity in prod.
51
+ if (process.env.NODE_ENV === "production")
52
+ return undefined;
53
+ // AUTH_MODE may be unset (default dev shim) or "local". Anything else
54
+ // means a non-dev auth mode is in play; don't try to fish a session
55
+ // out of the DB on its behalf.
56
+ const authMode = process.env.AUTH_MODE;
57
+ if (authMode && authMode !== "local")
58
+ return undefined;
59
+ try {
60
+ const { getDbExec } = await import("../db/client.js");
61
+ const { rows } = await getDbExec().execute({
62
+ sql: `SELECT email FROM sessions
63
+ WHERE email IS NOT NULL AND email <> ?
64
+ ORDER BY created_at DESC LIMIT 1`,
65
+ args: [DEV_FALLBACK_EMAIL],
66
+ });
67
+ const email = rows[0]?.email;
68
+ if (!email || email.trim().length === 0)
69
+ return undefined;
70
+ console.log(`[dev-session] auto-bound to ${email} (set AGENT_USER_EMAIL to override)`);
71
+ return email;
72
+ }
73
+ catch {
74
+ // The sessions table doesn't exist yet (fresh install where the web
75
+ // server has never booted) or the DB isn't reachable. Either way,
76
+ // we can't produce an identity — let the caller throw with the
77
+ // friendlier "sign in first" hint.
78
+ return undefined;
79
+ }
80
+ }
81
+ //# sourceMappingURL=dev-session.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-session.js","sourceRoot":"","sources":["../../src/scripts/dev-session.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AAEH,MAAM,kBAAkB,GAAG,iBAAiB,CAAC,CAAC,qGAAqG;AAEnJ;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,mBAAmB;IACvC,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;IAC9C,IAAI,QAAQ;QAAE,OAAO,QAAQ,CAAC;IAE9B,gEAAgE;IAChE,IAAI,OAAO,CAAC,GAAG,CAAC,QAAQ,KAAK,YAAY;QAAE,OAAO,SAAS,CAAC;IAE5D,sEAAsE;IACtE,oEAAoE;IACpE,+BAA+B;IAC/B,MAAM,QAAQ,GAAG,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC;IACvC,IAAI,QAAQ,IAAI,QAAQ,KAAK,OAAO;QAAE,OAAO,SAAS,CAAC;IAEvD,IAAI,CAAC;QACH,MAAM,EAAE,SAAS,EAAE,GAAG,MAAM,MAAM,CAAC,iBAAiB,CAAC,CAAC;QACtD,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,SAAS,EAAE,CAAC,OAAO,CAAC;YACzC,GAAG,EAAE;;6CAEkC;YACvC,IAAI,EAAE,CAAC,kBAAkB,CAAC;SAC3B,CAAC,CAAC;QACH,MAAM,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,EAAE,KAA2B,CAAC;QACnD,IAAI,CAAC,KAAK,IAAI,KAAK,CAAC,IAAI,EAAE,CAAC,MAAM,KAAK,CAAC;YAAE,OAAO,SAAS,CAAC;QAC1D,OAAO,CAAC,GAAG,CACT,+BAA+B,KAAK,qCAAqC,CAC1E,CAAC;QACF,OAAO,KAAK,CAAC;IACf,CAAC;IAAC,MAAM,CAAC;QACP,oEAAoE;QACpE,kEAAkE;QAClE,+DAA+D;QAC/D,mCAAmC;QACnC,OAAO,SAAS,CAAC;IACnB,CAAC;AACH,CAAC","sourcesContent":["/**\n * Dev-only session bootstrap for `pnpm action <name>` (and any other CLI\n * caller of `runScript`).\n *\n * After changes-53, db-exec / db-query / db-patch refuse to run unless\n * `getRequestUserEmail()` returns a real identity. In an HTTP request the\n * Nitro plugin wraps the handler in `runWithRequestContext({ userEmail })`\n * so scoping just works. CLI invocations have no such wrapper, so without\n * this helper every db-* CLI run hands the user a stack trace.\n *\n * What this does: when the runner is about to dispatch, resolve a real\n * email by reading the most-recent row from the legacy `sessions` table\n * (the same table that `addSession()` writes from google-oauth.ts and the\n * A2A receiver fallback already consults). The runner then wraps dispatch\n * in `runWithRequestContext({ userEmail })` so the action sees a real\n * identity.\n *\n * SHARED-DEV-BOX CAVEAT: the `SELECT email FROM sessions ORDER BY\n * created_at DESC LIMIT 1` query is unscoped — on a machine where\n * multiple developers have signed in (or after a `pnpm action …` run\n * from another team's app), this will bind to whoever signed in most\n * recently across *all* sessions in the DB. If that is wrong, set\n * `AGENT_USER_EMAIL=<your-email>` in your shell or `.env`; explicit env\n * always wins. A `[dev-session]` log line is emitted so wrong-binding\n * is easy to spot.\n *\n * Strict gating mirrors the A2A precedent in\n * `server/agent-chat-plugin.ts` (search for \"latest session\"):\n * - NODE_ENV !== \"production\".\n * - AUTH_MODE unset or === \"local\" — don't auto-impersonate when an\n * admin or hosted auth mode is in use.\n *\n * If `process.env.AGENT_USER_EMAIL` is already set we return it unchanged\n * — explicit env wins over any DB-derived guess (matches how\n * `getRequestUserEmail()` itself behaves).\n */\n\nconst DEV_FALLBACK_EMAIL = \"local@localhost\"; // guard:allow-localhost-fallback — sentinel intentionally rejected so the resolver doesn't return it\n\n/**\n * Resolve the local dev user's email for the current CLI invocation.\n *\n * Returns the resolved email, or `undefined` when no real identity is\n * available. Callers should let the downstream \"no authenticated user\"\n * error propagate — its message points the user at the two fixes\n * (sign in via the running app, or set `AGENT_USER_EMAIL`).\n */\nexport async function resolveDevUserEmail(): Promise<string | undefined> {\n const explicit = process.env.AGENT_USER_EMAIL;\n if (explicit) return explicit;\n\n // Hard refusal: this helper must never source identity in prod.\n if (process.env.NODE_ENV === \"production\") return undefined;\n\n // AUTH_MODE may be unset (default dev shim) or \"local\". Anything else\n // means a non-dev auth mode is in play; don't try to fish a session\n // out of the DB on its behalf.\n const authMode = process.env.AUTH_MODE;\n if (authMode && authMode !== \"local\") return undefined;\n\n try {\n const { getDbExec } = await import(\"../db/client.js\");\n const { rows } = await getDbExec().execute({\n sql: `SELECT email FROM sessions\n WHERE email IS NOT NULL AND email <> ?\n ORDER BY created_at DESC LIMIT 1`,\n args: [DEV_FALLBACK_EMAIL],\n });\n const email = rows[0]?.email as string | undefined;\n if (!email || email.trim().length === 0) return undefined;\n console.log(\n `[dev-session] auto-bound to ${email} (set AGENT_USER_EMAIL to override)`,\n );\n return email;\n } catch {\n // The sessions table doesn't exist yet (fresh install where the web\n // server has never booted) or the DB isn't reachable. Either way,\n // we can't produce an identity — let the caller throw with the\n // friendlier \"sign in first\" hint.\n return undefined;\n }\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/scripts/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAuBH;;;;;GAKG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CA+H/C"}
1
+ {"version":3,"file":"runner.d.ts","sourceRoot":"","sources":["../../src/scripts/runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAyBH;;;;;GAKG;AACH,wBAAsB,SAAS,IAAI,OAAO,CAAC,IAAI,CAAC,CAgE/C"}
@@ -15,6 +15,8 @@ import { pathToFileURL } from "url";
15
15
  import { coreScripts, getCoreScriptNames } from "./core-scripts.js";
16
16
  import { closeDbExec } from "../db/client.js";
17
17
  import { loadEnv } from "./utils.js";
18
+ import { runWithRequestContext } from "../server/request-context.js";
19
+ import { resolveDevUserEmail } from "./dev-session.js";
18
20
  // Load .env from cwd so DATABASE_URL and other vars are available to all actions.
19
21
  loadEnv();
20
22
  async function runAppDbPluginIfPresent() {
@@ -70,6 +72,25 @@ export async function runScript() {
70
72
  process.exit(1);
71
73
  }
72
74
  const args = process.argv.slice(3);
75
+ // Establish a request context for the duration of this CLI run. Without
76
+ // it, db-exec / db-query / db-patch and any action that calls
77
+ // `getRequestUserEmail()` see no identity and refuse to run. The
78
+ // resolver picks up `AGENT_USER_EMAIL` if explicitly set, otherwise
79
+ // reads the most-recent signed-in session from the DB (dev-only,
80
+ // narrowly gated — see dev-session.ts).
81
+ //
82
+ // This wrap is intentionally a single point of injection: it covers
83
+ // both the local-action branch and the fall-through to core scripts
84
+ // (db-query, db-exec, …) so every CLI entrypoint runs scoped to a real
85
+ // user. It uses `runWithRequestContext` rather than mutating
86
+ // `process.env.AGENT_USER_EMAIL` because env mutation leaks across
87
+ // boundaries — see the cautionary comment in
88
+ // `server/request-context.ts` about exactly that pattern.
89
+ const userEmail = await resolveDevUserEmail();
90
+ const orgId = process.env.AGENT_ORG_ID || undefined;
91
+ return runWithRequestContext({ userEmail, orgId }, () => dispatchAction(actionName, args));
92
+ }
93
+ async function dispatchAction(actionName, args) {
73
94
  // 1. Try local app action first (actions/ then scripts/ for backwards compat)
74
95
  const actionsPath = path.resolve(process.cwd(), "actions", `${actionName}.ts`);
75
96
  const scriptsPath = path.resolve(process.cwd(), "scripts", `${actionName}.ts`);