From d409c989c05a7f09fa9427e46ce6d90c340fd22c Mon Sep 17 00:00:00 2001 From: fforres Date: Tue, 20 Oct 2026 00:02:55 -0700 Subject: [PATCH] feat(artifacts): share new artifacts with the whole workspace by default Artifacts are now created at the org tier, so every member of the tenant can list, open, rename and delete them. Artifacts saved earlier at the user tier stay personal. --- packages/core/sdk/src/artifact.ts | 4 +- packages/core/sdk/src/artifacts.test.ts | 59 +++++++++++++++++++++---- packages/core/sdk/src/core-schema.ts | 5 +-- packages/core/sdk/src/executor.ts | 9 ++-- 4 files changed, 58 insertions(+), 19 deletions(-) diff --git a/packages/core/sdk/src/artifact.ts b/packages/core/sdk/src/artifact.ts index af9a8ba068..56f9c6ed1c 100644 --- a/packages/core/sdk/src/artifact.ts +++ b/packages/core/sdk/src/artifact.ts @@ -5,8 +5,8 @@ // // An artifact IS the stored JSX source plus the title/description an agent // matches against ("show me my active users dashboard"). Owner-scoped like a -// connection: created at the `user` tier, readable through the same owner -// policy, so org-tier sharing later needs no new machinery. +// connection, and created at the `org` tier so every member of the tenant can +// read it through the same owner policy. // --------------------------------------------------------------------------- import { Option, Schema, type Effect } from "effect"; diff --git a/packages/core/sdk/src/artifacts.test.ts b/packages/core/sdk/src/artifacts.test.ts index d07a56fbc5..e875f8428b 100644 --- a/packages/core/sdk/src/artifacts.test.ts +++ b/packages/core/sdk/src/artifacts.test.ts @@ -1,9 +1,11 @@ import { describe, expect, it } from "@effect/vitest"; import { Effect, Predicate, Result } from "effect"; +import { withQueryContext } from "@executor-js/fumadb/query"; -import { makeTestExecutor } from "./testing"; +import { createExecutor } from "./executor"; +import { makeTestExecutor, makeTestWorkspaceHarness } from "./testing"; import type { ArtifactBinding } from "./artifact"; -import { ConnectionName, IntegrationSlug } from "./ids"; +import { ConnectionName, IntegrationSlug, Subject, Tenant } from "./ids"; const binding = (integration: string, owner: "user" | "org", name: string): ArtifactBinding => ({ integration: IntegrationSlug.make(integration), @@ -34,7 +36,7 @@ describe("executor.artifacts", () => { code: CODE, }); expect(saved.id).not.toBe(""); - expect(saved.owner).toBe("user"); + expect(saved.owner).toBe("org"); expect(saved.title).toBe("Active users"); expect(saved.description).toBe("Daily active users over time"); expect(saved.code).toBe(CODE); @@ -123,7 +125,7 @@ describe("executor.artifacts", () => { expect(listed).toEqual([ { id: saved.id, - owner: "user", + owner: "org", title: "Active users", description: "described", preview: null, @@ -250,16 +252,55 @@ describe("executor.artifacts", () => { }), ); - it.effect("an executor with no subject cannot save a user artifact", () => + it.effect("an executor with no subject can save an artifact", () => Effect.gen(function* () { - // Pure-org executors (host request paths with no member bound) have no - // user partition to file a personal artifact into. const executor = yield* makeTestExecutor({ subject: null }); - const result = yield* Effect.result(executor.artifacts.save({ title: "Orphan", code: CODE })); - expect(Result.isFailure(result)).toBe(true); + const saved = yield* executor.artifacts.save({ title: "Shared", code: CODE }); + expect(saved.owner).toBe("org"); + expect((yield* executor.artifacts.list()).map((a) => a.title)).toEqual(["Shared"]); }), ); + it.effect("another member of the tenant can open, rename and delete a saved artifact", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const bob = yield* createExecutor({ + ...config, + subject: Subject.make("bob"), + db: withQueryContext(config.testDb.db, { tenant: String(config.tenant), subject: "bob" }), + }); + yield* Effect.addFinalizer(() => bob.close().pipe(Effect.ignore)); + + const saved = yield* alice.artifacts.save({ title: "Revenue", code: CODE }); + + expect((yield* bob.artifacts.list()).map((a) => a.title)).toEqual(["Revenue"]); + expect((yield* bob.artifacts.get(saved.id)).code).toBe(CODE); + + yield* bob.artifacts.rename({ id: saved.id, title: "Revenue by month" }); + expect((yield* alice.artifacts.get(saved.id)).title).toBe("Revenue by month"); + + yield* bob.artifacts.remove({ id: saved.id }); + expect(yield* alice.artifacts.list()).toEqual([]); + }).pipe(Effect.scoped), + ); + + it.effect("another tenant cannot see a saved artifact", () => + Effect.gen(function* () { + const { config, executor: alice } = yield* makeTestWorkspaceHarness(); + const outsider = yield* createExecutor({ + ...config, + tenant: Tenant.make("other-tenant"), + subject: Subject.make("mallory"), + db: withQueryContext(config.testDb.db, { tenant: "other-tenant", subject: "mallory" }), + }); + yield* Effect.addFinalizer(() => outsider.close().pipe(Effect.ignore)); + + yield* alice.artifacts.save({ title: "Revenue", code: CODE }); + + expect(yield* outsider.artifacts.list()).toEqual([]); + }).pipe(Effect.scoped), + ); + describe("previews", () => { const MARKUP = '

Revenue

'; const SETTLED = '

Revenue

12,480 active

'; diff --git a/packages/core/sdk/src/core-schema.ts b/packages/core/sdk/src/core-schema.ts index 65aecda3ee..0593c4f2f7 100644 --- a/packages/core/sdk/src/core-schema.ts +++ b/packages/core/sdk/src/core-schema.ts @@ -393,9 +393,8 @@ export const coreTables = defineTables({ // A saved generative-UI artifact — the JSX source a model produced, kept so // it can be re-rendered later and matched by title/description from any MCP - // client. Owner-scoped like every other personal row: artifacts are created - // at the `user` tier, and the `org` tier the table already carries is what - // sharing will use later without new machinery. + // client. Owner-scoped like every other row: new artifacts are created at + // the `org` tier, so they are shared with the whole tenant. artifact: ownedExecutorTable( "artifact", { diff --git a/packages/core/sdk/src/executor.ts b/packages/core/sdk/src/executor.ts index ad14303dd3..f544b97988 100644 --- a/packages/core/sdk/src/executor.ts +++ b/packages/core/sdk/src/executor.ts @@ -6705,9 +6705,9 @@ export const createExecutor = (b: AnyCb) => @@ -6836,9 +6836,8 @@ export const createExecutor = ownedKeys("user"), + try: () => ownedKeys("org"), catch: (cause) => storageFailureFromUnknown("invalid owner", cause), }); const created = yield* core.create("artifact", {