History

Add artifact table and core artifacts API

Rhys Sullivan committed 3 months ago

Commit 2567d781 parent 54df2e321 files · +2322 −2Browse files

Unified · Split

21 files
  1. apps/cloud/drizzle/0013_curly_rocket_racer.sql +15 −0
  2. apps/cloud/drizzle/meta/0013_snapshot.json +1474 −0
  3. apps/cloud/drizzle/meta/_journal.json +7 −0
  4. apps/cloud/src/db/executor-schema.ts +20 −0
  5. apps/cloud/src/db/org-deletion.test.ts +13 −0
  6. apps/cloud/src/db/org-deletion.ts +2 −0
  7. packages/core/api/src/api.ts +2 −0
  8. packages/core/api/src/artifacts/api.ts +95 −0
  9. packages/core/api/src/handlers/artifacts.ts +78 −0
  10. packages/core/api/src/handlers/index.ts +3 −0
  11. packages/core/api/src/index.ts +1 −0
  12. packages/core/api/src/server.ts +1 −0
  13. packages/core/sdk/src/artifact-table.test.ts +131 −0
  14. packages/core/sdk/src/artifact.ts +63 −0
  15. packages/core/sdk/src/artifacts.test.ts +197 −0
  16. packages/core/sdk/src/core-schema.ts +34 −0
  17. packages/core/sdk/src/errors.ts +25 −2
  18. packages/core/sdk/src/executor.ts +132 −0
  19. packages/core/sdk/src/ids.ts +4 −0
  20. packages/core/sdk/src/index.ts +14 −0
  21. packages/core/sdk/src/shared.ts +11 −0
apps/cloud/drizzle/0013_curly_rocket_racer.sql+15−0
@@ -0,0 +1,15 @@
1+CREATE TABLE "artifact" (
2+ "id" varchar(255) NOT NULL,
3+ "title" text NOT NULL,
4+ "description" text,
5+ "code" text NOT NULL,
6+ "created_at" timestamp NOT NULL,
7+ "updated_at" timestamp NOT NULL,
8+ "row_id" varchar(255) PRIMARY KEY NOT NULL,
9+ "tenant" varchar(255) NOT NULL,
10+ "owner" varchar(255) NOT NULL,
11+ "subject" varchar(255) NOT NULL
12+);
13+--> statement-breakpoint
14+ALTER TABLE "definition" ALTER COLUMN "name" SET DATA TYPE varchar(255);--> statement-breakpoint
15+CREATE UNIQUE INDEX "artifact_uidx" ON "artifact" USING btree ("tenant","owner","subject","id");
apps/cloud/drizzle/meta/0013_snapshot.json+1474−0
@@ -0,0 +1,1474 @@
1+{
2+ "id": "edc90067-822c-443a-8fcd-3bac77bca317",
3+ "prevId": "97114994-f0fc-4419-80bf-21c529a6cf0b",
4+ "version": "7",
5+ "dialect": "postgresql",
6+ "tables": {
7+ "public.accounts": {
8+ "name": "accounts",
9+ "schema": "",
10+ "columns": {
11+ "id": {
12+ "name": "id",
13+ "type": "text",
14+ "primaryKey": true,
15+ "notNull": true
16+ },
17+ "created_at": {
18+ "name": "created_at",
19+ "type": "timestamp with time zone",
20+ "primaryKey": false,
21+ "notNull": true,
22+ "default": "now()"
23+ }
24+ },
25+ "indexes": {},
26+ "foreignKeys": {},
27+ "compositePrimaryKeys": {},
28+ "uniqueConstraints": {},
29+ "policies": {},
30+ "checkConstraints": {},
31+ "isRLSEnabled": false
32+ },
33+ "public.memberships": {
34+ "name": "memberships",
35+ "schema": "",
36+ "columns": {
37+ "account_id": {
38+ "name": "account_id",
39+ "type": "text",
40+ "primaryKey": false,
41+ "notNull": true
42+ },
43+ "organization_id": {
44+ "name": "organization_id",
45+ "type": "text",
46+ "primaryKey": false,
47+ "notNull": true
48+ },
49+ "created_at": {
50+ "name": "created_at",
51+ "type": "timestamp with time zone",
52+ "primaryKey": false,
53+ "notNull": true,
54+ "default": "now()"
55+ }
56+ },
57+ "indexes": {},
58+ "foreignKeys": {
59+ "memberships_account_id_accounts_id_fk": {
60+ "name": "memberships_account_id_accounts_id_fk",
61+ "tableFrom": "memberships",
62+ "tableTo": "accounts",
63+ "columnsFrom": ["account_id"],
64+ "columnsTo": ["id"],
65+ "onDelete": "cascade",
66+ "onUpdate": "no action"
67+ },
68+ "memberships_organization_id_organizations_id_fk": {
69+ "name": "memberships_organization_id_organizations_id_fk",
70+ "tableFrom": "memberships",
71+ "tableTo": "organizations",
72+ "columnsFrom": ["organization_id"],
73+ "columnsTo": ["id"],
74+ "onDelete": "cascade",
75+ "onUpdate": "no action"
76+ }
77+ },
78+ "compositePrimaryKeys": {
79+ "memberships_account_id_organization_id_pk": {
80+ "name": "memberships_account_id_organization_id_pk",
81+ "columns": ["account_id", "organization_id"]
82+ }
83+ },
84+ "uniqueConstraints": {},
85+ "policies": {},
86+ "checkConstraints": {},
87+ "isRLSEnabled": false
88+ },
89+ "public.organizations": {
90+ "name": "organizations",
91+ "schema": "",
92+ "columns": {
93+ "id": {
94+ "name": "id",
95+ "type": "text",
96+ "primaryKey": true,
97+ "notNull": true
98+ },
99+ "name": {
100+ "name": "name",
101+ "type": "text",
102+ "primaryKey": false,
103+ "notNull": true
104+ },
105+ "slug": {
106+ "name": "slug",
107+ "type": "text",
108+ "primaryKey": false,
109+ "notNull": true
110+ },
111+ "created_at": {
112+ "name": "created_at",
113+ "type": "timestamp with time zone",
114+ "primaryKey": false,
115+ "notNull": true,
116+ "default": "now()"
117+ }
118+ },
119+ "indexes": {
120+ "organizations_slug_unique": {
121+ "name": "organizations_slug_unique",
122+ "columns": [
123+ {
124+ "expression": "slug",
125+ "isExpression": false,
126+ "asc": true,
127+ "nulls": "last"
128+ }
129+ ],
130+ "isUnique": true,
131+ "concurrently": false,
132+ "method": "btree",
133+ "with": {}
134+ }
135+ },
136+ "foreignKeys": {},
137+ "compositePrimaryKeys": {},
138+ "uniqueConstraints": {},
139+ "policies": {},
140+ "checkConstraints": {},
141+ "isRLSEnabled": false
142+ },
143+ "public.artifact": {
144+ "name": "artifact",
145+ "schema": "",
146+ "columns": {
147+ "id": {
148+ "name": "id",
149+ "type": "varchar(255)",
150+ "primaryKey": false,
151+ "notNull": true
152+ },
153+ "title": {
154+ "name": "title",
155+ "type": "text",
156+ "primaryKey": false,
157+ "notNull": true
158+ },
159+ "description": {
160+ "name": "description",
161+ "type": "text",
162+ "primaryKey": false,
163+ "notNull": false
164+ },
165+ "code": {
166+ "name": "code",
167+ "type": "text",
168+ "primaryKey": false,
169+ "notNull": true
170+ },
171+ "created_at": {
172+ "name": "created_at",
173+ "type": "timestamp",
174+ "primaryKey": false,
175+ "notNull": true
176+ },
177+ "updated_at": {
178+ "name": "updated_at",
179+ "type": "timestamp",
180+ "primaryKey": false,
181+ "notNull": true
182+ },
183+ "row_id": {
184+ "name": "row_id",
185+ "type": "varchar(255)",
186+ "primaryKey": true,
187+ "notNull": true
188+ },
189+ "tenant": {
190+ "name": "tenant",
191+ "type": "varchar(255)",
192+ "primaryKey": false,
193+ "notNull": true
194+ },
195+ "owner": {
196+ "name": "owner",
197+ "type": "varchar(255)",
198+ "primaryKey": false,
199+ "notNull": true
200+ },
201+ "subject": {
202+ "name": "subject",
203+ "type": "varchar(255)",
204+ "primaryKey": false,
205+ "notNull": true
206+ }
207+ },
208+ "indexes": {
209+ "artifact_uidx": {
210+ "name": "artifact_uidx",
211+ "columns": [
212+ {
213+ "expression": "tenant",
214+ "isExpression": false,
215+ "asc": true,
216+ "nulls": "last"
217+ },
218+ {
219+ "expression": "owner",
220+ "isExpression": false,
221+ "asc": true,
222+ "nulls": "last"
223+ },
224+ {
225+ "expression": "subject",
226+ "isExpression": false,
227+ "asc": true,
228+ "nulls": "last"
229+ },
230+ {
231+ "expression": "id",
232+ "isExpression": false,
233+ "asc": true,
234+ "nulls": "last"
235+ }
236+ ],
237+ "isUnique": true,
238+ "concurrently": false,
239+ "method": "btree",
240+ "with": {}
241+ }
242+ },
243+ "foreignKeys": {},
244+ "compositePrimaryKeys": {},
245+ "uniqueConstraints": {},
246+ "policies": {},
247+ "checkConstraints": {},
248+ "isRLSEnabled": false
249+ },
250+ "public.blob": {
251+ "name": "blob",
252+ "schema": "",
253+ "columns": {
254+ "namespace": {
255+ "name": "namespace",
256+ "type": "varchar(255)",
257+ "primaryKey": false,
258+ "notNull": true
259+ },
260+ "key": {
261+ "name": "key",
262+ "type": "varchar(255)",
263+ "primaryKey": false,
264+ "notNull": true
265+ },
266+ "value": {
267+ "name": "value",
268+ "type": "text",
269+ "primaryKey": false,
270+ "notNull": true
271+ },
272+ "row_id": {
273+ "name": "row_id",
274+ "type": "varchar(255)",
275+ "primaryKey": true,
276+ "notNull": true
277+ },
278+ "id": {
279+ "name": "id",
280+ "type": "varchar(255)",
281+ "primaryKey": false,
282+ "notNull": true
283+ }
284+ },
285+ "indexes": {
286+ "blob_id_uidx": {
287+ "name": "blob_id_uidx",
288+ "columns": [
289+ {
290+ "expression": "id",
291+ "isExpression": false,
292+ "asc": true,
293+ "nulls": "last"
294+ }
295+ ],
296+ "isUnique": true,
297+ "concurrently": false,
298+ "method": "btree",
299+ "with": {}
300+ }
301+ },
302+ "foreignKeys": {},
303+ "compositePrimaryKeys": {},
304+ "uniqueConstraints": {},
305+ "policies": {},
306+ "checkConstraints": {},
307+ "isRLSEnabled": false
308+ },
309+ "public.connection": {
310+ "name": "connection",
311+ "schema": "",
312+ "columns": {
313+ "integration": {
314+ "name": "integration",
315+ "type": "varchar(255)",
316+ "primaryKey": false,
317+ "notNull": true
318+ },
319+ "name": {
320+ "name": "name",
321+ "type": "varchar(255)",
322+ "primaryKey": false,
323+ "notNull": true
324+ },
325+ "template": {
326+ "name": "template",
327+ "type": "text",
328+ "primaryKey": false,
329+ "notNull": true
330+ },
331+ "provider": {
332+ "name": "provider",
333+ "type": "text",
334+ "primaryKey": false,
335+ "notNull": true
336+ },
337+ "item_ids": {
338+ "name": "item_ids",
339+ "type": "json",
340+ "primaryKey": false,
341+ "notNull": true
342+ },
343+ "identity_label": {
344+ "name": "identity_label",
345+ "type": "text",
346+ "primaryKey": false,
347+ "notNull": false
348+ },
349+ "description": {
350+ "name": "description",
351+ "type": "text",
352+ "primaryKey": false,
353+ "notNull": false
354+ },
355+ "last_health": {
356+ "name": "last_health",
357+ "type": "json",
358+ "primaryKey": false,
359+ "notNull": false
360+ },
361+ "tools_synced_at": {
362+ "name": "tools_synced_at",
363+ "type": "bigint",
364+ "primaryKey": false,
365+ "notNull": false
366+ },
367+ "oauth_client": {
368+ "name": "oauth_client",
369+ "type": "text",
370+ "primaryKey": false,
371+ "notNull": false
372+ },
373+ "oauth_client_owner": {
374+ "name": "oauth_client_owner",
375+ "type": "text",
376+ "primaryKey": false,
377+ "notNull": false
378+ },
379+ "refresh_item_id": {
380+ "name": "refresh_item_id",
381+ "type": "text",
382+ "primaryKey": false,
383+ "notNull": false
384+ },
385+ "expires_at": {
386+ "name": "expires_at",
387+ "type": "bigint",
388+ "primaryKey": false,
389+ "notNull": false
390+ },
391+ "oauth_scope": {
392+ "name": "oauth_scope",
393+ "type": "text",
394+ "primaryKey": false,
395+ "notNull": false
396+ },
397+ "oauth_token_url": {
398+ "name": "oauth_token_url",
399+ "type": "text",
400+ "primaryKey": false,
401+ "notNull": false
402+ },
403+ "provider_state": {
404+ "name": "provider_state",
405+ "type": "json",
406+ "primaryKey": false,
407+ "notNull": false
408+ },
409+ "created_at": {
410+ "name": "created_at",
411+ "type": "timestamp",
412+ "primaryKey": false,
413+ "notNull": true
414+ },
415+ "updated_at": {
416+ "name": "updated_at",
417+ "type": "timestamp",
418+ "primaryKey": false,
419+ "notNull": true
420+ },
421+ "row_id": {
422+ "name": "row_id",
423+ "type": "varchar(255)",
424+ "primaryKey": true,
425+ "notNull": true
426+ },
427+ "tenant": {
428+ "name": "tenant",
429+ "type": "varchar(255)",
430+ "primaryKey": false,
431+ "notNull": true
432+ },
433+ "owner": {
434+ "name": "owner",
435+ "type": "varchar(255)",
436+ "primaryKey": false,
437+ "notNull": true
438+ },
439+ "subject": {
440+ "name": "subject",
441+ "type": "varchar(255)",
442+ "primaryKey": false,
443+ "notNull": true
444+ }
445+ },
446+ "indexes": {
447+ "connection_uidx": {
448+ "name": "connection_uidx",
449+ "columns": [
450+ {
451+ "expression": "tenant",
452+ "isExpression": false,
453+ "asc": true,
454+ "nulls": "last"
455+ },
456+ {
457+ "expression": "owner",
458+ "isExpression": false,
459+ "asc": true,
460+ "nulls": "last"
461+ },
462+ {
463+ "expression": "subject",
464+ "isExpression": false,
465+ "asc": true,
466+ "nulls": "last"
467+ },
468+ {
469+ "expression": "integration",
470+ "isExpression": false,
471+ "asc": true,
472+ "nulls": "last"
473+ },
474+ {
475+ "expression": "name",
476+ "isExpression": false,
477+ "asc": true,
478+ "nulls": "last"
479+ }
480+ ],
481+ "isUnique": true,
482+ "concurrently": false,
483+ "method": "btree",
484+ "with": {}
485+ }
486+ },
487+ "foreignKeys": {},
488+ "compositePrimaryKeys": {},
489+ "uniqueConstraints": {},
490+ "policies": {},
491+ "checkConstraints": {},
492+ "isRLSEnabled": false
493+ },
494+ "public.definition": {
495+ "name": "definition",
496+ "schema": "",
497+ "columns": {
498+ "integration": {
499+ "name": "integration",
500+ "type": "varchar(255)",
501+ "primaryKey": false,
502+ "notNull": true
503+ },
504+ "connection": {
505+ "name": "connection",
506+ "type": "varchar(255)",
507+ "primaryKey": false,
508+ "notNull": true
509+ },
510+ "plugin_id": {
511+ "name": "plugin_id",
512+ "type": "text",
513+ "primaryKey": false,
514+ "notNull": true
515+ },
516+ "name": {
517+ "name": "name",
518+ "type": "varchar(255)",
519+ "primaryKey": false,
520+ "notNull": true
521+ },
522+ "schema": {
523+ "name": "schema",
524+ "type": "json",
525+ "primaryKey": false,
526+ "notNull": true
527+ },
528+ "created_at": {
529+ "name": "created_at",
530+ "type": "timestamp",
531+ "primaryKey": false,
532+ "notNull": true
533+ },
534+ "row_id": {
535+ "name": "row_id",
536+ "type": "varchar(255)",
537+ "primaryKey": true,
538+ "notNull": true
539+ },
540+ "tenant": {
541+ "name": "tenant",
542+ "type": "varchar(255)",
543+ "primaryKey": false,
544+ "notNull": true
545+ },
546+ "owner": {
547+ "name": "owner",
548+ "type": "varchar(255)",
549+ "primaryKey": false,
550+ "notNull": true
551+ },
552+ "subject": {
553+ "name": "subject",
554+ "type": "varchar(255)",
555+ "primaryKey": false,
556+ "notNull": true
557+ }
558+ },
559+ "indexes": {
560+ "definition_uidx": {
561+ "name": "definition_uidx",
562+ "columns": [
563+ {
564+ "expression": "tenant",
565+ "isExpression": false,
566+ "asc": true,
567+ "nulls": "last"
568+ },
569+ {
570+ "expression": "owner",
571+ "isExpression": false,
572+ "asc": true,
573+ "nulls": "last"
574+ },
575+ {
576+ "expression": "subject",
577+ "isExpression": false,
578+ "asc": true,
579+ "nulls": "last"
580+ },
581+ {
582+ "expression": "integration",
583+ "isExpression": false,
584+ "asc": true,
585+ "nulls": "last"
586+ },
587+ {
588+ "expression": "connection",
589+ "isExpression": false,
590+ "asc": true,
591+ "nulls": "last"
592+ },
593+ {
594+ "expression": "name",
595+ "isExpression": false,
596+ "asc": true,
597+ "nulls": "last"
598+ }
599+ ],
600+ "isUnique": true,
601+ "concurrently": false,
602+ "method": "btree",
603+ "with": {}
604+ }
605+ },
606+ "foreignKeys": {},
607+ "compositePrimaryKeys": {},
608+ "uniqueConstraints": {},
609+ "policies": {},
610+ "checkConstraints": {},
611+ "isRLSEnabled": false
612+ },
613+ "public.integration": {
614+ "name": "integration",
615+ "schema": "",
616+ "columns": {
617+ "slug": {
618+ "name": "slug",
619+ "type": "varchar(255)",
620+ "primaryKey": false,
621+ "notNull": true
622+ },
623+ "plugin_id": {
624+ "name": "plugin_id",
625+ "type": "text",
626+ "primaryKey": false,
627+ "notNull": true
628+ },
629+ "name": {
630+ "name": "name",
631+ "type": "text",
632+ "primaryKey": false,
633+ "notNull": false
634+ },
635+ "description": {
636+ "name": "description",
637+ "type": "text",
638+ "primaryKey": false,
639+ "notNull": false
640+ },
641+ "config": {
642+ "name": "config",
643+ "type": "json",
644+ "primaryKey": false,
645+ "notNull": false
646+ },
647+ "health_check": {
648+ "name": "health_check",
649+ "type": "json",
650+ "primaryKey": false,
651+ "notNull": false
652+ },
653+ "config_revised_at": {
654+ "name": "config_revised_at",
655+ "type": "bigint",
656+ "primaryKey": false,
657+ "notNull": false
658+ },
659+ "can_remove": {
660+ "name": "can_remove",
661+ "type": "boolean",
662+ "primaryKey": false,
663+ "notNull": true,
664+ "default": true
665+ },
666+ "can_refresh": {
667+ "name": "can_refresh",
668+ "type": "boolean",
669+ "primaryKey": false,
670+ "notNull": true,
671+ "default": false
672+ },
673+ "created_at": {
674+ "name": "created_at",
675+ "type": "timestamp",
676+ "primaryKey": false,
677+ "notNull": true
678+ },
679+ "updated_at": {
680+ "name": "updated_at",
681+ "type": "timestamp",
682+ "primaryKey": false,
683+ "notNull": true
684+ },
685+ "row_id": {
686+ "name": "row_id",
687+ "type": "varchar(255)",
688+ "primaryKey": true,
689+ "notNull": true
690+ },
691+ "tenant": {
692+ "name": "tenant",
693+ "type": "varchar(255)",
694+ "primaryKey": false,
695+ "notNull": true
696+ }
697+ },
698+ "indexes": {
699+ "integration_uidx": {
700+ "name": "integration_uidx",
701+ "columns": [
702+ {
703+ "expression": "tenant",
704+ "isExpression": false,
705+ "asc": true,
706+ "nulls": "last"
707+ },
708+ {
709+ "expression": "slug",
710+ "isExpression": false,
711+ "asc": true,
712+ "nulls": "last"
713+ }
714+ ],
715+ "isUnique": true,
716+ "concurrently": false,
717+ "method": "btree",
718+ "with": {}
719+ }
720+ },
721+ "foreignKeys": {},
722+ "compositePrimaryKeys": {},
723+ "uniqueConstraints": {},
724+ "policies": {},
725+ "checkConstraints": {},
726+ "isRLSEnabled": false
727+ },
728+ "public.oauth_client": {
729+ "name": "oauth_client",
730+ "schema": "",
731+ "columns": {
732+ "slug": {
733+ "name": "slug",
734+ "type": "varchar(255)",
735+ "primaryKey": false,
736+ "notNull": true
737+ },
738+ "authorization_url": {
739+ "name": "authorization_url",
740+ "type": "text",
741+ "primaryKey": false,
742+ "notNull": true
743+ },
744+ "token_url": {
745+ "name": "token_url",
746+ "type": "text",
747+ "primaryKey": false,
748+ "notNull": true
749+ },
750+ "grant": {
751+ "name": "grant",
752+ "type": "text",
753+ "primaryKey": false,
754+ "notNull": true
755+ },
756+ "client_id": {
757+ "name": "client_id",
758+ "type": "text",
759+ "primaryKey": false,
760+ "notNull": true
761+ },
762+ "client_secret_item_id": {
763+ "name": "client_secret_item_id",
764+ "type": "text",
765+ "primaryKey": false,
766+ "notNull": false
767+ },
768+ "resource": {
769+ "name": "resource",
770+ "type": "text",
771+ "primaryKey": false,
772+ "notNull": false
773+ },
774+ "origin_kind": {
775+ "name": "origin_kind",
776+ "type": "text",
777+ "primaryKey": false,
778+ "notNull": false
779+ },
780+ "origin_integration": {
781+ "name": "origin_integration",
782+ "type": "text",
783+ "primaryKey": false,
784+ "notNull": false
785+ },
786+ "origin_issuer": {
787+ "name": "origin_issuer",
788+ "type": "text",
789+ "primaryKey": false,
790+ "notNull": false
791+ },
792+ "origin_redirect_uri": {
793+ "name": "origin_redirect_uri",
794+ "type": "text",
795+ "primaryKey": false,
796+ "notNull": false
797+ },
798+ "created_at": {
799+ "name": "created_at",
800+ "type": "timestamp",
801+ "primaryKey": false,
802+ "notNull": true
803+ },
804+ "row_id": {
805+ "name": "row_id",
806+ "type": "varchar(255)",
807+ "primaryKey": true,
808+ "notNull": true
809+ },
810+ "tenant": {
811+ "name": "tenant",
812+ "type": "varchar(255)",
813+ "primaryKey": false,
814+ "notNull": true
815+ },
816+ "owner": {
817+ "name": "owner",
818+ "type": "varchar(255)",
819+ "primaryKey": false,
820+ "notNull": true
821+ },
822+ "subject": {
823+ "name": "subject",
824+ "type": "varchar(255)",
825+ "primaryKey": false,
826+ "notNull": true
827+ }
828+ },
829+ "indexes": {
830+ "oauth_client_uidx": {
831+ "name": "oauth_client_uidx",
832+ "columns": [
833+ {
834+ "expression": "tenant",
835+ "isExpression": false,
836+ "asc": true,
837+ "nulls": "last"
838+ },
839+ {
840+ "expression": "owner",
841+ "isExpression": false,
842+ "asc": true,
843+ "nulls": "last"
844+ },
845+ {
846+ "expression": "subject",
847+ "isExpression": false,
848+ "asc": true,
849+ "nulls": "last"
850+ },
851+ {
852+ "expression": "slug",
853+ "isExpression": false,
854+ "asc": true,
855+ "nulls": "last"
856+ }
857+ ],
858+ "isUnique": true,
859+ "concurrently": false,
860+ "method": "btree",
861+ "with": {}
862+ }
863+ },
864+ "foreignKeys": {},
865+ "compositePrimaryKeys": {},
866+ "uniqueConstraints": {},
867+ "policies": {},
868+ "checkConstraints": {},
869+ "isRLSEnabled": false
870+ },
871+ "public.oauth_session": {
872+ "name": "oauth_session",
873+ "schema": "",
874+ "columns": {
875+ "state": {
876+ "name": "state",
877+ "type": "varchar(255)",
878+ "primaryKey": false,
879+ "notNull": true
880+ },
881+ "client_slug": {
882+ "name": "client_slug",
883+ "type": "text",
884+ "primaryKey": false,
885+ "notNull": true
886+ },
887+ "integration": {
888+ "name": "integration",
889+ "type": "text",
890+ "primaryKey": false,
891+ "notNull": true
892+ },
893+ "name": {
894+ "name": "name",
895+ "type": "text",
896+ "primaryKey": false,
897+ "notNull": true
898+ },
899+ "template": {
900+ "name": "template",
901+ "type": "text",
902+ "primaryKey": false,
903+ "notNull": true
904+ },
905+ "redirect_url": {
906+ "name": "redirect_url",
907+ "type": "text",
908+ "primaryKey": false,
909+ "notNull": true
910+ },
911+ "pkce_verifier": {
912+ "name": "pkce_verifier",
913+ "type": "text",
914+ "primaryKey": false,
915+ "notNull": false
916+ },
917+ "identity_label": {
918+ "name": "identity_label",
919+ "type": "text",
920+ "primaryKey": false,
921+ "notNull": false
922+ },
923+ "payload": {
924+ "name": "payload",
925+ "type": "json",
926+ "primaryKey": false,
927+ "notNull": true
928+ },
929+ "expires_at": {
930+ "name": "expires_at",
931+ "type": "bigint",
932+ "primaryKey": false,
933+ "notNull": true
934+ },
935+ "created_at": {
936+ "name": "created_at",
937+ "type": "timestamp",
938+ "primaryKey": false,
939+ "notNull": true
940+ },
941+ "row_id": {
942+ "name": "row_id",
943+ "type": "varchar(255)",
944+ "primaryKey": true,
945+ "notNull": true
946+ },
947+ "tenant": {
948+ "name": "tenant",
949+ "type": "varchar(255)",
950+ "primaryKey": false,
951+ "notNull": true
952+ },
953+ "owner": {
954+ "name": "owner",
955+ "type": "varchar(255)",
956+ "primaryKey": false,
957+ "notNull": true
958+ },
959+ "subject": {
960+ "name": "subject",
961+ "type": "varchar(255)",
962+ "primaryKey": false,
963+ "notNull": true
964+ }
965+ },
966+ "indexes": {
967+ "oauth_session_uidx": {
968+ "name": "oauth_session_uidx",
969+ "columns": [
970+ {
971+ "expression": "tenant",
972+ "isExpression": false,
973+ "asc": true,
974+ "nulls": "last"
975+ },
976+ {
977+ "expression": "state",
978+ "isExpression": false,
979+ "asc": true,
980+ "nulls": "last"
981+ }
982+ ],
983+ "isUnique": true,
984+ "concurrently": false,
985+ "method": "btree",
986+ "with": {}
987+ }
988+ },
989+ "foreignKeys": {},
990+ "compositePrimaryKeys": {},
991+ "uniqueConstraints": {},
992+ "policies": {},
993+ "checkConstraints": {},
994+ "isRLSEnabled": false
995+ },
996+ "public.plugin_storage": {
997+ "name": "plugin_storage",
998+ "schema": "",
999+ "columns": {
1000+ "plugin_id": {
1001+ "name": "plugin_id",
1002+ "type": "varchar(255)",
1003+ "primaryKey": false,
1004+ "notNull": true
1005+ },
1006+ "collection": {
1007+ "name": "collection",
1008+ "type": "varchar(255)",
1009+ "primaryKey": false,
1010+ "notNull": true
1011+ },
1012+ "key": {
1013+ "name": "key",
1014+ "type": "varchar(255)",
1015+ "primaryKey": false,
1016+ "notNull": true
1017+ },
1018+ "data": {
1019+ "name": "data",
1020+ "type": "json",
1021+ "primaryKey": false,
1022+ "notNull": true
1023+ },
1024+ "created_at": {
1025+ "name": "created_at",
1026+ "type": "timestamp",
1027+ "primaryKey": false,
1028+ "notNull": true
1029+ },
1030+ "updated_at": {
1031+ "name": "updated_at",
1032+ "type": "timestamp",
1033+ "primaryKey": false,
1034+ "notNull": true
1035+ },
1036+ "row_id": {
1037+ "name": "row_id",
1038+ "type": "varchar(255)",
1039+ "primaryKey": true,
1040+ "notNull": true
1041+ },
1042+ "tenant": {
1043+ "name": "tenant",
1044+ "type": "varchar(255)",
1045+ "primaryKey": false,
1046+ "notNull": true
1047+ },
1048+ "owner": {
1049+ "name": "owner",
1050+ "type": "varchar(255)",
1051+ "primaryKey": false,
1052+ "notNull": true
1053+ },
1054+ "subject": {
1055+ "name": "subject",
1056+ "type": "varchar(255)",
1057+ "primaryKey": false,
1058+ "notNull": true
1059+ }
1060+ },
1061+ "indexes": {
1062+ "plugin_storage_uidx": {
1063+ "name": "plugin_storage_uidx",
1064+ "columns": [
1065+ {
1066+ "expression": "tenant",
1067+ "isExpression": false,
1068+ "asc": true,
1069+ "nulls": "last"
1070+ },
1071+ {
1072+ "expression": "owner",
1073+ "isExpression": false,
1074+ "asc": true,
1075+ "nulls": "last"
1076+ },
1077+ {
1078+ "expression": "subject",
1079+ "isExpression": false,
1080+ "asc": true,
1081+ "nulls": "last"
1082+ },
1083+ {
1084+ "expression": "plugin_id",
1085+ "isExpression": false,
1086+ "asc": true,
1087+ "nulls": "last"
1088+ },
1089+ {
1090+ "expression": "collection",
1091+ "isExpression": false,
1092+ "asc": true,
1093+ "nulls": "last"
1094+ },
1095+ {
1096+ "expression": "key",
1097+ "isExpression": false,
1098+ "asc": true,
1099+ "nulls": "last"
1100+ }
1101+ ],
1102+ "isUnique": true,
1103+ "concurrently": false,
1104+ "method": "btree",
1105+ "with": {}
1106+ }
1107+ },
1108+ "foreignKeys": {},
1109+ "compositePrimaryKeys": {},
1110+ "uniqueConstraints": {},
1111+ "policies": {},
1112+ "checkConstraints": {},
1113+ "isRLSEnabled": false
1114+ },
1115+ "public.private_executor_cloud_settings": {
1116+ "name": "private_executor_cloud_settings",
1117+ "schema": "",
1118+ "columns": {
1119+ "id": {
1120+ "name": "id",
1121+ "type": "varchar(255)",
1122+ "primaryKey": true,
1123+ "notNull": true
1124+ },
1125+ "version": {
1126+ "name": "version",
1127+ "type": "varchar(255)",
1128+ "primaryKey": false,
1129+ "notNull": true,
1130+ "default": "'1.0.0'"
1131+ }
1132+ },
1133+ "indexes": {},
1134+ "foreignKeys": {},
1135+ "compositePrimaryKeys": {},
1136+ "uniqueConstraints": {},
1137+ "policies": {},
1138+ "checkConstraints": {},
1139+ "isRLSEnabled": false
1140+ },
1141+ "public.subject": {
1142+ "name": "subject",
1143+ "schema": "",
1144+ "columns": {
1145+ "external_id": {
1146+ "name": "external_id",
1147+ "type": "varchar(255)",
1148+ "primaryKey": false,
1149+ "notNull": true
1150+ },
1151+ "created_at": {
1152+ "name": "created_at",
1153+ "type": "timestamp",
1154+ "primaryKey": false,
1155+ "notNull": true
1156+ },
1157+ "last_seen_at": {
1158+ "name": "last_seen_at",
1159+ "type": "bigint",
1160+ "primaryKey": false,
1161+ "notNull": false
1162+ },
1163+ "status": {
1164+ "name": "status",
1165+ "type": "text",
1166+ "primaryKey": false,
1167+ "notNull": false
1168+ },
1169+ "row_id": {
1170+ "name": "row_id",
1171+ "type": "varchar(255)",
1172+ "primaryKey": true,
1173+ "notNull": true
1174+ },
1175+ "tenant": {
1176+ "name": "tenant",
1177+ "type": "varchar(255)",
1178+ "primaryKey": false,
1179+ "notNull": true
1180+ }
1181+ },
1182+ "indexes": {
1183+ "subject_uidx": {
1184+ "name": "subject_uidx",
1185+ "columns": [
1186+ {
1187+ "expression": "tenant",
1188+ "isExpression": false,
1189+ "asc": true,
1190+ "nulls": "last"
1191+ },
1192+ {
1193+ "expression": "external_id",
1194+ "isExpression": false,
1195+ "asc": true,
1196+ "nulls": "last"
1197+ }
1198+ ],
1199+ "isUnique": true,
1200+ "concurrently": false,
1201+ "method": "btree",
1202+ "with": {}
1203+ }
1204+ },
1205+ "foreignKeys": {},
1206+ "compositePrimaryKeys": {},
1207+ "uniqueConstraints": {},
1208+ "policies": {},
1209+ "checkConstraints": {},
1210+ "isRLSEnabled": false
1211+ },
1212+ "public.tool": {
1213+ "name": "tool",
1214+ "schema": "",
1215+ "columns": {
1216+ "integration": {
1217+ "name": "integration",
1218+ "type": "varchar(255)",
1219+ "primaryKey": false,
1220+ "notNull": true
1221+ },
1222+ "connection": {
1223+ "name": "connection",
1224+ "type": "varchar(255)",
1225+ "primaryKey": false,
1226+ "notNull": true
1227+ },
1228+ "plugin_id": {
1229+ "name": "plugin_id",
1230+ "type": "text",
1231+ "primaryKey": false,
1232+ "notNull": true
1233+ },
1234+ "name": {
1235+ "name": "name",
1236+ "type": "varchar(255)",
1237+ "primaryKey": false,
1238+ "notNull": true
1239+ },
1240+ "description": {
1241+ "name": "description",
1242+ "type": "text",
1243+ "primaryKey": false,
1244+ "notNull": true
1245+ },
1246+ "input_schema": {
1247+ "name": "input_schema",
1248+ "type": "json",
1249+ "primaryKey": false,
1250+ "notNull": false
1251+ },
1252+ "output_schema": {
1253+ "name": "output_schema",
1254+ "type": "json",
1255+ "primaryKey": false,
1256+ "notNull": false
1257+ },
1258+ "annotations": {
1259+ "name": "annotations",
1260+ "type": "json",
1261+ "primaryKey": false,
1262+ "notNull": false
1263+ },
1264+ "created_at": {
1265+ "name": "created_at",
1266+ "type": "timestamp",
1267+ "primaryKey": false,
1268+ "notNull": true
1269+ },
1270+ "updated_at": {
1271+ "name": "updated_at",
1272+ "type": "timestamp",
1273+ "primaryKey": false,
1274+ "notNull": true
1275+ },
1276+ "row_id": {
1277+ "name": "row_id",
1278+ "type": "varchar(255)",
1279+ "primaryKey": true,
1280+ "notNull": true
1281+ },
1282+ "tenant": {
1283+ "name": "tenant",
1284+ "type": "varchar(255)",
1285+ "primaryKey": false,
1286+ "notNull": true
1287+ },
1288+ "owner": {
1289+ "name": "owner",
1290+ "type": "varchar(255)",
1291+ "primaryKey": false,
1292+ "notNull": true
1293+ },
1294+ "subject": {
1295+ "name": "subject",
1296+ "type": "varchar(255)",
1297+ "primaryKey": false,
1298+ "notNull": true
1299+ }
1300+ },
1301+ "indexes": {
1302+ "tool_uidx": {
1303+ "name": "tool_uidx",
1304+ "columns": [
1305+ {
1306+ "expression": "tenant",
1307+ "isExpression": false,
1308+ "asc": true,
1309+ "nulls": "last"
1310+ },
1311+ {
1312+ "expression": "owner",
1313+ "isExpression": false,
1314+ "asc": true,
1315+ "nulls": "last"
1316+ },
1317+ {
1318+ "expression": "subject",
1319+ "isExpression": false,
1320+ "asc": true,
1321+ "nulls": "last"
1322+ },
1323+ {
1324+ "expression": "integration",
1325+ "isExpression": false,
1326+ "asc": true,
1327+ "nulls": "last"
1328+ },
1329+ {
1330+ "expression": "connection",
1331+ "isExpression": false,
1332+ "asc": true,
1333+ "nulls": "last"
1334+ },
1335+ {
1336+ "expression": "name",
1337+ "isExpression": false,
1338+ "asc": true,
1339+ "nulls": "last"
1340+ }
1341+ ],
1342+ "isUnique": true,
1343+ "concurrently": false,
1344+ "method": "btree",
1345+ "with": {}
1346+ }
1347+ },
1348+ "foreignKeys": {},
1349+ "compositePrimaryKeys": {},
1350+ "uniqueConstraints": {},
1351+ "policies": {},
1352+ "checkConstraints": {},
1353+ "isRLSEnabled": false
1354+ },
1355+ "public.tool_policy": {
1356+ "name": "tool_policy",
1357+ "schema": "",
1358+ "columns": {
1359+ "id": {
1360+ "name": "id",
1361+ "type": "varchar(255)",
1362+ "primaryKey": false,
1363+ "notNull": true
1364+ },
1365+ "pattern": {
1366+ "name": "pattern",
1367+ "type": "text",
1368+ "primaryKey": false,
1369+ "notNull": true
1370+ },
1371+ "action": {
1372+ "name": "action",
1373+ "type": "text",
1374+ "primaryKey": false,
1375+ "notNull": true
1376+ },
1377+ "position": {
1378+ "name": "position",
1379+ "type": "text",
1380+ "primaryKey": false,
1381+ "notNull": true
1382+ },
1383+ "created_at": {
1384+ "name": "created_at",
1385+ "type": "timestamp",
1386+ "primaryKey": false,
1387+ "notNull": true
1388+ },
1389+ "updated_at": {
1390+ "name": "updated_at",
1391+ "type": "timestamp",
1392+ "primaryKey": false,
1393+ "notNull": true
1394+ },
1395+ "row_id": {
1396+ "name": "row_id",
1397+ "type": "varchar(255)",
1398+ "primaryKey": true,
1399+ "notNull": true
1400+ },
1401+ "tenant": {
1402+ "name": "tenant",
1403+ "type": "varchar(255)",
1404+ "primaryKey": false,
1405+ "notNull": true
1406+ },
1407+ "owner": {
1408+ "name": "owner",
1409+ "type": "varchar(255)",
1410+ "primaryKey": false,
1411+ "notNull": true
1412+ },
1413+ "subject": {
1414+ "name": "subject",
1415+ "type": "varchar(255)",
1416+ "primaryKey": false,
1417+ "notNull": true
1418+ }
1419+ },
1420+ "indexes": {
1421+ "tool_policy_uidx": {
1422+ "name": "tool_policy_uidx",
1423+ "columns": [
1424+ {
1425+ "expression": "tenant",
1426+ "isExpression": false,
1427+ "asc": true,
1428+ "nulls": "last"
1429+ },
1430+ {
1431+ "expression": "owner",
1432+ "isExpression": false,
1433+ "asc": true,
1434+ "nulls": "last"
1435+ },
1436+ {
1437+ "expression": "subject",
1438+ "isExpression": false,
1439+ "asc": true,
1440+ "nulls": "last"
1441+ },
1442+ {
1443+ "expression": "id",
1444+ "isExpression": false,
1445+ "asc": true,
1446+ "nulls": "last"
1447+ }
1448+ ],
1449+ "isUnique": true,
1450+ "concurrently": false,
1451+ "method": "btree",
1452+ "with": {}
1453+ }
1454+ },
1455+ "foreignKeys": {},
1456+ "compositePrimaryKeys": {},
1457+ "uniqueConstraints": {},
1458+ "policies": {},
1459+ "checkConstraints": {},
1460+ "isRLSEnabled": false
1461+ }
1462+ },
1463+ "enums": {},
1464+ "schemas": {},
1465+ "sequences": {},
1466+ "roles": {},
1467+ "policies": {},
1468+ "views": {},
1469+ "_meta": {
1470+ "columns": {},
1471+ "schemas": {},
1472+ "tables": {}
1473+ }
1474+}
apps/cloud/drizzle/meta/_journal.json+7−0
@@ -92,6 +92,13 @@ · 91 unmodified lines
9292 "when": 1785193313601,
9393 "tag": "0012_woozy_shiva",
9494 "breakpoints": true
95+ },
96+ {
97+ "idx": 13,
98+ "version": "7",
99+ "when": 1785354087138,
100+ "tag": "0013_curly_rocket_racer",
101+ "breakpoints": true
95102 }
96103 ]
97104 }
apps/cloud/src/db/executor-schema.ts+20−0
@@ -226,6 +226,26 @@ export const tool_policy = pgTable( · 225 unmodified lines
226226 ],
227227 );
228228
229+export const artifact = pgTable(
230+ "artifact",
231+ {
232+ id: varchar("id", { length: 255 }).notNull(),
233+ title: text("title").notNull(),
234+ description: text("description"),
235+ code: text("code").notNull(),
236+ created_at: timestamp("created_at").notNull(),
237+ updated_at: timestamp("updated_at").notNull(),
238+ row_id: varchar("row_id", { length: 255 })
239+ .primaryKey()
240+ .notNull()
241+ .$defaultFn(() => createId()),
242+ tenant: varchar("tenant", { length: 255 }).notNull(),
243+ owner: varchar("owner", { length: 255 }).notNull(),
244+ subject: varchar("subject", { length: 255 }).notNull(),
245+ },
246+ (table) => [uniqueIndex("artifact_uidx").on(table.tenant, table.owner, table.subject, table.id)],
247+);
248+
229249 export const plugin_storage = pgTable(
230250 "plugin_storage",
231251 {
apps/cloud/src/db/org-deletion.test.ts+13−0
@@ -28,6 +28,7 @@ import * as cloudSchema from "./schema"; · 27 unmodified lines
2828 import * as executorSchema from "./executor-schema";
2929 import { memberships, accounts } from "./schema";
3030 import {
31+ artifact,
3132 blob,
3233 connection,
3334 definition,
@@ -146,6 +147,17 @@ const seedTenant = async (db: DrizzleDb, tenant: string, tag: string) => { · 112 unmodified lines
146147 tenant,
147148 });
148149
150+ await db.insert(artifact).values({
151+ id: `art-${tag}`,
152+ title: "Dashboard",
153+ code: "export default function App() { return null; }",
154+ created_at: now,
155+ updated_at: now,
156+ tenant,
157+ owner: "o",
158+ subject: "s",
159+ });
160+
149161 const orgNs = `o:${tenant}/plugin`;
150162 const userNs = `u:${tenant}:subject/plugin`;
151163 await db.insert(blob).values({
@@ -172,6 +184,7 @@ const TENANT_TABLES = [ · 20 unmodified lines
172184 tool_policy,
173185 plugin_storage,
174186 subject,
187+ artifact,
175188 ] as const;
176189
177190 // Tables that are NOT purged by org id, each with the reason it is exempt. Any
apps/cloud/src/db/org-deletion.ts+2−0
@@ -16,6 +16,7 @@ import { eq, or, sql } from "drizzle-orm"; · 15 unmodified lines
1616 import type { DrizzleDb } from "./db";
1717 import { organizations } from "./schema";
1818 import {
19+ artifact,
1920 blob,
2021 connection,
2122 definition,
@@ -50,6 +51,7 @@ export const purgeOrganizationData = (db: DrizzleDb, organizationId: string): Pr · 28 unmodified lines
5051 await tx.delete(tool_policy).where(eq(tool_policy.tenant, organizationId));
5152 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));
5253 await tx.delete(subject).where(eq(subject.tenant, organizationId));
54+ await tx.delete(artifact).where(eq(artifact.tenant, organizationId));
5355
5456 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by
5557 // owner: `o:<org>/<plugin>` (org scope) and `u:<org>:<subject>/<plugin>`
packages/core/api/src/api.ts+2−0
@@ -8,6 +8,7 @@ import { ProvidersApi } from "./providers/api"; · 7 unmodified lines
88 import { ExecutionsApi } from "./executions/api";
99 import { OAuthApi } from "./oauth/api";
1010 import { PoliciesApi } from "./policies/api";
11+import { ArtifactsApi } from "./artifacts/api";
1112
1213 export const CoreExecutorApi = HttpApi.make("executor")
1314 .add(ToolsApi)
@@ -17,6 +18,7 @@ export const CoreExecutorApi = HttpApi.make("executor") · 3 unmodified lines
1718 .add(ExecutionsApi)
1819 .add(OAuthApi)
1920 .add(PoliciesApi)
21+ .add(ArtifactsApi)
2022 .annotateMerge(
2123 OpenApi.annotations({
2224 title: "Executor API",
packages/core/api/src/artifacts/api.ts+95−0
@@ -0,0 +1,95 @@
1+// ---------------------------------------------------------------------------
2+// Artifacts HTTP API — saved generative-UI components.
3+//
4+// An artifact is the JSX source a model produced plus the title/description an
5+// agent matches against. Owner-scoped: every endpoint reads and writes only
6+// what the bound owner scope may see, so no owner travels on the wire.
7+// ---------------------------------------------------------------------------
8+
9+import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
10+import { Schema } from "effect";
11+import { ArtifactId, ArtifactNotFoundError, InternalError, Owner } from "@executor-js/sdk/shared";
12+
13+// ---------------------------------------------------------------------------
14+// Params
15+// ---------------------------------------------------------------------------
16+
17+const ArtifactParams = { artifactId: ArtifactId };
18+
19+// ---------------------------------------------------------------------------
20+// Response / payload schemas
21+// ---------------------------------------------------------------------------
22+
23+/** What a list returns — no `code`, so a long list stays cheap. */
24+const ArtifactSummaryResponse = Schema.Struct({
25+ id: ArtifactId,
26+ owner: Owner,
27+ title: Schema.String,
28+ description: Schema.NullOr(Schema.String),
29+ createdAt: Schema.Number,
30+ updatedAt: Schema.Number,
31+});
32+
33+const ArtifactResponse = Schema.Struct({
34+ ...ArtifactSummaryResponse.fields,
35+ code: Schema.String,
36+});
37+
38+/** Omit `id` to create; pass one to overwrite that artifact in place. */
39+const SaveArtifactPayload = Schema.Struct({
40+ id: Schema.optional(ArtifactId),
41+ title: Schema.String,
42+ description: Schema.optional(Schema.NullOr(Schema.String)),
43+ code: Schema.String,
44+});
45+
46+const RenameArtifactPayload = Schema.Struct({
47+ title: Schema.String,
48+});
49+
50+// ---------------------------------------------------------------------------
51+// Error schemas with HTTP status annotations
52+// ---------------------------------------------------------------------------
53+
54+const ArtifactNotFound = ArtifactNotFoundError.annotate({ httpApiStatus: 404 });
55+
56+// ---------------------------------------------------------------------------
57+// Group
58+// ---------------------------------------------------------------------------
59+
60+export const ArtifactsApi = HttpApiGroup.make("artifacts")
61+ .add(
62+ HttpApiEndpoint.get("list", "/artifacts", {
63+ success: Schema.Array(ArtifactSummaryResponse),
64+ error: InternalError,
65+ }),
66+ )
67+ .add(
68+ HttpApiEndpoint.get("get", "/artifacts/:artifactId", {
69+ params: ArtifactParams,
70+ success: ArtifactResponse,
71+ error: [InternalError, ArtifactNotFound],
72+ }),
73+ )
74+ .add(
75+ HttpApiEndpoint.post("save", "/artifacts", {
76+ payload: SaveArtifactPayload,
77+ success: ArtifactResponse,
78+ error: [InternalError, ArtifactNotFound],
79+ }),
80+ )
81+ .add(
82+ HttpApiEndpoint.patch("rename", "/artifacts/:artifactId", {
83+ params: ArtifactParams,
84+ payload: RenameArtifactPayload,
85+ success: ArtifactResponse,
86+ error: [InternalError, ArtifactNotFound],
87+ }),
88+ )
89+ .add(
90+ HttpApiEndpoint.delete("remove", "/artifacts/:artifactId", {
91+ params: ArtifactParams,
92+ success: Schema.Struct({ removed: Schema.Boolean }),
93+ error: InternalError,
94+ }),
95+ );
packages/core/api/src/handlers/artifacts.ts+78−0
@@ -0,0 +1,78 @@
1+import { HttpApiBuilder } from "effect/unstable/httpapi";
2+import { Effect } from "effect";
3+import type { Artifact, ArtifactSummary } from "@executor-js/sdk";
4+
5+import { ExecutorApi } from "../api";
6+import { ExecutorService } from "../services";
7+import { capture } from "@executor-js/api";
8+
9+const summaryToResponse = (a: ArtifactSummary) => ({
10+ id: a.id,
11+ owner: a.owner,
12+ title: a.title,
13+ description: a.description,
14+ createdAt: a.createdAt.getTime(),
15+ updatedAt: a.updatedAt.getTime(),
16+});
17+
18+const artifactToResponse = (a: Artifact) => ({
19+ ...summaryToResponse(a),
20+ code: a.code,
21+});
22+
23+export const ArtifactsHandlers = HttpApiBuilder.group(ExecutorApi, "artifacts", (handlers) =>
24+ handlers
25+ .handle("list", () =>
26+ capture(
27+ Effect.gen(function* () {
28+ const executor = yield* ExecutorService;
29+ const artifacts = yield* executor.artifacts.list();
30+ return artifacts.map(summaryToResponse);
31+ }),
32+ ),
33+ )
34+ .handle("get", ({ params: path }) =>
35+ capture(
36+ Effect.gen(function* () {
37+ const executor = yield* ExecutorService;
38+ const artifact = yield* executor.artifacts.get(path.artifactId);
39+ return artifactToResponse(artifact);
40+ }),
41+ ),
42+ )
43+ .handle("save", ({ payload }) =>
44+ capture(
45+ Effect.gen(function* () {
46+ const executor = yield* ExecutorService;
47+ const saved = yield* executor.artifacts.save({
48+ id: payload.id,
49+ title: payload.title,
50+ description: payload.description,
51+ code: payload.code,
52+ });
53+ return artifactToResponse(saved);
54+ }),
55+ ),
56+ )
57+ .handle("rename", ({ params: path, payload }) =>
58+ capture(
59+ Effect.gen(function* () {
60+ const executor = yield* ExecutorService;
61+ const renamed = yield* executor.artifacts.rename({
62+ id: path.artifactId,
63+ title: payload.title,
64+ });
65+ return artifactToResponse(renamed);
66+ }),
67+ ),
68+ )
69+ .handle("remove", ({ params: path }) =>
70+ capture(
71+ Effect.gen(function* () {
72+ const executor = yield* ExecutorService;
73+ yield* executor.artifacts.remove({ id: path.artifactId });
74+ return { removed: true };
75+ }),
76+ ),
77+ ),
78+);
packages/core/api/src/handlers/index.ts+3−0
@@ -7,6 +7,7 @@ import { ProvidersHandlers } from "./providers"; · 6 unmodified lines
77 import { ExecutionsHandlers } from "./executions";
88 import { OAuthHandlers } from "./oauth";
99 import { PoliciesHandlers } from "./policies";
10+import { ArtifactsHandlers } from "./artifacts";
1011
1112 export { ToolsHandlers } from "./tools";
1213 export { IntegrationsHandlers } from "./integrations";
@@ -15,6 +16,7 @@ export { ProvidersHandlers } from "./providers"; · 2 unmodified lines
1516 export { ExecutionsHandlers } from "./executions";
1617 export { OAuthHandlers } from "./oauth";
1718 export { PoliciesHandlers } from "./policies";
19+export { ArtifactsHandlers } from "./artifacts";
1820
1921 export const CoreHandlers = Layer.mergeAll(
2022 ToolsHandlers,
@@ -24,4 +26,5 @@ export const CoreHandlers = Layer.mergeAll( · 3 unmodified lines
2426 ExecutionsHandlers,
2527 OAuthHandlers,
2628 PoliciesHandlers,
29+ ArtifactsHandlers,
2730 );
packages/core/api/src/index.ts+1−0
@@ -34,6 +34,7 @@ export { · 33 unmodified lines
3434 type RunOAuthCallbackInput,
3535 } from "./oauth-popup";
3636 export { PoliciesApi } from "./policies/api";
37+export { ArtifactsApi } from "./artifacts/api";
3738 export {
3839 AccountApi,
3940 AccountHttpApi,
packages/core/api/src/server.ts+1−0
@@ -7,6 +7,7 @@ export { · 6 unmodified lines
77 ProvidersHandlers,
88 OAuthHandlers,
99 PoliciesHandlers,
10+ ArtifactsHandlers,
1011 ExecutionsHandlers,
1112 } from "./handlers";
1213 export {
packages/core/sdk/src/artifact-table.test.ts+131−0
@@ -0,0 +1,131 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect } from "effect";
3+import { withQueryContext } from "@executor-js/fumadb/query";
4+
5+import { collectTables } from "./executor";
6+import { createSqliteTestFumaDb, type SqliteTestFumaDb } from "./sqlite-test-db";
7+
8+// The artifact table is OWNER-scoped: a saved artifact belongs to the subject
9+// who generated it, and nobody else in the tenant may read it (org-tier
10+// sharing is a later change that reuses the `owner` column already here).
11+// Written against the real SQLite bring-up (`createSqliteTestFumaDb` runs the
12+// same statements the local/self-host/D1 hosts boot with) instead of asserting
13+// on the table definition object.
14+
15+const TENANT = "t1";
16+const SUBJECT = "user_a";
17+const OTHER_SUBJECT = "user_b";
18+
19+const withDb = <A>(body: (db: SqliteTestFumaDb) => Promise<A>): Promise<A> =>
20+ Effect.runPromise(
21+ Effect.acquireUseRelease(
22+ Effect.promise(() => createSqliteTestFumaDb({ tables: collectTables() })),
23+ (db) => Effect.promise(() => body(db)),
24+ (db) => Effect.promise(() => db.close()),
25+ ),
26+ );
27+
28+const insertArtifact = (
29+ db: SqliteTestFumaDb,
30+ row: {
31+ readonly rowId: string;
32+ readonly tenant: string;
33+ readonly subject: string;
34+ readonly id: string;
35+ readonly title?: string;
36+ },
37+): Promise<unknown> =>
38+ db.client.execute({
39+ sql: `INSERT INTO artifact (row_id, tenant, owner, subject, id, title, description, code, created_at, updated_at)
40+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
41+ args: [
42+ row.rowId,
43+ row.tenant,
44+ "user",
45+ row.subject,
46+ row.id,
47+ row.title ?? "Dashboard",
48+ null,
49+ "export default function App() { return null; }",
50+ Date.now(),
51+ Date.now(),
52+ ],
53+ });
54+
55+describe("artifact table", () => {
56+ it.effect("is created by the runtime schema bring-up", () =>
57+ Effect.promise(() =>
58+ withDb(async (db) => {
59+ const info = await db.client.execute("PRAGMA table_info('artifact')");
60+ const columns = info.rows.map((row) => String(row["name"]));
61+ expect(columns).toEqual(
62+ expect.arrayContaining([
63+ "row_id",
64+ "tenant",
65+ "owner",
66+ "subject",
67+ "id",
68+ "title",
69+ "description",
70+ "code",
71+ "created_at",
72+ "updated_at",
73+ ]),
74+ );
75+ }),
76+ ),
77+ );
78+
79+ it.effect("keys one row per (tenant, owner, subject, id)", () =>
80+ Effect.promise(() =>
81+ withDb(async (db) => {
82+ await insertArtifact(db, { rowId: "a1", tenant: TENANT, subject: SUBJECT, id: "art_1" });
83+
84+ // The same artifact id under a DIFFERENT subject is a different row —
85+ // ids are only unique within one owner partition.
86+ await insertArtifact(db, {
87+ rowId: "a2",
88+ tenant: TENANT,
89+ subject: OTHER_SUBJECT,
90+ id: "art_1",
91+ });
92+
93+ await expect(
94+ insertArtifact(db, { rowId: "a3", tenant: TENANT, subject: SUBJECT, id: "art_1" }),
95+ ).rejects.toThrow();
96+
97+ const rows = await db.client.execute("SELECT row_id FROM artifact ORDER BY row_id");
98+ expect(rows.rows.map((row) => String(row["row_id"]))).toEqual(["a1", "a2"]);
99+ }),
100+ ),
101+ );
102+
103+ it.effect("hides another subject's artifacts from a bound executor", () =>
104+ Effect.promise(() =>
105+ withDb(async (db) => {
106+ await insertArtifact(db, {
107+ rowId: "a1",
108+ tenant: TENANT,
109+ subject: SUBJECT,
110+ id: "art_mine",
111+ title: "Mine",
112+ });
113+ await insertArtifact(db, {
114+ rowId: "a2",
115+ tenant: TENANT,
116+ subject: OTHER_SUBJECT,
117+ id: "art_theirs",
118+ title: "Theirs",
119+ });
120+
121+ const scoped = withQueryContext(db.db, { tenant: TENANT, subject: SUBJECT });
122+ const visible = await scoped.findMany("artifact", {});
123+ expect(visible.map((row) => row.id)).toEqual(["art_mine"]);
124+
125+ const otherScoped = withQueryContext(db.db, { tenant: TENANT, subject: OTHER_SUBJECT });
126+ const otherVisible = await otherScoped.findMany("artifact", {});
127+ expect(otherVisible.map((row) => row.id)).toEqual(["art_theirs"]);
128+ }),
129+ ),
130+ );
131+});
packages/core/sdk/src/artifact.ts+63−0
@@ -0,0 +1,63 @@
1+// ---------------------------------------------------------------------------
2+// Artifacts — saved generative-UI components. Row → public projection plus the
3+// operation inputs; the executor stitches these into `executor.artifacts` and
4+// the core `artifacts` HTTP group.
5+//
6+// An artifact IS the stored JSX source plus the title/description an agent
7+// matches against ("show me my active users dashboard"). Owner-scoped like a
8+// connection: created at the `user` tier, readable through the same owner
9+// policy, so org-tier sharing later needs no new machinery.
10+// ---------------------------------------------------------------------------
11+
12+import type { ArtifactRow, ArtifactSummaryRow } from "./core-schema";
13+import { ArtifactId, type Owner } from "./ids";
14+
15+export interface Artifact {
16+ readonly id: ArtifactId;
17+ readonly owner: Owner;
18+ readonly title: string;
19+ /** Model-supplied prose used for agent matching. Null when none was given. */
20+ readonly description: string | null;
21+ /** The JSX source. Only the full read carries it — lists stay light. */
22+ readonly code: string;
23+ readonly createdAt: Date;
24+ readonly updatedAt: Date;
25+}
26+
27+/** What a list returns: everything except the (potentially large) source. */
28+export type ArtifactSummary = Omit<Artifact, "code">;
29+
30+/** Create a new artifact, or overwrite an existing one in place when `id` names
31+ * one. v1 has no version history: a save replaces the stored source. */
32+export interface SaveArtifactInput {
33+ readonly id?: string;
34+ readonly title: string;
35+ readonly description?: string | null;
36+ readonly code: string;
37+}
38+
39+export interface RenameArtifactInput {
40+ readonly id: string;
41+ readonly title: string;
42+}
43+
44+export interface RemoveArtifactInput {
45+ readonly id: string;
46+}
47+
48+const asDate = (value: Date | number | string): Date =>
49+ value instanceof Date ? value : new Date(value);
50+
51+export const rowToArtifactSummary = (row: ArtifactSummaryRow): ArtifactSummary => ({
52+ id: ArtifactId.make(row.id),
53+ owner: row.owner as Owner,
54+ title: row.title,
55+ description: row.description ?? null,
56+ createdAt: asDate(row.created_at),
57+ updatedAt: asDate(row.updated_at),
58+});
59+
60+export const rowToArtifact = (row: ArtifactRow): Artifact => ({
61+ ...rowToArtifactSummary(row),
62+ code: row.code,
63+});
packages/core/sdk/src/artifacts.test.ts+197−0
@@ -0,0 +1,197 @@
1+import { describe, expect, it } from "@effect/vitest";
2+import { Effect, Predicate, Result } from "effect";
3+
4+import { makeTestExecutor } from "./testing";
5+
6+// The `executor.artifacts` surface against the real SQLite test db. Artifacts
7+// are owner-scoped rows with no plugin involvement, so the default test
8+// executor (one tenant + one bound subject) is the whole fixture.
9+
10+const CODE = "export default function App() { return <div>hi</div>; }";
11+
12+describe("executor.artifacts", () => {
13+ it.effect("list is empty when nothing is saved", () =>
14+ Effect.gen(function* () {
15+ const executor = yield* makeTestExecutor();
16+ expect(yield* executor.artifacts.list()).toEqual([]);
17+ }),
18+ );
19+
20+ it.effect("save mints an id and stores the source", () =>
21+ Effect.gen(function* () {
22+ const executor = yield* makeTestExecutor();
23+ const saved = yield* executor.artifacts.save({
24+ title: "Active users",
25+ description: "Daily active users over time",
26+ code: CODE,
27+ });
28+ expect(saved.id).not.toBe("");
29+ expect(saved.owner).toBe("user");
30+ expect(saved.title).toBe("Active users");
31+ expect(saved.description).toBe("Daily active users over time");
32+ expect(saved.code).toBe(CODE);
33+
34+ const fetched = yield* executor.artifacts.get(saved.id);
35+ expect(fetched).toEqual(saved);
36+ }),
37+ );
38+
39+ it.effect("save defaults a missing description to null", () =>
40+ Effect.gen(function* () {
41+ const executor = yield* makeTestExecutor();
42+ const saved = yield* executor.artifacts.save({ title: "Untitled", code: CODE });
43+ expect(saved.description).toBe(null);
44+ }),
45+ );
46+
47+ it.effect("list omits the source", () =>
48+ Effect.gen(function* () {
49+ const executor = yield* makeTestExecutor();
50+ const saved = yield* executor.artifacts.save({
51+ title: "Active users",
52+ description: "described",
53+ code: CODE,
54+ });
55+
56+ const listed = yield* executor.artifacts.list();
57+ // The list projection is deliberately code-free: a saved component can be
58+ // large and a picker only needs title/description to match on.
59+ expect(listed).toEqual([
60+ {
61+ id: saved.id,
62+ owner: "user",
63+ title: "Active users",
64+ description: "described",
65+ createdAt: saved.createdAt,
66+ updatedAt: saved.updatedAt,
67+ },
68+ ]);
69+ }),
70+ );
71+
72+ // `it.live` (real clock) with a full-second gap: `dateColumn` lands in SQLite
73+ // as a SECOND-resolution integer, so two artifacts saved inside one second
74+ // carry the same `updated_at` and only the id tiebreak separates them. This
75+ // pins the ordering the picker actually depends on, at the resolution the
76+ // storage layer really has.
77+ it.live("list returns the most recently updated artifact first", () =>
78+ Effect.gen(function* () {
79+ const executor = yield* makeTestExecutor();
80+ const first = yield* executor.artifacts.save({ title: "First", code: CODE });
81+ yield* Effect.sleep("1100 millis");
82+ const second = yield* executor.artifacts.save({ title: "Second", code: CODE });
83+ expect(second.updatedAt.getTime()).toBeGreaterThan(first.updatedAt.getTime());
84+
85+ expect((yield* executor.artifacts.list()).map((a) => a.id)).toEqual([second.id, first.id]);
86+
87+ // Touching the OLDER artifact floats it back to the top — the list is
88+ // keyed on `updated_at`, not on creation order.
89+ yield* Effect.sleep("1100 millis");
90+ yield* executor.artifacts.rename({ id: first.id, title: "First, touched" });
91+ expect((yield* executor.artifacts.list()).map((a) => a.id)).toEqual([first.id, second.id]);
92+ }),
93+ );
94+
95+ it.effect("save with an id overwrites in place", () =>
96+ Effect.gen(function* () {
97+ const executor = yield* makeTestExecutor();
98+ const saved = yield* executor.artifacts.save({ title: "Draft", code: CODE });
99+ const updated = yield* executor.artifacts.save({
100+ id: saved.id,
101+ title: "Final",
102+ description: "now described",
103+ code: "export default function App() { return null; }",
104+ });
105+
106+ expect(updated.id).toBe(saved.id);
107+ expect(updated.title).toBe("Final");
108+ expect(updated.description).toBe("now described");
109+ expect(updated.code).toBe("export default function App() { return null; }");
110+ expect(updated.createdAt.getTime()).toBe(saved.createdAt.getTime());
111+
112+ // Overwrite, not append: still exactly one row.
113+ const listed = yield* executor.artifacts.list();
114+ expect(listed.map((a) => a.id)).toEqual([saved.id]);
115+ }),
116+ );
117+
118+ it.effect("save fails for an id that names no visible artifact", () =>
119+ Effect.gen(function* () {
120+ const executor = yield* makeTestExecutor();
121+ const result = yield* Effect.result(
122+ executor.artifacts.save({ id: "art_missing", title: "Ghost", code: CODE }),
123+ );
124+ expect(Result.isFailure(result)).toBe(true);
125+ if (!Result.isFailure(result)) return;
126+ expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);
127+ }),
128+ );
129+
130+ it.effect("get fails for an unknown id", () =>
131+ Effect.gen(function* () {
132+ const executor = yield* makeTestExecutor();
133+ const result = yield* Effect.result(executor.artifacts.get("art_missing"));
134+ expect(Result.isFailure(result)).toBe(true);
135+ if (!Result.isFailure(result)) return;
136+ expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);
137+ }),
138+ );
139+
140+ it.effect("rename changes only the title", () =>
141+ Effect.gen(function* () {
142+ const executor = yield* makeTestExecutor();
143+ const saved = yield* executor.artifacts.save({
144+ title: "Old name",
145+ description: "kept",
146+ code: CODE,
147+ });
148+ const renamed = yield* executor.artifacts.rename({ id: saved.id, title: "New name" });
149+ expect(renamed.title).toBe("New name");
150+ expect(renamed.description).toBe("kept");
151+ expect(renamed.code).toBe(CODE);
152+ }),
153+ );
154+
155+ it.effect("rename fails for an unknown id", () =>
156+ Effect.gen(function* () {
157+ const executor = yield* makeTestExecutor();
158+ const result = yield* Effect.result(
159+ executor.artifacts.rename({ id: "art_missing", title: "Nope" }),
160+ );
161+ expect(Result.isFailure(result)).toBe(true);
162+ if (!Result.isFailure(result)) return;
163+ expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);
164+ }),
165+ );
166+
167+ it.effect("remove hard-deletes the artifact", () =>
168+ Effect.gen(function* () {
169+ const executor = yield* makeTestExecutor();
170+ const saved = yield* executor.artifacts.save({ title: "Temp", code: CODE });
171+ yield* executor.artifacts.remove({ id: saved.id });
172+
173+ expect(yield* executor.artifacts.list()).toEqual([]);
174+ const result = yield* Effect.result(executor.artifacts.get(saved.id));
175+ expect(Result.isFailure(result)).toBe(true);
176+ if (!Result.isFailure(result)) return;
177+ expect(Predicate.isTagged("ArtifactNotFoundError")(result.failure)).toBe(true);
178+ }),
179+ );
180+
181+ it.effect("remove is idempotent", () =>
182+ Effect.gen(function* () {
183+ const executor = yield* makeTestExecutor();
184+ yield* executor.artifacts.remove({ id: "art_missing" });
185+ }),
186+ );
187+
188+ it.effect("an executor with no subject cannot save a user artifact", () =>
189+ Effect.gen(function* () {
190+ // Pure-org executors (host request paths with no member bound) have no
191+ // user partition to file a personal artifact into.
192+ const executor = yield* makeTestExecutor({ subject: null });
193+ const result = yield* Effect.result(executor.artifacts.save({ title: "Orphan", code: CODE }));
194+ expect(Result.isFailure(result)).toBe(true);
195+ }),
196+ );
197+});
packages/core/sdk/src/core-schema.ts+34−0
@@ -357,6 +357,27 @@ export const coreTables = defineTables({ · 356 unmodified lines
357357 ["tenant", "owner", "subject", "id"],
358358 ),
359359
360+ // A saved generative-UI artifact — the JSX source a model produced, kept so
361+ // it can be re-rendered later and matched by title/description from any MCP
362+ // client. Owner-scoped like every other personal row: artifacts are created
363+ // at the `user` tier, and the `org` tier the table already carries is what
364+ // sharing will use later without new machinery.
365+ artifact: ownedExecutorTable(
366+ "artifact",
367+ {
368+ id: keyColumn("id"),
369+ title: textColumn("title"),
370+ // Model-supplied prose the agent matches against ("my active users
371+ // dashboard"). Nullable: the title alone is enough to save one.
372+ description: nullableTextColumn("description"),
373+ // The JSX source. Free-form TEXT, never indexed.
374+ code: textColumn("code"),
375+ created_at: dateColumn("created_at"),
376+ updated_at: dateColumn("updated_at"),
377+ },
378+ ["tenant", "owner", "subject", "id"],
379+ ),
380+
360381 // Host-owned plugin storage (shared `plugin_storage` table, owner-scoped).
361382 plugin_storage: ownedExecutorTable(
362383 "plugin_storage",
@@ -410,6 +431,19 @@ export const TOOL_INVOCATION_COLUMNS = [ · 47 unmodified lines
410431 ] as const satisfies readonly (keyof ToolRow)[];
411432 export type DefinitionRow = FumaRow<CoreSchema["definition"]>;
412433 export type ToolPolicyRow = FumaRow<CoreSchema["tool_policy"]>;
434+export type ArtifactRow = FumaRow<CoreSchema["artifact"]>;
435+/** The columns a list projects — everything except the JSX source, which only
436+ * a full read needs. */
437+export const ARTIFACT_SUMMARY_COLUMNS = [
438+ "owner",
439+ "id",
440+ "title",
441+ "description",
442+ "created_at",
443+ "updated_at",
444+] as const satisfies readonly (keyof ArtifactRow)[];
445+/** The artifact-row projection {@link ARTIFACT_SUMMARY_COLUMNS} selects. */
446+export type ArtifactSummaryRow = Pick<ArtifactRow, (typeof ARTIFACT_SUMMARY_COLUMNS)[number]>;
413447 export type PluginStorageRow = FumaRow<CoreSchema["plugin_storage"]>;
414448 export type BlobRow = FumaRow<CoreSchema["blob"]>;
415449
packages/core/sdk/src/errors.ts+25−2
@@ -2,7 +2,14 @@ import { Schema } from "effect"; · 1 unmodified line
22
33 import { ElicitationDeclinedError } from "./elicitation";
44 import type { StorageFailure } from "./fuma-runtime";
5−import { ConnectionName, IntegrationSlug, Owner, ProviderKey, ToolAddress } from "./ids";
5+import {
6+ ArtifactId,
7+ ConnectionName,
8+ IntegrationSlug,
9+ Owner,
10+ ProviderKey,
11+ ToolAddress,
12+} from "./ids";
613
714 export interface UserActionableError {
815 readonly __executorUserActionable: true;
@@ -191,6 +198,21 @@ export class CredentialResolutionError extends Schema.TaggedErrorClass<Credentia · 182 unmodified lines
191198 },
192199 ) {}
193200
201+// ---------------------------------------------------------------------------
202+// Artifacts
203+// ---------------------------------------------------------------------------
204+
205+/** No artifact with this id is visible to the bound owner scope — it was never
206+ * saved, was deleted, or belongs to another subject. */
207+export class ArtifactNotFoundError extends Schema.TaggedErrorClass<ArtifactNotFoundError>()(
208+ "ArtifactNotFoundError",
209+ { id: ArtifactId },
210+) {
211+ override get message(): string {
212+ return `Artifact not found: ${this.id}`;
213+ }
214+}
215+
194216 // ---------------------------------------------------------------------------
195217 // Union — the failure channel of `execute`.
196218 // ---------------------------------------------------------------------------
@@ -211,4 +233,5 @@ export type ExecuteError = · 14 unmodified lines
211233 export type ExecutorError =
212234 | ExecuteError
213235 | IntegrationNotFoundError
214− | IntegrationRemovalNotAllowedError;
236+ | IntegrationRemovalNotAllowedError
237+ | ArtifactNotFoundError;
packages/core/sdk/src/executor.ts+132−0
@@ -28,6 +28,7 @@ import type { · 27 unmodified lines
2828 import { HealthCheckResult, HealthCheckSpec } from "./health-check";
2929 import type { HealthCheckCandidate } from "./health-check";
3030 import {
31+ ARTIFACT_SUMMARY_COLUMNS,
3132 coreSchema,
3233 isToolPolicyAction,
3334 TOOL_INVOCATION_COLUMNS,
@@ -51,6 +52,16 @@ import { · 17 unmodified lines
5152
5253 export type { OnElicitation, InvokeOptions } from "./elicitation";
5354 import {
55+ rowToArtifact,
56+ rowToArtifactSummary,
57+ type Artifact,
58+ type ArtifactSummary,
59+ type RemoveArtifactInput,
60+ type RenameArtifactInput,
61+ type SaveArtifactInput,
62+} from "./artifact";
63+import {
64+ ArtifactNotFoundError,
5465 ConnectionNotFoundError,
5566 CredentialProviderNotRegisteredError,
5667 CredentialResolutionError,
@@ -65,6 +76,7 @@ import { · 8 unmodified lines
6576 type ExecuteError,
6677 } from "./errors";
6778 import {
79+ ArtifactId,
6880 AuthTemplateSlug,
6981 ConnectionAddress,
7082 ConnectionName,
@@ -377,6 +389,20 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 306 unmodified lines
377389 * Internal admin surface; the public HTTP shape is a separate concern.
378390 */
379391 readonly admin?: ExecutorAdmin;
392+ /** Saved generative-UI artifacts, visible to the bound owner scope. */
393+ readonly artifacts: {
394+ /** Newest first, without the JSX source — lists stay light. */
395+ readonly list: () => Effect.Effect<readonly ArtifactSummary[], StorageFailure>;
396+ readonly get: (id: string) => Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure>;
397+ /** Create, or overwrite an existing artifact in place when `id` is given. */
398+ readonly save: (
399+ input: SaveArtifactInput,
400+ ) => Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure>;
401+ readonly rename: (
402+ input: RenameArtifactInput,
403+ ) => Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure>;
404+ readonly remove: (input: RemoveArtifactInput) => Effect.Effect<void, StorageFailure>;
405+ };
380406
381407 readonly execute: (
382408 address: ToolAddress,
@@ -3854,6 +3880,105 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3471 unmodified lines
38543880 return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval);
38553881 });
38563882
3883+ // ------------------------------------------------------------------
3884+ // Artifacts — saved generative-UI components, owner-scoped.
3885+ // ------------------------------------------------------------------
3886+
3887+ // Reads take no explicit owner: the owner policy already narrows to the
3888+ // rows this binding may see (org rows plus this subject's own), which is
3889+ // exactly "visible to the bound owner scope" and stays correct unchanged
3890+ // when org-tier sharing lands. Writes are always `user` tier in v1.
3891+ const artifactById =
3892+ (id: string): CoreWhere =>
3893+ (b: AnyCb) =>
3894+ b("id", "=", id);
3895+
3896+ const artifactsList = (): Effect.Effect<readonly ArtifactSummary[], StorageFailure> =>
3897+ core
3898+ .findMany("artifact", {
3899+ // Newest first. `id` breaks ties so two artifacts sharing an
3900+ // `updated_at` millisecond list in a repeatable order rather than
3901+ // whatever the storage engine happens to return; which of the two
3902+ // comes first is arbitrary, only its stability is guaranteed.
3903+ orderBy: [
3904+ ["updated_at", "desc"],
3905+ ["id", "desc"],
3906+ ],
3907+ select: ARTIFACT_SUMMARY_COLUMNS,
3908+ })
3909+ .pipe(Effect.map((rows) => rows.map(rowToArtifactSummary)));
3910+
3911+ const artifactsGet = (
3912+ id: string,
3913+ ): Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure> =>
3914+ Effect.gen(function* () {
3915+ const row = yield* core.findFirst("artifact", { where: artifactById(id) });
3916+ if (!row) return yield* new ArtifactNotFoundError({ id: ArtifactId.make(id) });
3917+ return rowToArtifact(row);
3918+ });
3919+
3920+ const artifactsSave = (
3921+ input: SaveArtifactInput,
3922+ ): Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure> =>
3923+ Effect.gen(function* () {
3924+ const now = new Date();
3925+ const description = input.description ?? null;
3926+ // An explicit id overwrites in place (v1 keeps no version history). It
3927+ // must already resolve to a visible row: minting a caller-chosen id
3928+ // would let a stale client resurrect a deleted artifact silently.
3929+ if (input.id !== undefined) {
3930+ const where = artifactById(input.id);
3931+ const existing = yield* core.findFirst("artifact", { where });
3932+ if (!existing) {
3933+ return yield* new ArtifactNotFoundError({ id: ArtifactId.make(input.id) });
3934+ }
3935+ const set = {
3936+ title: input.title,
3937+ description,
3938+ code: input.code,
3939+ updated_at: now,
3940+ };
3941+ yield* core.updateMany("artifact", { where, set });
3942+ return rowToArtifact({ ...existing, ...set });
3943+ }
3944+ yield* requireUserSubject("user");
3945+ const keys = yield* Effect.try({
3946+ try: () => ownedKeys("user"),
3947+ catch: (cause) => storageFailureFromUnknown("invalid owner", cause),
3948+ });
3949+ const created = yield* core.create("artifact", {
3950+ tenant: keys.tenant,
3951+ owner: keys.owner,
3952+ subject: keys.subject,
3953+ id: `art_${Math.random().toString(36).slice(2)}${Date.now().toString(36)}`,
3954+ title: input.title,
3955+ description,
3956+ code: input.code,
3957+ created_at: now,
3958+ updated_at: now,
3959+ });
3960+ return rowToArtifact(created);
3961+ });
3962+
3963+ const artifactsRename = (
3964+ input: RenameArtifactInput,
3965+ ): Effect.Effect<Artifact, ArtifactNotFoundError | StorageFailure> =>
3966+ Effect.gen(function* () {
3967+ const where = artifactById(input.id);
3968+ const existing = yield* core.findFirst("artifact", { where });
3969+ if (!existing) {
3970+ return yield* new ArtifactNotFoundError({ id: ArtifactId.make(input.id) });
3971+ }
3972+ const set = { title: input.title, updated_at: new Date() };
3973+ yield* core.updateMany("artifact", { where, set });
3974+ return rowToArtifact({ ...existing, ...set });
3975+ });
3976+
3977+ // Hard delete: an artifact is not a connection, so there is no disabled or
3978+ // archived state to fall back to.
3979+ const artifactsRemove = (input: RemoveArtifactInput): Effect.Effect<void, StorageFailure> =>
3980+ core.deleteMany("artifact", { where: artifactById(input.id) });
3981+
38573982 // ------------------------------------------------------------------
38583983 // Elicitation
38593984 // ------------------------------------------------------------------
@@ -4614,6 +4739,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 754 unmodified lines
46144739 resolve: policiesResolve,
46154740 },
46164741 ...(admin ? { admin } : {}),
4742+ artifacts: {
4743+ list: artifactsList,
4744+ get: artifactsGet,
4745+ save: artifactsSave,
4746+ rename: artifactsRename,
4747+ remove: artifactsRemove,
4748+ },
46174749 execute,
46184750 close,
46194751 };
packages/core/sdk/src/ids.ts+4−0
@@ -59,6 +59,10 @@ export type ElicitationId = typeof ElicitationId.Type; · 58 unmodified lines
5959 export const PolicyId = Schema.String.pipe(Schema.brand("PolicyId"));
6060 export type PolicyId = typeof PolicyId.Type;
6161
62+/** A saved generative-UI artifact's id, unique within its owner partition. */
63+export const ArtifactId = Schema.String.pipe(Schema.brand("ArtifactId"));
64+export type ArtifactId = typeof ArtifactId.Type;
65+
6266 /**
6367 * The isolation partition (the org/workspace). Owns the catalog and namespaces
6468 * every connection. The executor is bound to one; `owner: "org"` files at this
packages/core/sdk/src/index.ts+14−0
@@ -52,6 +52,7 @@ export { · 51 unmodified lines
5252 ToolName,
5353 ElicitationId,
5454 PolicyId,
55+ ArtifactId,
5556 Tenant,
5657 Subject,
5758 Owner,
@@ -71,6 +72,7 @@ export { · 13 unmodified lines
7172 ConnectionNotFoundError,
7273 CredentialProviderNotRegisteredError,
7374 CredentialResolutionError,
75+ ArtifactNotFoundError,
7476 isUserActionableError,
7577 type ExecuteError,
7678 type ExecutorError,
@@ -150,6 +152,7 @@ export { · 73 unmodified lines
150152 type ToolInvocationRow,
151153 type DefinitionRow,
152154 type ToolPolicyRow,
155+ type ArtifactRow,
153156 type PluginStorageRow,
154157 type BlobRow,
155158 type ToolPolicyAction,
@@ -188,6 +191,17 @@ export { · 32 unmodified lines
188191 type PolicySource,
189192 } from "./policies";
190193
194+// Artifacts — saved generative-UI components.
195+export {
196+ rowToArtifact,
197+ rowToArtifactSummary,
198+ type Artifact,
199+ type ArtifactSummary,
200+ type SaveArtifactInput,
201+ type RenameArtifactInput,
202+ type RemoveArtifactInput,
203+} from "./artifact";
204+
191205 // Elicitation.
192206 export {
193207 FormElicitation,
packages/core/sdk/src/shared.ts+11−0
@@ -10,6 +10,7 @@ · 9 unmodified lines
1010
1111 // Branded ids + the owner literal.
1212 export {
13+ ArtifactId,
1314 AuthTemplateSlug,
1415 ConnectionAddress,
1516 ConnectionName,
@@ -62,6 +63,7 @@ export { · 46 unmodified lines
6263 InvalidConnectionInputError,
6364 CredentialProviderNotRegisteredError,
6465 CredentialResolutionError,
66+ ArtifactNotFoundError,
6567 isUserActionableError,
6668 type ExecuteError,
6769 type ExecutorError,
@@ -101,6 +103,15 @@ export { · 33 unmodified lines
101103 } from "./policies";
102104 export type { ToolPolicyAction } from "./core-schema";
103105
106+// Artifact projections (types only — the row mappers are server-side).
107+export type {
108+ Artifact,
109+ ArtifactSummary,
110+ SaveArtifactInput,
111+ RenameArtifactInput,
112+ RemoveArtifactInput,
113+} from "./artifact";
114+
104115 // Schema-side views + onboarding autodetect.
105116 export { ToolSchemaView, IntegrationDetectionResult } from "./types";
106117