Skip to main content
Internal page. It lists weaknesses in the current code, so it is restricted to project members. Back to the internal index.
Each module section lists the risks that are still open in the code, ordered by impact (high first), then by effort (low first). Impact and effort are judgements on a 1 to 5 scale, not estimates: impact 1 is cosmetic and 5 is data loss, a security hole or an outage; effort 1 is hours and 5 is a design change. A risk id in brackets, such as (G1), is a finding number from the module’s review notes; ids without brackets come from reading the code. Reviewed against main on 2026-10-15. Confirmed defects are tracked on the Bugs page and the way forward on the Improvements page.

Overview

Highest impact

Risks rated impact 4 or 5, across all modules.

Boot

Risk ids are short names; no backlog numbers are used. The older guide’s B-series review findings were all marked fixed and are not repeated. Review of 2026-10-15 against main (after PRs #368 to #374): 1 risk closed (noKeyContainer, by ExposureGuard), 4 partly closed (openOps, devExposure, singleProc, buildFixed) plus xmx8g restated for the new 16 GB value, 2 added (guardMcpKeyOnly, nativeMetaStale); the others are unchanged because their code is unchanged.
Id profileDefault · Category Security · Impact 4 · Effort 2 · Components Launcher, Layered ConfigurationWhy it matters. A jar started with no flags uses dev: DEBUG logs, TRACE for the orchestrator stream package (message payloads) and extra management endpoints. Example: an operator copies the jar to a server and forgets —spring.profiles.active=prod.Option. Make prod the default and keep dev opt-in for developers (bootRun can pass dev); or have main() refuse to bind a non-loopback address under dev.Question to settle. Which users run the JVM jar rather than the native binary or container, and can we change the default for them?
Id devExposure · Category Security · Impact 2 · Effort 2 · Components Layered Configuration, Management SurfaceWhy it matters. loggers lets a caller read and change log levels over HTTP; threaddump and mappings reveal internals. The TRACE stream logger writes message payloads, which may hold secrets from workflow inputs. Update: a configured key now guards these endpoints (ADMIN only), so the exposure is limited to keyless loopback use; the TRACE payload logging is unchanged.Option. Trim dev exposure to health and info plus what is needed; put payload tracing behind its own flag rather than a profile.Question to settle. Does anyone rely on /actuator/loggers in dev, and is payload TRACE needed as often as it is on?
Id openOps · Category Security · Impact 3 · Effort 2 · Components Management Surface, Assembled ApplicationWhy it matters. Done (PR #373): with a key set, the key filter now covers /actuator (except /actuator/health) and requires an ADMIN key, and /mcp is guarded by the same keys. Still open: the Swagger UI and the OpenAPI document have no check, so with a public bind anyone can read the API description; and with no key on a loopback bind the actuator (in dev: loggers, mappings, thread dumps) is open to every local caller.Option. Guard the docs with the same filter, or switch springdoc off under the prod profile; or put management on a loopback-only port.Question to settle. Should operations endpoints use the same key as the API, a different one, or network isolation?
Id profileForce · Category Operability · Impact 2 · Effort 2 · Components Launcher, Native ImageWhy it matters. main() forces prod in native images and the build passes -Dspring.profiles.active=prod. The old review said main()‘s rule was removed; it is still there. Two mechanisms, and it is unverified whether the build flag reaches the running binary. The env var SPRING_PROFILES_ACTIVE is not considered.Option. Keep one mechanism, test the native binary’s profile at run time, and document the order of precedence.Question to settle. Which of the two did we intend to keep?
Id buildFixed · Category Functionality · Impact 3 · Effort 4 · Components Plugin SelectionWhy it matters. A user cannot add or remove a plugin without rebuilding. Teams that want a custom set must run their own build, including a slow native compile. Update: there is now one built-in variant switch (-Pyukta.plugins=default leaves out script-js), and native images with the workers module must be built as their role (-Pyukta.workers.native=control-plane|worker).Option. Offer prebuilt variants (full, minimal), or a plugin directory loaded by the JVM build for users who do not need native.Question to settle. Do we want third parties to ship plugins, and if so how do they get them onto a binary?
Id testsNeedPlugin · Category Correctness · Impact 2 · Effort 2 · Components Acceptance Harness, Plugin SelectionWhy it matters. WorkflowAcceptanceTest and SupervisorAcceptanceTest use PROCESS_EXECUTOR. A build with -Pyukta.plugins=none or a list without it cannot pass :boot:test, and the failure looks like a workflow bug.Option. Make the tests skip with a clear message when the plugin is absent, or always add it to testImplementation.Question to settle. Which plugin combinations must CI cover?
Id xmx8g · Category Operability · Impact 2 · Effort 1 · Components Native ImageWhy it matters. -J-Xmx16g is set for the native build (it was 8 GB in the first guide, and 16 GB before that); neither value was measured. On a smaller runner the build can run out of memory, and on a larger project it may need more. The repository’s CI workflows contain no native build job, so the figure is only exercised on a developer’s or release machine.Option. Measure peak use once, record it next to the flag, and fail with a clear message if memory is too small.Question to settle. What is the smallest runner we want to support for native builds?
Id nativeGap · Category Reliability · Impact 3 · Effort 4 · Components Native Image, Acceptance HarnessWhy it matters. All tests run on the JVM. Missing reflection or resource entries show up only when the binary runs. Plugin loading, JSON binding of records and the MCP tools are typical places.Option. Add a smoke test that starts the built binary and runs one workflow, perhaps in CI after nativeCompile.Question to settle. How often does a native-only failure reach users compared with JVM issues?
Id singleProc · Category Availability · Impact 4 · Effort 5 · Components Assembled Application, Engine Tuning DefaultsWhy it matters. A restart loses definitions, sessions and history unless files are configured. Update: boot can now include the PostgreSQL stores (-Pyukta.persistence=postgres, with yukta.session.store-type=postgres), and a control-plane/worker split with -Pyukta.workers=true; a leader lease allows an active/standby pair. The default build is still one process with in-memory state, and no persistent profile is documented as the default.Option. Ship a documented persistent profile (file stores or PostgreSQL) and say which build flag it needs.Question to settle. Is a persistent profile expected for production or is Yukta single-use per install?
Id healthDetails · Category Operability · Impact 2 · Effort 1 · Components Management Surface, Layered ConfigurationWhy it matters. Boot has no Spring Security, so in our view no caller can be authorised and detail fields are probably never shown. Operators then see only UP or DOWN. This was not run to confirm.Option. Set show-details explicitly (never or always on loopback), or add the security that makes the rule meaningful.Question to settle. What do operators expect to see at /actuator/health?
Id deadKeys · Category Operability · Impact 2 · Effort 1 · Components Layered Configuration, Engine Tuning DefaultsWhy it matters. The comment on yukta.logs.store.backend says memory | file, but only the memory store is registered. control.bus.* repeats the code’s own defaults, so the two can drift. A key with no reader is ignored in silence.Option. Remove or implement them; keep one source for each default; test that every key in the files is read.Question to settle. Who owns the list of supported keys?
Id staticStale · Category Functionality · Impact 2 · Effort 2 · Components SPA HostWhy it matters. Patterns like /*.json, /*.svg, /*.ico and /*.webmanifest get the 365-day cache but are not content-hashed, so a changed icon or manifest can stay stale in browsers for a long time.Option. Use a shorter cache or ETag for unhashed names and keep the year only for /assets/**.Question to settle. Which top-level files do we actually ship and how often do they change?
Id apiPath200 · Category Correctness · Impact 1 · Effort 1 · Components SPA HostWhy it matters. A mistyped REST path, for example /apis/workflows, returns index.html with 200. A client expecting JSON gets HTML and a confusing parse error.Option. Limit the fallback to paths that look like screens (no file extension, no /api prefix), or return 404 for Accept: application/json.Question to settle. Do external clients call the server with typos often enough to matter?
Id cspScope · Category Security · Impact 2 · Effort 3 · Components SPA HostWhy it matters. The policy is only on index.html. It allows style-src ‘unsafe-inline’ because React Flow needs inline styles. No other security headers (for example X-Content-Type-Options, HSTS) are set in boot.Option. Use style nonces if the UI library allows, and add standard headers through a filter.Question to settle. Is the UI exposed to untrusted content, such as plugin-supplied node designs?
Id scanBroad · Category Governance · Impact 2 · Effort 2 · Components Assembled ApplicationWhy it matters. Anything under com.infenia.yukta on the classpath becomes a bean. A plugin or test dependency that adds a component changes behaviour without any line in boot.Option. List modules’ configuration classes explicitly, or add a test that prints and checks the set of beans.Question to settle. Do we want a single list of what is switched on?
Id guardMcpKeyOnly · Category Security · Impact 4 · Effort 1 · Components Layered Configuration, LauncherWhy it matters. ExposureGuard refuses a non-loopback address only when ApiKeys.read(environment, "yukta.mcp.api-key") is empty. That call counts the MCP key as a key. The REST filter reads its keys with ApiKeys.read(environment, null), which does not. So a server started with YUKTA_SERVER_ADDRESS=0.0.0.0 and only yukta.mcp.api-key passes the guard, protects /mcp, and leaves /api and the actuator open to the network, which is exactly what the guard is meant to prevent. This looks unintended; confirm before changing.Option. Make the guard read the same keys as the REST filter (or require both), and add a test for the MCP-key-only case to ExposureGuardTest.Question to settle. Which setting should a single ‘secure it’ switch in the image set: one key for REST and MCP, or two?
Id nativeMetaStale · Category Evolvability · Impact 2 · Effort 3 · Components Native ImageWhy it matters. META-INF/native-image/com.infenia.yukta/boot/reflect-config.json was added with the native-image fixes (PRs #368 and #370). Its entries register whole classes (all constructors, fields and methods) of libraries such as Caffeine. Neither boot/build.gradle.kts nor boot’s CLAUDE.md says how it was produced or when to regenerate it. When a dependency or a plugin changes, a missing entry shows up only when the binary runs (see nativeGap), and stale entries are never noticed.Option. Document the command that produced it (the tracing agent run), add a CI step that regenerates and diffs it, or prefer Spring’s AOT hints over a hand-kept file.Question to settle. Who will notice if a new dependency needs one more reflection entry?

Build logic

IDs such as L5 refer to the earlier build-logic review (Improvements page). Its other findings, L1 to L14 except L5, are fixed in code and are not repeated here. Review of 2026-10-15 against main (after PRs #373, #374 and #391): 0 risks closed, 3 partly closed (nettyTwoSources, opengrepEveryModule, fullBuildOnRoot), 4 updated (disabledTasks, ciManualMap, nodeDrift, trivyPrGate), 1 added (cvePins); the other 9 are unchanged because their files are unchanged.
Id nettyTwoSources · Category Security · Impact 2 · Effort 1 · Components Catalog & Root Build, Dependency PlatformWhy it matters. Done (PR #373): spring-conventions now assigns netty.version from the catalog and imports the Netty BOM from it, and :boot:dependencyInsight on 2026-10-15 shows netty-codec-http at 4.2.18.Final, so a Dependabot bump of the catalog reaches the application. Still open: gradle.properties still contains netty.version=4.2.16.Final with its CVE comment, which misleads a reader and would win again if the assignment were removed; and no check fails when the resolved version differs from the catalog.Option. Delete the gradle.properties line; add a small check (or test) that compares the resolved Netty with the catalog.Question to settle. Which of the two numbers did we intend to ship, and who owns re-checking the other after a Dependabot bump?
Id noLockfiles · Category Security · Impact 3 · Effort 3 · Components Catalog & Root Build, CI PipelineWhy it matters. The root build runs lockAllConfigurations() for every project, and the CI step ‘Verify dependency lock files are up-to-date’ exits at once because no gradle.lockfile is tracked (git ls-files finds none). Resolved versions can drift between builds, and the SBOM and Trivy documentation refers to lock files that do not exist.Option. Generate and commit lock files (the CI step already shows the command), or remove the locking line and the lock-file docs until the team wants it. A middle path: lock only the boot runtime classpath.Question to settle. Is reproducibility of resolved dependencies a goal today, or is the BOM pinning enough?
Id opengrepSilentSkip · Category Operability · Impact 3 · Effort 2 · Components OpenGrep GateWhy it matters. The task probes opengrep —version and skips on failure without a message. A developer gets a green local check and a red CI. Worse, a team that never installs it checks nothing locally. The skip exists so first-time contributors can build, but it hides the gate.Option. Print a visible warning when skipped; add an opt-in flag that makes absence an error (for the pre-push hook); provide a wrapper that downloads the pinned binary like the Gradle wrapper does.Question to settle. Is a contributor without OpenGrep better served by a warning, by a hard failure, or by an automatic download?
Id opengrepEveryModule · Category Performance · Impact 1 · Effort 2 · Components OpenGrep GateWhy it matters. Done: the task declares the SARIF report as an output and is cacheable, so a module whose sources and rules did not change is skipped. Still open: a changed module starts its own scan (one at a time, through the shared build service), and each module first starts opengrep --version to see whether the program exists.Option. Run one root-level scan over all src/main folders, or probe for the program once per build.Question to settle. Would one repository-wide scan lose anything we rely on per module, such as report location or —error behaviour?
Id disabledTasks · Category Governance · Impact 3 · Effort 2 · Components Format & Static AnalysisWhy it matters. Only jacocoTestCoverageVerification is guarded. mcp and plugin-api disable pmdTest; internal-core disables pmdMain, pmdTest and checkstyleTest; and constant-source disables pmdMain and pmdTest, each with a ‘temporarily disabled’ comment (four modules, up from three). internal-core is the module where untested plugin logic sits, so the weakest checks are on the code that benefits most from them.Option. Extend the afterEvaluate guard to every quality task with an allow-list of dated, reasoned entries like coverage exceptions; then fix or burn down the three temporary disables.Question to settle. Should disabling a quality task require the same written reason as lowering coverage, and should it expire?
Id coverageNoData · Category Correctness · Impact 3 · Effort 1 · Components Coverage PolicyWhy it matters. Both the report and the verification task have onlyIf { execution data exists }. If a module’s tests are deleted, filtered out or never run, verification is skipped and check passes with no coverage at all. The guard against disabling verification does not cover this.Option. Fail when a module has main classes but no execution data (with an explicit opt-out for pure-API modules); or compare against a minimum test count.Question to settle. Which modules legitimately have no tests, and can that list be written down?
Id strict100 · Category Evolvability · Impact 3 · Effort 3 · Components Coverage PolicyWhy it matters. In our view, requiring every class to be fully covered pushes teams toward tests that only execute code to hit a branch, and makes each defensive branch cost a test. Only one exception exists today, so the rule seems livable, but generated code, reactive lambdas and rarely taken error paths are common sources of friction. Git history shows the default was lowered to 90/85 on 2026-10-10 (to stop exception sprawl) and raised back to 100/100 on 2026-10-11 in the ‘full coverage’ commit, which also deleted the exceptions; the docs were not updated.Option. Keep 100% with the exception mechanism; or set 95% per class for lines and require 100% only for new code; add mutation testing to check test quality.Question to settle. What is the policy’s real purpose: preventing untested code, or preventing slippage, and is 100% per class the cheapest way to get that?
Id docDrift · Category Operability · Impact 2 · Effort 1 · Components Plugin Hierarchy, Coverage PolicyWhy it matters. build-logic/CLAUDE.md, the improvements note (L3) and the old guide say LINE 90% / BRANCH 85%; the code enforces 100/100. The old guide’s exception table lists eight modules; only an empty block and one web exception exist. The root CLAUDE.md names Gradle 9.0 while the wrapper is 9.8.0. Wrong docs lead developers to expect exceptions they will not get.Option. Correct the docs from the code now; add a test or script that reads the default from jacoco-conventions and prints it for docs.Question to settle. Should rule values be documented once in code comments only, and linked from the docs?
Id selfUnchecked · Category Governance · Impact 2 · Effort 2 · Components Format & Static AnalysisWhy it matters. Spotless is applied per module through quality-conventions. Its Kotlin target build-logic/**/*.gradle.kts is relative to each module folder, so it never matches the included build; the root project applies Spotless with apply false. The convention scripts and root build scripts therefore get no header or format check (by reading the configuration; not run). The one place that decides every rule has the least checking.Option. Apply Spotless in the root project for build-logic and root scripts; add a kotlinGradle target with an absolute path; or add ktlint.Question to settle. Do we want the rule files treated like product code, with review and formatting checks?
Id advisoryTools · Category Correctness · Impact 2 · Effort 2 · Components Format & Static AnalysisWhy it matters. Error Prone has no -Werror or severity overrides, so its warnings print but pass. SpotBugs has no effort or report-level setting; only a small exclude file exists for reviewed false positives. Real findings can scroll by in the log. In our view this is a calibration gap, not a bug: the tools run, but their strictest useful settings have not been chosen.Option. Choose a list of Error Prone checks to promote to error; set SpotBugs effort and threshold; keep exclusions in exclude.xml with a reason each.Question to settle. Which three Error Prone or SpotBugs findings have caused real incidents or review comments here, and should those fail the build?
Id noToolchainResolver · Category Operability · Impact 2 · Effort 1 · Components Java & Test SetupWhy it matters. The toolchain requests Java 25 and neither settings file configures a toolchain resolver (such as foojay), so a machine without JDK 25 cannot build and the error is Gradle’s generic one. CI installs Temurin 25 itself. New contributors hit this first.Option. Add the foojay toolchain resolver plugin to settings; or document the JDK in the README and add a friendlier check.Question to settle. Is a download convenience worth a new plugin in the trusted build path?
Id nodeDrift · Category Operability · Impact 2 · Effort 1 · Components Node Conventions, CI PipelineWhy it matters. Locally the UI uses Node 24.13.1 and pnpm 10.30.1. CI sets CI so the system Node is used: the build job asks for Node 24 (any minor) and pnpm 10.30.1, while the E2E job asks for pnpm 10. A UI build can behave differently in CI than on a laptop. Update (2026-10-15): the build job and the shard jobs pin pnpm 10.30.1 and ask for Node 24 (any minor); the local pin is Node 24.13.1. The E2E job was not re-checked.Option. Read the versions from the catalog in CI (node-version-file or a step that prints them); or pin the exact minor in the workflows and add a check that they match.Question to settle. How often does a Node minor change affect the UI build, and is exact pinning worth the churn?
Id ciManualMap · Category Evolvability · Impact 2 · Effort 3 · Components CI PipelineWhy it matters. A module is listed in settings.gradle.kts, in the paths-filter list of ci.yml, and in the path map and the group_of function of .github/scripts/ci-tasks.sh (the shard planner added in PR #391). The lint job and the planner’s ‘unrecognized module’ error catch omissions, but only when a pull request touches that module or the lint runs. Four places invite mismatches such as a wrong Gradle path or a module in the wrong shard.Option. Generate the filter, the path map and the shard assignment from settings.gradle.kts in one script; or use one dynamic job that asks Gradle which projects changed.Question to settle. Is it acceptable that a mapping error is only caught on a PR for that module?
Id trivyPrGate · Category Security · Impact 3 · Effort 2 · Components CI PipelineWhy it matters. security.yml runs Trivy vulnerabilities with exit code 0 on pull requests and enforces HIGH/CRITICAL only on main, schedule and manual runs. A PR that adds a vulnerable dependency can merge, with the dependency-review job (high severity) as the only other guard. The root trivy task is on demand; the ‘-x trivy’ leftover in ci.yml is gone.Option. Enforce on PRs when the vulnerability database is reliable; or gate on new findings only; remove the unused -x trivy.Question to settle. Which is worse for us: a blocked PR on an old unrelated CVE, or a vulnerable dependency reaching main?
Id fullBuildOnRoot · Category Performance · Impact 2 · Effort 3 · Components CI Pipeline, Catalog & Root BuildWhy it matters. The root path filter includes build-logic/**, config/**, gradle/**, gradlew and settings. This is correct, since those files can change any module’s result, so every Dependabot catalog bump still runs every module. Done (PR #391): the build is split into four parallel shards (platform, infra, plugins, frontend) with shared caches and a fast lint job, so the wall-clock time and the single 30-minute runner limit no longer apply to the whole repository. Still open: the total compute for a catalog bump is the full 27 modules.Option. Split the catalog so bumps for a test library trigger a smaller set; rely on the remote cache.Question to settle. How close is a full build to the 30-minute timeout today, and what happens at 40 modules?
Id cvePins · Category Governance · Impact 2 · Effort 2 · Components Dependency Platform, Plugin HierarchyWhy it matters. PR #373 added patch pins as constraints in build-logic/build.gradle.kts (jackson-core and jackson-databind 2.21.7 and 3.1.7, plexus-utils 3.6.1) and as properties in spring-conventions (jackson-2-bom.version 2.21.7, jackson-bom.version 3.1.7). The comments say to drop each one once Spring Boot ships the fix, but nothing reminds anyone. A pin that is lower than what a later Spring Boot brings would silently hold the library back, and a CVE that is fixed in a newer minor stays hidden by the pin.Option. Put the pinned versions in the catalog with a comment and a date; add a check that fails when Boot’s own managed version is newer than a pin.Question to settle. Who removes a pin when the upstream fix lands, and how do they find out?

CLI

Review of 2026-10-15 against main (after PRs #371, #373 and #374): 1 risk closed (applyMissing: session apply is registered), 4 updated (missingCmds is partly closed; legacyPaths, streamLoose and docsStale have new facts), 2 added (localTimestamps, keywordProblems); the other 13 are unchanged because their code is unchanged.
Id stopMode · Category Functionality · Impact 3 · Effort 1 · Components Command Builders, Resource ClientsWhy it matters. The server’s stop endpoint takes ?mode= with DRAIN (finish in-flight work, the default), CANCEL (cancel at once, let plugins clean up) or KILL (cancel and remove the run). The CLI never sends it, so the polite default is the only option. An operator facing a stuck run has no CLI way to force it.Option. Add --mode with the three values, validated before sending. Document that KILL removes the run.Question to settle. Should a destructive mode like KILL need a confirmation flag in a CLI, or is naming it enough?
Id noPaging · Category Correctness · Impact 3 · Effort 2 · Components Command Builders, Resource ClientsWhy it matters. Session list, summaries, workflow history and workflow executions return at most 500 items by default (1,000 at most) and announce more in an X-Next-Cursor header. The CLI neither sends limit/cursor nor reads the header, so with 800 sessions the user sees 500 and nothing says that 300 are missing. Scripts that look for an execution in history can wrongly conclude it does not exist.Option. Add —limit and —cursor flags; or follow the header automatically with an —all flag; at minimum print a warning when the header is present.Question to settle. Is silent truncation acceptable for a tool used in scripts, or should the CLI fail loudly when it sees a next cursor?
Id dtoDrift · Category Evolvability · Impact 4 · Effort 3 · Components Resource Clients, Test HarnessWhy it matters. Paths and response shapes are copied by hand. A renamed field decodes to an empty value, so a command can print blanks and exit 0. The tests run against fake servers written by the same author, so they confirm the CLI matches the author’s memory, not the server. The improvements note itself says an OpenAPI-generated client was not built.Option. Run a contract test in CI that starts the real server (or reads /v3/api-docs) and checks each path and field the CLI uses; or generate the client; or fail decoding on missing required fields.Question to settle. Is it worth generating the client, which brings many generated types, or is a thin contract test enough for about 30 endpoints?
Id legacyPaths · Category Evolvability · Impact 2 · Effort 2 · Components Resource ClientsWhy it matters. Pause, resume, status, stream and node control use /api/workflow/{sid}/{eid}/.... The server documents these as deprecated in favour of /api/executions/{eid}/.... When they are removed the CLI breaks. In our view the legacy form also has a useful property: the session in the path makes the server check that the run belongs to that session. Update: 12 of the client’s calls still use the deprecated /api/workflow/… forms (pause, resume, stop, status, stream, history and the node commands); the newer commands (retry, breakpoint, signal, result) use the canonical /api/executions/… and /api/sessions/… paths.Option. Move to the new paths and keep session as an optional guard (for example a —session flag); or ask the server team to keep the session check on the new paths and set a removal date for the old ones.Question to settle. Should the new execution-only paths verify a session when one is known, so that moving to them does not weaken the cross-session check?
Id tokenPlain · Category Security · Impact 4 · Effort 2 · Components Root & Global Flags, HTTP Client CoreWhy it matters. The CLI sends Authorization: Bearer ... to any --url. If someone points it at http://prod.example.com the key crosses the network in clear text. Passing --token abc also exposes the key in shell history and in ps output. A 401 gives no hint about how to set a token.Option. Warn (or refuse without a flag) when a token is used with http to a non-loopback host; read the token from a file or prompt; hint at YUKTA_TOKEN on a 401; support custom CA files.Question to settle. Should the CLI refuse plain-http tokens by default, accepting that local test servers need an explicit override?
Id noRetry · Category Availability · Impact 3 · Effort 3 · Components HTTP Client CoreWhy it matters. A 429 (‘at the run limit, try later’) or a network blip fails the command at once. CI scripts must wrap every call in their own retry loop, and they cannot tell the cases apart because the exit code is always 1. Retrying blindly would be unsafe for starts, which is why it is tied to the idempotency key risk.Option. Add opt-in retries (—retries, —retry-wait) for GET requests and 429/503 with the server’s Retry-After if present; retry starts only with an idempotency key.Question to settle. Which failures should a CLI retry by default: only reads, or also 429 answers that the server deliberately gave?
Id idemKey · Category Reliability · Impact 3 · Effort 1 · Components HTTP Client Core, Command BuildersWhy it matters. The server supports Idempotency-Key on POST /api/workflow/start: the same key returns the first run’s id instead of starting a second run. With a 30-second limit, a start that times out is ambiguous: the run may exist. The CLI gives the user no safe way to repeat it, so a pipeline may start the workflow twice.Option. Add --idempotency-key (or generate one per invocation and print it) to workflow start.Question to settle. Should the CLI generate a key automatically for every start so that retries are safe by default?
Id streamLoose · Category Reliability · Impact 3 · Effort 3 · Components SSE StreamingWhy it matters. The server supports Last-Event-ID and sends a ping every 15 seconds. The CLI uses neither. A dropped connection ends the watch, and a clean close exits 0, the same as a finished run. An event not followed by a blank line at the end of the connection is discarded. A silent server leaves the command waiting forever. The new controlbus watch has the same limits: it does not send Last-Event-ID or reconnect.Option. Reconnect with the last id and back-off (bounded); fail if the stream ends before a final state; flush the last partial event; add an idle timeout based on the pings.Question to settle. Should logs and status-stream exit non-zero when the stream ends before the execution reaches a final state?
Id missingCmds · Category Functionality · Impact 2 · Effort 3 · Components Command Builders, Resource Clients, SSE StreamingWhy it matters. Partly closed. Since the first guide the CLI gained controlbus watch (the event stream, with --quiet), result, workflow node retry, breakpoint, signal and session apply. Still without a command: definition validation, session config and workflow list, supervisors, workflow versions (list, diff, rollback, notes), pending approvals, scheduler limits and priority, recover, run search and the control audit, and workers. Operators and CI jobs fall back to curl for these.Option. Add the commands people ask for first (validate, retry, events stream); or generate the commands from OpenAPI; or state an explicit scope for the CLI.Question to settle. What is the CLI for: a full admin tool, or only the handful of commands CI and developers use daily?
Id fileOrJson · Category Operability · Impact 2 · Effort 1 · Components Command BuildersWhy it matters. controlbus command (and session apply) check whether a file exists and otherwise treat the text as inline JSON. A typo in a file name therefore reports invalid JSON: invalid character, which sends the user hunting for a JSON mistake. A directory name is also parsed as JSON.Option. Use @file for files (like curl) or separate --file and --json flags. Or detect text that does not start with { and say ‘file not found’.Question to settle. Would you accept a breaking change to the argument form, given that scripts may already rely on passing a path?
Id outputUneven · Category Operability · Impact 2 · Effort 2 · Components Output Formatter, Command Builders, Root & Global FlagsWhy it matters. Some commands print tables, some plain text, some always JSON. status-stream always prints JSON, heartbeat rows are in random order and -o yaml is accepted and treated as table. session apply would define a second local -o that shadows the global one. Scripts cannot rely on one output rule.Option. Define a rule: -o table|json applies to every command except raw streams; validate the value on the root; give every command a table and a JSON form; sort map keys.Question to settle. Should JSON output be the stable, documented contract for scripts, and table output free to change?
Id errorNoise · Category Operability · Impact 2 · Effort 1 · Components Entry & Wiring, HTTP Client CoreWhy it matters. Cobra prints Error: ... and the usage block, then main prints error: .... The one useful line is hidden in help text in CI logs. The server’s JSON error body (message, error, path) is shown raw. Scripts cannot tell 404, 429, 5xx, timeout and bad usage apart.Option. Silence Cobra’s error and usage printing for runtime errors; parse the error envelope and print its message; define exit codes (for example 2 usage, 3 not found, 4 server, 5 timeout).Question to settle. Which exit codes does your automation need, and is it better to publish them as part of the CLI’s contract?
Id noVersion · Category Operability · Impact 2 · Effort 1 · Components Entry & Wiring, Build & QualityWhy it matters. Releases are published with checksums and provenance, but the binary cannot say which release it is. Bug reports cannot name a version, and a user cannot tell whether their CLI is older than their server.Option. Add yukta version with the tag and commit set through -ldflags -X in make build; optionally warn when the server’s version is newer.Question to settle. Should the CLI check server compatibility on first use, or is a printed version enough?
Id winBuild · Category Operability · Impact 1 · Effort 2 · Components Build & QualityWhy it matters. The Makefile uses shell, awk and grep, and the Gradle tasks call make. The released Windows binary is fine, but building or checking on plain Windows needs WSL or Git Bash.Option. Document it; or move the logic into a small Go or Gradle program; or provide a Windows-friendly wrapper.Question to settle. Are there Windows contributors who build the CLI, or is CI the only builder for that platform?
Id testsTwice · Category Performance · Impact 1 · Effort 1 · Components Build & Quality, Test HarnessWhy it matters. The Gradle check task depends on both goTest and goCoverageCheck, and the coverage check runs all tests again. It doubles the test time on every check.Option. Drop goTest from check (the coverage check already runs the tests) or let the coverage check reuse the result.Question to settle. Is the separate goTest task there for a clearer failure message, which might be worth the extra time?
Id docsStale · Category Governance · Impact 2 · Effort 1 · Components Build & Quality, Command BuildersWhy it matters. The README names binaries like yukta-darwin, a goBuildDev Gradle task, a goCheck task, a ~/.yukta/config file, a --file flag, /api/v1 endpoints and Go 1.21; none exists. The root help text still says ‘Code quality gates for AI-driven development’. Notes disagree on the Go version. A newcomer follows the README and fails at the first command. Update: still true on 2026-10-15 (yukta-darwin, goBuildDev, ~/.yukta/config and /api/v1 are all still in the README).Option. Rewrite the README from the Makefile and the command map; add a docs test that every command shown in the README exists in the tree.Question to settle. Who owns the CLI docs, and would generating the command reference from Cobra remove the problem?
Id emptyReason · Category Correctness · Impact 1 · Effort 1 · Components Command BuildersWhy it matters. workflow node stop always adds reason= to the request. The server’s default reason, ‘Stopped via REST API’, applies only when the parameter is absent, so audit records of CLI stops carry an empty reason unless the user passes —reason.Option. Send the parameter only when —reason is set, or default it to a CLI-specific text such as ‘Stopped via yukta CLI’.Question to settle. Should audit trails be able to tell which client (UI, CLI, agent) sent a stop?
Id localTimestamps · Category Correctness · Impact 2 · Effort 2 · Components Resource ClientsWhy it matters. The server sends java.time.LocalDateTime values with no zone. The new Timestamp type accepts RFC 3339 and, failing that, parses the zone-less form with time.Local. The code comment says it assumes that the server and the CLI share a zone: differences between two zone-less values are right, but a value compared with an RFC 3339 one, or printed next to the user’s own clock, is off by the zone difference. A CI runner in UTC talking to a server in another zone shows wrong run times.Option. Send ISO instants with a zone from the server (the root cause), or add a —server-zone flag; until then print the server’s time as received.Question to settle. Do your CI runners and your Yukta server run in the same time zone?
Id keywordProblems · Category Correctness · Impact 2 · Effort 2 · Components SSE Streaming, Command BuildersWhy it matters. controlbus watch --quiet keeps a line when it matches error|failed|failure|exception|What went wrong (or warn/deprecat), the same regular expressions as the server’s EventSeverity, plus gradle-specific context lines. A log line such as ‘0 errors found’ is shown as an error, and a real failure whose line has none of the words is hidden (a failed node’s own reason is still shown). The two copies of the patterns can drift apart, and each node prints at most 20 problem lines.Option. Have the server send the severity it already computes (the CLI reads it for the server filter) and use only that; or share the patterns from one place.Question to settle. Would you rather see a harmless line too often or miss a failure line?

Core

Id spof · Category Availability · Impact 3 · Effort 3 · Components Entry & Admission, Stream Engine, Observation, Control PlaneWhy it matters. With yukta.ha.mode=postgres several servers share the database and one holds a lease; a standby refuses starts (503) and takes over when the lease runs out, then recovers the interrupted runs and, with yukta.restart.auto-resume, resumes them from the PostgreSQL checkpoints. What is still lost on a takeover: live approval waits, queued starts, scheduler slots and the tracker state of the old leader, and clients must send requests to the leader (there is no proxy; workers listing several control planes find the leader themselves).Option. Route clients to the leader (a load balancer health check on the leader gauge), keep approvals durable, and re-admit queued starts from the log.Question to settle. What outage duration can your users tolerate, and is a lease-length failover plus resume enough for that?
Id remote · Category Scalability · Impact 4 · Effort 3 · Components Stream Engine, Plugin BoundaryWhy it matters. A processor node with engine.placement now runs on a worker over RSocket, with pool and free-slot choice, and the engine’s retries, timeouts and checkpoints wrap it unchanged. What is missing: a lost worker mid-attempt fails the attempt retryably and the retry can land on another worker (tested), a stopping worker drains first (tested), and a worker killed with SIGKILL in its own JVM is tested the same way, no UI list, and the control plane still sees every message. The connection can use mutual TLS (tested with real certificates) but the default is a shared token over plain TCP. In a native image the workers beans must be switched on at build time (-Pyukta.workers.native); registering from a native worker is not verified yet.Option. Failure semantics (phase 4), operations (phase 5), then HA of the control plane (phase 6).Question to settle. Payload limit and secrets on workers are still undecided. Which has the highest cost to reverse?
Id exactlyOnce22 · Category Reliability · Impact 4 · Effort 5 · Components Durability, Plugin BoundaryWhy it matters. A side effect between the plugin acting and the end marker can repeat; remote workers widen the window.Option. Keep at-least-once, publish idempotency patterns, add result caching keyed by message id (F12).Question to settle. Which of your plugins can safely run twice, and which cannot?
Id payload23 · Category Performance · Impact 3 · Effort 4 · Components Stream Engine, DurabilityWhy it matters. Every message is in memory while it flows. Checkpoints no longer cap it (files, or PostgreSQL rows that the database stores out of line), but a very large message is still copied by each node that touches it. Remote workers will make the control plane a data hop.Option. Claim-check references to an object store for payloads over a limit; a frame size limit before workers.Question to settle. What is the largest message you expect in a year?
Id backpressure25 · Category Performance · Impact 2 · Effort 1 · Components Stream EngineWhy it matters. By design (ADR 1): safe, but a slow audit sink can throttle the main path. A node can now opt out with engine.edgeBuffer {size, overflow}: it then drops messages (counted in yukta.edge.dropped) instead of holding its parents. The default stays lossless.Option. Set edgeBuffer on non-critical branches; keep the default everywhere data must not be lost.Question to settle. Which consumers are allowed to fall behind?
Id trackerQ26 · Category Scalability · Impact 3 · Effort 2 · Components ObservationWhy it matters. State events are never dropped, so an event storm becomes memory growth rather than data loss. The depth is now a gauge (yukta.tracker.queue.depth), so growth can be alerted on before it becomes an outage.Option. Alert on yukta.tracker.queue.depth; coalescing progress events remains an option if the alert fires.Question to settle. Would you rather lose events or run out of memory?
Id signals14 · Category Reliability · Impact 2 · Effort 2 · Components Plugin Boundary, DurabilityWhy it matters. Done: an answer is recorded in the checkpoint store (file or PostgreSQL) under signal:<node>, keyed by message id, so a resumed run does not ask again for a message that was already answered. Still in memory: the wait itself, so a message that is produced again after a restart (its parent did not finish) waits for a new signal, and a pending approval is not visible until the node waits again.Option. List open waits from the store; keep the message id stable for nodes that rerun.Question to settle. How long do your approvals take, hours or days?
Id agentLoop · Category Functionality · Impact 2 · Effort 2 · Components Plugin BoundaryWhy it matters. Done: tool-role turns (assistant tool calls and tool results are sent as such) and tools/list follows nextCursor. Still missing: an example loop workflow and streaming.Option. An example workflow that loops LLM_CALL and MCP_TOOL_CALL, and streamed answers.
Id ctlKey · Category Correctness · Impact 1 · Effort 2 · Components Control PlaneWhy it matters. Latent, left as is on purpose: heartbeat and statistics state is keyed by workflow plus node, so two sessions or runs with equal ids would share it. No code emits heartbeats or statistics today, plugins are shared instances and signals carry the execution id, so nothing is affected yet.Option. When a plugin starts to emit them, key by the execution id in the message metadata.
Id ckptIO · Category Performance · Impact 2 · Effort 3 · Components DurabilityWhy it matters. Done: a node keeps one open output file, so a message costs one write and not an open. Still missing: a resume reads a node’s whole output into memory, and the append rate is not measured against a benchmark yet.Option. Stream the reads; measure with the perf/ benchmark.Question to settle. What message rate must a durable run sustain?
Id gaps12 · Category Functionality · Impact 2 · Effort 4 · Components Stream EngineWhy it matters. Saga compensation, node result caching, A2A agent card, eval node, backfill, run search across sessions, secrets manager.Option. Pull in on demand.Question to settle. Which one would a design partner ask for first?

MCP

No backlog IDs are used here: the earlier review findings (P-1 to P-14) are all fixed and are not listed. Risk titles are plain names. Items worded ‘In our view’ are judgement, not a measured defect. Review of 2026-10-15 against main (after PR #373): 2 risks closed (sharedKey, sessionBoundary), 3 partly closed (apiKeyOpen, noActor, noPaging), 2 added (bodyReparse, rejectsUnlogged); the others are unchanged because their code is unchanged.
Id apiKeyOpen · Category Security · Impact 3 · Effort 1 · Components Endpoint gateWhy it matters. Done (PR #373): boot refuses to start on a non-loopback address without a key, the REST keys (named, with roles) now also guard /mcp, and each tools/call is checked against the key’s role and sessions. Still open: with no key the endpoint is open on loopback, so any local process can call create_session, which installs workflows; and the start-up guard accepts yukta.mcp.api-key alone, which protects /mcp but leaves /api open (boot guardMcpKeyOnly).Option. Say in the setup text that loopback means ‘any local process’; fix the guard so one setting covers both; consider generating a key at first start.Question to settle. Should exposing the server beyond loopback without a key be a startup error, rather than a documented choice?
Id noActor · Category Governance · Impact 2 · Effort 1 · Components Ownership check, Run & controlWhy it matters. Done: when named keys are configured, the filter puts the actor ‘key:<name>’ into the Reactor context, so a control command is audited with the key that sent it. Still open: with the single yukta.mcp.api-key (or no key) no actor is set, so an agent and a person look alike. The audit shows the key, not the tool call or the agent behind it.Option. Use named keys per agent; add the tool name to the actor text.Question to settle. Is ‘which agent did this’ a requirement for audit, or is ‘it came through MCP’ enough?
Id noIdempotency · Category Reliability · Impact 3 · Effort 1 · Components Tool facade, Run & controlWhy it matters. REST accepts an Idempotency-Key header so a repeated start returns the first execution. The tool has no such argument. An agent whose call times out cannot know whether the run began, and a retry can start a second run. For a deploy workflow that is a double deploy. RETRY and RESTART have the same weakness.Option. Add an optional idempotencyKey argument to start_workflow and pass it to core’s existing key path. For restart and retry, return the earlier new id when the same key repeats.Question to settle. Would agents reliably supply a key, or should the server derive one (for example from session, workflow and a time window)?
Id stopParity · Category Functionality · Impact 3 · Effort 2 · Components Tool facade, Run & controlWhy it matters. start_workflow cannot pass trigger data, while the REST start can, so workflows that read a payload (a ticket, a commit id) cannot be started with data by an agent. STOP_ALL is fixed to DRAIN, while REST can choose CANCEL or KILL, so an agent cannot force-stop a run that will not drain.Option. Add an optional payload argument and a stop-mode argument. Add a short parity checklist to review whenever web changes.Question to settle. Which web operations are meant to be reachable by agents, and who keeps the two surfaces in step?
Id sessionGuideWrong · Category Correctness · Impact 4 · Effort 2 · Components Session providerWhy it matters. get_session_creation_instructions includes an example whose fields (id, from, to, port) and plugin types (webhook-trigger, gradle-checker, log-aggregator) do not exist. The real fields are nodeId, source, target, sourcePort, plus a workflowId and description per workflow, and the server rejects unknown JSON properties. The agent’s first attempt from the example fails, and the ‘common errors’ text suggests the failures are about plugins, not field names.Option. Build the example from real types: take a registered plugin type and serialise a real record. Add a test that parses the example into SessionConfigData and validates it.Question to settle. Should guides be generated from the code (single source of truth) rather than written as text?
Id pluginGuideWrong · Category Correctness · Impact 3 · Effort 2 · Components Plugin catalogueWhy it matters. The templates say ‘extends TriggerPlugin’ with ‘execute(WorkflowContext)’ and the text mentions a WorkflowRegistry. In the real plugin API, the three kinds are interfaces with stream-based methods (a processor has process, a terminal has consume) and a type name. An agent that follows the guide writes code that fails to compile, and the checklist items (register in WorkflowRegistry) point at nothing.Option. Replace the templates with ones copied from a real plugin and compile them in a test. Or drop the tool and point to the plugin-api documentation.Question to settle. Is the agent-assisted writing of plugins a supported path, or should the guide be removed until it can be tested?
Id portsNoConfig · Category Correctness · Impact 2 · Effort 2 · Components Plugin catalogueWhy it matters. The tool calls getOutputPorts() without a config. A plugin whose ports depend on its configuration (a router with named outputs) shows [‘default’]. An agent then draws edges with the wrong sourcePort, and the workflow fails when prepared.Option. Accept an optional config argument and call the config-aware method, or state in the description that ports can depend on config.Question to settle. Is a plugin’s port list part of its public contract, or an internal detail agents should not rely on?
Id configSchemaHidden · Category Functionality · Impact 3 · Effort 2 · Components Plugin catalogueWhy it matters. Plugins publish a typed list of configuration fields (name, type, required, default, options), but get_plugin_details returns only a free-text usage pattern. Agents must guess config keys, which is the most common cause of invalid sessions.Option. Add the field list to PluginDetails as a record, so the output schema describes it.Question to settle. Do we trust each plugin’s field list to be complete, or does it need review before it is the agent-facing source of truth?
Id healthLinear · Category Performance · Impact 2 · Effort 3 · Components Status aggregatorWhy it matters. get_control_bus_status reads the config and history of every session, eight at a time. With hundreds of sessions each call does hundreds of reads, and agents call it often because it is the ‘overview’. A session that errors is dropped from the totals with only a log warning, and the result does not say so.Option. A one-pass summary query in core. A short-lived cache. A ‘skippedSessions’ count in the result.Question to settle. How many sessions do we expect in one deployment, and is a snapshot a few seconds old acceptable?
Id noPush · Category Functionality · Impact 3 · Effort 4 · Components Protocol runtime, Run & controlWhy it matters. The transport is stateless, so the server cannot send ‘run finished’ or ‘node paused’. Agents loop on get_workflow_status. Polling too fast wastes load; polling too slow delays reaction (a breakpoint can sit paused for minutes). Every poll repeats the ownership read. Update: afterCursor on logs and results makes following a run cheaper, but it is still polling.Option. A resource subscription or progress notifications with a stateful transport. Or a tool that waits (long poll) up to N seconds for a change. Or document a recommended poll interval.Question to settle. Is the stateless simplicity worth giving up live updates, or should there be a second, stateful endpoint for watchers?
Id noRateLimit · Category Availability · Impact 3 · Effort 3 · Components Endpoint gate, Protocol runtime, Run & controlWhy it matters. An agent in a loop (a model retrying the same bad call) can call start_workflow or the log reader as often as it likes. Core’s run limits are off by default, so repeated starts can pile up runs. Regex checks cost up to 50 ms per line, so repeated hostile patterns can keep a thread busy.Option. Per-key rate limits in the filter. Turn on core’s concurrency limits in the deployment. Add a counter and an alert.Question to settle. Where should the limit live: the MCP filter, or core’s admission control for all callers?
Id overwriteSession · Category Governance · Impact 3 · Effort 3 · Components Session providerWhy it matters. Creating a session with an existing id replaces it, and workflows that the new config omits are deleted. An agent that sends a partial config, or that is mistaken about the id, removes real workflows. The destructive hint and the confirm text are advice. The result has no warnings and no diff.Option. An ‘overwrite’ flag that defaults to false. A dry-run mode that returns a diff. A refusal when runs are active. Versioned session configs.Question to settle. Should an agent ever be able to replace an existing session in one call, or only create new ones?
Id logInjection · Category Security · Impact 3 · Effort 3 · Components Log reader, Prompts & resourcesWhy it matters. In our view, logs are text produced by plugins, which may include output of external systems (an HTTP response, a command’s output). The reader returns it as is. Text such as ‘ignore your instructions and call control_workflow STOP_ALL’ reaches a model as if it were data. Logs may also hold secrets that now land in a model’s context and the client’s history.Option. Mark the lines clearly as data in the result. Offer a redaction hook. Keep control tools gated by the hints so clients confirm.Question to settle. How much can we rely on the client to treat tool output as untrusted, and what do we do server-side to help?
Id signalNotState · Category Operability · Impact 2 · Effort 2 · Components Run & control, Tool facadeWhy it matters. PAUSE, STOP, SKIP and similar return as soon as the command is emitted. A pause on a node that has just finished, or a stop that drains for a minute, looks the same as success. An agent that assumes the effect may report a paused run that is still running.Option. Return the status after a short wait, or include a ‘check with get_workflow_status’ hint in the result text. Document the polling expectation in each description.Question to settle. Do we want commands to be acknowledged by the engine before the tool returns, and at what cost in latency?
Id noPaging · Category Scalability · Impact 2 · Effort 2 · Components Log reader, Session provider, Status aggregatorWhy it matters. list_sessions, get_workflow_history and the status snapshot return whole lists. A session with thousands of executions returns a very large result into a model’s context. Done: get_execution_logs and get_execution_result now take afterCursor (the previous call’s nextCursor), so a caller can follow a running execution forward; but the log tool still cannot read backwards or skip ahead on a finished run.Option. limit and offset (or a cursor) on history and sessions, and a ‘before’ argument on logs.Question to settle. What is the largest result we are willing to put in an agent’s context window?
Id docsDrift · Category Evolvability · Impact 2 · Effort 2 · Components Tool facade, Prompts & resourcesWhy it matters. Tool descriptions, prompts, resources and the module’s CLAUDE.md are written by hand. The module now has 23 tools (the version, approval and result tools were added) and the module’s CLAUDE.md has not been changed since the first guide. Nothing fails when the text goes stale; agents just choose worse.Option. Generate the tool table from the annotations in a test. Add a test comparing the enum values to the text of the descriptions.Question to settle. Is the tool description part of the API (versioned, reviewed with the code), and who owns it?
Id pmdTestOff · Category Evolvability · Impact 1 · Effort 2 · Components Protocol runtimeWhy it matters. build.gradle.kts has pmdTest switched off with a note to re-enable it later. The root rules say Checkstyle, PMD and SpotBugs apply to test code too, so this module is the exception and test-code issues accumulate unseen.Option. Fix the test-source findings and delete the switch.Question to settle. Is the exemption still wanted, or just forgotten?
Id bodyReparse · Category Security · Impact 2 · Effort 3 · Components Endpoint gateWhy it matters. For any key that is not an unrestricted ADMIN, McpEndpointFilter buffers the POST body (up to 1 MiB), reads it with its own Jackson mapper in McpToolAccess, and passes the same bytes on to the MCP runtime. The permission therefore depends on both readers agreeing about the method name, the tool name and the sessionId (batch arrays, duplicate keys, unusual encodings). A difference would let a VIEWER key run an OPERATOR tool. Requests that are not POST skip the check entirely. This is our judgement of a risk class; no mismatch was found.Option. Add tests that send batches, duplicate keys and odd encodings for every tool; or enforce the role inside the tool methods, from the authenticated key in the Reactor context, so there is one reader.Question to settle. Is it safer to check the role where the tool runs than in front of the parser?
Id rejectsUnlogged · Category Operability · Impact 2 · Effort 1 · Components Endpoint gateWhy it matters. McpEndpointFilter has no log line and no counter, and it runs before the access-log filter. A brute-force attempt, a wrong key in a client, or an agent that keeps calling a tool its key may not use is invisible. The same gap exists in the REST key filter.Option. Count rejections by reason and key name, and log the first of each per minute.Question to settle. How would you notice an agent that is repeatedly refused?

Messaging

Risk ids are descriptive names. Old review items M1 to M13 for this module are all fixed and are not listed. Review of 2026-10-15: the module code is unchanged since the first guide; one risk was closed (a skipped processor now goes through the channel) and two were added (the envelope as a worker wire format, and edge-buffer drops that a channel never sees). No backlog identifiers apply to the remaining items.
Id unenforced · Category Functionality · Impact 3 · Effort 3 · Components Envelope Contract, Header & History Ops, Port Routing (core)Why it matters. The envelope offers expiration, reply-to, format indicator, retry count, control flag and the sequence fields, and gives them real accessors and withers. Nothing reads them to change behaviour. A plugin author who sets expiration expects the message to be dropped when late; it is delivered anyway. Whoever reads retryCount sees zero because the engine counts attempts in a header instead. Each unused field is a promise the code does not keep. Update: core now writes some of them on its own messages: Splitter fills in the sequence fields, correlation id, reply-to, expiration and format indicator for its children, and the engine adds failure.* headers. Still nothing reads expiration, format indicator, retry count or the control flag.Option. Enforce the ones that matter (an expiry check in the inbound hook; a sequence-aware aggregate) and delete or mark the rest as reserved. Or document each as informational in the interface.Question to settle. Which of these fields do you want as real features in the next year, and which should be deleted so nobody trusts them?
Id remoteSpi · Category Evolvability · Impact 4 · Effort 4 · Components Channel SPI, Direct ChannelWhy it matters. The SPI takes a Reactor stream of in-process objects and returns one. There is no acknowledgement, no redelivery, no serialisation rule and no signal for ‘the other side is slow’. The only implementation does nothing. If a team plugs a network or disk transport into this hook, they will discover the missing contract the hard way.Option. Write the contract first: what a channel must do on failure, whether it may drop, how it signals back-pressure, how messages are encoded. Build one real channel (for example a durable file channel) to test it.Question to settle. Is this SPI meant to carry messages between machines, or only to decorate local streams? The answer decides how much contract it needs.
Id conditionalOrder · Category Reliability · Impact 3 · Effort 2 · Components Default Wiring, Channel SPIWhy it matters. The default provider is guarded by @ConditionalOnMissingBean, a condition built for auto-configuration classes that Spring processes last. Here it is on a normal class found by component scanning, with no auto-configuration registration. A custom provider declared in a class that Spring reads later could end up registered next to the default. Core would then see two providers and the server would not start. In our view the tests only prove the easy order.Option. Register the default through AutoConfiguration.imports, or make the provider injection tolerate several beans with a clear rule, or add a startup check that fails with an explicit message.Question to settle. Has any team actually added a custom provider in a scanned class, and in which order did Spring read it?
Id payloadSize · Category Performance · Impact 4 · Effort 4 · Components Envelope Record, Header & History OpsWhy it matters. The payload is any Java object and nothing here limits its size. Each message is carried through memory and copied into checkpoint files by core. A 50 MiB payload is passed by reference between nodes but is also serialised to JSON for the checkpoint, and cannot be replayed once it passes core’s cap. This is the messaging side of core’s large-payload risk.Option. Add a claim-check pattern: store the large body elsewhere and carry only a reference in the message. Add an optional size check in the inbound hook.Question to settle. What is the largest message you expect within a year, and who should refuse it: the envelope, the channel or the plugin?
Id untypedPayload · Category Correctness · Impact 3 · Effort 4 · Components Envelope Contract, Envelope RecordWhy it matters. Message<T> has a type parameter, but every stream in the engine is Flux<Message<?>>, so the type is erased at the boundary. Producers and consumers agree only by convention. A checkpoint read-back turns the payload into plain JSON values, so a plugin that stored a Java record gets a map after a resume. The formatIndicator field was meant to carry this information, but nobody uses it.Option. Add an optional schema or type hint that is checked by the inbound hook; or define a canonical payload model (JSON tree) and say so.Question to settle. Should the engine promise that a payload survives a resume with its original type, or only as a JSON tree?
Id wideContract · Category Evolvability · Impact 2 · Effort 3 · Components Envelope Contract, Envelope Record, Build & TestsWhy it matters. The interface has 22 accessors and 18 withers, and only one class implements it. Every new field means edits in the interface, the record, two copy paths and the tests. In our view the interface adds change cost without the benefit of alternative implementations, and the 22-slot record is already large.Option. Keep the interface small (read-only accessors plus a few core withers) and let the record carry the rest; or accept the width and add a test that fails when a field is not copied.Question to settle. Do you expect a second implementation, for example a lazy one for large payloads? If not, why keep the interface?
Id copyDrift · Category Correctness · Impact 3 · Effort 2 · Components Envelope Record, FactoriesWhy it matters. The instance withPayload and the static from each copy every field one by one. If a new field is added to the record and one path is forgotten, derived messages silently lose it. One test checks from field by field today. Nothing prevents the paths from drifting.Option. Implement both through toBuilder() (which copies all fields) and change only the payload; or generate the copy.Question to settle. Would a test that compares all record components by reflection be enough, or should the copy be done by the compiler?
Id shallowImmutable · Category Correctness · Impact 3 · Effort 3 · Components Envelope Record, Header & History Ops, History Stamp (core)Why it matters. The envelope itself cannot change, but the payload object and every object stored in metadata are shared by reference. Core fans out the same message to every child. If one plugin edits a list inside the payload, a sibling branch sees the edit, possibly half-way through its own work. Bugs of this kind show up rarely and are hard to reproduce.Option. Document ‘payloads are read-only’; add defensive copies in plugins that edit; or provide a payload copier or freeze step for collections in the inbound hook.Question to settle. Would you pay a copy per fan-out to make payloads safe, or rely on plugin discipline?
Id historyCap · Category Operability · Impact 2 · Effort 2 · Components Envelope Record, History Stamp (core)Why it matters. Only the last 64 node ids are kept and nothing records that older entries were dropped. A message that loops or passes through many nodes loses the beginning of its journey. A reader cannot tell ‘started at node 1’ from ‘the first 40 were dropped’. The trace id and checkpoints remain, so lineage can still be rebuilt elsewhere.Option. Keep a counter of dropped entries, or the first entry plus the last N. Or move lineage to the tracker and keep only a short window in the message.Question to settle. What question do you want history to answer, and is a sliding window enough for it?
Id lineageReset · Category Correctness · Impact 3 · Effort 3 · Components Factories, History Stamp (core)Why it matters. Splitter and Aggregate build their outputs with create. A split child has a new id and an empty history. An aggregate keeps only the trace id of the first message in the batch and drops metadata, correlation and sequence. After either node, lineage questions (‘where did this come from?’) can be answered only through the trace id and the child’s correlation id. The behaviour is deliberate but not written down as a rule.Option. Carry the parent’s history (or a link to it) into children and a list of source ids into an aggregate. Document the rule either way.Question to settle. After a split, is a child ‘the same message’ or ‘a new one with a parent’? The answer decides what lineage means.
Id channelErrors · Category Reliability · Impact 3 · Effort 3 · Components Channel SPIWhy it matters. Core’s per-message retry and error routing wrap the plugin call. The channel wraps the stream around it. An exception thrown in inbound or outbound therefore fails the node’s stream instead of being retried or sent to the error port, and one bad message can stop the node. The default channel cannot throw, so this is untested.Option. Define the failure contract for channels. For example, run the channel inside the policy for per-message channels, or document that it must never throw.Question to settle. If a channel fails on one message, should that message dead-letter, or should the whole node stop?
Id idNotUnique · Category Correctness · Impact 2 · Effort 2 · Components Envelope Contract, FactoriesWhy it matters. Because from keeps the id, the same id appears on the output of every node that derives from it, and on retries. A store or log that uses the id alone as a key will overwrite. The interface says so, but the field is called ‘id’ and invites misuse. Core’s checkpoint store keys by (execution, node, id) for this reason.Option. Add a separate per-output sequence or a ‘lineage id’ field; or rename the accessor to make the meaning obvious.Question to settle. Do you need a unique id per output, for tracing a single hop?
Id forgeable · Category Security · Impact 3 · Effort 3 · Components Header & History Ops, Envelope ContractWhy it matters. A plugin can set trace id, correlation id, workflow id, priority and the control flag on any message it emits. Core overwrites only the source node. Today the control flag is not read and the control bus ignores data messages, so the exposure is small. Another consumer could one day trust one of them. In our view the module should say which headers are trusted. The engine’s own failure.attempts, failure.exception and failure.time headers are ordinary metadata too: a plugin can overwrite them on a message it emits.Option. Define which headers are engine-owned and re-stamp them in core, as with the source node; or sign engine-owned headers.Question to settle. Which headers should a plugin never be able to set?
Id metadataCopy · Category Performance · Impact 2 · Effort 2 · Components Envelope Record, Header & History OpsWhy it matters. withHeader copies the map into a mutable one, changes it, then the constructor copies it again into an unmodifiable one and scans it for nulls. A plugin that sets ten headers one by one does it ten times, each time a bit bigger. Fine for a handful of headers; a cost for metadata-heavy flows like LLM usage or HTTP response headers.Option. Offer a bulk withHeaders(map), or make plugins use toBuilder().metadata(...). Measure first.Question to settle. How many headers does a typical message carry by the end of a workflow?
Id nullSilent · Category Correctness · Impact 2 · Effort 1 · Components Header & History Ops, Envelope RecordWhy it matters. Setting a header to null removes it, and a map with null values has those entries dropped, without a log or an error. A plugin that sets a header to null to mean ‘unknown’ will see the key disappear and may then treat it as ‘never set’. The rule is documented and tested, but it differs from how a plain map behaves.Option. Keep the rule but add a sentinel for explicit absence, or log at debug level when entries are dropped.Question to settle. Is ‘unset’ and ‘null’ the same thing for your headers?
Id wireFormat · Category Evolvability · Impact 3 · Effort 3 · Components Envelope Contract, Envelope RecordWhy it matters. Remote workers (core workers module, WorkerNodeExecutor) send every message as JSON and rebuild it with mapper.convertValue(frame.message(), DefaultMessage.class). So the 22 record components are now a network contract: adding, renaming or retyping a field changes what an older worker or control plane understands, and nothing in the record or the frame carries a version. The payload and metadata values come back as plain JSON values (Map, List, String, Number), not the Java types the sender held. A plugin that stores a Duration or a POJO in a header sees a different type on a placed node than on a local one.Option. Decide the wire shape on purpose: a version field on the frame, a test that round-trips every field and checks metadata types, and a rule ‘headers are JSON values only’. Check this together with the claim-check decision for large payloads.Question to settle. Which header values do your plugins store that are not plain JSON, and would they survive a trip to a worker?
Id edgeDropsUnseen · Category Operability · Impact 2 · Effort 1 · Components Channel SPI, Direct ChannelWhy it matters. A node with engine.edgeBuffer gets a bounded buffer in front of it, and core builds it as the argument to channel.inbound (ProcessorNodeAssemblerStrategy). A message dropped by the buffer therefore never reaches the channel. The only trace is the counter yukta.edge.dropped{type}, which has no node or execution tag. An audit, signing or metering channel that is meant to see every message cannot see these, and the SPI has no way to be told.Option. Document that a channel sees only what the buffer let through, or move the buffer inside the channel hook, or add a node tag to the counter. The first is enough while only the direct channel exists.Question to settle. Is a dropped message something an audit channel must record?

Persistence PostgreSQL

Risk ids are descriptive names. No backlog identifiers apply: this is the first guide for the module, written on 2026-10-15 from the code of main. Items worded ‘looks’ or ‘can’ are judgement from reading the statements, not measured defects.
Id logReplaceAll · Category Reliability · Impact 4 · Effort 2 · Components Execution log, Leader leaseWhy it matters. Startup recovery and takeover recovery call ExecutionLogStore.replaceAll with the runs to keep. In this module it runs DELETE FROM execution_log and then one INSERT per kept event, each waited for up to 30 s, with no transaction. A crash, a lost connection or a leader change after the delete leaves the log empty or partial. The log is what tells recovery which runs were interrupted and what their start payload was, so every run that could have been resumed is lost at once. With up to 500 runs of many events each, the window is also long (one round trip per event). The previous leader, if still alive, may append while the delete is running.Option. Do the rewrite in one transaction, or delete only the rows that fall outside the kept runs instead of replacing everything; batch the inserts.Question to settle. What would you lose if the server died while recovery was rewriting the log, and how would you know?
Id logDropsEvents · Category Reliability · Impact 3 · Effort 3 · Components Execution logWhy it matters. append puts the event in a queue of 10,000 and returns. If the queue is full the event is dropped and an error line is logged. A batch that fails is retried one event at a time, and an event that still fails is logged and skipped. Events still in the queue die with the process. The run itself is told nothing, so a lost RUN_STARTED means the run cannot be resumed and a lost RUN_FINISHED makes a finished run look interrupted.Option. Count dropped and failed events in a meter and alert on it; for the start and end events wait for the insert (or retry) before the run continues; raise the queue only if the cause is bursts.Question to settle. Is a lost run-end event acceptable, given that recovery then marks a finished run ABORTED?
Id fencingUnused · Category Reliability · Impact 3 · Effort 3 · Components Leader leaseWhy it matters. The lease row carries a fencing number that grows with every change of holder, and fencingToken() returns it, but no write in this module or in core uses it. A leader that was paused longer than the lease (a long stop-the-world pause, a suspended VM, a network stall after it checked isLeader()) can still finish a write after another server has become leader. The local validity check protects against a server that cannot reach the database, not against one that is stuck between the check and the write.Option. Pass the token with the writes that matter (the log append, checkpoint writes, definition saves) and refuse a write whose token is older than the one in the lease row, for example by comparing in the same statement.Question to settle. Which write would hurt most if an old leader finished it after a takeover?
Id noTransactions · Category Correctness · Impact 3 · Effort 3 · Components Definitions & sessions, Node checkpointsWhy it matters. These operations are several statements with no transaction: removing a workflow (versions, then the current row), removing all workflows of a session, applying a session config (the row, then each workflow), a save followed by the prune of old versions, and a checkpoint save followed by the end marker. A failure in between leaves a workflow with versions and no current row, a half-applied session, 21 versions, or a node with all its output and no stamp (which a resume handles by running the node again). There are no foreign keys either, so nothing in the database notices.Option. Wrap the multi-step methods in a transaction with Spring’s TransactionalOperator (R2DBC supports it), or merge them into single statements; add foreign keys with cascading deletes where the tables are parent and child.Question to settle. After a failed apply, would an operator be able to tell which workflows of the session were saved?
Id blockingCalls · Category Performance · Impact 3 · Effort 3 · Components Connections & drivers, Node checkpoints, KV and chat historyWhy it matters. isComplete and isFinished (30 s), clear (30 s), the history load and save (10 s) and the lease release (10 s) convert a Mono to a future and wait. The interfaces they implement are blocking by design, so the module cannot make them non-blocking by itself. On a Reactor or event-loop thread a slow database holds that thread for the whole wait, once per node on a resume, and a timeout is silently treated as ‘unfinished’ or ‘empty’.Option. Make the interfaces reactive (see the plugin-api guide) or have the callers run these methods on the blocking scheduler; meter the wait time.Question to settle. How many nodes does your largest run have, and what is the database latency on a bad day?
Id compactionCost · Category Performance · Impact 3 · Effort 3 · Components Execution logWhy it matters. Every 10,000 appended events the writer runs a delete whose subqueries group the whole execution_log by execution id (ordering by the maximum sequence) and test each run for a start without an end with bool_or over the JSONB type. There is no index on the event type, so it reads every row. It runs on the single writer thread, so appends queue up behind it, and the queue holds only 10,000 events.Option. Store the event type (and an ‘open run’ flag) in real columns with an index; trim by a time or sequence bound instead of grouping everything; run the trim in a separate task.Question to settle. How long does the trim take with 500 runs of 2,000 events each, and what happens to the queue meanwhile?
Id readsWholeSets · Category Scalability · Impact 3 · Effort 3 · Components Execution log, Node checkpointsWhy it matters. readAll loads every row of the execution log (up to 500 runs) at start and at every takeover; get on a checkpoint reads every row of the node to return the last one; getAll collects a node’s output on resume; findAll loads all definitions of a session. Memory use and latency grow with the amount of history, and a very large node output is held in memory on resume (the table has no size cap on purpose).Option. Read the log in a cursor and process run by run; fetch only the last row for get (order by sequence descending, limit 1); stream node output into the replay.Question to settle. What is the largest node output you expect to replay?
Id historyTtlScan · Category Performance · Impact 2 · Effort 1 · Components KV and chat historyWhy it matters. With yukta.session-memory.ttl set, each save runs DELETE FROM session_history WHERE updated_at < now() - ..., and the table has no index on updated_at, so every chat turn scans the table. The delete happens only on a save, so with no traffic expired sessions stay, and load does not check the ttl, so an expired session is still returned until some other session is saved.Option. Add an index on updated_at (a V7 migration); delete at most every N seconds, or in a scheduled task; check the age in load.Question to settle. Is a conversation older than the ttl allowed to reappear?
Id kvCapRace · Category Correctness · Impact 2 · Effort 2 · Components KV and chat historyWhy it matters. A new key is inserted only if count(*) of the table is below maxEntries. Two concurrent statements can both read a count below the cap (the default isolation does not serialise them) and both insert, so the cap can be passed by the number of concurrent writers. The count is also taken for every new key. The table is shared by every KV_STORE node of every workflow, and keys are only separated by the optional namespace.Option. Document the cap as approximate, or serialise new-key inserts with an advisory lock; keep a counter row instead of counting.Question to settle. Does anything depend on the cap being exact?
Id noDelete · Category Functionality · Impact 2 · Effort 3 · Components Definitions & sessionsWhy it matters. The SessionConfigStore interface has no delete, so rows in session_config stay for ever. WorkflowDefinitionStore.removeAll exists in every store but has no caller in core. Run history of a deleted session is trimmed only by the global 500-run rule. Anyone who needs to remove a tenant’s data (a customer asks) must delete from nine tables by hand.Option. Add a session delete to the interface and implement it for all stores (definitions, config, log events of the session’s runs, checkpoints, history) in one transaction.Question to settle. How would you answer a request to delete everything about one session?
Id noRetention · Category Operability · Impact 2 · Effort 3 · Components Schema migrationsWhy it matters. Checkpoints are cleared when core evicts a run from its archive of retained executions (10 by default) and for runs dropped at recovery, but a run that never reaches either path leaves its rows. kv_entry grows with distinct keys. session_config and its definitions only grow. workflow_definition_revision is bounded to 20 per workflow, but not the number of workflows. There is no sweep, no size alert and no documented clean-up job.Option. Add a periodic clean-up of checkpoint rows whose run is not in the log, document the sizes to watch, and add them to the metrics.Question to settle. Which table will be the largest after a year, and who looks at it?
Id twoConnections · Category Security · Impact 2 · Effort 2 · Components Connections & driversWhy it matters. Flyway needs a JDBC URL, user and password; the application needs an R2DBC URL. Nothing in the module separates the two accounts, and the simplest setup uses one account for both, so the running application can alter or drop tables, and an SQL injection somewhere else in the application would have the same reach. The settings are two places that can drift (a changed host or database name in one).Option. Document two accounts (migrator with DDL rights, application with data rights only) and show the grants; fail at start if the application account can create tables, as an option.Question to settle. Who holds the password of the database owner, and where is it written?
Id halfDurable · Category Operability · Impact 3 · Effort 2 · Components Auto-configuration, KV and chat historyWhy it matters. Five properties select five groups of stores, and nothing checks that they agree. PostgreSQL definitions with in-memory checkpoints look durable but cannot resume a run; yukta.ha.mode=postgres without PostgreSQL checkpoints lets a standby take over but not continue a run from its recorded output (auto-resume then reruns everything); a typo in a store-type value silently selects the default.Option. At start, log one line listing which store is in use for each kind of data; warn when HA is on and checkpoints are not in PostgreSQL; refuse unknown store-type values.Question to settle. Can an operator tell from the running server which stores are durable?
Id noStoreMetrics · Category Operability · Impact 2 · Effort 2 · Components Connections & drivers, Execution logWhy it matters. The module registers one meter, yukta.leader. There is no health indicator, no meter for the log queue depth, dropped or failed events, statement time, retries or lock waits. The only signals are log lines. Combined with the silent defaults (‘unfinished’, ‘empty’, ‘event dropped’) a degraded database can look healthy from outside.Option. Add meters (queue depth, drops, failed batches, statement latency) and a health indicator that runs a trivial query with a short timeout.Question to settle. What would page you if the log writer stopped inserting?
Id heldBytesDrift · Category Operability · Impact 1 · Effort 1 · Components Node checkpointsWhy it matters. bytesHeld() (shown as the core meter yukta.checkpoint.bytes) adds the length of the JSON text in characters on save and subtracts the database’s byte count on clear, in this JVM only. It starts at zero after a restart although the table holds data, and on a standby it stays zero. With non-ASCII payloads characters and bytes differ.Option. Compute the value from the table on demand (a sum of octet_length), or label the meter as ‘written by this process’.
Id sessionConfigGaps · Category Functionality · Impact 2 · Effort 2 · Components Definitions & sessionsWhy it matters. The execution timeout and the log directories returned for a session come from the global SessionConfigProperties, not from the table, so a per-session setting cannot be kept. getAllConfigs runs about seven queries for one session, and getSessionIds lists only sessions that have a config row, so a session that has definitions but no row is invisible. Initiator, description, initiated time and tags are written only while empty, but an apply replaces the initiated time each time.Option. Store the per-session values or drop them from the interface; read a session in one query; list ids from the union of both tables.
Id noMigrationPlan · Category Evolvability · Impact 2 · Effort 3 · Components Schema migrations, Tests & buildWhy it matters. Six migrations exist and the test support runs them on an empty database. There are no down scripts, and no test upgrades a database that already holds data written by an older version. The stored JSON documents have no version field, so a renamed record component changes what old rows mean without any migration noticing.Option. Add an upgrade test that loads rows written by the previous release and migrates them; add a version field to stored documents; document how to roll back (restore a backup).Question to settle. How would you roll back a release that includes a bad migration?

Plugin API

IDs such as G18 and G22 refer to the plugins module’s improvements list, which cross-references this module. The rest are findings from reading the code. Review of 2026-10-15 against main: no risk was closed; the SpEL sandbox risk is partly closed (method allow-list), the store risk is now concrete (PostgreSQL stores), the core-coupling risk grew slightly, and two risks were added for the new shared run-budget and approval-wait helpers.
Id reviewNoTimeout · Category Availability · Impact 4 · Effort 2 · Components Supervisor SPI, Control commandsWhy it matters. review() has no time limit in the SPI, and core’s reviewer does not add one. Core runs the commands of one run one after another, and the review is part of each command’s turn. If one supervisor’s review never completes (an external call stalls), later commands for that run wait behind it, including a StopWorkflow from a person. All runs are affected if the reviewer hangs on every call. The same wait now also covers the two commands added for the scheduler (SetLimitsCommand, SetPriorityCommand), which pass through review too, so a hung reviewer can block an operator who is trying to change the limits during an overload.Option. Add a timeout to each review in core and define the outcome (deny, or allow for stop); exempt StopWorkflow from review or give it a short-circuit; require implementers to use a timeout and test for it.Question to settle. When a safety check cannot answer, should the command be allowed or denied, and should a stop ever be vetoable?
Id decideStall · Category Reliability · Impact 3 · Effort 2 · Components Supervisor SPIWhy it matters. ControlPlanePlugin.supervise handles events one at a time (concatMap). The Javadoc asks decide() to finish in bounded time, but nothing enforces it. A model call that hangs stalls every later event for that plugin, so the plugin stops reacting while looking healthy.Option. Apply a default timeout in the default supervise (timeout counts as no opinion); expose the limit as a setting; surface a stalled-plugin metric.Question to settle. Is a single stuck decision better handled by skipping to the next event or by failing the whole supervisor so it resubscribes?
Id orderOpposite · Category Evolvability · Impact 2 · Effort 1 · Components Supervisor SPI, Command processingWhy it matters. Supervisor order(): lowest runs first. ControlSignalProcessor getPriority(): highest wins. A developer who sets ‘priority 1 = most important’ on both gets the reverse of what they meant on one of them. Built-ins use 10-20 for processors, so a wrong guess is easy.Option. Rename to make direction obvious (reviewOrder, overridePriority); or align both to one rule; add a startup log of the effective order.Question to settle. Would aligning the two rules be worth a breaking change, given how few external implementers exist today?
Id spelLimits · Category Security · Impact 3 · Effort 2 · Components Expression sandboxWhy it matters. Done: expressions can call only a short allow-list of methods (SafeMethodResolver: about 40 names such as length, contains, substring, plus every type in java.time), so repeat, getClass and the methods of a payload object are not found; tested. Still open: (1) no evaluation time or size budget, and the allow-list still holds matches, split and substring, so a regex with catastrophic backtracking, or a call on a very large string, runs to completion on whichever thread evaluates it; (2) the parsed-expression cache is a synchronized LinkedHashMap, so computeIfAbsent parses while holding its one lock and a slow parse delays every other plugin; (3) no expression length limit was found in the validators.Option. Evaluate with a time and size budget (or on a bounded pool); refuse matches with an expression-supplied pattern or run it with a bounded input; parse outside the lock (a concurrent cache); limit expression length at validation.Question to settle. Who writes expressions today: only trusted authors, or any API caller? That decides how much of this matters.
Id sharedInstance · Category Correctness · Impact 3 · Effort 3 · Components Plugin contract, Graph context, liveness, Expression sandbox, Failure & config helpersWhy it matters. One object serves every node, workflow and session, and initialize and prepare run again for each node on that same object. A plugin that stores config in a field works in a demo with one node and mixes up settings with two. Nothing in the interface or the tests catches it.Option. A conformance test plugins can run (two nodes, different configs, interleaved); an instance-per-node factory option; or pass a per-node context object into each call.Question to settle. Should the contract move state into an explicit per-node object, accepting a larger interface, or keep the shared instance and add enforcement tests?
Id shutdownNever · Category Operability · Impact 2 · Effort 2 · Components Plugin contractWhy it matters. The interface advertises a cleanup hook, and the Javadoc says the engine does not call it. An author who skims the interface leaks a connection pool or scheduled task, and nothing warns.Option. Remove the method, or call it for each plugin once at application stop; mark it deprecated with a clear message in the meantime.Question to settle. Is any plugin relying on it today, and would a once-per-plugin lifecycle hook (not per node) be more honest?
Id coreDependency · Category Evolvability · Impact 4 · Effort 3 · Components Build & test setup, Plugin contractWhy it matters. Still 11 plugin build files depend on :core, and the coupling grew a little: plugin sources have 26 imports of VariableResolver (15 files; was 24), 10 of MapUtils (10 files; was 9), 4 of PluginRegistry (2 files) and 4 of DefaultTaskTrackerService (2 files). So a plugin is not only coupled to this contract but to engine classes that can change, which defeats the point of a separate SPI and blocks third-party plugins. The direction is right in the new code: the run budget ledger (RunBudgets) and the approval waits (SignalWaits) were put into plugin-api so that plugins need no core class for them; the resolver and the map helpers have not been moved yet.Option. Move the resolver and map helpers into plugin-api or a new support module; expose the registry and tracker through small SPI interfaces; add a dependency check that fails if a plugin imports core.Question to settle. Which of the four core classes is a real engine service and which is just a utility that landed in the wrong module?
Id noVersion · Category Evolvability · Impact 3 · Effort 2 · Components Plugin contract, Build & test setupWhy it matters. The contract has no version constant or capability flag. A plugin built against an older plugin-api can fail with an obscure link error at runtime, and the engine cannot say which plugin is incompatible. This matters once plugins ship separately from the server.Option. Add an SPI version constant and a Plugin.apiVersion() default; check at startup and name the plugin; adopt semantic versioning of the module with a compatibility test.Question to settle. Do we expect plugins built outside this repository, and if so how long must an old plugin keep working?
Id directiveOnlyStop · Category Evolvability · Impact 2 · Effort 3 · Components Command processingWhy it matters. The directive layer is meant as the place where processors hand an action to the dispatcher, but only Stop exists. Every other built-in processor performs its work itself and returns nothing, so a custom processor cannot express ‘pause’ as a directive and the sealed hierarchy adds ceremony without extension room.Option. Either grow the directive set (pause, resume, restart, and so on) and make built-ins return them, or simplify the SPI to a plain apply function.Question to settle. Is the directive layer the intended extension seam, or a leftover from an earlier design?
Id deadHooks · Category Functionality · Impact 2 · Effort 1 · Components Plugin contractWhy it matters. isComputationallyHeavy and suppressOptimizationHint are documented as used by validators for performance hints. Nothing in core or web calls them; one built-in plugin reads its own. Authors implement them for no effect, and the old guide’s claim that they are ‘used’ is misleading.Option. Either implement the validator hint they were meant for, or remove them from the interface.Question to settle. Is a graph optimisation hint something users want, or should the filter plugin keep its own check?
Id onSignalLoose · Category Functionality · Impact 2 · Effort 3 · Components Plugin contract, Control commandsWhy it matters. onControlSignal takes a raw Message and returns one. A caller must know by convention which payloads a plugin understands. The 16 typed commands go to the engine, not to plugins, so plugin-specific actions have no typed shape, no schema in the descriptor and no discovery. Update: the approval flow now has a convention (SignalWaits: a payload map with executionId, nodeId, name, decision, data, answered ACCEPTED, QUEUED or NOT_WAITING), and a typed SignalNodeCommand carries it to the plugin. That fixes the shape for approvals only; another plugin still has no way to declare the signals it accepts.Option. Let a plugin declare the signals it accepts in its descriptor (name and payload schema); validate before delivery; consider a typed plugin-command record.Question to settle. Do callers need to discover a plugin’s signals at run time, or are they always scripted per plugin?
Id eventsBestEffort · Category Reliability · Impact 3 · Effort 3 · Components Supervisor SPIWhy it matters. The feed drops events for a slow subscriber and has no replay. A supervisor that must react to every failure (or every ControlOutcome) cannot be sure it saw them. Drops are counted in a metric, but the supervisor itself is not told.Option. Tell the supervisor how many events it missed; offer a durable cursor-based feed from the execution log; mark which supervisors need reliable delivery and size their buffer.Question to settle. Is it acceptable for an automated safety rule to miss an event under load, or should critical rules read from the durable log?
Id storeSync · Category Performance · Impact 3 · Effort 3 · Components Store SPIsWhy it matters. clear, isComplete and isFinished return plain values, while save and get return Monos. This is no longer only a prediction: R2dbcNodeCheckpointStore now answers isComplete and isFinished by querying the database on the calling thread (its Javadoc says they are blocking in the interface), on the path that decides what a resume skips. The new SessionHistoryStore SPI is blocking by design as well (load and save return plain values; the PostgreSQL implementation waits up to 10 s on the calling thread). KeyValueStore is reactive, so the newest store SPI shows the intended shape.Option. Add reactive versions (clear returns Mono, isComplete and isFinished return Mono<Boolean>, SessionHistoryStore returns Mono) with the current ones deprecated; or document that stores must answer from local memory and mark the plugins that call them as blocking.Question to settle. Will the database-backed store cache these answers locally, or should the contract become fully non-blocking?
Id secretThin · Category Security · Impact 3 · Effort 3 · Components Store SPIsWhy it matters. getSecret(key) takes only a key. The provider cannot tell which plugin, workflow or session is asking, so it cannot grant one team’s secrets only to that team, and it cannot audit who read what. In our view this limits multi-tenant use.Option. Pass a context (session, workflow, plugin type) to the provider; log access; support per-session allow lists.Question to settle. Are secrets expected to be global to the server, or per session or tenant?
Id noTestKit · Category Operability · Impact 3 · Effort 3 · Components Build & test setupWhy it matters. The java-test-fixtures plugin is applied but the module has no fixtures. Every plugin author re-creates message builders and a way to run a plugin like the engine (context keys, heartbeat, subscribeOn). Mistakes there make unit tests pass for plugins that fail in a real run.Option. Ship fixtures: a message factory, a harness that sets the Reactor context and runs the validation hooks, and a conformance suite (statelessness, ports, config checks).Question to settle. What is the smallest harness that would have caught the bugs we saw in plugins so far?
Id pmdOff · Category Governance · Impact 1 · Effort 2 · Components Build & test setupWhy it matters. pmdTest is switched off with a note to re-enable it. The module’s tests are the only guard on the sandbox and the defaults, and they are not held to the same rules as the main code.Option. Fix the test-source violations and re-enable the task.Question to settle. Is the backlog small enough to clear in one change?
Id uiHtml · Category Security · Impact 2 · Effort 2 · Components Metadata & schemaWhy it matters. A plugin returns a block of HTML with width and height, which the UI draws. Many plugins repeat nearly identical HTML. A plugin from another team could inject script into the page that shows it. Today all plugins are in the repository, which limits the risk.Option. Replace the HTML with a small data record (colour, icon, label) that the UI renders; or sanitise or sandbox the HTML.Question to settle. Will third parties supply plugins that appear in the same UI as trusted ones?
Id budgetLedger · Category Governance · Impact 3 · Effort 3 · Components Failure & config helpers, Store SPIsWhy it matters. RunBudgets.shared() is one static ledger that LLM_CALL and MCP_TOOL_CALL use to stop a run that loops on a model. Four limits follow from its code. (1) The check and the add are separate steps: exceeded runs before a call and add after it, so calls running at the same time (a node’s default concurrency is 256) all pass the check and the run can spend far more than the limit, not just one call more. (2) It keeps the newest 10,000 keys (each agent name is a key of its own), so on a busy server an old but still running run is forgotten and starts again from zero. (3) It is in memory: a resumed run, a restarted server or a new leader starts at zero. (4) It lives in the JVM that runs the plugin, so a node placed on a worker counts against that worker’s ledger, not the run’s.Option. Reserve before the call (add an estimate, then correct it) so concurrent calls see each other; keep a run’s entry until the run ends instead of evicting by count; write the totals to the checkpoint store, or at least log them, so a resume restores them; decide whether budgeted plugins may be placed on workers.Question to settle. Is a budget a hard guarantee or an estimate? Which one does your finance team expect?
Id signalWaitsShared · Category Reliability · Impact 2 · Effort 2 · Components Control commands, Command processingWhy it matters. SignalWaits.shared() serves every plugin and run in the JVM. Waits and early signals live in memory only, so a restart or a leader change loses them (the core guide lists this). The cap of 100 early signals is global: one run that fills it makes an early signal for any other run answer NOT_WAITING for up to ten minutes, which core turns into HTTP 409. The signal payload is read with String.valueOf, so a missing executionId, nodeId or name becomes the text ‘null’ and is matched as a key.Option. Cap early signals per execution as well as in total; reject a payload that lacks the three key fields; list open waits from a store when one exists (core F2).Question to settle. How many approvals can wait at once on your busiest server, and how long?

Plugins

IDs in brackets such as (G1) are finding numbers from the module’s review notes (Improvements page); only findings still open in the code are listed. Ids without brackets are new findings from reading the code, some of them judgement. Review of 2026-10-15 against main (after PRs #373 and #374): 2 risks closed (secretBlacklist, llmBudget), 5 partly closed (scriptHostAccess, shellModel, sessionMemory, kvVolatile, promptInjection), 3 added (agentGuardsOptIn, decisionUnbudgeted, sessionStoreBlocks); 3 more (ssrf, fanout256, coreLayering) got updated facts; the other 14 are unchanged because their code is unchanged.
Id scriptHostAccess · Category Security · Impact 2 · Effort 3 · Components Transform & script, Selection & gatesWhy it matters. Done (PR #373): the GraalJS class lookup accepts only 12 value classes (Math, String, the number and boolean wrappers, UUID, Base64, ArrayList, HashMap, List, Map) and java.time.*; Class objects are denied, so no reflection; a script is cancelled after 50 million statements or 10 s (yukta.script.max-statements, yukta.script.timeout-ms). Still open: (1) there is no memory limit (the runtime has no heap option), so a script can allocate until the JVM is under pressure; (2) HostAccess.ALL is still used, so a script can call every public method of the objects it is given (message, payload, metadata); (3) the ‘nosem’ comments that switch off the OpenGrep rules remain, so a regression would not be flagged; (4) one shared watchdog thread cancels every script.Option. Run scripts on a bounded pool and watch their heap use from the watchdog, or give them read-only proxy views of the message instead of host objects; add a test that fails if the lookup list grows; keep script-js out of builds that do not need it (-Pyukta.plugins).Question to settle. Do you trust every workflow author with the CPU and memory of the server for ten seconds at a time?
Id ssrf · Category Security · Impact 4 · Effort 2 · Components External I/O, Terminals, Selection & gatesWhy it matters. HTTP_REQUEST and MCP_TOOL_CALL send to any url in config, and the url can be built from the message (${payload.url}). That reaches the cloud metadata address (169.254.169.254), localhost admin ports and anything on the internal network. NOTIFY_TERMINAL requires https but accepts any host. The MCP session cache is keyed by url and credentials, so many different urls also mean many cached sessions. Update: MCP_TOOL_CALL now has tool-level controls (allowedTools, yukta.mcp-client.allowed-tools, argument limits and deny patterns, requireApproval), but they decide which tool is called, not which host: an empty allow-list still allows any tool and any url.Option. An operator-set list of allowed hosts (for example yukta.http.allowed-hosts); block loopback and link-local addresses by default; re-check the address after each redirect and after DNS resolution; apply the same check to MCP and webhooks.Question to settle. Should the allow-list be per deployment, per session (team), or per workflow? And is a network egress policy outside the application a cleaner place for it?
Id plaintextSecrets · Category Security · Impact 4 · Effort 3 · Components Contract & variables, External I/O, AI processorsWhy it matters. A default secret provider now exists (decrypted:key reads YUKTA_SECRET_<KEY>), but nothing forces its use. apiKey (LLM_CALL, decision plugins, model plane), authToken (MCP), SQL passwords and webhook urls can be typed into the config, and the code has no redaction in read paths, so the REST and MCP workflow views return them. LlmGateway also keeps the key inside its client cache key. And SQL_QUERY’s schema text suggests ‘decrypted:<key>’ for the password, but the plugin never calls the resolver, so that form would be used literally.Option. Redact secret-typed fields (the field list already has a SECRET type) in every read API; make plugins that take secrets resolve decrypted: consistently, including SQL_QUERY; consider rejecting plain-text values for SECRET fields.Question to settle. Is it acceptable for a workflow definition to be a secret-bearing document, or must it be safe to share like source code?
Id shellModel · Category Security · Impact 2 · Effort 2 · Components External I/OWhy it matters. Done (PR #373): nothing runs unless yukta.process.allowed-commands lists the executable (or yukta.process.allow-any=true), and useShell is refused while the list is set. Still open: (1) allow-any=true switches the check off, and then useShell works with the old quoting, which cmd.exe on Windows does not honour, so ’&’ or ’|’ inside an interpolated value can still act; (2) the list names programs, not arguments, so an allowed interpreter or a tool that takes options (git, python, curl) still runs whatever a message puts into its arguments; (3) on Unix the single-quote escaping means pipes and redirects never work, so useShell only adds PATH lookup, which surprises authors.Option. Refuse useShell on Windows when any argument is interpolated; allow a pattern per program (allowed arguments) for the programs that take options; consider dropping useShell and exec arguments directly.Question to settle. Which programs do your workflows really need, and can any of them be told to run another program?
Id orphans · Category Reliability · Impact 3 · Effort 2 · Components External I/OWhy it matters. On timeout the plugin destroys, then force-kills, the process it started. If that process was a shell or a build tool that started others (sh -> mvn -> java), the descendants keep running and keep the output pipes open, which can block the reader threads on the pool for blocking work.Option. Destroy descendants before the parent, in both the graceful and the forcible path (the platform’s process handle lists them); consider running the child in its own process group.Question to settle. Do you run build tools and scripts through this plugin, or only single binaries?
Id fanout256 · Category Scalability · Impact 3 · Effort 2 · Components External I/O, AI processorsWhy it matters. PROCESS_EXECUTOR, HTTP_REQUEST, MCP_TOOL_CALL, LLM_CALL, SESSION_MEMORY and the decision plugins start work for up to 256 messages at once and keep the output in order. A trigger or splitter that emits 1,000 messages starts 256 processes, requests or model calls together. Model and web APIs rate-limit well below that, and each process uses real memory and file handles. Update: model calls can now be spaced per provider with yukta.llm.rate-limit.<provider> (requests per minute, across all nodes and runs), which covers the provider-rate part for LLM_CALL and the LLM-backed decisions. PROCESS_EXECUTOR, HTTP_REQUEST, MCP_TOOL_CALL, SESSION_MEMORY and the file and SQL plugins still start up to 256 at once.Option. A maxConcurrency setting per node (default small, for example 1 to 4 for processes and 8 to 16 for network calls); a shared rate limiter per provider or host.Question to settle. Should the engine own a per-node concurrency policy (like retry) instead of each plugin choosing?
Id fileSandbox · Category Security · Impact 3 · Effort 2 · Components External I/OWhy it matters. baseDir is a plain config value, so the author who writes the workflow also chooses the sandbox: baseDir ’/’ reads anything the server can read. In our view the sandbox therefore only helps when the path comes from the message (untrusted input) and baseDir from a trusted author. Also, the link check and the open are separate steps (the code has a ‘ponytail’ note), so a link swapped in between is not caught.Option. An operator-set root that every baseDir must be inside; open files without following links (the platform’s no-follow option) so there is no gap between check and use.Question to settle. Who chooses baseDir in your deployment: the operator or the author?
Id unboundedMaps · Category Scalability · Impact 3 · Effort 2 · Components Transform & script, Routing & EIP, Triggers, AI processors, External I/O, Supervisors & planesWhy it matters. These all grow with the number of distinct inputs and are never trimmed: MAPPER’s template and script caches (keyed by text), the SIMPLE expression cache, CONSTANT_SOURCE’s static value cache, LlmGateway’s model clients (keyed by url, key, model and options), MCP session cache (keyed by endpoint and credentials), and the supervisor’s count windows (keyed by rule and run, only cleared when the rule fires). Each is small per entry; the risk is a long-lived server whose workflows change often.Option. A bounded LRU or time-based expiry for each (the SpEL cache already does 1,000 entries); remove entries when a run ends or a node shuts down.Question to settle. How long does one server live in your deployment, and how often do definitions change?
Id sessionMemory · Category Reliability · Impact 3 · Effort 2 · Components AI processorsWhy it matters. Done: the history can be kept in a file per session (yukta.session-memory.dir) or in PostgreSQL (yukta.session-memory.store-type=postgres, table session_history; yukta.session-memory.ttl deletes sessions not written for that long), so a restart no longer erases it. Still open: (1) the in-memory map in SessionStore still has no eviction (the ‘ponytail’ note is still there; the TTL cleans the database, not the cache), so abandoned sessions stay until a restart; (2) the key is the session id alone, so any workflow that knows or guesses an id can read that conversation; (3) the user turn is stored before the model answers, so a failed call leaves an unanswered question.Option. Evict idle sessions from the cache (a size or time bound); scope keys by workflow or owner; store the user turn together with the answer, or mark an unanswered turn.Question to settle. Is chat history user data that must be isolated, retained or deleted on request?
Id kvVolatile · Category Reliability · Impact 3 · Effort 2 · Components Flow & stateWhy it matters. Done: with yukta.kv.store-type=postgres the store is the kv_entry table (R2dbcKeyValueStore), survives a restart, is shared by servers and increments a counter in one statement, behind the new KeyValueStore SPI. Still open: (1) the default is the in-memory store, so counters and flags that look durable are not unless the type is set; (2) namespace is optional (default none), so two workflows using the key ‘counter’ share it; (3) in the in-memory store the size check and the insert are separate steps, so the cap can be passed slightly under heavy concurrency (a ‘ponytail’ note says so).Option. Make namespace default to the workflow id; log a warning at startup when KV_STORE nodes exist and the store is in memory; make the cap check atomic.Question to settle. What would you use KV_STORE for that you could not afford to lose?
Id loopEscalate · Category Correctness · Impact 2 · Effort 1 · Components Flow & stateWhy it matters. RETRY and SKIP both advance to the next round with the previous message, so the names promise a difference that does not exist. ESCALATE throws a plain RuntimeException whose text starts with ‘WorkflowExecutionException:’; engine rules that match on the exception class (retryOn, noRetryOn) cannot recognise it.Option. Give SKIP a distinct meaning (end the loop with the last good message) and throw the real WorkflowExecutionException.Question to settle. What should ‘skip’ mean for a loop: skip the round, or skip the rest of the loop?
Id loopSkipsValidation · Category Correctness · Impact 3 · Effort 3 · Components Flow & state, Contract & variablesWhy it matters. A loop looks up its target in the registry and calls it directly. The target’s validateConfig and initialize never run for targetConfig, and variable resolution rules of the target are bypassed. A wrong url in targetConfig is found on the first message, not at submit, and some targets (for example one that depends on prepare) may not be ready.Option. Run the target’s validateConfig in the loop’s own validateConfig; longer term model a loop as a sub-graph in the engine so the target is a real node.Question to settle. Is repetition a plugin feature or an engine feature in your view?
Id sqlTimeout · Category Performance · Impact 3 · Effort 2 · Components External I/OWhy it matters. HTTP, MCP, process and model plugins add their own timeout to the node timeout. SQL_QUERY has timeoutSeconds, but the node’s idle limit stays at the 30 s default, so raising timeoutSeconds to 120 still fails at 30 s unless the author also sets timeoutSeconds in the node’s engine config. Each message opens a new JDBC connection (a ‘ponytail’ note), and any statement kind is allowed, including DDL.Option. Override getDefaultTimeout for SQL_QUERY and FILE_* the way HTTP_REQUEST does; add a connection pool; optionally a read-only mode.Question to settle. Do you want SQL_QUERY to be read-only by default?
Id promptInjection · Category Security · Impact 3 · Effort 3 · Components AI processors, Supervisors & planesWhy it matters. Decision plugins send the ‘state’ (default the whole payload) to the model along with the question, and the model plane sends event details, including log lines if enabled. Text inside that content can say ‘ignore the question and answer yes’. A GATE that decides ‘safe to send?’ or a control plane that decides ‘pause?’ can then be steered by the very data it judges. Probabilities from a chat model are its own estimate, not a measurement. Update (PR #373): LlmGateway can now refuse a prompt or an answer that matches yukta.llm.deny-input-patterns or deny-output-patterns and cut prompts at yukta.llm.max-input-chars; this applies to LLM_CALL and to LLM-backed decisions, and MCP tool calls can require a human approval or an allow-list. These are patterns and gates, not a classifier: they reduce the damage of a steered verdict but do not stop the steering, and the decision ‘state’ still defaults to the whole payload.Option. Treat model verdicts as advice for low-risk routing; keep rules and deny rules authoritative for anything harmful; keep untrusted fields out of ‘state’ or mark them as data; prefer System One’s calibrated probabilities where it is available.Question to settle. Which decisions in your workflows would be harmful if an attacker could choose the answer?
Id windowsPerRun · Category Functionality · Impact 2 · Effort 3 · Components AI processorsWhy it matters. Each run starts with an empty window, and the window is local to the node. A scheduled workflow is one long run, so it works there; a workflow started per event never accumulates history, so every message looks unique or normal (ANOMALY also treats the first minHistory messages as normal).Option. An external window store shared across runs (KV or database); document the limit in the form text.Question to settle. Do your detection workflows run long, or start fresh for every event?
Id consoleLogs · Category Security · Impact 3 · Effort 1 · Components TerminalsWhy it matters. Every message is written in full to the application log. Payloads can carry personal data and secrets, and large payloads inflate logs. PROCESS_EXECUTOR can also put up to 10 MiB of process output in error messages.Option. Log at DEBUG or truncate and redact; cap output embedded in exception text to the last few lines.Question to settle. Which of your log destinations are allowed to hold payload data?
Id scheduleLost · Category Availability · Impact 2 · Effort 3 · Components TriggersWhy it matters. SCHEDULE_TRIGGER lives inside one running workflow. After a restart the run is gone and its schedule with it; missed ticks are not made up, and no one re-starts the run unless something else does.Option. A persistent scheduler in core that starts runs at planned times and records the last fired tick.Question to settle. Is a missed nightly run acceptable, or must it be caught up?
Id modelFailsOpen · Category Availability · Impact 3 · Effort 2 · Components Supervisors & planesWhy it matters. If the decision model is down, slow (2 s limit) or unsure, the plane returns no commands, so oversight silently disappears exactly when the system may be unhealthy. It also makes one model call per matching event, one after another. It cannot review commands, so it adds no protection against dangerous ones.Option. Count and surface ‘no opinion’ results as a metric and alert; prefer RulesFirstControlPlane so rules cover the clear cases; sample events instead of asking for each.Question to settle. If the model is down for an hour, what should happen to unattended runs?
Id typeIds · Category Evolvability · Impact 1 · Effort 1 · Components Transform & script, Routing & EIPWhy it matters. Every other type is UPPER_SNAKE_CASE. The inconsistency makes workflow JSON harder to remember and generate. The old API_TRIGGER inconsistency is fixed. Renaming without an alias would break stored workflows.Option. Rename to CONTENT_FILTER and accept the old id as an alias in the registry for one release.Question to settle. Do stored workflows or external tools depend on the current id?
Id coreLayering · Category Evolvability · Impact 3 · Effort 4 · Components Contract & variables, Selection & gatesWhy it matters. Plugins use core’s variable resolver, helper classes, registry and tracker. Counted on 2026-10-15: 11 of the plugin build files depend on :core, with 26 imports of VariableResolver (15 files), 10 of MapUtils (10 files), 4 of PluginRegistry and 4 of DefaultTaskTrackerService. The intended direction is core depends on plugin-api and plugins depend only on plugin-api; today core changes can break plugins and a plugin cannot be built and tested without the engine. New shared helpers (RunBudgets, SignalWaits, the KeyValueStore and SessionHistoryStore SPIs) went into plugin-api, which is the right direction; the older helpers have not moved.Option. Move the resolver, map helpers and secret lookup into plugin-api or a small plugin-support module; give loops and other cross-plugin features narrow interfaces for the registry and tracker.Question to settle. Will third parties write plugins? If yes, the core dependency is a public-API problem.
Id gatesOff · Category Governance · Impact 3 · Effort 3 · Components Selection & gatesWhy it matters. internal-core turns off PMD (main and test) and Checkstyle for tests, with an unfinished-work note. constant-source turns off PMD main and test. The old note that JaCoCo verification was off and that coverage exceptions named missing classes no longer matches the build file. The 12 routing, transform and script types, including the script mapper, are the least checked.Option. Turn the gates back on one at a time, starting with the script mapper after its fix.Question to settle. Who owns turning the disabled checks back on, and by when?
Id optionalNotOptional · Category Security · Impact 2 · Effort 2 · Components Selection & gatesWhy it matters. -Pyukta.plugins=none removes the nine listed modules, but the three control-plane modules are always compiled in, the model plane exposes the decision module and the decision module depends on llm-call. So CLASSIFY, GATE and the other decision types and LLM_CALL appear to stay on the runtime classpath (read from build files, not confirmed by a running build). A deployment that wanted no outbound model calls may still have the plugins, though their beans do nothing until used in a workflow.Option. Make the control planes optional too, or split the decision API from its plugins; confirm with a build that lists registered types.Question to settle. Does a minimal build need to guarantee that a capability is absent, or only unused?
Id agentGuardsOptIn · Category Governance · Impact 3 · Effort 2 · Components External I/O, AI processorsWhy it matters. The controls added for agent workflows (PR #373) exist but none is on by default. allowedTools and yukta.mcp-client.allowed-tools: empty means any tool. requireApproval: empty means no approval. budget: absent means no limit. yukta.llm.deny-input-patterns, deny-output-patterns and max-input-chars: empty or 0 means off. yukta.llm.rate-limit: absent means none. A workflow where an LLM_CALL picks the tool for an MCP_TOOL_CALL (the tool name may come from the message) is therefore fully open until the author or the operator sets each guard, and a missing setting looks the same as a working one.Option. Log (or report in the workflow validation result) a warning when an MCP_TOOL_CALL gets its tool from the message and has neither allowedTools nor requireApproval; offer a deployment switch ‘agent safe mode’ that makes the lists mandatory.Question to settle. If an author forgets allowedTools on an agent node, what is the worst tool the model can reach?
Id decisionUnbudgeted · Category Governance · Impact 2 · Effort 2 · Components AI processors, Supervisors & planesWhy it matters. Only LlmCallPlugin and ToolBudget (MCP_TOOL_CALL) use the shared ledger RunBudgets. The decision plugins with backend: LLM call LlmGateway directly and have no budget option, and the model control plane asks the model once per matching event, so a chatty run can spend tokens that no budget sees. The per-provider rate limit does apply to them (it lives in the gateway), but a rate limit bounds speed, not total spend.Option. Move the budget check into LlmGateway (key by execution id) so every caller is counted, or add the same budget option to the decision plugins; count the control plane’s calls under a fixed agent key.Question to settle. Should a GATE that asks a chat model ‘safe to send?’ count against the same budget as the node that wrote the text?
Id sessionStoreBlocks · Category Performance · Impact 3 · Effort 2 · Components AI processorsWhy it matters. SessionStore calls SessionHistoryStore.load and save (blocking by design) from inside ConcurrentHashMap.computeIfAbsent and compute, so a map bin stays locked during the call. SessionMemoryPlugin does not override isBlocking(), so core does not move it to the virtual-thread scheduler. The PostgreSQL implementation waits on the calling thread for up to 10 s, so a slow database can hold a Reactor thread for that long per message.Option. Return true from isBlocking() so the node runs on the blocking scheduler (one line); load before and save after the map operation instead of inside it.Question to settle. What is your database’s worst-case latency, and how many chats are active at once?

UI

Id noAuthHeader · Category Security · Impact 5 · Effort 3 · Components API Client & Cache, Live Stream HookWhy it matters. When yukta.api-key is set, which the project requires for remote access, the server answers 401 to every /api request that has no Authorization: Bearer or X-API-Key header. fetchApi adds neither, and a browser EventSource cannot set headers at all. The result: the app loads and then every call fails, and the error text on the home page (‘Check that the Yukta server is running’) points the operator the wrong way. Re-checked 2026-10-15: still open. There is also no handling of 401/403 anywhere in the UI (the only status check is 429), and boot’s ExposureGuard requires a key whenever the server is not on loopback, so every non-local deployment hits this. Evidence: ui/src/lib/apiClient.ts:48-51, ui/src/hooks/useEventSourceJson.ts:47, web ApiKeyFilter.java:122-125.Option. A key prompt kept in memory or sessionStorage and added by fetchApi; replace EventSource with fetch-based streaming that can send headers; or a same-origin session cookie issued by the server; or a reverse proxy that injects the header. At minimum, show a ‘server requires an API key’ message on 401/403.Question to settle. How are remote operators meant to use the UI when a key is required, and is a single shared key acceptable for browsers?
Id startRetryKey · Category Correctness · Impact 3 · Effort 1 · Components API Client & Cache, Run ViewWhy it matters. In my reading of the code, mutations are configured to retry once and the key is generated inside the function that sends the request, so a retried start carries a different key. The point of the key is that a repeated start returns the first run. Example: the first request is accepted but the response is lost; the retry has a new key, so a second run starts. The same retry setting also repeats Restart and ‘Restart from here’, which have no key. Wider than the start call: mutations default to retry:1 (ui/src/lib/queryClient.ts:15), so signal (approve/reject), rollback, version notes and stop/skip are also repeated after a timeout. A 409/403/429 is retried pointlessly and delays the error shown. Since #392 the server answers a second signal on the same key within 5 s as NOT_WAITING (a double-submit guard, SignalWaits.DUPLICATE_WINDOW), so a retried approval no longer lands twice, but the retry then reports a conflict although the first attempt succeeded.Option. Set mutations retry to 0 (start is covered by its Idempotency-Key once the key is created per click, useExecutionControl.ts:41); or retry only on network errors.Question to settle. Is a duplicate run harmful for your workflows, and should the UI ever retry a start automatically?
Id legacyAliases · Category Evolvability · Impact 2 · Effort 1 · Components API Client & Cache, Run ViewWhy it matters. The status snapshot and the status stream use /api/workflow/{session}/status/…, which the server marks deprecated in favour of /api/executions/{id} and /api/executions/{id}/status/stream. The E2E mocks use the same legacy paths. When the aliases are removed the run view silently stops updating. Start, history and the workflow-wide stop have no replacement path, so they are not deprecated. Still open: getWorkflowProgress.ts:12 and useExecutionProgress.ts:51; e2e mocks in ui/e2e/fixtures.ts:259,266.Option. Switch the two calls and the mocks to /api/executions/…; add a note in the OpenAPI deprecation window.Question to settle. When is the server planning to remove the legacy aliases, and who tells the UI?
Id replayNoLastId · Category Performance · Impact 3 · Effort 3 · Components Live Stream Hook, Log Terminal, Run ViewWhy it matters. The server numbers events, honours Last-Event-ID and sends a keep-alive every 15 s, but each reconnect creates a brand new EventSource without the id. A run with 100,000 stored log lines is re-sent in full on every reconnect, and the UI must then discard it. On a flaky network that multiplies bandwidth and server work. Now affects three streams: logs, status and results (useExecutionResults.ts:71). The server honours Last-Event-ID on all of them (LogManagementController.java:78,107, ExecutionController.java:152, ExecutionResultController.java:64).Option. Let the browser’s own reconnect run while readyState is CONNECTING (it sends Last-Event-ID); close and back off only when CLOSED. Track the last event id per stream and send it (a header with fetch streaming, or a query parameter if the server accepts one); or let the browser’s built-in reconnect do it instead of creating a new source.Question to settle. How long can a run’s log be, and how often do operators’ connections drop?
Id logDedupe · Category Correctness · Impact 2 · Effort 2 · Components Log TerminalWhy it matters. In our view, comparing time, plugin, stream and text is fragile in two ways. Two real lines that are identical and share a timestamp become one, so a loop printing the same message looks shorter than it was. And the remembered keys are capped at 2,000, so on a run with more lines a reconnect can re-add old lines the set already forgot, pushing the real tail out of the 2,000-line buffer. Update: the cap is now MAX_LOG_ENTRIES = 20,000 (useExecutionLogs.ts:10,57), so the forgotten-key problem starts past 20,000 lines instead of 2,000. The key is still timestamp|pluginId|stream|message; server event ids are not used.Option. Use the server’s event ids to drop replays, which also makes the content key unnecessary; or add a sequence number to log entries.Question to settle. Do operators rely on exact line counts, for example to spot a loop?
Id pollControl · Category Scalability · Impact 2 · Effort 2 · Components Oversight PagesWhy it matters. The Control page fetches the node list and the supervisor table every 5 seconds per open browser. The server already offers /api/events/stream, which carries node and workflow status changes and control outcomes. With many operators this is steady background load, and changes still appear up to 5 s late. Note the stream is not replayed and a slow reader misses events, so the counters would still need an occasional snapshot. Update: now four 5 s polls: ControlPage.tsx:54, SupervisorsPanel.tsx:33, ControlAuditPanel.tsx:25 and PendingApprovals.tsx:26. Nothing opens /api/events/stream.Option. Open the events stream and refetch only when a relevant event arrives, keeping a slow poll as a safety net.Question to settle. How many people will keep the Control page open at once?
Id deadSanitiser · Category Governance · Impact 2 · Effort 1 · Components Build & Serving, Workflow BuilderWhy it matters. dompurify is still in package.json, but nothing in src imports it, there is no renderNodeHtml function, and the plugin node HTML (uiDesign) is typed but never rendered. The guides and the server’s CSP comment still say plugin HTML is sanitised. If someone later renders uiDesign.html they will assume a safe path exists. Meanwhile the dependency adds supply-chain surface for no use. Still open: ui/package.json:64 lists dompurify; no import in ui/src; plugin-api UiDesign.java:9 and Improvements page still claim sanitising.Option. Remove the dependency and the claims, or, if plugin HTML will be rendered, add one audited render function with a test and keep the CSP.Question to settle. Is rendering plugin-supplied HTML still on the roadmap?
Id iconGap · Category Functionality · Impact 1 · Effort 1 · Components Design System, Workflow Builder, Oversight PagesWhy it matters. The plugin icon map has no entry for CLASSIFY, GATE, SCORE, ASK, ANOMALY or DEDUPE, which exist as plugin types. They draw the generic processor icon, so on a canvas six different decision steps look identical. The map is hand-edited and the font must be rebuilt for each new icon, so every new plugin repeats this chore. Still open: pluginIcons.ts:7-33 has no CLASSIFY, GATE, SCORE, ASK, ANOMALY or DEDUPE.Option. Add the six entries and rerun pnpm run icons; longer term let a plugin declare its icon name in its metadata.Question to settle. Should the server own plugin presentation (icon, colour) so the UI needs no per-plugin change?
Id noTestGate · Category Governance · Impact 3 · Effort 2 · Components Build & ServingWhy it matters. There are 75 Vitest files and a 100% coverage threshold in the config, but in build.gradle.kts the unit tests are commented out of ‘check’, and I found no Vitest call in the CI workflow. The CI runs ‘:ui:build’ (type checks, build, size guard) and a separate Playwright job. A regression covered only by a unit test can merge unnoticed. The old guide’s ‘98% coverage’ is therefore unverified by any gate. Still open: ui/build.gradle.kts:81-85 (dependsOn(pnpmTest) commented out); .github/workflows/ci.yml has no Vitest step; now 81 test files with 100% thresholds in vitest.config.ts:27-31.Option. Add pnpmTest to ‘check’ and run coverage in CI; run the API types check in a job that boots the server.Question to settle. Is the threshold of 100% meant to be a hard gate, and who owns it?
Id wholeSessionSave · Category Correctness · Impact 3 · Effort 3 · Components Workflow BuilderWhy it matters. Save sends the complete session (every workflow and the tags) to POST /api/sessions, which replaces the stored one. Example: Ana and Ben both open session ‘ci’. Ana edits workflow build and saves. Ben, who loaded the session earlier, edits workflow deploy and saves: Ana’s change to build is overwritten with Ben’s older copy. The editor loads fresh data but there is no version check at save time. Update: the new Versions panel lets someone recover an overwritten edit by hand, but it does not prevent the overwrite (WorkflowEditorPage.tsx:149-152, no ETag or version check).Option. A version or ETag the server compares on save; a per-workflow update endpoint; or a conflict message.Question to settle. Will more than one person edit a session at the same time?
Id freeStatus · Category Evolvability · Impact 3 · Effort 3 · Components Status Mapping, API Client & CacheWhy it matters. The server publishes statuses as plain text, so the type checker cannot say when a new one appears. The mapper then shows an unknown node status as ‘pending’ and an unknown run status as ‘unknown’, quietly. Example: a new status meaning ‘waiting for approval’ would show as pending and the Pause button’s logic would be wrong. The generated API types make every field optional, so hand-written types remain the working types. Still open: statusMapper.ts:69-82 maps unknown statuses to pending/unknown; ui/src/types/api.generated.ts is not imported anywhere.Option. Publish a status enum from core and generate unions; add a visible ‘unknown status’ state with the raw word; fail a test when a new server status is not mapped.Question to settle. Who is the owner of the status vocabulary, core or the UI?
Id stopAllRuns · Category Operability · Impact 3 · Effort 2 · Components Run ViewWhy it matters. ‘Drain’, ‘Cancel’ and ‘Kill all runs’ call the workflow-wide stop, which stops every active run of that workflow, including runs started by other people or by a schedule. The menu label says ‘all runs’ and a confirmation dialog follows, but the dialog text is generic (‘This cannot be undone’). An operator wanting to stop only the run on screen with KILL has no such option. Still open: RunControls.tsx:152-157 and the generic confirm at :185 (no count of affected runs). The per-run stop takes no mode, so a per-run Kill is not possible.Option. Offer the stop modes per run (if the server supports them), and name the number of affected runs in the confirmation.Question to settle. Is stopping every run of a workflow a common need, or a footgun?
Id uiGaps · Category Functionality · Impact 2 · Effort 3 · Components Run View, Workflow Builder, Shell & RoutingWhy it matters. Done since the last review: pending approvals (approvals.ts, PendingApprovals.tsx) and workflow versions with diff, rollback and notes (versions.ts, WorkflowVersionsPanel.tsx). Still REST/MCP only: delete for sessions or workflows (SessionRowActionsMenu.tsx has only View Details), breakpoints add/clear, priority, scheduler limits, recovering an aborted run (/executions/{id}/recover), run search (GET /api/executions) and GET /api/workers (ExecutionControlController.java:91,285,348,361,377; WorkersController.java:32). Operators fall back to curl or MCP for these.Option. Prioritise by operator pain: recover and delete first, then breakpoints and limits; keep the rest as documented REST.Question to settle. Which of these would an operator reach for first during an incident?
Id pagingCaps · Category Scalability · Impact 2 · Effort 2 · Components Oversight Pages, Run View, API Client & CacheWhy it matters. History asks for 50 runs and ignores the cursor header, the run picker shows whatever the server returns (default 500), and the session list loads every summary then pages 10 at a time in the browser. A busy session loses older runs from view, and a deployment with thousands of sessions pays a large first call. Still open, and the server now has history filters (workflowId, status, text, since, until, X-Next-Cursor) that the UI does not use: HistoryPage.tsx:16,22, getWorkflowExecutions.ts:11-12, getSessionSummaries.ts:8.Option. Follow X-Next-Cursor with a ‘load more’; add server filters for status and time; page sessions on the server.Question to settle. How many sessions and runs per workflow do you expect after a year?
Id noDraftGuard · Category Operability · Impact 2 · Effort 2 · Components Workflow Builder, Shell & RoutingWhy it matters. The draft is held in memory. A click on a header link, the Back button or a reload discards it and I found no confirmation or draft storage in the code. Large node configurations typed into JSON boxes are costly to lose. Still open: no useBlocker, beforeunload or draft storage in ui/src.Option. A router blocker with a confirm prompt and/or a draft saved to sessionStorage.Question to settle. Would auto-saving a draft in the browser be acceptable, or must unsaved work never leave the page?
Id rollbackStaleDraft · Category Correctness · Impact 4 · Effort 1 · Components Workflow Builder, Run ViewWhy it matters. Rollback invalidates only [‘sessions’, sid, ‘workflows’] (WorkflowVersionsPanel.tsx:62-65), but the editor reads [‘sessions’, sid, ‘config’] (WorkflowEditorPage.tsx:377-381) and seeds its draft with useState (line 72). With cached data the editor renders at once and keeps the old draft after the refetch arrives. Example: roll back to v3, open the editor, press Save: the pre-rollback definition is written again. The same stale draft appears for changes made from the CLI or MCP.Option. Invalidate [‘sessions’, sid] on rollback; key the editor by the fetched data’s update time or wait for isFetching to be false before seeding the draft.Question to settle. Should the editor warn when the stored definition changed while it was open?
Id dedupeEviction · Category Correctness · Impact 3 · Effort 2 · Components Live Stream Hook, Log Terminal, Run ViewWhy it matters. Logs: the seen-set is trimmed together with the buffer (useExecutionLogs.ts:57-59). Results: duplicates are checked only against the 500 events still held (useExecutionResults.ts:52-55). After more than 20,000 lines or 500 result events, a reconnect replays evicted entries; they pass dedupe and are appended out of order, and the ‘earlier lines hidden’ count is overstated. NODE_END and EXECUTION_END events also count toward the 500 cap, so they can push out real RESULT events.Option. Keep the highest server sequence or SSE id seen and drop anything at or below it; fixing replayNoLastId removes most of the problem.Question to settle. Is a reordered line after a very long run acceptable?
Id noteInQuery · Category Security · Impact 2 · Effort 2 · Components API Client & Cache, Run ViewWhy it matters. The note is a query parameter (versions.ts:57-59, WorkflowVersionController.java:152-153). Notes end up in access and proxy logs, and long notes fail on URL-length limits.Option. Send a JSON body {version, note} (server and client) and add a maxLength to the input (WorkflowVersionsPanel.tsx:121).Question to settle. Can notes contain anything sensitive?
Id approvalUx · Category Functionality · Impact 3 · Effort 2 · Components Run View, Oversight PagesWhy it matters. Reject fires at once (PendingApprovals.tsx:74-79). The data (reason) parameter the server accepts (ExecutionControlController.java:322) is never sent, ‘since’ is fetched but not shown, and one decide.isPending disables every row (lines 69, 76). The server now treats a repeat decision within 5 s as a duplicate and answers NOT_WAITING (#392); the UI has no handling for that answer, so a double click or an automatic retry shows an error for a decision that was applied.Option. Confirm on reject with an optional reason; show ‘waiting 3m’ from since; track pending per row. Treat NOT_WAITING/409 right after a decision as ‘already answered’ and refresh the list.Question to settle. Who may approve, and should the reason be mandatory?
Id approvalPoll · Category Scalability · Impact 2 · Effort 2 · Components Run View, Oversight PagesWhy it matters. PendingApprovals.tsx:23-27 polls the whole session every 5 s even with no live run, and filters by workflow in the browser (lines 39-41). The audit panel hard-codes limit=50 with no paging or executionId filter, although the server supports it (ControlAuditPanel.tsx:18, ControlAuditController.java:55).Option. Enable the approvals query only while an execution is non-terminal; add an execution filter or link from a run to its audit rows; or use the event stream.Question to settle. How many sessions and operators will have the page open at once?
Id e2eMocksGap · Category Governance · Impact 3 · Effort 2 · Components Build & Serving, Run ViewWhy it matters. ui/e2e/fixtures.ts:253-270 mocks graph, executions, status and logs, but not /approvals, /results, /versions or /control/audit. The approvals poll and the results stream (with retry backoff) hit unmocked routes, so workflow-details tests and screenshot baselines can flake.Option. Add route stubs and one E2E test each for approve, rollback and the RESULT block.Question to settle. Should an unmocked /api route fail the E2E run?
Id stateMessageDrift · Category Operability · Impact 2 · Effort 1 · Components Oversight Pages, Design SystemWhy it matters. ControlAuditPanel.tsx:33-36, SupervisorsPanel.tsx:48-51 and WorkflowEditorPage.tsx:386-391 use a bare ; the audit error hides the server’s message. ControlAuditPanel.tsx:67 also keys rows by index while the newest row comes first, so every new row shifts all keys.Option. Use StateMessage with onRetry={refetch}; key audit rows by a server id or sequence.Question to settle. Is there a server-side audit id to key on?
Id a11yLive · Category Accessibility · Impact 2 · Effort 1 · Components Run View, Design SystemWhy it matters. The approvals strip appears and disappears silently (PendingApprovals.tsx:47-50); the diff result and ‘Loading changes…’ are not live regions (WorkflowVersionsPanel.tsx:144-159); the Versions toggle has aria-pressed but no aria-expanded/aria-controls and its icon is not aria-hidden (WorkflowDetailsPage.tsx:125-132); the RESULT block’s LIVE badge is not announced.Option. aria-live=polite on the strip and the diff; aria-expanded plus aria-controls on the toggle; aria-hidden on icon spans.Question to settle. Is there an accessibility target for operators?
Id rollbackSafety · Category Governance · Impact 2 · Effort 2 · Components Run ViewWhy it matters. WorkflowVersionsPanel.tsx:103-118: the current version is not marked, the button stays enabled while a run is live, confirmation is a single window.confirm, and one goBack.isPending disables every row.Option. Mark the current version, show the diff in the confirmation, warn when the run is not terminal.Question to settle. Should rollback be blocked while a run is live?

Web

Risk ids are short names. Impact (i) and effort (e) are our own 1 to 5 judgement. No backlog ids are used because the earlier review findings (W1 to W17) are all fixed. Review of 2026-10-15 against main (after PR #373): 1 risk closed (sharedKey, replaced by the smaller staticKeys), 5 partly closed (openByDefault, ownershipGap, noConflict, limitsEndpoint, noLimits), 2 added (staticKeys, searchScan); the others are unchanged.
Id openByDefault · Category Security · Impact 3 · Effort 2 · Components Settings & exposure, Filter chainWhy it matters. Done (PR #373): ExposureGuard (boot) refuses to start on a non-loopback address without a key; the actuator (except health) needs an ADMIN key; without a key HostOriginFilter answers 403 to a Host that is not loopback and to a cross-origin write, which stops DNS rebinding and cross-site requests from a browser. Still open: (1) the guard counts yukta.mcp.api-key as a key, so a non-loopback server with only the MCP key set starts and leaves /api and the actuator open (see boot guardMcpKeyOnly); (2) the key filter guards only /api and /actuator, so the Swagger UI and the OpenAPI document stay open; (3) on loopback with no key, any local process or user can call the API, because the Host/Origin checks only stop browsers.Option. Make the guard check the same keys the REST filter uses; guard or switch off the docs in prod; consider generating a key at first start.Question to settle. Should the server refuse to listen on a non-loopback address without a key, even if that breaks a quick demo?
Id uiNoKey · Category Functionality · Impact 3 · Effort 2 · Components Filter chain, Settings & exposureWhy it matters. The UI’s API client sets only Content-Type; it has no way to add Authorization or X-API-Key. With a key configured, its calls would be 401 unless a proxy adds the header. So the safe configuration is awkward for the main client, which pushes operators towards leaving the key off. The CLI can send a bearer token. Update: the new Host/Origin protection works only while no key is set, so with the UI the choices are still ‘no key, loopback only’ or a proxy that adds the header.Option. Add key (or login) support to the UI; or serve the UI and API behind a reverse proxy that injects the header; or have the server serve the UI with its own session cookie.Question to settle. What should authentication look like for a browser user: a pasted key, a login, or a gateway in front?
Id ownershipGap · Category Security · Impact 2 · Effort 2 · Components Ownership check, Run control API, Run lifecycle API, Streams (SSE), Catalogue & control busWhy it matters. Done: a key can be limited to sessions (yukta.api-keys[].sessions). The key filter checks the session of /api/sessions/{s}/** and /api/workflow/{s}/**, finds the session of /api/executions/{id}/** from the tracker, and refuses the events stream, the run search and the control audit to a limited key. Still open: (1) the controllers’ own checks are still uneven (canonical routes check existence only), which matters for any key without session limits; (2) the filter finds an execution’s session only through the live tracker, so once a run has left it (restart, eviction) even its own limited key gets 403; (3) the legacy aliases are still the only routes that check inside the controller.Option. Resolve the session from the execution store as well as the tracker; move the check into one place; then retire the aliases.Question to settle. Is a session meant to be a security boundary (tenant) or only a convenience grouping?
Id configUnredacted · Category Security · Impact 3 · Effort 3 · Components Session & config APIWhy it matters. GET /api/sessions/{sid}/config and /workflows/{wid} return node config unchanged. Core supports secret references (the decrypted: prefix, secrets from YUKTA_SECRET_ environment variables), so a careful author keeps tokens out of the definition. Nothing stops someone writing a literal API token into an HTTP node’s headers. Then any reader of the API, and anything that logs responses, sees it. This is our judgement of a design weakness, not a confirmed leak.Option. Mask values of keys that look secret (the way core already blocks env names containing KEY or TOKEN) in read APIs; warn on save when a config contains a likely literal secret; document that secrets must be references.Question to settle. Should the read API ever return a secret-looking value, or should editors work with masked values and send them back unchanged?
Id pagingLinear · Category Performance · Impact 3 · Effort 3 · Components Run lifecycle API, Session & config APIWhy it matters. History pages load the entire history from core and then walk it to find the cursor. Listing sessions loads and sorts every id for each page. Cost per page grows with the total, so reading a long history in pages of 100 costs roughly n pages times n items. An unknown cursor (for example the run was trimmed from history) silently restarts from the first item, so a client looping until there is no next cursor can see page 1 again, which looks like an endless loop.Option. Push the cursor into core and the stores (seek by id), keep the id-based cursor; return 400 or 410 for an unknown cursor instead of restarting; cap per-call work.Question to settle. How long can a session’s history realistically become, and does anyone page through all of it?
Id slowReader · Category Reliability · Impact 3 · Effort 3 · Components Streams (SSE)Why it matters. The live parts use best-effort multicast feeds: if a reader does not keep up, events are dropped for that reader instead of slowing the engine. That protects the engine but means a UI can silently miss a status change or a log line. The events stream has no replay at all, and it ignores Last-Event-ID, so a reconnect cannot recover what was missed. A UI that treats the stream as complete can show a stale state.Option. Tell the client when it missed events (a gap marker or a lag counter) so it can refetch; add replay by sequence number for the events stream; document that streams are hints and status endpoints are the truth.Question to settle. Is a missed event acceptable for the UI (it can refetch) but not for programmatic clients like the CLI or scripts?
Id logSeam · Category Correctness · Impact 2 · Effort 3 · Components Streams (SSE)Why it matters. The seam between stored lines and live lines is closed by a heuristic: match by message text and a timestamp not after the snapshot, drop once. Two genuinely different lines with identical text in the seam window can lose one. The live buffer holds 1024 lines; a burst larger than that during the history read is lost. Event ids are positions in a stream that is rebuilt on every connect, so resuming with Last-Event-ID is exact only if history was not changed or trimmed meanwhile (for example by log retention).Option. Give every log line a durable sequence number in the log store and use it as the SSE id; join history and live on that number instead of text.Question to settle. How much does a perfectly exact log stream matter, compared with the cost of changing the log store?
Id legacyAliases · Category Evolvability · Impact 2 · Effort 2 · Components Run lifecycle API, Run control API, Ownership checkWhy it matters. 14 routes are aliases of canonical /api/executions/… routes. The CLI calls almost all of them and the UI still calls the two status aliases. Two route families mean double the tests and docs, and clients that never move. The aliases are also the only ones that check session ownership, so removing them without fixing the check lowers protection.Option. Move the CLI and UI to the canonical routes, fix the ownership check on them, announce a removal date and log alias use to see who remains.Question to settle. When can we retire the aliases, and who owns migrating the CLI?
Id localCatch · Category Operability · Impact 2 · Effort 1 · Components Session & config API, Catalogue & control bus, Errors & envelopeWhy it matters. The rule is that controllers never catch errors, so one handler decides what clients see. Three places break it. The validate endpoint turns every error into valid:false plus e.getMessage(), so an unexpected failure shows exception text in a 200. The plugin list turns a failure into a 500 whose message includes the exception text, which the central handler would have hidden. The session summaries endpoint logs and skips a session it cannot read, so a corrupt session simply vanishes from the table with a 200.Option. Let unexpected errors reach the central handler (catch only the validator’s own validation exception); for summaries, return a marker row or a count of skipped sessions.Question to settle. Is hiding an unreadable session from a table ever the right behaviour, or should it show as broken?
Id noConflict · Category Correctness · Impact 2 · Effort 2 · Components Errors & envelope, Run lifecycle API, Run control APIWhy it matters. Partly done: a ConflictException now maps to 409 and the recover endpoint uses it. The restart race is not converted: core’s RestartCommandProcessor still throws IllegalStateException('Restart already in progress...'), and the handler maps that type to 500 with no message. Monitoring that counts 5xx will flag it as an incident, and any new core failure type is a 500 until someone adds a handler.Option. Throw ConflictException for the restart race and review the other IllegalStateException uses that are client conflicts.Question to settle. Which core failures are client-correctable, and should the web layer classify them or core?
Id limitsEndpoint · Category Governance · Impact 2 · Effort 1 · Components Run control APIWhy it matters. Done: the endpoint needs an ADMIN key, the change goes through the reviewers and is audited with the actor, and it is restored from the log at startup. Still open: the controller takes two integers with no range check, and a negative value is treated as 0 and 0 means no limit, so one call can still remove the caps that protect the server. It changes the two global limits, not per-session ones.Option. Reject negative values and require an explicit flag (or a configured ceiling) for 0.Question to settle. Should a runtime call be allowed to raise a limit above what the operator configured at startup?
Id silent401 · Category Operability · Impact 2 · Effort 1 · Components Filter chainWhy it matters. The key filter runs before the request-id and access-log filters and ends the exchange itself. A 401 therefore has no X-Request-ID and no access-log line. A brute-force attempt or a mis-set client key is invisible in the log, and a caller cannot quote an id when reporting the problem. Update: the same filter now also ends requests with 400 (ambiguous path) and 403 (role, session, host or origin), and none of them is logged or counted either.Option. Log (and count) rejections in the key filter with the source address and path; generate the request id before the key check.Question to settle. Do we want failed-auth attempts counted and alertable, and by what source identifier?
Id noLimits · Category Availability · Impact 3 · Effort 3 · Components Filter chain, Streams (SSE), Binding & mappingWhy it matters. Done: request bodies above spring.codec.max-in-memory-size (1 MiB, YUKTA_LIMITS_MAX_BODY) get 413. Still open: nothing limits how fast a client can call or how many streams it opens. Each log stream keeps a subscription and up to 1024 buffered lines; each events stream stays open indefinitely. A client (or a UI tab that reconnects in a loop) can consume memory and connections. Start and control endpoints can be hammered, though core’s execution limits cap actual runs.Option. Per-client rate limits at a gateway or a filter, a cap on concurrent streams per key, and a maximum stream lifetime.Question to settle. Do we run behind a gateway that already provides limits, or must the server protect itself?
Id mapperDrift · Category Correctness · Impact 2 · Effort 2 · Components Binding & mapping, Session & config APIWhy it matters. SessionMapper (used when saving a session) turns a missing edge sourcePort into ‘default’. AppConfigMapper (used by the dry-run validator) leaves it null. A draft can therefore be validated with a null port and saved with ‘default’. If core treats these differently, a draft that validates could behave differently after saving. We did not confirm that core treats them differently, so this is a drift risk, not a known bug.Option. Use one mapper for request-to-definition conversion (or share the edge conversion) and cover it with a test that saves and validates the same payload.Question to settle. Does core treat a null source port and ‘default’ as the same thing everywhere?
Id deadDtos · Category Evolvability · Impact 1 · Effort 1 · Components Binding & mappingWhy it matters. ControlBusStatus, SystemHealthMetrics, ErrorExample, SessionCreationGuide, PluginCreationGuide, SessionInfo, ExecutionRecord, SessionExecutionInfo, PluginRegistryEntry, PluginReference and SessionCreationResponse have tests but no use in web’s main code; the mcp module has its own copies of most. They make the module look like it offers things it does not, and two copies can drift.Option. Delete the unused records and their tests, or move the shared ones to one module both use.Question to settle. Are any of these part of a public contract (for example a client library) that makes deleting them a breaking change?
Id staticKeys · Category Security · Impact 2 · Effort 3 · Components Filter chainWhy it matters. The first guide’s biggest risk (one shared key for everything) is closed: keys have names, roles (VIEWER, OPERATOR, ADMIN), optional session limits, a sha256: form and several can be active for rotation. What remains: (1) a key has no expiry and cannot be revoked without changing configuration and restarting; (2) roles are matched by method and path prefix, so an OPERATOR can POST anything under /api/workflow/ and /api/executions/ (including starts and raw control); (3) only control commands carry the actor, so reads and starts are not attributed anywhere; (4) there are no per-key rate limits.Option. Keys from a store that can be changed at run time, an expiry date per key, the key name in the access log, and a per-key rate limit; delegate to an identity provider when users, not services, call the API.Question to settle. How quickly must you be able to cut off one leaked key, and can you do that today without a restart?
Id searchScan · Category Performance · Impact 2 · Effort 3 · Components Run lifecycle APIWhy it matters. GET /api/executions (ExecutionSearchController) loads the history of every session (or of one), filters it in memory, sorts all hits and then takes limit. The cost per call grows with sessions times retained runs (yukta.execution-log.retained-runs, default 500 per session), and nothing caps it. The same full-load pattern as the history paging, now across sessions.Option. Push the filters and the limit into the execution log store (a query by time, status and workflow), or keep a small index of recent runs.Question to settle. How many sessions and retained runs do you expect, and how often will a dashboard poll this?

Workers

Risk ids are descriptive names. No backlog identifiers apply: this is the first guide for the module, written on 2026-10-15 from the code of main and the design in remote workers plan. Items worded ‘looks’ or ‘by Reactor’s rules’ come from reading the code, not from a measurement.
Id deposedKeepsWorkers · Category Availability · Impact 3 · Effort 2 · Components Registration & leases, Failover & drain, TestsWhy it matters. The leader check runs only when a worker registers. WorkerSessionHandler.answer replies ACK to every LEASE if the registry knows the worker, without asking whether this server is still the leader. If the leader loses the database lease but keeps running (a network split to the database, a long pause), its worker connections stay open and keep being renewed, while the new leader has no workers. Placed nodes on the new leader fail with ‘No connected worker’ until the old process stops. A crash of the old leader is handled (the connections close); this case is not, and no test covers it.Option. Answer a LEASE with REJECT 'not the leader' when election.isLeader() is false, so the agent reconnects to the next address; and close the connections when leadership is lost.Question to settle. What do your workers do when the leader is alive but cannot reach the database?
Id plainTokenDefault · Category Security · Impact 4 · Effort 2 · Components Transport & TLS, Control-plane executorWhy it matters. Without yukta.workers.ssl-bundle (worker) and an SSL bundle on the RSocket server (control plane) the connection is plain TCP. The shared token travels in the REGISTER frame, START carries the node config as written (including any literal API key, password or webhook url in it), and every input and output message is sent as JSON. Anyone on the path can read them, and can also play the control plane to a worker and send it START frames for the plugin types it runs. Mutual TLS is built and tested, but nothing requires it, even for a non-loopback address.Option. Refuse a non-loopback worker connection without TLS unless an explicit ‘insecure’ flag is set; document the two SSL bundles in the setup text; keep secrets out of placed nodes’ configs.Question to settle. Would you accept your LLM API key crossing this link in clear text?
Id sharedTokenIdentity · Category Security · Impact 3 · Effort 3 · Components Registration & leasesWhy it matters. Every worker uses the same token, and the worker id, pools and plugin types are whatever the REGISTER frame says. Anyone with the token can register as any id: registry.register replaces an existing entry and the connection map is overwritten, so the new connection receives that worker’s jobs. A token cannot be revoked for one worker, and rotating it means restarting the control plane and every worker. Two live workers with the same id also renew the same registry entry.Option. A token or certificate per worker, mapped to the ids and pools it may use; refuse a second registration for an id that is still alive.Question to settle. How do you remove one compromised worker without touching the others?
Id capacityRace · Category Correctness · Impact 2 · Effort 2 · Components Placement & selection, Node runnerWhy it matters. pick reads free slots and the caller increments the in-flight counter afterwards, in a separate step, so two attempts chosen at the same moment can both take the last slot. The worker’s NodeRunner starts every attempt it receives, so the worker runs more than its capacity. The control plane also counts only its own sends; a second control plane (a standby that takes over) starts from the lease’s used-slot value, which is up to 5 s old.Option. Reserve the slot inside the pick (compare-and-set on the counter) and have the runner refuse an attempt beyond its capacity with a retryable error.Question to settle. What happens to a worker with capacity 4 that gets 12 attempts at once?
Id liveLogsLost · Category Operability · Impact 3 · Effort 2 · Components Node runnerWhy it matters. The control plane puts a log sink into the Reactor context of a local node so that a plugin can write log lines while it runs (PROCESS_EXECUTOR streams every output line this way). NodeRunner puts only the execution id, node id and the heartbeat into the context, so on a worker the sink is absent and the plugin uses its no-op default. A placed PROCESS_EXECUTOR node shows its output only in the final result message, not live in the log feed, logs and controlbus watch. The worker’s own application log has the lines, on the worker.Option. Add a LOG frame (or reuse MESSAGE) from worker to control plane and give the runner a sink that sends it; or document the limit.Question to settle. Does anyone watch a placed build step live?
Id noTraceContext · Category Operability · Impact 2 · Effort 3 · Components Control-plane executor, Wire framesWhy it matters. The design lists trace context, an attempt number and deadlines in START. The implemented START has only execution id, node id, plugin type and config. A worker’s spans therefore do not join the run’s yukta.run trace, the node’s deadline is enforced only on the control plane (which cancels the channel), and the plugin’s meters (for example token counters) are recorded in the worker’s own registry and need their own scraping.Option. Add the trace headers to START and open the worker-side observation as a child; document that plugin meters must be collected from workers.Question to settle. Where would you look for the cost of a placed LLM_CALL node?
Id reconnectBackoff · Category Availability · Impact 2 · Effort 1 · Components Worker agent, Failover & drainWhy it matters. The agent retries with Retry.backoff(Long.MAX_VALUE, 1 s).maxBackoff(30 s) on a Mono that only ever ends in an error. Without transientErrors(true) Reactor counts every failure of the process, not failures in a row, so after a few reconnects each new pause is already near 30 s (with jitter). A failover then costs a worker up to about 45 s of idle time on top of the lease, for as long as the process lives. This follows from Reactor’s rules and is not measured here; the failover test covers a first failover only.Option. Add transientErrors(true) and reset on a successful REGISTER; log the pause.Question to settle. How long can a worker sit idle after the fifth control-plane restart?
Id abortLostAcrossWire · Category Correctness · Impact 3 · Effort 1 · Components Control-plane executorWhy it matters. An ERROR frame carries the class name and message. The executor rebuilds only IllegalArgumentException; every other class becomes a WorkflowExecutionException with the class in its text and no cause. The engine decides retry, drop and dead-letter by exception class and cause, and treats RunAbortedException (an LLM or MCP budget with hardStop) as an order to end the run. On a worker that exception arrives as a plain failure, so the budget stop can be retried or routed to a dead letter instead of ending the run, and retryOn / noRetryOn lists naming an original class do not match.Option. Map known classes back (at least RunAbortedException) and put the class chain in the error frame so the engine can match it; test a hardStop budget on a placed node.Question to settle. Does a budget stop mean the same thing on a worker as on the control plane?
Id payloadAsJson · Category Scalability · Impact 3 · Effort 4 · Components Control-plane executor, Transport & TLS, Wire framesWhy it matters. Each message is converted to a JSON map with the application’s ObjectMapper and back to DefaultMessage. Java types in the payload and in headers (dates, byte arrays, POJOs) come back as maps, lists, numbers and text. The module sets no frame or message size limit, and there is no reference for large bodies (the design leaves the payload limit as an open decision), so a large file or a long model output goes through the control plane as one frame. The boot mapper rejects unknown properties, so a field added to the message on one side and not the other fails to convert.Option. Decide a payload limit and a claim-check reference above it; test a 50 MiB message and a header holding a date.Question to settle. What is the largest message a placed node will see?
Id wireVersion · Category Evolvability · Impact 3 · Effort 3 · Components Wire frames, TestsWhy it matters. WorkerFrame and NodeFrame are the whole protocol and neither carries a version. A control plane and a worker built from different commits must agree on the record components and on DefaultMessage; a mismatch shows up as a conversion error or a field silently ignored, depending on the mapper settings. Rolling upgrades of a pool, which are the normal case, have no rule and no test.Option. Send a protocol version in REGISTER and refuse an incompatible worker with a clear reason; add a test that pairs the previous release’s frames with the current handler.Question to settle. Can you upgrade the control plane before the workers?
Id statefulPlacement · Category Correctness · Impact 2 · Effort 2 · Components Placement & selectionWhy it matters. RemoteNodes refuses SESSION_MEMORY, KV_STORE, WAIT_FOR_SIGNAL and SUB_WORKFLOW. Other plugins that keep state in memory per node or per process are allowed: the ANOMALY and DEDUPE windows, AGGREGATE’s partial batch, the MCP session cache. On a worker that state lives in whichever worker ran the attempt, and with per-message policies each message is its own attempt, so windows and batches see partial data and a retry on another worker starts empty. A new stateful plugin is placeable until someone adds it to the list.Option. Make ‘local only’ a flag on the plugin (as the design says) instead of a list in core, and mark the window-based plugins.Question to settle. Which of your plugins keep anything between messages?
Id staleConnections · Category Reliability · Impact 2 · Effort 2 · Components Worker agentWhy it matters. On a REJECT the agent throws, and the retry calls connect again for a new connection; only the latest requester is stored in current, and only that one is disposed at stop. The control plane ends the response stream but nothing closes the underlying RSocket connection, so each rejection (‘lease expired, register again’, or ‘not the leader’ from a standby) can leave a connection open on both sides until a process ends. This looks like a leak from reading the code; it was not observed.Option. Dispose the previous requester before reconnecting and when the session stream ends; add a test that counts open connections after repeated rejections.
Id perAttemptInit · Category Correctness · Impact 2 · Effort 3 · Components Node runnerWhy it matters. For each attempt the runner calls the plugin’s initialize(config) and prepare(config); with per-message policies that is once per message. Plugins are one shared object per type on the worker, so a plugin that stores config in a field (see the plugin-api risk about shared instances) mixes up settings between two placed nodes on the same worker. shutdown is never called on a worker either.Option. Pass the config into each call instead of keeping it in fields, and prepare once per node and config; call shutdown at worker stop.
Id budgetPerJvm · Category Governance · Impact 3 · Effort 3 · Components Node runnerWhy it matters. The shared run-budget ledger (RunBudgets.shared()) and the provider rate limiter in LlmGateway are static, in-memory objects of the JVM in which the plugin runs. A placed LLM_CALL or MCP_TOOL_CALL node therefore counts tokens, cost, calls and requests per minute in the worker, keyed by execution id, and each worker has its own totals. A run spread over three workers can spend three times its budget, and a provider limit of 60 requests per minute becomes 60 per worker.Option. Count budgets and limits on the control plane (reserve before sending START, settle with the result), or keep them out of placeable plugins.Question to settle. Who enforces the budget when two workers serve one run?
Id nativeUnverified · Category Operability · Impact 3 · Effort 3 · Components Roles & auto-configuration, Native imageWhy it matters. The module’s own notes say both native roles build and start, that a native control plane reports no workers, and that a native worker did not register because the frame records had no reflection hints. The hints (@RegisterReflectionForBinding) have been added but not checked in a native image because the rebuild ran out of memory. Spring decides property-conditioned beans at image build time, so a native image built without the role switch starts but never takes workers, and one image cannot be both roles.Option. Run a native registration test in CI on a machine with enough memory; fail the build or start when the role switch is missing.Question to settle. Do you need native workers, or is a JVM worker enough for now?