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-sandbox-workflows.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Exclude sandbox workspace assets from application workflow discovery. Copied source containing workflow or step directives no longer causes build errors or enters the application's workflow bundles.
2 changes: 2 additions & 0 deletions docs/sandbox.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,8 @@ agent/sandbox/

Every file under `workspace/` mirrors into the sandbox cwd with its structure intact, and eve lists the top-level entries to the model in the prompt automatically. `agent/skills/` files are materialized separately under `$HOME/.agents/skills/`, so `agent/sandbox/workspace/skills/...` is an ordinary workspace subtree when you choose to author one.

Files under `sandbox/workspace/` are sandbox assets. eve excludes them from application workflow discovery, so copied source containing `"use workflow"` or `"use step"` does not register application workflows or steps. This also applies to subagents' sandbox workspaces.

## Overriding the sandbox

To add setup, seed files, or pick a backend, author `defineSandbox`. There are two layouts:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,38 @@ afterEach(async () => {
});

describe("discoverAuthoredWorkflowModules", () => {
it.each(["agent", "agent/subagents/researcher", "custom-agent"])(
"skips sandbox workspace source under %s",
async (agentRoot) => {
await write(
`${agentRoot}/sandbox/workspace/eve-source/src/tools/framework/agent.js`,
`export default { execute() { "use workflow"; return 1; } };`,
);
await write(
`${agentRoot}/sandbox/workspace/eve-source/src/flow.js`,
`import { missing } from "uninstalled-sandbox-dependency";
export async function copiedWorkflow() { "use workflow"; return missing(); }`,
);
await write(
`${agentRoot}/sandbox/workspace/eve-source/src/step.js`,
`export async function copiedStep() { "use step"; return 1; }`,
);
const authoredPaths = await Promise.all(
[
`${agentRoot}/sandbox/sandbox.ts`,
`${agentRoot}/sandbox/workspace-helper.ts`,
`${agentRoot}/sandbox/workspace-tools/helper.ts`,
`${agentRoot}/workspace/helper.ts`,
].map((path) => write(path, `export async function helper() { "use step"; return 1; }`)),
);

await expect(discoverAuthoredWorkflowModules(appRoot)).resolves.toEqual({
directiveModules: authoredPaths.sort(),
workflowModules: [],
});
},
);

it("separates workflow modules from step-only modules and skips the rest", async () => {
const tool = await write(
"agent/tools/deploy.ts",
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type { Dirent } from "node:fs";
import { readdir, readFile } from "node:fs/promises";
import { join } from "node:path";
import { basename, join } from "node:path";

import { isGeneratedWorkflowFile } from "#compiled/@workflow/builders/index.js";

Expand Down Expand Up @@ -80,6 +80,8 @@ async function collectSourceFiles(root: string): Promise<string[]> {
for (const entry of entries) {
const entryPath = join(directory, entry.name);
if (entry.isDirectory()) {
// Seeded sandbox files are assets, including copied application or framework source.
if (entry.name === "workspace" && basename(directory) === "sandbox") continue;
if (!isIgnoredDirectory(entry.name)) await visit(entryPath);
continue;
}
Expand Down
17 changes: 17 additions & 0 deletions packages/eve/src/internal/workflow-bundle/builder.scenario.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -764,12 +764,26 @@ describe("WorkflowBundleBuilder", () => {
const compiledArtifactsBootstrapPath = join(tempRoot, "compiled-artifacts-bootstrap.mjs");
const toolPath = join(appRoot, "agent", "tools", "deploy.ts");
const stepsPath = join(appRoot, "agent", "lib", "steps.ts");
const sandboxSourceRoot = join(appRoot, "agent", "sandbox", "workspace", "eve-source");

try {
await mkdir(join(appRoot, "agent", "tools"), { recursive: true });
await mkdir(join(appRoot, "agent", "lib"), { recursive: true });
await mkdir(join(appRoot, "node_modules", "vendored"), { recursive: true });
await mkdir(sandboxSourceRoot, { recursive: true });
await Promise.all([
writeFile(
join(sandboxSourceRoot, "agent.js"),
'export default { execute() { "use workflow"; return 1; } };',
),
writeFile(
join(sandboxSourceRoot, "flow.js"),
'import { missing } from "uninstalled-sandbox-dependency";\nexport async function copiedWorkflow() { "use workflow"; return missing(); }',
),
writeFile(
join(sandboxSourceRoot, "step.js"),
'export async function copiedStep() { "use step"; return 1; }',
),
writeFile(compiledArtifactsBootstrapPath, "export {};\n"),
writeFile(
flowFilePath,
Expand Down Expand Up @@ -851,6 +865,7 @@ describe("WorkflowBundleBuilder", () => {
expect(stepsSource).toContain("agent/lib/steps.ts");
expect(stepsSource).toContain("agent/lib/run.ts");
expect(stepsSource).not.toContain("vendored");
expect(stepsSource).not.toContain("eve-source");

const workflowsSource = await readFile(join(outDir, "workflows.mjs"), "utf8");
const encodedChunksMatch = workflowsSource.match(
Expand All @@ -864,6 +879,8 @@ describe("WorkflowBundleBuilder", () => {
expect(workflowCode).toContain("deploy ${service}");
expect(workflowCode).not.toContain("defineWorkflowTool");
expect(workflowCode).not.toContain("node:crypto");
expect(workflowCode).not.toContain("copiedWorkflow");
expect(workflowCode).not.toContain("copiedStep");
} finally {
await rm(tempRoot, { force: true, recursive: true });
}
Expand Down