Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/tidy-module-bindings.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Record a required physical binding for every runtime-loaded module in the compiled agent graph. Generated and development module maps now load those bindings directly and reject incomplete graphs instead of reconstructing source paths from logical identity.
33 changes: 30 additions & 3 deletions packages/eve/src/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ import type {
} from "#shared/agent-definition.js";
import type { InternalToolDefinition } from "#shared/tool-definition.js";
import type { WebSearchProvider } from "#shared/web-search.js";
import {
compiledModuleBindingSchema,
createFilesystemModuleBindings,
type CompiledModuleBinding,
} from "#compiler/module-binding.js";

/**
* Stable manifest kind emitted by the compiler for runtime loading.
Expand All @@ -43,7 +48,7 @@ export const ROOT_COMPILED_AGENT_NODE_ID = "__root__";
/**
* Current compiled manifest schema version.
*/
export const COMPILED_AGENT_MANIFEST_VERSION = 41;
export const COMPILED_AGENT_MANIFEST_VERSION = 42;

/**
* Compiled channel entry preserved in the compiled manifest.
Expand Down Expand Up @@ -689,6 +694,7 @@ const compiledExtensionMountSchema: z.ZodType<CompiledExtensionMount> = z
const compiledAgentResourceFields = {
agentRoot: z.string(),
appRoot: z.string(),
bindings: z.record(z.string(), compiledModuleBindingSchema).readonly(),
channels: z.array(compiledChannelEntrySchema),
connections: z.array(compiledConnectionDefinitionSchema),
diagnosticsSummary: discoverDiagnosticsSummarySchema,
Expand Down Expand Up @@ -797,6 +803,7 @@ export const compiledAgentManifestSchema = z
.object({
agentRoot: z.string(),
appRoot: z.string(),
bindings: z.record(z.string(), compiledModuleBindingSchema).readonly(),
extensionMounts: z.array(compiledExtensionMountSchema).default([]),
channels: z.array(compiledChannelEntrySchema),
config: compiledAgentConfigSchema,
Expand Down Expand Up @@ -827,6 +834,7 @@ export const compiledAgentManifestSchema = z
export interface CreateCompiledAgentResourcesInput {
readonly agentRoot: string;
readonly appRoot: string;
readonly bindings?: Readonly<Record<string, CompiledModuleBinding>>;
readonly channels?: readonly CompiledChannelEntry[];
readonly connections?: readonly CompiledConnectionDefinition[];
readonly diagnosticsSummary?: DiscoverDiagnosticsSummary;
Expand Down Expand Up @@ -855,6 +863,7 @@ export function createCompiledAgentResources(
const resources: CompiledAgentResources = {
agentRoot: input.agentRoot,
appRoot: input.appRoot,
bindings: { ...input.bindings },
channels: [...(input.channels ?? [])],
connections: [...(input.connections ?? [])],
diagnosticsSummary: input.diagnosticsSummary ?? {
Expand Down Expand Up @@ -888,17 +897,35 @@ export function createCompiledAgentResources(
},
};

return resources;
if (input.bindings !== undefined) return resources;

return {
...resources,
bindings: createFilesystemModuleBindings({
agentRoot: resources.agentRoot,
manifest: resources,
}),
};
}

/** Creates a compiled authored agent payload with stable defaults. */
export function createCompiledAgentNodeManifest(
input: CreateCompiledAgentResourcesInput & { readonly config: CompiledAgentDefinition },
): CompiledAgentNodeManifest {
return {
const manifest = {
...createCompiledAgentResources(input),
config: cloneCompiledAgentDefinition(input.config),
};
if (input.bindings !== undefined) return manifest;

return {
...manifest,
bindings: createFilesystemModuleBindings({
agentRoot: manifest.agentRoot,
externalDependencies: manifest.config.build?.externalDependencies,
manifest,
}),
};
}

function cloneCompiledAgentDefinition(config: CompiledAgentDefinition): CompiledAgentDefinition {
Expand Down
98 changes: 98 additions & 0 deletions packages/eve/src/compiler/module-binding.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { describe, expect, it } from "vitest";

import { createCompiledAgentResources } from "#compiler/manifest.js";
import {
assertTotalModuleBindings,
createFilesystemModuleBindings,
} from "#compiler/module-binding.js";

function createResources() {
return createCompiledAgentResources({
agentRoot: "/app/agent",
appRoot: "/app",
extensionMounts: [
{
externalDependencies: ["extension-runtime"],
mountLogicalPath: "extensions/crm.ts",
mountSourceId: "extensions/crm.ts",
namespace: "crm",
packageName: "@acme/crm",
packageNamespace: "acme-crm",
sourceRoot: "/packages/crm/extension",
},
],
tools: [
{
description: "Searches CRM records.",
inputSchema: null,
logicalPath: "../../packages/crm/extension/tools/search.ts",
name: "crm__search",
sourceId: "ext:crm:tools/search.ts",
sourceKind: "module",
},
],
});
}

describe("compiled module bindings", () => {
it("separates consumer-visible identity from extension package storage", () => {
const resources = createResources();
const bindings = createFilesystemModuleBindings({
agentRoot: resources.agentRoot,
externalDependencies: ["app-runtime", "extension-runtime"],
manifest: resources,
});

expect(bindings["ext:crm:tools/search.ts"]).toEqual({
backing: {
externalDependencies: ["app-runtime", "extension-runtime"],
extensionScope: {
namespace: "acme-crm",
sourceRoot: "/packages/crm/extension",
},
kind: "filesystem",
sourcePath: "/packages/crm/extension/tools/search.ts",
},
logicalPath: "../../packages/crm/extension/tools/search.ts",
owner: {
kind: "extension",
namespace: "crm",
packageName: "@acme/crm",
},
});
});

it("rejects missing and unreferenced bindings", () => {
const resources = createResources();

expect(() =>
assertTotalModuleBindings({
bindings: {},
manifest: resources,
nodeId: "__root__",
}),
).toThrow('missing a binding for "ext:crm:tools/search.ts"');

expect(() =>
assertTotalModuleBindings({
bindings: {
...createFilesystemModuleBindings({
agentRoot: resources.agentRoot,
manifest: resources,
}),
extra: {
backing: {
externalDependencies: [],
kind: "filesystem",
sourcePath: "/app/agent/tools/extra.ts",
},
logicalPath: "tools/extra.ts",
owner: { kind: "application" },
},
},
manifest: resources,
nodeId: "__root__",
}),
).toThrow('unreferenced binding for "extra"');
});
});
162 changes: 162 additions & 0 deletions packages/eve/src/compiler/module-binding.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
import { isAbsolute, relative, resolve, sep } from "node:path";

import { z } from "#compiled/zod/index.js";
import type {
CompiledAgentNodeManifest,
CompiledAgentResources,
CompiledExtensionMount,
} from "#compiler/manifest.js";
import { collectModuleRefsForManifest } from "#compiler/module-references.js";
import type { ModuleSourceRef } from "#shared/source-ref.js";

export type AgentSourceOwner = z.infer<typeof agentSourceOwnerSchema>;
export type CompiledModuleBacking = z.infer<typeof compiledModuleBackingSchema>;
export type CompiledModuleBinding = z.infer<typeof compiledModuleBindingSchema>;

const agentSourceOwnerSchema = z.discriminatedUnion("kind", [
z.object({ kind: z.literal("application") }).strict(),
z.object({ feature: z.string(), kind: z.literal("framework") }).strict(),
z
.object({
kind: z.literal("extension"),
namespace: z.string(),
packageName: z.string(),
})
.strict(),
]);

const compiledModuleBackingSchema = z.discriminatedUnion("kind", [
z
.object({
externalDependencies: z.array(z.string()).readonly(),
extensionScope: z
.object({ namespace: z.string(), sourceRoot: z.string() })
.strict()
.optional(),
kind: z.literal("filesystem"),
sourcePath: z.string(),
})
.strict(),
z
.object({
kind: z.literal("programmatic"),
moduleId: z.string(),
registryId: z.string(),
})
.strict(),
]);

export const compiledModuleBindingSchema = z
.object({
backing: compiledModuleBackingSchema,
logicalPath: z.string(),
owner: agentSourceOwnerSchema,
})
.strict();

export function createFilesystemModuleBindings(input: {
readonly additionalRefs?: readonly ModuleSourceRef[];
readonly agentRoot: string;
readonly externalDependencies?: readonly string[];
readonly manifest: CompiledAgentNodeManifest | CompiledAgentResources;
}): Record<string, CompiledModuleBinding> {
const bindings: Record<string, CompiledModuleBinding> = {};
const extensionMounts = input.manifest.extensionMounts;

for (const ref of [
...collectModuleRefsForManifest(input.manifest),
...(input.additionalRefs ?? []),
]) {
const sourcePath = resolve(input.agentRoot, ref.logicalPath);
const extension = extensionMounts.find((mount) => isPathInside(mount.sourceRoot, sourcePath));
const existing = bindings[ref.sourceId];

if (existing !== undefined) {
if (existing.logicalPath !== ref.logicalPath) {
throw new Error(
`Module source id "${ref.sourceId}" refers to both "${existing.logicalPath}" and "${ref.logicalPath}".`,
);
}
continue;
}

bindings[ref.sourceId] = createFilesystemModuleBinding({
externalDependencies: input.externalDependencies,
extension,
logicalPath: ref.logicalPath,
sourcePath,
});
}

return bindings;
}

export function assertTotalModuleBindings(input: {
readonly additionalRefs?: readonly ModuleSourceRef[];
readonly bindings: Readonly<Record<string, CompiledModuleBinding>>;
readonly manifest: CompiledAgentNodeManifest | CompiledAgentResources;
readonly nodeId: string;
}): void {
const refs = new Map(
[...collectModuleRefsForManifest(input.manifest), ...(input.additionalRefs ?? [])].map(
(ref) => [ref.sourceId, ref],
),
);

for (const [sourceId, ref] of refs) {
const binding = input.bindings[sourceId];
if (binding === undefined) {
throw new Error(`Compiled node "${input.nodeId}" is missing a binding for "${sourceId}".`);
}
if (binding.logicalPath !== ref.logicalPath) {
throw new Error(
`Compiled node "${input.nodeId}" binds "${sourceId}" to "${binding.logicalPath}", but its manifest references "${ref.logicalPath}".`,
);
}
}

for (const sourceId of Object.keys(input.bindings)) {
if (!refs.has(sourceId)) {
throw new Error(
`Compiled node "${input.nodeId}" has an unreferenced binding for "${sourceId}".`,
);
}
}
}

function createFilesystemModuleBinding(input: {
readonly externalDependencies?: readonly string[];
readonly extension?: CompiledExtensionMount;
readonly logicalPath: string;
readonly sourcePath: string;
}): CompiledModuleBinding {
const extension = input.extension;
return {
backing: {
externalDependencies: [...(input.externalDependencies ?? [])],
extensionScope:
extension === undefined
? undefined
: { namespace: extension.packageNamespace, sourceRoot: extension.sourceRoot },
kind: "filesystem",
sourcePath: input.sourcePath,
},
logicalPath: input.logicalPath,
owner:
extension === undefined
? { kind: "application" }
: {
kind: "extension",
namespace: extension.namespace,
packageName: extension.packageName,
},
};
}

function isPathInside(root: string, path: string): boolean {
const relativePath = relative(resolve(root), resolve(path));
return (
relativePath === "" ||
(relativePath !== ".." && !relativePath.startsWith(`..${sep}`) && !isAbsolute(relativePath))
);
}
Loading
Loading