Skip to content
Open
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
13 changes: 13 additions & 0 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ import {
createDrizzleRoutineStore,
createMyraRoutineDrafting,
createRoutineRoutes,
createRoutineTargetRoutes,
createWorkflowRoutineRoutes,
resolveLaunchableDefinition,
routine as routineTable,
Expand Down Expand Up @@ -2917,6 +2918,18 @@ export async function createHub(config: HubConfig) {
validateRoutineInput: routineInputValid,
}),
);
// Routine target discovery (CL-7351): the one list of deployed, frozen
// definitions a routine may reference, beside the platform's own
// `/workflows/definitions` listing but authorized per row for the
// acting principal — see `@corbits/routines`' targets.ts.
app.route(
`${TENANT_PREFIX}/workflows/targets`,
createRoutineTargetRoutes({
db,
grantStore: routineGrantStore,
conditionRegistry: chatConditionRegistry,
}),
);
// Myra's own routine-management surface (`@corbits/routines-tools`'
// `routine_list`/`routine_create`/`routine_update`/`routine_run_now`):
// the workflow-run-authenticated counterpart to the tenant-session
Expand Down
20 changes: 10 additions & 10 deletions apps/web/src/pages/routine-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ import { ROUTINE_HEALTH_TONE } from "../routine-health-tone";
import { useOpenRoutineInCanvas } from "../shell/canvas-availability";
import { StageTopBar } from "../shell/stage-top-bar";
import { nextRunLabel, RunStatusCell, TriggeredByCell } from "./routines-page";
import { listWorkflowDefinitions, useTenantQuery } from "../routines-api";
import { listAllRoutineTargets, useTenantQuery } from "../routines-api";
import { tenantKeys } from "../query-client";

/** The cron expression behind a routine's schedule — `null` for the
Expand Down Expand Up @@ -439,22 +439,22 @@ function RoutineNotice({
);
}

/** The workflow's own display name for `definitionId`. Falls back to the
* id only while the catalog is still loading or when the definition is no
* longer listed — a routine pointing at a retired workflow still has to
/** The target's own display name for `definitionId`. Falls back to the
* id only while targets are still loading or when the definition is no
* longer offered — a routine pointing at a retired workflow still has to
* render. */
function useWorkflowName(row: GlobalRoutineRow | undefined): string {
const tenantId = row?.tenantId ?? "";
const definitions = useTenantQuery(
[...tenantKeys.routines(tenantId), "definitions"],
const targets = useTenantQuery(
[...tenantKeys.routines(tenantId), "targets"],
tenantId !== "",
() => listWorkflowDefinitions(tenantId),
() => listAllRoutineTargets(tenantId),
);
if (definitions.kind !== "ready" || row === undefined) {
if (targets.kind !== "ready" || row === undefined) {
return row?.routine.definitionAssetId ?? "";
}
const match = definitions.data.find(
(definition) => definition.id === row.routine.definitionAssetId,
const match = targets.data.find(
(target) => target.definitionAssetId === row.routine.definitionAssetId,
);
return match?.name ?? row.routine.definitionAssetId ?? "";
}
Expand Down
70 changes: 0 additions & 70 deletions apps/web/src/purpose-definitions.ts

This file was deleted.

99 changes: 38 additions & 61 deletions apps/web/src/routines-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,16 @@
// and `agents-directory.ts` / `@corbits/agent-directory/client`).
// `@corbits/routines` itself is never imported directly — its public
// surface also exports Drizzle schema tables and a Postgres-backed store,
// none of which belong in a browser bundle. Definitions come from the
// platform's own `/api/tenants/:tenantId/workflows/definitions` listing
// (native to `@intx/hub-api`, not part of routines), the same catalog a
// routine's `definitionId` points into.
//
// The create-flow picker only surfaces automatable workflows (see
// `purpose-definitions.ts` + `@corbits/workflow-catalog`). Labels prefer
// the catalog display name over raw asset names.
// none of which belong in a browser bundle. Targets a routine may
// reference come from `GET /api/tenants/:tenantId/workflows/targets`
// (`@corbits/routines`' targets.ts): deployed, frozen, authorized for the
// signed-in principal, already filtered to what the product offers, with
// display names attached — the browser never re-derives that list from
// the platform's raw definitions listing.

import { type } from "arktype";
import type { ArkErrors } from "arktype";
import { useQuery } from "@tanstack/react-query";
import { workflowDisplayName } from "@corbits/workflow-catalog";
import type { APIQuery } from "@corbits/api-query";
import {
ApiQueryError,
Expand All @@ -31,6 +28,7 @@ import {
RoutineRun,
RoutinesResponse,
RoutineRunsResponse,
RoutineTargetsResponse,
routineCreatedToast,
routineDraftApprovePath,
routineDraftDiscardPath,
Expand All @@ -40,14 +38,14 @@ import {
routineRunStartedToast,
routineRunsPath,
routinesPath,
routineTargetsPath,
} from "@corbits/routines/client";
import type {
CreateDraftInput,
CreateRoutineInput,
RoutineTarget,
UpdateRoutineInput,
} from "@corbits/routines/client";
import { purposeDefinitions, withCatalogFields } from "./purpose-definitions";
import type { CatalogFields } from "./purpose-definitions";

export {
DraftedStep,
Expand All @@ -57,32 +55,15 @@ export {
type Routine,
type RoutineDraft,
type RoutineRun,
type RoutineTarget,
type RoutineTargetKind,
type RoutineTriggerT as RoutineTrigger,
type UpdateRoutineInput,
} from "@corbits/routines/client";

const WorkflowDefinitionRecord = type({
id: "string",
name: "string",
status: "string",
"description?": "string | null",
});
type WorkflowDefinitionRecord = typeof WorkflowDefinitionRecord.infer;

/** An automatable workflow definition, enriched with its catalog
* demo-card fields (see `withCatalogFields`) — the shape the Routines
* create picker renders a card from. */
export type WorkflowDefinitionSummary = WorkflowDefinitionRecord &
CatalogFields;

const DefinitionsPage = type({
data: WorkflowDefinitionRecord.array(),
"nextCursor?": "string | null",
});

/** One page is enough for a seeded bench; walk cursors so a large catalog
* never silently truncates automatable options. */
const PAGE_LIMIT = 100;
/** One page is enough for a seeded bench; `listAllRoutineTargets` walks
* cursors so a large tenant never silently truncates options. */
const TARGETS_PAGE_LIMIT = 100;

type Validator<T> = (data: unknown) => T | ArkErrors;

Expand Down Expand Up @@ -239,38 +220,34 @@ export function discardRoutineDraft(
});
}

async function listAllDefinitions(
/** One page of definitions the signed-in principal may target from a
* routine, ordered by name; pass the previous page's `nextCursor` to
* continue. */
export function listRoutineTargets(
tenantId: string,
): Promise<readonly WorkflowDefinitionRecord[]> {
const collected: WorkflowDefinitionRecord[] = [];
let cursor: string | null = null;
for (;;) {
const query = new URLSearchParams({ limit: String(PAGE_LIMIT) });
if (cursor !== null) query.set("cursor", cursor);
const page = await request(
`/api/tenants/${tenantId}/workflows/definitions?${query}`,
DefinitionsPage,
);
collected.push(...page.data);
if (page.nextCursor === undefined || page.nextCursor === null) break;
cursor = page.nextCursor;
}
return collected;
cursor?: string,
): Promise<RoutineTargetsResponse> {
return request(
routineTargetsPath(tenantId, {
limit: TARGETS_PAGE_LIMIT,
...(cursor !== undefined ? { cursor } : {}),
}),
RoutineTargetsResponse,
);
}

/**
* All automatable workflow definitions for the Routines create picker.
* Walks pagination, filters via the catalog allowlist, and attaches a
* friendly label for Menu items (never a raw id).
*/
export async function listWorkflowDefinitions(
/** Every routine target in the tenant, cursor-walked. */
export async function listAllRoutineTargets(
tenantId: string,
): Promise<readonly WorkflowDefinitionSummary[]> {
const collected = await listAllDefinitions(tenantId);
return withCatalogFields(purposeDefinitions(collected)).map((definition) => ({
...definition,
name: workflowDisplayName(definition.name, definition.description),
}));
): Promise<readonly RoutineTarget[]> {
const collected: RoutineTarget[] = [];
let cursor: string | undefined;
for (;;) {
const page = await listRoutineTargets(tenantId, cursor);
collected.push(...page.items);
if (page.nextCursor === null) return collected;
cursor = page.nextCursor;
}
}

/**
Expand Down
60 changes: 0 additions & 60 deletions apps/web/test/purpose-definitions.test.ts

This file was deleted.

3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading