Skip to content

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

Merged
joehan merged 30 commits into
mainfrom
feat/ailogic-templates
Jul 31, 2026
Merged

feat(ailogic): add ailogic:templates CLI commands#10853
joehan merged 30 commits into
mainfrom
feat/ailogic-templates

Conversation

@marb2000

@marb2000 marb2000 commented Jul 27, 2026

Copy link
Copy Markdown
Member

Description

Adds Firebase AI Logic server prompt template management to the CLI, gated behind the ailogic experiment (same as ailogic:providers and ailogic:config). Builds on the merged #10849 (providers) and #10850 (config).

Deploying templates goes through firebase deploy, like every other declared-state product (per review feedback, replacing an earlier standalone ailogic:templates:deploy command):

// firebase.json
"ailogic": { "templates": "prompts" }
  • firebase deploy --only ailogic (or --only ailogic:templates; unknown filters are an error) reconciles the prompts directory with the project: new .prompt files are created, changed files are updated, unchanged files are skipped, and templates whose files were removed are deleted after confirmation (--force in CI). An empty directory is refused as "delete everything". --dry-run shows the plan without writing.
  • The directory is read recursively; a nested file's relative path flattens into its id with / becoming . (agents/support.promptagents.support, since template ids are single URL segments). Two files mapping to one id is a validation error naming both.
  • The target registers like any other but asserts the ailogic experiment in prepare (mirroring the webframeworks precedent), so nothing is reachable while the API is pre-approval.

Management commands for cloud-side inspection and one-off mutations:

  • ailogic:templates:list — 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 / unlock <templateId> — a lock blocks updates and deletes, including by deploy; --force never overrides a lock.

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

  • Deploy planning is a pure function (planTemplateDeploy in src/ailogic/templates.ts): local files plus remote state in, creates/updates/unchanged/deletes/lockedViolations out (mirrors the functions release planner). Prepare does all reads, validation, planning, and the prune confirmation; release does all writes, so nothing mutates until every target's prepare has passed.
  • 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 checks are a fail-fast nicety on top of real server enforcement. A locked template with unchanged content does not block a deploy (nothing would be written).
  • 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 deletes. 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.

Scenarios Tested

  • Unit tests: src/ailogic/templates.spec.ts (validator, recursive directory reader against real temp dirs, planner), src/deploy/ailogic/ailogic.spec.ts (experiment gate, filters, full prepare+release flows, conflict and 404 handling), and one spec per management command. 1714 tests pass repo-wide; npm run build, lint, and tsc --noEmit are clean.
  • Live end-to-end against a real project: experiment gate error, --dry-run planning, creates including nested dotted ids, unchanged idempotent re-deploy ("already up to date"), --only ailogic:templates sub-target, deletion of removed files with --force, a locked template blocking a real deploy during planning, and a displayName set outside the CLI surviving a content deploy.
  • Runtime smoke test: CLI startup with the experiment off (commands absent, --only ailogic fails with the experiment error) and on (commands registered, deploy target active).

Sample Commands

firebase experiments:enable ailogic

firebase deploy --only ailogic
firebase deploy --only ailogic:templates --force
firebase deploy --only hosting,functions,ailogic
firebase deploy --only ailogic --dry-run

firebase ailogic:templates:list
firebase ailogic:templates:get welcome
firebase ailogic:templates:lock welcome
firebase ailogic:templates:unlock 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 3 Low
Software Management Finding Software Management Findings -
Total 6 Medium 3 Low

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
Comment thread src/ailogic/templates.ts Outdated
Comment thread src/gcp/ailogic.ts Outdated
…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.

prune?: boolean;
}

export const command = new Command("ailogic:templates:deploy")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

High level feedback on the shape of this command:

I'm wondering if this should actually be properly part of firebase deploy. For most of our products, we use the single firebase deploy command so that our users can treat all the resources in their app as a single deployable unit. For the odd cases where they want to deploy just a single piece at a time, we have the --only flag (ie`firebase deploy --only firestore'). The deploy command has the same semantics as this one - it diifs what you have locally with what is deployed, and determines that API calls to make to reconcile them.

Is there a clear reason that we should treat ALF as a special case?

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.

I thought about it and you're right, you've convinced me. The semantics here are exactly deploy semantics: diff the declared local state against what is live, and reconcile. No reason to treat AI Logic as a special case.

So I'm moving this into the deploy command, with all the consequences:

  • firebase.json gets a new section: "ailogic": { "templates": "prompts" }
  • firebase deploy --only ailogic deploys the templates. --only ailogic:templates also works, so triggers can join later the same way firestore:rules and firestore:indexes did
  • ailogic:templates:deploy goes away. The other template commands (list, get, delete, lock, unlock) stay
  • behavior stays the same under the hood: validation before any API call, confirmation before pruning, locked templates block the deploy, unchanged files are skipped
  • subfolders inside prompts/ work: agents/support.prompt becomes the id agents.support, and two files ending up with the same id is an error
  • still behind the ailogic experiment

I'll also fix the backticks in the help text. New code coming shortly.

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 and pushed in b11dda0. firebase deploy --only ailogic (and --only ailogic:templates) is live in this PR exactly as described above: firebase.json section, recursive prompts dir with dotted ids, all the guard behavior carried over, and ailogic:templates:deploy is gone. Verified end to end against a live project, including --dry-run and a locked template blocking a real deploy during planning. The PR description is updated to match.

Comment thread src/commands/ailogic-templates-list.ts Outdated
Comment thread src/commands/ailogic-templates-lock.ts Outdated
Comment thread src/commands/ailogic-templates-lock.ts Outdated
Comment thread src/commands/ailogic-templates-lock.ts Outdated
marb2000 and others added 2 commits July 29, 2026 23:05
Per review, template deployment moves from a standalone command into the
standard deploy pipeline, so AI Logic state deploys like every other
declared-state product:

- firebase.json gains an 'ailogic' section ({"templates": "prompts"});
  schema regenerated
- firebase deploy --only ailogic (and --only ailogic:templates, so future
  sub-targets like triggers can join the way firestore:rules did); unknown
  filters are an error, not a silent no-op
- the target registers like any other but asserts the ailogic experiment
  in prepare, mirroring the webframeworks precedent
- prepare does all reads, validation, planning, and the prune
  confirmation; release does all writes, so nothing mutates until every
  target's prepare has passed. Etag preconditions, masked updates,
  unchanged skipping, locked-template blocking, and the empty-directory
  prune refusal all carry over from the command implementation
- the prompts directory is now read recursively: a nested file's path
  flattens into its id with '/' as '.' (agents/support.prompt ->
  agents.support); two files mapping to one id is a validation error
  naming both
- ailogic:templates:deploy is removed; list/get/delete/lock/unlock stay
- help texts wrap command references in backticks per review

Live-verified end to end against a real project: experiment gate,
dry-run, creates (nested ids included), unchanged idempotent re-deploy,
sub-target filter, prune on file removal, and a locked template blocking
a real deploy during planning.

@joehan joehan left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Overall, this is looking great & code is clean, thank you for moving this over into firebase deploy.

The one remaining issue I see is that AFAICT, we are not hiding the 'deploy AI Logic templates' logic behind the experiment flag anymore. The simplest way to handle this is probably to add a .before() check to the command in src/commands/deploy that checks if AI logic is in the filtered targets. If it is and the expeirement is not enabled, we should just throw an error right there saying something like 'AI Logic deployment is experimental. Please run 'firebase experiments:enable ailogic' to use this feature'

marb2000 added 2 commits July 31, 2026 12:38
Move the ailogic experiment assert from the target's prepare() into the
deploy command's target-filtering before hook, so a gated deploy fails
immediately, before permissions checks and before any other target does
prepare work. Previously the target prepared last, so a full deploy with
an ailogic section but no experiment burned every other target's prepare
first.
@marb2000

Copy link
Copy Markdown
Member Author

The gate was still there, just not where you looked: the first line of the ailogic target's prepare() was experiments.assertEnabled("ailogic", ...), so firebase deploy --only ailogic with the experiment off already failed with the enable instructions. I tested that path against a live project.

That said, you're right that the command level is a better place for it. Targets prepare in order and ailogic went last, so the error fired after every other target already did its prepare work. Moved it in b9a8f75: the deploy command's target-filtering hook now asserts the experiment whenever ailogic is in the filtered targets, so it fails immediately, before the permissions check and before any target runs. Branch is also synced with main.

@marb2000
marb2000 requested a review from joehan July 31, 2026 19:40
@joehan
joehan merged commit 7876c7f into main Jul 31, 2026
53 checks passed
@joehan
joehan deleted the feat/ailogic-templates branch July 31, 2026 20:01
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