@nightowlsdev/storage-supabase 0.3.0 → 2.0.0
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/README.md +206 -62
- package/dist/index.cjs +496 -20
- package/dist/index.d.cts +119 -16
- package/dist/index.d.ts +119 -16
- package/dist/index.js +486 -19
- package/package.json +5 -5
package/dist/index.js
CHANGED
|
@@ -112,6 +112,19 @@ function makeEventStore(ctx) {
|
|
|
112
112
|
const rows = await many(ctx.pool, "select run_id, seq, payload from events where org_id=$1 and run_id=$2 and seq>$3 order by seq", [tenantId, runId, sinceSeq]);
|
|
113
113
|
return rows.map(fromRow);
|
|
114
114
|
},
|
|
115
|
+
// The full ordered log for a CONTAINER: join events → runs and match the container (the root thread_id, OR a
|
|
116
|
+
// lane sub-thread `<container>:<slug>`). `seq` is GENERATED ALWAYS (global, monotonic across all runs), so a
|
|
117
|
+
// plain `order by seq` interleaves every run correctly. Org-scoped (service connection bypasses RLS, so we
|
|
118
|
+
// enforce tenancy here). split_part is injection-safe; a container id never contains ':'.
|
|
119
|
+
async listForContainer(tenantId, container) {
|
|
120
|
+
const rows = await many(
|
|
121
|
+
ctx.pool,
|
|
122
|
+
`select e.run_id, e.seq, e.payload, r.thread_id from events e join runs r on r.id = e.run_id
|
|
123
|
+
where e.org_id = $1 and split_part(r.thread_id, ':', 1) = $2 order by e.seq`,
|
|
124
|
+
[tenantId, container]
|
|
125
|
+
);
|
|
126
|
+
return rows.map((r) => ({ ...fromRow(r), threadId: r.thread_id }));
|
|
127
|
+
},
|
|
115
128
|
subscribe: makeSubscribe(ctx)
|
|
116
129
|
};
|
|
117
130
|
}
|
|
@@ -258,6 +271,34 @@ function makeScratchpadStore(ctx) {
|
|
|
258
271
|
};
|
|
259
272
|
}
|
|
260
273
|
|
|
274
|
+
// src/threads.ts
|
|
275
|
+
function makeThreadStore(ctx) {
|
|
276
|
+
return {
|
|
277
|
+
async ensure({ id, orgId, userId, projectId }) {
|
|
278
|
+
await ctx.pool.query(
|
|
279
|
+
"insert into threads(id, org_id, user_id, project_id) values($1,$2,$3,$4) on conflict (id) do nothing",
|
|
280
|
+
[id, orgId, userId, projectId ?? null]
|
|
281
|
+
);
|
|
282
|
+
}
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// src/agents.ts
|
|
287
|
+
import { assertActorMayMutateDefinition } from "@nightowlsdev/core";
|
|
288
|
+
|
|
289
|
+
// src/versioning.ts
|
|
290
|
+
async function appendVersion(client, entity, headId, tenantId, insertVersionRow, action, actor, auditAfter) {
|
|
291
|
+
await client.query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [entity.versionTable, headId]);
|
|
292
|
+
const nextV = (await client.query(`select coalesce(max(version),0)+1 v from ${entity.versionTable} where ${entity.fkColumn}=$1`, [headId])).rows[0].v;
|
|
293
|
+
const versionRowId = await insertVersionRow(nextV);
|
|
294
|
+
await client.query(`update ${entity.headTable} set current_version_id=$2 where id=$1`, [headId, versionRowId]);
|
|
295
|
+
await client.query(
|
|
296
|
+
"insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,$4,$5,$6)",
|
|
297
|
+
[tenantId, actor, action, entity.kind, headId, JSON.stringify({ version: nextV, ...auditAfter })]
|
|
298
|
+
);
|
|
299
|
+
return nextV;
|
|
300
|
+
}
|
|
301
|
+
|
|
261
302
|
// src/subscribe-invalidations.ts
|
|
262
303
|
var INVALIDATE_CHANNEL = "nightowls_agent_invalidate";
|
|
263
304
|
async function listenForInvalidations(ctx, onInvalidate) {
|
|
@@ -269,7 +310,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
|
|
|
269
310
|
client.on("error", (e) => {
|
|
270
311
|
client.removeListener("notification", onNotification);
|
|
271
312
|
console.warn(
|
|
272
|
-
"[
|
|
313
|
+
"[@nightowlsdev/storage-supabase] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
|
|
273
314
|
e instanceof Error ? e.message : e
|
|
274
315
|
);
|
|
275
316
|
});
|
|
@@ -293,6 +334,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
|
|
|
293
334
|
}
|
|
294
335
|
|
|
295
336
|
// src/agents.ts
|
|
337
|
+
var AGENT_ENTITY = { kind: "agent", versionTable: "agent_versions", headTable: "agents", fkColumn: "agent_id" };
|
|
296
338
|
function rowToVersion(r) {
|
|
297
339
|
return {
|
|
298
340
|
slug: r.slug,
|
|
@@ -305,6 +347,18 @@ function rowToVersion(r) {
|
|
|
305
347
|
modelId: r.model_id
|
|
306
348
|
};
|
|
307
349
|
}
|
|
350
|
+
function auditActor(actor) {
|
|
351
|
+
switch (actor.type) {
|
|
352
|
+
case "human":
|
|
353
|
+
return `user:${actor.userId}`;
|
|
354
|
+
case "service":
|
|
355
|
+
return `service:${actor.serviceId}`;
|
|
356
|
+
case "system":
|
|
357
|
+
return `system:${actor.reason}`;
|
|
358
|
+
case "agent":
|
|
359
|
+
return `agent:${actor.agentSlug}`;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
308
362
|
function makeAgentRepo(ctx) {
|
|
309
363
|
return {
|
|
310
364
|
async head(tenantId, slug) {
|
|
@@ -333,19 +387,32 @@ function makeAgentRepo(ctx) {
|
|
|
333
387
|
}
|
|
334
388
|
};
|
|
335
389
|
}
|
|
390
|
+
function makeVersionedRepo(ctx) {
|
|
391
|
+
const read = makeAgentRepo(ctx);
|
|
392
|
+
return {
|
|
393
|
+
...read,
|
|
394
|
+
async publish(tenantId, slug, content, actor) {
|
|
395
|
+
const { version } = await publishAgentVersion(ctx, { tenantId, ...content, slug, actor });
|
|
396
|
+
return { version };
|
|
397
|
+
},
|
|
398
|
+
async rollback(tenantId, slug, toVersion, actor) {
|
|
399
|
+
return rollbackAgentVersion(ctx, { tenantId, slug, toVersion, actor });
|
|
400
|
+
},
|
|
401
|
+
async listVersions(tenantId, slug, actor) {
|
|
402
|
+
assertActorMayMutateDefinition(actor);
|
|
403
|
+
return listAgentVersions(ctx, tenantId, slug);
|
|
404
|
+
}
|
|
405
|
+
};
|
|
406
|
+
}
|
|
336
407
|
async function commitVersion(client, agentId, tenantId, content, action, actor, auditAfter) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
"insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,'agent',$4,$5)",
|
|
346
|
-
[tenantId, actor, action, agentId, JSON.stringify({ version: nextV, ...auditAfter })]
|
|
347
|
-
);
|
|
348
|
-
return nextV;
|
|
408
|
+
return appendVersion(client, AGENT_ENTITY, agentId, tenantId, async (version) => {
|
|
409
|
+
const ver = (await client.query(
|
|
410
|
+
`insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
|
|
411
|
+
values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
|
|
412
|
+
[agentId, tenantId, version, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
|
|
413
|
+
)).rows[0];
|
|
414
|
+
return ver.id;
|
|
415
|
+
}, action, actor, auditAfter);
|
|
349
416
|
}
|
|
350
417
|
async function notifyInvalidate(client, tenantId, slug) {
|
|
351
418
|
try {
|
|
@@ -353,7 +420,10 @@ async function notifyInvalidate(client, tenantId, slug) {
|
|
|
353
420
|
} catch {
|
|
354
421
|
}
|
|
355
422
|
}
|
|
423
|
+
var SEED_ACTOR = { type: "system", reason: "seed" };
|
|
356
424
|
async function publishAgentVersion(ctx, def) {
|
|
425
|
+
const actor = def.actor ?? SEED_ACTOR;
|
|
426
|
+
assertActorMayMutateDefinition(actor);
|
|
357
427
|
const client = await ctx.pool.connect();
|
|
358
428
|
try {
|
|
359
429
|
await client.query("begin");
|
|
@@ -362,9 +432,10 @@ async function publishAgentVersion(ctx, def) {
|
|
|
362
432
|
on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
|
|
363
433
|
[def.tenantId, def.slug, def.role === "orchestrator"]
|
|
364
434
|
)).rows[0];
|
|
365
|
-
await commitVersion(client, agent.id, def.tenantId, def, "publish",
|
|
435
|
+
const version = await commitVersion(client, agent.id, def.tenantId, def, "publish", auditActor(actor), { slug: def.slug });
|
|
366
436
|
await client.query("commit");
|
|
367
437
|
await notifyInvalidate(client, def.tenantId, def.slug);
|
|
438
|
+
return { version };
|
|
368
439
|
} catch (e) {
|
|
369
440
|
await client.query("rollback");
|
|
370
441
|
throw e;
|
|
@@ -372,6 +443,41 @@ async function publishAgentVersion(ctx, def) {
|
|
|
372
443
|
client.release();
|
|
373
444
|
}
|
|
374
445
|
}
|
|
446
|
+
async function ensureAgentVersion(ctx, def) {
|
|
447
|
+
const actor = def.actor ?? SEED_ACTOR;
|
|
448
|
+
assertActorMayMutateDefinition(actor);
|
|
449
|
+
const client = await ctx.pool.connect();
|
|
450
|
+
try {
|
|
451
|
+
await client.query("begin");
|
|
452
|
+
await client.query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", ["ensure_agent", `${def.tenantId}:${def.slug}`]);
|
|
453
|
+
const head = (await client.query(
|
|
454
|
+
`select v.version from agents a join agent_versions v on v.id = a.current_version_id where a.org_id=$1 and a.slug=$2 and a.project_id is null`,
|
|
455
|
+
[def.tenantId, def.slug]
|
|
456
|
+
)).rows[0];
|
|
457
|
+
if (head) {
|
|
458
|
+
await client.query("commit");
|
|
459
|
+
return { version: head.version, created: false };
|
|
460
|
+
}
|
|
461
|
+
const agent = (await client.query(
|
|
462
|
+
`insert into agents(org_id, slug, is_orchestrator) values($1,$2,$3)
|
|
463
|
+
on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
|
|
464
|
+
[def.tenantId, def.slug, def.role === "orchestrator"]
|
|
465
|
+
)).rows[0];
|
|
466
|
+
const version = await commitVersion(client, agent.id, def.tenantId, def, "publish", auditActor(actor), { slug: def.slug });
|
|
467
|
+
await client.query("commit");
|
|
468
|
+
await notifyInvalidate(client, def.tenantId, def.slug);
|
|
469
|
+
return { version, created: true };
|
|
470
|
+
} catch (e) {
|
|
471
|
+
await client.query("rollback");
|
|
472
|
+
throw e;
|
|
473
|
+
} finally {
|
|
474
|
+
client.release();
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function listTenants(ctx) {
|
|
478
|
+
const rows = await many(ctx.pool, "select id::text as id from orgs order by created_at asc");
|
|
479
|
+
return rows.map((r) => r.id);
|
|
480
|
+
}
|
|
375
481
|
async function listAgentVersions(ctx, tenantId, slug) {
|
|
376
482
|
const rows = await many(
|
|
377
483
|
ctx.pool,
|
|
@@ -383,6 +489,8 @@ async function listAgentVersions(ctx, tenantId, slug) {
|
|
|
383
489
|
return rows.map((r) => ({ version: Number(r.version), role: r.role, modelId: r.model_id, status: r.status, isCurrent: r.is_current }));
|
|
384
490
|
}
|
|
385
491
|
async function rollbackAgentVersion(ctx, args) {
|
|
492
|
+
const actor = args.actor ?? { type: "system", reason: "rollback" };
|
|
493
|
+
assertActorMayMutateDefinition(actor);
|
|
386
494
|
const client = await ctx.pool.connect();
|
|
387
495
|
try {
|
|
388
496
|
await client.query("begin");
|
|
@@ -401,7 +509,7 @@ async function rollbackAgentVersion(ctx, args) {
|
|
|
401
509
|
delegateSlugs: target.delegate_slugs ?? [],
|
|
402
510
|
modelId: target.model_id
|
|
403
511
|
};
|
|
404
|
-
const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback",
|
|
512
|
+
const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", auditActor(actor), {
|
|
405
513
|
slug: args.slug,
|
|
406
514
|
restoredFrom: args.toVersion
|
|
407
515
|
});
|
|
@@ -416,6 +524,114 @@ async function rollbackAgentVersion(ctx, args) {
|
|
|
416
524
|
}
|
|
417
525
|
}
|
|
418
526
|
|
|
527
|
+
// src/bundles.ts
|
|
528
|
+
import { assertActorMayMutateDefinition as assertActorMayMutateDefinition2 } from "@nightowlsdev/core";
|
|
529
|
+
var BUNDLE_ENTITY = { kind: "bundle", versionTable: "bundle_versions", headTable: "bundles", fkColumn: "bundle_id" };
|
|
530
|
+
function auditActor2(actor) {
|
|
531
|
+
switch (actor.type) {
|
|
532
|
+
case "human":
|
|
533
|
+
return `user:${actor.userId}`;
|
|
534
|
+
case "service":
|
|
535
|
+
return `service:${actor.serviceId}`;
|
|
536
|
+
case "system":
|
|
537
|
+
return `system:${actor.reason}`;
|
|
538
|
+
case "agent":
|
|
539
|
+
return `agent:${actor.agentSlug}`;
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
function rowToBundleVersion(r) {
|
|
543
|
+
return { version: Number(r.version), ...r.content };
|
|
544
|
+
}
|
|
545
|
+
function makeBundleRepo(ctx) {
|
|
546
|
+
return {
|
|
547
|
+
async head(tenantId, slug) {
|
|
548
|
+
const r = await one(
|
|
549
|
+
ctx.pool,
|
|
550
|
+
`select v.version, v.content from bundles b join bundle_versions v on v.id = b.current_version_id
|
|
551
|
+
where b.org_id=$1 and b.slug=$2`,
|
|
552
|
+
[tenantId, slug]
|
|
553
|
+
);
|
|
554
|
+
return r ? rowToBundleVersion(r) : null;
|
|
555
|
+
},
|
|
556
|
+
async getVersion(tenantId, slug, version) {
|
|
557
|
+
const r = await one(
|
|
558
|
+
ctx.pool,
|
|
559
|
+
`select v.version, v.content from bundles b join bundle_versions v on v.bundle_id = b.id
|
|
560
|
+
where b.org_id=$1 and b.slug=$2 and v.version=$3`,
|
|
561
|
+
[tenantId, slug, version]
|
|
562
|
+
);
|
|
563
|
+
return r ? rowToBundleVersion(r) : null;
|
|
564
|
+
},
|
|
565
|
+
async listSlugs(tenantId) {
|
|
566
|
+
const rows = await many(ctx.pool, "select slug from bundles where org_id=$1", [tenantId]);
|
|
567
|
+
return rows.map((r) => r.slug);
|
|
568
|
+
}
|
|
569
|
+
};
|
|
570
|
+
}
|
|
571
|
+
async function commitBundle(ctx, tenantId, slug, content, action, actor, auditAfter) {
|
|
572
|
+
const client = await ctx.pool.connect();
|
|
573
|
+
try {
|
|
574
|
+
await client.query("begin");
|
|
575
|
+
const bundle = (await client.query(
|
|
576
|
+
`insert into bundles(org_id, slug) values($1,$2)
|
|
577
|
+
on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
|
|
578
|
+
[tenantId, slug]
|
|
579
|
+
)).rows[0];
|
|
580
|
+
const version = await appendVersion(client, BUNDLE_ENTITY, bundle.id, tenantId, async (v) => {
|
|
581
|
+
const row = (await client.query(
|
|
582
|
+
`insert into bundle_versions(bundle_id, org_id, version, content, status) values($1,$2,$3,$4,'published') returning id`,
|
|
583
|
+
[bundle.id, tenantId, v, JSON.stringify(content)]
|
|
584
|
+
)).rows[0];
|
|
585
|
+
return row.id;
|
|
586
|
+
}, action, auditActor2(actor), auditAfter);
|
|
587
|
+
await client.query("commit");
|
|
588
|
+
return { version };
|
|
589
|
+
} catch (e) {
|
|
590
|
+
await client.query("rollback");
|
|
591
|
+
throw e;
|
|
592
|
+
} finally {
|
|
593
|
+
client.release();
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
async function listBundleVersions(ctx, tenantId, slug) {
|
|
597
|
+
const rows = await many(
|
|
598
|
+
ctx.pool,
|
|
599
|
+
`select v.version, v.content, v.status, (v.id = b.current_version_id) as is_current
|
|
600
|
+
from bundles b join bundle_versions v on v.bundle_id = b.id
|
|
601
|
+
where b.org_id=$1 and b.slug=$2 order by v.version`,
|
|
602
|
+
[tenantId, slug]
|
|
603
|
+
);
|
|
604
|
+
return rows.map((r) => ({
|
|
605
|
+
version: Number(r.version),
|
|
606
|
+
title: r.content?.title ?? "",
|
|
607
|
+
status: r.status,
|
|
608
|
+
isCurrent: r.is_current,
|
|
609
|
+
memberCount: Array.isArray(r.content?.agents) ? r.content.agents.length : 0
|
|
610
|
+
}));
|
|
611
|
+
}
|
|
612
|
+
function makeBundleWritableRepo(ctx) {
|
|
613
|
+
const read = makeBundleRepo(ctx);
|
|
614
|
+
return {
|
|
615
|
+
...read,
|
|
616
|
+
async publish(tenantId, slug, content, actor) {
|
|
617
|
+
assertActorMayMutateDefinition2(actor);
|
|
618
|
+
return commitBundle(ctx, tenantId, slug, content, "publish", actor, { slug });
|
|
619
|
+
},
|
|
620
|
+
async rollback(tenantId, slug, toVersion, actor) {
|
|
621
|
+
assertActorMayMutateDefinition2(actor);
|
|
622
|
+
const target = await read.getVersion(tenantId, slug, toVersion);
|
|
623
|
+
if (!target) throw new Error(`cannot roll back bundle ${slug} to v${toVersion}: no such version for this tenant`);
|
|
624
|
+
const { version: _v, ...content } = target;
|
|
625
|
+
const { version } = await commitBundle(ctx, tenantId, slug, content, "rollback", actor, { slug, restoredFrom: toVersion });
|
|
626
|
+
return { version, restoredFrom: toVersion };
|
|
627
|
+
},
|
|
628
|
+
async listVersions(tenantId, slug, actor) {
|
|
629
|
+
assertActorMayMutateDefinition2(actor);
|
|
630
|
+
return listBundleVersions(ctx, tenantId, slug);
|
|
631
|
+
}
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
|
|
419
635
|
// src/mastra-store.ts
|
|
420
636
|
import { PostgresStore, PgVector } from "@mastra/pg";
|
|
421
637
|
function createMastraPgStore(opts) {
|
|
@@ -427,6 +643,78 @@ function createMastraVectorStore(opts) {
|
|
|
427
643
|
return new PgVector({ id: "nightowls-vector", connectionString: opts.dbUrl, schemaName: "nightowls" });
|
|
428
644
|
}
|
|
429
645
|
|
|
646
|
+
// src/usage-sink.ts
|
|
647
|
+
var IDENT = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
648
|
+
var TABLE = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
|
|
649
|
+
function defaultMap(ev, ctx) {
|
|
650
|
+
const b = ev.data.breakdown;
|
|
651
|
+
return {
|
|
652
|
+
generation_id: ev.data.generationId,
|
|
653
|
+
run_id: ctx.runId,
|
|
654
|
+
org_id: ctx.tenantId,
|
|
655
|
+
agent_slug: ev.data.slug,
|
|
656
|
+
model_id: ev.data.modelId,
|
|
657
|
+
input_tokens: b.inputTokens ?? 0,
|
|
658
|
+
output_tokens: b.outputTokens ?? 0,
|
|
659
|
+
cost_usd: ev.data.cost.usd
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
function supabaseUsageSink(opts) {
|
|
663
|
+
const conflict = opts.conflictColumn ?? "generation_id";
|
|
664
|
+
if (!IDENT.test(conflict)) throw new Error(`supabaseUsageSink: unsafe conflictColumn ${JSON.stringify(conflict)}`);
|
|
665
|
+
if (!TABLE.test(opts.table)) throw new Error(`supabaseUsageSink: unsafe table ${JSON.stringify(opts.table)}`);
|
|
666
|
+
const map = opts.map ?? defaultMap;
|
|
667
|
+
return async (ev, ctx) => {
|
|
668
|
+
if (ev.type !== "swarm.usage") return;
|
|
669
|
+
const row = map(ev, ctx);
|
|
670
|
+
if (!row) return;
|
|
671
|
+
const cols = Object.keys(row);
|
|
672
|
+
for (const c of cols) if (!IDENT.test(c)) throw new Error(`supabaseUsageSink: unsafe column ${JSON.stringify(c)}`);
|
|
673
|
+
const placeholders = cols.map((_c, i) => `$${i + 1}`).join(",");
|
|
674
|
+
const sql = `insert into ${opts.table}(${cols.join(",")}) values(${placeholders}) on conflict (${conflict}) do nothing`;
|
|
675
|
+
await opts.pool.query(sql, cols.map((c) => row[c]));
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/host-org-source.ts
|
|
680
|
+
var QUALIFIED = /^[a-zA-Z_][a-zA-Z0-9_]*(\.[a-zA-Z_][a-zA-Z0-9_]*)?$/;
|
|
681
|
+
var PLAIN = /^[a-zA-Z_][a-zA-Z0-9_]*$/;
|
|
682
|
+
function assertIdent(v, kind, re) {
|
|
683
|
+
if (!re.test(v)) throw new Error(`hostOwnedOrgMembership: unsafe ${kind} ${JSON.stringify(v)}`);
|
|
684
|
+
}
|
|
685
|
+
function hostOwnedOrgMembershipSql(opts) {
|
|
686
|
+
const schema = opts.schema ?? "nightowls";
|
|
687
|
+
const orgIdColumn = opts.orgIdColumn ?? "organization_id";
|
|
688
|
+
const userIdColumn = opts.userIdColumn ?? "user_id";
|
|
689
|
+
const userIdExpr = opts.userIdExpr ?? "(select auth.uid())";
|
|
690
|
+
assertIdent(schema, "schema", PLAIN);
|
|
691
|
+
assertIdent(opts.membershipTable, "membershipTable", QUALIFIED);
|
|
692
|
+
assertIdent(orgIdColumn, "orgIdColumn", PLAIN);
|
|
693
|
+
assertIdent(userIdColumn, "userIdColumn", PLAIN);
|
|
694
|
+
if (/;|--|\/\*|\$\$/.test(userIdExpr)) throw new Error(`hostOwnedOrgMembership: unsafe userIdExpr ${JSON.stringify(userIdExpr)}`);
|
|
695
|
+
return `-- FR-015 \u2014 override ${schema}.is_org_member to read the host membership table ${opts.membershipTable}.
|
|
696
|
+
create or replace function ${schema}.is_org_member(p_org uuid)
|
|
697
|
+
returns boolean language sql stable set search_path = '' as $$
|
|
698
|
+
select exists (
|
|
699
|
+
select 1 from ${opts.membershipTable} m
|
|
700
|
+
where m.${orgIdColumn} = p_org and m.${userIdColumn} = ${userIdExpr}
|
|
701
|
+
);
|
|
702
|
+
$$;
|
|
703
|
+
-- The Realtime gate calls is_org_member as the AUTHENTICATED role during a private subscribe, so it must be able
|
|
704
|
+
-- to read the membership table. (No-op if you already granted it.)
|
|
705
|
+
grant select on ${opts.membershipTable} to authenticated;
|
|
706
|
+
`;
|
|
707
|
+
}
|
|
708
|
+
function hostOwnedOrgMigration(opts) {
|
|
709
|
+
const version = opts.version ?? "host_org_source";
|
|
710
|
+
assertIdent(version, "version", PLAIN);
|
|
711
|
+
return {
|
|
712
|
+
version,
|
|
713
|
+
name: `host-owned org/membership source (is_org_member \u2192 ${opts.membershipTable})`,
|
|
714
|
+
sql: hostOwnedOrgMembershipSql(opts)
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
|
|
430
718
|
// src/migrations/0001_core.ts
|
|
431
719
|
var M0001_CORE = {
|
|
432
720
|
version: "0001_core",
|
|
@@ -1553,12 +1841,171 @@ begin
|
|
|
1553
1841
|
execute format('alter index nightowls.%I rename to %I', r.indexname, 'nightowls_' || substring(r.indexname from 8));
|
|
1554
1842
|
end loop;
|
|
1555
1843
|
end $$;
|
|
1844
|
+
|
|
1845
|
+
-- Repoint any function BODY in the (now) nightowls schema that still hardcodes the old 'corale.' schema \u2014
|
|
1846
|
+
-- ALTER SCHEMA RENAME does not rewrite dollar-quoted bodies (see the header note). Recreate each via its own
|
|
1847
|
+
-- definition with 'corale.' \u2192 'nightowls.'. Generic + idempotent: once rewritten, prosrc no longer matches.
|
|
1848
|
+
do $$
|
|
1849
|
+
declare r record;
|
|
1850
|
+
begin
|
|
1851
|
+
for r in
|
|
1852
|
+
select p.oid
|
|
1853
|
+
from pg_proc p
|
|
1854
|
+
join pg_namespace n on n.oid = p.pronamespace
|
|
1855
|
+
where n.nspname = 'nightowls' and p.prosrc like '%corale.%'
|
|
1856
|
+
loop
|
|
1857
|
+
execute replace(pg_get_functiondef(r.oid), 'corale.', 'nightowls.');
|
|
1858
|
+
end loop;
|
|
1859
|
+
end $$;
|
|
1860
|
+
`
|
|
1861
|
+
)
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
// src/migrations/0014_agent_versions_immutable.ts
|
|
1865
|
+
var M0014_AGENT_VERSIONS_IMMUTABLE = {
|
|
1866
|
+
version: "0014_agent_versions_immutable",
|
|
1867
|
+
name: "agent_versions: published rows are append-only (no content UPDATE, no DELETE)",
|
|
1868
|
+
sql: (
|
|
1869
|
+
/* sql */
|
|
1870
|
+
`
|
|
1871
|
+
-- Guard function: reject any mutation that would rewrite or remove a PUBLISHED agent_versions row.
|
|
1872
|
+
create or replace function nightowls.agent_versions_enforce_immutable()
|
|
1873
|
+
returns trigger language plpgsql as $$
|
|
1874
|
+
begin
|
|
1875
|
+
if (tg_op = 'DELETE') then
|
|
1876
|
+
if (old.status = 'published') then
|
|
1877
|
+
raise exception 'agent_versions: published version v% of agent % is immutable (DELETE forbidden \u2014 append-only)',
|
|
1878
|
+
old.version, old.agent_id
|
|
1879
|
+
using errcode = 'restrict_violation';
|
|
1880
|
+
end if;
|
|
1881
|
+
return old;
|
|
1882
|
+
end if;
|
|
1883
|
+
|
|
1884
|
+
-- UPDATE: a published row's CONTENT is frozen. Allow the lifecycle status to advance (e.g. archive), but
|
|
1885
|
+
-- forbid changing any identifying/behavior column once published.
|
|
1886
|
+
if (old.status = 'published') then
|
|
1887
|
+
if ( new.agent_id is distinct from old.agent_id
|
|
1888
|
+
or new.org_id is distinct from old.org_id
|
|
1889
|
+
or new.version is distinct from old.version
|
|
1890
|
+
or new.role is distinct from old.role
|
|
1891
|
+
or new.personality is distinct from old.personality
|
|
1892
|
+
or new.capabilities is distinct from old.capabilities
|
|
1893
|
+
or new.skill_names is distinct from old.skill_names
|
|
1894
|
+
or new.delegate_slugs is distinct from old.delegate_slugs
|
|
1895
|
+
or new.model_id is distinct from old.model_id
|
|
1896
|
+
or new.model_settings is distinct from old.model_settings
|
|
1897
|
+
or new.guardrails_override is distinct from old.guardrails_override
|
|
1898
|
+
or new.created_at is distinct from old.created_at ) then
|
|
1899
|
+
raise exception 'agent_versions: published version v% of agent % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
|
|
1900
|
+
old.version, old.agent_id
|
|
1901
|
+
using errcode = 'restrict_violation';
|
|
1902
|
+
end if;
|
|
1903
|
+
end if;
|
|
1904
|
+
return new;
|
|
1905
|
+
end; $$;
|
|
1906
|
+
|
|
1907
|
+
-- Fire BEFORE every UPDATE/DELETE on agent_versions. (No INSERT trigger \u2014 appends are the whole point.)
|
|
1908
|
+
drop trigger if exists agent_versions_immutable on nightowls.agent_versions;
|
|
1909
|
+
create trigger agent_versions_immutable
|
|
1910
|
+
before update or delete on nightowls.agent_versions
|
|
1911
|
+
for each row execute function nightowls.agent_versions_enforce_immutable();
|
|
1912
|
+
`
|
|
1913
|
+
)
|
|
1914
|
+
};
|
|
1915
|
+
|
|
1916
|
+
// src/migrations/0015_drop_saas_tables.ts
|
|
1917
|
+
var M0015_DROP_SAAS_TABLES = {
|
|
1918
|
+
version: "0015_drop_saas_tables",
|
|
1919
|
+
name: "drop SaaS-only tenant_policies + eval_runs (host-owned, not engine schema)",
|
|
1920
|
+
sql: (
|
|
1921
|
+
/* sql */
|
|
1922
|
+
`
|
|
1923
|
+
drop table if exists nightowls.tenant_policies cascade;
|
|
1924
|
+
drop table if exists nightowls.eval_runs cascade;
|
|
1925
|
+
`
|
|
1926
|
+
)
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
// src/migrations/0016_bundle_versions.ts
|
|
1930
|
+
var M0016_BUNDLE_VERSIONS = {
|
|
1931
|
+
version: "0016_bundle_versions",
|
|
1932
|
+
name: "bundles + bundle_versions (append-only bundle definition versioning) + immutability trigger",
|
|
1933
|
+
sql: (
|
|
1934
|
+
/* sql */
|
|
1935
|
+
`
|
|
1936
|
+
-- Head pointer (mirrors nightowls.agents). NULLS NOT DISTINCT so the common project_id = NULL case still
|
|
1937
|
+
-- collides on (org, slug) \u2014 required for the publish upsert's ON CONFLICT to fire.
|
|
1938
|
+
create table nightowls.bundles (
|
|
1939
|
+
id uuid primary key default gen_random_uuid(),
|
|
1940
|
+
org_id uuid not null references nightowls.orgs(id) on delete cascade,
|
|
1941
|
+
project_id uuid, slug text not null, current_version_id uuid,
|
|
1942
|
+
created_at timestamptz not null default now(),
|
|
1943
|
+
unique nulls not distinct (org_id, project_id, slug)
|
|
1944
|
+
);
|
|
1945
|
+
|
|
1946
|
+
-- Append-only versions (mirrors nightowls.agent_versions; the whole bundle content is a single jsonb snapshot).
|
|
1947
|
+
create table nightowls.bundle_versions (
|
|
1948
|
+
id uuid primary key default gen_random_uuid(),
|
|
1949
|
+
bundle_id uuid not null references nightowls.bundles(id) on delete cascade,
|
|
1950
|
+
org_id uuid not null references nightowls.orgs(id) on delete cascade,
|
|
1951
|
+
version integer not null,
|
|
1952
|
+
content jsonb not null,
|
|
1953
|
+
status text not null default 'draft' check (status in ('draft','published','archived')),
|
|
1954
|
+
created_by uuid, created_at timestamptz not null default now(),
|
|
1955
|
+
unique (bundle_id, version)
|
|
1956
|
+
);
|
|
1957
|
+
alter table nightowls.bundles add constraint bundles_current_version_fk
|
|
1958
|
+
foreign key (current_version_id) references nightowls.bundle_versions(id);
|
|
1959
|
+
|
|
1960
|
+
-- RLS + org indexes (mirrors 0001's per-table pattern for the new tables). The adapter uses the service role
|
|
1961
|
+
-- (RLS-exempt), so this is defense-in-depth for any future direct authenticated-client read: enable RLS, add an
|
|
1962
|
+
-- org-scoped SELECT policy (writes stay service-role-only, like every other definition table), and index org_id.
|
|
1963
|
+
alter table nightowls.bundles enable row level security;
|
|
1964
|
+
alter table nightowls.bundle_versions enable row level security;
|
|
1965
|
+
create policy org_read_bundles on nightowls.bundles
|
|
1966
|
+
for select to authenticated using ((select nightowls.is_org_member(org_id)));
|
|
1967
|
+
create policy org_read_bundle_versions on nightowls.bundle_versions
|
|
1968
|
+
for select to authenticated using ((select nightowls.is_org_member(org_id)));
|
|
1969
|
+
create index bundles_org_idx on nightowls.bundles (org_id);
|
|
1970
|
+
create index bundle_versions_org_idx on nightowls.bundle_versions (org_id);
|
|
1971
|
+
|
|
1972
|
+
-- Published-row immutability (mirrors 0014): once status='published', the content + identity columns are frozen
|
|
1973
|
+
-- and the row may not be DELETEd. The lifecycle status may still advance (e.g. \u2192 'archived'), which doesn't
|
|
1974
|
+
-- rewrite history. No INSERT trigger \u2014 appends are the whole point.
|
|
1975
|
+
create or replace function nightowls.bundle_versions_enforce_immutable()
|
|
1976
|
+
returns trigger language plpgsql as $$
|
|
1977
|
+
begin
|
|
1978
|
+
if (tg_op = 'DELETE') then
|
|
1979
|
+
if (old.status = 'published') then
|
|
1980
|
+
raise exception 'bundle_versions: published version v% of bundle % is immutable (DELETE forbidden \u2014 append-only)',
|
|
1981
|
+
old.version, old.bundle_id using errcode = 'restrict_violation';
|
|
1982
|
+
end if;
|
|
1983
|
+
return old;
|
|
1984
|
+
end if;
|
|
1985
|
+
|
|
1986
|
+
-- UPDATE: a published row's content + identity is frozen; allow only the lifecycle status to advance.
|
|
1987
|
+
if (old.status = 'published') then
|
|
1988
|
+
if ( new.bundle_id is distinct from old.bundle_id
|
|
1989
|
+
or new.org_id is distinct from old.org_id
|
|
1990
|
+
or new.version is distinct from old.version
|
|
1991
|
+
or new.content is distinct from old.content
|
|
1992
|
+
or new.created_at is distinct from old.created_at ) then
|
|
1993
|
+
raise exception 'bundle_versions: published version v% of bundle % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
|
|
1994
|
+
old.version, old.bundle_id using errcode = 'restrict_violation';
|
|
1995
|
+
end if;
|
|
1996
|
+
end if;
|
|
1997
|
+
return new;
|
|
1998
|
+
end; $$;
|
|
1999
|
+
|
|
2000
|
+
drop trigger if exists bundle_versions_immutable on nightowls.bundle_versions;
|
|
2001
|
+
create trigger bundle_versions_immutable before update or delete on nightowls.bundle_versions
|
|
2002
|
+
for each row execute function nightowls.bundle_versions_enforce_immutable();
|
|
1556
2003
|
`
|
|
1557
2004
|
)
|
|
1558
2005
|
};
|
|
1559
2006
|
|
|
1560
2007
|
// src/migrations/index.ts
|
|
1561
|
-
var MIGRATIONS = [M0001_CORE, M0002_MASTRA, M0003_FOLLOWUPS, M0004_MEMORY_VECTOR, M0005_THREAD_TEXT_IDS, M0006_SCRATCHPAD, M0007_SCRATCHPAD_ENTRIES, M0008_FLOOR, M0009_SCRATCHPAD_REALTIME, M0010_PRESENCE_REALTIME, M0011_BROADCAST_ALLOWLIST, M0012_THREAD_SCOPED_RESOURCE, M0013_RENAME_SCHEMA];
|
|
2008
|
+
var MIGRATIONS = [M0001_CORE, M0002_MASTRA, M0003_FOLLOWUPS, M0004_MEMORY_VECTOR, M0005_THREAD_TEXT_IDS, M0006_SCRATCHPAD, M0007_SCRATCHPAD_ENTRIES, M0008_FLOOR, M0009_SCRATCHPAD_REALTIME, M0010_PRESENCE_REALTIME, M0011_BROADCAST_ALLOWLIST, M0012_THREAD_SCOPED_RESOURCE, M0013_RENAME_SCHEMA, M0014_AGENT_VERSIONS_IMMUTABLE, M0015_DROP_SAAS_TABLES, M0016_BUNDLE_VERSIONS];
|
|
1562
2009
|
|
|
1563
2010
|
// src/plugin.ts
|
|
1564
2011
|
var nightOwlsPlugin = {
|
|
@@ -1710,12 +2157,22 @@ function createPostgresFloor(pool, opts = {}) {
|
|
|
1710
2157
|
function createSupabaseStorage(opts) {
|
|
1711
2158
|
const ctx = makeCtx(opts);
|
|
1712
2159
|
let invalidationSub = null;
|
|
2160
|
+
const versionedAgents = makeVersionedRepo(ctx);
|
|
2161
|
+
const writableBundles = makeBundleWritableRepo(ctx);
|
|
1713
2162
|
return {
|
|
1714
|
-
agents:
|
|
2163
|
+
agents: versionedAgents,
|
|
2164
|
+
agentsWritable: versionedAgents,
|
|
2165
|
+
bundles: writableBundles,
|
|
2166
|
+
bundlesWritable: writableBundles,
|
|
1715
2167
|
runs: makeRunStore(ctx),
|
|
1716
2168
|
events: makeEventStore(ctx),
|
|
1717
2169
|
messages: makeMessageStore(ctx),
|
|
2170
|
+
// FR-009: the engine ensures this thread row at run start, so a host no longer hand-writes raw SQL against
|
|
2171
|
+
// `nightowls.threads`, and `messages.append` cannot throw `unknown thread` through the supported path.
|
|
2172
|
+
threads: makeThreadStore(ctx),
|
|
1718
2173
|
scratchpad: makeScratchpadStore(ctx),
|
|
2174
|
+
// FR-016: enumerate the engine's tenants for idempotent per-tenant crew backfill (pairs with ensureAgentVersion).
|
|
2175
|
+
listTenants: () => listTenants(ctx),
|
|
1719
2176
|
// Record the suspended run in the tenant-scoped followup index so `runs.findSuspended` (the resume
|
|
1720
2177
|
// authz gate) can resolve it. RE-OPEN on conflict (reset `answered_at`): Mastra REUSES the same
|
|
1721
2178
|
// `toolCallId` when an agent asks AGAIN after a resume, so the engine's `followupId = runId:toolCallId`
|
|
@@ -1732,10 +2189,11 @@ function createSupabaseStorage(opts) {
|
|
|
1732
2189
|
// Mark a followup answered so `findSuspended` (which filters `answered_at is null`) stops returning
|
|
1733
2190
|
// it — closes the replay window once a resume begins. Tenant-scoped + idempotent.
|
|
1734
2191
|
markFollowupAnswered: async (followupId, tenantId) => {
|
|
1735
|
-
await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
|
|
2192
|
+
const r = await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
|
|
1736
2193
|
followupId,
|
|
1737
2194
|
tenantId
|
|
1738
2195
|
]);
|
|
2196
|
+
return (r.rowCount ?? 0) > 0;
|
|
1739
2197
|
},
|
|
1740
2198
|
// R12: cross-process cache invalidation via Postgres LISTEN. The engine wires this to
|
|
1741
2199
|
// `rowCache.invalidate`; a `publishAgentVersion` elsewhere NOTIFYs the key and every instance evicts
|
|
@@ -1783,8 +2241,17 @@ export {
|
|
|
1783
2241
|
createMastraVectorStore,
|
|
1784
2242
|
createPostgresFloor,
|
|
1785
2243
|
createSupabaseStorage,
|
|
2244
|
+
ensureAgentVersion,
|
|
2245
|
+
hostOwnedOrgMembershipSql,
|
|
2246
|
+
hostOwnedOrgMigration,
|
|
1786
2247
|
listAgentVersions,
|
|
2248
|
+
listTenants,
|
|
2249
|
+
makeBundleRepo,
|
|
2250
|
+
makeBundleWritableRepo,
|
|
2251
|
+
makeThreadStore,
|
|
2252
|
+
makeVersionedRepo,
|
|
1787
2253
|
nightOwlsPlugin,
|
|
1788
2254
|
publishAgentVersion,
|
|
1789
|
-
rollbackAgentVersion
|
|
2255
|
+
rollbackAgentVersion,
|
|
2256
|
+
supabaseUsageSink
|
|
1790
2257
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nightowlsdev/storage-supabase",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "2.0.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"publishConfig": {
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"peerDependencies": {
|
|
34
34
|
"@mastra/core": "^1.38.0",
|
|
35
35
|
"@mastra/pg": "^1.12.0",
|
|
36
|
-
"@nightowlsdev/core": "0.
|
|
36
|
+
"@nightowlsdev/core": "0.5.0"
|
|
37
37
|
},
|
|
38
38
|
"devDependencies": {
|
|
39
39
|
"@mastra/core": "^1.38.0",
|
|
@@ -46,9 +46,9 @@
|
|
|
46
46
|
"typescript": "6.0.3",
|
|
47
47
|
"vitest": "^3.2.0",
|
|
48
48
|
"zod": "^4.0.0",
|
|
49
|
-
"@nightowlsdev/
|
|
50
|
-
"@nightowlsdev/
|
|
51
|
-
"@nightowlsdev/
|
|
49
|
+
"@nightowlsdev/core": "0.5.0",
|
|
50
|
+
"@nightowlsdev/eslint-config": "0.0.0",
|
|
51
|
+
"@nightowlsdev/tsconfig": "0.0.0"
|
|
52
52
|
},
|
|
53
53
|
"scripts": {
|
|
54
54
|
"build": "tsup",
|