Skip to content
Merged
5 changes: 5 additions & 0 deletions .changeset/checkout-independent-artifacts.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"agent-bundle": patch
---

Make `agent-bundle build` emit byte-identical artifacts when one source is built from different checkout paths: generated wrappers import project modules by project-relative POSIX specifiers, and the manifest `modelDigest` hashes route, handler, `web`, and `state` paths relative to the project root. The `NormalizedPlugin` model returned by `validate`, `inspect`, and `build` now carries `projectRoot` (#835).
3 changes: 2 additions & 1 deletion packages/agent-bundle/src/adapters/amp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type TargetArtifactPlan,
} from './types.ts';
import { operatorEnvLayerImport } from '../build/launch-env-shell.ts';
import { generatedModuleSpecifier } from '../build/meta.ts';
import type { Diagnostic } from '../core/diagnostics.ts';
import { deepFreeze } from '../core/freeze.ts';
import { isPortablePathSegment } from '../core/paths.ts';
Expand Down Expand Up @@ -169,7 +170,7 @@ const encodeAmpPlaygroundOutput = (

const ampHookWrapperSource = (entry: TargetHookWrapper): string => [
operatorEnvLayerImport,
`import * as handlerModule from ${JSON.stringify(entry.hook.source)};`,
`import * as handlerModule from ${JSON.stringify(generatedModuleSpecifier(entry.projectRoot, entry.hook.source))};`,
'',
'const target = "amp";',
`const canonicalEvent = ${JSON.stringify(entry.event)};`,
Expand Down
10 changes: 7 additions & 3 deletions packages/agent-bundle/src/adapters/hook-contract.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { Diagnostic } from '../core/diagnostics.ts';
import { dataArrayValues, hasDataKeys, isPlainDataRecord, isRecord, ownDataValue } from '../core/strict-json.ts';
import { escapeRegExp } from '../core/strings.ts';
import { operatorEnvLayerImport } from '../build/launch-env-shell.ts';
import { generatedModuleSpecifier } from '../build/meta.ts';
import type { CanonicalAgentEvent } from '../routes/public.ts';
import {
canonicalHookEvents,
Expand All @@ -23,6 +24,8 @@ export interface TargetHookWrapper {
readonly nativeEvent: string;
/** The computed native tool matcher, absent when the host applies the hook unconditionally. */
readonly nativeMatcher?: string;
/** The project root the hook's absolute sources lie under. */
readonly projectRoot: string;
readonly relativePath: string;
readonly target: string;
}
Expand Down Expand Up @@ -934,7 +937,7 @@ const eventRouteHandlerWrapperSource = (
"import { spawn } from 'node:child_process';",
"import { fileURLToPath } from 'node:url';",
`import { ${projectBindings.join(', ')} } from ${JSON.stringify(eventProjectRuntimeSpecifier)};`,
`import gateHandler from ${JSON.stringify(handler.source)};`,
`import gateHandler from ${JSON.stringify(generatedModuleSpecifier(entry.projectRoot, handler.source))};`,
"import { available, resolvePluginRoot, runAgentRequest, unavailable, useAgent } from '@agent-bundle/runtime/request';",
"import { resolveStandaloneLineage } from '@agent-bundle/runtime/lineage';",
"const pluginRoot = resolvePluginRoot({ fallback: fileURLToPath(new URL('..', import.meta.url)), stateAnchor: 'user-data' });",
Expand Down Expand Up @@ -1061,7 +1064,7 @@ export const cursorHookWrapperSource = (entry: TargetHookWrapper): string => [
// it evaluates before the handler module — a module-level `process.env`
// read there sees the composed environment.
operatorEnvLayerImport,
`import * as handlerModule from ${JSON.stringify(entry.hook.source)};`,
`import * as handlerModule from ${JSON.stringify(generatedModuleSpecifier(entry.projectRoot, entry.hook.source))};`,
'const target = "cursor";',
`const canonicalEvent = ${JSON.stringify(entry.event)};`,
`const nativeEvent = ${JSON.stringify(entry.nativeEvent)};`,
Expand Down Expand Up @@ -1415,6 +1418,7 @@ export const planHooks = (
...(contract.indexedWrappers === false ? { indexed: false as const } : {}),
nativeEvent,
...(matcher === undefined ? {} : { nativeMatcher: matcher }),
projectRoot: model.projectRoot,
relativePath,
target,
...(timeout === undefined ? {} : { timeout }),
Expand Down Expand Up @@ -1473,7 +1477,7 @@ export const nativeHookWrapperSource = (
// The installed pack's operator `.env` layer (#469): the first import, so
// it evaluates before the handler module (see cursorHookWrapperSource).
operatorEnvLayerImport,
`import * as handlerModule from ${JSON.stringify(entry.hook.source)};`,
`import * as handlerModule from ${JSON.stringify(generatedModuleSpecifier(entry.projectRoot, entry.hook.source))};`,
`const target = ${JSON.stringify(entry.target)};`,
`const canonicalEvent = ${JSON.stringify(entry.event)};`,
`const nativeEvent = ${JSON.stringify(nativeEvent)};`,
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-bundle/src/build/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -800,6 +800,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
...noticePolicy,
outDir: stageRoot,
plugin,
projectRoot: options.model.projectRoot,
providers: options.model.providers ?? [],
...(options.model.state === undefined ? {} : { state: options.model.state }),
}),
Expand All @@ -814,6 +815,7 @@ export const build = async (options: BuildOptions): Promise<BuildResult> => {
...noticePolicy,
outDir: stageRoot,
plugin,
projectRoot: options.model.projectRoot,
providers: options.model.providers ?? [],
...(options.model.state === undefined ? {} : { state: options.model.state }),
target: composite.identity,
Expand Down
2 changes: 2 additions & 0 deletions packages/agent-bundle/src/build/cli-bins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -172,6 +172,7 @@ export const cliBinRslibEntries = (
source: entry.source,
sourceInputs: entry.sourceInputs,
virtualSource: generatedCliBinEntrySource({
projectRoot: model.projectRoot,
commands: cli.commands,
plugin: {
...(model.metadata.description === undefined ? {} : { description: model.metadata.description }),
Expand Down Expand Up @@ -210,6 +211,7 @@ export const cliBinRslibEntries = (
source: entry.source,
sourceInputs: entry.sourceInputs,
virtualSource: generatedRenderedRouteWorkerSource({
projectRoot: model.projectRoot,
layouts: model.layouts ?? [],
...(model.notices === undefined ? {} : { noticeRetention: model.notices.retention.resolved }),
providers: model.providers ?? [],
Expand Down
11 changes: 9 additions & 2 deletions packages/agent-bundle/src/build/entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,7 @@ export const planScriptsSurface = async (
source,
sourceInputs: workerSourceInputs,
virtualSource: generatedRenderedRouteWorkerSource({
projectRoot: options.cwd,
...(options.layouts === undefined ? {} : { layouts: options.layouts }),
...(options.providers === undefined ? {} : { providers: options.providers }),
routes: [{
Expand Down Expand Up @@ -253,7 +254,7 @@ export const planScriptsSurface = async (
...(mainExports.get(source) === true
? {
aliases: { [terminalCapabilityRuntimeSpecifier]: terminalProbe! },
virtualSource: generatedExecutableEntrySource({ entrySource: source, exportName: 'main', hostSurface: 'script' }),
virtualSource: generatedExecutableEntrySource({ projectRoot: options.cwd, entrySource: source, exportName: 'main', hostSurface: 'script' }),
}
: {}),
})];
Expand Down Expand Up @@ -396,6 +397,7 @@ export const planMcpEntriesSurface = async (
readonly noticeDelivery?: NoticeDeliveryAdvertisement;
readonly outDir: string;
readonly plugin: { readonly name: string; readonly version: string };
readonly projectRoot: string;
readonly providers?: readonly CompiledProvider[];
readonly noticeRetention?: NormalizedNoticeRetentionPolicy;
readonly state?: NormalizedStateDefinition;
Expand Down Expand Up @@ -430,6 +432,7 @@ export const planMcpEntriesSurface = async (
return server?.generatedRoutes === undefined
? undefined
: generatedRouteMcpEntrySource({
projectRoot: options.projectRoot,
artifactEpoch: options.artifactEpoch,
eventRoutes: hostsRuntime(entry.id) ? options.eventHooks : [],
...(options.noticeDelivery === undefined ? {} : { noticeDelivery: options.noticeDelivery }),
Expand All @@ -448,6 +451,7 @@ export const planMcpEntriesSurface = async (
return server?.generatedRoutes === undefined
? undefined
: generatedRouteFlightWorkerSource({
projectRoot: options.projectRoot,
artifactEpoch: generatedRouteArtifactEpoch(options.plugin),
eventRoutes: hostsRuntime(entry.id) ? options.eventHooks : [],
layouts: options.layouts ?? [],
Expand All @@ -466,6 +470,7 @@ export const planMcpEntriesSurface = async (
const entryShells = compiled.map((entry, index) => {
const serverName = entry.id.startsWith('mcp:') ? entry.id.slice('mcp:'.length) : entry.name;
return generatedStdioMcpEntrySource({
projectRoot: options.projectRoot,
entrySource: generatedRouteSources[index] === undefined ? entry.source : routeModuleSpecifier,
serverName,
});
Expand Down Expand Up @@ -626,6 +631,7 @@ export const planHooksSurface = (
readonly noticeDelivery?: NoticeDeliveryAdvertisement;
readonly outDir: string;
readonly plugin: { readonly name: string; readonly version: string };
readonly projectRoot: string;
readonly providers?: readonly CompiledProvider[];
readonly noticeRetention?: NormalizedNoticeRetentionPolicy;
readonly state?: NormalizedStateDefinition;
Expand Down Expand Up @@ -659,6 +665,7 @@ export const planHooksSurface = (
]),
]),
virtualSource: generatedRouteFlightWorkerSource({
projectRoot: options.projectRoot,
artifactEpoch: workerArtifactEpoch,
eventRoutes: standaloneEventRoutes,
...(options.noticeDelivery === undefined ? {} : { noticeDelivery: options.noticeDelivery }),
Expand Down Expand Up @@ -696,7 +703,7 @@ export const planHooksSurface = (
source: entry.source,
sourceInputs: entry.sourceInputs,
virtualSource: hook.virtualSource
.replace(eventProviderRegistryToken, [...eventHandlerStateSource(options.state, options), ...providerRegistrySource(options.providers ?? [])].join('\n'))
.replace(eventProviderRegistryToken, [...eventHandlerStateSource(options.projectRoot, options.state, options), ...providerRegistrySource(options.projectRoot, options.providers ?? [])].join('\n'))
.replace(eventProviderFieldsToken, providersFieldSource(options.providers ?? [], { indent: ' ', invocation: '{ kind: "event", props: { event: canonicalEvent, payload: native } }', observe: 'true', observer: 'observeProvider' }).join('\n'))
.replaceAll(eventArtifactEpochToken, options.artifactEpoch)
.replaceAll(eventFlightArtifactEpochToken, workerArtifactEpoch),
Expand Down
Loading
Loading