Skip to content

Fix: validate the agent name at the start of adk create - #598

Open
AmaadMartin wants to merge 2 commits into
mainfrom
fix/cli-create-validate-agent-name
Open

Fix: validate the agent name at the start of adk create#598
AmaadMartin wants to merge 2 commits into
mainfrom
fix/cli-create-validate-agent-name

Conversation

@AmaadMartin

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):

No existing public issue; described below.

  1. Or, if no issue exists, describe the change:

Problem: adk create performs no validation of the agent name before it starts writing to the filesystem.

createAgent() (dev/src/cli/cli_create.ts) took the raw commander argument and immediately computed path.join(process.cwd(), options.agentName) and called generateAgentFolder(), which creates — or, after a confirmation prompt, removeFolder()s and recreates — that directory. The same raw string is also interpolated into the generated package.json "name" field.

Invocation Behaviour before this change
adk create ../../foo Scaffolds outside the current working directory; generateAgentFolder may removeFolder() an out-of-tree directory.
adk create user Scaffolds an app named user, which is reserved for end-user input and breaks session/branch semantics at runtime.
adk create "my agent" Scaffolds a directory with a space and writes an invalid package.json "name".

adk-python already blocks these: run_cmd in src/google/adk/cli/cli_create.py calls validate_app_name(...) as its first statement, before any folder is touched.

Solution: call the validator adk-js already has, as the first statement of createAgent, so the error is raised before any I/O.

export async function createAgent(options: AgentCreationOptions) {
  validateAppName(options.agentName);

  const agentDir = path.join(dirname, options.agentName);
  await generateAgentFolder(agentDir, options.forceYes);

validateAppName is already public API of @google/adk (core/src/apps/app.tscore/src/common.ts:328core/src/index.ts:40), and is already used by the App constructor. No new export, no new helper module, no new type, no refactor — the source diff is 3 added lines (1 import + 1 call + 1 blank). validateAppName stays the single source of truth: its regex, reserved-name list, and message strings are not duplicated or reworded in the dev package.

Why this solution over the alternatives:

  • Re-implementing the rule in dev would fork the source of truth and drift from App's constructor and from adk-python.
  • Validating later (e.g. inside generateAgentFolder) would not meet the requirement, which is specifically that nothing is written to disk for a bad name.
  • Adding process.exit(1) to the create action's catch block is deliberately not done here — the non-zero-exit gap is a separate concern and is already addressed by a separate change.

Deliberate divergence from adk-python (cross-language parity — which side wins):
adk-python validates os.path.basename(os.path.normpath(agent_name)), so it accepts path-shaped arguments such as ../../foo (basename foo) and scaffolds out of tree. adk-js validates options.agentName as given, with no path.basename / path.normalize preprocessing, because:

  1. the adk-js CLI documents the argument as a name, not a path — .argument('[agent]', 'Name to give the new agent', 'adk_agent') (dev/src/cli/cli.ts:307);
  2. the value is also interpolated into the generated package.json "name", where a path separator is invalid; and
  3. rejecting traversal is the security-relevant half of this fix.

Local convention wins here because the divergence is stricter: the resulting rule can never accept a name adk-python would reject. The error messages are kept verbatim identical to adk-python's, since those are the observable, user-facing surface. A dedicated test pins this divergence, so a future path.basename normalisation cannot land silently.

Breaking-change note: this tightens accepted input for adk create. Path-shaped names, user, and names with spaces or leading digits/hyphens are now rejected. All of those previously produced a scaffold that was broken at runtime or in npm, so the rejection is the fix rather than a regression. grep -rn "adk create" finds only README.md:50, which documents no path form. No API signature, export, or type changes; no effect on any other command.

Collision check (required before implementing): gh pr list --repo AmaadMartin/adk-js --state open --limit 1000 → 496 open PRs, none of which adds name validation to adk create (verified by grepping every plausibly adjacent title for create/agent name/validate/app name, then gh pr diff --name-only on the 13 candidates and grepping the diffs of the three that touch dev/src/cli/cli_create.ts for validateAppName). Three open PRs touch the same file but different, non-overlapping regions — #456 (dotenv secret warning), #364 (.gitignore generation), #286 (GOOGLE_GENAI_USE_ENTERPRISE) — all editing the template/output blocks, not createAgent's prologue. Since they are disjoint hunks in the same file rather than the same change, this branches from main rather than stacking; pulling their diffs in would violate diff hygiene.

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.

Added a new describe('Agent Name Validation', ...) block to the existing dev/test/cli/cli_create_test.ts. No existing test was modified, weakened, skipped, or deleted. Four cases:

  1. rejects 'my agent' with /Invalid app name 'my agent'/ and asserts isFolderExists, createFolder, removeFolder, saveToFile were none of them called (the direct analogue of adk-python's test_run_cmd_invalid_app_name);
  2. rejects 'user' with /reserved for end-user input/, same no-I/O assertion;
  3. rejects '../../foo' with /Invalid app name '\.\.\/\.\.\/foo'/, same no-I/O assertion — this pins the deliberate divergence above;
  4. accepts 'valid_agent-1' and asserts createFolder was still called with a path containing it.

The validateAppName rule set itself is already unit-tested in core/test/apps/app_test.ts; those cases are deliberately not duplicated here. These tests exist to prove the CLI calls the validator before touching the filesystem.

$ npx vitest run --project unit:dev dev/test/cli/cli_create_test.ts
 Test Files  1 passed (1)
      Tests  15 passed (15)

Coverage. The change adds exactly one executable statement in src. Per-line coverage of dev/src/cli/cli_create.ts confirms it is hit 15 times, with both outcomes exercised (3 tests take the throw path, 12 the pass-through path):

$ npx vitest run --project unit:dev dev/test/cli/cli_create_test.ts \
    --coverage.enabled --coverage.include='dev/src/cli/cli_create.ts'
line 200 hit counts: [ 15 ]
uncovered statement lines: 106,107,121,122,140,152,153,239,240,256,257,264,276,283,284,294,295

i.e. 100% line and branch coverage of the new code. The file-level number is 91.86% lines / 82.81% branches, but every uncovered line listed is pre-existing and untouched by this change — none is within 39 lines of the added statement.

Proof the tests can fail (mutation testing). Two mutations were run against the new tests:

Mutation 1 — delete the validateAppName(options.agentName); line (i.e. the unfixed code). Cases 1–3 fail, case 4 correctly still passes:

   × Agent Name Validation > should reject a name containing a space before touching the filesystem
   × Agent Name Validation > should reject the reserved name "user" before touching the filesystem
   × Agent Name Validation > should reject a path-traversal name rather than scaffolding out of tree
   ✓ Agent Name Validation > should scaffold a valid name unchanged
AssertionError: promise resolved "undefined" instead of rejecting
      Tests  3 failed | 12 passed (15)

Mutation 2 — adopt adk-python's basename behaviour, validateAppName(path.basename(path.normalize(options.agentName))). Only the traversal case fails, confirming case 3 uniquely pins the documented divergence and is not merely redundant with case 1:

   ✓ should reject a name containing a space before touching the filesystem
   ✓ should reject the reserved name "user" before touching the filesystem
   × should reject a path-traversal name rather than scaffolding out of tree
   ✓ should scaffold a valid name unchanged
AssertionError: promise resolved "undefined" instead of rejecting
      Tests  1 failed | 14 passed (15)

The source was restored to the committed state and all 15 tests re-run green afterwards.

Note on a pre-existing, unrelated failure. On a machine with ambient GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION exported, the pre-existing test Interactive Mode > should handle Vertex AI selection with gcloud defaults fails. It fails identically on the unmodified branch (1 failed | 10 passed), so it is not caused by this change; it is the non-hermeticity already tracked by open PR #569. It is out of scope here and was not touched. With those variables unset the whole file is green.

No integration test was added: adk create is not covered by tests/integration/** today, and the failure path is fully observable at the unit level. Adding one would mean running a real npm install.

Manual End-to-End (E2E) Tests:

Run against the real built CLI with no mocks — real commander, real filesystem:

npm install && npm run build
cd "$(mktemp -d)"
node <repo>/dev/dist/esm/cli_entrypoint.js create user -y
node <repo>/dev/dist/esm/cli_entrypoint.js create ../../foo -y
node <repo>/dev/dist/esm/cli_entrypoint.js create "my agent" -y
node <repo>/dev/dist/esm/cli_entrypoint.js create 123app -y
ls -A                                    # expect: empty
node <repo>/dev/dist/esm/cli_entrypoint.js create my_agent -y   # expect: scaffold as before

Observed:

[ADK CLI] Error creating agent: App name cannot be 'user'; reserved for end-user input.
[ADK CLI] Error creating agent: Invalid app name '../../foo': must start with a letter and can only consist of letters, digits, underscores, and hyphens.
[ADK CLI] Error creating agent: Invalid app name 'my agent': must start with a letter and can only consist of letters, digits, underscores, and hyphens.
[ADK CLI] Error creating agent: Invalid app name '123app': must start with a letter and can only consist of letters, digits, underscores, and hyphens.

ls -A in the scratch directory was empty after all four, and no foo directory appeared in any ancestor — confirming nothing was written for a rejected name. The happy path is unchanged: create my_agent -y produced .env, agent.ts, package.json ("name": "my_agent"), tsconfig.json, package-lock.json and node_modules, and printed the same summary as before.

Other checks run locally on the pushed commit:

$ npm run build          # ok
$ npm run lint           # eslint "**/*.ts" — exit 0
$ npm run format:check   # All matched files use Prettier code style!
$ npx secretlint "dev/**/*"   # clean

npm run ts:check reports 281 errors on this tree, but the count is identical with and without this change (281 vs 281) and none is in dev/. They come from core/dist/types co-existing with core/src after a build, are pre-existing, and ts:check is not part of the validation workflow.

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.

No any, @ts-expect-error, @ts-ignore, eslint-disable, or coverage suppression is added anywhere in this diff (source or tests) — verified by grep over git diff.

Amaad Martin added 2 commits August 3, 2026 16:47
createAgent() passed the raw commander argument straight to
path.join(process.cwd(), name) and generateAgentFolder(), so a malformed
or reserved name was only discovered after a directory had been created
(or, on confirmation, deleted and recreated). 'adk create ../../foo'
scaffolded out of tree, 'adk create user' produced an app whose name is
reserved for end-user input, and 'adk create "my agent"' wrote an
invalid package.json name.

Call the existing validateAppName() from @google/adk as the first
statement of createAgent so the error is raised before any I/O, matching
the contract adk-python's run_cmd already provides.

Unlike adk-python, which validates os.path.basename(os.path.normpath(name))
and therefore accepts path-shaped arguments, this validates the argument
as given: the adk-js CLI documents it as a name, and the value is also
interpolated into the generated package.json "name" field where a path
separator is invalid.
Add an 'Agent Name Validation' describe block to cli_create_test that
pins the three rejected shapes (space, reserved 'user', path traversal)
and asserts none of the file_utils mocks were called, plus a valid name
that still reaches createFolder. No existing test is modified.
AmaadMartin pushed a commit that referenced this pull request Aug 4, 2026
#599)

* fix(core): fall back to node:crypto so randomUUID cannot throw on Node

randomUUID() consults only globalThis.crypto. That global was added in
Node v17.4.0 and stayed behind --experimental-global-webcrypto until
v19.0.0, so on a default Node 18 or earlier neither branch matches and
the function throws, where it previously returned a weak UUID. That
surfaces as a hard failure in createSession, AuthHandler.generateAuthUri
and every A2A message id.

node:crypto's randomUUID has existed since v14.17.0 and does not depend
on the global, so using it as the last resort makes the throw
unreachable on any Node this package could plausibly target. The two
globalThis.crypto branches keep precedence, so browsers and Node 19+ are
unaffected.

randomUUID reaches the web bundle via index_web.ts -> common.ts ->
events/event.js, so the node:crypto import is aliased to a browser shim,
following the existing node:async_hooks precedent in build.js. The shim
throws the message the function used to throw: it is reached only once
both globalThis.crypto branches have been ruled out, which in a browser
means the Web Crypto API is genuinely absent.

Fixes #598.

* docs(core): scope the alias note to the bundled web build

The comment said the import is aliased in "the web build", but the alias
in build.js applies only when platform is browser and bundle is set. The
output package.json#browser points at, dist/web/index_web.js, comes from
the non-bundle path and keeps the import verbatim, as it already does for
node:async_hooks and node:path.
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