From cf0a8f034afd36c644a74e24d98021781ee44d3f Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:07:09 +0000 Subject: [PATCH 1/2] interpreter: read transport response_format as the authored WireFormat variant, as the emitter does; refuse any other spelling dispatch_rest read response_format only as a string literal and defaulted every other spelling to Json, so response_format: Text (github.Pulls.Diff's typed spelling) was silently JSON-decoded while emitted code read it as text. It now reads through v1.std.core transport_response_format and authored_name_at, the emitter's own reading. Replay witnesses: a plain-text body under Text reads as text (RED before: RestBodyUndecodable at 200), and an unrecognized spelling refuses. RFM row response_format_variant_silently_read_as_json. Co-Authored-By: Claude Opus 5.5 (1M context) --- ...e_format_variant_silently_read_as_json.dag | 31 +++++++++++ dag/test/claim/rest_exchange_replay_test.dag | 40 ++++++++++++++ .../fixture/rest_exchange_replay_probe.dag | 24 +++++++++ src/v1/stage0/src/v1_interpreter.rs | 53 +++++++++++++------ 4 files changed, 133 insertions(+), 15 deletions(-) create mode 100644 dag/gunbc/recurring_failure_mode/response_format_variant_silently_read_as_json.dag diff --git a/dag/gunbc/recurring_failure_mode/response_format_variant_silently_read_as_json.dag b/dag/gunbc/recurring_failure_mode/response_format_variant_silently_read_as_json.dag new file mode 100644 index 00000000000..780895d0c19 --- /dev/null +++ b/dag/gunbc/recurring_failure_mode/response_format_variant_silently_read_as_json.dag @@ -0,0 +1,31 @@ +module gunbc.recurring_failure_mode.response_format_variant_silently_read_as_json + +import std.types { NonEmptyStr } +import std.decl_ref { DeclarationRef, WholeDeclaration } +import gunbc.recurring_failure_mode { RecurringFailureMode } + +data response_format_variant_silently_read_as_json: RecurringFailureMode = RecurringFailureMode { + identity: "response_format_variant_silently_read_as_json" as NonEmptyStr, + receipts: [ + "**a `transport rest` response_format the two realizations read differently, with the interpreter's unread spelling silently defaulting to Json** (INVALID STATE: `response_format: Text`, the std.serialization WireFormat variant extdeps.github.pulls github.Pulls.Diff writes, was read as text by emitted code -- v1.compiler.emit_rust emit_plain_response_body takes the authored variant name -- and as ABSENT by the interpreter, whose dispatch_rest read the property only as a string literal and defaulted every other spelling to Json. HARM: a silent wrong answer (DESIGN section 5). The interpreter JSON-decoded a plain-text body and answered RestBodyUndecodable at status 200, or raised on an operation with no outcome field, while the declaration said text; the quoted \"Text\" that DID work in the interpreter was read as Json by emitted code. One declaration, two meanings, neither refusal located at the declaration.)", + + "DISCRIMINATING PROBE, 2026-10-04, live against api.github.com on gunb-ai/gunbc#13225 with the PR-diff Accept header: two operations identical except for the spelling. `response_format: Text` answered RestBodyUndecodable { status: 200, cause: JSON body did not decode: expected value at line 1 column 1 }; `response_format: \"Text\"` answered RestOk with 2,964 diff lines. Found while modeling extdeps.github.workflow_runs DownloadJobLogsForWorkflowRun, whose first live read failed the same way.", + + "HERMETIC RED, now enrolled: test.claim.rest_exchange_replay_test a_text_response_format_reads_a_plain_body_as_text replays a plain-text 200 through dispatch_rest for an operation spelled `response_format: Text` (test.fixture.rest_exchange_replay_probe TextBody); before the fix it answered RestBodyUndecodable. an_unrecognized_response_format_refuses_rather_than_defaulting_to_json is the other side: any spelling that is not a WireFormat variant refuses with a located message instead of defaulting.", + + "THE FIX MADE ONE READING, NOT A SECOND ONE: the interpreter now reads the property through the same v1.std.core transport_response_format and authored_name_at the emitter uses, accepts Text and Json, keeps absent as Json, and refuses anything else.", + + "WHAT IS NOT FIXED, named so it is not read as covered: emitted code still treats any non-Text spelling, including the quoted string, as Json without refusing (emit_plain_response_body's is_text is a Bool over the authored name). Making that a refusal is a change to the .dag source of v1.compiler.emit_rust plus the stage0 mirror regeneration, and is this row's next rung.", + + "RUNG FOUND AT: below the ladder -- silent wrongness in the interpreter. RUNG NOW: mechanically preventable for the interpreter (the replay witness reds a regression on the acceptance path); the emitter path stays mitigatable at best.", + + "CEILING: structurally impossible. response_format's value is a closed two-variant type, so the declaration can carry WireFormat itself and the reserved-key read can decode it as that type, leaving no spelling for either realization to misread.", + + "NEXT-RUNG TRIGGER, A CAPABILITY: the transport property is decoded as std.serialization WireFormat at parse, once, and both the interpreter and v1.compiler.emit_rust consume that decoded value -- sufficient that a spelling which is not a variant is refused at the declaration and the two realizations cannot read one declaration two ways.", + ], + evidence: [ + DeclarationRef { module_path: "test.claim.rest_exchange_replay_test", decl_name: "a_text_response_format_reads_a_plain_body_as_text", field: WholeDeclaration }, + DeclarationRef { module_path: "test.claim.rest_exchange_replay_test", decl_name: "an_unrecognized_response_format_refuses_rather_than_defaulting_to_json", field: WholeDeclaration }, + DeclarationRef { module_path: "std.serialization", decl_name: "WireFormat", field: WholeDeclaration }, + ], +} diff --git a/dag/test/claim/rest_exchange_replay_test.dag b/dag/test/claim/rest_exchange_replay_test.dag index 0ffe7ca3bb1..0a4386c8b25 100644 --- a/dag/test/claim/rest_exchange_replay_test.dag +++ b/dag/test/claim/rest_exchange_replay_test.dag @@ -444,3 +444,43 @@ test fn a_coproduct_with_no_declared_wire_spelling_refuses_even_an_authored_matc WitnessInterrupted { at: _, route: _, state: _ } => false } } + +// THE TYPED RESPONSE FORMAT IS HONOURED (gunbc.recurring_failure_mode +// response_format_variant_silently_read_as_json). RED before the fix: the interpreter read +// response_format only as a string literal, so `response_format: Text` fell back to Json and this +// plain-text body answered RestBodyUndecodable at status 200. +test fn a_text_response_format_reads_a_plain_body_as_text() -> Bool { + let result = evaluate_in_witness_frame( + frame: replay_frame(fixtures: [fixture_for( + operation: "TextBody", + observation: RestResponseObserved { status: 200, body: RestBodyRead { body: "diff --git a/x b/x\n+plain text, not JSON" } } + )]), + subject: fn(_scope) { test.RestReplay.TextBody() } + ) + match result { + WitnessReturned { value } => value.text == "diff --git a/x b/x\n+plain text, not JSON" && match value.observed { + RestOk => true + _ => false + } + WitnessRefused { diagnostic } => false + WitnessInterrupted { at: _, route: _, state: _ } => false + } +} + +// ANY OTHER SPELLING REFUSES. The quoted string is the one that used to be the only working +// spelling in the interpreter and was read as Json by emitted code; now neither realization +// can take it, and the interpreter says so instead of guessing. +test fn an_unrecognized_response_format_refuses_rather_than_defaulting_to_json() -> Bool { + let result = evaluate_in_witness_frame( + frame: replay_frame(fixtures: [fixture_for( + operation: "FormatUnrecognized", + observation: RestResponseObserved { status: 200, body: RestBodyRead { body: "plain" } } + )]), + subject: fn(_scope) { test.RestReplay.FormatUnrecognized() } + ) + match result { + WitnessReturned { value } => false + WitnessRefused { diagnostic } => contains(witness_diagnostic_rendered_reason(diagnostic: diagnostic), "response_format must be a std.serialization WireFormat variant") + WitnessInterrupted { at: _, route: _, state: _ } => false + } +} diff --git a/dag/test/fixture/rest_exchange_replay_probe.dag b/dag/test/fixture/rest_exchange_replay_probe.dag index cea8cdc691f..b0462475ca5 100644 --- a/dag/test/fixture/rest_exchange_replay_probe.dag +++ b/dag/test/fixture/rest_exchange_replay_probe.dag @@ -2,6 +2,7 @@ module test.fixture.rest_exchange_replay_probe import std.types { Bool, Int, List, String } import extdeps.transports.rest { RestOutcome } +import std.serialization { Text } import extdeps.github.workflow_runs { WorkflowRunList } import extdeps.cloud.gcp.gcp { WifProviderWire, WifProviderListWire, WifPoolWire } @@ -40,6 +41,29 @@ service test.RestReplay { transport rest { method: GET, path: "/answer" } } + // THE RESPONSE FORMAT, SPELLED AS THE std.serialization WireFormat VARIANT github.Pulls.Diff uses. + // A plain-text body must come back as text through dispatch_rest, not through the JSON decoder. + operation TextBody { + output { + text: String + observed: RestOutcome + } + readonly + transport rest { method: GET, path: "/answer", response_format: Text } + response { 200 => String } + } + + // A response_format that is no WireFormat variant: a refusal, never a silent Json default. + operation FormatUnrecognized { + output { + text: String + observed: RestOutcome + } + readonly + transport rest { method: GET, path: "/answer", response_format: "Text" } + response { 200 => String } + } + operation RootArray { output { pulls: List diff --git a/src/v1/stage0/src/v1_interpreter.rs b/src/v1/stage0/src/v1_interpreter.rs index dbbd8d460b9..e3f0630332c 100644 --- a/src/v1/stage0/src/v1_interpreter.rs +++ b/src/v1/stage0/src/v1_interpreter.rs @@ -63,16 +63,16 @@ use crate::v1_std_core::{ binop_right, block_stmts, cast_expr, cast_target, expr_call_func_at, expr_field_access_summary, expr_method_call_semantics, expr_method_name_at, expr_var_name_at, field_access_base, field_access_field_at, field_binding_name_at, field_binding_pattern, field_init_node_name_at, - field_init_node_value, find_property, find_property_string, foreach_body, foreach_collection, - foreach_variable_at, if_condition, if_else_branch, if_then_branch, import_is_all, - import_specific_names_at, index_base, index_expr, is_file_transport, is_rest_transport, - is_shell_transport, lambda_body, lambda_param_names_at, let_binding_name_at, let_body, - let_value, match_arm_nodes, match_scrutinee, method_arg_nodes, method_receiver, - param_node_default_value, param_node_name_at, qualified_last_segment, record_lit_type_name_at, - return_value, slice_base, slice_end, slice_start, transport_stdin, type_name_compatible, - unaryop_operand, CallSemantics, Cardinality, Connective, ErrorNode, ExprData, FieldAccessStyle, - FieldSummary, FieldValueShape, InferredNode, MatchPattern, MethodSemantics, NewlineIndex, Node, - SourceSpan, StringPart, UnaryOpKind, VarBindingKind, + field_init_node_value, find_property, foreach_body, foreach_collection, foreach_variable_at, + if_condition, if_else_branch, if_then_branch, import_is_all, import_specific_names_at, + index_base, index_expr, is_file_transport, is_rest_transport, is_shell_transport, lambda_body, + lambda_param_names_at, let_binding_name_at, let_body, let_value, match_arm_nodes, + match_scrutinee, method_arg_nodes, method_receiver, param_node_default_value, + param_node_name_at, qualified_last_segment, record_lit_type_name_at, return_value, slice_base, + slice_end, slice_start, transport_stdin, type_name_compatible, unaryop_operand, CallSemantics, + Cardinality, Connective, ErrorNode, ExprData, FieldAccessStyle, FieldSummary, FieldValueShape, + InferredNode, MatchPattern, MethodSemantics, NewlineIndex, Node, SourceSpan, StringPart, + UnaryOpKind, VarBindingKind, }; #[path = "bounded_shell_host_drain.rs"] @@ -19080,12 +19080,35 @@ fn dispatch_rest( None => None, }; - let response_format = find_property_string( - transport.properties.clone(), - "response_format".to_string(), + // THE RESPONSE FORMAT IS THE AUTHORED std.serialization WireFormat VARIANT, READ THE WAY THE + // EMITTER READS IT (v1.compiler.emit_rust emit_plain_response_body: transport_response_format, + // then authored_name_at), so the interpreter and emitted code cannot disagree on one + // declaration. This site used to read only a string literal and default every other spelling + // to Json, so `response_format: Text` -- the typed spelling github.Pulls.Diff writes -- was + // silently JSON-decoded here while emitted code read it as text + // (gunbc.recurring_failure_mode response_format_variant_silently_read_as_json). Absent is + // Json, as before; a present value that is not a WireFormat variant refuses rather than + // defaulting. + let response_format = match crate::v1_std_core::transport_response_format( + transport.clone(), si.clone(), - ) - .unwrap_or_else(|| "Json".to_string()); + ) { + None => "Json".to_string(), + Some(node) => { + let authored = crate::v1_std_core::authored_name_at(si.clone(), node.clone()); + if authored == "Text" || authored == "Json" { + authored + } else { + return Err(InterpError::TypeError { + msg: format!( + "transport rest response_format must be a std.serialization WireFormat variant (Json or Text); \ + found {:?} on {}", + authored, op_node.name + ), + }); + } + } + }; // TLS posture (extdeps.transports.rest TlsPosture). Absent = VerifyPeer, the fail-closed // default (ureq's stock rustls verifier). InsecureAcceptAnyCert is the modeled dissolution From bf0d1546e4c9b7b3130571f32ad51cfa81f81a5e Mon Sep 17 00:00:00 2001 From: gunbc-ci-auto-heal Date: Sun, 4 Oct 2026 06:29:00 +0000 Subject: [PATCH 2/2] rest replay probe: move the response_format rationale to module-item grain (DESIGN 4c; parse phase refused annotations inside the service body) Co-Authored-By: Claude Opus 5.5 (1M context) --- dag/test/fixture/rest_exchange_replay_probe.dag | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/dag/test/fixture/rest_exchange_replay_probe.dag b/dag/test/fixture/rest_exchange_replay_probe.dag index b0462475ca5..190bfbfaa2c 100644 --- a/dag/test/fixture/rest_exchange_replay_probe.dag +++ b/dag/test/fixture/rest_exchange_replay_probe.dag @@ -29,6 +29,10 @@ type WireUncontractedShape { // WifProvidersPage and WifPool carry the OUTPUT SHAPES of extdeps.cloud.gcp.iam_admin // ListWorkloadIdentityPoolProviders and GetWorkloadIdentityPool verbatim, so a recorded IAM body // reaches the same type-directed decode gunbc.auth.heal_publisher_provision reads through. +// TextBody spells response_format as the std.serialization WireFormat variant github.Pulls.Diff +// uses, so a plain-text body must come back as text through dispatch_rest rather than through the +// JSON decoder. FormatUnrecognized spells it as no WireFormat variant, which must refuse rather than +// default to Json. service test.RestReplay { config { endpoint: "https://rest-replay.invalid" } @@ -41,8 +45,6 @@ service test.RestReplay { transport rest { method: GET, path: "/answer" } } - // THE RESPONSE FORMAT, SPELLED AS THE std.serialization WireFormat VARIANT github.Pulls.Diff uses. - // A plain-text body must come back as text through dispatch_rest, not through the JSON decoder. operation TextBody { output { text: String @@ -53,7 +55,6 @@ service test.RestReplay { response { 200 => String } } - // A response_format that is no WireFormat variant: a refusal, never a silent Json default. operation FormatUnrecognized { output { text: String