feat(ailogic): add ailogic:templates CLI commands - #10853
Conversation
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.
# Conflicts: # CHANGELOG.md
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.
…by progressive help
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 Scan Summary
To detect these findings earlier in the dev lifecycle, try the Wiz Code extension for VS Code, JetBrains, or Visual Studio. |
There was a problem hiding this comment.
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.
…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.
|
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
|
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
| prune?: boolean; | ||
| } | ||
|
|
||
| export const command = new Command("ailogic:templates:deploy") |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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 ailogicdeploys the templates.--only ailogic:templatesalso works, so triggers can join later the same way firestore:rules and firestore:indexes didailogic:templates:deploygoes 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.promptbecomes the idagents.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.
There was a problem hiding this comment.
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.
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
left a comment
There was a problem hiding this comment.
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'
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.
…eat/ailogic-templates
|
The gate was still there, just not where you looked: the first line of the ailogic target's prepare() was 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. |
Description
Adds Firebase AI Logic server prompt template management to the CLI, gated behind the
ailogicexperiment (same asailogic:providersandailogic: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 standaloneailogic:templates:deploycommand):firebase deploy --only ailogic(or--only ailogic:templates; unknown filters are an error) reconciles the prompts directory with the project: new.promptfiles are created, changed files are updated, unchanged files are skipped, and templates whose files were removed are deleted after confirmation (--forcein CI). An empty directory is refused as "delete everything".--dry-runshows the plan without writing./becoming.(agents/support.prompt→agents.support, since template ids are single URL segments). Two files mapping to one id is a validation error naming both.ailogicexperiment in prepare (mirroring thewebframeworksprecedent), 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 (--forceto skip).ailogic:templates:lock/unlock <templateId>— a lock blocks updates and deletes, including by deploy;--forcenever overrides a lock.Design notes (concurrency behavior verified against the live v1beta API, not assumed):
planTemplateDeployinsrc/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.:modifyLockRPC;lockedis 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).templates:deletepasses 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.templateString(an unmasked PATCH replaces the whole resource), so adisplayNameset outside the CLI survives deploys;displayNamedefaults to the template id on creates only.Scenarios Tested
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, andtsc --noEmitare clean.--dry-runplanning, creates including nested dotted ids, unchanged idempotent re-deploy ("already up to date"),--only ailogic:templatessub-target, deletion of removed files with--force, a locked template blocking a real deploy during planning, and adisplayNameset outside the CLI surviving a content deploy.--only ailogicfails with the experiment error) and on (commands registered, deploy target active).Sample Commands