Skip to content

Feat: warn that generated .env holds secrets in adk create (adk-python parity) - #456

Open
AmaadMartin wants to merge 2 commits into
mainfrom
feat/cli-create-dotenv-secret-warning
Open

Feat: warn that generated .env holds secrets in adk create (adk-python parity)#456
AmaadMartin wants to merge 2 commits into
mainfrom
feat/cli-create-dotenv-secret-warning

Conversation

@AmaadMartin

@AmaadMartin AmaadMartin commented Aug 1, 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 — no public issue is tracking this. It is a cross-language parity port from
    adk-python (src/google/adk/cli/cli_create.py, _SUCCESS_MSG_CODE /
    _SUCCESS_MSG_CONFIG).

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

Problem: adk create always writes a .env into the new agent folder, and
when the user passes --api_key (or picks the "Google AI" backend in the
interactive flow) that file holds a plaintext credential
(dev/src/cli/cli_create.ts, generateEnvFileGOOGLE_API_KEY=<key>). The
TypeScript CLI never told the user this: createAgent() finished by listing the
created files and printing the npm run web hint, so a user who scaffolded an
agent with --api_key got no signal that a secret had just been written to
disk in cleartext.

adk-python already emits that signal — its success banner ends with:

⚠️  WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.

Solution: Port that one line into createAgent(), printed immediately after
the created-files listing and before the npm run web hint. Three lines of
source; no new module, constant file, helper, import, type, or exported symbol.

Created the following files in /tmp/adk_create_smoke/my_agent:
  - .env
  - agent.ts
  - package.json
  - tsconfig.json

⚠️  WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
Run 'cd my_agent && npm run web' to start the agent in a web interface

Parity judgement calls (stated explicitly):

  • stdout via console.log, not logger / console.warn. adk-python
    prints the whole banner with click.secho(..., fg="green")
    (cli_create.py), i.e. stdout, in the same stream as the file listing.
    Matching that keeps the two CLIs byte-comparable. The repo-wide "prefer
    logger.debug over console.log" guidance targets library code in
    core/src; this is a CLI banner the user is supposed to see, and
    logger.debug would make the warning invisible at the default log level. The
    two adjacent calls in the same block already use console.log.
  • Only the surviving one-liner is ported. The warning historically carried a
    second sentence — "Please ensure .env is added to your .gitignore…" — which
    adk-python deleted when it started generating a .gitignore. It is
    deliberately not resurrected here: .gitignore generation for adk create
    is tracked as separate work, and porting only the surviving line keeps this
    change identical to the reference.
  • Unconditional, not gated on options.apiKey. adk-python prints it on
    every successful create, and adk-js writes a .env on every successful create.
    Gating it would diverge from the reference and would also miss the user who
    later hand-edits a key into the generated .env.
  • The literal is byte-for-byte identical to the reference, including the
    U+26A0 U+FE0F warning sign and the two spaces before WARNING:
    (verified with cat -A against cli_create.py).
  • The credential is never interpolated. The message names the variable
    GOOGLE_API_KEY; a non-template single-quoted string is used so no ${...}
    can creep in. Printing the key would turn a security warning into a credential
    leak into terminal scrollback and CI logs. This is pinned by a test.

Collision check (open PRs on this fork, --state open --limit 200, every PR's
file list queried):
four open PRs touch dev/src/cli/cli_create.ts or its
test — #364 (generates a .gitignore ignoring .env), #286 (renames the
GOOGLE_GENAI_USE_VERTEXAI env var), #313 (geminiInitParams API-key
fallback), #259 (test hermeticity against ambient GOOGLE_CLOUD_*). None of
them prints this warning
, and none touches the console block at the end of
createAgent(), so this is not a duplicate. They overlap only at file level, in
disjoint regions (imports / generateFiles / env-var contents / test setup), so
this branches from main rather than stacking; the hunks merge without
conflict.

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.

Four new cases in a new describe('Secret Warning', ...) block at the end of
dev/test/cli/cli_create_test.ts. No existing test was edited, deleted,
skipped, or weakened
— the pre-existing cases remain the regression signal for
the prompting and file-generation behaviour.

  1. api-key path — the warning is printed, and printed after the
    created-files listing.
  2. Vertex path (no api key) — the warning is unconditional, so a future
    refactor cannot quietly gate it behind if (options.apiKey).
  3. verbatim parity + no secret — the emitted string equals
    `\n⚠️ ${WARNING_TEXT}` exactly. A byte-exact match is what pins the
    absence of the api key value; a separate not.toContain('my-api-key')
    assertion was dropped in review because it could never fail independently of
    the toBe.
  4. failure path — a create that exits early (user declines overwriting an
    existing folder) prints no warning.
$ npx vitest run --project unit:dev dev/test/cli/cli_create_test.ts
 ✓ |unit:dev| dev/test/cli/cli_create_test.ts (15 tests) 32ms
   Test Files  1 passed (1)
        Tests  15 passed (15)

Note for reproducers: cli_create_test.ts is not hermetic against ambient
GOOGLE_CLOUD_PROJECT / GOOGLE_CLOUD_LOCATION — the pre-existing case
should handle Vertex AI selection with gcloud defaults fails when they are set,
on unmodified main as well as on this branch (verified by stashing: 1 failed /
10 passed before, 1 failed / 14 passed after, same case). That flake is out of
scope here and is being fixed separately. Runs above use
env -u GOOGLE_CLOUD_PROJECT -u GOOGLE_CLOUD_LOCATION.

Coverage. The new statement is at dev/src/cli/cli_create.ts:311, hit 11
times, no branches — 100% line and branch coverage of new code (v8 JSON report;
the remaining uncovered lines in the file are the pre-existing process.exit
paths at 105-106, 120-121, 139, 151-152, 236-237, 253-254, 261, 273, 280-281,
291-292). Coverage is reported as a floor, not as proof; the mutation results
below are the proof.

Proof the tests can fail (mutation testing). Six mutations were applied to
cli_create.ts, each run against the new suite, then reverted. Every mutation is
killed, and every new test is killed by at least one mutation:

# Mutation Tests killed Failure message
1 Delete the console.log(...) statement entirely 1, 2, 3 AssertionError: expected "log" to be called with arguments: [ StringContaining{…} ]
2 stored in .env.stored in .env.local. 3 AssertionError: … to be … // Object.is equality
- ⚠️ … are stored in .env.
+ ⚠️ … are stored in .env.local.
3 Wrap the warning in if (options.apiKey) { … } 2 AssertionError: expected "log" to be called with arguments: [ StringContaining{…} ]
4 Move the warning above the Created the following files in … header 1 AssertionError: expected +0 to be greater than 1
5 Interpolate the secret: GOOGLE_API_KEY=${options.apiKey} 1, 2, 3 AssertionError: expected undefined to be '\n⚠️ WARNING: …'
6 Print the warning at the top of createAgent(), before the exit guards 1, 4 AssertionError: expected "log" to not be called with arguments: [ StringContaining{…} ]

Mutation 2 is why test 3 asserts the exact literal rather than a substring:
expect.stringContaining('… stored in .env.') also matches … stored in .env.local., so a suffix append survived substring-only assertions. The exact
assertion is deliberately confined to one case; the other three stay on the
ASCII substring so they are not brittle against the emoji prefix.

Other gates, all green on the pushed commit:

npm run build          # exit 0
npm run lint           # eslint "**/*.ts" — exit 0
npm run format:check   # All matched files use Prettier code style!
npx tsc --noEmit       # 281 pre-existing errors repo-wide, identical count
                       # before and after this change (verified by stashing);
                       # 0 in either file this PR touches

Integration test: deliberately none. The change is a console line in a
function already exercised by the unit:dev project. An integration test would
have to run a real npm install in a temp directory to observe one string —
disproportionate and network-dependent.

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

npm install && npm run build
mkdir -p /tmp/adk_create_smoke && cd /tmp/adk_create_smoke
node <adk-js>/dev/dist/esm/cli_entrypoint.js create my_agent -y --api_key test-key-123

Run against the built CLI, no mocks. Actual output tail:

Created the following files in /tmp/adk_create_smoke/my_agent:
  - .env
  - agent.ts
  - package.json
  - tsconfig.json

⚠️  WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.
Run 'cd my_agent && npm run web' to start the agent in a web interface

Also verified in that run:

  • the warning arrives on stdout (captured with 2>/dev/null, still present);
  • the warning line does not contain test-key-123, while the generated
    .env does (GOOGLE_API_KEY=test-key-123) — the message names the variable,
    not the value;
  • cat -A of the emitted line is
    M-bM-^ZM- M-oM-8M-^O WARNING: Secrets (like GOOGLE_API_KEY) are stored in .env.$,
    byte-identical to _SUCCESS_MSG_CODE in adk-python's cli_create.py;
  • re-running with --project demo-project --region us-central1 (no api key)
    prints the same warning, confirming it is unconditional.

Caveat: createAgent shells out to npm install @google/adk @google/adk-devtools zod dotenv, and this sandbox's npm registry returns 403 Forbidden for
installs into a scratch directory. The two npm install invocations were
therefore stubbed with a no-op npm on PATH; everything else in the run —
folder creation, real .env/agent.ts/package.json writes, the real
listFiles call, and all console output — executed for real against the built
dev/dist/esm/cli_entrypoint.js.

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 2 commits August 1, 2026 06:59
adk create always writes a .env into the new agent folder, and with
--api_key (or the interactive Google AI backend) that file contains a
plaintext credential. The CLI gave the user no signal that a secret had
just been written to disk.

Port the one-line warning adk-python emits at the end of its success
banner (src/google/adk/cli/cli_create.py, _SUCCESS_MSG_CODE /
_SUCCESS_MSG_CONFIG) so both CLIs read identically. The line is printed
on stdout via console.log, matching the two adjacent calls in the same
block and adk-python's click.secho banner, and is unconditional because
a .env is written on every successful create.

The message names the GOOGLE_API_KEY variable; it never interpolates the
key value.
The byte-exact toBe on the preceding line already pins the emitted string
in full, so not.toContain('my-api-key') could never fail independently:
if toBe passes the key cannot be present, and if toBe fails the test has
already failed. Fold the leak-safety intent into the comment above the
surviving assertion.
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