@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.
- package/dist/agent/context-xray/plugin.js +2 -6
- package/dist/agent/context-xray/schema-store.d.ts +1 -0
- package/dist/agent/context-xray/schema-store.js +7 -0
- package/dist/agent/harness/store.js +3 -0
- package/dist/agent/observational-memory/plugin.js +2 -6
- package/dist/agent/observational-memory/store.js +3 -0
- package/dist/agent/run-store.js +3 -0
- package/dist/agent/tool-approval-store.js +4 -0
- package/dist/automation/index.d.ts +1 -1
- package/dist/chat-threads/store.js +3 -0
- package/dist/db/migrations.d.ts +1 -0
- package/dist/db/migrations.js +1 -1
- package/dist/db/runtime-facts.d.ts +7 -0
- package/dist/db/runtime-facts.js +18 -0
- package/dist/db/schema-catalog.js +4 -1
- package/dist/db/store-ledger.test-helpers.d.ts +5 -0
- package/dist/db/store-ledger.test-helpers.js +19 -0
- package/dist/db/store-registry.d.ts +34 -0
- package/dist/db/store-registry.generated.js +9 -0
- package/dist/db/store-registry.js +230 -47
- package/dist/db/versioned-store-migrations.d.ts +14 -0
- package/dist/db/versioned-store-migrations.js +37 -0
- package/dist/db/widen-columns.js +5 -2
- package/dist/extensions/html-shell.js +67 -0
- package/dist/integrations/remote-devices-store.js +3 -0
- package/dist/jobs/run-history.d.ts +3 -2
- package/dist/jobs/run-history.js +5 -5
- package/dist/jobs/scheduler-health.d.ts +3 -2
- package/dist/jobs/scheduler-health.js +5 -5
- package/dist/oauth-tokens/store.js +3 -0
- package/dist/observability/routes.d.ts +7 -7
- package/dist/observability/store.js +4 -0
- package/dist/org/index.d.ts +1 -1
- package/dist/org/plugin.js +2 -4
- package/dist/org/schema-store.d.ts +1 -0
- package/dist/org/schema-store.js +13 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +15 -15
- package/dist/provider-api/actions/provider-api.d.ts +16 -16
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/resources/store.js +5 -2
- package/dist/secrets/routes.d.ts +10 -10
- package/dist/server/agent-engine-ollama-models-route.d.ts +2 -2
- package/dist/server/auth-plugin.js +2 -6
- package/dist/server/identity-sso-store.js +3 -0
- package/dist/server/release-migrations.js +0 -57
- package/dist/server/release-schema.js +10 -2
- package/dist/triggers/actions/manage-automation.d.ts +3 -3
- package/dist/triggers/event-queue.d.ts +4 -2
- package/dist/triggers/event-queue.js +38 -38
- package/dist/usage/alerts-store.js +3 -0
- package/dist/workspace-connections/groups.js +4 -0
- package/dist/workspace-connections/store.js +3 -0
- 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 (
|
|
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
|
-
|
|
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
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
156
|
-
|
|
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
|
-
*
|
|
161
|
-
* database clients (tests, dev reloads
|
|
162
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
|
353
|
+
function readLedgerSnapshot(exec) {
|
|
354
|
+
const state = ledgerStateFor(exec);
|
|
355
|
+
const snapshot = (state.snapshot ??= loadLedger(exec));
|
|
190
356
|
snapshot.catch(() => {
|
|
191
|
-
if (
|
|
192
|
-
|
|
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
|
-
|
|
203
|
-
|
|
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 === "
|
|
327
|
-
?
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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
|
-
|
|
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
|
+
}
|
package/dist/db/widen-columns.js
CHANGED
|
@@ -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 {
|
|
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
|
|
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 {
|
package/dist/jobs/run-history.js
CHANGED
|
@@ -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
|
|
126
|
-
|
|
127
|
-
|
|
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 {
|
|
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
|
|
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
|
|
54
|
-
|
|
55
|
-
|
|
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() {
|