Skip to content

Fix: refuse a skill script whose language the tool cannot launch - #880

Open
AmaadMartin wants to merge 3 commits into
feat/run-skill-script-error-code-enumfrom
fix/skill-script-unsupported-language-guard
Open

Fix: refuse a skill script whose language the tool cannot launch#880
AmaadMartin wants to merge 3 commits into
feat/run-skill-script-error-code-enumfrom
fix/skill-script-unsupported-language-guard

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Aug 9, 2026

Copy link
Copy Markdown
Owner

Please ensure you have read the contribution guide before creating a pull request.

Link to Issue or Description of Change

  1. Link to an existing issue (if applicable):
    N/A
  2. Or, if no issue exists, describe the change:
    Problem: RunSkillScriptTool maps a .ts script to CodeExecutionLanguage.TYPESCRIPT and emits a require('ts-node/register') wrapper that no code executor in this repo accepts. UnsafeLocalCodeExecutor answers with {stdout: '', stderr: 'Unsupported language: typescript', outputFiles: []}, which the tool forwards unchanged, so the model reads a failure as a script that ran and printed nothing. AgentEngineSandboxCodeExecutor throws instead, so the same user mistake produces two different shapes. An extension with no language mapping is a third shape: buildWrapperCode throws and the catch mislabels it EXECUTION_ERROR.

Solution: buildWrapperCode now returns undefined for a language it has no arm for, and the tool returns UNSUPPORTED_SCRIPT_LANGUAGE before it calls the executor. The switch alone decides what the tool can launch; SUPPORTED_SCRIPT_EXTENSIONS is message text only and must be kept in step with the switch by hand. The dead TypeScript arm is gone. The model-facing script_path description now names the accepted extensions.

The refusal message names the extension the caller wrote, not the resolved CodeExecutionLanguage: Script 'scripts/hello.ts' has unsupported extension '.ts'. Skill scripts must be one of: .js, .py, .sh, .ps1, .bat, .cmd. An unmapped extension would otherwise report the internal enum value 'unspecified' in a sentence that then lists extensions.

Stacked on #356. The collision check found #356 (feat/run-skill-script-error-code-enum), which adds the RunSkillScriptErrorCode enum this change needs an eighth member of, so this PR branches from it instead of main and adds only the guard. I scanned all 764 open PRs on the fork: #634 and #729 also edit run_skill_script_tool.ts, but in different hunks (the resource-key lookup and the not-found counter), and no open PR adds this guard.

Behaviour change on two already-broken paths. A .ts script moves from a silent empty success to a typed error. An unknown extension moves from EXECUTION_ERROR to UNSUPPORTED_SCRIPT_LANGUAGE; no test pinned the old value. .js, .py, .sh, .ps1, .bat and .cmd produce byte-identical wrappers.

Testing Plan

Please describe the tests that you ran to verify your changes. This is required for all PRs that are not small documentation or typo fixes.
Unit Tests:
[x] I have added or updated unit tests for my change.
[x] All unit tests pass locally.

npx vitest run --project unit:core core/test/tools/skills/run_skill_script_tool_test.ts   # 17 passed
npx vitest run --project integration tests/integration/tools/run_skill_script_tool_test.ts   # 9 passed, 4 skipped (Windows-only)
npm run build   # ok
npm run lint    # ok
npm run format:check / prettier --check on the three files   # ok
npm run docs:check   # ok

CI did not run on this PR: .github/workflows/validation.yaml triggers on pull_request: branches: [main], and this PR targets feat/run-skill-script-error-code-enum. The commands above were run locally on the pushed commit.

I proved each new test can fail. Three mutations of the source, each reverted afterwards:

  1. Restored case CodeExecutionLanguage.TYPESCRIPT and deleted the guard. The three refusal unit cases and the integration case failed. The integration failure is the original bug: expected "errorCode": "UNSUPPORTED_SCRIPT_LANGUAGE", received {"outputFiles": [], "stderr": "Unsupported language: typescript", "stdout": ""}.
  2. Changed UNSUPPORTED_SCRIPT_LANGUAGE to 'UNSUPPORTED_LANGUAGE'. The enum-pinning case failed: expected 'UNSUPPORTED_LANGUAGE' to be 'UNSUPPORTED_SCRIPT_LANGUAGE'.
  3. Deleted the PYTHON arm of buildWrapperCode. The Python wrapper case failed: expected undefined to be 'import runpy\nrunpy.run_path(...)'.

Coverage of the added lines is 100% (v8, measured on core/src/tools/skill/run_skill_script_tool.ts with the two suites above): both guard outcomes, the new enum member, the new default: return undefined, and the new declaration text. The lines still uncovered in that file are all pre-existing: the registry-error catch, the agent-executor fallback, the PowerShell and cmd wrapper arms (Windows-only tests), and two continue branches in getSkillResourceFiles.

Manual End-to-End (E2E) Tests:
Please provide instructions on how to manually test your changes, including any necessary setup or configuration.

import {
  RunSkillScriptTool,
  SkillToolset,
  UnsafeLocalCodeExecutor,
} from '@google/adk';

const skill = {
  frontmatter: {name: 'ts-skill', description: 'ships a TypeScript script'},
  instructions: 'Run scripts.',
  resources: {
    scripts: {'hello.ts': {src: 'const msg: string = "hi"; console.log(msg);'}},
  },
};
const toolset = new SkillToolset([skill], {
  codeExecutor: new UnsafeLocalCodeExecutor(),
});
await new RunSkillScriptTool(toolset).runAsync({
  args: {skill_name: 'ts-skill', script_path: 'scripts/hello.ts'},
  toolContext,
});

Before: {stdout: '', stderr: 'Unsupported language: typescript', outputFiles: []}. After: {error: "Script 'scripts/hello.ts' has unsupported extension '.ts'. Skill scripts must be one of: .js, .py, .sh, .ps1, .bat, .cmd.", errorCode: 'UNSUPPORTED_SCRIPT_LANGUAGE'}. Swap the path to scripts/hello.js and the script still runs and prints.

Checklist

[x] I have read the CONTRIBUTING.md document.
[x] I have performed a self-review of my own code.
[x] I have commented my code, particularly in hard-to-understand areas.
[x] I have added tests that prove my fix is effective or that my feature works.
[x] New and existing unit tests pass locally with my changes.

Amaad Martin added 3 commits August 9, 2026 10:48
RunSkillScriptTool mapped a .ts script to CodeExecutionLanguage.TYPESCRIPT
and emitted a ts-node wrapper that no code executor accepts.
UnsafeLocalCodeExecutor answered with a success-shaped result, so the model
read the failure as a script that ran and printed nothing.

buildWrapperCode now returns undefined for a language it has no arm for, and
the tool returns UNSUPPORTED_SCRIPT_LANGUAGE before it calls the executor.
The switch stays the single source of truth for what the tool can launch.
Unit cases pin both spellings of a .ts path, an extension with no language,
and that the executor is never called. Two cases pin the Python and Shell
wrappers so the buildWrapperCode signature change stays behaviour-preserving.
The integration case runs the refusal against a real UnsafeLocalCodeExecutor.
The resolved CodeExecutionLanguage is an internal value: an unmapped
extension reported 'unspecified' in a sentence that then lists extensions.
The message now names the extension the caller wrote.

Also drops a test-only interface in the integration case that no assertion
consulted.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant