diff --git a/apps/agent-worker/README.md b/apps/agent-worker/README.md index c4edeb2d..b653de47 100644 --- a/apps/agent-worker/README.md +++ b/apps/agent-worker/README.md @@ -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 diff --git a/apps/agent-worker/src/durable-objects/agent-run-workflow-runtime.ts b/apps/agent-worker/src/durable-objects/agent-run-workflow-runtime.ts index 80d99ed4..b348f468 100644 --- a/apps/agent-worker/src/durable-objects/agent-run-workflow-runtime.ts +++ b/apps/agent-worker/src/durable-objects/agent-run-workflow-runtime.ts @@ -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", diff --git a/packages/agent-core/README.md b/packages/agent-core/README.md index 3af79c44..8b58a77d 100644 --- a/packages/agent-core/README.md +++ b/packages/agent-core/README.md @@ -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 @@ -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. diff --git a/packages/agent-core/src/mastra/durable-agent-step.ts b/packages/agent-core/src/mastra/durable-agent-step.ts index abbe0c9b..c52ac280 100644 --- a/packages/agent-core/src/mastra/durable-agent-step.ts +++ b/packages/agent-core/src/mastra/durable-agent-step.ts @@ -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; + interface ExecuteGeneralAgentToolOptions { abortSignal?: AbortSignal; input: JSONValue; @@ -57,11 +60,10 @@ export async function generateGeneralAgentStep( options: GenerateGeneralAgentStepOptions, ): Promise { 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 } } } } : {}), @@ -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[] { diff --git a/packages/agent-core/src/mastra/system-prompt.ts b/packages/agent-core/src/mastra/system-prompt.ts index 86a980fa..01cdb056 100644 --- a/packages/agent-core/src/mastra/system-prompt.ts +++ b/packages/agent-core/src/mastra/system-prompt.ts @@ -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 diff --git a/packages/agent-core/src/mastra/tool-defs/app-builder-file-policy-support.ts b/packages/agent-core/src/mastra/tool-defs/app-builder-file-policy-support.ts index 13c3e7e5..af22918b 100644 --- a/packages/agent-core/src/mastra/tool-defs/app-builder-file-policy-support.ts +++ b/packages/agent-core/src/mastra/tool-defs/app-builder-file-policy-support.ts @@ -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); } diff --git a/packages/agent-core/src/mastra/tool-defs/app-builder-shell-policy-support.ts b/packages/agent-core/src/mastra/tool-defs/app-builder-shell-policy-support.ts index 8122430a..9a0af583 100644 --- a/packages/agent-core/src/mastra/tool-defs/app-builder-shell-policy-support.ts +++ b/packages/agent-core/src/mastra/tool-defs/app-builder-shell-policy-support.ts @@ -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( @@ -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, }, ); @@ -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]; diff --git a/packages/agent-core/src/mastra/tool-defs/browser-tools.ts b/packages/agent-core/src/mastra/tool-defs/browser-tools.ts index 5a3452d4..fd084626 100644 --- a/packages/agent-core/src/mastra/tool-defs/browser-tools.ts +++ b/packages/agent-core/src/mastra/tool-defs/browser-tools.ts @@ -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", }, }, diff --git a/packages/agent-core/src/tools/browser/browser-action-contract-support.ts b/packages/agent-core/src/tools/browser/browser-action-contract-support.ts index 132bb6f7..3ecb2fd3 100644 --- a/packages/agent-core/src/tools/browser/browser-action-contract-support.ts +++ b/packages/agent-core/src/tools/browser/browser-action-contract-support.ts @@ -37,15 +37,6 @@ const BrowserValueActionMethodSchema = z.enum([ "type", ]); -interface BrowserActInput { - method: z.infer; - ref: string; - targetRef: string | null; - value: string | null; -} - -type BrowserValueActionMethod = z.infer; - export const BrowserBoundActionSchema = z.union([ z.strictObject({ method: BrowserNoValueActionMethodSchema.describe( @@ -70,76 +61,14 @@ export const BrowserBoundActionSchema = z.union([ }), ]); -export const BrowserActInputSchema = z - .strictObject({ - method: BrowserActionMethodSchema.describe( - "Deterministic action to perform on the exact ref from the latest browser tree.", - ), - ref: BrowserElementRefSchema, - targetRef: BrowserElementRefSchema.nullable().describe( - "Destination ref for dragAndDrop; null for every other method.", - ), - value: z - .string() - .min(1) - .max(2_000) - .nullable() - .describe( - "Text, key, option, or percentage for fill, press, scrollTo, selectOptionFromDropdown, and type; null for every other method.", - ), - }) - .superRefine(validateBrowserActInput); - -function validateBrowserActInput(input: BrowserActInput, context: z.RefinementCtx): void { - if (input.method === "dragAndDrop") { - requireDragTarget(input, context); - return; - } - if (isValueActionMethod(input.method)) { - if (input.value === null) { - addIssue(context, "value", `${input.method} requires value.`); - } - } else if (input.value !== null) { - addIssue(context, "value", `${input.method} does not accept value.`); - } - if (input.targetRef !== null) { - addIssue(context, "targetRef", `${input.method} does not accept targetRef.`); - } -} - -function requireDragTarget(input: BrowserActInput, context: z.RefinementCtx): void { - if (input.targetRef === null) { - addIssue(context, "targetRef", "dragAndDrop requires targetRef."); - } - if (input.value !== null) { - addIssue(context, "value", "dragAndDrop does not accept value."); - } -} - -function addIssue(context: z.RefinementCtx, path: "targetRef" | "value", message: string): void { - context.addIssue({ code: "custom", message, path: [path] }); -} +// Provider and driver share one method-specific contract. Placeholder nulls add no runtime safety +// and are emitted inconsistently across otherwise compatible model transports. +export const BrowserActInputSchema = BrowserBoundActionSchema.describe( + "One method-specific action using an exact ref from the latest browser tree.", +); export function browserBoundActionFromInput( input: z.infer, ): z.infer { - if (input.method === "dragAndDrop") { - if (input.targetRef === null) { - throw new Error("Validated dragAndDrop input is missing targetRef."); - } - return { method: input.method, ref: input.ref, targetRef: input.targetRef }; - } - if (isValueActionMethod(input.method)) { - if (input.value === null) { - throw new Error(`Validated ${input.method} input is missing value.`); - } - return { method: input.method, ref: input.ref, value: input.value }; - } - return { method: input.method, ref: input.ref }; -} - -function isValueActionMethod( - method: BrowserActInput["method"], -): method is BrowserValueActionMethod { - return BrowserValueActionMethodSchema.options.some((candidate) => candidate === method); + return BrowserBoundActionSchema.parse(input); }