From 32d1e809a1dae5f0bd62ed7b61c8e2e68f062a94 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 1 Sep 2026 23:21:16 -0700 Subject: [PATCH 1/7] Enforce authorization on routine targets (CL-7354) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A create or PATCH-carried retarget now must clear the same two gates before it is persisted: resolveLaunchableDefinition must resolve the asset, and the acting principal must be authorized for workflow-definition:/read — the same verb listRoutineTargets already checks per row. A denial is a typed 403 alongside the existing 400/404/409 target envelopes, on both the tenant-session and Myra's workflow-run-authenticated routine surfaces. Also routes a pre-existing catch in postRoutineEnabledNotice through reportError, since this change's diff now touches that line and check:report-error requires it. --- packages/routines/src/routes.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/packages/routines/src/routes.ts b/packages/routines/src/routes.ts index 8567d38a4..04bf85276 100644 --- a/packages/routines/src/routes.ts +++ b/packages/routines/src/routes.ts @@ -889,6 +889,27 @@ export function createRoutineRoutes( const effectiveDefinitionAssetId = body.definitionAssetId ?? existing.definitionAssetId; + if ( + body.definitionAssetId !== undefined && + body.definitionAssetId !== existing.definitionAssetId + ) { + const rejection = await rejectUnlaunchableTarget( + deps, + tenant.id, + principal.id, + body.definitionAssetId, + ); + if (rejection !== undefined) { + return c.json( + makeErrorEnvelope({ + code: rejection.code, + userMessage: rejection.userMessage, + }), + rejection.status, + ); + } + } + if ( body.trigger !== undefined && !(await webhookTriggerValid( From 9808fcc60d8b0060e5f16c1e61a05f1e3596d3fd Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 1 Sep 2026 23:41:20 -0700 Subject: [PATCH 2/7] Add tests for deploying Agent Builder definitions natively Route tests that asserted DefinitionFreezer freeze/refreeze calls now assert the injected WorkflowDeployer is called with the commit each write produces, and a new source-grep test asserts this package never imports @corbits/workflow-freeze (CL-7363). --- .../src/native-deploy-cutover.test.ts | 41 +++++++++ .../test/routes.integration.test.ts | 90 ++++++++++++++----- packages/agent-directory/test/routes.test.ts | 86 ++++++++++-------- .../test/workflow-capability-routes.test.ts | 58 +++++++----- .../test/workflow-create-routes.test.ts | 43 +++++---- .../test/workflow-skill-pin-routes.test.ts | 58 +++++++----- 6 files changed, 253 insertions(+), 123 deletions(-) create mode 100644 packages/agent-directory/src/native-deploy-cutover.test.ts diff --git a/packages/agent-directory/src/native-deploy-cutover.test.ts b/packages/agent-directory/src/native-deploy-cutover.test.ts new file mode 100644 index 000000000..58783034e --- /dev/null +++ b/packages/agent-directory/src/native-deploy-cutover.test.ts @@ -0,0 +1,41 @@ +// Regression for CL-7363: Agent Builder definitions used to self-freeze +// through `@corbits/workflow-freeze`'s `DefinitionFreezer`, a hub-local +// path that bypasses the native sidecar probe. This package now deploys +// every definition write through the injected `WorkflowDeployer` — the +// SAME seam `@corbits/agent-workflow-authoring`'s own registry calls +// (`sessionService.deployWorkflowFromSource`, install -> sidecar probe +// -> gate -> freeze) — so `@corbits/workflow-freeze` must never again +// appear in this package's source or its dependency manifest. +import { describe, expect, test } from "bun:test"; +import { readFileSync, readdirSync } from "node:fs"; +import path from "node:path"; + +const SRC_DIR = path.join(import.meta.dir, "."); + +function tsFilesUnder(dir: string): string[] { + return readdirSync(dir, { withFileTypes: true }).flatMap((entry) => { + const full = path.join(dir, entry.name); + if (entry.isDirectory()) return tsFilesUnder(full); + return entry.name.endsWith(".ts") && !entry.name.endsWith(".test.ts") + ? [full] + : []; + }); +} + +describe("workflow-freeze cutover", () => { + test("no source file in this package imports @corbits/workflow-freeze", () => { + const offenders = tsFilesUnder(SRC_DIR).filter((file) => + readFileSync(file, "utf8").includes("@corbits/workflow-freeze"), + ); + expect(offenders).toEqual([]); + }); + + test("package.json declares no @corbits/workflow-freeze dependency", () => { + const packageJson = JSON.parse( + readFileSync(path.join(import.meta.dir, "../package.json"), "utf8"), + ) as { dependencies?: Record }; + expect( + Object.keys(packageJson.dependencies ?? {}), + ).not.toContain("@corbits/workflow-freeze"); + }); +}); diff --git a/packages/agent-directory/test/routes.integration.test.ts b/packages/agent-directory/test/routes.integration.test.ts index 2c138ef34..6ce799b72 100644 --- a/packages/agent-directory/test/routes.integration.test.ts +++ b/packages/agent-directory/test/routes.integration.test.ts @@ -25,15 +25,12 @@ import { eq, inArray } from "drizzle-orm"; import { Hono } from "hono"; import type { MiddlewareHandler } from "hono"; -import { - createDB, - loadFrozenGrantSnapshot, - loadFrozenWireProjection, -} from "@intx/db"; +import { createDB } from "@intx/db"; import { asset as assetTable, principal as principalTable, tenant as tenantTable, + workflowDefinition, } from "@intx/db/schema"; import { createAgentRepoStore, createAssetService } from "@intx/hub-sessions"; import { generateKeyPair } from "@intx/crypto"; @@ -43,7 +40,6 @@ import { dbTargetFromUrl } from "../../../scripts/db-setup"; import { applyAgentDirectoryMigrations } from "../src/migrations"; import { definitionSkills } from "../src/schema"; import { createAgentDefinitionRoutes } from "../src/routes"; -import { createDefinitionFreezer } from "@corbits/workflow-freeze"; import type { PinnedSkillIndexResolver } from "../src/routes"; import { createDrizzleDefinitionSkillsStore } from "../src/skills-store"; import type { DefinitionAssetHistory } from "../src/definition-history"; @@ -108,11 +104,57 @@ async function post(app: Hono, body: unknown): Promise { }); } +/** Records deploy calls instead of running a real + * `sessionService.deployWorkflowFromSource`, which needs a connected + * sidecar this DB-only integration harness does not stand up — the same + * stub `routes.test.ts` uses. This suite's own job is proving the real + * `AssetService`'s tree validator accepts what this package writes, so + * the stub still projects a `workflow_definition` row directly (the one + * piece of the real deploy every route's read-back depends on) rather + * than faking the whole install/probe/gate/freeze pipeline. */ +function recordingAgentDefinitionDeployer(db: ReturnType["db"]) { + const deploys: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }[] = []; + return { + deploys, + deploy: async (input: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }) => { + deploys.push(input); + await db + .insert(workflowDefinition) + .values({ + id: `wfd_${randomUUID()}`, + tenantId: input.tenantId, + assetId: input.assetId, + wireHash: input.commitSha, + name: input.assetId, + }) + .onConflictDoNothing(); + return { + deploymentId: "dep_1", + definitionAssetId: input.assetId, + status: "deployed", + }; + }, + }; +} + describeIfDb("agent-directory routes against a real assetService", () => { let dataDir: string; let db: ReturnType["db"]; let close: () => Promise; let app: Hono; + let deployer: ReturnType; beforeAll(async () => { if (databaseUrl === undefined) { @@ -135,6 +177,7 @@ describeIfDb("agent-directory routes against a real assetService", () => { repoStore: agentRepoStore.repoStore, }); const skillsStore = createDrizzleDefinitionSkillsStore(db); + deployer = recordingAgentDefinitionDeployer(db); const routes = createAgentDefinitionRoutes({ db, @@ -144,7 +187,7 @@ describeIfDb("agent-directory routes against a real assetService", () => { history: fakeHistory, capabilityInventory: fakeCapabilityInventory, requireGrant: allowAllRequireGrant, - definitionFreezer: createDefinitionFreezer(db), + deployer, }); const asPrincipal: MiddlewareHandler = async (c, next) => { c.set("tenant", TENANT); @@ -213,7 +256,7 @@ describeIfDb("agent-directory routes against a real assetService", () => { expect(gotAfterBody.skills).toEqual([]); }); - test("a created definition is launch-resolvable: its projection and grant snapshot are frozen (CL-6447)", async () => { + test("a created definition deploys its commit through the native source pipeline (CL-7363)", async () => { const handle = `launchable-${suffix}`; const created = await post(app, { name: "Launchable", @@ -222,19 +265,19 @@ describeIfDb("agent-directory routes against a real assetService", () => { skills: [], }); expect(created.status).toBe(201); - const { id: definitionId } = (await created.json()) as { id: string }; - // The exact reads the chat invite path (`readDefinitionProjection`) - // and the mail-triggered turn path (`loadFrozenGrantSnapshot`) fail - // closed on: both must be frozen at create or the agent 409s - // `not_launchable` forever. - const projection = await loadFrozenWireProjection(db, definitionId); - expect(projection).not.toBeNull(); - expect(JSON.stringify(projection)).toContain("You answer launch checks."); - expect(await loadFrozenGrantSnapshot(db, definitionId)).not.toBeNull(); - - // An instructions save re-freezes in place: the frozen projection - // follows the edit under the same definition id. + // Create deploys exactly once, with the commit the asset write just + // produced — the same sequence a launch depends on being launchable + // (CL-6447), now driven through the native install/probe/gate/freeze + // pipeline instead of a bare freeze. + expect(deployer.deploys).toHaveLength(1); + const [firstDeploy] = deployer.deploys; + expect(firstDeploy?.tenantId).toBe(TENANT.id); + expect(firstDeploy?.commitSha).toBeDefined(); + + // An instructions save deploys again, in place: the same definition + // asset, a new commit. + const { id: definitionId } = (await created.json()) as { id: string }; const updated = await app.request(`/${definitionId}`, { method: "PUT", headers: { "content-type": "application/json" }, @@ -244,9 +287,8 @@ describeIfDb("agent-directory routes against a real assetService", () => { }), }); expect(updated.status).toBe(200); - const refrozen = await loadFrozenWireProjection(db, definitionId); - expect(JSON.stringify(refrozen)).toContain( - "You answer edited launch checks.", - ); + expect(deployer.deploys).toHaveLength(2); + expect(deployer.deploys[1]?.assetId).toBe(firstDeploy?.assetId); + expect(deployer.deploys[1]?.commitSha).not.toBe(firstDeploy?.commitSha); }); }); diff --git a/packages/agent-directory/test/routes.test.ts b/packages/agent-directory/test/routes.test.ts index 81e31863a..8c714ed8c 100644 --- a/packages/agent-directory/test/routes.test.ts +++ b/packages/agent-directory/test/routes.test.ts @@ -263,22 +263,33 @@ const allowAllRequireGrant: RequireGrant = () => async (_c, next) => { await next(); }; -/** Records freeze/re-freeze calls instead of running the real - * `@corbits/workflow-freeze` machinery (whose own suites cover the DB - * half); routes here are asserted to invoke it on every content write. */ -function recordingDefinitionFreezer() { - const freezes: { assetId: string; workflowJson: string }[] = []; - const refreezes: { definitionId: string; workflowJson: string }[] = []; +/** Records deploy calls instead of running the real + * `sessionService.deployWorkflowFromSource` machinery (whose own suites + * cover the install/probe/gate/freeze half); routes here are asserted to + * invoke it on every content write, with the commit the write produced. */ +function recordingAgentDefinitionDeployer() { + const deploys: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }[] = []; return { - freezes, - refreezes, - freeze: (input: { assetId: string; workflowJson: string }) => { - freezes.push(input); - return Promise.resolve({ definitionId: "def_new", wireHash: "hash_1" }); - }, - refreeze: (input: { definitionId: string; workflowJson: string }) => { - refreezes.push(input); - return Promise.resolve({ wireHash: "hash_2" }); + deploys, + deploy: (input: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }) => { + deploys.push(input); + return Promise.resolve({ + deploymentId: "dep_1", + definitionAssetId: input.assetId, + status: "deployed", + }); }, }; } @@ -289,9 +300,9 @@ function buildApp( history: DefinitionAssetHistory = fakeHistory(), capabilityInventory: CapabilityInventoryProvider = fakeCapabilityInventory, skillsStore: DefinitionSkillsStore = createInMemoryDefinitionSkillsStore(), - definitionFreezer: ReturnType< - typeof recordingDefinitionFreezer - > = recordingDefinitionFreezer(), + deployer: ReturnType< + typeof recordingAgentDefinitionDeployer + > = recordingAgentDefinitionDeployer(), ): Hono { const routes = createAgentDefinitionRoutes({ db, @@ -301,7 +312,7 @@ function buildApp( history, capabilityInventory, requireGrant, - definitionFreezer, + deployer, }); const asPrincipal: MiddlewareHandler = async (c, next) => { c.set("tenant", TENANT); @@ -470,7 +481,7 @@ function fakeCreateDb(): DB["db"] { test("a create request with skills writes the definition source tree to the asset and records skills in the skills store", async () => { let writtenFiles: Record | undefined; const skillsStore = createInMemoryDefinitionSkillsStore(); - const freezer = recordingDefinitionFreezer(); + const deployer = recordingAgentDefinitionDeployer(); const app = buildApp( fakeAssetService({ createAsset: () => @@ -494,7 +505,7 @@ test("a create request with skills writes the definition source tree to the asse fakeHistory(), fakeCapabilityInventory, skillsStore, - freezer, + deployer, ); const response = await post(app, { name: "Research Buddy", @@ -505,11 +516,13 @@ test("a create request with skills writes the definition source tree to the asse expect(response.status).toBe(201); expect(writtenFiles).toBeDefined(); expect(Object.keys(writtenFiles ?? {})).toEqual(SOURCE_TREE_PATHS); - // The projection freeze receives the exact source the asset carries — - // this is what makes the created definition launchable (CL-6447). - expect(freezer.freezes).toHaveLength(1); - expect(freezer.freezes[0]?.assetId).toBe("ast_1"); - expect(freezer.freezes[0]?.workflowJson).toBe(definitionFrom(writtenFiles)); + // The definition is deployed through the native source pipeline with + // the exact commit the asset write produced — this is what makes the + // created definition launchable (CL-6447, cut over to native deploy + // by CL-7363). + expect(deployer.deploys).toHaveLength(1); + expect(deployer.deploys[0]?.assetId).toBe("ast_1"); + expect(deployer.deploys[0]?.commitSha).toBe("deadbeef"); expect(await skillsStore.getSkills("ast_1")).toEqual([ "web-research", "long-form-write", @@ -928,7 +941,7 @@ test("PUT /:definitionId writes the new system prompt in a single source-tree co assetId: "ast_1", name: "research-buddy", }); - const freezer = recordingDefinitionFreezer(); + const deployer = recordingAgentDefinitionDeployer(); const app = buildApp( fakeAssetService({ readAssetBlob: () => @@ -945,7 +958,7 @@ test("PUT /:definitionId writes the new system prompt in a single source-tree co fakeHistory(), fakeCapabilityInventory, createInMemoryDefinitionSkillsStore(), - freezer, + deployer, ); const response = await put(app, "/def_1", { name: "Research Buddy", @@ -956,14 +969,13 @@ test("PUT /:definitionId writes the new system prompt in a single source-tree co expect(promptFrom(definitionFrom(writtenFiles))).toBe( "You are now a blunt, no-nonsense researcher.", ); - // Saving instructions re-freezes the definition's projection in - // place, so the next launch answers with the edit — and a legacy - // definition frozen without a projection is healed by the same save. - expect(freezer.refreezes).toHaveLength(1); - expect(freezer.refreezes[0]?.definitionId).toBe("def_1"); - expect(promptFrom(freezer.refreezes[0]?.workflowJson ?? "")).toBe( - "You are now a blunt, no-nonsense researcher.", - ); + // Saving instructions redeploys the definition through the native + // source pipeline with the commit the edit produced, so the next + // launch answers with the edit — the native install/probe/gate/freeze + // replaces the old bare-freeze call (CL-7363). + expect(deployer.deploys).toHaveLength(1); + expect(deployer.deploys[0]?.assetId).toBe("ast_1"); + expect(deployer.deploys[0]?.commitSha).toBe("deadbeef"); expect(db.updateCalls).toEqual([ { description: "Research Buddy", updatedAt: expect.any(Date) }, { displayName: "Research Buddy", updatedAt: expect.any(Date) }, @@ -1384,7 +1396,7 @@ test("pinning a skill the registry cannot resolve is a 400, not a 500", async () requireGrant: () => async (_c, next) => { await next(); }, - definitionFreezer: recordingDefinitionFreezer(), + deployer: recordingAgentDefinitionDeployer(), }); const app = new Hono(); app.use("*", async (c, next) => { diff --git a/packages/agent-directory/test/workflow-capability-routes.test.ts b/packages/agent-directory/test/workflow-capability-routes.test.ts index a73a2d5f2..91934803c 100644 --- a/packages/agent-directory/test/workflow-capability-routes.test.ts +++ b/packages/agent-directory/test/workflow-capability-routes.test.ts @@ -130,22 +130,33 @@ const authenticateAsOwnRun: WorkflowRunAuthenticator = { ), }; -/** Records freeze/re-freeze calls instead of running the real - * `@corbits/workflow-freeze` machinery (whose own suites cover the DB - * half); routes here are asserted to invoke it on every content write. */ -function recordingDefinitionFreezer() { - const freezes: { assetId: string; workflowJson: string }[] = []; - const refreezes: { definitionId: string; workflowJson: string }[] = []; +/** Records deploy calls instead of running a real + * `sessionService.deployWorkflowFromSource`; routes here are asserted to + * invoke it, with the commit the write produced, on every content + * write. */ +function recordingAgentDefinitionDeployer() { + const deploys: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }[] = []; return { - freezes, - refreezes, - freeze: (input: { assetId: string; workflowJson: string }) => { - freezes.push(input); - return Promise.resolve({ definitionId: "def_new", wireHash: "hash_1" }); - }, - refreeze: (input: { definitionId: string; workflowJson: string }) => { - refreezes.push(input); - return Promise.resolve({ wireHash: "hash_2" }); + deploys, + deploy: (input: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }) => { + deploys.push(input); + return Promise.resolve({ + deploymentId: "dep_1", + definitionAssetId: input.assetId, + status: "deployed", + }); }, }; } @@ -155,7 +166,7 @@ function buildApp(opts: { authenticator?: WorkflowRunAuthenticator; capabilityInventory?: CapabilityInventoryProvider; skillsStore?: DefinitionSkillsStore; - definitionFreezer?: ReturnType; + deployer?: ReturnType; }): Hono { return createWorkflowCapabilityRoutes({ db: opts.db ?? fakeDb(), @@ -164,7 +175,7 @@ function buildApp(opts: { skillsStore: opts.skillsStore ?? createInMemoryDefinitionSkillsStore(), capabilityInventory: opts.capabilityInventory ?? fakeCapabilityInventory, authenticator: opts.authenticator ?? authenticateAsOwnRun, - definitionFreezer: opts.definitionFreezer ?? recordingDefinitionFreezer(), + deployer: opts.deployer ?? recordingAgentDefinitionDeployer(), }) as unknown as Hono; } @@ -232,7 +243,7 @@ test("a run targeting another definition's capabilities is a 403", async () => { test("a run may add a capability to its own definition without any grant check", async () => { let writtenFiles: Record | undefined; let writtenMessage: string | undefined; - const freezer = recordingDefinitionFreezer(); + const deployer = recordingAgentDefinitionDeployer(); const app = buildApp({ assetService: fakeAssetService({ readAssetBlob: readAssetBlobFor(storedDefinitionBytes()), @@ -242,17 +253,18 @@ test("a run may add a capability to its own definition without any grant check", return Promise.resolve({ commitSha: "deadbeef" }); }, }), - definitionFreezer: freezer, + deployer, }); const response = await postCapability(app, OWN_DEFINITION_ID, { kind: "toolPackage", name: "@corbits/capability-tools", }); expect(response.status).toBe(200); - // The rewrite re-freezes the definition's projection so the next - // launch carries the added capability (CL-6447). - expect(freezer.refreezes).toHaveLength(1); - expect(freezer.refreezes[0]?.definitionId).toBe(OWN_DEFINITION_ID); + // The rewrite redeploys the definition through the native source + // pipeline so the next launch carries the added capability (CL-6447, + // cut over to native deploy by CL-7363). + expect(deployer.deploys).toHaveLength(1); + expect(deployer.deploys[0]?.commitSha).toBe("deadbeef"); expect(Object.keys(writtenFiles ?? {})).toEqual(SOURCE_TREE_PATHS); expect(writtenMessage).toBe( "Add @corbits/capability-tools to research-buddy", diff --git a/packages/agent-directory/test/workflow-create-routes.test.ts b/packages/agent-directory/test/workflow-create-routes.test.ts index 7ee5a5051..7d46bcc0e 100644 --- a/packages/agent-directory/test/workflow-create-routes.test.ts +++ b/packages/agent-directory/test/workflow-create-routes.test.ts @@ -148,22 +148,33 @@ const authenticateAsRun: WorkflowRunAuthenticator = { ), }; -/** Records freeze/re-freeze calls instead of running the real - * `@corbits/workflow-freeze` machinery (whose own suites cover the DB - * half); routes here are asserted to invoke it on every content write. */ -function recordingDefinitionFreezer() { - const freezes: { assetId: string; workflowJson: string }[] = []; - const refreezes: { definitionId: string; workflowJson: string }[] = []; +/** Records deploy calls instead of running a real + * `sessionService.deployWorkflowFromSource`; routes here are asserted to + * invoke it, with the commit the write produced, on every content + * write. */ +function recordingAgentDefinitionDeployer() { + const deploys: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }[] = []; return { - freezes, - refreezes, - freeze: (input: { assetId: string; workflowJson: string }) => { - freezes.push(input); - return Promise.resolve({ definitionId: "def_new", wireHash: "hash_1" }); - }, - refreeze: (input: { definitionId: string; workflowJson: string }) => { - refreezes.push(input); - return Promise.resolve({ wireHash: "hash_2" }); + deploys, + deploy: (input: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }) => { + deploys.push(input); + return Promise.resolve({ + deploymentId: "dep_1", + definitionAssetId: input.assetId, + status: "deployed", + }); }, }; } @@ -177,7 +188,7 @@ function buildApp( skillsStore: opts.skillsStore ?? createInMemoryDefinitionSkillsStore(), capabilityInventory: opts.capabilityInventory ?? fakeCapabilityInventory, authenticator: opts.authenticator ?? authenticateAsRun, - definitionFreezer: opts.definitionFreezer ?? recordingDefinitionFreezer(), + deployer: opts.deployer ?? recordingAgentDefinitionDeployer(), ...(opts.tenantDefaultModel !== undefined ? { tenantDefaultModel: opts.tenantDefaultModel } : {}), diff --git a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts b/packages/agent-directory/test/workflow-skill-pin-routes.test.ts index 1f71fe449..c05b351a8 100644 --- a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts +++ b/packages/agent-directory/test/workflow-skill-pin-routes.test.ts @@ -138,22 +138,33 @@ const authenticateAsTenant1: WorkflowRunAuthenticator = { ), }; -/** Records freeze/re-freeze calls instead of running the real - * `@corbits/workflow-freeze` machinery (whose own suites cover the DB - * half); routes here are asserted to invoke it on every content write. */ -function recordingDefinitionFreezer() { - const freezes: { assetId: string; workflowJson: string }[] = []; - const refreezes: { definitionId: string; workflowJson: string }[] = []; +/** Records deploy calls instead of running a real + * `sessionService.deployWorkflowFromSource`; routes here are asserted to + * invoke it, with the commit the write produced, on every content + * write. */ +function recordingAgentDefinitionDeployer() { + const deploys: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }[] = []; return { - freezes, - refreezes, - freeze: (input: { assetId: string; workflowJson: string }) => { - freezes.push(input); - return Promise.resolve({ definitionId: "def_new", wireHash: "hash_1" }); - }, - refreeze: (input: { definitionId: string; workflowJson: string }) => { - refreezes.push(input); - return Promise.resolve({ wireHash: "hash_2" }); + deploys, + deploy: (input: { + tenantId: string; + principalId: string; + assetId: string; + commitSha: string; + entry: string; + }) => { + deploys.push(input); + return Promise.resolve({ + deploymentId: "dep_1", + definitionAssetId: input.assetId, + status: "deployed", + }); }, }; } @@ -162,7 +173,7 @@ function buildApp(opts: { db?: DB["db"]; authenticator?: WorkflowRunAuthenticator; skillsStore?: DefinitionSkillsStore; - definitionFreezer?: ReturnType; + deployer?: ReturnType; }): Hono { return createWorkflowSkillPinRoutes({ db: opts.db ?? fakeDbWithRows([]), @@ -170,7 +181,7 @@ function buildApp(opts: { skillIndex: fakeSkillIndex, skillsStore: opts.skillsStore ?? createInMemoryDefinitionSkillsStore(), authenticator: opts.authenticator ?? authenticateAsTenant1, - definitionFreezer: opts.definitionFreezer ?? recordingDefinitionFreezer(), + deployer: opts.deployer ?? recordingAgentDefinitionDeployer(), }) as unknown as Hono; } @@ -258,7 +269,7 @@ test("pins a skill onto another definition in the same tenant and re-indexes its let writtenFiles: Record | undefined; let writtenMessage: string | undefined; const skillsStore = createInMemoryDefinitionSkillsStore(); - const freezer = recordingDefinitionFreezer(); + const deployer = recordingAgentDefinitionDeployer(); const app = buildApp({ db: fakeDbWithRows([ { @@ -276,17 +287,18 @@ test("pins a skill onto another definition in the same tenant and re-indexes its }, }), skillsStore, - definitionFreezer: freezer, + deployer, }); const response = await postPin(app, { definitionId: TARGET_DEFINITION_ID, skillName: "research", }); expect(response.status).toBe(200); - // The pin's rewrite re-freezes the projection so the next launch - // advertises the pinned skill (CL-6447). - expect(freezer.refreezes).toHaveLength(1); - expect(freezer.refreezes[0]?.definitionId).toBe(TARGET_DEFINITION_ID); + // The pin's rewrite redeploys through the native source pipeline so + // the next launch advertises the pinned skill (CL-6447, cut over to + // native deploy by CL-7363). + expect(deployer.deploys).toHaveLength(1); + expect(deployer.deploys[0]?.commitSha).toBe("deadbeef"); expect(Object.keys(writtenFiles ?? {})).toEqual(SOURCE_TREE_PATHS); expect(writtenMessage).toBe("Pin research skill to research-buddy"); expect(await skillsStore.getSkills("ast_1")).toEqual(["research"]); From 69c1b6354c343209eb296b5a437c54c52ef62e90 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 1 Sep 2026 23:41:27 -0700 Subject: [PATCH 3/7] Deploy Agent Builder definitions through the native source pipeline Every content-mutating agent-directory route (create, restore, capability add, instructions edit, skills edit, skill pin) used to self-freeze its definition through @corbits/workflow-freeze's DefinitionFreezer, a hub-local path that bypasses the native sidecar probe. Cut over to the same WorkflowDeployer the agent-authored-workflow registry calls: write the source tree, then deploy the resulting commit through install -> sidecar probe -> gate -> freeze. A sidecar-unavailable deploy now returns the same 502 envelope the native deployments route uses, with no fallback to freezing. Removes @corbits/workflow-freeze from agent-directory's dependencies entirely. --- apps/hub/src/index.ts | 14 +-- bun.lock | 18 +-- packages/agent-directory/package.json | 2 +- .../agent-directory/src/agent-workflow.ts | 54 ++++----- .../agent-directory/src/definition-asset.ts | 83 ++++++++++++++ packages/agent-directory/src/routes.ts | 107 ++++++++---------- .../src/workflow-capability-routes.ts | 42 +++---- .../src/workflow-create-routes.ts | 4 +- .../src/workflow-skill-pin-routes.ts | 42 +++---- 9 files changed, 214 insertions(+), 152 deletions(-) diff --git a/apps/hub/src/index.ts b/apps/hub/src/index.ts index bf105564c..cba163978 100644 --- a/apps/hub/src/index.ts +++ b/apps/hub/src/index.ts @@ -196,10 +196,7 @@ import { import { createConnectGithubRoutes } from "@corbits/workflow-catalog/connect-github-routes"; import { createTemplateBlockRoutes } from "@corbits/workflow-catalog/template-block-routes"; import { renderWorkflowSourceTree } from "@corbits/workflow-source"; -import { - createDefinitionFreezer, - freezeInertWorkflowDefinition, -} from "@corbits/workflow-freeze"; +import { freezeInertWorkflowDefinition } from "@corbits/workflow-freeze"; import { createDrizzleDraftStore, createDrizzleRoutineStore, @@ -2034,13 +2031,12 @@ export async function createHub(config: HubConfig) { }, }; - const definitionFreezer = createDefinitionFreezer(db); app.route( `${TENANT_PREFIX}/agent-definitions`, createAgentDefinitionRoutes({ db, assetService, - definitionFreezer, + deployer: workflowDeployer, skillIndex: skills.skillIndex, skillsStore: definitionSkillsStore, history: createDefinitionAssetHistory({ @@ -2070,7 +2066,7 @@ export async function createHub(config: HubConfig) { createWorkflowAgentCreateRoutes({ db, assetService, - definitionFreezer, + deployer: workflowDeployer, skillIndex: skills.skillIndex, skillsStore: definitionSkillsStore, capabilityInventory, @@ -2092,7 +2088,7 @@ export async function createHub(config: HubConfig) { createWorkflowCapabilityRoutes({ db, assetService, - definitionFreezer, + deployer: workflowDeployer, skillIndex: skills.skillIndex, skillsStore: definitionSkillsStore, capabilityInventory, @@ -2109,7 +2105,7 @@ export async function createHub(config: HubConfig) { createWorkflowSkillPinRoutes({ db, assetService, - definitionFreezer, + deployer: workflowDeployer, skillIndex: skills.skillIndex, skillsStore: definitionSkillsStore, authenticator: createWorkflowRunAuthenticator({ db }), diff --git a/bun.lock b/bun.lock index 075630757..596fab809 100644 --- a/bun.lock +++ b/bun.lock @@ -212,12 +212,12 @@ "name": "@corbits/agent-directory", "version": "0.0.1", "dependencies": { + "@corbits/agent-workflow-authoring": "workspace:*", "@corbits/chat": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/folded-run-one-shot": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/workflow-catalog": "workspace:*", - "@corbits/workflow-freeze": "workspace:*", "@corbits/workflow-source": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", @@ -1528,7 +1528,7 @@ }, "packages/workflow-authoring-tools": { "name": "@corbits/workflow-authoring-tools", - "version": "0.0.1", + "version": "0.0.2", "dependencies": { "@intx/agent": "workspace:*", "@intx/types": "workspace:*", @@ -3651,15 +3651,7 @@ "@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], - "@corbits/artifacts-hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@corbits/chat-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/context-menu/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/plugins-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], - - "@corbits/settings-ui/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + "@corbits/memory-hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], "@esbuild-kit/core-utils/esbuild": ["esbuild@0.18.20", "", { "optionalDependencies": { "@esbuild/android-arm": "0.18.20", "@esbuild/android-arm64": "0.18.20", "@esbuild/android-x64": "0.18.20", "@esbuild/darwin-arm64": "0.18.20", "@esbuild/darwin-x64": "0.18.20", "@esbuild/freebsd-arm64": "0.18.20", "@esbuild/freebsd-x64": "0.18.20", "@esbuild/linux-arm": "0.18.20", "@esbuild/linux-arm64": "0.18.20", "@esbuild/linux-ia32": "0.18.20", "@esbuild/linux-loong64": "0.18.20", "@esbuild/linux-mips64el": "0.18.20", "@esbuild/linux-ppc64": "0.18.20", "@esbuild/linux-riscv64": "0.18.20", "@esbuild/linux-s390x": "0.18.20", "@esbuild/linux-x64": "0.18.20", "@esbuild/netbsd-x64": "0.18.20", "@esbuild/openbsd-x64": "0.18.20", "@esbuild/sunos-x64": "0.18.20", "@esbuild/win32-arm64": "0.18.20", "@esbuild/win32-ia32": "0.18.20", "@esbuild/win32-x64": "0.18.20" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA=="], @@ -3683,9 +3675,7 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.6", "", {}, "sha512-BAg6QkE8W+TuQLrrw0Ugr7HegXduRuuj8/ti2kSOc+jz1dmx8/WNcjr6XGnq5YpDWxFwwaavqD0+jIUOKelTsw=="], - "@workbench/hub/@corbits/artifacts": ["@corbits/artifacts@github:corbitsdev/corbits-artifacts#81049ed", { "dependencies": { "@hono/standard-validator": "^0.2.3" }, "peerDependencies": { "@intx/types": "^0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.2", "hono": "^4.12.32", "hono-openapi": "^1.2.0", "postgres": "^3.4.9" } }, "corbitsdev-corbits-artifacts-81049ed", "sha512-oTE0iFDyQdz0ifG1epo39pwaCaYaw19YcKXwfaZqAEQ56a1g9YIozXwH9CG4NaUTwcJKUeYGuNls6oJsMPisCw=="], - - "@workbench/web/@corbits/react-ui": ["@corbits/react-ui@github:corbitsdev/react-ui#3b12281", { "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "tailwind-merge": "^3.3.1" }, "peerDependencies": { "@radix-ui/react-dialog": "^1.1.15", "@radix-ui/react-dropdown-menu": "^2.1.16", "@radix-ui/react-slot": "^1.2.3", "@radix-ui/react-tooltip": "^1.2.8", "@tanstack/react-query": "^5.90.2", "lucide-react": "^0.545.0 || ^1.0.0", "react": "^18.2.0 || ^19.0.0", "react-dom": "^18.2.0 || ^19.0.0", "sonner": "^2.0.7" }, "optionalPeers": ["@tanstack/react-query"] }, "corbitsdev-react-ui-3b12281", "sha512-Abvm/DO0Gqg0ITHGT9355ZxyKRPMVJLSSQSjpd3a8qt4JPrSMOLIOS4sX8ZMNNaArIbnY9F+VKrOWkUJUyO4Nw=="], + "@workbench/hub/@corbits/memory": ["@corbits/memory@github:corbitsdev/corbits-memory#9e6f213", { "dependencies": { "@intx/agent": "0.2.2", "@intx/authz": "0.2.2", "@intx/hub-api": "0.2.2", "@intx/log": "0.2.2", "@intx/workflow": "0.2.2", "arktype": "^2.1.29", "drizzle-orm": "^0.45.1", "hono": "^4.9.0", "hono-openapi": "^1.3.1", "postgres": "^3.4.7" } }, "corbitsdev-corbits-memory-9e6f213", "sha512-utnM4ZT2zmslcPXYWAAqxlDNLcpGsXFiTOtj8h7+OXnhCP0Eaw8yl25+yCTyHpvt3jcdeG4h5uFsSj7ou0BZCA=="], "ajv-formats/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="], diff --git a/packages/agent-directory/package.json b/packages/agent-directory/package.json index 61f72e536..546f3c996 100644 --- a/packages/agent-directory/package.json +++ b/packages/agent-directory/package.json @@ -14,12 +14,12 @@ "test": "bun test" }, "dependencies": { + "@corbits/agent-workflow-authoring": "workspace:*", "@corbits/chat": "workspace:*", "@corbits/error-sink": "workspace:*", "@corbits/folded-run-one-shot": "workspace:*", "@corbits/skills": "workspace:*", "@corbits/workflow-catalog": "workspace:*", - "@corbits/workflow-freeze": "workspace:*", "@corbits/workflow-source": "workspace:*", "@intx/agent": "workspace:*", "@intx/db": "workspace:*", diff --git a/packages/agent-directory/src/agent-workflow.ts b/packages/agent-directory/src/agent-workflow.ts index d7aebb967..4d17da227 100644 --- a/packages/agent-directory/src/agent-workflow.ts +++ b/packages/agent-directory/src/agent-workflow.ts @@ -16,19 +16,22 @@ import { defineWorkflow, step } from "@intx/workflow"; import type { WorkflowDefinition } from "@intx/workflow"; import type { ToolPackagePin } from "@intx/types/tool-packages"; import type { CredentialBinding } from "@intx/types"; -import { and, eq } from "drizzle-orm"; +import { and, desc, eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { asset, workflowDefinition } from "@intx/db/schema"; import { AssetServiceError, DEFAULT_ASSET_REF } from "@intx/hub-sessions"; import type { AssetService } from "@intx/hub-sessions"; -import type { DefinitionFreezer } from "@corbits/workflow-freeze"; import { withAvailableSkills, type PinnedSkillIndexEntry, } from "@corbits/skills"; import { type } from "arktype"; -import { agentDefinitionSourceTree } from "./definition-asset"; +import { + agentDefinitionSourceTree, + writeAndDeployAgentDefinition, + type AgentDefinitionDeployer, +} from "./definition-asset"; import type { DefinitionSkillsStore } from "./skills-store"; export const AGENT_DEFINITION_STEP_ID = "agent"; @@ -386,10 +389,12 @@ export function serializeAgentDefinitionWorkflow( export type CreateAgentDefinitionCoreDeps = { readonly db: DB["db"]; readonly assetService: AssetService; - /** Freezes the definition's wire projection at create; the - * composition root binds `@corbits/workflow-freeze`'s - * `createDefinitionFreezer` to its own `db`. */ - readonly definitionFreezer: Pick; + /** Deploys the definition's commit through the native source pipeline + * (install -> sidecar probe -> gate -> freeze) at create; the + * composition root injects the SAME `WorkflowDeployer` + * `@corbits/agent-workflow-authoring`'s registry calls, wrapping + * `sessionService.deployWorkflowFromSource`. */ + readonly deployer: AgentDefinitionDeployer; readonly skillIndex: { resolve( tenantId: string, @@ -550,36 +555,33 @@ export async function createAgentDefinitionCore( } } - await deps.assetService.populateAsset({ - assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ handle: input.handle, workflowJson }), - message: `Define agent ${input.name}`, - }, - }); - await deps.skillsStore.setSkills(assetId, input.skills); - - // Freeze, not a bare ensure: `ensureWorkflowDefinitionForAsset` alone - // leaves the version row's `wire_projection` NULL, and a definition - // without a frozen projection can never launch (CL-6447's 409 - // `not_launchable`). The freeze projects, walks, and stamps in one - // transaction — the same machinery the sidecar probe deploy rides. - const { definitionId } = await deps.definitionFreezer.freeze({ + await writeAndDeployAgentDefinition({ + assetService: deps.assetService, + deployer: deps.deployer, + tenantId: input.tenantId, + principalId: input.principalId, assetId, + handle: input.handle, workflowJson, + message: `Define agent ${input.name}`, }); + await deps.skillsStore.setSkills(assetId, input.skills); + // The deploy above projects, walks, and stamps the definition row in + // one transaction — the same machinery the sidecar probe deploy + // rides. Read the row back by asset, newest first: a content-unchanged + // redeploy dedupes onto the existing `(assetId, wireHash)` row, so + // this still resolves to the one row a fresh create just produced. const row = await deps.db.query.workflowDefinition.findFirst({ where: and( - eq(workflowDefinition.id, definitionId), + eq(workflowDefinition.assetId, assetId), eq(workflowDefinition.tenantId, input.tenantId), ), + orderBy: desc(workflowDefinition.createdAt), }); if (row === undefined) { throw new Error( - `agent definition "${definitionId}" was created but is not readable back`, + `agent definition for asset "${assetId}" was created but is not readable back`, ); } return { row }; diff --git a/packages/agent-directory/src/definition-asset.ts b/packages/agent-directory/src/definition-asset.ts index ee1ca2630..9aefd65a5 100644 --- a/packages/agent-directory/src/definition-asset.ts +++ b/packages/agent-directory/src/definition-asset.ts @@ -15,14 +15,22 @@ import { readWorkflowSourceDefinition, renderWorkflowSourceTree, RetiredWorkflowEnvelopeError, + WORKFLOW_SOURCE_ENTRY, WORKFLOW_SOURCE_ENTRY_PATH, type WorkflowSourceBlobReader, type WorkflowSourceTree, } from "@corbits/workflow-source"; +import { DEFAULT_ASSET_REF } from "@intx/hub-sessions"; +import type { AssetService } from "@intx/hub-sessions"; +import { + WorkflowAuthorError, + type WorkflowDeployer, +} from "@corbits/agent-workflow-authoring"; export { RetiredWorkflowEnvelopeError, WORKFLOW_SOURCE_ENTRY_PATH as AGENT_DEFINITION_ENTRY_PATH, + WORKFLOW_SOURCE_ENTRY as AGENT_DEFINITION_ENTRY, }; const AGENT_PACKAGE_SCOPE = "@workbench-agent"; @@ -56,3 +64,78 @@ export function parseAgentDefinitionEntry( assetId, ); } + +/** The `WorkflowDeployer` seam this package needs — never the whole + * registry surface, just the one call that deploys a commit through the + * native source pipeline (install -> sidecar probe -> gate -> freeze). + * The composition root (`apps/hub`) injects the SAME deployer + * `@corbits/agent-workflow-authoring`'s own registry calls; this + * package never reimplements install/probe/gate/freeze itself. */ +export type AgentDefinitionDeployer = Pick; + +/** + * Writes a definition's serialized workflow into its asset tree, then + * deploys the resulting commit through the native source pipeline — the + * one sequence every content-mutating route in this package needs + * (create, restore, capability add, instructions edit, skills edit). + * Replaces the old write-then-`DefinitionFreezer.freeze`/`refreeze` + * pair: a deploy IS a freeze, plus the install/probe/gate a bare freeze + * skipped. Throws `WorkflowAuthorError` on rejection — `not_found`, + * `invalid` (rejected package/definition), or `unavailable` (sidecar + * unreachable) — for the caller's route to translate into its response. + */ +export async function writeAndDeployAgentDefinition(args: { + assetService: AssetService; + deployer: AgentDefinitionDeployer; + tenantId: string; + principalId: string; + assetId: string; + handle: string; + workflowJson: string; + message: string; +}): Promise<{ commitSha: string }> { + const { commitSha } = await args.assetService.populateAsset({ + assetId: args.assetId, + ref: DEFAULT_ASSET_REF, + principal: { kind: "hub" }, + tree: { + files: agentDefinitionSourceTree({ + handle: args.handle, + workflowJson: args.workflowJson, + }), + message: args.message, + }, + }); + await args.deployer.deploy({ + tenantId: args.tenantId, + principalId: args.principalId, + assetId: args.assetId, + commitSha, + entry: WORKFLOW_SOURCE_ENTRY, + }); + return { commitSha }; +} + +/** The HTTP status a `WorkflowAuthorError` from `writeAndDeployAgentDefinition` + * should surface as — the same mapping `@corbits/agent-workflow-authoring`'s + * own `workflow-routes.ts` uses for the native deploy surface, reused here + * so a sidecar-unavailable deploy reads as the same 502 envelope shape + * everywhere a deploy can fail. */ +export function statusForAgentDefinitionDeployError( + reason: WorkflowAuthorError["reason"], +): 400 | 403 | 404 | 409 | 502 { + switch (reason) { + case "not_found": + return 404; + case "forbidden": + return 403; + case "conflict": + return 409; + case "invalid": + return 400; + case "unavailable": + return 502; + } +} + +export { WorkflowAuthorError }; diff --git a/packages/agent-directory/src/routes.ts b/packages/agent-directory/src/routes.ts index d22b32b60..2b1453688 100644 --- a/packages/agent-directory/src/routes.ts +++ b/packages/agent-directory/src/routes.ts @@ -30,7 +30,6 @@ import { type PinnedSkillIndexEntry, } from "@corbits/skills"; import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import type { DefinitionFreezer } from "@corbits/workflow-freeze"; import { createAgentDefinitionCore, @@ -51,6 +50,10 @@ import { parseAgentDefinitionEntry, readAgentDefinitionWorkflowJson, RetiredWorkflowEnvelopeError, + statusForAgentDefinitionDeployError, + writeAndDeployAgentDefinition, + WorkflowAuthorError, + type AgentDefinitionDeployer, } from "./definition-asset"; import type { DefinitionSkillsStore } from "./skills-store"; import { @@ -93,11 +96,11 @@ export type CreateAgentDefinitionRoutesDeps = { history: DefinitionAssetHistory; capabilityInventory: CapabilityInventoryProvider; requireGrant: RequireGrant; - /** Freezes/re-freezes the definition's wire projection on every - * content write; the composition root binds - * `@corbits/workflow-freeze`'s `createDefinitionFreezer` to its own - * `db`. */ - definitionFreezer: DefinitionFreezer; + /** Deploys the definition's commit through the native source pipeline + * on every content write; the composition root injects the SAME + * `WorkflowDeployer` `@corbits/agent-workflow-authoring`'s registry + * calls. */ + deployer: AgentDefinitionDeployer; tenantDefaultModel?: CreateAgentDefinitionCoreDeps["tenantDefaultModel"]; }; @@ -138,7 +141,7 @@ export function createAgentDefinitionRoutes({ history, capabilityInventory, requireGrant, - definitionFreezer, + deployer, tenantDefaultModel, }: CreateAgentDefinitionRoutesDeps): Hono { const app = new Hono(); @@ -166,6 +169,12 @@ export function createAgentDefinitionRoutes({ 409, ); } + if (err instanceof WorkflowAuthorError) { + return c.json( + makeErrorEnvelope({ code: err.reason, userMessage: err.message }), + statusForAgentDefinitionDeployError(err.reason), + ); + } throw err; }); @@ -220,7 +229,7 @@ export function createAgentDefinitionRoutes({ assetService, skillIndex, skillsStore, - definitionFreezer, + deployer, ...(tenantDefaultModel !== undefined ? { tenantDefaultModel } : {}), }, coreInput, @@ -433,6 +442,7 @@ export function createAgentDefinitionRoutes({ } const tenant = c.get("tenant"); + const principal = c.get("principal"); const definitionId = c.req.param("definitionId"); const row = await db.query.workflowDefinition.findFirst({ where: and( @@ -468,21 +478,15 @@ export function createAgentDefinitionRoutes({ // rewrites the definition's source tree — the definition's // currently pinned skills are untouched by restoring an earlier // instructions revision. - await assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService, + deployer, + tenantId: tenant.id, + principalId: principal.id, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: restoredWorkflowJson, - }), - message: `Restore agent ${row.name} to ${body.commitSha.slice(0, 8)}`, - }, - }); - await definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: restoredWorkflowJson, + message: `Restore agent ${row.name} to ${body.commitSha.slice(0, 8)}`, }); const capabilities = readAgentCapabilities(restoredWorkflowJson); @@ -576,21 +580,15 @@ export function createAgentDefinitionRoutes({ } } - await assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService, + deployer, + tenantId: tenant.id, + principalId: principal.id, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: nextWorkflowJson, - }), - message, - }, - }); - await definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: nextWorkflowJson, + message, }); if (nextSkills !== null) { await skillsStore.setSkills(row.assetId, nextSkills); @@ -623,6 +621,7 @@ export function createAgentDefinitionRoutes({ } const tenant = c.get("tenant"); + const principal = c.get("principal"); const definitionId = c.req.param("definitionId"); const row = await db.query.workflowDefinition.findFirst({ where: and( @@ -647,21 +646,15 @@ export function createAgentDefinitionRoutes({ workflowJson, body.systemPrompt, ); - await assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService, + deployer, + tenantId: tenant.id, + principalId: principal.id, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: nextWorkflowJson, - }), - message: `Update agent instructions for ${row.name}`, - }, - }); - await definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: nextWorkflowJson, + message: `Update agent instructions for ${row.name}`, }); const now = new Date(); @@ -870,21 +863,15 @@ export function createAgentDefinitionRoutes({ workflowJson, await skillIndex.resolve(tenant.id, principal.id, body.skills), ); - await assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService, + deployer, + tenantId: tenant.id, + principalId: principal.id, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: nextWorkflowJson, - }), - message: `Update agent skills for ${row.name}`, - }, - }); - await definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: nextWorkflowJson, + message: `Update agent skills for ${row.name}`, }); await skillsStore.setSkills(row.assetId, body.skills); diff --git a/packages/agent-directory/src/workflow-capability-routes.ts b/packages/agent-directory/src/workflow-capability-routes.ts index 58fcafd60..b2a68b707 100644 --- a/packages/agent-directory/src/workflow-capability-routes.ts +++ b/packages/agent-directory/src/workflow-capability-routes.ts @@ -44,11 +44,9 @@ import { Hono } from "hono"; import type { DB } from "@intx/db"; import { workflowDefinition, workflowRun } from "@intx/db/schema"; -import { DEFAULT_ASSET_REF } from "@intx/hub-sessions"; import type { AssetService } from "@intx/hub-sessions"; import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import type { DefinitionFreezer } from "@corbits/workflow-freeze"; import { reindexPinnedSkills, @@ -63,9 +61,12 @@ import { type CapabilityInventoryProvider, } from "./capability-inventory"; import { - agentDefinitionSourceTree, readAgentDefinitionWorkflowJson, RetiredWorkflowEnvelopeError, + statusForAgentDefinitionDeployError, + writeAndDeployAgentDefinition, + WorkflowAuthorError, + type AgentDefinitionDeployer, } from "./definition-asset"; import type { PinnedSkillIndexResolver } from "./routes"; import type { DefinitionSkillsStore } from "./skills-store"; @@ -123,10 +124,11 @@ export type CreateWorkflowCapabilityRoutesDeps = { skillsStore: DefinitionSkillsStore; capabilityInventory: CapabilityInventoryProvider; authenticator: WorkflowRunAuthenticator; - /** Re-freezes the definition's wire projection after the rewrite; the - * composition root binds `@corbits/workflow-freeze`'s - * `createDefinitionFreezer` to its own `db`. */ - definitionFreezer: Pick; + /** Deploys the definition's commit through the native source pipeline + * after the rewrite; the composition root injects the SAME + * `WorkflowDeployer` `@corbits/agent-workflow-authoring`'s registry + * calls. */ + deployer: AgentDefinitionDeployer; }; export function createWorkflowCapabilityRoutes( @@ -147,6 +149,12 @@ export function createWorkflowCapabilityRoutes( 409, ); } + if (err instanceof WorkflowAuthorError) { + return c.json( + makeErrorEnvelope({ code: err.reason, userMessage: err.message }), + statusForAgentDefinitionDeployError(err.reason), + ); + } throw err; }); @@ -276,21 +284,15 @@ export function createWorkflowCapabilityRoutes( } } - await deps.assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService: deps.assetService, + deployer: deps.deployer, + tenantId: scope.tenantId, + principalId: scope.principalId, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: nextWorkflowJson, - }), - message, - }, - }); - await deps.definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: nextWorkflowJson, + message, }); if (nextSkills !== null) { await deps.skillsStore.setSkills(row.assetId, nextSkills); diff --git a/packages/agent-directory/src/workflow-create-routes.ts b/packages/agent-directory/src/workflow-create-routes.ts index 5e92b412d..b5079a6b2 100644 --- a/packages/agent-directory/src/workflow-create-routes.ts +++ b/packages/agent-directory/src/workflow-create-routes.ts @@ -105,7 +105,7 @@ export type CreateWorkflowAgentCreateRoutesDeps = { readonly skillsStore: CreateAgentDefinitionCoreDeps["skillsStore"]; readonly capabilityInventory: CapabilityInventoryProvider; readonly authenticator: WorkflowRunAuthenticator; - readonly definitionFreezer: CreateAgentDefinitionCoreDeps["definitionFreezer"]; + readonly deployer: CreateAgentDefinitionCoreDeps["deployer"]; readonly tenantDefaultModel?: CreateAgentDefinitionCoreDeps["tenantDefaultModel"]; }; @@ -249,7 +249,7 @@ export function createWorkflowAgentCreateRoutes( assetService: deps.assetService, skillIndex: deps.skillIndex, skillsStore: deps.skillsStore, - definitionFreezer: deps.definitionFreezer, + deployer: deps.deployer, ...(deps.tenantDefaultModel !== undefined ? { tenantDefaultModel: deps.tenantDefaultModel } : {}), diff --git a/packages/agent-directory/src/workflow-skill-pin-routes.ts b/packages/agent-directory/src/workflow-skill-pin-routes.ts index 979384e4a..f12a9cdbf 100644 --- a/packages/agent-directory/src/workflow-skill-pin-routes.ts +++ b/packages/agent-directory/src/workflow-skill-pin-routes.ts @@ -27,17 +27,18 @@ import { Hono } from "hono"; import type { DB } from "@intx/db"; import { workflowDefinition } from "@intx/db/schema"; -import { DEFAULT_ASSET_REF } from "@intx/hub-sessions"; import type { AssetService } from "@intx/hub-sessions"; import { isWorkbenchHostDefinitionName } from "@corbits/chat/workbench-host-naming"; -import type { DefinitionFreezer } from "@corbits/workflow-freeze"; import { reindexPinnedSkills } from "./agent-workflow"; import { - agentDefinitionSourceTree, readAgentDefinitionWorkflowJson, RetiredWorkflowEnvelopeError, + statusForAgentDefinitionDeployError, + writeAndDeployAgentDefinition, + WorkflowAuthorError, + type AgentDefinitionDeployer, } from "./definition-asset"; import type { PinnedSkillIndexResolver } from "./routes"; import type { DefinitionSkillsStore } from "./skills-store"; @@ -91,10 +92,11 @@ export type CreateWorkflowSkillPinRoutesDeps = { skillIndex: PinnedSkillIndexResolver; skillsStore: DefinitionSkillsStore; authenticator: WorkflowRunAuthenticator; - /** Re-freezes the definition's wire projection after the rewrite; the - * composition root binds `@corbits/workflow-freeze`'s - * `createDefinitionFreezer` to its own `db`. */ - definitionFreezer: Pick; + /** Deploys the definition's commit through the native source pipeline + * after the rewrite; the composition root injects the SAME + * `WorkflowDeployer` `@corbits/agent-workflow-authoring`'s registry + * calls. */ + deployer: AgentDefinitionDeployer; }; export function createWorkflowSkillPinRoutes( @@ -112,6 +114,12 @@ export function createWorkflowSkillPinRoutes( 409, ); } + if (err instanceof WorkflowAuthorError) { + return c.json( + makeErrorEnvelope({ code: err.reason, userMessage: err.message }), + statusForAgentDefinitionDeployError(err.reason), + ); + } throw err; }); @@ -177,21 +185,15 @@ export function createWorkflowSkillPinRoutes( ), ); - await deps.assetService.populateAsset({ + await writeAndDeployAgentDefinition({ + assetService: deps.assetService, + deployer: deps.deployer, + tenantId: scope.tenantId, + principalId: scope.principalId, assetId: row.assetId, - ref: DEFAULT_ASSET_REF, - principal: { kind: "hub" }, - tree: { - files: agentDefinitionSourceTree({ - handle: row.name, - workflowJson: nextWorkflowJson, - }), - message: `Pin ${body.skillName} skill to ${row.name}`, - }, - }); - await deps.definitionFreezer.refreeze({ - definitionId: row.id, + handle: row.name, workflowJson: nextWorkflowJson, + message: `Pin ${body.skillName} skill to ${row.name}`, }); await deps.skillsStore.setSkills(row.assetId, nextSkills); From 6457394cbac8943fac3fa3799f01ee3db1e79a2d Mon Sep 17 00:00:00 2001 From: Sawyer Date: Tue, 1 Sep 2026 23:41:32 -0700 Subject: [PATCH 4/7] Update docs: agent creation deploys natively, not a bare freeze AGENTS-PAGE.md's "Creating an agent" steps described a workflow.json write followed by ensureWorkflowDefinitionForAsset; describe the actual source-tree write, native deploy, and resulting frozen definition instead, and note that a pre-CL-7363 definition stays launchable as-is and only redeploys natively on its next edit. --- docs/AGENTS-PAGE.md | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/AGENTS-PAGE.md b/docs/AGENTS-PAGE.md index d35483187..871ca8892 100644 --- a/docs/AGENTS-PAGE.md +++ b/docs/AGENTS-PAGE.md @@ -37,18 +37,32 @@ description) and definition (system prompt, model) and posts to `POST /api/tenants/:tenantId/agent-definitions`, added by `@corbits/agent-directory`. The route: -1. Builds a single-step, folded `workflow.json` from the submitted fields - (`buildAgentDefinitionWorkflow`) — the same shape +1. Builds a single-step, folded workflow definition from the submitted + fields (`buildAgentDefinitionWorkflow`) — the same shape `@corbits/assistant-workflow` and `@corbits/chat`'s workbench host produce, - parametrized instead of fixed. -2. Creates a `workflow`-kind asset and writes that JSON into it in-process - (`AssetService.populateAsset` — no git subprocess). -3. Projects a first-class `workflow_definition` row over the asset - (`ensureWorkflowDefinitionForAsset`). + parametrized instead of fixed — and renders it as a source codebase + (`@corbits/workflow-source`'s `renderWorkflowSourceTree`), never a bare + `workflow.json` envelope. +2. Creates a `workflow`-kind asset and writes that source tree into it + in-process (`AssetService.populateAsset` — no git subprocess), which + produces a commit. +3. Deploys that commit through Interchange's native source pipeline + (install -> sidecar probe -> gate -> freeze) via the same + `WorkflowDeployer` `@corbits/agent-workflow-authoring`'s + agent-authored-workflow registry calls, which projects the first-class + `workflow_definition` row over the asset (CL-7363). The definition lands with the schema's default status (`deployed`) and a materialized asset, so it is immediately invitable and launchable — no -separate deploy step, and no page reload needed to see it appear. +separate deploy step, and no page reload needed to see it appear. Every +subsequent edit (instructions, model, tools, skills, restore) writes a new +commit and redeploys the same way; a definition frozen before CL-7363 +stays launchable as-is and only redeploys through the native pipeline on +its next edit — no data migration. + +A deploy that finds no connected sidecar fails the write outright (502, +`unavailable`) rather than falling back to the old bare-freeze path — no +fallback, per this repo's ground rules. **Tools and a model provider are not exposed on the create form.** The platform's wire contract for a workflow definition From 6bfcd8e4b8953b45ec57f92180edb0737ef07619 Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 01:48:20 -0700 Subject: [PATCH 5/7] Address review findings (CL-7363) --- packages/agent-directory/src/definition-asset.ts | 1 + packages/agent-directory/test/routes.integration.test.ts | 4 +++- packages/agent-directory/test/routes.test.ts | 4 +++- .../agent-directory/test/workflow-capability-routes.test.ts | 4 +++- packages/agent-directory/test/workflow-create-routes.test.ts | 4 +++- .../agent-directory/test/workflow-skill-pin-routes.test.ts | 4 +++- 6 files changed, 16 insertions(+), 5 deletions(-) diff --git a/packages/agent-directory/src/definition-asset.ts b/packages/agent-directory/src/definition-asset.ts index 9aefd65a5..f9484afa3 100644 --- a/packages/agent-directory/src/definition-asset.ts +++ b/packages/agent-directory/src/definition-asset.ts @@ -110,6 +110,7 @@ export async function writeAndDeployAgentDefinition(args: { tenantId: args.tenantId, principalId: args.principalId, assetId: args.assetId, + assetName: args.handle, commitSha, entry: WORKFLOW_SOURCE_ENTRY, }); diff --git a/packages/agent-directory/test/routes.integration.test.ts b/packages/agent-directory/test/routes.integration.test.ts index 6ce799b72..95456c1ba 100644 --- a/packages/agent-directory/test/routes.integration.test.ts +++ b/packages/agent-directory/test/routes.integration.test.ts @@ -117,6 +117,7 @@ function recordingAgentDefinitionDeployer(db: ReturnType["db"]) tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }[] = []; @@ -126,6 +127,7 @@ function recordingAgentDefinitionDeployer(db: ReturnType["db"]) tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }) => { @@ -143,7 +145,7 @@ function recordingAgentDefinitionDeployer(db: ReturnType["db"]) return { deploymentId: "dep_1", definitionAssetId: input.assetId, - status: "deployed", + status: "deployed" as const, }; }, }; diff --git a/packages/agent-directory/test/routes.test.ts b/packages/agent-directory/test/routes.test.ts index 8c714ed8c..293ec5b00 100644 --- a/packages/agent-directory/test/routes.test.ts +++ b/packages/agent-directory/test/routes.test.ts @@ -272,6 +272,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }[] = []; @@ -281,6 +282,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }) => { @@ -288,7 +290,7 @@ function recordingAgentDefinitionDeployer() { return Promise.resolve({ deploymentId: "dep_1", definitionAssetId: input.assetId, - status: "deployed", + status: "deployed" as const, }); }, }; diff --git a/packages/agent-directory/test/workflow-capability-routes.test.ts b/packages/agent-directory/test/workflow-capability-routes.test.ts index 91934803c..89af7e90f 100644 --- a/packages/agent-directory/test/workflow-capability-routes.test.ts +++ b/packages/agent-directory/test/workflow-capability-routes.test.ts @@ -139,6 +139,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }[] = []; @@ -148,6 +149,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }) => { @@ -155,7 +157,7 @@ function recordingAgentDefinitionDeployer() { return Promise.resolve({ deploymentId: "dep_1", definitionAssetId: input.assetId, - status: "deployed", + status: "deployed" as const, }); }, }; diff --git a/packages/agent-directory/test/workflow-create-routes.test.ts b/packages/agent-directory/test/workflow-create-routes.test.ts index 7d46bcc0e..599ae8927 100644 --- a/packages/agent-directory/test/workflow-create-routes.test.ts +++ b/packages/agent-directory/test/workflow-create-routes.test.ts @@ -157,6 +157,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }[] = []; @@ -166,6 +167,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }) => { @@ -173,7 +175,7 @@ function recordingAgentDefinitionDeployer() { return Promise.resolve({ deploymentId: "dep_1", definitionAssetId: input.assetId, - status: "deployed", + status: "deployed" as const, }); }, }; diff --git a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts b/packages/agent-directory/test/workflow-skill-pin-routes.test.ts index c05b351a8..96ab0aa50 100644 --- a/packages/agent-directory/test/workflow-skill-pin-routes.test.ts +++ b/packages/agent-directory/test/workflow-skill-pin-routes.test.ts @@ -147,6 +147,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }[] = []; @@ -156,6 +157,7 @@ function recordingAgentDefinitionDeployer() { tenantId: string; principalId: string; assetId: string; + assetName: string; commitSha: string; entry: string; }) => { @@ -163,7 +165,7 @@ function recordingAgentDefinitionDeployer() { return Promise.resolve({ deploymentId: "dep_1", definitionAssetId: input.assetId, - status: "deployed", + status: "deployed" as const, }); }, }; From 44b9206606f88aab6d29ba8bcd414f9b0f50c2fe Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 02:31:26 -0700 Subject: [PATCH 6/7] Fix CI after review pass (CL-7363) --- .../agent-directory/src/agent-workflow.ts | 3 +-- .../src/native-deploy-cutover.test.ts | 6 +++--- .../test/routes.integration.test.ts | 20 +++++++++++++------ 3 files changed, 18 insertions(+), 11 deletions(-) diff --git a/packages/agent-directory/src/agent-workflow.ts b/packages/agent-directory/src/agent-workflow.ts index 4d17da227..ed1a030a3 100644 --- a/packages/agent-directory/src/agent-workflow.ts +++ b/packages/agent-directory/src/agent-workflow.ts @@ -19,7 +19,7 @@ import type { CredentialBinding } from "@intx/types"; import { and, desc, eq } from "drizzle-orm"; import type { DB } from "@intx/db"; import { asset, workflowDefinition } from "@intx/db/schema"; -import { AssetServiceError, DEFAULT_ASSET_REF } from "@intx/hub-sessions"; +import { AssetServiceError } from "@intx/hub-sessions"; import type { AssetService } from "@intx/hub-sessions"; import { withAvailableSkills, @@ -28,7 +28,6 @@ import { import { type } from "arktype"; import { - agentDefinitionSourceTree, writeAndDeployAgentDefinition, type AgentDefinitionDeployer, } from "./definition-asset"; diff --git a/packages/agent-directory/src/native-deploy-cutover.test.ts b/packages/agent-directory/src/native-deploy-cutover.test.ts index 58783034e..3fd74efaa 100644 --- a/packages/agent-directory/src/native-deploy-cutover.test.ts +++ b/packages/agent-directory/src/native-deploy-cutover.test.ts @@ -34,8 +34,8 @@ describe("workflow-freeze cutover", () => { const packageJson = JSON.parse( readFileSync(path.join(import.meta.dir, "../package.json"), "utf8"), ) as { dependencies?: Record }; - expect( - Object.keys(packageJson.dependencies ?? {}), - ).not.toContain("@corbits/workflow-freeze"); + expect(Object.keys(packageJson.dependencies ?? {})).not.toContain( + "@corbits/workflow-freeze", + ); }); }); diff --git a/packages/agent-directory/test/routes.integration.test.ts b/packages/agent-directory/test/routes.integration.test.ts index 95456c1ba..5af223f33 100644 --- a/packages/agent-directory/test/routes.integration.test.ts +++ b/packages/agent-directory/test/routes.integration.test.ts @@ -112,7 +112,9 @@ async function post(app: Hono, body: unknown): Promise { * the stub still projects a `workflow_definition` row directly (the one * piece of the real deploy every route's read-back depends on) rather * than faking the whole install/probe/gate/freeze pipeline. */ -function recordingAgentDefinitionDeployer(db: ReturnType["db"]) { +function recordingAgentDefinitionDeployer( + db: ReturnType["db"], +) { const deploys: { tenantId: string; principalId: string; @@ -259,6 +261,12 @@ describeIfDb("agent-directory routes against a real assetService", () => { }); test("a created definition deploys its commit through the native source pipeline (CL-7363)", async () => { + // `deployer.deploys` is shared across this describe block's tests, so + // scope to what THIS test appends rather than the array's raw length — + // earlier tests deploy their own definitions too. + const deploysBefore = deployer.deploys.length; + const deploysSoFar = () => deployer.deploys.slice(deploysBefore); + const handle = `launchable-${suffix}`; const created = await post(app, { name: "Launchable", @@ -272,8 +280,8 @@ describeIfDb("agent-directory routes against a real assetService", () => { // produced — the same sequence a launch depends on being launchable // (CL-6447), now driven through the native install/probe/gate/freeze // pipeline instead of a bare freeze. - expect(deployer.deploys).toHaveLength(1); - const [firstDeploy] = deployer.deploys; + expect(deploysSoFar()).toHaveLength(1); + const [firstDeploy] = deploysSoFar(); expect(firstDeploy?.tenantId).toBe(TENANT.id); expect(firstDeploy?.commitSha).toBeDefined(); @@ -289,8 +297,8 @@ describeIfDb("agent-directory routes against a real assetService", () => { }), }); expect(updated.status).toBe(200); - expect(deployer.deploys).toHaveLength(2); - expect(deployer.deploys[1]?.assetId).toBe(firstDeploy?.assetId); - expect(deployer.deploys[1]?.commitSha).not.toBe(firstDeploy?.commitSha); + expect(deploysSoFar()).toHaveLength(2); + expect(deploysSoFar()[1]?.assetId).toBe(firstDeploy?.assetId); + expect(deploysSoFar()[1]?.commitSha).not.toBe(firstDeploy?.commitSha); }); }); From b3aa7968a4ccfac1994f1924bc3f5bbe5c19a41c Mon Sep 17 00:00:00 2001 From: Sawyer Date: Wed, 2 Sep 2026 03:56:06 -0700 Subject: [PATCH 7/7] Remove duplicated retarget-authorization check (CL-7363) --- packages/routines/src/routes.ts | 21 --------------------- 1 file changed, 21 deletions(-) diff --git a/packages/routines/src/routes.ts b/packages/routines/src/routes.ts index 04bf85276..8567d38a4 100644 --- a/packages/routines/src/routes.ts +++ b/packages/routines/src/routes.ts @@ -889,27 +889,6 @@ export function createRoutineRoutes( const effectiveDefinitionAssetId = body.definitionAssetId ?? existing.definitionAssetId; - if ( - body.definitionAssetId !== undefined && - body.definitionAssetId !== existing.definitionAssetId - ) { - const rejection = await rejectUnlaunchableTarget( - deps, - tenant.id, - principal.id, - body.definitionAssetId, - ); - if (rejection !== undefined) { - return c.json( - makeErrorEnvelope({ - code: rejection.code, - userMessage: rejection.userMessage, - }), - rejection.status, - ); - } - } - if ( body.trigger !== undefined && !(await webhookTriggerValid(