(
&self,
tsfn: &napi::threadsafe_function::ThreadsafeFunction<
serde_json::Value,
napi::threadsafe_function::ErrorStrategy::Fatal,
>,
args: serd
| 5140 | |
| 5141 | impl NodeBudgetGuard { |
| 5142 | fn call_decision( |
| 5143 | &self, |
| 5144 | tsfn: &napi::threadsafe_function::ThreadsafeFunction< |
| 5145 | serde_json::Value, |
| 5146 | napi::threadsafe_function::ErrorStrategy::Fatal, |
| 5147 | >, |
| 5148 | args: serde_json::Value, |
| 5149 | ) -> a3s_code_core::budget::BudgetDecision { |
| 5150 | let (tx, rx) = std::sync::mpsc::sync_channel::<a3s_code_core::budget::BudgetDecision>(1); |
| 5151 | tsfn.call_with_return_value( |
| 5152 | args, |
| 5153 | napi::threadsafe_function::ThreadsafeFunctionCallMode::NonBlocking, |
| 5154 | move |ret: napi::JsUnknown| { |
| 5155 | // FAIL-CLOSED: if the JS return value can't even be read as |
| 5156 | // a napi value, deny rather than allow. A budget guard is a |
| 5157 | // cost/quota control — silently permitting on a broken |
| 5158 | // response is the dangerous direction. (Explicit responses |
| 5159 | // like null / {decision:'allow'} are still parsed leniently |
| 5160 | // as Allow inside parse_js_budget_decision.) |
| 5161 | let decision = parse_js_budget_decision(ret).unwrap_or_else(|_| { |
| 5162 | a3s_code_core::budget::BudgetDecision::Deny { |
| 5163 | resource: "budget_guard_error".to_string(), |
| 5164 | reason: "budget guard return value could not be read".to_string(), |
| 5165 | } |
| 5166 | }); |
| 5167 | let _ = tx.send(decision); |
| 5168 | Ok(()) |
| 5169 | }, |
| 5170 | ); |
| 5171 | // FAIL-CLOSED on timeout: a hung or throwing guard (under Fatal |
| 5172 | // strategy a JS throw means the return closure never fires, so the |
| 5173 | // channel stays empty and we hit this timeout) must DENY, not |
| 5174 | // Allow. Previously this defaulted to Allow — meaning a slow/buggy |
| 5175 | // guard silently disabled budget enforcement (a fail-open hole). |
| 5176 | tokio::task::block_in_place(|| { |
| 5177 | rx.recv_timeout(std::time::Duration::from_millis(self.timeout_ms)) |
| 5178 | .unwrap_or_else(|_| a3s_code_core::budget::BudgetDecision::Deny { |
| 5179 | resource: "budget_guard_timeout".to_string(), |
| 5180 | reason: format!("budget guard did not respond within {}ms", self.timeout_ms), |
| 5181 | }) |
| 5182 | }) |
| 5183 | } |
| 5184 | } |
| 5185 | |
| 5186 | #[async_trait::async_trait] |
no test coverage detected