@agent-native/dispatch 0.29.5 → 0.30.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/actions/create-workspace-resource.d.ts +13 -13
- package/dist/actions/delete-workspace-resource.d.ts +13 -13
- package/dist/actions/provider-api-register.d.ts +8 -8
- package/dist/actions/update-workspace-resource.d.ts +13 -13
- package/dist/components/workspace-app-card.d.ts.map +1 -1
- package/dist/components/workspace-app-card.js +2 -1
- package/dist/components/workspace-app-card.js.map +1 -1
- package/dist/db/index.d.ts.map +1 -1
- package/dist/db/index.js +26 -1
- package/dist/db/index.js.map +1 -1
- package/dist/db/schema.d.ts +1 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +4 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/lib/workspace-apps.d.ts +1 -0
- package/dist/lib/workspace-apps.d.ts.map +1 -1
- package/dist/lib/workspace-apps.js +3 -1
- package/dist/lib/workspace-apps.js.map +1 -1
- package/dist/lib/workspace-apps.test.js +17 -3
- package/dist/lib/workspace-apps.test.js.map +1 -1
- package/dist/server/lib/app-creation-store.d.ts +3 -0
- package/dist/server/lib/app-creation-store.d.ts.map +1 -1
- package/dist/server/lib/app-creation-store.js +206 -11
- package/dist/server/lib/app-creation-store.js.map +1 -1
- package/package.json +3 -3
- package/src/components/dispatch-control-plane.spec.tsx +17 -3
- package/src/components/workspace-app-card.spec.tsx +17 -0
- package/src/components/workspace-app-card.tsx +12 -0
- package/src/components/workspace-app-host.spec.tsx +14 -6
- package/src/db/index.ts +28 -1
- package/src/db/schema.ts +5 -0
- package/src/lib/workspace-apps.test.ts +20 -2
- package/src/lib/workspace-apps.ts +4 -1
- package/src/server/lib/app-creation-store.spec.ts +120 -0
- package/src/server/lib/app-creation-store.ts +264 -41
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-apps.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHAT_FIRST_DEFAULT_APP_IDS } from \"@agent-native/core/client/chat-first\";\nimport { isInBuilderFrame } from \"@agent-native/core/client/host\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\n\nimport {\n CANONICAL_WORKSPACE_SSO_APP_ORIGINS,\n isWorkspaceSsoAppUrl,\n} from \"../shared/workspace-sso\";\n\nexport interface WorkspaceAppSummary {\n id: string;\n name: string;\n description?: string;\n path: string;\n url?: string | null;\n isDispatch?: boolean;\n audience?: \"internal\" | \"public\";\n publicPaths?: string[];\n protectedPaths?: string[];\n status?: \"ready\" | \"pending\";\n statusLabel?: string;\n builderUrl?: string | null;\n branchName?: string | null;\n createdAt?: string | null;\n createdBy?: string | null;\n owner?: string | null;\n teams?: string[];\n agentCardUrl?: string | null;\n agentCardReachable?: boolean;\n a2aEndpointUrl?: string | null;\n agentName?: string | null;\n agentSkillsCount?: number | null;\n archived?: boolean;\n workspaceSso?: boolean;\n}\n\ninterface WorkspaceAppHrefSource {\n path?: string | null;\n url?: string | null;\n workspaceSso?: boolean;\n}\n\nexport function isDispatchWorkspaceAppId(appId: string): boolean {\n return appId.trim().toLowerCase() === \"dispatch\";\n}\n\nfunction clientRuntimeEnvironment(): \"production\" | \"development\" {\n const importMetaEnv = (\n import.meta as unknown as {\n env?: Record<string, string | boolean | undefined>;\n }\n ).env;\n const processEnv = (\n globalThis as typeof globalThis & {\n process?: { env?: Record<string, string | boolean | undefined> };\n }\n ).process?.env;\n const env = { ...processEnv, ...importMetaEnv };\n if (\n env.PROD === true ||\n env.MODE === \"production\" ||\n env.NODE_ENV === \"production\"\n ) {\n return \"production\";\n }\n if (\n env.DEV === true ||\n env.MODE === \"development\" ||\n env.MODE === \"test\" ||\n env.NODE_ENV === \"development\" ||\n env.NODE_ENV === \"test\"\n ) {\n return \"development\";\n }\n return \"production\";\n}\n\n/**\n * The workspace SSO action only accepts an exact registered app identity and\n * origin. The catalog's boolean is a server-derived projection for custom\n * registrations; the URL check keeps malformed metadata out of the action.\n */\nexport function isWorkspaceSsoApp(\n app: WorkspaceAppHrefSource & { id: string },\n): boolean {\n const rawUrl = app.url?.trim();\n if (!rawUrl) return false;\n try {\n const url = new URL(rawUrl);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (app.workspaceSso === true) return true;\n return isWorkspaceSsoAppUrl(\n { id: app.id, url: rawUrl },\n { nodeEnv: clientRuntimeEnvironment() },\n );\n // coercion-ok: malformed app metadata is not eligible for workspace SSO.\n } catch {\n return false;\n }\n}\n\nfunction isCanonicalWorkspaceSsoOrigin(rawUrl: string): boolean {\n try {\n const origin = new URL(rawUrl).origin;\n return Object.values(CANONICAL_WORKSPACE_SSO_APP_ORIGINS).some(\n (canonicalOrigin) => canonicalOrigin === origin,\n );\n } catch {\n // coercion-ok: malformed app metadata is not a canonical first-party app.\n return false;\n }\n}\n\n/**\n * A mounted app URL leaves Dispatch's `/apps/:id` host route. Canonical\n * first-party origins can stay inline even at `/`, while external published\n * apps must open at their own origin regardless of their path.\n */\nexport function isPathMountedWorkspaceApp(\n app: WorkspaceAppHrefSource,\n): boolean {\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const pathname = new URL(rawUrl).pathname.replace(/\\/+$/, \"\") || \"/\";\n return pathname !== \"/\" || !isCanonicalWorkspaceSsoOrigin(rawUrl);\n // coercion-ok: invalid absolute URLs use the mounted path fallback.\n } catch {\n // Fall through to the mounted path for relative manifest values.\n }\n }\n const path = app.path?.trim().replace(/\\/+$/, \"\") || \"/\";\n return path !== \"/\";\n}\n\nexport function isDefaultWorkspaceAppHiddenId(appId: string): boolean {\n const normalized = appId.trim().toLowerCase();\n return normalized === \"chat\" || isDispatchWorkspaceAppId(normalized);\n}\n\nexport function isWorkspaceAppVisibleInDefaultLaunchers(\n app: Pick<WorkspaceAppSummary, \"id\" | \"isDispatch\">,\n): boolean {\n return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);\n}\n\nexport function workspaceAppRoute(appId: string): string {\n return `/apps/${encodeURIComponent(appId)}`;\n}\n\nexport function workspaceAppIdFromRoute(pathname: string): string | null {\n const match = pathname.match(/^\\/apps\\/([^/]+)(?:\\/|$)/);\n if (!match) return null;\n try {\n const appId = decodeURIComponent(match[1]).trim();\n return appId || null;\n } catch {\n // coercion-ok: malformed app routes are inactive, not app ids.\n return null;\n }\n}\n\nexport function workspaceAppHref(app: WorkspaceAppSummary): string | null {\n if (app.status === \"pending\") {\n return app.builderUrl\n ? withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })\n : null;\n }\n return app.path || app.url || null;\n}\n\nexport function workspaceAppEmbedTarget(\n app: Pick<WorkspaceAppSummary, \"path\" | \"url\">,\n): { path?: string; url?: string } {\n const url = app.url?.trim();\n if (url) return { url };\n\n const path = app.path.trim();\n return path.startsWith(\"/\") ? { path } : path ? { url: path } : {};\n}\n\n/**\n * Resolve an app route without an embed ticket so the target can render its\n * own error document when session setup fails.\n */\nexport function workspaceAppDirectHref(\n app: WorkspaceAppHrefSource,\n targetPath: string,\n): string | null {\n const target = targetPath.trim();\n if (!target || !target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return null;\n }\n\n let targetUrl: URL;\n try {\n targetUrl = new URL(target, \"https://agent-native.invalid\");\n } catch {\n // coercion-ok: invalid relative target input has no safe href.\n return null;\n }\n const targetPathname = targetUrl.pathname || \"/\";\n\n let absoluteBase: URL | null = null;\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const parsedBase = new URL(rawUrl);\n if (parsedBase.protocol === \"http:\" || parsedBase.protocol === \"https:\") {\n absoluteBase = parsedBase;\n }\n // coercion-ok: invalid app URLs use the mounted path fallback below.\n } catch {\n absoluteBase = null;\n }\n }\n\n const mountedPath = app.path?.trim();\n const basePath = absoluteBase\n ? absoluteBase.pathname\n : mountedPath\n ? `/${mountedPath.replace(/^[/\\\\]+/, \"\").split(/[?#]/, 1)[0]}`\n : null;\n if (!basePath) return null;\n\n const normalizedBasePath = basePath.replace(/\\/+$/, \"\") || \"/\";\n const targetIsMountedPath =\n normalizedBasePath !== \"/\" &&\n (targetPathname === normalizedBasePath ||\n targetPathname.startsWith(`${normalizedBasePath}/`));\n const resolvedPath = targetIsMountedPath\n ? targetPathname\n : normalizedBasePath === \"/\"\n ? targetPathname\n : targetPathname === \"/\"\n ? normalizedBasePath\n : `${normalizedBasePath}/${targetPathname.replace(/^\\/+/, \"\")}`;\n\n if (absoluteBase) {\n absoluteBase.pathname = resolvedPath;\n absoluteBase.search = targetUrl.search;\n absoluteBase.hash = targetUrl.hash;\n return absoluteBase.toString();\n }\n\n return `${resolvedPath}${targetUrl.search}${targetUrl.hash}`;\n}\n\nexport function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {\n return app.status === \"pending\" && !!app.builderUrl;\n}\n\nexport function shouldOpenWorkspaceAppInTopWindow(): boolean {\n if (typeof window === \"undefined\") return false;\n return isInBuilderFrame() || window.parent !== window;\n}\n\nexport function navigateToWorkspaceApp(href: string): boolean {\n if (typeof window === \"undefined\") return false;\n\n try {\n const targetUrl = new URL(href, window.location.href);\n if (targetUrl.protocol !== \"http:\" && targetUrl.protocol !== \"https:\") {\n return false;\n }\n const targetWindow =\n shouldOpenWorkspaceAppInTopWindow() && window.top ? window.top : window;\n targetWindow.location.href = targetUrl.href;\n return true;\n } catch {\n // coercion-ok: a blocked top-window assignment is an expected fallback signal.\n return false;\n }\n}\n\n/**\n * Keep the chat-first rail useful before a workspace manifest is populated.\n * Mounted workspace rows still win, so custom names and routes remain the\n * source of truth once an app exists in the workspace.\n */\nexport function mergeChatFirstWorkspaceApps(\n apps: readonly WorkspaceAppSummary[] | undefined,\n): WorkspaceAppSummary[] {\n const merged = new Map<string, WorkspaceAppSummary>();\n for (const id of CHAT_FIRST_DEFAULT_APP_IDS) {\n merged.set(id, {\n id,\n name: id.charAt(0).toUpperCase() + id.slice(1),\n // The five default rows are hosted sibling apps, not routes owned by\n // Dispatch. Keep a mounted path for legacy callers, but give embed\n // session resolution the exact canonical origin.\n path: \"/\",\n url: CANONICAL_WORKSPACE_SSO_APP_ORIGINS[id],\n status: \"ready\",\n });\n }\n for (const app of apps ?? []) merged.set(app.id, app);\n\n return [...merged.values()];\n}\n"]}
|
|
1
|
+
{"version":3,"file":"workspace-apps.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.ts"],"names":[],"mappings":"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","sourcesContent":["import { CHAT_FIRST_DEFAULT_APP_IDS } from \"@agent-native/core/client/chat-first\";\nimport { isInBuilderFrame } from \"@agent-native/core/client/host\";\nimport { withBuilderUtmTrackingParams } from \"@agent-native/core/shared/builder-link-tracking\";\n\nimport {\n CANONICAL_WORKSPACE_SSO_APP_ORIGINS,\n isWorkspaceSsoAppUrl,\n} from \"../shared/workspace-sso\";\n\nexport interface WorkspaceAppSummary {\n id: string;\n name: string;\n description?: string;\n path: string;\n url?: string | null;\n isDispatch?: boolean;\n audience?: \"internal\" | \"public\";\n visibility?: \"private\" | \"org\";\n publicPaths?: string[];\n protectedPaths?: string[];\n status?: \"ready\" | \"pending\";\n statusLabel?: string;\n builderUrl?: string | null;\n branchName?: string | null;\n createdAt?: string | null;\n createdBy?: string | null;\n owner?: string | null;\n teams?: string[];\n agentCardUrl?: string | null;\n agentCardReachable?: boolean;\n a2aEndpointUrl?: string | null;\n agentName?: string | null;\n agentSkillsCount?: number | null;\n archived?: boolean;\n workspaceSso?: boolean;\n}\n\ninterface WorkspaceAppHrefSource {\n path?: string | null;\n url?: string | null;\n workspaceSso?: boolean;\n}\n\nexport function isDispatchWorkspaceAppId(appId: string): boolean {\n return appId.trim().toLowerCase() === \"dispatch\";\n}\n\nfunction clientRuntimeEnvironment(): \"production\" | \"development\" {\n const importMetaEnv = (\n import.meta as unknown as {\n env?: Record<string, string | boolean | undefined>;\n }\n ).env;\n const processEnv = (\n globalThis as typeof globalThis & {\n process?: { env?: Record<string, string | boolean | undefined> };\n }\n ).process?.env;\n const env = { ...processEnv, ...importMetaEnv };\n if (\n env.PROD === true ||\n env.MODE === \"production\" ||\n env.NODE_ENV === \"production\"\n ) {\n return \"production\";\n }\n if (\n env.DEV === true ||\n env.MODE === \"development\" ||\n env.MODE === \"test\" ||\n env.NODE_ENV === \"development\" ||\n env.NODE_ENV === \"test\"\n ) {\n return \"development\";\n }\n return \"production\";\n}\n\n/**\n * The workspace SSO action only accepts an exact registered app identity and\n * origin. The catalog's boolean is a server-derived projection for custom\n * registrations; the URL check keeps malformed metadata out of the action.\n */\nexport function isWorkspaceSsoApp(\n app: WorkspaceAppHrefSource & { id: string },\n): boolean {\n const rawUrl = app.url?.trim();\n if (!rawUrl) return false;\n try {\n const url = new URL(rawUrl);\n if (url.protocol !== \"http:\" && url.protocol !== \"https:\") return false;\n if (app.workspaceSso === true) return true;\n return isWorkspaceSsoAppUrl(\n { id: app.id, url: rawUrl },\n { nodeEnv: clientRuntimeEnvironment() },\n );\n // coercion-ok: malformed app metadata is not eligible for workspace SSO.\n } catch {\n return false;\n }\n}\n\nfunction isCanonicalWorkspaceSsoOrigin(rawUrl: string): boolean {\n try {\n const origin = new URL(rawUrl).origin;\n return Object.values(CANONICAL_WORKSPACE_SSO_APP_ORIGINS).some(\n (canonicalOrigin) => canonicalOrigin === origin,\n );\n } catch {\n // coercion-ok: malformed app metadata is not a canonical first-party app.\n return false;\n }\n}\n\n/**\n * A mounted app URL leaves Dispatch's `/apps/:id` host route. Canonical\n * first-party origins can stay inline even at `/`, while external published\n * apps must open at their own origin regardless of their path.\n */\nexport function isPathMountedWorkspaceApp(\n app: WorkspaceAppHrefSource,\n): boolean {\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const pathname = new URL(rawUrl).pathname.replace(/\\/+$/, \"\") || \"/\";\n return pathname !== \"/\" || !isCanonicalWorkspaceSsoOrigin(rawUrl);\n // coercion-ok: invalid absolute URLs use the mounted path fallback.\n } catch {\n // Fall through to the mounted path for relative manifest values.\n }\n }\n const path = app.path?.trim().replace(/\\/+$/, \"\") || \"/\";\n return path !== \"/\";\n}\n\nexport function isDefaultWorkspaceAppHiddenId(appId: string): boolean {\n const normalized = appId.trim().toLowerCase();\n return normalized === \"chat\" || isDispatchWorkspaceAppId(normalized);\n}\n\nexport function isWorkspaceAppVisibleInDefaultLaunchers(\n app: Pick<WorkspaceAppSummary, \"id\" | \"isDispatch\">,\n): boolean {\n return !app.isDispatch && !isDefaultWorkspaceAppHiddenId(app.id);\n}\n\nexport function workspaceAppRoute(appId: string): string {\n return `/apps/${encodeURIComponent(appId)}`;\n}\n\nexport function workspaceAppIdFromRoute(pathname: string): string | null {\n const match = pathname.match(/^\\/apps\\/([^/]+)(?:\\/|$)/);\n if (!match) return null;\n try {\n const appId = decodeURIComponent(match[1]).trim();\n return appId || null;\n } catch {\n // coercion-ok: malformed app routes are inactive, not app ids.\n return null;\n }\n}\n\nexport function workspaceAppHref(app: WorkspaceAppSummary): string | null {\n if (app.status === \"pending\") {\n return app.builderUrl\n ? withBuilderUtmTrackingParams(app.builderUrl, {\n campaign: \"product\",\n content: \"dispatch_branch\",\n })\n : null;\n }\n return app.path || app.url || null;\n}\n\nexport function workspaceAppEmbedTarget(\n app: Pick<WorkspaceAppSummary, \"path\" | \"url\">,\n): { path?: string; url?: string } {\n const url = app.url?.trim();\n if (url) return { url };\n\n const path = app.path.trim();\n return path.startsWith(\"/\") ? { path } : path ? { url: path } : {};\n}\n\n/**\n * Resolve an app route without an embed ticket so the target can render its\n * own error document when session setup fails.\n */\nexport function workspaceAppDirectHref(\n app: WorkspaceAppHrefSource,\n targetPath: string,\n): string | null {\n const target = targetPath.trim();\n if (!target || !target.startsWith(\"/\") || target.startsWith(\"//\")) {\n return null;\n }\n\n let targetUrl: URL;\n try {\n targetUrl = new URL(target, \"https://agent-native.invalid\");\n } catch {\n // coercion-ok: invalid relative target input has no safe href.\n return null;\n }\n const targetPathname = targetUrl.pathname || \"/\";\n\n let absoluteBase: URL | null = null;\n const rawUrl = app.url?.trim();\n if (rawUrl) {\n try {\n const parsedBase = new URL(rawUrl);\n if (parsedBase.protocol === \"http:\" || parsedBase.protocol === \"https:\") {\n absoluteBase = parsedBase;\n }\n // coercion-ok: invalid app URLs use the mounted path fallback below.\n } catch {\n absoluteBase = null;\n }\n }\n\n const mountedPath = app.path?.trim();\n const basePath = absoluteBase\n ? absoluteBase.pathname\n : mountedPath\n ? `/${mountedPath.replace(/^[/\\\\]+/, \"\").split(/[?#]/, 1)[0]}`\n : null;\n if (!basePath) return null;\n\n const normalizedBasePath = basePath.replace(/\\/+$/, \"\") || \"/\";\n const targetIsMountedPath =\n normalizedBasePath !== \"/\" &&\n (targetPathname === normalizedBasePath ||\n targetPathname.startsWith(`${normalizedBasePath}/`));\n const resolvedPath = targetIsMountedPath\n ? targetPathname\n : normalizedBasePath === \"/\"\n ? targetPathname\n : targetPathname === \"/\"\n ? normalizedBasePath\n : `${normalizedBasePath}/${targetPathname.replace(/^\\/+/, \"\")}`;\n\n if (absoluteBase) {\n absoluteBase.pathname = resolvedPath;\n absoluteBase.search = targetUrl.search;\n absoluteBase.hash = targetUrl.hash;\n return absoluteBase.toString();\n }\n\n return `${resolvedPath}${targetUrl.search}${targetUrl.hash}`;\n}\n\nexport function isPendingBuilderHref(app: WorkspaceAppSummary): boolean {\n return app.status === \"pending\" && !!app.builderUrl;\n}\n\nexport function shouldOpenWorkspaceAppInTopWindow(): boolean {\n if (typeof window === \"undefined\") return false;\n // A generic iframe is an inline host by design. Only Builder owns the\n // parent navigation contract for workspace apps.\n return isInBuilderFrame();\n}\n\nexport function navigateToWorkspaceApp(href: string): boolean {\n if (typeof window === \"undefined\") return false;\n\n try {\n const targetUrl = new URL(href, window.location.href);\n if (targetUrl.protocol !== \"http:\" && targetUrl.protocol !== \"https:\") {\n return false;\n }\n const targetWindow =\n shouldOpenWorkspaceAppInTopWindow() && window.top ? window.top : window;\n targetWindow.location.href = targetUrl.href;\n return true;\n } catch {\n // coercion-ok: a blocked top-window assignment is an expected fallback signal.\n return false;\n }\n}\n\n/**\n * Keep the chat-first rail useful before a workspace manifest is populated.\n * Mounted workspace rows still win, so custom names and routes remain the\n * source of truth once an app exists in the workspace.\n */\nexport function mergeChatFirstWorkspaceApps(\n apps: readonly WorkspaceAppSummary[] | undefined,\n): WorkspaceAppSummary[] {\n const merged = new Map<string, WorkspaceAppSummary>();\n for (const id of CHAT_FIRST_DEFAULT_APP_IDS) {\n merged.set(id, {\n id,\n name: id.charAt(0).toUpperCase() + id.slice(1),\n // The five default rows are hosted sibling apps, not routes owned by\n // Dispatch. Keep a mounted path for legacy callers, but give embed\n // session resolution the exact canonical origin.\n path: \"/\",\n url: CANONICAL_WORKSPACE_SSO_APP_ORIGINS[id],\n status: \"ready\",\n });\n }\n for (const app of apps ?? []) merged.set(app.id, app);\n\n return [...merged.values()];\n}\n"]}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// @vitest-environment happy-dom
|
|
2
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
-
import { navigateToWorkspaceApp, workspaceAppDirectHref, workspaceAppEmbedTarget, } from "./workspace-apps.js";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
3
|
+
import { navigateToWorkspaceApp, shouldOpenWorkspaceAppInTopWindow, workspaceAppDirectHref, workspaceAppEmbedTarget, } from "./workspace-apps.js";
|
|
4
4
|
describe("workspaceAppEmbedTarget", () => {
|
|
5
5
|
it("uses the app URL as the embed root when a mount path is also present", () => {
|
|
6
6
|
expect(workspaceAppEmbedTarget({
|
|
@@ -32,7 +32,11 @@ describe("workspaceAppDirectHref", () => {
|
|
|
32
32
|
});
|
|
33
33
|
});
|
|
34
34
|
describe("navigateToWorkspaceApp", () => {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
window.history.replaceState({}, "", "/");
|
|
37
|
+
});
|
|
35
38
|
afterEach(() => {
|
|
39
|
+
window.history.replaceState({}, "", "/");
|
|
36
40
|
Object.defineProperty(window, "parent", {
|
|
37
41
|
configurable: true,
|
|
38
42
|
value: window,
|
|
@@ -42,8 +46,16 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
42
46
|
value: window,
|
|
43
47
|
});
|
|
44
48
|
});
|
|
45
|
-
it("
|
|
49
|
+
it("keeps a normal iframe inline", () => {
|
|
50
|
+
Object.defineProperty(window, "parent", {
|
|
51
|
+
configurable: true,
|
|
52
|
+
value: {},
|
|
53
|
+
});
|
|
54
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);
|
|
55
|
+
});
|
|
56
|
+
it("uses the top window when Dispatch is inside a Builder webview", () => {
|
|
46
57
|
const topWindow = { location: { href: "" } };
|
|
58
|
+
window.history.replaceState({}, "", "/?builder.preview=interact");
|
|
47
59
|
Object.defineProperty(window, "parent", {
|
|
48
60
|
configurable: true,
|
|
49
61
|
value: {},
|
|
@@ -52,6 +64,7 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
52
64
|
configurable: true,
|
|
53
65
|
value: topWindow,
|
|
54
66
|
});
|
|
67
|
+
expect(shouldOpenWorkspaceAppInTopWindow()).toBe(true);
|
|
55
68
|
navigateToWorkspaceApp("/mail");
|
|
56
69
|
expect(topWindow.location.href).toBe(new URL("/mail", window.location.href).href);
|
|
57
70
|
});
|
|
@@ -64,6 +77,7 @@ describe("navigateToWorkspaceApp", () => {
|
|
|
64
77
|
},
|
|
65
78
|
});
|
|
66
79
|
const topWindow = { location };
|
|
80
|
+
window.history.replaceState({}, "", "/?builder.preview=interact");
|
|
67
81
|
Object.defineProperty(window, "parent", {
|
|
68
82
|
configurable: true,
|
|
69
83
|
value: {},
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"workspace-apps.test.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.test.ts"],"names":[],"mappings":"AAAA,gCAAgC;AAChC,OAAO,EAAE,SAAS,EAAE,QAAQ,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAC;
|
|
1
|
+
{"version":3,"file":"workspace-apps.test.js","sourceRoot":"","sources":["../../src/lib/workspace-apps.test.ts"],"names":[],"mappings":"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","sourcesContent":["// @vitest-environment happy-dom\nimport { afterEach, beforeEach, describe, expect, it } from \"vitest\";\n\nimport {\n navigateToWorkspaceApp,\n shouldOpenWorkspaceAppInTopWindow,\n workspaceAppDirectHref,\n workspaceAppEmbedTarget,\n} from \"./workspace-apps\";\n\ndescribe(\"workspaceAppEmbedTarget\", () => {\n it(\"uses the app URL as the embed root when a mount path is also present\", () => {\n expect(\n workspaceAppEmbedTarget({\n path: \"/content\",\n url: \"https://workspace.example.test/content\",\n }),\n ).toEqual({ url: \"https://workspace.example.test/content\" });\n });\n\n it(\"falls back to the mounted path when no app URL is available\", () => {\n expect(workspaceAppEmbedTarget({ path: \"/content\", url: null })).toEqual({\n path: \"/content\",\n });\n });\n});\n\ndescribe(\"workspaceAppDirectHref\", () => {\n it(\"joins an app-relative route onto an absolute app URL\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/emails?status=failed#latest\",\n ),\n ).toBe(\"https://workspace.example.test/atlas/emails?status=failed#latest\");\n });\n\n it(\"does not duplicate a mount path already present in the target\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"https://workspace.example.test/atlas\" },\n \"/atlas\",\n ),\n ).toBe(\"https://workspace.example.test/atlas\");\n });\n\n it(\"resolves a mounted relative app path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"/emails\")).toBe(\n \"/atlas/emails\",\n );\n });\n\n it(\"falls back to a safe mount path when the app URL is invalid\", () => {\n expect(\n workspaceAppDirectHref(\n { path: \"/atlas\", url: \"javascript:alert(1)\" },\n \"/\",\n ),\n ).toBe(\"/atlas\");\n });\n\n it(\"rejects an unsafe target path\", () => {\n expect(workspaceAppDirectHref({ path: \"/atlas\" }, \"//evil.example\")).toBe(\n null,\n );\n });\n});\n\ndescribe(\"navigateToWorkspaceApp\", () => {\n beforeEach(() => {\n window.history.replaceState({}, \"\", \"/\");\n });\n\n afterEach(() => {\n window.history.replaceState({}, \"\", \"/\");\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: window,\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: window,\n });\n });\n\n it(\"keeps a normal iframe inline\", () => {\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n\n expect(shouldOpenWorkspaceAppInTopWindow()).toBe(false);\n });\n\n it(\"uses the top window when Dispatch is inside a Builder webview\", () => {\n const topWindow = { location: { href: \"\" } } as unknown as Window;\n window.history.replaceState({}, \"\", \"/?builder.preview=interact\");\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n expect(shouldOpenWorkspaceAppInTopWindow()).toBe(true);\n navigateToWorkspaceApp(\"/mail\");\n\n expect(topWindow.location.href).toBe(\n new URL(\"/mail\", window.location.href).href,\n );\n });\n\n it(\"reports when the embedded top window rejects navigation\", () => {\n const location = {} as Location;\n Object.defineProperty(location, \"href\", {\n configurable: true,\n set: () => {\n throw new Error(\"Top navigation is blocked\");\n },\n });\n const topWindow = { location } as unknown as Window;\n window.history.replaceState({}, \"\", \"/?builder.preview=interact\");\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n expect(navigateToWorkspaceApp(\"/mail\")).toBe(false);\n });\n\n it(\"rejects non-http navigation targets\", () => {\n const topWindow = { location: { href: \"\" } } as unknown as Window;\n Object.defineProperty(window, \"parent\", {\n configurable: true,\n value: {},\n });\n Object.defineProperty(window, \"top\", {\n configurable: true,\n value: topWindow,\n });\n\n expect(navigateToWorkspaceApp(\"javascript:alert(1)\")).toBe(false);\n expect(topWindow.location.href).toBe(\"\");\n });\n});\n"]}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import "../../db/index.js";
|
|
1
2
|
type WorkspaceAppAudience = "internal" | "public";
|
|
3
|
+
type WorkspaceAppVisibility = "private" | "org";
|
|
2
4
|
export interface WorkspaceAppSummary {
|
|
3
5
|
id: string;
|
|
4
6
|
name: string;
|
|
@@ -15,6 +17,7 @@ export interface WorkspaceAppSummary {
|
|
|
15
17
|
branchName?: string | null;
|
|
16
18
|
createdAt?: string | null;
|
|
17
19
|
createdBy?: string | null;
|
|
20
|
+
visibility?: WorkspaceAppVisibility;
|
|
18
21
|
owner?: string | null;
|
|
19
22
|
teams?: string[];
|
|
20
23
|
agentCardUrl?: string | null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"app-creation-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/app-creation-store.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"app-creation-store.d.ts","sourceRoot":"","sources":["../../../src/server/lib/app-creation-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,8 +6,12 @@ import { signA2AToken } from "@agent-native/core/a2a";
|
|
|
6
6
|
import { getDbExec } from "@agent-native/core/db";
|
|
7
7
|
import { getOrgA2ASecret, getOrgDomain } from "@agent-native/core/org";
|
|
8
8
|
import { ensureBuilderProject, getBuilderBranchProjectId, getRequestContext, isIntegrationCallerRequest, resolveBuilderCredentialsDetailed, runBuilderAgent, } from "@agent-native/core/server";
|
|
9
|
+
import { getOrgSetting } from "@agent-native/core/settings";
|
|
9
10
|
import { getSetting, putSetting } from "@agent-native/core/settings";
|
|
10
11
|
import { assertValidWorkspaceAppId } from "@agent-native/core/shared";
|
|
12
|
+
import { resolveAccess } from "@agent-native/core/sharing";
|
|
13
|
+
// Register the workspace-app shareable resource before any access lookup.
|
|
14
|
+
import "../../db/index.js";
|
|
11
15
|
import { isWorkspaceSsoAppUrl } from "../../shared/workspace-sso.js";
|
|
12
16
|
import { identityKeyForIncoming } from "./dispatch-integrations.js";
|
|
13
17
|
import { currentOrgId, currentOwnerEmail, recordAudit, resolveLinkedOwner, } from "./dispatch-store.js";
|
|
@@ -21,10 +25,12 @@ const DEFAULT_BUILDER_WORKSPACE_PROJECT_NAME = "Agent-Native Workspace";
|
|
|
21
25
|
const APP_CREATION_SETTINGS_AUTHORIZATION_MESSAGE = "Only organization owners and admins can update app creation settings.";
|
|
22
26
|
const APP_CREATION_SETTINGS_REQUIRED_MESSAGE = "An organization owner or admin must configure the Builder workspace project before members can create apps.";
|
|
23
27
|
const WORKSPACE_APP_METADATA_SETTINGS_KEY = "workspace-app-metadata";
|
|
28
|
+
const WORKSPACE_APP_DEFAULT_VISIBILITY_KEY = "workspace-app-default-visibility";
|
|
24
29
|
const WORKSPACE_APPS_ENV_KEY = "AGENT_NATIVE_WORKSPACE_APPS_JSON";
|
|
25
30
|
const WORKSPACE_APPS_MANIFEST_FILE = "workspace-apps.json";
|
|
26
31
|
const WORKSPACE_APPS_GATEWAY_PATH = "/_workspace/apps";
|
|
27
32
|
const WORKSPACE_APPS_GATEWAY_TIMEOUT_MS = 2_500;
|
|
33
|
+
const WORKSPACE_APP_ACCESS_CONCURRENCY = 8;
|
|
28
34
|
const MAX_PENDING_APPS = 50;
|
|
29
35
|
const PENDING_WORKSPACE_APP_TTL_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
30
36
|
const AGENT_CARD_PATH = "/.well-known/agent-card.json";
|
|
@@ -184,6 +190,7 @@ function parseWorkspaceAppMetadataSettings(raw) {
|
|
|
184
190
|
const sourcePrompt = cleanOptionalText(item.sourcePrompt);
|
|
185
191
|
const updatedAt = cleanOptionalText(item.updatedAt);
|
|
186
192
|
const updatedBy = cleanOptionalText(item.updatedBy);
|
|
193
|
+
const createdBy = cleanOptionalText(item.createdBy);
|
|
187
194
|
if (name)
|
|
188
195
|
override.name = name;
|
|
189
196
|
if (description)
|
|
@@ -196,6 +203,11 @@ function parseWorkspaceAppMetadataSettings(raw) {
|
|
|
196
203
|
override.updatedAt = updatedAt;
|
|
197
204
|
if (updatedBy)
|
|
198
205
|
override.updatedBy = updatedBy;
|
|
206
|
+
if (createdBy)
|
|
207
|
+
override.createdBy = createdBy;
|
|
208
|
+
if (item.visibility === "private" || item.visibility === "org") {
|
|
209
|
+
override.visibility = item.visibility;
|
|
210
|
+
}
|
|
199
211
|
if (Object.keys(override).length > 0)
|
|
200
212
|
apps[id.trim()] = override;
|
|
201
213
|
}
|
|
@@ -218,6 +230,7 @@ async function writeWorkspaceAppMetadataOverride(input) {
|
|
|
218
230
|
const description = cleanOptionalText(input.description);
|
|
219
231
|
const sourcePrompt = cleanOptionalText(input.sourcePrompt);
|
|
220
232
|
const updatedBy = cleanOptionalText(input.updatedBy);
|
|
233
|
+
const createdBy = cleanOptionalText(input.createdBy);
|
|
221
234
|
if (name)
|
|
222
235
|
next.name = name;
|
|
223
236
|
else
|
|
@@ -234,6 +247,10 @@ async function writeWorkspaceAppMetadataOverride(input) {
|
|
|
234
247
|
next.sourcePrompt = sourcePrompt;
|
|
235
248
|
if (updatedBy)
|
|
236
249
|
next.updatedBy = updatedBy;
|
|
250
|
+
if (createdBy)
|
|
251
|
+
next.createdBy = createdBy;
|
|
252
|
+
if (input.visibility)
|
|
253
|
+
next.visibility = input.visibility;
|
|
237
254
|
current.apps[appId] = next;
|
|
238
255
|
await putSetting(key, { apps: current.apps });
|
|
239
256
|
return current;
|
|
@@ -247,17 +264,23 @@ function applyWorkspaceAppMetadataOverride(app, settings) {
|
|
|
247
264
|
const generated = override.generated === true;
|
|
248
265
|
const shouldApplyName = !!name && !generated;
|
|
249
266
|
const shouldApplyDescription = !!description && (!generated || !cleanOptionalText(app.description));
|
|
250
|
-
|
|
267
|
+
const visibility = override.visibility;
|
|
268
|
+
if (!shouldApplyName && !shouldApplyDescription && !visibility)
|
|
251
269
|
return app;
|
|
252
270
|
return {
|
|
253
271
|
...app,
|
|
254
272
|
...(shouldApplyName ? { name } : {}),
|
|
255
273
|
...(shouldApplyDescription ? { description } : {}),
|
|
256
|
-
...(
|
|
257
|
-
|
|
274
|
+
...(visibility ? { visibility } : {}),
|
|
275
|
+
...(app.status === "pending" &&
|
|
276
|
+
!app.createdBy &&
|
|
277
|
+
(override.createdBy || override.updatedBy)
|
|
278
|
+
? { createdBy: override.createdBy ?? override.updatedBy }
|
|
258
279
|
: {}),
|
|
259
|
-
...(app.status === "pending" &&
|
|
260
|
-
|
|
280
|
+
...(app.status === "pending" &&
|
|
281
|
+
!app.owner &&
|
|
282
|
+
(override.createdBy || override.updatedBy)
|
|
283
|
+
? { owner: override.createdBy ?? override.updatedBy }
|
|
261
284
|
: {}),
|
|
262
285
|
};
|
|
263
286
|
}
|
|
@@ -726,6 +749,156 @@ async function maybeIncludeAgentCards(apps, options) {
|
|
|
726
749
|
return { ...app, ...metadata };
|
|
727
750
|
}));
|
|
728
751
|
}
|
|
752
|
+
async function workspaceAppDefaultVisibility() {
|
|
753
|
+
const orgId = currentOrgId();
|
|
754
|
+
if (!orgId)
|
|
755
|
+
return "org";
|
|
756
|
+
const setting = await getOrgSetting(orgId, WORKSPACE_APP_DEFAULT_VISIBILITY_KEY);
|
|
757
|
+
if (setting === null)
|
|
758
|
+
return "org";
|
|
759
|
+
if (setting.visibility === "private" || setting.visibility === "org") {
|
|
760
|
+
return setting.visibility;
|
|
761
|
+
}
|
|
762
|
+
throw new Error("Workspace app default visibility is invalid; refusing to widen access.");
|
|
763
|
+
}
|
|
764
|
+
function appRecordTimestamp(value) {
|
|
765
|
+
const parsed = value ? Date.parse(value) : NaN;
|
|
766
|
+
return Number.isFinite(parsed) ? parsed : Date.now();
|
|
767
|
+
}
|
|
768
|
+
/**
|
|
769
|
+
* Persist creator and visibility separately from the deploy manifest. The
|
|
770
|
+
* manifest describes code; this SQL record is the access-control source of
|
|
771
|
+
* truth shared by Dispatch and the mounted app's auth guard.
|
|
772
|
+
*/
|
|
773
|
+
async function ensureWorkspaceAppRecords(apps) {
|
|
774
|
+
const readyApps = apps.filter((app) => app.status !== "pending" && !app.isDispatch);
|
|
775
|
+
if (readyApps.length === 0)
|
|
776
|
+
return apps;
|
|
777
|
+
const orgId = currentOrgId();
|
|
778
|
+
const metadata = await readWorkspaceAppMetadataSettings();
|
|
779
|
+
const defaultVisibility = await workspaceAppDefaultVisibility();
|
|
780
|
+
const db = getDbExec();
|
|
781
|
+
const records = new Map();
|
|
782
|
+
try {
|
|
783
|
+
const existingRecords = new Map();
|
|
784
|
+
for (let start = 0; start < readyApps.length; start += 500) {
|
|
785
|
+
const ids = readyApps.slice(start, start + 500).map((app) => app.id);
|
|
786
|
+
const result = await db.execute({
|
|
787
|
+
sql: `SELECT id, owner_email, org_id, visibility
|
|
788
|
+
FROM workspace_apps
|
|
789
|
+
WHERE id IN (${ids.map(() => "?").join(", ")})`,
|
|
790
|
+
args: ids,
|
|
791
|
+
});
|
|
792
|
+
for (const row of result.rows) {
|
|
793
|
+
const id = cleanOptionalText(row.id);
|
|
794
|
+
if (id) {
|
|
795
|
+
existingRecords.set(id, row);
|
|
796
|
+
}
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
for (const app of readyApps) {
|
|
800
|
+
const existing = existingRecords.get(app.id);
|
|
801
|
+
if (!existing) {
|
|
802
|
+
const override = metadata.apps[app.id];
|
|
803
|
+
// Never infer ownership from the person who happened to list apps.
|
|
804
|
+
// Legacy manifests without trusted creation metadata remain
|
|
805
|
+
// ownerless until an admin-controlled migration/claim flow exists.
|
|
806
|
+
const ownerEmail = cleanOptionalText(override?.createdBy) ??
|
|
807
|
+
cleanOptionalText(app.createdBy) ??
|
|
808
|
+
"";
|
|
809
|
+
const hasTrustedCreationMetadata = Boolean(ownerEmail);
|
|
810
|
+
const visibility = override?.visibility === "private"
|
|
811
|
+
? "private"
|
|
812
|
+
: override?.visibility === "org"
|
|
813
|
+
? "org"
|
|
814
|
+
: hasTrustedCreationMetadata
|
|
815
|
+
? defaultVisibility
|
|
816
|
+
: "org";
|
|
817
|
+
const createdAt = appRecordTimestamp(app.createdAt);
|
|
818
|
+
await db.execute({
|
|
819
|
+
sql: `INSERT INTO workspace_apps
|
|
820
|
+
(id, owner_email, org_id, visibility, name, description, path, created_at, updated_at)
|
|
821
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
822
|
+
args: [
|
|
823
|
+
app.id,
|
|
824
|
+
ownerEmail,
|
|
825
|
+
orgId,
|
|
826
|
+
visibility,
|
|
827
|
+
app.name,
|
|
828
|
+
app.description || null,
|
|
829
|
+
app.path,
|
|
830
|
+
createdAt,
|
|
831
|
+
Date.now(),
|
|
832
|
+
],
|
|
833
|
+
});
|
|
834
|
+
records.set(app.id, { ownerEmail, orgId, visibility });
|
|
835
|
+
}
|
|
836
|
+
else {
|
|
837
|
+
records.set(app.id, {
|
|
838
|
+
ownerEmail: typeof existing.owner_email === "string"
|
|
839
|
+
? existing.owner_email
|
|
840
|
+
: "",
|
|
841
|
+
orgId: (typeof existing.org_id === "string"
|
|
842
|
+
? existing.org_id
|
|
843
|
+
: "").trim() || null,
|
|
844
|
+
visibility: existing.visibility === "private" ? "private" : "org",
|
|
845
|
+
});
|
|
846
|
+
}
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
catch (error) {
|
|
850
|
+
console.warn("[dispatch] workspace app access records unavailable", error);
|
|
851
|
+
return apps;
|
|
852
|
+
}
|
|
853
|
+
return apps.map((app) => {
|
|
854
|
+
const record = records.get(app.id);
|
|
855
|
+
return record ? { ...app, visibility: record.visibility } : app;
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
async function filterWorkspaceAppsByAccess(apps) {
|
|
859
|
+
let userEmail;
|
|
860
|
+
try {
|
|
861
|
+
userEmail = currentOwnerEmail();
|
|
862
|
+
}
|
|
863
|
+
catch {
|
|
864
|
+
// coercion-ok: anonymous requests fail closed by receiving no app metadata.
|
|
865
|
+
// App metadata is access-controlled. An anonymous request must not receive
|
|
866
|
+
// the full registry simply because there is no caller to resolve.
|
|
867
|
+
return [];
|
|
868
|
+
}
|
|
869
|
+
const orgId = currentOrgId() ?? undefined;
|
|
870
|
+
const visibleIds = new Set();
|
|
871
|
+
const candidates = [];
|
|
872
|
+
for (const app of apps) {
|
|
873
|
+
if (app.status === "pending" || app.isDispatch) {
|
|
874
|
+
visibleIds.add(app.id);
|
|
875
|
+
continue;
|
|
876
|
+
}
|
|
877
|
+
candidates.push(app);
|
|
878
|
+
}
|
|
879
|
+
for (let start = 0; start < candidates.length; start += WORKSPACE_APP_ACCESS_CONCURRENCY) {
|
|
880
|
+
const batch = candidates.slice(start, start + WORKSPACE_APP_ACCESS_CONCURRENCY);
|
|
881
|
+
const decisions = await Promise.all(batch.map(async (app) => {
|
|
882
|
+
try {
|
|
883
|
+
const access = await resolveAccess("workspace-app", app.id, { userEmail, orgId }, { skipResourceBody: true });
|
|
884
|
+
return { app, allowed: Boolean(access) };
|
|
885
|
+
}
|
|
886
|
+
catch (error) {
|
|
887
|
+
// Rolling deployments may discover an app before the additive access
|
|
888
|
+
// migrations have run. A missing schema is not an authorization
|
|
889
|
+
// result: returning the candidate would expose private metadata
|
|
890
|
+
// during a migration failure.
|
|
891
|
+
console.warn("[dispatch] workspace app access lookup failed", error);
|
|
892
|
+
return { app, allowed: false };
|
|
893
|
+
}
|
|
894
|
+
}));
|
|
895
|
+
for (const decision of decisions) {
|
|
896
|
+
if (decision.allowed)
|
|
897
|
+
visibleIds.add(decision.app.id);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
return apps.filter((app) => visibleIds.has(app.id));
|
|
901
|
+
}
|
|
729
902
|
async function recordPendingWorkspaceApp(input) {
|
|
730
903
|
const now = new Date().toISOString();
|
|
731
904
|
const context = pendingWorkspaceAppContext();
|
|
@@ -767,6 +940,7 @@ async function recordPendingWorkspaceApp(input) {
|
|
|
767
940
|
generated: true,
|
|
768
941
|
sourcePrompt: input.sourcePrompt,
|
|
769
942
|
updatedBy: currentOwnerEmail(),
|
|
943
|
+
createdBy: currentOwnerEmail(),
|
|
770
944
|
});
|
|
771
945
|
await recordAudit({
|
|
772
946
|
action: "workspace-app.pending",
|
|
@@ -1069,23 +1243,29 @@ export async function updateWorkspaceAppMetadata(input) {
|
|
|
1069
1243
|
return updated ?? { ...app, name, description };
|
|
1070
1244
|
}
|
|
1071
1245
|
export async function listWorkspaceApps(options = {}) {
|
|
1246
|
+
const finalize = async (apps) => {
|
|
1247
|
+
const annotated = await applyArchivedAndPending(apps, options);
|
|
1248
|
+
const recorded = await ensureWorkspaceAppRecords(annotated);
|
|
1249
|
+
const visible = await filterWorkspaceAppsByAccess(recorded);
|
|
1250
|
+
return maybeIncludeAgentCards(visible, options);
|
|
1251
|
+
};
|
|
1072
1252
|
const gatewayApps = await readWorkspaceAppsFromGateway();
|
|
1073
1253
|
if (gatewayApps) {
|
|
1074
|
-
return
|
|
1254
|
+
return finalize(gatewayApps);
|
|
1075
1255
|
}
|
|
1076
1256
|
const workspaceRoot = findWorkspaceRoot();
|
|
1077
1257
|
const localFilesystemApps = workspaceRoot && isLocalAppCreationRuntime()
|
|
1078
1258
|
? readWorkspaceAppsFromFilesystem(workspaceRoot)
|
|
1079
1259
|
: null;
|
|
1080
1260
|
if (localFilesystemApps) {
|
|
1081
|
-
return
|
|
1261
|
+
return finalize(localFilesystemApps);
|
|
1082
1262
|
}
|
|
1083
1263
|
const manifestApps = readWorkspaceAppsFromEnv() ?? readWorkspaceAppsFromManifestFile();
|
|
1084
1264
|
if (manifestApps) {
|
|
1085
|
-
return
|
|
1265
|
+
return finalize(manifestApps);
|
|
1086
1266
|
}
|
|
1087
1267
|
if (!workspaceRoot) {
|
|
1088
|
-
return
|
|
1268
|
+
return finalize([
|
|
1089
1269
|
{
|
|
1090
1270
|
id: "dispatch",
|
|
1091
1271
|
name: "Dispatch",
|
|
@@ -1098,10 +1278,10 @@ export async function listWorkspaceApps(options = {}) {
|
|
|
1098
1278
|
protectedPaths: [],
|
|
1099
1279
|
status: "ready",
|
|
1100
1280
|
},
|
|
1101
|
-
]
|
|
1281
|
+
]);
|
|
1102
1282
|
}
|
|
1103
1283
|
const apps = readWorkspaceAppsFromFilesystem(workspaceRoot) ?? [];
|
|
1104
|
-
return
|
|
1284
|
+
return finalize(apps);
|
|
1105
1285
|
}
|
|
1106
1286
|
/**
|
|
1107
1287
|
* First-party templates the user can scaffold into this workspace via the
|
|
@@ -1259,6 +1439,21 @@ export async function scaffoldWorkspaceAppFromTemplate(input) {
|
|
|
1259
1439
|
summary: `Scaffolded apps/${appId} from ${template}`,
|
|
1260
1440
|
metadata: { template },
|
|
1261
1441
|
});
|
|
1442
|
+
await ensureWorkspaceAppRecords([
|
|
1443
|
+
{
|
|
1444
|
+
id: appId,
|
|
1445
|
+
name: titleCase(appId),
|
|
1446
|
+
description: "",
|
|
1447
|
+
path: `/${appId}`,
|
|
1448
|
+
url: workspaceAppUrl(`/${appId}`),
|
|
1449
|
+
isDispatch: false,
|
|
1450
|
+
audience: DEFAULT_WORKSPACE_APP_AUDIENCE,
|
|
1451
|
+
publicPaths: [],
|
|
1452
|
+
protectedPaths: [],
|
|
1453
|
+
status: "ready",
|
|
1454
|
+
createdBy: currentOwnerEmail(),
|
|
1455
|
+
},
|
|
1456
|
+
]);
|
|
1262
1457
|
return { appId, template, output };
|
|
1263
1458
|
}
|
|
1264
1459
|
function runScaffoldCli(input) {
|