Description
While diagnosing the rendered sidebar pagination failure in #572, we found a pre-existing contract problem in the shared Tauri RPC layer.
Rust returns native session categories using wire values such as:
The frontend output schema intentionally transforms those wire values into the canonical dispatch categories used by the application:
agent -> rust_agent
cli -> cli_agent
human -> human_session
However, typedInvoke calls output.safeParse(transformed) only as a development-time check and discards the successful parsed.data. It then returns the original transformed value:
if (output && isDev) {
const parsed = output.safeParse(transformed);
// parsed.data is not returned
}
return transformed;
As a result, Zod output transforms, preprocessors, defaults, and normal object parsing semantics are described by the TypeScript return type but are not applied to the runtime value.
This behavior has existed since the shared RPC wrapper was introduced; #572 did not create it. The new native sidebar pagination path exposed it because the returned page was loaded successfully but its rows still carried the Rust wire category agent, while the frontend grouping code looked for rust_agent.
Steps to Reproduce
-
Define an RPC output schema that transforms a backend value:
const Output = z.object({
category: z
.enum(["agent"])
.transform(() => "rust_agent" as const),
});
-
Mock the Tauri command response as:
-
Call the procedure through typedInvoke.
-
Inspect the returned runtime value.
The TypeScript return type and a direct Output.parse(...) both report category: "rust_agent", but typedInvoke returns category: "agent".
The same behavior can be reproduced with a Zod output .default(...) or z.preprocess(...): direct schema parsing applies the change, but the actual RPC call does not.
Expected Behavior
- The runtime value returned by an RPC call matches the procedure's declared output type.
- Output normalization has the same semantics in development and production builds.
- If output schemas are allowed to transform values or provide defaults, the RPC layer returns the parsed result.
- If output schemas are validation-only by design, mutation-capable Zod features are prohibited there and all required normalization is performed by an explicit always-running mapper.
Actual Behavior
- RPC inputs use
parsed.data, so input transforms and defaults work.
- Explicit procedure-level
.transform(...) functions run before validation and work.
- Zod output schema transformations/defaults/preprocessors do not affect the returned value.
- Development builds log invalid output but still return it.
- Production builds skip output parsing entirely for performance.
- TypeScript therefore promises a post-parse output shape that the runtime does not guarantee.
Confirmed User-Visible Impact
Session aggregate category routing
Affected procedures:
session_aggregate_list
session_native_sidebar_page
Affected schema:
SessionAggregateRecordSchema
Rust returns cli | agent | os | human, while the frontend schema declares cli_agent | rust_agent | human_session.
During #572 verification, a native sidebar page could load successfully but fail to render because an agent row did not match the frontend's rust_agent grouping filters. This presented as a Load more click that fetched data without displaying a new row.
The #572 branch currently contains a narrow safety normalization in the session conversion boundary. That workaround fixes the sidebar behavior but does not change the shared RPC contract.
Other Affected Output Schemas
The following are currently latent or compatibility risks rather than confirmed second user-visible regressions.
| Domain |
Intended Zod behavior that is currently discarded |
Current practical risk |
| Session event/history |
z.preprocess(...) converts legacy string/null/primitive args and result values into records |
Rust currently performs similar normalization, but an older or alternate response path can still expose a runtime shape that contradicts the TypeScript type |
| Turn metadata |
Missing modifiedFiles, resourceInteractions, and gitArtifacts default to [] |
Current Rust responses populate these vectors; a partial or cross-version response could cause .length, .filter, or iteration failures |
| MCP |
Missing mcpServers, disabled, and scope receive safe defaults |
Current Rust structs normally serialize these fields; incomplete responses could produce wrong toggle/scope behavior or undefined collection access |
| Key Vault and model metadata |
Missing model context maps, display names, variant flags, and account metadata receive defaults |
Current Rust structs normally serialize the fields; compatibility fallbacks advertised by the schemas are not real at the RPC boundary |
| Cursor native models |
Missing aliases and maxMode receive defaults |
Current backend normally supplies them; partial responses do not receive the declared fallback |
A repository scan currently finds 237 procedure output declarations across 21 RPC procedure files. Most schemas are validation-only and will work as long as Rust returns exactly the expected shape. They are not all known bugs, but they all inherit the same runtime-contract limitation.
Additional Contract Risks
-
Runtime/type mismatch
Callers compile against z.output<TOutput>, even though the actual value may still have the wire shape.
-
Development and production do not enforce the same contract
Development performs a warning-only parse. Production does not parse output.
-
Successful Zod sanitization is discarded
Standard z.object(...) parsing normally returns the parsed object according to its schema. Returning the raw value means unexpected backend fields can remain present at runtime even when the TypeScript type omits them. No confirmed data exposure has been identified, but the schema is not acting as a runtime sanitization boundary.
-
Schema-only tests can provide false confidence
Current tests prove that direct schema parsing applies category conversion and legacy event normalization. They do not prove that calling the procedure through typedInvoke returns the parsed value.
Relevant Areas
src/api/tauri/rpc/invoke.ts
src/api/tauri/rpc/__tests__/router.test.ts
src/api/tauri/rpc/schemas/sessionAggregate.ts
src/api/tauri/rpc/schemas/sessionCore.ts
src/api/tauri/rpc/schemas/mcp.ts
src/api/tauri/rpc/schemas/validationValueObjects.ts
src/api/tauri/rpc/schemas/validationProcedures.ts
src/api/tauri/session/index.ts
Suggested Resolution
First make the RPC contract explicit, then implement one consistent model:
Option A: parsed output is the contract
- Parse output in every build.
- Return
parsed.data.
- Define a clear failure policy for invalid responses.
- Measure the cost on high-frequency RPC paths before landing globally.
Option B: schemas validate only; normalization is explicit
- Keep production output validation optional if required for performance.
- Prohibit Zod output transforms, preprocessors, and defaults.
- Move required wire-to-frontend normalization into an explicit procedure-level mapper that runs in every build.
- Type the procedure return value from the mapper's output rather than from a schema transformation that is never returned.
Avoid returning parsed.data only in development, because that would make development and production return different semantic values.
The narrow #572 session normalization should remain until the shared contract is fixed and the session procedures have been migrated and verified.
Acceptance Criteria
Environment
Additional Context
Description
While diagnosing the rendered sidebar pagination failure in #572, we found a pre-existing contract problem in the shared Tauri RPC layer.
Rust returns native session categories using wire values such as:
{ "category": "agent" }The frontend output schema intentionally transforms those wire values into the canonical dispatch categories used by the application:
However,
typedInvokecallsoutput.safeParse(transformed)only as a development-time check and discards the successfulparsed.data. It then returns the originaltransformedvalue:As a result, Zod output transforms, preprocessors, defaults, and normal object parsing semantics are described by the TypeScript return type but are not applied to the runtime value.
This behavior has existed since the shared RPC wrapper was introduced; #572 did not create it. The new native sidebar pagination path exposed it because the returned page was loaded successfully but its rows still carried the Rust wire category
agent, while the frontend grouping code looked forrust_agent.Steps to Reproduce
Define an RPC output schema that transforms a backend value:
Mock the Tauri command response as:
{ "category": "agent" }Call the procedure through
typedInvoke.Inspect the returned runtime value.
The TypeScript return type and a direct
Output.parse(...)both reportcategory: "rust_agent", buttypedInvokereturnscategory: "agent".The same behavior can be reproduced with a Zod output
.default(...)orz.preprocess(...): direct schema parsing applies the change, but the actual RPC call does not.Expected Behavior
Actual Behavior
parsed.data, so input transforms and defaults work..transform(...)functions run before validation and work.Confirmed User-Visible Impact
Session aggregate category routing
Affected procedures:
session_aggregate_listsession_native_sidebar_pageAffected schema:
SessionAggregateRecordSchemaRust returns
cli | agent | os | human, while the frontend schema declarescli_agent | rust_agent | human_session.During #572 verification, a native sidebar page could load successfully but fail to render because an
agentrow did not match the frontend'srust_agentgrouping filters. This presented as aLoad moreclick that fetched data without displaying a new row.The #572 branch currently contains a narrow safety normalization in the session conversion boundary. That workaround fixes the sidebar behavior but does not change the shared RPC contract.
Other Affected Output Schemas
The following are currently latent or compatibility risks rather than confirmed second user-visible regressions.
z.preprocess(...)converts legacy string/null/primitiveargsandresultvalues into recordsmodifiedFiles,resourceInteractions, andgitArtifactsdefault to[].length,.filter, or iteration failuresmcpServers,disabled, andscopereceive safe defaultsaliasesandmaxModereceive defaultsA repository scan currently finds 237 procedure output declarations across 21 RPC procedure files. Most schemas are validation-only and will work as long as Rust returns exactly the expected shape. They are not all known bugs, but they all inherit the same runtime-contract limitation.
Additional Contract Risks
Runtime/type mismatch
Callers compile against
z.output<TOutput>, even though the actual value may still have the wire shape.Development and production do not enforce the same contract
Development performs a warning-only parse. Production does not parse output.
Successful Zod sanitization is discarded
Standard
z.object(...)parsing normally returns the parsed object according to its schema. Returning the raw value means unexpected backend fields can remain present at runtime even when the TypeScript type omits them. No confirmed data exposure has been identified, but the schema is not acting as a runtime sanitization boundary.Schema-only tests can provide false confidence
Current tests prove that direct schema parsing applies category conversion and legacy event normalization. They do not prove that calling the procedure through
typedInvokereturns the parsed value.Relevant Areas
src/api/tauri/rpc/invoke.tssrc/api/tauri/rpc/__tests__/router.test.tssrc/api/tauri/rpc/schemas/sessionAggregate.tssrc/api/tauri/rpc/schemas/sessionCore.tssrc/api/tauri/rpc/schemas/mcp.tssrc/api/tauri/rpc/schemas/validationValueObjects.tssrc/api/tauri/rpc/schemas/validationProcedures.tssrc/api/tauri/session/index.tsSuggested Resolution
First make the RPC contract explicit, then implement one consistent model:
Option A: parsed output is the contract
parsed.data.Option B: schemas validate only; normalization is explicit
Avoid returning
parsed.dataonly in development, because that would make development and production return different semantic values.The narrow #572 session normalization should remain until the shared contract is fixed and the session procedures have been migrated and verified.
Acceptance Criteria
typedInvokeintegration test proves that an output transformation affects the actual returned runtime value..transform(...)calls, including snake-case-to-camel-case mappings, continue to work.session_aggregate_listandsession_native_sidebar_pagereturn canonical frontend categories to their callers without relying on an inaccurate TypeScript assertion.npm run typecheckand focused RPC/session/MCP/Key Vault tests pass.Environment
rust_agentsession paginationAdditional Context