Download src/manifests/automation-interface.ts from SaylorTwift/openhands: direct link, hf CLI and curl.
- Browser
- Download file 9 kB
-
https://huggingface.co/SaylorTwift/openhands/resolve/main/src/manifests/automation-interface.ts
- Command line
-
hf download hf://SaylorTwift/openhands/src/manifests/automation-interface.ts
-
curl -L -o automation-interface.ts https://huggingface.co/SaylorTwift/openhands/resolve/main/src/manifests/automation-interface.ts
9 kB
| /** | |
| * The automation interface seam. | |
| * | |
| * Every automation-specific datum the host's surfaces render — navigation and | |
| * page copy, the settable attributes, the import/export envelope, endpoint | |
| * paths, the featured and responder id lists, the sub-page surface — is served | |
| * from the interface manifest the pinned `@openhands/extensions` publishes. | |
| * The host holds none of it. When the package publishes no manifest, or the | |
| * published one fails admission, there is nothing to serve: the nav entries do | |
| * not render and the routes 404. `hasAutomationInterface()` is that gate, and | |
| * every accessor that needs the manifest is reachable only from behind it. | |
| * | |
| * Routes are the exception, and they are not a definition: `src/routes.ts` | |
| * mounts them, so the host states them here and admission checks a manifest's | |
| * routes against them. That check is what keeps a published deep link | |
| * resolving to the page it named. | |
| */ | |
| import { AUTOMATION_CATALOG } from "@openhands/extensions/automations"; | |
| import { validateInterfaceManifest } from "./interface-validation"; | |
| import { AUTOMATION_INTERFACE_CANDIDATE } from "./manifest-sources"; | |
| import type { | |
| AutomationAttributeName, | |
| InterfaceDashboardFilter, | |
| InterfaceDashboardSort, | |
| InterfaceEndpointName, | |
| InterfaceIconSlug, | |
| InterfaceImportExport, | |
| InterfaceListInsights, | |
| InterfaceManifest, | |
| InterfaceOverview, | |
| InterfaceRoutes, | |
| InterfaceSubPageId, | |
| InterfaceTemplatesPage, | |
| } from "./types"; | |
| /** The routes this host has registrations for, in `src/routes.ts`. */ | |
| const MOUNTED_ROUTES = { | |
| list: "/automations", | |
| setup: "/automations/new/:automationId", | |
| detail: "/automations/:automationId", | |
| templates: "/automations/templates", | |
| } satisfies InterfaceRoutes; | |
| /** | |
| * File-format section for portable `.automation.json` export/import. A docs | |
| * pointer for the host's import picker, not interface data — the manifest's | |
| * `importExport` block has no docs field. | |
| */ | |
| export const AUTOMATION_FILE_FORMAT_DOCS_URL = | |
| "https://docs.openhands.dev/openhands/usage/agent-canvas/managing-automations#exported-file-format"; | |
| function isRecord(value: unknown): value is Record<string, unknown> { | |
| return typeof value === "object" && value !== null && !Array.isArray(value); | |
| } | |
| function getCatalogIds(): ReadonlySet<string> { | |
| return new Set( | |
| (AUTOMATION_CATALOG as readonly unknown[]).flatMap((entry) => | |
| isRecord(entry) && typeof entry.id === "string" ? [entry.id] : [], | |
| ), | |
| ); | |
| } | |
| function admitInterfaceManifest(candidate: unknown): InterfaceManifest | null { | |
| if (candidate === undefined) return null; | |
| const result = validateInterfaceManifest(candidate, { | |
| catalogIds: getCatalogIds(), | |
| mountedRoutes: MOUNTED_ROUTES, | |
| }); | |
| if (!result.valid) { | |
| // Mirrors the setup registry: a rejected manifest is skipped loudly, and | |
| // the surfaces it would have described stay unrendered. | |
| console.warn( | |
| "Rejected the automation interface manifest:", | |
| result.errors.join("; "), | |
| ); | |
| return null; | |
| } | |
| return candidate as InterfaceManifest; | |
| } | |
| const ADMITTED = admitInterfaceManifest(AUTOMATION_INTERFACE_CANDIDATE); | |
| /** | |
| * Whether this deployment has an automation interface at all. The nav entries | |
| * and the route loaders ask before rendering; nothing else has to. | |
| */ | |
| export function hasAutomationInterface(): boolean { | |
| return ADMITTED !== null; | |
| } | |
| /** | |
| * The admitted manifest, for the accessors that cannot answer without one. | |
| * Reaching one of those without a manifest means a surface rendered past its | |
| * gate — a wiring mistake, not a state to render. | |
| */ | |
| function requireInterface(): InterfaceManifest { | |
| if (!ADMITTED) { | |
| throw new Error( | |
| "No automation interface manifest is admitted, so this surface should not have rendered.", | |
| ); | |
| } | |
| return ADMITTED; | |
| } | |
| function substituteRouteParam(pattern: string, id: string): string { | |
| return pattern.replace(":automationId", encodeURIComponent(id)); | |
| } | |
| export function automationListPath(): string { | |
| return MOUNTED_ROUTES.list; | |
| } | |
| export function automationSetupPath(id: string): string { | |
| return substituteRouteParam(MOUNTED_ROUTES.setup, id); | |
| } | |
| export function automationDetailPath(id: string): string { | |
| return substituteRouteParam(MOUNTED_ROUTES.detail, id); | |
| } | |
| export function automationTemplatesPath(): string { | |
| return MOUNTED_ROUTES.templates; | |
| } | |
| /** | |
| * Whether `path` (a location pathname) is inside the automation surface: the | |
| * list route itself or anything nested under it (detail, setup, templates, | |
| * git-sync). Prefix-safe: "/automations-foo" does not match. | |
| */ | |
| export function isAutomationsRoute(path: string): boolean { | |
| return ( | |
| path === MOUNTED_ROUTES.list || path.startsWith(`${MOUNTED_ROUTES.list}/`) | |
| ); | |
| } | |
| /** | |
| * A declared endpoint path. Empty for one the manifest may omit - the two a | |
| * bundle needs were added after the block shipped - so a caller that needs one | |
| * says so rather than reading a host-held default that does not exist. | |
| */ | |
| export function getAutomationEndpoint(name: InterfaceEndpointName): string { | |
| return requireInterface().endpoints[name] ?? ""; | |
| } | |
| /** An id-parameterized endpoint with `{id}` substituted, encoded. */ | |
| export function getAutomationIdEndpoint( | |
| name: "detail" | "dispatch" | "runs" | "tarball", | |
| id: string, | |
| ): string { | |
| return getAutomationEndpoint(name).replace("{id}", encodeURIComponent(id)); | |
| } | |
| export interface InterfaceCopy { | |
| sidebarLabel: string; | |
| commandMenuTitle: string; | |
| commandMenuDescription: string; | |
| commandMenuKeywords: string; | |
| listTitle: string; | |
| listSubtitle: string; | |
| detailBackLabel: string; | |
| editTitle: string; | |
| } | |
| export function getInterfaceCopy(): InterfaceCopy { | |
| const manifest = requireInterface(); | |
| return { | |
| sidebarLabel: manifest.navigation.sidebar.label, | |
| commandMenuTitle: manifest.navigation.commandMenu.title, | |
| commandMenuDescription: manifest.navigation.commandMenu.description, | |
| commandMenuKeywords: manifest.navigation.commandMenu.keywords, | |
| listTitle: manifest.pages.list.title, | |
| listSubtitle: manifest.pages.list.subtitle, | |
| detailBackLabel: manifest.pages.detail.backLabel, | |
| editTitle: manifest.pages.edit.title, | |
| }; | |
| } | |
| export interface AttributeSpec { | |
| present: boolean; | |
| label: string; | |
| /** Null when the manifest states no help text for the attribute. */ | |
| help: string | null; | |
| required: boolean; | |
| min: number | null; | |
| max: number | null; | |
| } | |
| /** An attribute the manifest does not declare is not offered at all. */ | |
| const ABSENT_ATTRIBUTE: AttributeSpec = { | |
| present: false, | |
| label: "", | |
| help: null, | |
| required: false, | |
| min: null, | |
| max: null, | |
| }; | |
| export function getAttributeSpec(name: AutomationAttributeName): AttributeSpec { | |
| const attribute = requireInterface().attributes[name]; | |
| if (!attribute) return ABSENT_ATTRIBUTE; | |
| return { | |
| present: true, | |
| label: attribute.label, | |
| help: attribute.help ?? null, | |
| required: attribute.required, | |
| min: attribute.constraints?.min ?? null, | |
| max: attribute.constraints?.max ?? null, | |
| }; | |
| } | |
| export function getImportExportSpec(): InterfaceImportExport { | |
| return requireInterface().importExport; | |
| } | |
| export function getAutomationsDocsUrl(): string { | |
| return requireInterface().docsUrl; | |
| } | |
| export function getFeaturedAutomationIds(): readonly string[] { | |
| return requireInterface().featuredAutomationIds; | |
| } | |
| export function getResponderIntegrationIds(): readonly string[] { | |
| return requireInterface().responderIntegrationIds; | |
| } | |
| export interface SubPageNavSpec { | |
| page: InterfaceSubPageId; | |
| /** The page's route, resolved through the host's route table. */ | |
| to: string; | |
| label: string; | |
| icon: InterfaceIconSlug; | |
| } | |
| /** | |
| * The sub-page navigation, or null when the manifest does not declare the | |
| * sub-page surface. | |
| */ | |
| export function getSubPagesSpec(): SubPageNavSpec[] | null { | |
| const subPages = ADMITTED?.navigation.subPages; | |
| if (!subPages) return null; | |
| return subPages.map((item) => ({ | |
| page: item.page, | |
| to: | |
| item.page === "templates" | |
| ? automationTemplatesPath() | |
| : automationListPath(), | |
| label: item.label, | |
| icon: item.icon, | |
| })); | |
| } | |
| export interface DashboardSpec { | |
| overview: InterfaceOverview; | |
| filters: InterfaceDashboardFilter[]; | |
| sort: InterfaceDashboardSort; | |
| insights: InterfaceListInsights; | |
| } | |
| /** | |
| * The list page's dashboard composition, or null when the manifest does not | |
| * declare it. Admission accepts the sub-page surface whole or not at all, so | |
| * these four are present together. | |
| */ | |
| export function getDashboardSpec(): DashboardSpec | null { | |
| const list = ADMITTED?.pages.list; | |
| if (!list?.overview || !list.filters || !list.sort || !list.insights) { | |
| return null; | |
| } | |
| return { | |
| overview: list.overview, | |
| filters: list.filters, | |
| sort: list.sort, | |
| insights: list.insights, | |
| }; | |
| } | |
| /** The templates page identity, or null when the manifest does not declare it. */ | |
| export function getTemplatesPageSpec(): InterfaceTemplatesPage | null { | |
| return ADMITTED?.pages.templates ?? null; | |
| } | |