Summary
A controller's response shape depends on whether its handler happened to log
Found during the e2e coverage wave and independently verified by a second engineer who did not
write the original report — verification was cross-assigned so nobody checked their own finding.
- Namespace / method: cross-cutting —
RpcOutcome::into_cli_compatible_json,
the shared return path for most controllers. Observed on
openhuman.javascript_list_tools / openhuman.javascript_execute_tool vs
openhuman.skills_read_resource / openhuman.skills_recent_runs.
- Where:
src/rpc/mod.rs:54-62
- Verified at:
openhuman main @ d1ed68f9b
Problem
Expected
one namespace, one response shape. A caller writes one parser.
Actual
pub fn into_cli_compatible_json(self) -> Result<Value, String> {
let RpcOutcome { value, logs } = self;
let value = serde_json::to_value(value)?;
if logs.is_empty() {
Ok(value) // bare
} else {
Ok(json!({ "result": value, "logs": logs })) // wrapped
}
}
The envelope is decided by the log vector, not by the method. So:
| method |
logs |
response |
javascript_list_tools |
always 1 line |
{result: {tools: […]}, logs: […]} |
skills_read_resource |
Vec::new() |
{workflow_id, content, bytes, …} bare |
skills_recent_runs |
Vec::new() |
{runs: […]} bare |
Both spellings live in the same skills/javascript family and neither is
documented in the controller catalog — the ControllerSchema outputs describe
the inner value in both cases, so the schema is silently right for one and
silently wrong for the other.
The failure mode is nasty because it is conditional: a handler that gains a
tracing-adjacent log line later changes its own wire shape without any
schema change, and a caller that worked yesterday reads undefined today.
Nothing in CI would catch it.
Steps to reproduce
tests/raw_coverage/skill_runtime_e2e.rs →
javascript_bridge_lists_and_executes_a_real_tool, which asserts the wrapped
form explicitly before unwrapping, so the day this is normalised the suite
fails loudly. Found the hard way: the first run of that case died on
no 'tools' array: {"logs":["javascript.list_tools: count=227"],"result":{"tools":[…]}}.
Independent verification (CONFIRMED — and the blast radius is materially larger than the report says.)
Cited location still accurate: yes, exactly. src/rpc/mod.rs:54-62, quoted
verbatim and unchanged.
Repro run: cargo test --test raw_coverage_all --features "$(bash scripts/ci/product-features.sh)" -- W5VERIFY_
plus grep -rln "'result' in " app/src/ | xargs grep -ln logs
Observed
test W5VERIFY_skill_runtime_e2e::javascript_bridge_lists_and_executes_a_real_tool ... ok
Per-method construction confirmed at source:
// src/openhuman/runtime/node/rpc.rs:26-33 → non-empty log → WRAPPED
let log = vec![format!("javascript.list_tools: count={}", …)];
RpcOutcome::new(payload, log).into_cli_compatible_json()
// src/openhuman/skills/schemas/handlers.rs:147-150 → Vec::new() → BARE
to_json(RpcOutcome::new(WorkflowsRecentRunsResult { runs }, Vec::new()))
// src/openhuman/skills/schemas/handlers.rs:203-211 → Vec::new() → BARE
Diagnosis assessment: correct, and I hit this independently while writing the W4
suites — billing_get_balance (RpcOutcome::single_log) comes back wrapped while
cost_get_dashboard (RpcOutcome::new(_, Vec::new())) comes back bare, from two
handlers a caller would reasonably treat as one family. My w4_shared::peel() helper
exists solely to absorb this, which is corroboration from a second worker who did not
know of this report.
Is the expected behaviour correct: yes — "one namespace, one response shape" is
the right expectation, and the report is right that fixing it is a breaking change
needing a migration rather than a quiet normalisation.
Solution
as the report says — make the envelope
unconditional, or bare with logs on a side channel. Whichever is chosen, the 13 files
below are the migration surface. Interim mitigation with no breaking change: have the
controller catalog record which shape each controller returns, since that is
derivable from whether the handler passes a non-empty log vector.
Blast radius:
Acceptance criteria
Related
Filed from the e2e coverage wave (#6053, #6054, #6055, #6058, #6059, #6060), which covered 244
previously untested RPC controllers and surfaced 33 defects along the way.
Summary
A controller's response shape depends on whether its handler happened to log
Found during the e2e coverage wave and independently verified by a second engineer who did not
write the original report — verification was cross-assigned so nobody checked their own finding.
RpcOutcome::into_cli_compatible_json,the shared return path for most controllers. Observed on
openhuman.javascript_list_tools / openhuman.javascript_execute_tool vs
openhuman.skills_read_resource / openhuman.skills_recent_runs.
src/rpc/mod.rs:54-62openhumanmain@d1ed68f9bProblem
Expected
one namespace, one response shape. A caller writes one parser.
Actual
The envelope is decided by the log vector, not by the method. So:
javascript_list_tools{result: {tools: […]}, logs: […]}skills_read_resourceVec::new(){workflow_id, content, bytes, …}bareskills_recent_runsVec::new(){runs: […]}bareBoth spellings live in the same
skills/javascriptfamily and neither isdocumented in the controller catalog — the
ControllerSchemaoutputs describethe inner value in both cases, so the schema is silently right for one and
silently wrong for the other.
The failure mode is nasty because it is conditional: a handler that gains a
tracing-adjacent log line later changes its own wire shape without anyschema change, and a caller that worked yesterday reads
undefinedtoday.Nothing in CI would catch it.
Steps to reproduce
tests/raw_coverage/skill_runtime_e2e.rs→javascript_bridge_lists_and_executes_a_real_tool, which asserts the wrappedform explicitly before unwrapping, so the day this is normalised the suite
fails loudly. Found the hard way: the first run of that case died on
no 'tools' array: {"logs":["javascript.list_tools: count=227"],"result":{"tools":[…]}}.Independent verification (CONFIRMED — and the blast radius is materially larger than the report says.)
Cited location still accurate: yes, exactly.
src/rpc/mod.rs:54-62, quotedverbatim and unchanged.
Repro run:
cargo test --test raw_coverage_all --features "$(bash scripts/ci/product-features.sh)" -- W5VERIFY_plus
grep -rln "'result' in " app/src/ | xargs grep -ln logsObserved
Per-method construction confirmed at source:
Diagnosis assessment: correct, and I hit this independently while writing the W4
suites —
billing_get_balance(RpcOutcome::single_log) comes back wrapped whilecost_get_dashboard(RpcOutcome::new(_, Vec::new())) comes back bare, from twohandlers a caller would reasonably treat as one family. My
w4_shared::peel()helperexists solely to absorb this, which is corroboration from a second worker who did not
know of this report.
Is the expected behaviour correct: yes — "one namespace, one response shape" is
the right expectation, and the report is right that fixing it is a breaking change
needing a migration rather than a quiet normalisation.
Solution
as the report says — make the envelope
unconditional, or bare with logs on a side channel. Whichever is chosen, the 13 files
below are the migration surface. Interim mitigation with no breaking change: have the
controller catalog record which shape each controller returns, since that is
derivable from whether the handler passes a non-empty log vector.
Blast radius:
Acceptance criteria
Related
Filed from the e2e coverage wave (#6053, #6054, #6055, #6058, #6059, #6060), which covered 244
previously untested RPC controllers and surfaced 33 defects along the way.