Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions apps/host-cloudflare/DELEGATION.md
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,20 @@ reaches through Access. With `X-Executor-Subject` (and `X-Executor-Subject-Email
it acts for that person, as a trusted delegator would. The trusted mode lives on a
config only the entrypoint builds (`loadConfig(env, { internal: true })`), so no
header or variable can switch the public `fetch` into it.

## The link-preview door

For link previews in internal services there is a second, much narrower
entrypoint, `ExecutorUnfurl`:

```jsonc
"services": [
{ "binding": "EXECUTOR_UNFURL", "service": "executor-cloudflare", "entrypoint": "ExecutorUnfurl" }
]
```

It is RPC only: `artifactSummary(id)` returns `{ title, description, updatedAt }`
for an artifact in this deployment's tenant, whoever owns it, or `null` for an
unknown or malformed id. It takes no identity, writes nothing and never returns
source, bindings, previews, owner or subject. Like the door above, the binding
is the trust boundary and it has no public route.
18 changes: 1 addition & 17 deletions apps/host-cloudflare/src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,14 @@ import {
ExecutorApp,
makeScopedExecutor,
textFailureStrategy,
type ExecutorDbHandle,
} from "@executor-js/api/server";

import { internalPrincipal } from "./auth/cloudflare-access";
import { reconcileInternalIntegrations } from "./internal-integrations/reconcile";
import { makeInternalListTools } from "./internal-integrations/list-tools";
import { loadConfig, type CloudflareConfig, type CloudflareEnv } from "./config";
import { makeCloudflarePlugins, type CloudflarePlugins } from "./plugins";
import { createD1ExecutorDb } from "./db/d1";
import { openDb } from "./db/handle";
import { cloudflareAccessIdentityLayer } from "./auth/cloudflare-access";
import {
CloudflareCodeExecutorProvider,
Expand Down Expand Up @@ -42,21 +41,6 @@ import { makeCloudflareMcpAgentHandler } from "./mcp/agent-handler";
// so the providers close over it instead of reading process.env.
// ===========================================================================

// One D1 handle per isolate, shared by the request path and the cron. A failed
// open is dropped so the next caller retries.
let dbHandle: Promise<ExecutorDbHandle> | null = null;

const openDb = (env: CloudflareEnv): Promise<ExecutorDbHandle> => {
if (dbHandle === null) {
const opening = createD1ExecutorDb(env.DB, env.BLOBS);
dbHandle = opening;
void opening.then(undefined, () => {
if (dbHandle === opening) dbHandle = null;
});
}
return dbHandle;
};

// One reconcile pass, run from the cron trigger (never on the request path), by a
// system executor that may write org rows. Also refreshes existing declared
// catalogs whose tool list changed. Never throws.
Expand Down
19 changes: 19 additions & 0 deletions apps/host-cloudflare/src/db/handle.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type { ExecutorDbHandle } from "@executor-js/api/server";

import type { CloudflareEnv } from "../config";
import { createD1ExecutorDb } from "./d1";

// One D1 handle per isolate, shared by the request path, the cron and the
// unfurl door. A failed open is dropped so the next caller retries.
let dbHandle: Promise<ExecutorDbHandle> | null = null;

export const openDb = (env: CloudflareEnv): Promise<ExecutorDbHandle> => {
if (dbHandle === null) {
const opening = createD1ExecutorDb(env.DB, env.BLOBS);
dbHandle = opening;
void opening.then(undefined, () => {
if (dbHandle === opening) dbHandle = null;
});
}
return dbHandle;
};
133 changes: 133 additions & 0 deletions apps/host-cloudflare/src/unfurl.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { afterAll, beforeAll, describe, expect, it } from "@effect/vitest";
import type { D1Database } from "@cloudflare/workers-types";
import { withQueryContext } from "@executor-js/fumadb/query";

import { collectTables, type SqliteDataMigrationClient } from "@executor-js/sdk";
import { createSqliteTestFumaDb, type SqliteTestFumaDb } from "@executor-js/sdk/testing";

import type { CloudflareEnv } from "./config";
import { artifactSummaryFor } from "./unfurl";

const TENANT = "acme";

const makeCountingD1 = (
client: SqliteDataMigrationClient,
): { readonly db: D1Database; readonly calls: () => number } => {
let calls = 0;
const prepare = (sql: string) => {
const statement = (args: readonly unknown[]): Record<string, unknown> => ({
bind: (...values: readonly unknown[]) => statement([...args, ...values]),
all: async () => {
calls += 1;
const result = await client.execute({ sql, args });
return { success: true, meta: {}, results: result.rows };
},
raw: async () => {
calls += 1;
const result = await client.execute({ sql, args });
return result.rows.map((row) => Object.values(row));
},
run: async () => {
calls += 1;
await client.execute({ sql, args });
return { success: true, meta: {}, results: [] };
},
});
return statement([]);
};
// oxlint-disable-next-line executor/no-double-cast -- test double: only the D1 methods used by schema preparation and reads are implemented
const db = { prepare, withSession: () => ({ prepare }) } as unknown as D1Database;
return { db, calls: () => calls };
};

const insertArtifact = (
sqlite: SqliteTestFumaDb,
row: {
readonly tenant: string;
readonly owner: "org" | "user";
readonly subject: string;
readonly id: string;
readonly title: string;
readonly description: string | null;
},
) =>
withQueryContext(sqlite.db, { tenant: row.tenant, subject: row.subject }).create("artifact", {
...row,
code: "export default function App() { return <div>SECRET-source</div>; }",
bindings: null,
preview: "<div>SECRET-preview</div>",
preview_capture: null,
compiled_fingerprint: null,
compiled_js: null,
compiled_css: null,
created_by: null,
created_at: new Date(1_700_000_000_000),
updated_at: new Date(1_700_000_060_000),
});

describe("artifactSummaryFor", () => {
let sqlite: SqliteTestFumaDb;
let d1Calls: () => number;
let env: CloudflareEnv;

beforeAll(async () => {
sqlite = await createSqliteTestFumaDb({ tables: collectTables() });
const counting = makeCountingD1(sqlite.client);
d1Calls = counting.calls;
// oxlint-disable-next-line executor/no-double-cast -- test double: only the vars and the D1 binding the code under test reads are set
env = {
DB: counting.db,
ENABLE_DEV_AUTH: "true",
EXECUTOR_SECRET_KEY: "test-secret-key-0123456789abcdef",
SELF_HOSTED_ORG_ID: TENANT,
} as unknown as CloudflareEnv;
// Open once so the schema exists, then seed straight through storage.
await artifactSummaryFor(env, "art_warmup");
await insertArtifact(sqlite, {
tenant: TENANT,
owner: "user",
subject: "someone",
id: "art_personal",
title: "My scratchpad",
description: "Only for me",
});
await insertArtifact(sqlite, {
tenant: "other",
owner: "org",
subject: "",
id: "art_foreign",
title: "Other tenant",
description: null,
});
});

afterAll(async () => {
await sqlite.close();
});

it("summarises a personal artifact of someone else, and nothing more than its summary", async () => {
const summary = await artifactSummaryFor(env, "art_personal");

expect(summary).toEqual({
title: "My scratchpad",
description: "Only for me",
updatedAt: 1_700_000_060_000,
});
expect(JSON.stringify(summary)).not.toContain("SECRET");
});

it("answers null for an unknown id and for another tenant's artifact", async () => {
expect(await artifactSummaryFor(env, "art_unknown")).toBe(null);
expect(await artifactSummaryFor(env, "art_foreign")).toBe(null);
});

it("answers null for a malformed id without touching storage", async () => {
const before = d1Calls();

for (const id of ["../x", "ART_personal", "art_", `art_${"a".repeat(61)}`, "", "art_a b"]) {
expect(await artifactSummaryFor(env, id)).toBe(null);
}

expect(d1Calls()).toBe(before);
});
});
55 changes: 55 additions & 0 deletions apps/host-cloudflare/src/unfurl.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { Effect, Layer } from "effect";

import { dbProviderLayer, makePlatformExecutor } from "@executor-js/api/server";
import type { AdminArtifactSummary, Executor } from "@executor-js/sdk";

import { loadConfig, type CloudflareEnv } from "./config";
import { openDb } from "./db/handle";
import { makeCloudflareHostConfig, makeCloudflarePluginsProvider } from "./execution";

// ---------------------------------------------------------------------------
// The read behind `ExecutorUnfurl`: link previews for internal services. One
// subject-less, read-only platform executor per isolate, bound to the same D1
// database and tenant (`organizationId`) as the public and internal doors. It
// sees every artifact in the tenant whoever owns it, and selects only title,
// description and last edit.
// ---------------------------------------------------------------------------

const ARTIFACT_ID = /^art_[a-z0-9]{1,60}$/;

let executor: Promise<Executor> | null = null;

// A failed build is dropped so the next caller retries.
const platformExecutor = (env: CloudflareEnv): Promise<Executor> => {
if (executor === null) {
const building = openDb(env).then((handle) => {
const config = loadConfig(env);
return Effect.runPromise(
makePlatformExecutor(config.organizationId).pipe(
Effect.provide(
Layer.mergeAll(
dbProviderLayer(Effect.succeed(handle)),
makeCloudflarePluginsProvider(config),
makeCloudflareHostConfig(config),
),
),
),
);
});
executor = building;
void building.then(undefined, () => {
if (executor === building) executor = null;
});
}
return executor;
};

export const artifactSummaryFor = async (
env: CloudflareEnv,
id: string,
): Promise<AdminArtifactSummary | null> => {
if (!ARTIFACT_ID.test(id)) return null;
const admin = (await platformExecutor(env)).admin;
if (admin === undefined) return null;
return Effect.runPromise(admin.artifactSummary(id));
};
17 changes: 17 additions & 0 deletions apps/host-cloudflare/src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { WorkerEntrypoint } from "cloudflare:workers";

import { setQuickJSModuleLoader } from "@executor-js/runtime-quickjs";
import type { AdminArtifactSummary } from "@executor-js/sdk";

import { makeCloudflareApp, reconcileInternalIntegrationsOnce } from "./app";
import {
Expand All @@ -10,6 +11,7 @@ import {
type CloudflareEnv,
} from "./config";
import { mcpResourceFromPath } from "./mcp/resource";
import { artifactSummaryFor } from "./unfurl";

// Workers cannot fetch the default QuickJS .wasm, so every QuickJS user in this
// script (the fetch handlers and the MCP Durable Objects) resolves the
Expand All @@ -34,6 +36,8 @@ export { McpExecutionOwnerDirectoryDO, McpSessionDO } from "./mcp";
// reaches it through a `services` binding with `entrypoint: "ExecutorInternal"`.
// It builds its OWN app from a config marked `trustedInternal`; nothing in
// `env` or in a request can switch the public door into that mode.
// - `ExecutorUnfurl` is a narrow read-only RPC door, reached the same way with
// `entrypoint: "ExecutorUnfurl"`: link previews for internal services.
// ---------------------------------------------------------------------------

interface Serve {
Expand Down Expand Up @@ -99,6 +103,19 @@ export class ExecutorInternal extends WorkerEntrypoint<CloudflareEnv> {
}
}

/**
* The link-preview door. Reachable only through a `services` binding with
* `entrypoint: "ExecutorUnfurl"`; it has no route and takes no identity. It
* returns the title, description and last edit of an artifact in this
* deployment's tenant, whoever owns it, and never the source, bindings,
* previews, owner or subject. Read-only.
*/
export class ExecutorUnfurl extends WorkerEntrypoint<CloudflareEnv> {
artifactSummary(id: string): Promise<AdminArtifactSummary | null> {
return artifactSummaryFor(this.env, id);
}
}

export default {
fetch: (request: Request, env: CloudflareEnv, ctx: ExecutionContext): Promise<Response> =>
serveRequest(request, env, ctx, resolvePublic),
Expand Down
1 change: 1 addition & 0 deletions packages/core/api/src/admin/admin-users.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1220,6 +1220,7 @@ const recordingAdmin = (calls: string[]): ExecutorAdmin => ({
calls.push("getSubjectWithConnections");
return Effect.succeed({ ...A_SUBJECT, connections: [] });
},
artifactSummary: () => Effect.succeed(null),
});

describe("admin users reads — the ?email= filter is applied before the read", () => {
Expand Down
41 changes: 41 additions & 0 deletions packages/core/sdk/src/executor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -678,6 +678,15 @@ const normalizeAdminPaging = (
return { limit, offset };
};

/** The title, description and last edit of one artifact: a link preview, and
* nothing that could render or run it. */
export interface AdminArtifactSummary {
readonly title: string;
readonly description: string | null;
/** Epoch ms of the last edit. */
readonly updatedAt: number;
}

export interface ExecutorAdmin {
/** One page of subjects under the tenant, oldest first (stable: ties break on
* `external_id`). ALWAYS bounded: no arguments means
Expand Down Expand Up @@ -719,6 +728,16 @@ export interface ExecutorAdmin {
readonly getSubjectWithConnections: (
externalId: string,
) => Effect.Effect<AdminSubjectWithConnections | null, StorageFailure>;
/**
* The preview fields of one artifact by id, whichever tier or subject owns
* it, or `null` when the tenant has none. Selects only title, description and
* `updated_at`: no source, bindings, compiled output, preview, owner or
* subject ever leaves storage. Ids are random, so a shared id is only
* theoretical; the most recently edited row wins.
*/
readonly artifactSummary: (
id: string,
) => Effect.Effect<AdminArtifactSummary | null, StorageFailure>;
}

export interface ExecutorDb {
Expand Down Expand Up @@ -7859,12 +7878,34 @@ export const createExecutor = <const TPlugins extends readonly AnyPlugin[] = rea
return { ...subject, connections };
});

const artifactSummary = (
id: string,
): Effect.Effect<AdminArtifactSummary | null, StorageFailure> =>
platformCore
.findFirst("artifact", {
where: (b: AnyCb) => b("id", "=", id),
orderBy: [["updated_at", "desc"]],
select: ["title", "description", "updated_at"],
})
.pipe(
Effect.map((row) =>
row === null
? null
: {
title: row.title,
description: row.description ?? null,
updatedAt: new Date(row.updated_at).getTime(),
},
),
);

return {
listSubjects,
getSubject,
listSubjectConnections,
listSubjectsWithConnections,
getSubjectWithConnections,
artifactSummary,
};
};

Expand Down
1 change: 1 addition & 0 deletions packages/core/sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -433,6 +433,7 @@ export {
export {
ADMIN_DEFAULT_PAGE_SIZE,
ADMIN_MAX_PAGE_SIZE,
type AdminArtifactSummary,
type AdminConnection,
type AdminListSubjectsOptions,
type AdminSubject,
Expand Down
Loading
Loading