Skip to content
Draft
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/six-insects-smile.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"eve": patch
---

Add an opt-in `defineAgent({ builtInTools })` framework-tool allowlist. Allowlisted agents deny new framework tools by default while preserving authored tools and overrides.
30 changes: 30 additions & 0 deletions docs/agent-config.md
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,35 @@ parent's quota, so a delegation tree can never outspend the budget configured
at its root. An authored child limit applies only when it is tighter than the
parent's grant; an uncapped parent delegates uncapped children.

## Built-in tool allowlist

By default, eve enables its framework-provided tools. To make production
capabilities deny-by-default, opt into `builtInTools.mode: "allowlist"` and
name the framework tools this agent needs:

```ts title="agent/agent.ts"
import { defineAgent } from "eve";

export default defineAgent({
model: "anthropic/claude-opus-4.8",
builtInTools: {
mode: "allowlist",
allow: ["ask_question", "todo", "web_fetch", "load_skill"],
},
});
```

The policy applies only to eve-built tools. Your authored tools, dynamic
tools, and same-slug authored overrides still work normally. This means a
custom `agent/tools/web_fetch.ts` continues to be available even when
`"web_fetch"` is absent from the allowlist. `disableTool()` remains available
for explicitly removing a known framework tool. New framework tools introduced
by a future eve upgrade are denied unless you add them to `allow`.

`GET /eve/v1/info` reports the effective policy in `tools.builtInPolicy`, the
final model-visible static inventory in `tools.available`, and marks framework
tools omitted by policy with `deniedByPolicy: true`.

## Workflow world

By default, eve selects the Workflow SDK world for the host: Vercel Workflow on
Expand Down Expand Up @@ -213,6 +242,7 @@ installed package must stay external in hosted output, list it in
| `reasoning` | `AgentReasoningDefinition` | provider default | Provider-agnostic reasoning effort forwarded to the agent's turn model calls. |
| `modelOptions` | `AgentModelOptionsDefinition` | none | Provider option overrides forwarded to the model call. |
| `limits` | `AgentLimitsDefinition` | field-specific | Framework-owned runtime limits. `maxInputTokensPerSession` defaults to `40_000_000` for root sessions, and delegated subagent sessions inherit the parent's remaining quota; `maxOutputTokensPerSession` is unset unless configured; `false` uncaps a session token limit. |
| `builtInTools` | `AgentBuiltInToolsDefinition` | all built-ins | Opt-in deny-by-default allowlist for eve framework tools. Authored tools and same-slug overrides remain available. |
| `experimental` | `{ workflow?: { world?: string } }` | unset | Opt-in settings that can change or disappear in any release. Treat them as unstable. `workflow.world` selects the Workflow world package backing session state, queues, hooks, and streams on the root agent. |
| `outputSchema` | Standard Schema or a JSON Schema object | none | Structured return type for task-mode runs (a subagent, schedule, or remote job). Interactive conversation turns ignore it unless the client supplies a per-message schema. |
| `build` | `{ externalDependencies?: string[] }` | none | Hosted-build packaging controls. `externalDependencies` keeps listed packages external while eve compiles authored modules such as tools and channels, and traces those packages into the hosted output. |
Expand Down
4 changes: 4 additions & 0 deletions docs/concepts/default-harness.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ Compaction also preserves the framework's own tool state automatically. It reset

Built-in tools require no imports. The exact set depends on the agent and session. `agent` is available only in the root session; `load_skill` and `connection_search` appear only when the agent declares the corresponding resources; `ask_question` requires a session that can request user input; and `web_search` requires a supported model provider. The harness advertises only the tools available to the current session.

Agents that need deny-by-default capabilities can configure a
[`builtInTools` allowlist](../agent-config#built-in-tool-allowlist). Authored
tools are not affected by that framework-tool policy.

The shell and file tools (`bash`, `read_file`, `write_file`, `glob`, `grep`) run in the app and proxy their work into the agent's [sandbox](../sandbox). The table shows where each tool's effect lands.

| Tool | Does | Where it runs |
Expand Down
7 changes: 7 additions & 0 deletions packages/eve/src/client/agent-info-schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ const tool = entry.extend({

const frameworkTool = tool.extend({
disabledByAuthor: z.boolean(),
deniedByPolicy: z.boolean().optional(),
replacedByAuthoredTool: z.boolean(),
status: z.enum(["active", "disabled", "replaced"]),
});
Expand Down Expand Up @@ -173,6 +174,12 @@ export const AgentInfoResultSchema = z.object({
tools: z.object({
authored: z.array(tool),
available: z.array(tool),
builtInPolicy: z
.discriminatedUnion("mode", [
z.object({ mode: z.literal("all") }),
z.object({ mode: z.literal("allowlist"), allow: z.array(z.string()) }),
])
.optional(),
disabledFramework: z.array(z.string()),
dynamic: z.array(dynamicResolver),
framework: z.array(frameworkTool),
Expand Down
17 changes: 16 additions & 1 deletion packages/eve/src/compiler/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ export const ROOT_COMPILED_AGENT_NODE_ID = "__root__";
/**
* Current compiled manifest schema version.
*/
export const COMPILED_AGENT_MANIFEST_VERSION = 36;
export const COMPILED_AGENT_MANIFEST_VERSION = 37;

/**
* Compiled channel entry preserved in the compiled manifest.
Expand Down Expand Up @@ -406,6 +406,13 @@ const compiledAgentLimitsDefinitionSchema = z
})
.strict();

const compiledAgentBuiltInToolsDefinitionSchema = z
.object({
allow: z.array(z.string()).readonly(),
mode: z.literal("allowlist"),
})
.strict();

const compiledWorkflowToolDefinitionSchema: z.ZodType<CompiledWorkflowToolDefinition> = z
.object({
maxSubagents: z.number().int().positive().optional(),
Expand All @@ -415,6 +422,7 @@ const compiledWorkflowToolDefinitionSchema: z.ZodType<CompiledWorkflowToolDefini
const compiledAgentConfigSchema: z.ZodType<CompiledAgentDefinition> = z
.object({
build: compiledAgentBuildDefinitionSchema.optional(),
builtInTools: compiledAgentBuiltInToolsDefinitionSchema.optional(),
compaction: compiledAgentCompactionDefinitionSchema.optional(),
description: z.string().optional(),
dynamicModel: compiledDynamicModelDefinitionSchema.optional(),
Expand Down Expand Up @@ -790,6 +798,13 @@ export function createCompiledAgentNodeManifest(input: {
? undefined
: [...input.config.build.externalDependencies],
},
builtInTools:
input.config.builtInTools === undefined
? undefined
: {
allow: [...input.config.builtInTools.allow],
mode: input.config.builtInTools.mode,
},
compaction: {
model:
input.config.compaction?.model === undefined
Expand Down
29 changes: 29 additions & 0 deletions packages/eve/src/compiler/normalize-agent-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,35 @@ describe("compileAgentConfig", () => {
sourceKind: "module",
});
});

it("preserves a framework built-in tool allowlist", async () => {
mocks.loadModuleBackedDefinition.mockResolvedValue({
model: "openai/gpt-5.5",
builtInTools: {
mode: "allowlist",
allow: ["todo", "connection_search"],
},
});

const manifest = createAgentSourceManifest({
agentId: "app",
agentRoot: "/app/agent",
appRoot: "/app",
configModule: createModuleSourceRef({
logicalPath: "agent.ts",
sourceId: "agent-config",
}),
});

const compiled = await compileAgentConfig(manifest, {
modelCatalog: createModelCatalog(),
});

expect(compiled.builtInTools).toEqual({
mode: "allowlist",
allow: ["todo", "connection_search"],
});
});
});

function createModelCatalog(): ManifestCompileContext["modelCatalog"] {
Expand Down
8 changes: 8 additions & 0 deletions packages/eve/src/compiler/normalize-agent-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ export async function compileAgentConfig(

const compiledConfig: {
build?: CompiledAgentDefinition["build"];
builtInTools?: CompiledAgentDefinition["builtInTools"];
compaction: {
model?: CompiledRuntimeModelReference;
thresholdPercent?: number;
Expand Down Expand Up @@ -117,6 +118,13 @@ export async function compileAgentConfig(
};
}

if (definition.builtInTools !== undefined) {
compiledConfig.builtInTools = {
mode: definition.builtInTools.mode,
allow: [...definition.builtInTools.allow],
};
}

if (definition.outputSchema !== undefined) {
compiledConfig.outputSchema = serializeOutputSchema(definition.outputSchema);
}
Expand Down
30 changes: 30 additions & 0 deletions packages/eve/src/execution/dispatch-runtime-actions-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,8 @@ import { hydrateDurableSession } from "#execution/session.js";
import { buildSubagentRunInput, type SubagentInputSource } from "#execution/subagent-tool.js";
import { createWorkflowRuntime, workflowEntryReference } from "#execution/workflow-runtime.js";
import { createLogger, logError } from "#internal/logging.js";
import { AGENT_TOOL_NAME } from "#runtime/framework-tools/agent.js";
import { isFrameworkToolAllowed } from "#runtime/framework-tools/index.js";
import { toErrorMessage } from "#shared/errors.js";
import { resolveSubagentDepth } from "#harness/subagent-depth.js";

Expand Down Expand Up @@ -99,6 +101,19 @@ export async function dispatchRuntimeActionsStep(input: {

try {
for (const action of batch.actions) {
if (
isRecursiveAgentAction(action, bundle.subagentRegistry.subagentsByNodeId) &&
!isFrameworkToolAllowed(bundle.resolvedAgent.config.builtInTools, AGENT_TOOL_NAME)
) {
log.warn("recursive agent call blocked by built-in tool policy", {
callId: action.callId,
nodeId: action.nodeId,
subagentName: action.subagentName,
});
results.push(createRecursiveAgentPolicyDeniedResult(action));
continue;
}

if (
isRecursiveAgentAction(action, bundle.subagentRegistry.subagentsByNodeId) &&
(session.rootSessionId !== undefined || subagentDepth.currentDepth > 0)
Expand Down Expand Up @@ -261,6 +276,21 @@ function createRemoteAgentStartFailureResult(input: {
};
}

function createRecursiveAgentPolicyDeniedResult(
action: RuntimeSubagentCallActionRequest,
): RuntimeSubagentResultActionResult {
return {
callId: action.callId,
isError: true,
kind: "subagent-result",
output: {
code: "FRAMEWORK_TOOL_DENIED_BY_POLICY",
message: 'The built-in "agent" tool is denied by this agent\'s built-in tool policy.',
},
subagentName: action.subagentName,
};
}

function createRecursiveAgentRootOnlyResult(
action: RuntimeSubagentCallActionRequest,
): RuntimeSubagentResultActionResult {
Expand Down
2 changes: 2 additions & 0 deletions packages/eve/src/execution/node-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import {
} from "#runtime/agent/resolve-model.js";
import type { RuntimeCompiledArtifactsSource } from "#runtime/compiled-artifacts-source.js";
import { AGENT_TOOL_DESCRIPTION, AGENT_TOOL_NAME } from "#runtime/framework-tools/agent.js";
import { isFrameworkToolAllowed } from "#runtime/framework-tools/index.js";
import { ROOT_RUNTIME_AGENT_NODE_ID, type ResolvedRuntimeAgentNode } from "#runtime/graph.js";

import type { PreparedRuntimeTool } from "#runtime/sessions/turn.js";
Expand Down Expand Up @@ -189,6 +190,7 @@ export function createNodeHarnessTools(input: {

if (
input.node.nodeId === ROOT_RUNTIME_AGENT_NODE_ID &&
isFrameworkToolAllowed(input.node.agent.config?.builtInTools, AGENT_TOOL_NAME) &&
!input.node.agent.disabledFrameworkTools.includes(AGENT_TOOL_NAME) &&
!tools.has(AGENT_TOOL_NAME)
) {
Expand Down
87 changes: 87 additions & 0 deletions packages/eve/src/execution/workflow-steps.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -742,6 +742,93 @@ describe("dispatchRuntimeActionsStep", () => {
);
expect(workflowWritesByNamespace.get(DEFAULT_WORKFLOW_STREAM_NAMESPACE)).toBeUndefined();
});

it("blocks a stale recursive agent call denied by the built-in tool policy", async () => {
const compiledBundle = {
adapterRegistry: {
adaptersByKind: new Map([[threadContextAdapter.kind, threadContextAdapter]]),
},
compiledArtifactsSource: {},
graph: {
nodesByNodeId: new Map(),
root: {
sandboxRegistry: { sandbox: null },
turnAgent: TestTurnAgent,
},
},
hookRegistry: createEmptyHookRegistry(),
resolvedAgent: {
config: {
builtInTools: { mode: "allowlist", allow: [] },
},
},
subagentRegistry: {
subagentsByNodeId: new Map(),
},
toolRegistry: {},
turnAgent: TestTurnAgent,
} as never;
vi.mocked(getCompiledRuntimeAgentBundle).mockResolvedValue(compiledBundle);
const warn = vi.spyOn(console, "warn").mockImplementation(() => {});

const session = setPendingRuntimeActionBatch({
actions: [
{
callId: "call-1",
description: "Delegate the work.",
input: { message: "try a stale action" },
kind: "subagent-call",
name: "agent",
nodeId: "__root__",
subagentName: "agent",
},
],
event: { sequence: 0, stepIndex: 0, turnId: "turn_0" },
responseMessages: [],
session: createStubSession({
continuationToken: "http:parent",
sessionId: "root-session",
}),
});
installSessionStoreMocks([session]);

const sessionState = createStubSessionState({
continuationToken: "http:parent",
sessionId: "root-session",
});

await expect(
dispatchRuntimeActionsStep({
parentContinuationToken: "turn-inbox",
parentWritable: createTestWritable(),
serializedContext: createSerializedContext(),
sessionState,
}),
).resolves.toEqual({
results: [
{
callId: "call-1",
isError: true,
kind: "subagent-result",
output: {
code: "FRAMEWORK_TOOL_DENIED_BY_POLICY",
message: 'The built-in "agent" tool is denied by this agent\'s built-in tool policy.',
},
subagentName: "agent",
},
],
sessionState,
});
expect(startMock).not.toHaveBeenCalled();
expect(warn).toHaveBeenCalledWith(
"[eve:execution.dispatch-runtime-actions] recursive agent call blocked by built-in tool policy",
expect.objectContaining({
callId: "call-1",
subagentName: "agent",
}),
);
expect(workflowWritesByNamespace.get(DEFAULT_WORKFLOW_STREAM_NAMESPACE)).toBeUndefined();
});
});

describe("turnStep", () => {
Expand Down
35 changes: 35 additions & 0 deletions packages/eve/src/internal/authored-definition/core.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,41 @@ import { defineDynamic } from "#public/definitions/tool.js";
const FAILURE_MESSAGE = "Expected the agent config to match the public eve shape.";

describe("normalizeAgentDefinition", () => {
it("accepts a framework built-in tool allowlist", () => {
const definition = normalizeAgentDefinition(
{
model: "openai/gpt-5.5",
builtInTools: {
mode: "allowlist",
allow: ["todo", "connection_search"],
},
},
FAILURE_MESSAGE,
);

expect(definition.builtInTools).toEqual({
mode: "allowlist",
allow: ["todo", "connection_search"],
});
});

it.each([
[{ mode: "denylist", allow: ["todo"] }],
[{ mode: "allowlist", allow: "todo" }],
[{ mode: "allowlist", allow: [42] }],
[{ mode: "allowlist", allow: ["todo"], extra: true }],
])("rejects an invalid framework built-in tool policy: %j", (builtInTools) => {
expect(() =>
normalizeAgentDefinition(
{
model: "openai/gpt-5.5",
builtInTools,
},
FAILURE_MESSAGE,
),
).toThrow(FAILURE_MESSAGE);
});

it("accepts provider-agnostic reasoning effort", () => {
const definition = normalizeAgentDefinition(
{
Expand Down
Loading