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
35 changes: 35 additions & 0 deletions apps/web/src/agent-deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import type { ToolEffect } from "@corbits/worker/definition-json";
import type { McpServerDeployment } from "@corbits/worker/workflow-ids";

import { ensureAgentHubCredential } from "./agent-hub-credential";
import { assignRolesToAgent, resolveAgentRoleIds } from "./agent-roles";
import { personMailAddress } from "./mail-address";
import { listMcpServers, resolveWorkspaceTenantId, toMcpServerDeployment } from "./mcp-servers";
import type { McpServer } from "./mcp-servers";
Expand Down Expand Up @@ -74,6 +75,11 @@ export type NewAgentInput = {
/** Tool permissions to re-declare on a redeploy, as creator-sourced grant
* requirements. */
readonly toolEffects?: readonly ToolEffect[];
/** Role names checked in the create form, resolved against this tenant's
* live roles and assigned to the agent's workflow principal once the
* install lands. An unknown name fails the deploy before anything
* installs; a missing principal (no first run yet) fails it after. */
readonly roles?: readonly string[];
/** Deploy anew even when nothing changed (a restart mints a fresh run). */
readonly redeploy?: boolean;
};
Expand Down Expand Up @@ -183,6 +189,16 @@ export async function deployAgentSource(
if (!isValidSlug(slug)) {
throw new AgentDeployError("this name doesn't produce a usable agent address");
}
// Fail fast on an unknown role name: resolving before anything installs
// leaves no half-deployed agent behind. An empty selection skips the read.
let roleIds: readonly string[] = [];
try {
roleIds = await resolveAgentRoleIds(args.tenantId, args.input.roles ?? [], fetchImpl);
} catch (cause) {
throw new AgentDeployError(
cause instanceof Error ? cause.message : "resolving this agent's roles failed",
);
}
const assetName = agentDeploySourceAssetName(slug);
const packageName = `@workbench-agent/${slug}`;

Expand Down Expand Up @@ -247,5 +263,24 @@ export async function deployAgentSource(
if (args.input.schedule !== undefined) {
await scheduleAgentRun(args.tenantId, args.input.schedule, assetName, tenant.domain, fetchImpl);
}
// The install already landed, so a missing principal (no first run yet)
// or a refused write fails the deploy with the agent in place — the panel
// stays open with this message, and resubmitting assigns the roles.
if (roleIds.length > 0) {
try {
await assignRolesToAgent(
{
tenantId: args.tenantId,
roleIds,
principalRef: { deploymentId: parsed.deploymentId, address: triggerAddress },
},
fetchImpl,
);
} catch (cause) {
throw new AgentDeployError(
cause instanceof Error ? cause.message : "assigning this agent's roles failed",
);
}
}
return parsed;
}
21 changes: 21 additions & 0 deletions apps/web/src/agent-roles-query.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// The roles a create form can offer: the tenant's live platform roles,
// read through react-query so the agent and workbench create surfaces share
// one loading/error/ready contract.
import { useQuery } from "@tanstack/react-query";

import { toAPIQuery, type APIQuery } from "@/lib/api-query";

import { listTenantRoles, type RoleOption } from "./agent-roles";

function tenantRolesKey(tenantId: string | null) {
return ["tenant", tenantId ?? "none", "roles"] as const;
}

export function useTenantRoles(tenantId: string | null): APIQuery<readonly RoleOption[]> {
const result = useQuery({
queryKey: tenantRolesKey(tenantId),
enabled: tenantId !== null,
queryFn: () => listTenantRoles(tenantId as string),
});
return toAPIQuery(result);
}
Loading
Loading