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 @@
1CREATE 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
14ALTER TABLE "definition" ALTER COLUMN "name" SET DATA TYPE varchar(255);--> statement-breakpoint
15CREATE 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
92 "when": 1785193313601,92 "when": 1785193313601,
93 "tag": "0012_woozy_shiva",93 "tag": "0012_woozy_shiva",
94 "breakpoints": true94 "breakpoints": true
95 },
96 {
97 "idx": 13,
98 "version": "7",
99 "when": 1785354087138,
100 "tag": "0013_curly_rocket_racer",
101 "breakpoints": true
95 }102 }
96 ]103 ]
97}104}
apps/cloud/src/db/executor-schema.ts+20−0
@@ -226,6 +226,26 @@ export const tool_policy = pgTable( · 225 unmodified lines
226 ],226 ],
227);227);
228 228
229export 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
229export const plugin_storage = pgTable(249export const plugin_storage = pgTable(
230 "plugin_storage",250 "plugin_storage",
231 {251 {
apps/cloud/src/db/org-deletion.test.ts+13−0
@@ -28,6 +28,7 @@ import * as cloudSchema from "./schema"; · 27 unmodified lines
28import * as executorSchema from "./executor-schema";28import * as executorSchema from "./executor-schema";
29import { memberships, accounts } from "./schema";29import { memberships, accounts } from "./schema";
30import {30import {
31 artifact,
31 blob,32 blob,
32 connection,33 connection,
33 definition,34 definition,
@@ -146,6 +147,17 @@ const seedTenant = async (db: DrizzleDb, tenant: string, tag: string) => { · 112 unmodified lines
146 tenant,147 tenant,
147 });148 });
148 149
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
149 const orgNs = `o:${tenant}/plugin`;161 const orgNs = `o:${tenant}/plugin`;
150 const userNs = `u:${tenant}:subject/plugin`;162 const userNs = `u:${tenant}:subject/plugin`;
151 await db.insert(blob).values({163 await db.insert(blob).values({
@@ -172,6 +184,7 @@ const TENANT_TABLES = [ · 20 unmodified lines
172 tool_policy,184 tool_policy,
173 plugin_storage,185 plugin_storage,
174 subject,186 subject,
187 artifact,
175] as const;188] as const;
176 189
177// Tables that are NOT purged by org id, each with the reason it is exempt. Any190// 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
16import type { DrizzleDb } from "./db";16import type { DrizzleDb } from "./db";
17import { organizations } from "./schema";17import { organizations } from "./schema";
18import {18import {
19 artifact,
19 blob,20 blob,
20 connection,21 connection,
21 definition,22 definition,
@@ -50,6 +51,7 @@ export const purgeOrganizationData = (db: DrizzleDb, organizationId: string): Pr · 28 unmodified lines
50 await tx.delete(tool_policy).where(eq(tool_policy.tenant, organizationId));51 await tx.delete(tool_policy).where(eq(tool_policy.tenant, organizationId));
51 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));52 await tx.delete(plugin_storage).where(eq(plugin_storage.tenant, organizationId));
52 await tx.delete(subject).where(eq(subject.tenant, organizationId));53 await tx.delete(subject).where(eq(subject.tenant, organizationId));
54 await tx.delete(artifact).where(eq(artifact.tenant, organizationId));
53 55
54 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by56 // Secrets, OAuth tokens, and cached specs live in `blob`, namespaced by
55 // owner: `o:<org>/<plugin>` (org scope) and `u:<org>:<subject>/<plugin>`57 // 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
8import { ExecutionsApi } from "./executions/api";8import { ExecutionsApi } from "./executions/api";
9import { OAuthApi } from "./oauth/api";9import { OAuthApi } from "./oauth/api";
10import { PoliciesApi } from "./policies/api";10import { PoliciesApi } from "./policies/api";
11import { ArtifactsApi } from "./artifacts/api";
11 12
12export const CoreExecutorApi = HttpApi.make("executor")13export const CoreExecutorApi = HttpApi.make("executor")
13 .add(ToolsApi)14 .add(ToolsApi)
@@ -17,6 +18,7 @@ export const CoreExecutorApi = HttpApi.make("executor") · 3 unmodified lines
17 .add(ExecutionsApi)18 .add(ExecutionsApi)
18 .add(OAuthApi)19 .add(OAuthApi)
19 .add(PoliciesApi)20 .add(PoliciesApi)
21 .add(ArtifactsApi)
20 .annotateMerge(22 .annotateMerge(
21 OpenApi.annotations({23 OpenApi.annotations({
22 title: "Executor API",24 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
9import { HttpApiEndpoint, HttpApiGroup } from "effect/unstable/httpapi";
10import { Schema } from "effect";
11import { ArtifactId, ArtifactNotFoundError, InternalError, Owner } from "@executor-js/sdk/shared";
12
13// ---------------------------------------------------------------------------
14// Params
15// ---------------------------------------------------------------------------
16
17const ArtifactParams = { artifactId: ArtifactId };
18
19// ---------------------------------------------------------------------------
20// Response / payload schemas
21// ---------------------------------------------------------------------------
22
23/** What a list returns — no `code`, so a long list stays cheap. */
24const 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
33const 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. */
39const 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
46const RenameArtifactPayload = Schema.Struct({
47 title: Schema.String,
48});
49
50// ---------------------------------------------------------------------------
51// Error schemas with HTTP status annotations
52// ---------------------------------------------------------------------------
53
54const ArtifactNotFound = ArtifactNotFoundError.annotate({ httpApiStatus: 404 });
55
56// ---------------------------------------------------------------------------
57// Group
58// ---------------------------------------------------------------------------
59
60export 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 @@
1import { HttpApiBuilder } from "effect/unstable/httpapi";
2import { Effect } from "effect";
3import type { Artifact, ArtifactSummary } from "@executor-js/sdk";
4
5import { ExecutorApi } from "../api";
6import { ExecutorService } from "../services";
7import { capture } from "@executor-js/api";
8
9const 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
18const artifactToResponse = (a: Artifact) => ({
19 ...summaryToResponse(a),
20 code: a.code,
21});
22
23export 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
7import { ExecutionsHandlers } from "./executions";7import { ExecutionsHandlers } from "./executions";
8import { OAuthHandlers } from "./oauth";8import { OAuthHandlers } from "./oauth";
9import { PoliciesHandlers } from "./policies";9import { PoliciesHandlers } from "./policies";
10import { ArtifactsHandlers } from "./artifacts";
10 11
11export { ToolsHandlers } from "./tools";12export { ToolsHandlers } from "./tools";
12export { IntegrationsHandlers } from "./integrations";13export { IntegrationsHandlers } from "./integrations";
@@ -15,6 +16,7 @@ export { ProvidersHandlers } from "./providers"; · 2 unmodified lines
15export { ExecutionsHandlers } from "./executions";16export { ExecutionsHandlers } from "./executions";
16export { OAuthHandlers } from "./oauth";17export { OAuthHandlers } from "./oauth";
17export { PoliciesHandlers } from "./policies";18export { PoliciesHandlers } from "./policies";
19export { ArtifactsHandlers } from "./artifacts";
18 20
19export const CoreHandlers = Layer.mergeAll(21export const CoreHandlers = Layer.mergeAll(
20 ToolsHandlers,22 ToolsHandlers,
@@ -24,4 +26,5 @@ export const CoreHandlers = Layer.mergeAll( · 3 unmodified lines
24 ExecutionsHandlers,26 ExecutionsHandlers,
25 OAuthHandlers,27 OAuthHandlers,
26 PoliciesHandlers,28 PoliciesHandlers,
29 ArtifactsHandlers,
27);30);
packages/core/api/src/index.ts+1−0
@@ -34,6 +34,7 @@ export { · 33 unmodified lines
34 type RunOAuthCallbackInput,34 type RunOAuthCallbackInput,
35} from "./oauth-popup";35} from "./oauth-popup";
36export { PoliciesApi } from "./policies/api";36export { PoliciesApi } from "./policies/api";
37export { ArtifactsApi } from "./artifacts/api";
37export {38export {
38 AccountApi,39 AccountApi,
39 AccountHttpApi,40 AccountHttpApi,
packages/core/api/src/server.ts+1−0
@@ -7,6 +7,7 @@ export { · 6 unmodified lines
7 ProvidersHandlers,7 ProvidersHandlers,
8 OAuthHandlers,8 OAuthHandlers,
9 PoliciesHandlers,9 PoliciesHandlers,
10 ArtifactsHandlers,
10 ExecutionsHandlers,11 ExecutionsHandlers,
11} from "./handlers";12} from "./handlers";
12export {13export {
packages/core/sdk/src/artifact-table.test.ts+131−0
@@ -0,0 +1,131 @@
1import { describe, expect, it } from "@effect/vitest";
2import { Effect } from "effect";
3import { withQueryContext } from "@executor-js/fumadb/query";
4
5import { collectTables } from "./executor";
6import { 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
15const TENANT = "t1";
16const SUBJECT = "user_a";
17const OTHER_SUBJECT = "user_b";
18
19const 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
28const 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
55describe("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
12import type { ArtifactRow, ArtifactSummaryRow } from "./core-schema";
13import { ArtifactId, type Owner } from "./ids";
14
15export 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. */
28export 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. */
32export interface SaveArtifactInput {
33 readonly id?: string;
34 readonly title: string;
35 readonly description?: string | null;
36 readonly code: string;
37}
38
39export interface RenameArtifactInput {
40 readonly id: string;
41 readonly title: string;
42}
43
44export interface RemoveArtifactInput {
45 readonly id: string;
46}
47
48const asDate = (value: Date | number | string): Date =>
49 value instanceof Date ? value : new Date(value);
50
51export 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
60export 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 @@
1import { describe, expect, it } from "@effect/vitest";
2import { Effect, Predicate, Result } from "effect";
3
4import { 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
10const CODE = "export default function App() { return <div>hi</div>; }";
11
12describe("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
357 ["tenant", "owner", "subject", "id"],357 ["tenant", "owner", "subject", "id"],
358 ),358 ),
359 359
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
360 // Host-owned plugin storage (shared `plugin_storage` table, owner-scoped).381 // Host-owned plugin storage (shared `plugin_storage` table, owner-scoped).
361 plugin_storage: ownedExecutorTable(382 plugin_storage: ownedExecutorTable(
362 "plugin_storage",383 "plugin_storage",
@@ -410,6 +431,19 @@ export const TOOL_INVOCATION_COLUMNS = [ · 47 unmodified lines
410] as const satisfies readonly (keyof ToolRow)[];431] as const satisfies readonly (keyof ToolRow)[];
411export type DefinitionRow = FumaRow<CoreSchema["definition"]>;432export type DefinitionRow = FumaRow<CoreSchema["definition"]>;
412export type ToolPolicyRow = FumaRow<CoreSchema["tool_policy"]>;433export type ToolPolicyRow = FumaRow<CoreSchema["tool_policy"]>;
434export type ArtifactRow = FumaRow<CoreSchema["artifact"]>;
435/** The columns a list projects — everything except the JSX source, which only
436 * a full read needs. */
437export 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. */
446export type ArtifactSummaryRow = Pick<ArtifactRow, (typeof ARTIFACT_SUMMARY_COLUMNS)[number]>;
413export type PluginStorageRow = FumaRow<CoreSchema["plugin_storage"]>;447export type PluginStorageRow = FumaRow<CoreSchema["plugin_storage"]>;
414export type BlobRow = FumaRow<CoreSchema["blob"]>;448export type BlobRow = FumaRow<CoreSchema["blob"]>;
415 449
packages/core/sdk/src/errors.ts+25−2
@@ -2,7 +2,14 @@ import { Schema } from "effect"; · 1 unmodified line
2 2
3import { ElicitationDeclinedError } from "./elicitation";3import { ElicitationDeclinedError } from "./elicitation";
4import type { StorageFailure } from "./fuma-runtime";4import type { StorageFailure } from "./fuma-runtime";
5import { ConnectionName, IntegrationSlug, Owner, ProviderKey, ToolAddress } from "./ids";5import {
6 ArtifactId,
7 ConnectionName,
8 IntegrationSlug,
9 Owner,
10 ProviderKey,
11 ToolAddress,
12} from "./ids";
6 13
7export interface UserActionableError {14export interface UserActionableError {
8 readonly __executorUserActionable: true;15 readonly __executorUserActionable: true;
@@ -191,6 +198,21 @@ export class CredentialResolutionError extends Schema.TaggedErrorClass<Credentia · 182 unmodified lines
191 },198 },
192) {}199) {}
193 200
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. */
207export 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
194// ---------------------------------------------------------------------------216// ---------------------------------------------------------------------------
195// Union — the failure channel of `execute`.217// Union — the failure channel of `execute`.
196// ---------------------------------------------------------------------------218// ---------------------------------------------------------------------------
@@ -211,4 +233,5 @@ export type ExecuteError = · 14 unmodified lines
211export type ExecutorError =233export type ExecutorError =
212 | ExecuteError234 | ExecuteError
213 | IntegrationNotFoundError235 | IntegrationNotFoundError
214 | IntegrationRemovalNotAllowedError;236 | IntegrationRemovalNotAllowedError
237 | ArtifactNotFoundError;
packages/core/sdk/src/executor.ts+132−0
@@ -28,6 +28,7 @@ import type { · 27 unmodified lines
28import { HealthCheckResult, HealthCheckSpec } from "./health-check";28import { HealthCheckResult, HealthCheckSpec } from "./health-check";
29import type { HealthCheckCandidate } from "./health-check";29import type { HealthCheckCandidate } from "./health-check";
30import {30import {
31 ARTIFACT_SUMMARY_COLUMNS,
31 coreSchema,32 coreSchema,
32 isToolPolicyAction,33 isToolPolicyAction,
33 TOOL_INVOCATION_COLUMNS,34 TOOL_INVOCATION_COLUMNS,
@@ -51,6 +52,16 @@ import { · 17 unmodified lines
51 52
52export type { OnElicitation, InvokeOptions } from "./elicitation";53export type { OnElicitation, InvokeOptions } from "./elicitation";
53import {54import {
55 rowToArtifact,
56 rowToArtifactSummary,
57 type Artifact,
58 type ArtifactSummary,
59 type RemoveArtifactInput,
60 type RenameArtifactInput,
61 type SaveArtifactInput,
62} from "./artifact";
63import {
64 ArtifactNotFoundError,
54 ConnectionNotFoundError,65 ConnectionNotFoundError,
55 CredentialProviderNotRegisteredError,66 CredentialProviderNotRegisteredError,
56 CredentialResolutionError,67 CredentialResolutionError,
@@ -65,6 +76,7 @@ import { · 8 unmodified lines
65 type ExecuteError,76 type ExecuteError,
66} from "./errors";77} from "./errors";
67import {78import {
79 ArtifactId,
68 AuthTemplateSlug,80 AuthTemplateSlug,
69 ConnectionAddress,81 ConnectionAddress,
70 ConnectionName,82 ConnectionName,
@@ -377,6 +389,20 @@ export type Executor<TPlugins extends readonly AnyPlugin[] = readonly []> = { · 306 unmodified lines
377 * Internal admin surface; the public HTTP shape is a separate concern.389 * Internal admin surface; the public HTTP shape is a separate concern.
378 */390 */
379 readonly admin?: ExecutorAdmin;391 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 };
380 406
381 readonly execute: (407 readonly execute: (
382 address: ToolAddress,408 address: ToolAddress,
@@ -3854,6 +3880,105 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 3471 unmodified lines
3854 return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval);3880 return resolveEffectivePolicy(toolId, policyRows, ownerRankForRow, requiresApproval);
3855 });3881 });
3856 3882
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
3857 // ------------------------------------------------------------------3982 // ------------------------------------------------------------------
3858 // Elicitation3983 // Elicitation
3859 // ------------------------------------------------------------------3984 // ------------------------------------------------------------------
@@ -4614,6 +4739,13 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea · 754 unmodified lines
4614 resolve: policiesResolve,4739 resolve: policiesResolve,
4615 },4740 },
4616 ...(admin ? { admin } : {}),4741 ...(admin ? { admin } : {}),
4742 artifacts: {
4743 list: artifactsList,
4744 get: artifactsGet,
4745 save: artifactsSave,
4746 rename: artifactsRename,
4747 remove: artifactsRemove,
4748 },
4617 execute,4749 execute,
4618 close,4750 close,
4619 };4751 };
packages/core/sdk/src/ids.ts+4−0
@@ -59,6 +59,10 @@ export type ElicitationId = typeof ElicitationId.Type; · 58 unmodified lines
59export const PolicyId = Schema.String.pipe(Schema.brand("PolicyId"));59export const PolicyId = Schema.String.pipe(Schema.brand("PolicyId"));
60export type PolicyId = typeof PolicyId.Type;60export type PolicyId = typeof PolicyId.Type;
61 61
62/** A saved generative-UI artifact's id, unique within its owner partition. */
63export const ArtifactId = Schema.String.pipe(Schema.brand("ArtifactId"));
64export type ArtifactId = typeof ArtifactId.Type;
65
62/**66/**
63 * The isolation partition (the org/workspace). Owns the catalog and namespaces67 * The isolation partition (the org/workspace). Owns the catalog and namespaces
64 * every connection. The executor is bound to one; `owner: "org"` files at this68 * 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
52 ToolName,52 ToolName,
53 ElicitationId,53 ElicitationId,
54 PolicyId,54 PolicyId,
55 ArtifactId,
55 Tenant,56 Tenant,
56 Subject,57 Subject,
57 Owner,58 Owner,
@@ -71,6 +72,7 @@ export { · 13 unmodified lines
71 ConnectionNotFoundError,72 ConnectionNotFoundError,
72 CredentialProviderNotRegisteredError,73 CredentialProviderNotRegisteredError,
73 CredentialResolutionError,74 CredentialResolutionError,
75 ArtifactNotFoundError,
74 isUserActionableError,76 isUserActionableError,
75 type ExecuteError,77 type ExecuteError,
76 type ExecutorError,78 type ExecutorError,
@@ -150,6 +152,7 @@ export { · 73 unmodified lines
150 type ToolInvocationRow,152 type ToolInvocationRow,
151 type DefinitionRow,153 type DefinitionRow,
152 type ToolPolicyRow,154 type ToolPolicyRow,
155 type ArtifactRow,
153 type PluginStorageRow,156 type PluginStorageRow,
154 type BlobRow,157 type BlobRow,
155 type ToolPolicyAction,158 type ToolPolicyAction,
@@ -188,6 +191,17 @@ export { · 32 unmodified lines
188 type PolicySource,191 type PolicySource,
189} from "./policies";192} from "./policies";
190 193
194// Artifacts — saved generative-UI components.
195export {
196 rowToArtifact,
197 rowToArtifactSummary,
198 type Artifact,
199 type ArtifactSummary,
200 type SaveArtifactInput,
201 type RenameArtifactInput,
202 type RemoveArtifactInput,
203} from "./artifact";
204
191// Elicitation.205// Elicitation.
192export {206export {
193 FormElicitation,207 FormElicitation,
packages/core/sdk/src/shared.ts+11−0
@@ -10,6 +10,7 @@ · 9 unmodified lines
10 10
11// Branded ids + the owner literal.11// Branded ids + the owner literal.
12export {12export {
13 ArtifactId,
13 AuthTemplateSlug,14 AuthTemplateSlug,
14 ConnectionAddress,15 ConnectionAddress,
15 ConnectionName,16 ConnectionName,
@@ -62,6 +63,7 @@ export { · 46 unmodified lines
62 InvalidConnectionInputError,63 InvalidConnectionInputError,
63 CredentialProviderNotRegisteredError,64 CredentialProviderNotRegisteredError,
64 CredentialResolutionError,65 CredentialResolutionError,
66 ArtifactNotFoundError,
65 isUserActionableError,67 isUserActionableError,
66 type ExecuteError,68 type ExecuteError,
67 type ExecutorError,69 type ExecutorError,
@@ -101,6 +103,15 @@ export { · 33 unmodified lines
101} from "./policies";103} from "./policies";
102export type { ToolPolicyAction } from "./core-schema";104export type { ToolPolicyAction } from "./core-schema";
103 105
106// Artifact projections (types only — the row mappers are server-side).
107export type {
108 Artifact,
109 ArtifactSummary,
110 SaveArtifactInput,
111 RenameArtifactInput,
112 RemoveArtifactInput,
113} from "./artifact";
114
104// Schema-side views + onboarding autodetect.115// Schema-side views + onboarding autodetect.
105export { ToolSchemaView, IntegrationDetectionResult } from "./types";116export { ToolSchemaView, IntegrationDetectionResult } from "./types";
106 117