@agent-native/core 0.209.2 → 0.209.3-nightly-20261016183441

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 (53) hide show
  1. package/dist/agent/context-xray/plugin.js +2 -6
  2. package/dist/agent/context-xray/schema-store.d.ts +1 -0
  3. package/dist/agent/context-xray/schema-store.js +7 -0
  4. package/dist/agent/harness/store.js +3 -0
  5. package/dist/agent/observational-memory/plugin.js +2 -6
  6. package/dist/agent/observational-memory/store.js +3 -0
  7. package/dist/agent/run-store.js +3 -0
  8. package/dist/agent/tool-approval-store.js +4 -0
  9. package/dist/automation/index.d.ts +1 -1
  10. package/dist/chat-threads/store.js +3 -0
  11. package/dist/db/migrations.d.ts +1 -0
  12. package/dist/db/migrations.js +1 -1
  13. package/dist/db/runtime-facts.d.ts +7 -0
  14. package/dist/db/runtime-facts.js +18 -0
  15. package/dist/db/schema-catalog.js +4 -1
  16. package/dist/db/store-ledger.test-helpers.d.ts +5 -0
  17. package/dist/db/store-ledger.test-helpers.js +19 -0
  18. package/dist/db/store-registry.d.ts +34 -0
  19. package/dist/db/store-registry.generated.js +9 -0
  20. package/dist/db/store-registry.js +230 -47
  21. package/dist/db/versioned-store-migrations.d.ts +14 -0
  22. package/dist/db/versioned-store-migrations.js +37 -0
  23. package/dist/db/widen-columns.js +5 -2
  24. package/dist/extensions/html-shell.js +67 -0
  25. package/dist/integrations/remote-devices-store.js +3 -0
  26. package/dist/jobs/run-history.d.ts +3 -2
  27. package/dist/jobs/run-history.js +5 -5
  28. package/dist/jobs/scheduler-health.d.ts +3 -2
  29. package/dist/jobs/scheduler-health.js +5 -5
  30. package/dist/oauth-tokens/store.js +3 -0
  31. package/dist/observability/routes.d.ts +7 -7
  32. package/dist/observability/store.js +4 -0
  33. package/dist/org/index.d.ts +1 -1
  34. package/dist/org/plugin.js +2 -4
  35. package/dist/org/schema-store.d.ts +1 -0
  36. package/dist/org/schema-store.js +13 -0
  37. package/dist/provider-api/actions/custom-provider-registration.d.ts +15 -15
  38. package/dist/provider-api/actions/provider-api.d.ts +16 -16
  39. package/dist/resources/handlers.d.ts +1 -1
  40. package/dist/resources/store.js +5 -2
  41. package/dist/secrets/routes.d.ts +10 -10
  42. package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
  43. package/dist/server/auth-plugin.js +2 -6
  44. package/dist/server/identity-sso-store.js +3 -0
  45. package/dist/server/release-migrations.js +0 -57
  46. package/dist/server/release-schema.js +10 -2
  47. package/dist/triggers/actions/manage-automation.d.ts +3 -3
  48. package/dist/triggers/event-queue.d.ts +4 -2
  49. package/dist/triggers/event-queue.js +38 -38
  50. package/dist/usage/alerts-store.js +3 -0
  51. package/dist/workspace-connections/groups.js +4 -0
  52. package/dist/workspace-connections/store.js +3 -0
  53. package/package.json +3 -3
@@ -16,8 +16,9 @@ import { getAsyncLocalStorageCtor } from "../shared/optional-node-builtins.js";
16
16
  import { getDbExec } from "./client.js";
17
17
  import { appMigratesAtRelease, schemaEnsureSkipRequested, } from "./migration-policy.js";
18
18
  import { isHostedFunctionInvocationRuntime, isMigrationAuthorizedRuntime, isProductionServerlessFunctionRuntime, withMigrationExecutionRuntime, } from "./migration-runtime.js";
19
+ import { runMigrations } from "./migrations.js";
19
20
  import { retainRequestDbPoolScope } from "./request-pool-context.js";
20
- import { dbClientGeneration, retryOnDdlRace } from "./runtime-facts.js";
21
+ import { dbClientGeneration, retryOnDdlRace, runRecordingStoreMigration, } from "./runtime-facts.js";
21
22
  export const STORE_MIGRATIONS_TABLE = "_an_store_migrations";
22
23
  // checksum, kind, source and core_version are written now because Phase 2
23
24
  // (snapshot installs, checksum enforcement) needs them for rows that already
@@ -65,10 +66,12 @@ export function schemaIsReleaseOwned() {
65
66
  * the caller's transaction, so a ledger row written there could claim a
66
67
  * migration that production never kept.
67
68
  */
69
+ function isReleaseStep() {
70
+ return isMigrationAuthorizedRuntime() && !isHostedFunctionInvocationRuntime();
71
+ }
68
72
  function readiness() {
69
- if (isMigrationAuthorizedRuntime() && !isHostedFunctionInvocationRuntime()) {
73
+ if (isReleaseStep())
70
74
  return "apply";
71
- }
72
75
  // No ledger read here: a cold start does zero schema queries, and a missed
73
76
  // release is caught by the release step's own verification, not turned into
74
77
  // a sitewide error by every request.
@@ -76,7 +79,13 @@ function readiness() {
76
79
  return "trust";
77
80
  if (schemaEnsureSkipRequested())
78
81
  return "skip";
79
- return isHostedRuntime() ? "hosted-replay" : "replay";
82
+ // Hosted requests never write the ledger: PR previews share production's
83
+ // database and a request's writes can roll back with its caller.
84
+ if (isHostedRuntime())
85
+ return "hosted-replay";
86
+ // Local dev, tests and self-hosted servers own their database, so they
87
+ // record migrations like the release step: each runs once per database.
88
+ return "apply";
80
89
  }
81
90
  function statementsOf(migration) {
82
91
  if (migration.sql === undefined)
@@ -113,66 +122,222 @@ async function runMigration(migration, exec) {
113
122
  if (migration.run)
114
123
  await retryOnDdlRace(() => migration.run(exec));
115
124
  }
116
- async function applyWithLedger(store) {
117
- const exec = getDbExec();
118
- if (!ledgerPresent) {
125
+ async function applyWithLedger(store, exec) {
126
+ try {
127
+ await applyPending(store, exec);
128
+ }
129
+ catch (err) {
130
+ // The ledger can vanish behind a long-lived executor (a database dropped
131
+ // and recreated under the same client). Rebuild this executor's view once
132
+ // rather than failing every store until the process restarts.
133
+ if (!isMissingLedger(err))
134
+ throw err;
135
+ ledgerStates.delete(exec);
136
+ await applyPending(store, exec);
137
+ }
138
+ }
139
+ function isMissingLedger(err) {
140
+ for (let e = err, depth = 0; e && depth < 4; e = e.cause, depth++) {
141
+ if ((e.code === "42P01" ||
142
+ /relation .* does not exist/i.test(String(e.message ?? ""))) &&
143
+ String(e.message ?? "").includes(STORE_MIGRATIONS_TABLE)) {
144
+ return true;
145
+ }
146
+ }
147
+ return false;
148
+ }
149
+ async function applyPending(store, exec) {
150
+ const state = ledgerStateFor(exec);
151
+ if (!state.present) {
119
152
  await retryOnDdlRace(() => exec.execute(LEDGER_DDL));
120
- ledgerPresent = true;
153
+ state.present = true;
121
154
  }
122
- const recorded = (await readLedgerSnapshot()).get(store.id);
155
+ const recorded = (await readLedgerSnapshot(exec)).get(store.id);
123
156
  if (recorded && store.migrations.every((m) => recorded.has(m.name)))
124
157
  return;
125
158
  const applied = new Set(recorded);
159
+ const legacy = new LegacyBookkeeping(exec);
160
+ const isRelease = isReleaseStep();
161
+ let ran = 0;
126
162
  for (const migration of store.migrations) {
127
163
  if (applied.has(migration.name))
128
164
  continue;
165
+ // An entry carried over from a `runMigrations` list ran only in the release
166
+ // step on main; local runtimes get those tables from the store's own
167
+ // migrations, and running the list there would need its release-only order.
168
+ if (migration.legacy && !migration.legacy.ranAtBoot && !isRelease) {
169
+ continue;
170
+ }
171
+ if (migration.legacy && (await legacy.recorded(migration.legacy))) {
172
+ await recordMigration(exec, store, migration, "legacy");
173
+ applied.add(migration.name);
174
+ continue;
175
+ }
129
176
  try {
130
- await runMigration(migration, exec);
177
+ await runRecordingStoreMigration(() => runMigration(migration, exec));
131
178
  }
132
179
  catch (err) {
133
180
  throw new Error(`Store "${store.id}" migration "${migration.name}" failed: ${err?.message ?? String(err)}`, { cause: err });
134
181
  }
135
- await exec.execute({
136
- sql: `INSERT INTO ${STORE_MIGRATIONS_TABLE} (store_id, name, applied_at, checksum, kind, source) VALUES (?, ?, ?, ?, ?, 'step') ON CONFLICT (store_id, name) DO NOTHING`,
137
- args: [
138
- store.id,
139
- migration.name,
140
- Date.now(),
141
- migrationChecksum(migration),
142
- migration.kind ?? "schema",
143
- ],
144
- });
182
+ await recordMigration(exec, store, migration, "step");
145
183
  applied.add(migration.name);
184
+ ran++;
185
+ }
186
+ state.snapshot = undefined;
187
+ if (ran > 0) {
188
+ console.log(`[db] Applied ${ran} migration(s) to store "${store.id}"`);
189
+ }
190
+ }
191
+ async function recordMigration(exec, store, migration, source) {
192
+ await exec.execute({
193
+ sql: `INSERT INTO ${STORE_MIGRATIONS_TABLE} (store_id, name, applied_at, checksum, kind, source) VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT (store_id, name) DO NOTHING`,
194
+ args: [
195
+ store.id,
196
+ migration.name,
197
+ Date.now(),
198
+ migrationChecksum(migration),
199
+ migration.kind ?? "schema",
200
+ source,
201
+ ],
202
+ });
203
+ }
204
+ /**
205
+ * Reads a `runMigrations` table's bookkeeping with its own gating: an unnamed
206
+ * entry counts as applied when its version is at or below `MAX(version)`, a
207
+ * named one only when its name is in `${table}_named`. A missing table means
208
+ * nothing was applied. Existence is checked with `to_regclass` so a missing
209
+ * table never aborts a caller's open transaction.
210
+ */
211
+ class LegacyBookkeeping {
212
+ exec;
213
+ cache = new Map();
214
+ constructor(exec) {
215
+ this.exec = exec;
216
+ }
217
+ async recorded(ref) {
218
+ const state = await this.read(ref.table);
219
+ return ref.name
220
+ ? state.names.has(ref.name)
221
+ : ref.version <= state.maxVersion;
222
+ }
223
+ read(table) {
224
+ let pending = this.cache.get(table);
225
+ if (!pending)
226
+ this.cache.set(table, (pending = this.load(table)));
227
+ return pending;
228
+ }
229
+ async load(table) {
230
+ if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(table)) {
231
+ throw new Error(`Invalid legacy migration table "${table}"`);
232
+ }
233
+ const exists = async (name) => {
234
+ const { rows } = await this.exec.execute({
235
+ sql: "SELECT to_regclass(?) IS NOT NULL AS present",
236
+ args: [name],
237
+ });
238
+ const present = rows[0]?.present;
239
+ return present === true || present === "t";
240
+ };
241
+ let maxVersion = 0;
242
+ const names = new Set();
243
+ if (await exists(table)) {
244
+ const { rows } = await this.exec.execute(`SELECT MAX(version) AS v FROM ${table}`);
245
+ maxVersion = Number(rows[0]?.v ?? 0);
246
+ }
247
+ if (await exists(`${table}_named`)) {
248
+ const { rows } = await this.exec.execute(
249
+ // guard:allow-unbounded-read — one row per applied entry of one migration list
250
+ `SELECT name FROM ${table}_named`);
251
+ for (const row of rows)
252
+ names.add(String(row.name));
253
+ }
254
+ return { maxVersion, names };
146
255
  }
147
- ledgerSnapshot = undefined;
148
256
  }
149
257
  async function replay(store) {
150
258
  const exec = getDbExec();
259
+ const bootLists = new Map();
151
260
  for (const migration of store.migrations) {
261
+ if (migration.legacy?.ranAtBoot && migration.legacy.entry) {
262
+ const list = bootLists.get(migration.legacy.table) ?? [];
263
+ list.push(migration.legacy.entry);
264
+ bootLists.set(migration.legacy.table, list);
265
+ continue;
266
+ }
267
+ // A request never runs a backfill or seed, nor an entry carried over from
268
+ // a release-only `runMigrations` list: neither ever ran in these apps'
269
+ // requests, and versioned entries are not all safe to repeat.
270
+ if (migration.kind === "data" || migration.legacy)
271
+ continue;
152
272
  await runMigration(migration, exec);
153
273
  }
274
+ // Lists that ran at boot on main keep their own bookkeeping here, so these
275
+ // apps neither re-run nor skip an entry when they upgrade.
276
+ for (const [table, entries] of bootLists) {
277
+ await runMigrations(entries, { table })(undefined);
278
+ }
154
279
  }
155
- let ledgerSnapshot;
156
- let ledgerPresent = false;
280
+ /**
281
+ * Registry order with each store's `after` dependencies first. Fails on an
282
+ * unknown id or a cycle rather than guessing an order.
283
+ */
284
+ export function orderStoresForRelease(stores) {
285
+ const byId = new Map(stores.map((store) => [store.id, store]));
286
+ const ordered = [];
287
+ const state = new Map();
288
+ const visit = (store, path) => {
289
+ const seen = state.get(store.id);
290
+ if (seen === "done")
291
+ return;
292
+ if (seen === "visiting") {
293
+ throw new Error(`Store order cycle: ${[...path, store.id].join(" -> ")}`);
294
+ }
295
+ state.set(store.id, "visiting");
296
+ for (const dependency of store.after ?? []) {
297
+ const before = byId.get(dependency);
298
+ if (!before) {
299
+ throw new Error(`Store "${store.id}" must run after unknown store "${dependency}"`);
300
+ }
301
+ visit(before, [...path, store.id]);
302
+ }
303
+ state.set(store.id, "done");
304
+ ordered.push(store);
305
+ };
306
+ for (const store of stores)
307
+ visit(store, []);
308
+ return ordered;
309
+ }
310
+ /**
311
+ * Ledger state is per database executor, not per process. A transaction
312
+ * (`withDbExec(tx, ...)`) gets its own, so nothing it records can outlive a
313
+ * rollback; tests that hand each case a fresh database get a fresh ledger
314
+ * view; and `closeDbExec()` replaces the shared executor, which starts over.
315
+ */
316
+ let ledgerStates = new WeakMap();
157
317
  const allPending = new Set();
158
318
  let stateGeneration;
319
+ function ledgerStateFor(exec) {
320
+ let state = ledgerStates.get(exec);
321
+ if (!state)
322
+ ledgerStates.set(exec, (state = { present: false }));
323
+ return state;
324
+ }
159
325
  /**
160
- * Ledger state and every store's memo describe one database. Closing the
161
- * database clients (tests, dev reloads, a process that migrates several
162
- * databases) may point the next query at another, so start over.
326
+ * Process-wide memos (hosted replay, trust, skip) describe one database.
327
+ * Closing the database clients (tests, dev reloads) may point the next query
328
+ * at another, so start over.
163
329
  */
164
330
  function syncWithDatabaseGeneration() {
165
331
  const generation = dbClientGeneration();
166
332
  if (stateGeneration === generation)
167
333
  return;
168
334
  stateGeneration = generation;
169
- ledgerSnapshot = undefined;
170
- ledgerPresent = false;
335
+ ledgerStates = new WeakMap();
171
336
  for (const pending of allPending)
172
337
  pending.clear();
173
338
  }
174
- async function loadLedger() {
175
- const { rows } = await getDbExec().execute(
339
+ async function loadLedger(exec) {
340
+ const { rows } = await exec.execute(
176
341
  // guard:allow-unbounded-read — one row per declared migration; this is the single shared read
177
342
  `SELECT store_id, name FROM ${STORE_MIGRATIONS_TABLE}`);
178
343
  const ledger = new Map();
@@ -185,11 +350,12 @@ async function loadLedger() {
185
350
  }
186
351
  return ledger;
187
352
  }
188
- function readLedgerSnapshot() {
189
- const snapshot = (ledgerSnapshot ??= loadLedger());
353
+ function readLedgerSnapshot(exec) {
354
+ const state = ledgerStateFor(exec);
355
+ const snapshot = (state.snapshot ??= loadLedger(exec));
190
356
  snapshot.catch(() => {
191
- if (ledgerSnapshot === snapshot)
192
- ledgerSnapshot = undefined;
357
+ if (state.snapshot === snapshot)
358
+ state.snapshot = undefined;
193
359
  });
194
360
  return snapshot;
195
361
  }
@@ -199,8 +365,9 @@ function readLedgerSnapshot() {
199
365
  * with the code it is about to publish fails there instead of in a request.
200
366
  */
201
367
  export async function findUnappliedStoreMigrations(stores) {
202
- ledgerSnapshot = undefined;
203
- const ledger = await readLedgerSnapshot();
368
+ const exec = getDbExec();
369
+ ledgerStateFor(exec).snapshot = undefined;
370
+ const ledger = await readLedgerSnapshot(exec);
204
371
  return stores
205
372
  .map((store) => ({
206
373
  storeId: store.id,
@@ -313,25 +480,40 @@ export function defineStore(definition) {
313
480
  }
314
481
  const pending = new Map();
315
482
  allPending.add(pending);
483
+ // The release step also runs list-origin migrations that local runtimes
484
+ // skip, so its readiness is memoized separately.
485
+ let applied = new WeakMap();
486
+ let appliedAtRelease = new WeakMap();
316
487
  const store = {
317
488
  id,
489
+ after: definition.after,
318
490
  releaseOnly: definition.releaseOnly,
319
491
  migrations,
320
492
  ready() {
321
493
  syncWithDatabaseGeneration();
322
494
  const mode = readiness();
495
+ if (mode === "apply") {
496
+ const exec = getDbExec();
497
+ const memo = isReleaseStep() ? appliedAtRelease : applied;
498
+ const existing = memo.get(exec);
499
+ if (existing)
500
+ return existing;
501
+ const tracked = retainRequestDbPoolScope(applyWithLedger(store, exec).catch((err) => {
502
+ if (memo.get(exec) === tracked)
503
+ memo.delete(exec);
504
+ throw err;
505
+ }));
506
+ memo.set(exec, tracked);
507
+ return tracked;
508
+ }
323
509
  const existing = pending.get(mode);
324
510
  if (existing)
325
511
  return existing;
326
- const run = mode === "apply"
327
- ? applyWithLedger(store)
328
- : mode === "hosted-replay"
329
- ? definition.releaseOnly
330
- ? Promise.resolve()
331
- : withMigrationExecutionRuntime(() => replay(store))
332
- : mode === "replay"
333
- ? replay(store)
334
- : Promise.resolve();
512
+ const run = mode === "hosted-replay"
513
+ ? definition.releaseOnly
514
+ ? Promise.resolve()
515
+ : withMigrationExecutionRuntime(() => replay(store))
516
+ : Promise.resolve();
335
517
  // The memo outlives the request that started it; on Workers that
336
518
  // request's pool must stay open until the work settles.
337
519
  const tracked = retainRequestDbPoolScope(run.catch((err) => {
@@ -344,11 +526,12 @@ export function defineStore(definition) {
344
526
  },
345
527
  reset() {
346
528
  pending.clear();
529
+ applied = new WeakMap();
530
+ appliedAtRelease = new WeakMap();
347
531
  },
348
532
  };
349
533
  return store;
350
534
  }
351
535
  export function __resetStoreLedgerSnapshotForTests() {
352
- ledgerSnapshot = undefined;
353
- ledgerPresent = false;
536
+ ledgerStates = new WeakMap();
354
537
  }
@@ -0,0 +1,14 @@
1
+ import { type MigrationEntry } from "./migrations.js";
2
+ import type { StoreMigration } from "./store-registry.js";
3
+ /**
4
+ * Turns `runMigrations` entries into store migrations that keep each entry's
5
+ * identity: a named entry keeps its name, an unnamed one becomes `v<version>`,
6
+ * and each carries a `legacy` reference so a database that already applied it
7
+ * through the old list records it instead of running it again. Multi-statement
8
+ * SQL is split here because the store runner sends one statement per call.
9
+ */
10
+ export declare function fromMigrationEntries(table: string, entries: readonly MigrationEntry[], options?: {
11
+ data?: readonly (string | number)[];
12
+ /** The list also ran at plugin boot on main, not only at release. */
13
+ ranAtBoot?: boolean;
14
+ }): StoreMigration[];
@@ -0,0 +1,37 @@
1
+ import { MIGRATION_DEFERRED, splitSqlStatements, } from "./migrations.js";
2
+ /**
3
+ * Turns `runMigrations` entries into store migrations that keep each entry's
4
+ * identity: a named entry keeps its name, an unnamed one becomes `v<version>`,
5
+ * and each carries a `legacy` reference so a database that already applied it
6
+ * through the old list records it instead of running it again. Multi-statement
7
+ * SQL is split here because the store runner sends one statement per call.
8
+ */
9
+ export function fromMigrationEntries(table, entries, options = {}) {
10
+ const data = new Set(options.data ?? []);
11
+ return entries.map((entry) => {
12
+ const raw = typeof entry.sql === "string" ? entry.sql : entry.sql.postgres;
13
+ const name = entry.name ?? `v${entry.version}`;
14
+ const run = entry.run;
15
+ return {
16
+ name,
17
+ kind: data.has(name) || data.has(entry.version)
18
+ ? "data"
19
+ : undefined,
20
+ sql: raw ? splitSqlStatements(raw) : undefined,
21
+ run: run
22
+ ? async (exec) => {
23
+ if ((await run(exec)) === MIGRATION_DEFERRED) {
24
+ throw new Error(`Migration "${name}" deferred itself; store migrations cannot be deferred.`);
25
+ }
26
+ }
27
+ : undefined,
28
+ legacy: {
29
+ table,
30
+ version: entry.version,
31
+ name: entry.name,
32
+ ranAtBoot: options.ranAtBoot,
33
+ entry: options.ranAtBoot ? entry : undefined,
34
+ },
35
+ };
36
+ });
37
+ }
@@ -1,5 +1,6 @@
1
1
  import { getDbExec } from "./client.js";
2
2
  import { isHostedFunctionInvocationRuntime, isMigrationExecutingRuntime, isMigrationAuthorizedRuntime, isProductionServerlessFunctionRuntime, } from "./migration-runtime.js";
3
+ import { isRecordingStoreMigration } from "./runtime-facts.js";
3
4
  const PLAIN_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
4
5
  /**
5
6
  * Widen pre-existing 32-bit `integer` columns to 64-bit `BIGINT` in place on
@@ -48,8 +49,9 @@ export async function widenIntColumnsToBigInt(table, columns, injectedClient) {
48
49
  }
49
50
  catch (err) {
50
51
  // At release the store ledger records success, so a skipped widen must fail.
51
- if (isMigrationAuthorizedRuntime())
52
+ if (isMigrationAuthorizedRuntime() || isRecordingStoreMigration()) {
52
53
  throw err;
54
+ }
53
55
  return;
54
56
  }
55
57
  for (const col of columns) {
@@ -59,8 +61,9 @@ export async function widenIntColumnsToBigInt(table, columns, injectedClient) {
59
61
  await client.execute(`ALTER TABLE ${table} ALTER COLUMN ${col} TYPE BIGINT`);
60
62
  }
61
63
  catch (err) {
62
- if (isMigrationAuthorizedRuntime())
64
+ if (isMigrationAuthorizedRuntime() || isRecordingStoreMigration()) {
63
65
  throw err;
66
+ }
64
67
  // A concurrent boot already widened it, or the role lacks ALTER — both
65
68
  // are safe to ignore; a later boot retries if still needed.
66
69
  }
@@ -517,6 +517,16 @@ export function buildExtensionHtml(content, themeVars, isDark, extensionId, bind
517
517
 
518
518
  var _extensionId = ${extensionIdJson};
519
519
  var _extensionBinding = ${bindingJson};
520
+ // Proves an open-url request comes from this shell document, not from a
521
+ // page the iframe later navigated to. Announced with the binding below.
522
+ // Without secure randomness there is no token, the host never offers to
523
+ // open links, and links keep their native behavior.
524
+ var _openUrlToken =
525
+ window.crypto && typeof window.crypto.getRandomValues === 'function'
526
+ ? Array.prototype.map.call(window.crypto.getRandomValues(new Uint8Array(16)), function(b) {
527
+ return ('0' + b.toString(16)).slice(-2);
528
+ }).join('')
529
+ : '';
520
530
  window.extensionBinding = _extensionBinding;
521
531
  // Legacy alias for extension bodies authored before the rename.
522
532
  window.toolBinding = _extensionBinding;
@@ -532,10 +542,67 @@ export function buildExtensionHtml(content, themeVars, isDark, extensionId, bind
532
542
  type: 'agent-native-extension-binding',
533
543
  extensionId: _extensionId,
534
544
  binding: _extensionBinding,
545
+ openUrlToken: _openUrlToken,
535
546
  },
536
547
  '*',
537
548
  );
538
549
  } catch (_) {}
550
+ // A tab opened from this sandboxed iframe inherits the sandbox (an opaque
551
+ // origin with no cookies), so apps like HubSpot render blank. Once the host
552
+ // says it can, hand new-tab web links to it: it opens them unsandboxed with
553
+ // noopener, so the new tab gets no handle on the host page. Without that
554
+ // reply (no host, a standalone /render, or no token) links stay native, and
555
+ // window.open is never touched. Set up here, before extension content runs,
556
+ // so the reply can't be missed and extension globals can't interfere.
557
+ (function(token) {
558
+ if (!token) return;
559
+ var hostOpensLinks = false;
560
+ window.addEventListener('message', function(event) {
561
+ if (event.source !== window.parent) return;
562
+ var msg = event.data;
563
+ if (msg && msg.type === 'agent-native-extension-open-url-ready') hostOpensLinks = true;
564
+ });
565
+ function linkOf(e) {
566
+ var path = typeof e.composedPath === 'function' ? e.composedPath() : [e.target];
567
+ for (var i = 0; i < path.length; i++) {
568
+ var node = path[i];
569
+ if (node instanceof HTMLAnchorElement || node instanceof HTMLAreaElement) {
570
+ if (node.hasAttribute('href')) return { element: node, url: node };
571
+ } else if (node && node.namespaceURI === 'http://www.w3.org/2000/svg' && node.localName === 'a') {
572
+ // An SVG link's href isn't a URL string; resolve it the way an HTML
573
+ // link would, against the document's base URL.
574
+ var raw = node.getAttribute('href') || node.getAttributeNS('http://www.w3.org/1999/xlink', 'href');
575
+ if (raw) {
576
+ var resolved = document.createElement('a');
577
+ resolved.href = raw;
578
+ return { element: node, url: resolved };
579
+ }
580
+ }
581
+ }
582
+ return null;
583
+ }
584
+ function targetOf(link) {
585
+ if (link.hasAttribute('target')) return link.getAttribute('target');
586
+ var base = document.querySelector('base[target]');
587
+ return base ? base.getAttribute('target') : '';
588
+ }
589
+ function route(e) {
590
+ if (!hostOpensLinks || e.defaultPrevented) return;
591
+ var link = linkOf(e);
592
+ if (!link || link.element.hasAttribute('download')) return;
593
+ if (link.url.protocol !== 'http:' && link.url.protocol !== 'https:') return;
594
+ var newTab = e.type === 'auxclick'
595
+ ? e.button === 1
596
+ : e.button === 0 && !e.altKey &&
597
+ (e.metaKey || e.ctrlKey || e.shiftKey || /^_blank$/i.test(targetOf(link.element)));
598
+ if (!newTab) return;
599
+ e.preventDefault();
600
+ window.parent.postMessage({ type: 'agent-native-extension-open-url', url: link.url.href, token: token }, '*');
601
+ }
602
+ // On window, so extension handlers delegated on document run first.
603
+ window.addEventListener('click', route);
604
+ window.addEventListener('auxclick', route);
605
+ })(_openUrlToken);
539
606
  // SECURITY: when the viewer is not the author of this extension, emit a
540
607
  // clear console warning. The bridge currently runs every helper with the
541
608
  // viewer's session — a malicious shared extension can call any action,
@@ -1,6 +1,8 @@
1
1
  import { getDbExec } from "../db/client.js";
2
2
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
3
3
  import { defineStore } from "../db/store-registry.js";
4
+ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
5
+ import { REMOTE_DEVICE_MIGRATIONS, REMOTE_DEVICE_MIGRATIONS_TABLE, } from "./remote-device-migrations.js";
4
6
  export const remoteDevicesStore = defineStore({
5
7
  id: "remote_devices",
6
8
  migrations: [
@@ -35,6 +37,7 @@ export const remoteDevicesStore = defineStore({
35
37
  await ensureIndexExists("idx_remote_devices_owner", `CREATE INDEX IF NOT EXISTS idx_remote_devices_owner ON integration_remote_devices(owner_email, org_id)`);
36
38
  },
37
39
  },
40
+ ...fromMigrationEntries(REMOTE_DEVICE_MIGRATIONS_TABLE, REMOTE_DEVICE_MIGRATIONS),
38
41
  ],
39
42
  });
40
43
  export function ensureTable() {
@@ -1,5 +1,5 @@
1
1
  import { type DbExec } from "../db/client.js";
2
- import { type MigrationEntry } from "../db/migrations.js";
2
+ import type { MigrationEntry } from "../db/migrations.js";
3
3
  export type AutomationRunStatus = "running" | "success" | "error" | "interrupted" | "skipped";
4
4
  export interface AutomationRun {
5
5
  id: string;
@@ -42,7 +42,8 @@ export declare function automationRunClaimLeaseMs(): number;
42
42
  /** How many finished runs each automation retains, in addition to unfinished runs. */
43
43
  export declare const RUNS_RETAINED_PER_AUTOMATION = 50;
44
44
  export declare const AUTOMATION_RUN_MIGRATIONS: MigrationEntry[];
45
- export declare function runAutomationRunMigrations(nitroApp: unknown): Promise<void>;
45
+ /** Kept for callers of the public export; the store owns these migrations. */
46
+ export declare function runAutomationRunMigrations(_nitroApp?: unknown): Promise<void>;
46
47
  export declare const automationRunHistoryStore: import("../db/store-registry.js").Store;
47
48
  export declare function ensureTable(): Promise<void>;
48
49
  export interface StartAutomationRunOptions {
@@ -3,8 +3,8 @@ import { z } from "zod";
3
3
  import { resolveBackgroundRunHardTimeoutMs } from "../agent/run-manager.js";
4
4
  import { getDbExec } from "../db/client.js";
5
5
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
6
- import { runMigrations } from "../db/migrations.js";
7
6
  import { defineStore } from "../db/store-registry.js";
7
+ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
8
8
  import { emit as emitBusEvent, registerEvent } from "../event-bus/index.js";
9
9
  import { automationRecoveryMessagesForLocale } from "../localization/automation-recovery-messages.js";
10
10
  import { decryptSecretValue, encryptSecretValue } from "../secrets/crypto.js";
@@ -122,10 +122,9 @@ export const AUTOMATION_RUN_MIGRATIONS = [
122
122
  ADD COLUMN IF NOT EXISTS failure_alert_unsubscribe_token TEXT`,
123
123
  },
124
124
  ];
125
- export async function runAutomationRunMigrations(nitroApp) {
126
- await runMigrations(AUTOMATION_RUN_MIGRATIONS, {
127
- table: "_automation_run_migrations",
128
- })(nitroApp);
125
+ /** Kept for callers of the public export; the store owns these migrations. */
126
+ export async function runAutomationRunMigrations(_nitroApp) {
127
+ await automationRunHistoryStore.ready();
129
128
  }
130
129
  export const automationRunHistoryStore = defineStore({
131
130
  id: "automation_run_history",
@@ -179,6 +178,7 @@ export const automationRunHistoryStore = defineStore({
179
178
  await ensureIndexExists(`idx_${TABLE}_owner_automation`, indexSql);
180
179
  },
181
180
  },
181
+ ...fromMigrationEntries("_automation_run_migrations", AUTOMATION_RUN_MIGRATIONS),
182
182
  ],
183
183
  });
184
184
  export function ensureTable() {
@@ -1,4 +1,4 @@
1
- import { type MigrationEntry } from "../db/migrations.js";
1
+ import type { MigrationEntry } from "../db/migrations.js";
2
2
  export declare const AUTOMATION_SCHEDULER_LEASE_MS: number;
3
3
  export declare const AUTOMATION_SCHEDULER_LEASE_RENEWAL_MS = 60000;
4
4
  export declare class AutomationSchedulerLeaseLostError extends Error {
@@ -6,7 +6,8 @@ export declare class AutomationSchedulerLeaseLostError extends Error {
6
6
  constructor(cause?: unknown);
7
7
  }
8
8
  export declare const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS: MigrationEntry[];
9
- export declare function runAutomationSchedulerHealthMigrations(nitroApp: unknown): Promise<void>;
9
+ /** Kept for callers of the public export; the store owns these migrations. */
10
+ export declare function runAutomationSchedulerHealthMigrations(_nitroApp?: unknown): Promise<void>;
10
11
  export interface AutomationSchedulerHealth {
11
12
  appId: string;
12
13
  orgId: string | null;
@@ -1,8 +1,8 @@
1
1
  import { randomUUID } from "node:crypto";
2
2
  import { getDbExec } from "../db/client.js";
3
3
  import { ensureColumnExists, ensureIndexExists, ensureTableExists, } from "../db/ddl-guard.js";
4
- import { runMigrations } from "../db/migrations.js";
5
4
  import { defineStore } from "../db/store-registry.js";
5
+ import { fromMigrationEntries } from "../db/versioned-store-migrations.js";
6
6
  import { automationRecoveryMessagesForLocale } from "../localization/automation-recovery-messages.js";
7
7
  const TABLE = "automation_scheduler_health";
8
8
  const DEFAULT_APP_ID = "default";
@@ -50,10 +50,9 @@ export const AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS = [
50
50
  `,
51
51
  },
52
52
  ];
53
- export async function runAutomationSchedulerHealthMigrations(nitroApp) {
54
- await runMigrations(AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS, {
55
- table: "_automation_scheduler_health_migrations",
56
- })(nitroApp);
53
+ /** Kept for callers of the public export; the store owns these migrations. */
54
+ export async function runAutomationSchedulerHealthMigrations(_nitroApp) {
55
+ await schedulerHealthStore.ready();
57
56
  }
58
57
  function normalizeAppId(appId) {
59
58
  const normalized = appId?.trim();
@@ -96,6 +95,7 @@ export const schedulerHealthStore = defineStore({
96
95
  await ensureIndexExists(`idx_${TABLE}_updated`, indexSql);
97
96
  },
98
97
  },
98
+ ...fromMigrationEntries("_automation_scheduler_health_migrations", AUTOMATION_SCHEDULER_HEALTH_MIGRATIONS),
99
99
  ],
100
100
  });
101
101
  export function ensureHealthTable() {