From b35e100d029fe612dc8849f4766edef2a88e9eb5 Mon Sep 17 00:00:00 2001 From: The-AarushiSingh <175547726+The-AarushiSingh@users.noreply.github.com> Date: Sat, 10 Oct 2026 15:05:33 +0530 Subject: [PATCH 1/3] feat(hosted-web): add blank app path to Add app --- .../web/src/contracts/dashboard-bindings.ts | 18 +++++++ .../web/src/implementation/pages/add-app.tsx | 47 ++++++++++++++++++- 2 files changed, 64 insertions(+), 1 deletion(-) diff --git a/apps/hosted/web/src/contracts/dashboard-bindings.ts b/apps/hosted/web/src/contracts/dashboard-bindings.ts index 233b73ccb3..91b48e332e 100644 --- a/apps/hosted/web/src/contracts/dashboard-bindings.ts +++ b/apps/hosted/web/src/contracts/dashboard-bindings.ts @@ -9,6 +9,16 @@ import { HostedClient, catalogAtom } from "./api.ts"; import { inventoryAtom } from "./organization.ts"; import { acknowledgeApp, toolsAtom } from "./apps.ts"; +/** Minimal app the Add app "blank" path deploys; must include root index.ts. */ +const blankAppFiles = [ + { + path: "index.ts", + content: `import { defineApp, router } from "apps"; +export default defineApp({ accounts: {} }, async () => ({ tools: router({}) })); +`, + }, +] as const; + /** A separate set of atom identities per organization prevents cross-organization cache reuse. */ export const dashboardAtoms = Atom.family((organization: OrganizationReference) => ({ inventory: Atom.map( @@ -53,4 +63,12 @@ export const dashboardAtoms = Atom.family((organization: OrganizationReference) client.apps.importCustom({ params: { organization }, payload: { source: input } }), ).pipe(Effect.tap((saved) => Effect.sync(() => acknowledgeApp(get, organization, saved)))), ), + createBlank: HostedClient.runtime.fn((input: { name: string }, get) => + Effect.flatMap(HostedClient, (client) => + client.apps.deploy({ + params: { organization }, + payload: { name: input.name, files: [...blankAppFiles] }, + }), + ).pipe(Effect.tap((saved) => Effect.sync(() => acknowledgeApp(get, organization, saved)))), + ), })); diff --git a/apps/hosted/web/src/implementation/pages/add-app.tsx b/apps/hosted/web/src/implementation/pages/add-app.tsx index 07840eeb77..0c91a9a1c4 100644 --- a/apps/hosted/web/src/implementation/pages/add-app.tsx +++ b/apps/hosted/web/src/implementation/pages/add-app.tsx @@ -7,6 +7,7 @@ import { Link, useNavigate } from "@tanstack/react-router"; import { Button } from "@executor-js/ui/components/button"; import { CatalogPage as Catalog, CatalogInstall } from "@executor-js/ui/dashboard/catalog"; import { InstallPublication } from "@executor-js/ui/dashboard/install-publication"; +import { AppCreateForm } from "@executor-js/ui/dashboard/app-create"; import { type AppAcknowledgement } from "@executor-js/ui/contracts/app-management"; import type { CatalogEntry } from "@executor-js/catalog/contracts"; import type { Publication } from "@executor-js/sdk"; @@ -19,7 +20,9 @@ import { acknowledgeApp } from "../../contracts/apps.ts"; type Selection = | { readonly kind: "catalog"; readonly entry: CatalogEntry } - | { readonly kind: "publication"; readonly publication: typeof Publication.Type }; + | { readonly kind: "publication"; readonly publication: typeof Publication.Type } + | { readonly kind: "blank" }; + /** Public publications and integration templates share Add app and the same organization-owned app records. */ export function AddAppPage() { const mcpOrigin = useMcpOrigin(); @@ -64,6 +67,37 @@ export function AddAppPage() { onInstalled={installed} /> ); + if (selection?.kind === "blank") + return ( +
+ +
+
+

+ Create blank app +

+

Start from an empty app and build with your agent.

+
+
+ ({ name })} + onCreated={installed} + onCancel={back} + label="Create app" + /> +
+ ); return ( + -
-
-

- Create blank app -

-

Start from an empty app and build with your agent.

-
-
- ({ name })} - onCreated={installed} - onCancel={back} - label="Create app" - /> - + { + reportBrowserUsage({ area: "apps", action: "blank_app", outcome: "cancelled" }); + setSelection(undefined); + }} + onCreated={(app) => + navigate({ + to: "/org/$organizationSlug/apps/$appId", + search: { view: "source" }, + params: { organizationSlug, appId: app.id }, + }) + } + /> ); return ( ({ + tools: router({}), +})); +`; + +/** `index.ts` and a `package.json` that pins this host's `apps` release. */ +export const blankAppFiles = ( + name: string, + appsVersion: string, +): readonly [SourceFile, ...SourceFile[]] => [ + { path: "index.ts", content: index }, + { + path: "package.json", + content: `${JSON.stringify( + { + name: appSlug(name), + private: true, + type: "module", + dependencies: { apps: appsVersion }, + }, + null, + 2, + )}\n`, + }, +]; diff --git a/packages/ui/src/implementation/dashboard/catalog.tsx b/packages/ui/src/implementation/dashboard/catalog.tsx index e044877d86..6870f4f387 100644 --- a/packages/ui/src/implementation/dashboard/catalog.tsx +++ b/packages/ui/src/implementation/dashboard/catalog.tsx @@ -7,7 +7,7 @@ import { AsyncResult } from "effect/reactivity"; import { useAtomRefresh, useAtomValue } from "@effect/atom-react"; import { useState, type ReactNode } from "react"; import { Option } from "effect"; -import type { DeployedApp } from "@executor-js/sdk"; +import type { App, DeployedApp } from "@executor-js/sdk"; import { HugeiconsIcon } from "@hugeicons/react"; import { ArrowLeft02Icon, ArrowRight02Icon } from "@hugeicons/core-free-icons"; import { quickAdd, type CatalogEntry } from "@executor-js/catalog/contracts"; @@ -191,6 +191,49 @@ export function CatalogPage({ ); } +/** Name a new app, then the product creates it the way an agent does and opens its source. */ +export function BlankAppSetup({ + mutation, + Failure, + onCreated, + onBack, +}: MutationProps<{ readonly name: string }, A, E> & { + readonly onCreated: (app: A) => void | Promise; + readonly onBack: () => void; +}) { + const result = useAtomValue(mutation); + return ( +
+ +
+
+

+ Create blank app +

+

Start from an empty app and build with your agent.

+
+
+ ({ name })} + onCreated={onCreated} + onCancel={onBack} + label="Create app" + /> +
+ ); +} /** * MCP servers and built-in apps are added directly; the server confirms how an MCP server * connects. Every other service is set up by the user's agent from a copied prompt. From fc558832380e145099a9c43441480a2b2ad735f7 Mon Sep 17 00:00:00 2001 From: The-AarushiSingh <175547726+The-AarushiSingh@users.noreply.github.com> Date: Sun, 11 Oct 2026 21:00:42 +0530 Subject: [PATCH 3/3] fix(oauth): avoid reusing non-manual previous callback clients --- packages/sdk/src/implementation/oauth.ts | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/packages/sdk/src/implementation/oauth.ts b/packages/sdk/src/implementation/oauth.ts index 5e868e8431..c6993edf4c 100644 --- a/packages/sdk/src/implementation/oauth.ts +++ b/packages/sdk/src/implementation/oauth.ts @@ -638,10 +638,8 @@ export const makeOAuth = ( : "manual"; // A client saved at a callback this host sent before is registered with that callback only. // Executor can replace one it registered or read from its metadata document, so those sign - // in again at the current callback. Only a client someone entered, or one for a server that - // offers no other way, keeps sending the old callback, since only its owner can change what - // the provider allows. A record saved before sources were recorded counts as registered - // where the server registers clients. + // in again at the current callback. Only a client someone entered keeps sending the old + // callback, since only its owner can change what the provider allows. const previous = redirect === undefined ? [] @@ -662,12 +660,9 @@ export const makeOAuth = ( if (saved === null) continue; const registered = yield* decrypt(id, saved.encrypted, OAuthRegistration); const metadata = yield* decrypt(id, saved.encrypted, OAuthSavedClientMetadata); - if ( - candidate !== redirect && - metadata.executor_source !== "manual" && - registration !== "manual" - ) - continue; + // Previous callbacks: only reuse clients the operator entered. Anything Executor + // registered (or saved before sources existed) must sign in again at `redirect`. + if (candidate !== redirect && metadata.executor_source !== "manual") continue; if ( registered.client_secret_expires_at !== undefined && registered.client_secret_expires_at !== 0 &&