Skip to content
Merged
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
7 changes: 4 additions & 3 deletions apps/agent-worker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,9 +170,10 @@ high-confidence fallback materializes the project, scaffolds its canonical works
registers the managed preview even when the selected model would otherwise attempt generic shell
work and finish without a Computer target.
Template preparation records managed-preview ownership in checkpointed Workflow state. Every model
turn excludes the generic dev-server tool in that state, and every tool execution receives the same
request-scoped policy marker so stale tool calls cannot restart the server or replace root framework
manifests. Imported repositories use the separate unmanaged path and retain those capabilities.
turn removes the generic dev-server tool from the provider-facing registry in that state, and every
tool execution receives the same request-scoped policy marker so stale tool calls cannot restart the
server, reinstall its prepared dependencies, or replace and bypass the root framework. Imported
repositories use the separate unmanaged path and retain those capabilities.
The starter page is an internal server-readiness target, not user-facing generated content. A
fresh template run emits the typed `app-preview-status` transition from `building` to `ready` only
after model execution, so the web client can keep
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -420,7 +420,7 @@ async function generateWithCredential(input: {
abortSignal: AbortSignal.timeout(MODEL_STEP_TIMEOUT_MS),
...(input.input.runIntent === "skill-creator"
? {
activeTools: [
includedTools: [
"fs_apply",
"fs_delete",
"fs_list",
Expand Down
14 changes: 7 additions & 7 deletions packages/agent-core/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,10 +51,9 @@ the returned post-action tree as the next actionable state. A click or fill ther
invoke a hidden model decision, expose a selector, reuse a stale ref, or cross the active origin.
Every first-party browser tool advertises its strict JSON schema to providers that support strict
tool calling, while the same Zod contract remains the provider-independent runtime boundary.
The model-facing action schema is a flat four-field object: method and ref are always present, while
unused value and targetRef fields are explicitly null. Runtime refinement maps that contract to the
driver's no-value, value, or drag shape, so operational timeout policy cannot become a substitute
for the action itself. A tool-validation or driver failure is verification
The model-facing action schema is the same method-specific union used by the driver: every action
has method and ref, value-taking actions add value, and dragAndDrop adds targetRef. Operational
timeout policy cannot become a substitute for the action itself. A tool-validation or driver failure is verification
failure, not evidence that generated application state is broken; app-builder agents preserve the
framework event model and correct the browser call instead of injecting page scripts.
The managed
Expand Down Expand Up @@ -82,9 +81,10 @@ text file use FastApply; new files, binary files, and intentional whole-file rew
use the deterministic file writer.
App-builder runs receive an existing framework workspace and managed preview before model
execution. Managed template runs do not advertise the dev-server tool, and their shell and file
boundaries reject alternate server launches or replacement of root framework manifests. A model
therefore cannot replace the canonical Next.js or Expo root with another scaffold, package manifest,
or nested project; ordinary source edits, package installation, and build commands remain available.
boundaries reject alternate server launches, redundant dependency reinstalls, and replacement or
bypass of the root framework. A model therefore cannot replace the canonical Next.js or Expo root
with another scaffold, package manifest, Vite entrypoint, or nested project; ordinary source edits,
explicit dependency changes, and build commands remain available.
Imported repositories keep their own dev-server and framework-file capabilities because their stack
is intentionally user-owned rather than image-owned.

Expand Down
29 changes: 18 additions & 11 deletions packages/agent-core/src/mastra/durable-agent-step.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,14 +35,17 @@ interface GeneralAgentStepResult {

interface GenerateGeneralAgentStepOptions {
abortSignal?: AbortSignal;
activeTools?: string[];
excludedTools?: string[];
excludedTools?: readonly CheatcodeToolName[];
includedTools?: readonly CheatcodeToolName[];
isDeepSeek: boolean;
messages: JSONValue[];
requestContext: RequestContext;
runId: string;
}

type CheatcodeToolName = keyof typeof cheatcodeTools;
type CheatcodeToolSubset = Partial<typeof cheatcodeTools>;

interface ExecuteGeneralAgentToolOptions {
abortSignal?: AbortSignal;
input: JSONValue;
Expand All @@ -57,11 +60,10 @@ export async function generateGeneralAgentStep(
options: GenerateGeneralAgentStepOptions,
): Promise<GeneralAgentStepResult> {
const messages = options.messages.map((message) => modelMessageSchema.parse(message));
const activeTools = resolveActiveTools(options);
const clientTools = resolveClientTools(options);
const result = await mastra.getAgent("generalStep").generate(messages as never, {
...(options.abortSignal ? { abortSignal: options.abortSignal } : {}),
...(activeTools ? { activeTools } : {}),
clientTools: cheatcodeTools,
clientTools,
...(options.isDeepSeek
? { providerOptions: { deepseek: { thinking: { type: "disabled" as const } } } }
: {}),
Expand All @@ -83,14 +85,19 @@ export async function generateGeneralAgentStep(
};
}

function resolveActiveTools(options: GenerateGeneralAgentStepOptions): string[] | undefined {
if (options.activeTools && options.excludedTools) {
function resolveClientTools(options: GenerateGeneralAgentStepOptions): CheatcodeToolSubset {
if (options.includedTools && options.excludedTools) {
throw new Error("Agent tools cannot be included and excluded in the same model step.");
}
if (options.activeTools) return options.activeTools;
if (!options.excludedTools || options.excludedTools.length === 0) return undefined;
const excludedTools = new Set(options.excludedTools);
return Object.keys(cheatcodeTools).filter((toolName) => !excludedTools.has(toolName));
if (!options.includedTools && (!options.excludedTools || options.excludedTools.length === 0)) {
return cheatcodeTools;
}
const toolNames = options.includedTools
? options.includedTools
: (Object.keys(cheatcodeTools) as CheatcodeToolName[]).filter(
(toolName) => !options.excludedTools?.includes(toolName),
);
return Object.fromEntries(toolNames.map((toolName) => [toolName, cheatcodeTools[toolName]]));
}

function toJsonValues(values: ModelMessage[]): JSONValue[] {
Expand Down
2 changes: 1 addition & 1 deletion packages/agent-core/src/mastra/system-prompt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,7 +219,7 @@ Build the Expo Router screens for a polished, native-feeling app: real screens,
// keeps WEB_MODULE's "start the dev server yourself" guidance; this note only applies here.
const APP_BUILDER_PREVIEW_NOTE = `## Your preview is already running — do not start your own

This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Preserve the existing Next.js or Expo framework: do NOT replace its package manifest, initialize or scaffold another framework, or create a nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref. Always pass browser_act all four fields: method, ref, targetRef, and value; use null for targetRef or value when the method does not use it. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. A browser-tool validation or driver error is a verification failure, not evidence of an app defect: correct the tool call once and preserve the framework's state and event model. Never replace React or React Native behavior with injected page scripts or manual DOM listeners to work around browser automation. If the rendered page or build output identifies a concrete code defect, fix that defect and repeat only the changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;
This project is scaffolded at the workspace root and its dev server + live preview are ALREADY running and managed for you before your turn begins (for a mobile app that's Metro serving the app on web plus the Expo Go QR). Preserve the existing Next.js or Expo framework: do NOT replace its package manifest, initialize or scaffold another framework, or create a nested project. Do NOT start, restart, or reconfigure the server yourself — no code_start_dev_server, \`expo start\`, \`npm run dev\`/\`web\`, or \`npx expo …\`: a second server fights the managed one for the project's port and breaks the preview. Use pnpm, never npm/npx, only when dependency changes are necessary; the existing dependency tree is already installed. Inspect and edit the existing root files; the preview hot-reloads on save. Verify by opening the running app in the sandbox's headed Chromium at its INTERNAL localhost address; it's shown to the user automatically in the Computer/App panel — never paste the preview URL. Metro may briefly show an empty document while rebuilding the first web bundle after edits: wait for page content once and reload at most once before treating it as a defect. Take one screenshot with the exact visual acceptance criterion and use its returned PASS/FAIL assessment; never judge screenshot byte size. Exercise one representative interaction by calling browser_observe once, choosing one exact hyphenated element ref from its accessibility tree, and calling browser_act with that ref. Pass method and ref, plus value only for a value-taking method or targetRef only for dragAndDrop. Use browser_act's actionable post-action tree as the result check; do not observe or extract again. If the request explicitly requires another interaction, chain it from a fresh ref in the returned tree; observe again only after navigation, an external page change, or when that tree lacks the required element. Never invent a ref or selector, write a separate Playwright/Python test, or install another browser. A browser-tool validation or driver error is a verification failure, not evidence of an app defect: correct the tool call once and preserve the framework's state and event model. Never replace React or React Native behavior with injected page scripts or manual DOM listeners to work around browser automation. If the rendered page or build output identifies a concrete code defect, fix that defect and repeat only the changed check once. Once the requested content renders, that interaction passes, and no blocking browser error remains, finish.`;

const DOCS_MODULE = `## Building documents & slides

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,35 @@ const FRAMEWORK_MANIFESTS = new Set([
"tsconfig.json",
"yarn.lock",
]);
const INCOMPATIBLE_TEMPLATE_ENTRYPOINT =
/(?:^|\/)(?:index\.html|src\/main\.[cm]?[jt]sx?|vite\.config\.[cm]?[jt]s)$/u;

/** Keeps the managed Next.js or Expo runtime immutable while source files remain fully editable. */
export function assertManagedAppFrameworkFileMutable(context: unknown, path: string): void {
const requestContext = requestContextFromToolContext(context);
if (requestContext.get(CONTEXT.appBuilderManagedPreview) !== true) return;
if (!isProjectRootFrameworkManifest(requestContext.get(CONTEXT.promptWorkspaceDir), path)) return;
if (!isManagedFrameworkPath(requestContext.get(CONTEXT.promptWorkspaceDir), path)) return;
throw new APIError(
422,
"tool_validation_failed",
"The managed app framework manifest cannot be replaced or deleted.",
"The managed app framework cannot be replaced or bypassed.",
{
hint: "Edit application source files. Use pnpm add or pnpm remove when dependencies need to change.",
hint: "Edit the existing Next.js or Expo app/ source files. Use pnpm add or pnpm remove when dependencies need to change.",
retriable: false,
},
);
}

function isProjectRootFrameworkManifest(workspaceDir: unknown, path: string): boolean {
function isManagedFrameworkPath(workspaceDir: unknown, path: string): boolean {
const normalized = path.replace(/\/+$/u, "");
const workspace = typeof workspaceDir === "string" ? workspaceDir.replace(/\/+$/u, "") : null;
const filename = normalized.slice(normalized.lastIndexOf("/") + 1);
if (!FRAMEWORK_MANIFESTS.has(filename)) return false;
const parent = normalized.slice(0, normalized.lastIndexOf("/")) || "/";
return parent === "/workspace" || (typeof workspaceDir === "string" && parent === workspaceDir);
if (FRAMEWORK_MANIFESTS.has(filename)) return true;
const relativePath =
workspace && normalized.startsWith(`${workspace}/`)
? normalized.slice(workspace.length + 1)
: normalized.startsWith("/workspace/")
? normalized.slice("/workspace/".length)
: normalized;
return INCOMPATIBLE_TEMPLATE_ENTRYPOINT.test(relativePath);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ const SHELL_SCAFFOLD_COMMAND =
/(?:^|&&|\|\||;|\n|\()\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*(?:command\s+)?(?:sudo\s+)?(?:corepack\s+)?(?:(?:npm|pnpm|yarn|bun)\s+(?:create|init)\b|(?:npx|pnpx|bunx)\s+(?:--[a-z-]+(?:=[^\s]+)?\s+)*(?:create(?:-[^\s;&|()]+)?|create-next-app|expo\s+init)\b|pnpm\s+(?:dlx|exec)\s+(?:--[a-z-]+(?:=[^\s]+)?\s+)*(?:create(?:-[^\s;&|()]+)?|create-next-app|expo\s+init)\b)/iu;
const MANAGED_PREVIEW_START_COMMAND =
/(?:^|&&|\|\||;|\n|\()\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*(?:command\s+)?(?:sudo\s+)?(?:corepack\s+)?(?:(?:npm|pnpm|yarn|bun)\s+(?:run\s+)?(?:dev|start|web)\b|(?:npx|pnpx|bunx|pnpm\s+(?:dlx|exec))\s+(?:--[a-z-]+(?:=[^\s]+)?\s+)*(?:expo\s+start|next\s+dev|vite)\b|(?:expo\s+start|next\s+dev|vite)(?:\s|$))/iu;
const MANAGED_DEPENDENCY_REINSTALL =
/(?:^|&&|\|\||;|\n|\()\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*(?:command\s+)?(?:sudo\s+)?(?:corepack\s+)?(?:npm|pnpm|bun)\s+install\b|(?:^|&&|\|\||;|\n|\()\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*(?:command\s+)?(?:sudo\s+)?(?:corepack\s+)?yarn(?:\s|$)/iu;
const MANAGED_NON_PNPM_COMMAND =
/(?:^|&&|\|\||;|\n|\()\s*(?:(?:[A-Za-z_][A-Za-z0-9_]*=[^\s;&|()]+)\s+)*(?:command\s+)?(?:sudo\s+)?(?:corepack\s+)?(?:npm|npx|pnpx|yarn|bun|bunx)\b/iu;

/** Prevents model-authored scaffolds from replacing the prepared app-builder workspace. */
export function assertAppBuilderShellCommandAllowed(
Expand All @@ -30,18 +34,31 @@ export function assertAppBuilderShellCommandAllowed(
},
);
}
if (
requestContext.get(CONTEXT.appBuilderManagedPreview) !== true ||
!isManagedPreviewStartCommand(command)
) {
return;
if (requestContext.get(CONTEXT.appBuilderManagedPreview) !== true) return;
if (isManagedPreviewStartCommand(command)) {
throw new APIError(
422,
"tool_validation_failed",
"This app's managed preview is already running.",
{
hint: "Edit the existing source files and verify the running preview in the browser.",
retriable: false,
},
);
}
if (usesNonPnpmPackageManager(command)) {
throw new APIError(422, "tool_validation_failed", "This managed app uses pnpm.", {
hint: "Use pnpm for an explicit dependency change. The existing dependencies are already installed.",
retriable: false,
});
}
if (!isManagedDependencyReinstall(command)) return;
throw new APIError(
422,
"tool_validation_failed",
"This app's managed preview is already running.",
"This managed app's dependencies are already installed.",
{
hint: "Edit the existing source files and verify the running preview in the browser.",
hint: "Continue editing. Use pnpm add or pnpm remove only when the app needs a dependency change.",
retriable: false,
},
);
Expand Down Expand Up @@ -94,6 +111,23 @@ function isManagedPreviewStartCommand(command: readonly string[] | string): bool
return runnerStartsPreview([executable, ...args]);
}

function isManagedDependencyReinstall(command: readonly string[] | string): boolean {
if (typeof command === "string") return MANAGED_DEPENDENCY_REINSTALL.test(command);
const normalized = unwrapCommand(command.map((argument) => argument.toLowerCase()));
const executable = basename(normalized[0]);
const commandName = normalized.slice(1).find((argument) => !argument.startsWith("-"));
return (
executable === "yarn" ||
(["bun", "npm", "pnpm"].includes(executable) && commandName === "install")
);
}

function usesNonPnpmPackageManager(command: readonly string[] | string): boolean {
if (typeof command === "string") return MANAGED_NON_PNPM_COMMAND.test(command);
const executable = basename(unwrapCommand(command.map((argument) => argument.toLowerCase()))[0]);
return ["bun", "bunx", "npm", "npx", "pnpx", "yarn"].includes(executable);
}

function runnerStartsPreview(args: readonly string[]): boolean {
const executableIndex = args.findIndex((argument) => !argument.startsWith("-"));
const executable = args[executableIndex];
Expand Down
5 changes: 1 addition & 4 deletions packages/agent-core/src/mastra/tool-defs/browser-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,22 +29,19 @@ export const mastraBrowserOpen = createTool({
export const mastraBrowserAct = createTool({
id: "browser_act",
description:
"Execute one deterministic action against an exact element ref from the latest browser_observe or browser_act tree. Always provide method, ref, targetRef, and value; use null for fields the selected method does not use. The consumed ref is page-bound and single-use; the result includes an actionable post-action page tree for the next step.",
"Execute one deterministic action against an exact element ref from the latest browser_observe or browser_act tree. Provide method and ref, plus value only for a value-taking method or targetRef only for dragAndDrop. The consumed ref is page-bound and single-use; the result includes an actionable post-action page tree for the next step.",
inputSchema: BrowserActInputSchema,
inputExamples: [
{
input: {
method: "click",
ref: "1-2",
targetRef: null,
value: null,
},
},
{
input: {
method: "fill",
ref: "1-3",
targetRef: null,
value: "Ada Lovelace",
},
},
Expand Down
Loading