From 8ea0f81a53334ef6b1766f34aa62233beff7e3e9 Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Fri, 24 Jul 2026 11:30:26 +0200 Subject: [PATCH 1/2] FE-1264: Block landing in comparison runs --- .../executor/__tests__/execute-land.test.ts | 48 ++++++++++++++++++- .../extensions/executor/execute-land/index.ts | 23 +++++++-- src/app/__tests__/pi-runtime.test.ts | 27 +++++++++++ src/app/brunch-tui.ts | 1 + src/app/pi-extensions.ts | 9 +++- 5 files changed, 103 insertions(+), 5 deletions(-) diff --git a/src/.pi/extensions/executor/__tests__/execute-land.test.ts b/src/.pi/extensions/executor/__tests__/execute-land.test.ts index c9ba5efaa..535c2dd85 100644 --- a/src/.pi/extensions/executor/__tests__/execute-land.test.ts +++ b/src/.pi/extensions/executor/__tests__/execute-land.test.ts @@ -7,7 +7,11 @@ import { describe, expect, it } from 'vitest'; import { createFakeGitHostLandPort } from '../../../../executor/__tests__/fake-ports.js'; import { promotionReportPath } from '../../../../executor/promotion.js'; import { runMetadataPath, runDirPath } from '../../../../executor/run.js'; -import { createExecuteLandPreflightTool, runBrunchLandCommand } from '../execute-land/index.js'; +import { + createExecuteLandPreflightTool, + registerBrunchExecuteLand, + runBrunchLandCommand, +} from '../execute-land/index.js'; const TIP = 'tip456'; @@ -80,6 +84,29 @@ async function runStatus(cwd: string, runId = 'run-1'): Promise { } describe('runBrunchLandCommand', () => { + it('refuses landing under comparison policy before inspection or confirmation', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-land-cmd-comparison-')); + await createPromotionPreparedRun(cwd); + const ui = makeUi(); + const inspectCalls: unknown[] = []; + const gitHostLand = createFakeGitHostLandPort({ + async inspect(args) { + inspectCalls.push(args); + return { status: 'failed', message: 'must not inspect', sideEffects: [] }; + }, + }); + + await runBrunchLandCommand('run-1', stubCtx(cwd, ui), { + gitHostLand, + allowHostLanding: false, + }); + + expect(inspectCalls).toEqual([]); + expect(ui.confirms).toEqual([]); + expect(ui.notifications).toEqual(['Landing is disabled for isolated execution comparisons.']); + await expect(runStatus(cwd)).resolves.toBe('promotion_prepared'); + }); + it('lands an explicit run after the user confirms', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-land-cmd-confirm-')); await createPromotionPreparedRun(cwd); @@ -412,6 +439,25 @@ describe('runBrunchLandCommand', () => { }); }); +describe('registerBrunchExecuteLand', () => { + it('registers no landing surface when host landing is disabled', () => { + const commands: string[] = []; + const tools: string[] = []; + + registerBrunchExecuteLand( + { + registerCommand: (name: string) => commands.push(name), + registerTool: (tool: { name: string }) => tools.push(tool.name), + } as never, + createFakeGitHostLandPort(), + { allowHostLanding: false }, + ); + + expect(commands).toEqual([]); + expect(tools).toEqual([]); + }); +}); + describe('createExecuteLandPreflightTool', () => { it('renders landing readiness read-only and points at /brunch:land', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-land-preflight-tool-')); diff --git a/src/.pi/extensions/executor/execute-land/index.ts b/src/.pi/extensions/executor/execute-land/index.ts index dfafc20ee..62ef746b2 100644 --- a/src/.pi/extensions/executor/execute-land/index.ts +++ b/src/.pi/extensions/executor/execute-land/index.ts @@ -60,11 +60,20 @@ export interface LandCommandContext { }; } +export interface HostLandingPolicyOptions { + /** Defaults on; isolated execution comparisons disable every landing surface. */ + readonly allowHostLanding?: boolean; +} + export async function runBrunchLandCommand( args: string, ctx: LandCommandContext, - deps: { readonly gitHostLand: GitHostLandPort }, + deps: { readonly gitHostLand: GitHostLandPort } & HostLandingPolicyOptions, ): Promise { + if (deps.allowHostLanding === false) { + ctx.ui.notify('Landing is disabled for isolated execution comparisons.', 'warning'); + return; + } if (!ctx.hasUI) { ctx.ui.notify('/brunch:land requires an interactive session to confirm the landing.', 'error'); return; @@ -160,12 +169,20 @@ function splitLandCommandArgs(args: string): readonly [string | undefined, strin return [trimmed.slice(0, boundary), target || undefined]; } -export function registerBrunchExecuteLand(pi: ExtensionAPI, gitHostLand: GitHostLandPort): void { +export function registerBrunchExecuteLand( + pi: ExtensionAPI, + gitHostLand: GitHostLandPort, + options: HostLandingPolicyOptions = {}, +): void { + if (options.allowHostLanding === false) return; pi.registerTool(createExecuteLandPreflightTool() as never); pi.registerCommand(BRUNCH_LAND_COMMAND, { description: 'Review and land a promoted run into the host (user-confirmed)', handler: async (args, ctx) => { - await runBrunchLandCommand(args ?? '', ctx as unknown as LandCommandContext, { gitHostLand }); + await runBrunchLandCommand(args ?? '', ctx as unknown as LandCommandContext, { + gitHostLand, + ...options, + }); }, }); } diff --git a/src/app/__tests__/pi-runtime.test.ts b/src/app/__tests__/pi-runtime.test.ts index 7b4ffee20..a3e3a07d6 100644 --- a/src/app/__tests__/pi-runtime.test.ts +++ b/src/app/__tests__/pi-runtime.test.ts @@ -11,6 +11,33 @@ import { createBrunchAgentSessionRuntimeFactory } from '../brunch-tui.js'; import { appendBrunchAgentRuntimeSwitch } from '../pi-extensions.js'; describe('Brunch Pi runtime', () => { + it('exposes no host-landing surface in an isolated execution comparison', async () => { + const cwd = await mkdtemp(join(tmpdir(), 'brunch-comparison-runtime-')); + const agentDir = await mkdtemp(join(tmpdir(), 'brunch-agent-dir-')); + const coordinator = createWorkspaceSessionCoordinator({ cwd }); + const workspace = await coordinator.createSetupSession({ + specTitle: 'Comparison runtime', + createNewSpec: true, + }); + const createRuntime = createBrunchAgentSessionRuntimeFactory({ + workspace, + coordinator, + comparisonIsolation: { targetRoot: cwd }, + }); + const created = await createRuntime({ + cwd, + agentDir, + sessionManager: workspace.session.manager, + }); + + try { + expect(created.session.extensionRunner.getCommand('brunch:land')).toBeUndefined(); + expect(created.session.getToolDefinition('execute_land_preflight')).toBeUndefined(); + } finally { + created.session.dispose(); + } + }); + it('registers graph and read-only tools without built-in write tools on the product runtime path', async () => { const cwd = await mkdtemp(join(tmpdir(), 'brunch-tui-graph-runtime-')); const agentDir = await mkdtemp(join(tmpdir(), 'brunch-agent-dir-')); diff --git a/src/app/brunch-tui.ts b/src/app/brunch-tui.ts index 806dc444c..4a01150f6 100644 --- a/src/app/brunch-tui.ts +++ b/src/app/brunch-tui.ts @@ -577,6 +577,7 @@ export function createBrunchAgentSessionRuntimeFactory( ...(comparisonIsolation ? { allowWebTools: false, + allowHostLanding: false, foregroundFilesystemRoot: comparisonIsolation.targetRoot, } : {}), diff --git a/src/app/pi-extensions.ts b/src/app/pi-extensions.ts index c6002e605..82f367ebf 100644 --- a/src/app/pi-extensions.ts +++ b/src/app/pi-extensions.ts @@ -254,6 +254,8 @@ export interface BrunchPiExtensionsOptions extends BrunchCommandsOptions { foregroundFilesystemRoot?: string; /** Defaults on; strict comparison launches set false. */ allowWebTools?: boolean; + /** Defaults on; strict comparison launches must stop at promotion_prepared. */ + allowHostLanding?: boolean; /** * Optional subagent registry (D44-L/D92-L). When provided with a non-empty * code-owned delegatable set, the product `subagent` tool is registered and @@ -378,7 +380,12 @@ export function createBrunchPiExtensions( ...(graph ? [(api: ExtensionAPI) => registerBrunchExecutePlanPreview(api, graph)] : []), registerBrunchExecutePetriExport, (api) => registerBrunchExecutePromotionPrepare(api, executionPorts.gitRunPromotion), - (api) => registerBrunchExecuteLand(api, executionPorts.gitHostLand), + (api) => + registerBrunchExecuteLand( + api, + executionPorts.gitHostLand, + options.allowHostLanding === undefined ? {} : { allowHostLanding: options.allowHostLanding }, + ), registerBrunchExecutePopulate, registerBrunchExecuteReportInit, registerBrunchExecuteRunComplete, From 5f83d52960482fa4425194282ecd302ab356819f Mon Sep 17 00:00:00 2001 From: Kostandin Angjellari Date: Fri, 24 Jul 2026 11:35:26 +0200 Subject: [PATCH 2/2] FE-1264: Record comparison landing release --- .changeset/fresh-comparisons-stop.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/fresh-comparisons-stop.md diff --git a/.changeset/fresh-comparisons-stop.md b/.changeset/fresh-comparisons-stop.md new file mode 100644 index 000000000..de9379ca3 --- /dev/null +++ b/.changeset/fresh-comparisons-stop.md @@ -0,0 +1,5 @@ +--- +"@hashintel/brunch": patch +--- + +Block host landing from isolated execution comparison runs.