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
1 change: 1 addition & 0 deletions apps/hub/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"@intx/mime": "workspace:*",
"@intx/types": "workspace:*",
"@intx/workflow": "workspace:*",
"@intx/workflow-deploy": "workspace:*",
"@modelcontextprotocol/sdk": "catalog:",
"@workbench/access-policy": "workspace:*",
"@workbench/connections": "workspace:*",
Expand Down
136 changes: 126 additions & 10 deletions apps/hub/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,9 +39,16 @@ import {
createMailTriggeredRunGrantsMaterializer,
createRequireGrant,
readDurableWorkflowRunLifecycles,
resolveDefinitionSources,
type AppEnv,
type TenantEnv,
} from "@intx/hub-api";
import {
deriveRunAddress,
deriveRunAgentId,
WorkflowDefinitionInvalidError,
} from "@intx/workflow-deploy";
import type { HarnessConfig } from "@intx/types/runtime";

import {
createAgentDefinitionRoutes,
Expand Down Expand Up @@ -330,6 +337,8 @@ import { createSkillRoutes, createWorkflowSkillRoutes } from "@corbits/skills";
import {
createWorkflowAuthorRegistry,
createWorkflowAuthorRoutes,
WorkflowAuthorError,
type WorkflowDeployer,
} from "@corbits/agent-workflow-authoring";
import { mountArtifacts } from "./artifacts-mount";
import { mountWorkbenchSlackTag } from "./slack-tag-mount";
Expand Down Expand Up @@ -1858,16 +1867,122 @@ export async function createHub(config: HubConfig) {
registry: skills.registry,
}),
);
// Agent-authored workflows (CL-agent-authored-workflows): an agent
// publishes a workflow codebase as a native `kind:"workflow"` asset
// through this workflow-run-authenticated surface, then deploys the
// resulting asset through the tenant-session `/workflows/deployments`
// route this hub already mounts (unchanged, below) — this package never
// reimplements that deploy gating. Unlike `/api/workflow-skills` above,
// every write here also runs a real `chatGrantStore` authorization
// check (`asset:*`/create, `asset:<id>`/write) before reaching
// `RepoStore`, because authoring is publishing executable code, not a
// markdown skill.
// CL-7361: the `deploy` half of the run-authenticated deployer this
// route's registry calls — the SAME `sessionService.
// deployWorkflowFromSource` call (already `withDeploySourceRecording`-
// wrapped above) the native `POST /workflows/deployments` route's own
// non-exclusive branch makes, not a reimplementation of install/probe/
// gate/freeze. Inference sources are resolved server-side from the
// tenant's catalog (`resolveDefinitionSources`) exactly as
// `agent-definitions`' `tenantDefaultModel` does above — an agent never
// supplies or sees a provider secret. Exclusive sidecar placement is out
// of scope: an agent-authored deploy always lands on shared capacity.
// Thin adapter over Interchange's native deploy: `registry.deploy()`
// (packages/agent-workflow-authoring) already resolves and authorizes the
// asset (own-tenant row check, `workflow:*`/create) before calling this,
// so this seam receives the already-resolved `assetId`/`assetName`
// rather than re-querying `assetTable` — the only work this adapter adds
// on top of native `sessionService.deployWorkflowFromSource` is
// server-side inference-source resolution (`resolveDefinitionSources`),
// because the native `/workflows/deployments` route requires the caller
// to supply `sources` directly and an agent caller must never see a
// provider secret to do that itself. `modelRequirements: null` is
// deliberate: a workflow's own declared model needs (if any) are not
// considered at this step, matching `agent-definitions`' identical
// tenant-default resolution above; deploy always resolves against the
// tenant's default/first-preference model.
const workflowDeployer: WorkflowDeployer = {
async deploy({ tenantId, principalId, assetId, commitSha, entry }) {
const tenantRow = await db.query.tenant.findFirst({
where: eq(tenantTable.id, tenantId),
});
if (tenantRow === undefined) {
throw new WorkflowAuthorError(
"not_found",
`tenant ${tenantId} not found`,
);
}

const fallbackModel =
(await workbenchHostInferencePreferencesResolver(tenantId))[0]?.model ??
null;
const resolution = await resolveDefinitionSources({
db,
tenantId,
modelRequirements: null,
fallbackModel,
invokerPreferences: {},
credentialCipher,
});
if (!resolution.ok) {
throw new WorkflowAuthorError("invalid", resolution.message);
}

const anchorRunId = generateId("workflowRun");
const agentAddress = deriveRunAddress({
runId: anchorRunId,
domain: tenantRow.domain,
});
const config: HarnessConfig = {
sessionId: generateId("session"),
agentId: deriveRunAgentId({ runId: anchorRunId }),
tenantId,
principalId,
agentAddress,
systemPrompt: "",
tools: [],
grants: [],
sources: resolution.sources,
defaultSource: resolution.defaultSource,
};

try {
const result = await sessionService.deployWorkflowFromSource({
tenantId,
anchorRunId,
deploymentDomain: tenantRow.domain,
agentAddress,
source: {
kind: "asset",
assetId,
package: { format: "source", commitSha },
},
entry,
definitionAssetId: assetId,
config,
});
return {
deploymentId: result.anchorRunId,
definitionAssetId: assetId,
status: "deployed",
};
} catch (err) {
// Mirrors `@intx/hub-api`'s own `/workflows/deployments` route: an
// install/gate rejection or an unapproved source chain is a
// client/definition error; anything else (a missing commit, an
// unreachable sidecar) is reported as `unavailable` rather than
// guessed apart, exactly as the native route's own catch-all does.
if (err instanceof WorkflowDefinitionInvalidError) {
throw new WorkflowAuthorError("invalid", err.message);
}
throw new WorkflowAuthorError(
"unavailable",
err instanceof Error ? err.message : "Failed to deploy workflow",
);
}
},
};
// Agent-authored workflows (CL-7360, CL-7361): an agent publishes a
// workflow codebase as a native `kind:"workflow"` asset AND deploys it,
// both through this workflow-run-authenticated surface — `deploy`
// reaches the exact same `sessionService.deployWorkflowFromSource` the
// tenant-session `/workflows/deployments` route drives (`workflowDeployer`
// above), never a second gating path. Unlike `/api/workflow-skills`
// above, every write here also runs a real `chatGrantStore`
// authorization check (`asset:*`/create, `asset:<id>`/write,
// `workflow:*`/create) before reaching `RepoStore` or the deploy call,
// because authoring and deploying are side effects, not a markdown
// skill edit.
app.route(
"/api/workflow-workflow-authoring",
createWorkflowAuthorRoutes({
Expand All @@ -1878,6 +1993,7 @@ export async function createHub(config: HubConfig) {
repoStore: agentRepoStore.repoStore,
grantStore: chatGrantStore,
conditionRegistry: chatConditionRegistry,
deployer: workflowDeployer,
}),
}),
);
Expand Down
15 changes: 15 additions & 0 deletions bun.lock

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

Loading
Loading