Repository navigation
fix(sdk): CLI tasks honour every redis form, and a build opens none of a server's stores - #1040
Merged
alexmercerpo merged 4 commits intoNov 3, 2026
Conversation
… the server does
The lite config parser of CLI mode passed the top-level redis through as z.any(), so a redis: { url } reached
createTaskStore raw. The factory mapped redis into a storage config only when it had a host, and fell back to
REDIS_URL / REDIS_HOST or memory otherwise, so the generated CLI kept its tasks where its detached workers could not
find them. The lite parser now validates redis with the server's schema, and createTaskStore and
createElicitationStore normalize their redis option with that schema, so every form reaches the store with its
password, database and TLS, and an unusable one fails at startup. A build still reads a config whose redis url comes
from an unset variable.
Fixes #1035
…ns no task store and needs none of their secrets The manifest boot left out the top-level redis before validating the config, but still validated sqlite, jobs.store and tasks.redis / tasks.sqlite, so a config object whose sqlite.encryption.secret or tasks.redis.url comes from an unset variable stopped frontmcp build, and it opened tasks.redis and created the tasks.sqlite file. Every store is now left out before the config is validated: redis, pubsub, sqlite, jobs.store, tasks.redis and tasks.sqlite.
…nd main) into fix/h02-cli-tasks-redis-url
frontegg-david
marked this pull request as ready for review
November 3, 2026 10:58
Contributor
|
Warning Review limit reachedYour organization has reached its usage spending cap. Adjust your spending cap in the billing tab. Next included review available in 10 minutes. View limit details
CodeRabbit Verify
|
Contributor
Performance Test ResultsStatus: ✅ All tests passed Summary
Total: 101 tests across 21 projects 📊 View full report in workflow run Generated at: 2026-11-03T11:14:02.074Z |
alexmercerpo
approved these changes
Nov 3, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacked on #1039 (native stack #1029).
What and why
Fixes #1035.
In CLI mode, a top-level
redis: { url: 'redis://…' }didn't reach the task store. The generated CLI then kept its tasks in memory, or in the Redis ofREDIS_URL/REDIS_HOST, while the same config as a server kept them in the Redis it names. A detached worker oftasks.runner: 'cli'could not see a task its host created.Why server mode worked
Both modes create the task store through the same code: the scope hands the top-level
redistocreateTaskStore(). The difference is the parser:frontMcpMetadataSchema) validatesrediswithredisOptionsSchema. Its URL branch reads{ url }into{ provider: 'redis', host, port, password, db, tls, keyPrefix, defaultTtlMs }.parseFrontMcpConfigLite) passedredisthrough asz.any(), so the scope got the raw{ url }.createTaskStore()mappedredisinto a storage config only when it had ahost. Any other shape fell through, without an error, tocreateStorage's auto-detection:REDIS_URL/REDIS_HOST, else memory.A server declared with
@FrontMcpwas not affected: the decorator runs the full parser, so the CLI got the normalized config. A config exported as an object was: an entry that exports one, orconnect(config, { mode: 'cli' }).The change
redisfromfrontMcpBaseSchema.shape, as it already does fortasks,jobsandsqlite. CLI mode now gets the same normalized config as the server, and an unusableredisfails at startup with the server's error. That covers every CLI-mode consumer of the top-levelredis: the task store, the job stores, and plugins that read it throughgetGlobalStoreConfig()(CachePlugin/RememberPluginwithtype: 'global-store'readhostand fell back tolocalhoston a URL).createTaskStore()andcreateElicitationStore()normalize theirredisoption withredisOptionsSchema, the schema the server parser uses, instead of their ownhostcheck. Both factories are public, and each had the samehost-only mapping. They now take every form (url,host/port, with or withoutprovider: 'redis'), withpassword,dband TLS (rediss://, ortls: true). An unusable one throwsTaskStoreNotSupportedError/ElicitationNotSupportedErrorwith the schema's message, instead of falling back. The shared helper,normalizeRedisOptions(), is internal (not exported from the package).createForManifest()leaves out every store before it validates the config: the top-levelredis,pubsubandsqlite,jobs.store,tasks.redisandtasks.sqlite.frontmcp buildboots the server in this mode only to read it, and reads none of them. It left outredis,pubsub,sqliteandjobs.storeonly after validating them, so with the lite parser now validatingredis, a config object withredis: { url: process.env['REDIS_URL'] }would stop building whereverREDIS_URLisn't set, as one withsqlite.encryption.secret,jobs.store.redis.urlortasks.redis.urlfrom an unset variable already did. It kepttasks.redisandtasks.sqlite, so the build also connected to that Redis and created that SQLite file.frontmcp build --target cliand--target mcpb(whoseuser_configreaches the server only as environment variables when it runs) for any config whose URLs or secrets exist only at runtime. The deploy targets go on without a manifest when the boot fails.@FrontMcpclass still differs: the decorator validates its whole config with the server's schema when the entry module loads, in every mode,FRONTMCP_SCHEMA_EXTRACTincluded, so an unset variable there stops the build's boot beforecreateForManifest()runs. Validating at build time would not remove that difference, only copy it onto config objects; deferring the decorator's store validation during schema extraction is a separate change.The other stores
redis(ortasks.redis)redisurl:createJobStateStore()handsurltocreateRedisClient(). It now gets the normalized fields instead, whichcreateRedisClient()takes the same way. Keys stay undermcp:jobs:unlesskeyPrefixis set.createElicitationStore()had the samehost-only mapping for its own callers, fixed here. Persistent questions already normalize withredisOptionsSchema(resolveQuestionStorage).User-facing change
frontmcp build --target cli,connect(config, { mode: 'cli' }),FrontMcpInstance.createForCli()) validates and normalizes the top-levelrediswith the server's schema.createTaskStore({ redis })andcreateElicitationStore({ redis })accept every form of theredisoption, and throw on an unusable one.redis: { url: 'redis://:pw@cache:6380/2' }: tasks were kept in memory, or in the Redis ofREDIS_URL/REDIS_HOST→ tasks are kept incache:6380, database 2, with the password.rediss://turns TLS on. This moves CLI tasks to the configured Redis.redis, such as{ url: 'http://cache:6379' }, aurlthat contradicts a field beside it, or{ url: undefined }from an unset variable: the CLI ran with its tasks in memory → it fails at startup with the server's error (redis.url must use the redis:// or rediss:// scheme, got "http://",redis port contradicts redis.url…, or Zod'sInvalid inputonredis), and exits with code 1.CachePlugin/RememberPluginwithtype: 'global-store'and a URL-formredis: connected tolocalhost→ connect to the URL's host, port, password and database.frontmcp buildof a config object whoseredis,pubsub,sqlite,jobs.store,tasks.redisortasks.sqlitetakes a URL or secret from an unset variable:sqlite,jobs.store(since CLI mode doesn't validate the jobs config #1032) andtasks.*stopped the boot (the CLI and mcpb targets failed; the others wrote no manifest) → builds. A@FrontMcpclass still needs the variables where it builds.frontmcp buildof a server withtasks.redisortasks.sqlite: connected to that Redis, or created that SQLite file → opens neither.createTaskStore()/createElicitationStore(), any mode):redis: { url }: ignored, so the store usedREDIS_URL/REDIS_HOSTor memory → the store uses that Redis.rediswithouthostorurl(such as{ port: 6379 }), or an invalid URL: ignored the same way →TaskStoreNotSupportedError/ElicitationNotSupportedError,Invalid Redis configuration: <reason>.urlform), the CLI build / deployment page (tasks in the generated CLI), and the background tasks page if it lists theredisforms.frontmcp-setup/references/setup-redis.md: the generated CLI and the two factories read every form and refuse an unusable one.frontmcp-deployment/references/build-for-cli.md: the CLI readsredisas the server does, its tasks go to that Redis, andfrontmcp buildof a config object doesn't need the variables of its stores.frontmcp-deployment/references/build-manifest.md: the boot leaves outtasks.redisandtasks.sqlitetoo, before validating, and a@FrontMcpclass still needs the variables.Tests
Unit (
libs/sdk):task/store/__tests__/task-store.factory.spec.ts: fiveredisforms (urlwith password and database,rediss://,provider+urlwith a password beside it,provider+host,hostonly) reachcreateStorageas the same connection fields; three unusable ones (http://, aurland a contradictingport, nohost) throwTaskStoreNotSupportedErrorand open no storage.elicitation/store/__tests__/elicitation-store.factory.spec.ts: arediss://URL and ahostreachcreateStorage; anhttp://URL throwsElicitationNotSupportedError.common/metadata/__tests__/parse-lite.spec.ts: the lite parser readsurl,rediss://andhostforms into the server's normalized shape, and refuses anhttp://URL with the server's message.scope/__tests__/scope-cli-redis.spec.ts(new):createForCli()withredis: { url: 'rediss://:secret@cache.internal:6380/3' }keeps the tasks in that Redis (taskBackendType: 'redis', with host, port, password, database and TLS), and anhttp://URL fails startup before any store opens.manifest/__tests__/build-manifest.boot.spec.ts:buildManifest()reads a config whoseredis,pubsub,sqlite,jobs.store,tasks.redisortasks.sqlitetakes a URL or secret from an unset variable, and a server withtasks.redisortasks.sqliteconnects to no Redis and writes no file.Each new test failed before the fix: the URL forms got
memoryand the unusable ones started. The manifest tests failed forsqlite,jobs.storeandtasks.*(and forrediswith the lite parser change alone), and the build made 4 connections totasks.redisand created thetasks.sqlitefile.E2E (
apps/e2e/demo-e2e-cli-exec/e2e/cli-build-tasks-redis.e2e.spec.ts, new): builds a CLI from a scratch project whose config object hasredis: { url: process.env['TASKS_REDIS_URL'] }, with the variable unset at build time.http://URL stops the CLI at startup with the server's message (it ran the tool before).requirepassset, running a tool withredis://:<password>@host:port/5shows the task store'sPINGin database 5 onMONITOR(nothing reached Redis before). It needs Docker, likedemo-e2e-distributed, and is skipped withSKIP_REDIS_TESTS=1.Run locally:
nx test sdk: 666 suites, 9,335 tests passed.nx test demo-e2e-cli-exec: 38 suites, 213 tests passed, the Redis test included (Docker). The new spec also passes withSKIP_REDIS_TESTS=1(the Redis test is skipped).nx test demo-e2e-bundle-size: 9 suites, 42 tests passed. Browser bundles:create()+ one tool 1,294,909 bytes (was 1,294,963), withWebMcpPlugin1,302,093 (was 1,302,147), Vite initial chunk 761,771 (unchanged). No budget changed.nx test skills: 249 passed.nx lint sdk skills: no errors.tsc --noEmit.