@nightowlsdev/storage-supabase 0.3.0 → 1.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/dist/index.cjs CHANGED
@@ -26,6 +26,9 @@ __export(index_exports, {
26
26
  createPostgresFloor: () => createPostgresFloor,
27
27
  createSupabaseStorage: () => createSupabaseStorage,
28
28
  listAgentVersions: () => listAgentVersions,
29
+ makeBundleRepo: () => makeBundleRepo,
30
+ makeBundleWritableRepo: () => makeBundleWritableRepo,
31
+ makeVersionedRepo: () => makeVersionedRepo,
29
32
  nightOwlsPlugin: () => nightOwlsPlugin,
30
33
  publishAgentVersion: () => publishAgentVersion,
31
34
  rollbackAgentVersion: () => rollbackAgentVersion
@@ -146,6 +149,19 @@ function makeEventStore(ctx) {
146
149
  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]);
147
150
  return rows.map(fromRow);
148
151
  },
152
+ // The full ordered log for a CONTAINER: join events → runs and match the container (the root thread_id, OR a
153
+ // lane sub-thread `<container>:<slug>`). `seq` is GENERATED ALWAYS (global, monotonic across all runs), so a
154
+ // plain `order by seq` interleaves every run correctly. Org-scoped (service connection bypasses RLS, so we
155
+ // enforce tenancy here). split_part is injection-safe; a container id never contains ':'.
156
+ async listForContainer(tenantId, container) {
157
+ const rows = await many(
158
+ ctx.pool,
159
+ `select e.run_id, e.seq, e.payload, r.thread_id from events e join runs r on r.id = e.run_id
160
+ where e.org_id = $1 and split_part(r.thread_id, ':', 1) = $2 order by e.seq`,
161
+ [tenantId, container]
162
+ );
163
+ return rows.map((r) => ({ ...fromRow(r), threadId: r.thread_id }));
164
+ },
149
165
  subscribe: makeSubscribe(ctx)
150
166
  };
151
167
  }
@@ -292,6 +308,22 @@ function makeScratchpadStore(ctx) {
292
308
  };
293
309
  }
294
310
 
311
+ // src/agents.ts
312
+ var import_core3 = require("@nightowlsdev/core");
313
+
314
+ // src/versioning.ts
315
+ async function appendVersion(client, entity, headId, tenantId, insertVersionRow, action, actor, auditAfter) {
316
+ await client.query("select pg_advisory_xact_lock(hashtext($1), hashtext($2))", [entity.versionTable, headId]);
317
+ const nextV = (await client.query(`select coalesce(max(version),0)+1 v from ${entity.versionTable} where ${entity.fkColumn}=$1`, [headId])).rows[0].v;
318
+ const versionRowId = await insertVersionRow(nextV);
319
+ await client.query(`update ${entity.headTable} set current_version_id=$2 where id=$1`, [headId, versionRowId]);
320
+ await client.query(
321
+ "insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,$4,$5,$6)",
322
+ [tenantId, actor, action, entity.kind, headId, JSON.stringify({ version: nextV, ...auditAfter })]
323
+ );
324
+ return nextV;
325
+ }
326
+
295
327
  // src/subscribe-invalidations.ts
296
328
  var INVALIDATE_CHANNEL = "nightowls_agent_invalidate";
297
329
  async function listenForInvalidations(ctx, onInvalidate) {
@@ -303,7 +335,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
303
335
  client.on("error", (e) => {
304
336
  client.removeListener("notification", onNotification);
305
337
  console.warn(
306
- "[nightowls] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
338
+ "[@nightowlsdev/storage-supabase] agent-cache invalidation LISTEN connection errored \u2014 cross-process eviction paused until restart; the 30s cache TTL still bounds staleness:",
307
339
  e instanceof Error ? e.message : e
308
340
  );
309
341
  });
@@ -327,6 +359,7 @@ async function listenForInvalidations(ctx, onInvalidate) {
327
359
  }
328
360
 
329
361
  // src/agents.ts
362
+ var AGENT_ENTITY = { kind: "agent", versionTable: "agent_versions", headTable: "agents", fkColumn: "agent_id" };
330
363
  function rowToVersion(r) {
331
364
  return {
332
365
  slug: r.slug,
@@ -339,6 +372,18 @@ function rowToVersion(r) {
339
372
  modelId: r.model_id
340
373
  };
341
374
  }
375
+ function auditActor(actor) {
376
+ switch (actor.type) {
377
+ case "human":
378
+ return `user:${actor.userId}`;
379
+ case "service":
380
+ return `service:${actor.serviceId}`;
381
+ case "system":
382
+ return `system:${actor.reason}`;
383
+ case "agent":
384
+ return `agent:${actor.agentSlug}`;
385
+ }
386
+ }
342
387
  function makeAgentRepo(ctx) {
343
388
  return {
344
389
  async head(tenantId, slug) {
@@ -367,19 +412,32 @@ function makeAgentRepo(ctx) {
367
412
  }
368
413
  };
369
414
  }
415
+ function makeVersionedRepo(ctx) {
416
+ const read = makeAgentRepo(ctx);
417
+ return {
418
+ ...read,
419
+ async publish(tenantId, slug, content, actor) {
420
+ const { version } = await publishAgentVersion(ctx, { tenantId, ...content, slug, actor });
421
+ return { version };
422
+ },
423
+ async rollback(tenantId, slug, toVersion, actor) {
424
+ return rollbackAgentVersion(ctx, { tenantId, slug, toVersion, actor });
425
+ },
426
+ async listVersions(tenantId, slug, actor) {
427
+ (0, import_core3.assertActorMayMutateDefinition)(actor);
428
+ return listAgentVersions(ctx, tenantId, slug);
429
+ }
430
+ };
431
+ }
370
432
  async function commitVersion(client, agentId, tenantId, content, action, actor, auditAfter) {
371
- const nextV = (await client.query("select coalesce(max(version),0)+1 v from agent_versions where agent_id=$1", [agentId])).rows[0].v;
372
- const ver = (await client.query(
373
- `insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
374
- values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
375
- [agentId, tenantId, nextV, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
376
- )).rows[0];
377
- await client.query("update agents set current_version_id=$2 where id=$1", [agentId, ver.id]);
378
- await client.query(
379
- "insert into audit_log(org_id, actor, action, entity, entity_id, after) values($1,$2,$3,'agent',$4,$5)",
380
- [tenantId, actor, action, agentId, JSON.stringify({ version: nextV, ...auditAfter })]
381
- );
382
- return nextV;
433
+ return appendVersion(client, AGENT_ENTITY, agentId, tenantId, async (version) => {
434
+ const ver = (await client.query(
435
+ `insert into agent_versions(agent_id, org_id, version, role, personality, capabilities, skill_names, delegate_slugs, model_id, status)
436
+ values($1,$2,$3,$4,$5,$6,$7,$8,$9,'published') returning id`,
437
+ [agentId, tenantId, version, content.role, content.personality, JSON.stringify(content.capabilities), JSON.stringify(content.skillNames), JSON.stringify(content.delegateSlugs), content.modelId]
438
+ )).rows[0];
439
+ return ver.id;
440
+ }, action, actor, auditAfter);
383
441
  }
384
442
  async function notifyInvalidate(client, tenantId, slug) {
385
443
  try {
@@ -387,7 +445,10 @@ async function notifyInvalidate(client, tenantId, slug) {
387
445
  } catch {
388
446
  }
389
447
  }
448
+ var SEED_ACTOR = { type: "system", reason: "seed" };
390
449
  async function publishAgentVersion(ctx, def) {
450
+ const actor = def.actor ?? SEED_ACTOR;
451
+ (0, import_core3.assertActorMayMutateDefinition)(actor);
391
452
  const client = await ctx.pool.connect();
392
453
  try {
393
454
  await client.query("begin");
@@ -396,9 +457,10 @@ async function publishAgentVersion(ctx, def) {
396
457
  on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
397
458
  [def.tenantId, def.slug, def.role === "orchestrator"]
398
459
  )).rows[0];
399
- await commitVersion(client, agent.id, def.tenantId, def, "publish", def.actor ?? "seed", { slug: def.slug });
460
+ const version = await commitVersion(client, agent.id, def.tenantId, def, "publish", auditActor(actor), { slug: def.slug });
400
461
  await client.query("commit");
401
462
  await notifyInvalidate(client, def.tenantId, def.slug);
463
+ return { version };
402
464
  } catch (e) {
403
465
  await client.query("rollback");
404
466
  throw e;
@@ -417,6 +479,8 @@ async function listAgentVersions(ctx, tenantId, slug) {
417
479
  return rows.map((r) => ({ version: Number(r.version), role: r.role, modelId: r.model_id, status: r.status, isCurrent: r.is_current }));
418
480
  }
419
481
  async function rollbackAgentVersion(ctx, args) {
482
+ const actor = args.actor ?? { type: "system", reason: "rollback" };
483
+ (0, import_core3.assertActorMayMutateDefinition)(actor);
420
484
  const client = await ctx.pool.connect();
421
485
  try {
422
486
  await client.query("begin");
@@ -435,7 +499,7 @@ async function rollbackAgentVersion(ctx, args) {
435
499
  delegateSlugs: target.delegate_slugs ?? [],
436
500
  modelId: target.model_id
437
501
  };
438
- const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", args.actor ?? "rollback", {
502
+ const version = await commitVersion(client, target.agent_id, args.tenantId, content, "rollback", auditActor(actor), {
439
503
  slug: args.slug,
440
504
  restoredFrom: args.toVersion
441
505
  });
@@ -450,6 +514,114 @@ async function rollbackAgentVersion(ctx, args) {
450
514
  }
451
515
  }
452
516
 
517
+ // src/bundles.ts
518
+ var import_core4 = require("@nightowlsdev/core");
519
+ var BUNDLE_ENTITY = { kind: "bundle", versionTable: "bundle_versions", headTable: "bundles", fkColumn: "bundle_id" };
520
+ function auditActor2(actor) {
521
+ switch (actor.type) {
522
+ case "human":
523
+ return `user:${actor.userId}`;
524
+ case "service":
525
+ return `service:${actor.serviceId}`;
526
+ case "system":
527
+ return `system:${actor.reason}`;
528
+ case "agent":
529
+ return `agent:${actor.agentSlug}`;
530
+ }
531
+ }
532
+ function rowToBundleVersion(r) {
533
+ return { version: Number(r.version), ...r.content };
534
+ }
535
+ function makeBundleRepo(ctx) {
536
+ return {
537
+ async head(tenantId, slug) {
538
+ const r = await one(
539
+ ctx.pool,
540
+ `select v.version, v.content from bundles b join bundle_versions v on v.id = b.current_version_id
541
+ where b.org_id=$1 and b.slug=$2`,
542
+ [tenantId, slug]
543
+ );
544
+ return r ? rowToBundleVersion(r) : null;
545
+ },
546
+ async getVersion(tenantId, slug, version) {
547
+ const r = await one(
548
+ ctx.pool,
549
+ `select v.version, v.content from bundles b join bundle_versions v on v.bundle_id = b.id
550
+ where b.org_id=$1 and b.slug=$2 and v.version=$3`,
551
+ [tenantId, slug, version]
552
+ );
553
+ return r ? rowToBundleVersion(r) : null;
554
+ },
555
+ async listSlugs(tenantId) {
556
+ const rows = await many(ctx.pool, "select slug from bundles where org_id=$1", [tenantId]);
557
+ return rows.map((r) => r.slug);
558
+ }
559
+ };
560
+ }
561
+ async function commitBundle(ctx, tenantId, slug, content, action, actor, auditAfter) {
562
+ const client = await ctx.pool.connect();
563
+ try {
564
+ await client.query("begin");
565
+ const bundle = (await client.query(
566
+ `insert into bundles(org_id, slug) values($1,$2)
567
+ on conflict (org_id, project_id, slug) do update set slug=excluded.slug returning id`,
568
+ [tenantId, slug]
569
+ )).rows[0];
570
+ const version = await appendVersion(client, BUNDLE_ENTITY, bundle.id, tenantId, async (v) => {
571
+ const row = (await client.query(
572
+ `insert into bundle_versions(bundle_id, org_id, version, content, status) values($1,$2,$3,$4,'published') returning id`,
573
+ [bundle.id, tenantId, v, JSON.stringify(content)]
574
+ )).rows[0];
575
+ return row.id;
576
+ }, action, auditActor2(actor), auditAfter);
577
+ await client.query("commit");
578
+ return { version };
579
+ } catch (e) {
580
+ await client.query("rollback");
581
+ throw e;
582
+ } finally {
583
+ client.release();
584
+ }
585
+ }
586
+ async function listBundleVersions(ctx, tenantId, slug) {
587
+ const rows = await many(
588
+ ctx.pool,
589
+ `select v.version, v.content, v.status, (v.id = b.current_version_id) as is_current
590
+ from bundles b join bundle_versions v on v.bundle_id = b.id
591
+ where b.org_id=$1 and b.slug=$2 order by v.version`,
592
+ [tenantId, slug]
593
+ );
594
+ return rows.map((r) => ({
595
+ version: Number(r.version),
596
+ title: r.content?.title ?? "",
597
+ status: r.status,
598
+ isCurrent: r.is_current,
599
+ memberCount: Array.isArray(r.content?.agents) ? r.content.agents.length : 0
600
+ }));
601
+ }
602
+ function makeBundleWritableRepo(ctx) {
603
+ const read = makeBundleRepo(ctx);
604
+ return {
605
+ ...read,
606
+ async publish(tenantId, slug, content, actor) {
607
+ (0, import_core4.assertActorMayMutateDefinition)(actor);
608
+ return commitBundle(ctx, tenantId, slug, content, "publish", actor, { slug });
609
+ },
610
+ async rollback(tenantId, slug, toVersion, actor) {
611
+ (0, import_core4.assertActorMayMutateDefinition)(actor);
612
+ const target = await read.getVersion(tenantId, slug, toVersion);
613
+ if (!target) throw new Error(`cannot roll back bundle ${slug} to v${toVersion}: no such version for this tenant`);
614
+ const { version: _v, ...content } = target;
615
+ const { version } = await commitBundle(ctx, tenantId, slug, content, "rollback", actor, { slug, restoredFrom: toVersion });
616
+ return { version, restoredFrom: toVersion };
617
+ },
618
+ async listVersions(tenantId, slug, actor) {
619
+ (0, import_core4.assertActorMayMutateDefinition)(actor);
620
+ return listBundleVersions(ctx, tenantId, slug);
621
+ }
622
+ };
623
+ }
624
+
453
625
  // src/mastra-store.ts
454
626
  var import_pg2 = require("@mastra/pg");
455
627
  function createMastraPgStore(opts) {
@@ -1587,12 +1759,171 @@ begin
1587
1759
  execute format('alter index nightowls.%I rename to %I', r.indexname, 'nightowls_' || substring(r.indexname from 8));
1588
1760
  end loop;
1589
1761
  end $$;
1762
+
1763
+ -- Repoint any function BODY in the (now) nightowls schema that still hardcodes the old 'corale.' schema \u2014
1764
+ -- ALTER SCHEMA RENAME does not rewrite dollar-quoted bodies (see the header note). Recreate each via its own
1765
+ -- definition with 'corale.' \u2192 'nightowls.'. Generic + idempotent: once rewritten, prosrc no longer matches.
1766
+ do $$
1767
+ declare r record;
1768
+ begin
1769
+ for r in
1770
+ select p.oid
1771
+ from pg_proc p
1772
+ join pg_namespace n on n.oid = p.pronamespace
1773
+ where n.nspname = 'nightowls' and p.prosrc like '%corale.%'
1774
+ loop
1775
+ execute replace(pg_get_functiondef(r.oid), 'corale.', 'nightowls.');
1776
+ end loop;
1777
+ end $$;
1778
+ `
1779
+ )
1780
+ };
1781
+
1782
+ // src/migrations/0014_agent_versions_immutable.ts
1783
+ var M0014_AGENT_VERSIONS_IMMUTABLE = {
1784
+ version: "0014_agent_versions_immutable",
1785
+ name: "agent_versions: published rows are append-only (no content UPDATE, no DELETE)",
1786
+ sql: (
1787
+ /* sql */
1788
+ `
1789
+ -- Guard function: reject any mutation that would rewrite or remove a PUBLISHED agent_versions row.
1790
+ create or replace function nightowls.agent_versions_enforce_immutable()
1791
+ returns trigger language plpgsql as $$
1792
+ begin
1793
+ if (tg_op = 'DELETE') then
1794
+ if (old.status = 'published') then
1795
+ raise exception 'agent_versions: published version v% of agent % is immutable (DELETE forbidden \u2014 append-only)',
1796
+ old.version, old.agent_id
1797
+ using errcode = 'restrict_violation';
1798
+ end if;
1799
+ return old;
1800
+ end if;
1801
+
1802
+ -- UPDATE: a published row's CONTENT is frozen. Allow the lifecycle status to advance (e.g. archive), but
1803
+ -- forbid changing any identifying/behavior column once published.
1804
+ if (old.status = 'published') then
1805
+ if ( new.agent_id is distinct from old.agent_id
1806
+ or new.org_id is distinct from old.org_id
1807
+ or new.version is distinct from old.version
1808
+ or new.role is distinct from old.role
1809
+ or new.personality is distinct from old.personality
1810
+ or new.capabilities is distinct from old.capabilities
1811
+ or new.skill_names is distinct from old.skill_names
1812
+ or new.delegate_slugs is distinct from old.delegate_slugs
1813
+ or new.model_id is distinct from old.model_id
1814
+ or new.model_settings is distinct from old.model_settings
1815
+ or new.guardrails_override is distinct from old.guardrails_override
1816
+ or new.created_at is distinct from old.created_at ) then
1817
+ raise exception 'agent_versions: published version v% of agent % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
1818
+ old.version, old.agent_id
1819
+ using errcode = 'restrict_violation';
1820
+ end if;
1821
+ end if;
1822
+ return new;
1823
+ end; $$;
1824
+
1825
+ -- Fire BEFORE every UPDATE/DELETE on agent_versions. (No INSERT trigger \u2014 appends are the whole point.)
1826
+ drop trigger if exists agent_versions_immutable on nightowls.agent_versions;
1827
+ create trigger agent_versions_immutable
1828
+ before update or delete on nightowls.agent_versions
1829
+ for each row execute function nightowls.agent_versions_enforce_immutable();
1830
+ `
1831
+ )
1832
+ };
1833
+
1834
+ // src/migrations/0015_drop_saas_tables.ts
1835
+ var M0015_DROP_SAAS_TABLES = {
1836
+ version: "0015_drop_saas_tables",
1837
+ name: "drop SaaS-only tenant_policies + eval_runs (host-owned, not engine schema)",
1838
+ sql: (
1839
+ /* sql */
1840
+ `
1841
+ drop table if exists nightowls.tenant_policies cascade;
1842
+ drop table if exists nightowls.eval_runs cascade;
1843
+ `
1844
+ )
1845
+ };
1846
+
1847
+ // src/migrations/0016_bundle_versions.ts
1848
+ var M0016_BUNDLE_VERSIONS = {
1849
+ version: "0016_bundle_versions",
1850
+ name: "bundles + bundle_versions (append-only bundle definition versioning) + immutability trigger",
1851
+ sql: (
1852
+ /* sql */
1853
+ `
1854
+ -- Head pointer (mirrors nightowls.agents). NULLS NOT DISTINCT so the common project_id = NULL case still
1855
+ -- collides on (org, slug) \u2014 required for the publish upsert's ON CONFLICT to fire.
1856
+ create table nightowls.bundles (
1857
+ id uuid primary key default gen_random_uuid(),
1858
+ org_id uuid not null references nightowls.orgs(id) on delete cascade,
1859
+ project_id uuid, slug text not null, current_version_id uuid,
1860
+ created_at timestamptz not null default now(),
1861
+ unique nulls not distinct (org_id, project_id, slug)
1862
+ );
1863
+
1864
+ -- Append-only versions (mirrors nightowls.agent_versions; the whole bundle content is a single jsonb snapshot).
1865
+ create table nightowls.bundle_versions (
1866
+ id uuid primary key default gen_random_uuid(),
1867
+ bundle_id uuid not null references nightowls.bundles(id) on delete cascade,
1868
+ org_id uuid not null references nightowls.orgs(id) on delete cascade,
1869
+ version integer not null,
1870
+ content jsonb not null,
1871
+ status text not null default 'draft' check (status in ('draft','published','archived')),
1872
+ created_by uuid, created_at timestamptz not null default now(),
1873
+ unique (bundle_id, version)
1874
+ );
1875
+ alter table nightowls.bundles add constraint bundles_current_version_fk
1876
+ foreign key (current_version_id) references nightowls.bundle_versions(id);
1877
+
1878
+ -- RLS + org indexes (mirrors 0001's per-table pattern for the new tables). The adapter uses the service role
1879
+ -- (RLS-exempt), so this is defense-in-depth for any future direct authenticated-client read: enable RLS, add an
1880
+ -- org-scoped SELECT policy (writes stay service-role-only, like every other definition table), and index org_id.
1881
+ alter table nightowls.bundles enable row level security;
1882
+ alter table nightowls.bundle_versions enable row level security;
1883
+ create policy org_read_bundles on nightowls.bundles
1884
+ for select to authenticated using ((select nightowls.is_org_member(org_id)));
1885
+ create policy org_read_bundle_versions on nightowls.bundle_versions
1886
+ for select to authenticated using ((select nightowls.is_org_member(org_id)));
1887
+ create index bundles_org_idx on nightowls.bundles (org_id);
1888
+ create index bundle_versions_org_idx on nightowls.bundle_versions (org_id);
1889
+
1890
+ -- Published-row immutability (mirrors 0014): once status='published', the content + identity columns are frozen
1891
+ -- and the row may not be DELETEd. The lifecycle status may still advance (e.g. \u2192 'archived'), which doesn't
1892
+ -- rewrite history. No INSERT trigger \u2014 appends are the whole point.
1893
+ create or replace function nightowls.bundle_versions_enforce_immutable()
1894
+ returns trigger language plpgsql as $$
1895
+ begin
1896
+ if (tg_op = 'DELETE') then
1897
+ if (old.status = 'published') then
1898
+ raise exception 'bundle_versions: published version v% of bundle % is immutable (DELETE forbidden \u2014 append-only)',
1899
+ old.version, old.bundle_id using errcode = 'restrict_violation';
1900
+ end if;
1901
+ return old;
1902
+ end if;
1903
+
1904
+ -- UPDATE: a published row's content + identity is frozen; allow only the lifecycle status to advance.
1905
+ if (old.status = 'published') then
1906
+ if ( new.bundle_id is distinct from old.bundle_id
1907
+ or new.org_id is distinct from old.org_id
1908
+ or new.version is distinct from old.version
1909
+ or new.content is distinct from old.content
1910
+ or new.created_at is distinct from old.created_at ) then
1911
+ raise exception 'bundle_versions: published version v% of bundle % is immutable (content UPDATE forbidden \u2014 append-only; republish a new version instead)',
1912
+ old.version, old.bundle_id using errcode = 'restrict_violation';
1913
+ end if;
1914
+ end if;
1915
+ return new;
1916
+ end; $$;
1917
+
1918
+ drop trigger if exists bundle_versions_immutable on nightowls.bundle_versions;
1919
+ create trigger bundle_versions_immutable before update or delete on nightowls.bundle_versions
1920
+ for each row execute function nightowls.bundle_versions_enforce_immutable();
1590
1921
  `
1591
1922
  )
1592
1923
  };
1593
1924
 
1594
1925
  // src/migrations/index.ts
1595
- 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];
1926
+ 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];
1596
1927
 
1597
1928
  // src/plugin.ts
1598
1929
  var nightOwlsPlugin = {
@@ -1744,8 +2075,13 @@ function createPostgresFloor(pool, opts = {}) {
1744
2075
  function createSupabaseStorage(opts) {
1745
2076
  const ctx = makeCtx(opts);
1746
2077
  let invalidationSub = null;
2078
+ const versionedAgents = makeVersionedRepo(ctx);
2079
+ const writableBundles = makeBundleWritableRepo(ctx);
1747
2080
  return {
1748
- agents: makeAgentRepo(ctx),
2081
+ agents: versionedAgents,
2082
+ agentsWritable: versionedAgents,
2083
+ bundles: writableBundles,
2084
+ bundlesWritable: writableBundles,
1749
2085
  runs: makeRunStore(ctx),
1750
2086
  events: makeEventStore(ctx),
1751
2087
  messages: makeMessageStore(ctx),
@@ -1766,10 +2102,11 @@ function createSupabaseStorage(opts) {
1766
2102
  // Mark a followup answered so `findSuspended` (which filters `answered_at is null`) stops returning
1767
2103
  // it — closes the replay window once a resume begins. Tenant-scoped + idempotent.
1768
2104
  markFollowupAnswered: async (followupId, tenantId) => {
1769
- await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
2105
+ const r = await ctx.pool.query("update followups set answered_at = now() where id=$1 and org_id=$2 and answered_at is null", [
1770
2106
  followupId,
1771
2107
  tenantId
1772
2108
  ]);
2109
+ return (r.rowCount ?? 0) > 0;
1773
2110
  },
1774
2111
  // R12: cross-process cache invalidation via Postgres LISTEN. The engine wires this to
1775
2112
  // `rowCache.invalidate`; a `publishAgentVersion` elsewhere NOTIFYs the key and every instance evicts
@@ -1819,6 +2156,9 @@ function createSupabaseStorage(opts) {
1819
2156
  createPostgresFloor,
1820
2157
  createSupabaseStorage,
1821
2158
  listAgentVersions,
2159
+ makeBundleRepo,
2160
+ makeBundleWritableRepo,
2161
+ makeVersionedRepo,
1822
2162
  nightOwlsPlugin,
1823
2163
  publishAgentVersion,
1824
2164
  rollbackAgentVersion
package/dist/index.d.cts CHANGED
@@ -1,4 +1,5 @@
1
- import { AgentVersion, ContainerFloor, StorageAdapter } from '@nightowlsdev/core';
1
+ import { AgentVersionContent, SwarmActor, AgentVersionInfo, VersionedRepo, BundleRepo, BundleWritableRepo, ContainerFloor, StorageAdapter } from '@nightowlsdev/core';
2
+ export { AgentVersionInfo } from '@nightowlsdev/core';
2
3
  import { Pool } from 'pg';
3
4
  import { SupabaseClient } from '@supabase/supabase-js';
4
5
  import { PostgresStore, PgVector } from '@mastra/pg';
@@ -30,23 +31,31 @@ declare function createMastraVectorStore(opts: {
30
31
  dbUrl: string;
31
32
  }): PgVector;
32
33
 
33
- /** The shape callers pass — `version` is derived, not supplied. */
34
- type PublishAgentDef = Omit<AgentVersion, "version"> & {
34
+ /**
35
+ * The WRITABLE agent definition repo (SP6) — the core `VersionedRepo` contract backed by Postgres. Extends the
36
+ * read-only repo with the append-only `publish`/`rollback`/`listVersions` surface. The supabase adapter wires
37
+ * its `agents` field to this (a `VersionedRepo` IS an `AgentRepo`) and also surfaces it as `agentsWritable`.
38
+ *
39
+ * SECURITY: every mutation calls `assertActorMayMutateDefinition(actor)` FIRST (inside the standalone
40
+ * functions) — the non-bypassable agent-bar. The removable `guardDefinitionMutation` POLICY hook is layered on
41
+ * by the caller that owns a `HookDispatcher` (the no-code builder); the storage layer only enforces the
42
+ * framework invariant, which no hook can weaken.
43
+ */
44
+ declare function makeVersionedRepo(ctx: Ctx$1): VersionedRepo;
45
+ /** The shape callers pass — `version` is derived, not supplied. `actor` is the typed `SwarmActor` principal
46
+ * (the agent-bar inspects it); omit it ⇒ the seed system actor. */
47
+ type PublishAgentDef = AgentVersionContent & {
35
48
  tenantId: string;
36
- actor?: string;
49
+ actor?: SwarmActor;
37
50
  version?: number;
38
51
  };
39
- /** Insert a new agent version, flip the head pointer, audit. Used by seeding/CLI (not in the core interface).
40
- * Takes the typed `Ctx` directly (no `__ctx` any-cast); call via `publishAgentVersion(storage.ctx, def)`. */
41
- declare function publishAgentVersion(ctx: Ctx$1, def: PublishAgentDef): Promise<void>;
42
- /** One row of an agent's version history (for rollback UX). */
43
- interface AgentVersionInfo {
52
+ /** Insert a new agent version, flip the head pointer, audit. Used by seeding/CLI (and `makeVersionedRepo`).
53
+ * Takes the typed `Ctx` directly (no `__ctx` any-cast); call via `publishAgentVersion(storage.ctx, def)`.
54
+ * Enforces the non-bypassable agent-bar (SP6): an `agent` principal can NEVER publish. Returns the derived
55
+ * (max+1) version number. */
56
+ declare function publishAgentVersion(ctx: Ctx$1, def: PublishAgentDef): Promise<{
44
57
  version: number;
45
- role: string;
46
- modelId: string;
47
- status: string;
48
- isCurrent: boolean;
49
- }
58
+ }>;
50
59
  /** List an agent's versions (oldest→newest), flagging the current head. Tenant-scoped. Read-only — use it to
51
60
  * pick a `toVersion` for `rollbackAgentVersion`. Returns [] for an unknown agent. */
52
61
  declare function listAgentVersions(ctx: Ctx$1, tenantId: string, slug: string): Promise<AgentVersionInfo[]>;
@@ -61,12 +70,22 @@ declare function rollbackAgentVersion(ctx: Ctx$1, args: {
61
70
  tenantId: string;
62
71
  slug: string;
63
72
  toVersion: number;
64
- actor?: string;
73
+ actor?: SwarmActor;
65
74
  }): Promise<{
66
75
  version: number;
67
76
  restoredFrom: number;
68
77
  }>;
69
78
 
79
+ /** The READ-ONLY bundle repo (head/getVersion/listSlugs). The writable surface lives in `makeBundleWritableRepo`. */
80
+ declare function makeBundleRepo(ctx: Ctx$1): BundleRepo;
81
+ /**
82
+ * The WRITABLE bundle definition repo (BN2) — the core `BundleWritableRepo` contract backed by Postgres. Extends
83
+ * the read repo with the append-only `publish`/`rollback`/`listVersions` surface, on the SAME shared
84
+ * `appendVersion` primitive as agents (so a bundle gets the same race-safe append-and-flip + audit + the
85
+ * published-row immutability trigger). Every mutation enforces the non-bypassable agent-bar first.
86
+ */
87
+ declare function makeBundleWritableRepo(ctx: Ctx$1): BundleWritableRepo;
88
+
70
89
  /** A single Night Owls migration: a stable `version`, a human `name`, and fully-qualified `nightowls.*` SQL.
71
90
  * Night Owls contributes these to the host's `supabase/migrations/` (via `owl install`/`db eject`);
72
91
  * the host applies them with its own tooling — Night Owls never runs DDL itself. */
@@ -162,4 +181,4 @@ interface SupabaseStorage extends StorageAdapter {
162
181
  }
163
182
  declare function createSupabaseStorage(opts: SupabaseStorageOpts): SupabaseStorage;
164
183
 
165
- export { type AgentVersionInfo, type Ctx$1 as Ctx, MIGRATIONS, type Migration, type PostgresFloorOpts, type SupabaseStorage, type SupabaseStorageOpts, createMastraPgStore, createMastraVectorStore, createPostgresFloor, createSupabaseStorage, listAgentVersions, nightOwlsPlugin, publishAgentVersion, rollbackAgentVersion };
184
+ export { type Ctx$1 as Ctx, MIGRATIONS, type Migration, type PostgresFloorOpts, type PublishAgentDef, type SupabaseStorage, type SupabaseStorageOpts, createMastraPgStore, createMastraVectorStore, createPostgresFloor, createSupabaseStorage, listAgentVersions, makeBundleRepo, makeBundleWritableRepo, makeVersionedRepo, nightOwlsPlugin, publishAgentVersion, rollbackAgentVersion };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
- import { AgentVersion, ContainerFloor, StorageAdapter } from '@nightowlsdev/core';
1
+ import { AgentVersionContent, SwarmActor, AgentVersionInfo, VersionedRepo, BundleRepo, BundleWritableRepo, ContainerFloor, StorageAdapter } from '@nightowlsdev/core';
2
+ export { AgentVersionInfo } from '@nightowlsdev/core';
2
3
  import { Pool } from 'pg';
3
4
  import { SupabaseClient } from '@supabase/supabase-js';
4
5
  import { PostgresStore, PgVector } from '@mastra/pg';
@@ -30,23 +31,31 @@ declare function createMastraVectorStore(opts: {
30
31
  dbUrl: string;
31
32
  }): PgVector;
32
33
 
33
- /** The shape callers pass — `version` is derived, not supplied. */
34
- type PublishAgentDef = Omit<AgentVersion, "version"> & {
34
+ /**
35
+ * The WRITABLE agent definition repo (SP6) — the core `VersionedRepo` contract backed by Postgres. Extends the
36
+ * read-only repo with the append-only `publish`/`rollback`/`listVersions` surface. The supabase adapter wires
37
+ * its `agents` field to this (a `VersionedRepo` IS an `AgentRepo`) and also surfaces it as `agentsWritable`.
38
+ *
39
+ * SECURITY: every mutation calls `assertActorMayMutateDefinition(actor)` FIRST (inside the standalone
40
+ * functions) — the non-bypassable agent-bar. The removable `guardDefinitionMutation` POLICY hook is layered on
41
+ * by the caller that owns a `HookDispatcher` (the no-code builder); the storage layer only enforces the
42
+ * framework invariant, which no hook can weaken.
43
+ */
44
+ declare function makeVersionedRepo(ctx: Ctx$1): VersionedRepo;
45
+ /** The shape callers pass — `version` is derived, not supplied. `actor` is the typed `SwarmActor` principal
46
+ * (the agent-bar inspects it); omit it ⇒ the seed system actor. */
47
+ type PublishAgentDef = AgentVersionContent & {
35
48
  tenantId: string;
36
- actor?: string;
49
+ actor?: SwarmActor;
37
50
  version?: number;
38
51
  };
39
- /** Insert a new agent version, flip the head pointer, audit. Used by seeding/CLI (not in the core interface).
40
- * Takes the typed `Ctx` directly (no `__ctx` any-cast); call via `publishAgentVersion(storage.ctx, def)`. */
41
- declare function publishAgentVersion(ctx: Ctx$1, def: PublishAgentDef): Promise<void>;
42
- /** One row of an agent's version history (for rollback UX). */
43
- interface AgentVersionInfo {
52
+ /** Insert a new agent version, flip the head pointer, audit. Used by seeding/CLI (and `makeVersionedRepo`).
53
+ * Takes the typed `Ctx` directly (no `__ctx` any-cast); call via `publishAgentVersion(storage.ctx, def)`.
54
+ * Enforces the non-bypassable agent-bar (SP6): an `agent` principal can NEVER publish. Returns the derived
55
+ * (max+1) version number. */
56
+ declare function publishAgentVersion(ctx: Ctx$1, def: PublishAgentDef): Promise<{
44
57
  version: number;
45
- role: string;
46
- modelId: string;
47
- status: string;
48
- isCurrent: boolean;
49
- }
58
+ }>;
50
59
  /** List an agent's versions (oldest→newest), flagging the current head. Tenant-scoped. Read-only — use it to
51
60
  * pick a `toVersion` for `rollbackAgentVersion`. Returns [] for an unknown agent. */
52
61
  declare function listAgentVersions(ctx: Ctx$1, tenantId: string, slug: string): Promise<AgentVersionInfo[]>;
@@ -61,12 +70,22 @@ declare function rollbackAgentVersion(ctx: Ctx$1, args: {
61
70
  tenantId: string;
62
71
  slug: string;
63
72
  toVersion: number;
64
- actor?: string;
73
+ actor?: SwarmActor;
65
74
  }): Promise<{
66
75
  version: number;
67
76
  restoredFrom: number;
68
77
  }>;
69
78
 
79
+ /** The READ-ONLY bundle repo (head/getVersion/listSlugs). The writable surface lives in `makeBundleWritableRepo`. */
80
+ declare function makeBundleRepo(ctx: Ctx$1): BundleRepo;
81
+ /**
82
+ * The WRITABLE bundle definition repo (BN2) — the core `BundleWritableRepo` contract backed by Postgres. Extends
83
+ * the read repo with the append-only `publish`/`rollback`/`listVersions` surface, on the SAME shared
84
+ * `appendVersion` primitive as agents (so a bundle gets the same race-safe append-and-flip + audit + the
85
+ * published-row immutability trigger). Every mutation enforces the non-bypassable agent-bar first.
86
+ */
87
+ declare function makeBundleWritableRepo(ctx: Ctx$1): BundleWritableRepo;
88
+
70
89
  /** A single Night Owls migration: a stable `version`, a human `name`, and fully-qualified `nightowls.*` SQL.
71
90
  * Night Owls contributes these to the host's `supabase/migrations/` (via `owl install`/`db eject`);
72
91
  * the host applies them with its own tooling — Night Owls never runs DDL itself. */
@@ -162,4 +181,4 @@ interface SupabaseStorage extends StorageAdapter {
162
181
  }
163
182
  declare function createSupabaseStorage(opts: SupabaseStorageOpts): SupabaseStorage;
164
183
 
165
- export { type AgentVersionInfo, type Ctx$1 as Ctx, MIGRATIONS, type Migration, type PostgresFloorOpts, type SupabaseStorage, type SupabaseStorageOpts, createMastraPgStore, createMastraVectorStore, createPostgresFloor, createSupabaseStorage, listAgentVersions, nightOwlsPlugin, publishAgentVersion, rollbackAgentVersion };
184
+ export { type Ctx$1 as Ctx, MIGRATIONS, type Migration, type PostgresFloorOpts, type PublishAgentDef, type SupabaseStorage, type SupabaseStorageOpts, createMastraPgStore, createMastraVectorStore, createPostgresFloor, createSupabaseStorage, listAgentVersions, makeBundleRepo, makeBundleWritableRepo, makeVersionedRepo, nightOwlsPlugin, publishAgentVersion, rollbackAgentVersion };