Skip to content

feat(ailogic): add ailogic:templates CLI commands - #10853

Open
marb2000 wants to merge 26 commits into
mainfrom
feat/ailogic-templates
Open

feat(ailogic): add ailogic:templates CLI commands#10853
marb2000 wants to merge 26 commits into
mainfrom
feat/ailogic-templates

Conversation

@marb2000

@marb2000 marb2000 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

Adds the firebase ailogic:templates:* commands to manage Firebase AI Logic server prompt templates from the CLI, gated behind the ailogic experiment (same as ailogic:providers and ailogic:config).

  • ailogic:templates:deploy [--dir <path>] [--prune] deploys every .prompt file in the prompts directory (default prompts/, resolved relative to the project root). The file name becomes the template id: new ids are created, existing ids are updated, templates whose content already matches the remote are skipped ("already up to date"), and --prune deletes remote templates with no local file after confirmation. An empty directory with --prune is refused rather than interpreted as "delete everything".
  • ailogic:templates:list lists deployed templates (id, display name, lock state, content preview).
  • ailogic:templates:get <templateId> prints one template in full.
  • ailogic:templates:delete <templateId> deletes after confirmation (--force to skip).
  • ailogic:templates:lock / ailogic:templates:unlock <templateId> toggle a lock that blocks updates and deletes. Locked templates block a deploy during planning with a message pointing at unlock; --force does not override a lock.

Design notes (the concurrency behavior was verified against the live v1beta API, not just assumed from the docs):

  • The deploy logic lives in src/ailogic/templates.ts as a pure planner (planTemplateDeploy): local files plus remote state in, a creates/updates/unchanged/deletes/lockedViolations plan out. No I/O in the planner, so the set arithmetic is directly unit-testable (mirrors the functions release planner and the src/remoteconfig layout).
  • Lock and unlock use the dedicated :modifyLock RPC; locked is output-only on the resource, and the server enforces locks on writes (FAILED_PRECONDITION), so the CLI-side lock checks are a fail-fast nicety on top of real server enforcement.
  • Writes carry etag preconditions: templates:delete passes the etag from its pre-confirmation read, and deploy passes each template's listed etag on updates and prunes. A template modified between planning and apply fails with a clear "re-run" message (409) instead of being silently overwritten or deleted.
  • Deploy updates are masked to templateString (an unmasked PATCH replaces the whole resource), so a displayName set outside the CLI survives deploys; displayName defaults to the template id on creates only.
  • Template ids are validated as URL-safe everywhere they enter the system (file names in deploy, <templateId> args in get/delete/lock/unlock) since they are spliced into REST resource paths.
  • Prompt file frontmatter is validated with anchored --- delimiters, so --- inside YAML values or the prompt body is not mistaken for a delimiter.
  • All commands live at the fixed global location, return structured values for --json, and follow the same read/write split as the config commands: read-only commands report when the AI Logic API is disabled, mutating commands run the interactive enablement flow.

Builds on the now-merged #10849 (providers) and #10850 (config); the branch is synced with main, so the diff is the template-only changes (16 files, pure additions).

Scenarios Tested

  • Unit tests: src/ailogic/templates.spec.ts (validator, directory reader, planner) and one spec per command. Covers frontmatter edge cases (unclosed block, --- in YAML values, CRLF, sequences), invalid/duplicate/directory-shaped prompt files reported in one pass, locked-template blocking for deploy and delete, unchanged-content skipping, prune confirmation and the --force/non-interactive paths, etag passthrough and 409 conflict mapping on delete/update/prune, concurrent-deletion (404) tolerance during prune, fail-fast template id validation, and --dir resolution against the project root.
  • npm run build, lint, and tsc --noEmit are clean; full ailogic suite passes (97 tests).
  • Runtime smoke test: CLI startup with the experiment off (commands absent) and on (commands registered, firebase help ailogic:templates lists all six).
  • Live end-to-end run against a real project: create/update/unchanged deploy paths, get, list, lock via :modifyLock, deploy and delete correctly blocked while locked, unlock, prune guard on an empty directory, delete with etag, friendly 404 after deletion, and a displayName set outside the CLI surviving a content deploy.

Sample Commands

firebase experiments:enable ailogic
firebase ailogic:templates:deploy --dir prompts --prune
firebase ailogic:templates:list
firebase ailogic:templates:get welcome
firebase ailogic:templates:lock welcome
firebase ailogic:templates:delete welcome --force

marb2000 and others added 23 commits July 8, 2026 09:42
Add `firebase ailogic:providers:{enable,disable,list}` to manage the
Gemini API providers (Gemini Developer API and Agent Platform Gemini API)
for Firebase AI Logic from the CLI.

- providers:enable enables the Firebase AI Logic API and the selected
  provider's underlying API; agent-platform-gemini-api requires the Blaze
  (pay-as-you-go) plan.
- providers:disable prompts for confirmation (--force to skip) and turns
  off the proxy when no providers remain enabled.
- providers:list reports which providers are enabled.
- Adds a shared interactive enablement flow (used when the API is not yet
  enabled) that checks enable permission up front before prompting.
- Adds `firebase help <namespace>` listing of subcommands under a prefix
  so ailogic commands are discoverable.

Requires the firebasevertexai and serviceusage IAM permissions; commands
support --json and --non-interactive.
- Rename provider id agent-platform-gemini-api -> gemini-agent-platform-api
  for symmetry with gemini-developer-api (paulb777).
- Centralize provider-type validation in ailogic.parseProviderType/isProviderType
  and reuse it in the enable/disable commands instead of copying the union
  (christhompsongoogle).
- Add AILOGIC_LOGGING_PREFIX constant; replace the repeated 'ailogic' literals.
- Move enablement-cache invalidation into serviceusage.disableServiceAndPoll and
  drop the per-callsite uncache calls; rename its 'prefix' param to loggingPrefix.
- Drop the redundant non-interactive guard in disable; confirm() already enforces
  --force in non-interactive mode.
- Gate the ailogic commands behind a new 'ailogic' experiment until API-council
  approval.
- Document the help.ts namespace-listing blocks.
- Update/extend unit tests accordingly.
Add `firebase ailogic:config:get [path]` and `ailogic:config:set <path> <value>`
to read and write Firebase AI Logic service configuration from the CLI.

- config:get prints the full config (providers, security, monitoring) or a
  single value by path; it is read-only and reports "not enabled" gracefully
  rather than forcing API enablement.
- config:set updates one setting. Tightening security.auth-only or
  security.template-only from false to true prompts for confirmation
  (--force to skip); monitoring.sample-rate-percentage is validated as an
  integer 1-100.
- Developer-facing paths map onto the underlying config resource
  (trafficFilter.*, telemetryConfig.*), with sample rate stored as a fraction.

Supports --json and --non-interactive.
- Bring config commands under the ailogic experiment gate (via cherry-pick of the
  providers review fixes) and inherit the provider rename + centralized validation.
- config:get/config:set: single source of valid paths (fixes get's validate-vs-error
  mismatch); rename provider id to gemini-agent-platform-api.
- config:set: drop the redundant non-interactive guard (confirm() enforces --force),
  use utils.logSuccess, extract a bool parser, validate the path before the
  API-enablement flow.
- gcp: GLOBAL_LOCATION constant for the config resource; AILOGIC_LOGGING_PREFIX in
  isAILogicApiEnabled.
- Add ailogic-config-get.spec.ts and ailogic-config-set.spec.ts.
- config:set validates the value before the API-enablement flow (fail-fast) and
  returns a {path, value} result so --json produces output.
- config:get path traversal uses an isRecord type guard instead of an `as` cast;
  provider keys/paths derive from ailogic.PROVIDER_TYPES (no duplicated literals).
- Add tests: monitoring.state=false -> NONE, template-only tightening, nested-path
  get, and fail-fast-before-enablement.
- Critical: register ailogic:config commands INSIDE the experiment gate; a
  rebase had left them outside, which crashed CLI startup for users without the
  ailogic experiment (client.ailogic was undefined).
- parseBool accepts case-insensitive true/false.
- listProviders runs its two independent enablement checks in parallel.
- Remove the unused security-rules layer (generateRulesContent, getSecurityRules,
  updateSecurityRules, the rules import, and their tests); nothing references it.
- Single source for config paths: WRITABLE_CONFIG_PATHS exported from gcp/ailogic,
  READABLE derived from it; shared assertKnownConfigPath error helper; shared
  samplingRateToPercent/percentToSamplingRate codec (both directions were
  hand-coded in each command).
- config:get validates the path before any API call and only checks provider
  enablement when the requested path needs it.
- config:set: buildUpdate is now the single per-path decision site (folds in the
  confirmation message and current-value read), the switch is exhaustive so a new
  path cannot silently fall into the sample-rate branch, the percentage requires a
  plain decimal integer (Number() also accepted hex and scientific notation), and
  the --json result echoes the normalized value.
- ensureAILogicApiEnabled reuses isAILogicApiEnabled instead of inlining the check.
- config:set preflights serviceusage.services.get (ensureAILogicApiEnabled reads
  enablement state via Service Usage; without it a cold cache surfaced a raw 403
  mid-command despite passing requirePermissions).
- Echo a normalized sample-rate value ('007' -> '7') so the success message and
  --json output match what was stored.
- ensureAILogicApiEnabled honors --force for its enable confirmation and the
  provider selection offers a cancel choice (the Spark-plan retry loop had no
  exit besides Ctrl+C).
- Strengthen provider specs: pin each provider to its own API via withArgs (a
  swapped destructure passed before) and assert the disable cross-check consults
  the other provider's API.
- Add detailed .help() text to ailogic:config:get/set and
  ailogic:providers:enable/disable documenting allowed values
- Drop CHANGELOG entries while the feature is behind the experiment flag
- Remove empty JSDoc blocks in gcp/ailogic.ts
- Inline single-use sampling-rate conversions instead of exporting helpers
- listProviders now reports a provider as enabled only when the Firebase
  AI Logic API (firebasevertexai.googleapis.com) is also enabled, so
  providers:list can no longer disagree with other ailogic commands about
  whether AI Logic is enabled on the project
- enableProvider enables the AI Logic API before the provider's service
  API, so a partial failure cannot land in that inconsistent state
…ive help

The custom namespace walk in the help command predates #10772, which
auto-registers a commander command for every namespace. getCommand now
always resolves namespaces to those commands, so the walk was
unreachable (and mishandled function-valued intermediate nodes like
client.ext, as flagged by review). firebase help ailogic:providers and
firebase help ext:dev are both served by progressive help.

Also remove two empty JSDoc blocks.
Exposes the Firebase AI Logic templates command surface under the `firebase ailogic` namespace:
- `templates:deploy`, `templates:list`, `templates:get`, `templates:delete`, `templates:lock`, and `templates:unlock` to version control and manage server prompt templates.

- Template validations, lock checks, and pruning configurations.
- Full unit test coverage for templates deploy command.
- Fix templates:deploy --dir: drop the Commander default so an explicit missing
  --dir errors while a missing default dir is a graceful no-op (was dead code).
- Reuse src/fsutils (dirExistsSync/listFiles/readFile) instead of raw fs.
- Add templateIdFromName helper; GLOBAL_LOCATION constant; PROMPT_FILE_EXT /
  DEFAULT_PROMPTS_DIR constants (no magic strings).
- 404 -> friendly 'does not exist' on templates:get/lock/unlock (matching delete).
- Drop redundant non-interactive guards in deploy/delete (confirm() enforces --force).
- utils.logSuccess for success output.
- Gate the template commands behind the ailogic experiment.
- Add specs for list/get/delete/lock/unlock; update the deploy prune test.
… fix review findings

- New src/ailogic/templates.ts lib (mirrors the functions release planner and
  remoteconfig lib layout): validatePromptFile, readPromptDirectory, and a pure
  planTemplateDeploy -> {creates, updates, deletes, lockedViolations}, unit-tested
  without stubs. Also positions a future `deploy --only ailogic` target as a
  thin wrapper.
- Fix: declare -f/--force on templates:deploy; the prune confirmation consumed
  options.force but the flag was never declared, so --prune --force was rejected
  as an unknown option (unusable in CI).
- Fix: frontmatter delimiters must be on their own line; '---' inside a quoted
  YAML value no longer breaks validation. Non-mapping frontmatter is rejected.
- Fix: a directory named *.prompt is reported as a validation error instead of
  crashing with a raw EISDIR; filename-derived template ids are validated
  (URL-safe charset) during the validation pass, closing a partial-deploy hole.
- deploy validates all local input before the API-enablement flow (fail-fast) and
  warns when --prune is ignored because no local prompt files exist.
- gcp: shared withTemplate404 helper replaces the 404 try/catch copied across
  get/delete/lock/unlock; byte-identical lockTemplate/unlockTemplate collapsed
  into setTemplateLocked; templateName() helper; the always-global location param
  dropped from template functions (matching getConfig).
- Commands return values for --json (deploy {deployed, pruned}; delete/lock/unlock
  return the template).
- deploy.spec: reset command.befores like every sibling spec (tests no longer
  depend on ambient credentials or make live IAM calls) and stub the fsutils seam
  the command actually uses.
- Add detailed .help() text to all six templates commands documenting
  template ids, lock semantics, and the deploy flow
- Resolve --dir against the project root instead of the cwd, falling
  back to the cwd outside a project
- Match the .prompt extension case-insensitively so files from
  case-insensitive filesystems are not silently skipped
- Validate <templateId> as URL-safe in get/delete/lock/unlock before any
  API call; an id like 'welcome#old' or '..' would otherwise be spliced
  raw into the REST path and silently address a different template. The
  same regex is now shared with the filename validation in deploy.
- Preflight all permissions deploy actually uses (get/update/delete plus
  serviceusage.services.get) so a partial deploy cannot start with
  permissions that only cover part of the plan; add the Service Usage
  read permission to the other five commands for parity with config
- Report case-variant files that collapse to one template id instead of
  letting the last file win
- Tolerate a 404 while pruning (template already deleted concurrently)
@wiz-9635d3485b

wiz-9635d3485b Bot commented Jul 27, 2026

Copy link
Copy Markdown

Wiz Scan Summary

Scanner Findings
Vulnerability Finding Vulnerabilities -
Data Finding Sensitive Data -
Secret Finding Secrets -
IaC Misconfiguration IaC Misconfigurations -
SAST Finding SAST Findings 6 Medium
Software Management Finding Software Management Findings -
Total 6 Medium

View scan details in Wiz

To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio.

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the firebase ailogic command surface, gated behind the ailogic experiment, which allows users to manage Gemini API providers, configurations, and server prompt templates. Key feedback on these changes includes: ensuring that CRLF line endings are fully stripped in template previews to prevent garbled terminal output, refining YAML frontmatter validation to correctly reject arrays (which also evaluate to type 'object'), and defensively using optional chaining when accessing API response bodies to avoid potential runtime crashes.

Comment thread src/commands/ailogic-templates-list.ts Outdated
clc.bold(ailogic.templateIdFromName(t.name)),
t.displayName || "",
t.locked ? "Yes" : "No",
preview.replace(/\n/g, " "),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

Since prompt files can have CRLF (\r\n) line endings (which is explicitly supported and tested), replacing only \n with a space leaves \r in the preview string. Printing a carriage return \r to the terminal resets the cursor to the beginning of the line, which will cause subsequent table columns to overwrite the start of the line and completely garble the terminal output. Replacing \r?\n ensures proper rendering across all platforms.

Suggested change
preview.replace(/\n/g, " "),
preview.replace(/\r?\n/g, " "),

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch, fixed. The preview now strips \r?\n, and there is a test asserting no carriage return survives into the table output.

Comment thread src/ailogic/templates.ts Outdated
Comment on lines +35 to +37
if (parsed !== undefined && parsed !== null && typeof parsed !== "object") {
return "Frontmatter must be a YAML mapping.";
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current check typeof parsed !== "object" is insufficient to verify that the parsed frontmatter is a YAML mapping. In JavaScript, arrays also have a typeof value of "object". If a user provides a YAML sequence (array) instead of a mapping, it will bypass this validation. Adding Array.isArray(parsed) ensures that only valid YAML mappings are accepted.

Suggested change
if (parsed !== undefined && parsed !== null && typeof parsed !== "object") {
return "Frontmatter must be a YAML mapping.";
}
if (parsed !== undefined && parsed !== null && (typeof parsed !== "object" || Array.isArray(parsed))) {
return "Frontmatter must be a YAML mapping.";
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. validatePromptFile now rejects arrays with an Array.isArray check, and the frontmatter test covers a YAML sequence.

Comment thread src/gcp/ailogic.ts Outdated
Comment on lines +396 to +399
if (res.body.templates) {
templates.push(...res.body.templates);
}
pageToken = res.body.nextPageToken;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

To be defensive and prevent potential runtime TypeErrors, use optional chaining when accessing res.body. If the API returns a successful response with an empty body (or if res.body is null/undefined), accessing res.body.templates directly will crash the CLI.

Suggested change
if (res.body.templates) {
templates.push(...res.body.templates);
}
pageToken = res.body.nextPageToken;
if (res.body?.templates) {
templates.push(...res.body.templates);
}
pageToken = res.body?.nextPageToken;
References
  1. TypeScript: Use strict null checks and handle undefined/null explicitly. (link)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done, switched to optional chaining on the list response body.

…ates

Verified against the live v1beta API (discovery doc + probes):

- 'locked' is output-only on PromptTemplate; a PATCH with
  updateMask=locked is rejected with 400 INVALID_ARGUMENT. lock/unlock
  now call POST templates/{id}:modifyLock, which the server accepts and
  enforces (PATCH/DELETE on a locked template fail FAILED_PRECONDITION).
- The API enforces etags when provided: PATCH with a mismatched body
  etag and DELETE with a mismatched ?etag= both return 409 ABORTED.
  templates:delete now passes the etag from its pre-confirmation read,
  and deploy passes each template's listed etag on updates and prunes,
  so a template modified after planning fails with a clear re-run
  message instead of being silently overwritten or deleted.

Live-tested all six commands against a real project: create/update
deploy, get, list, lock, deploy and delete blocked while locked,
unlock, prune guard, delete, and the friendly 404.
@marb2000

Copy link
Copy Markdown
Member Author

Heads up on a fix pushed in aacf0a5: while verifying the API's concurrency semantics against a live project, I found that 'locked' is output-only on PromptTemplate, so the original lock/unlock implementation (PATCH with updateMask=locked) was rejected by the server with 400 INVALID_ARGUMENT. They now use the dedicated POST :modifyLock RPC.

Same commit adds etag preconditions, since the API turned out to fully enforce them: templates:delete passes the etag from its pre-confirmation read, and deploy passes each template's listed etag on updates and prunes. A template modified between planning and apply now fails with a clear re-run message (409) instead of being silently overwritten.

All six commands were exercised end to end against a real project, including lock enforcement on deploy/delete (the server rejects writes to locked templates with FAILED_PRECONDITION, so the CLI-side checks are a fail-fast nicety on top of real server enforcement).

…ntics

- Strip CRLF from list previews; a raw carriage return in a table cell
  resets the terminal cursor and garbles the rendered table
- Reject YAML sequences as frontmatter; arrays are typeof object but
  are not mappings
- Optional-chain the list response body defensively
- Mask deploy updates to templateString: probing the live API showed an
  unmasked PATCH replaces the whole resource, clearing a displayName set
  outside the CLI; under the mask the body etag is still enforced as a
  precondition (verified stale -> 409, current -> 200). displayName is
  now only set as a friendly default on creates
- Skip templates whose content already matches the remote: no
  updateTime/etag churn, an all-unchanged deploy reports 'already up to
  date', and a lock on an identical template no longer blocks the deploy
@joehan

joehan commented Jul 27, 2026

Copy link
Copy Markdown
Member

Seems like all the previous changes are showing up in the PR as well - can you go submit those and sync this to main, and that should get us to a cleaner diff to review?

# Conflicts:
#	src/commands/index.ts
#	src/gcp/ailogic.spec.ts
#	src/gcp/ailogic.ts
@marb2000

Copy link
Copy Markdown
Member Author

Done: #10849 and #10850 are submitted and this branch is synced to main, so the diff is now the template-only changes (16 files, pure additions). Ready for review.

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.

3 participants