oxedyne/daimond/src/wasm/web.rs
20.2 KiB, 1 run
created by r2519314175:999, which is this file's identity for as long as the history lasts, whatever it is later renamed to
download · who wrote it · its history
| 1 | //! The Web panel edge — thin bindings to the JS driver `window.DaimondWeb`. |
| 2 | //! |
| 3 | //! `DaimondWeb` is the one interface the agent's web tools call, and it |
| 4 | //! hides which driver is attached (none, an iframe, or the Daimond Hands |
| 5 | //! extension), so the tools do not change when the extension appears. |
| 6 | //! Every method returns a `Promise`; each binding below awaits it and |
| 7 | //! hands back the resolved JSON, stringified. |
| 8 | //! |
| 9 | //! A rejection carries a plain-English `Error` the model is meant to read |
| 10 | //! and act on ("No page is open. Call web_open first."), so its `message` |
| 11 | //! is passed through **verbatim**: no prefix, no rewording. Mangling it |
| 12 | //! would destroy the only instruction the model gets about what to do |
| 13 | //! next. |
| 14 | |
| 15 | use crate::llm::json_escape; |
| 16 | use crate::tools::SearchAnswer; |
| 17 | use crate::tools::SearchHit; |
| 18 | use crate::tools::Verdict; |
| 19 | use crate::wasm::js_str; |
| 20 | |
| 21 | use oxedyne_fe2o3_core::prelude::*; |
| 22 | |
| 23 | use wasm_bindgen::prelude::wasm_bindgen; |
| 24 | use wasm_bindgen::{JsCast, JsValue}; |
| 25 | use wasm_bindgen_futures::JsFuture; |
| 26 | |
| 27 | |
| 28 | #[wasm_bindgen] |
| 29 | extern "C" { |
| 30 | |
| 31 | /// The driver object the Web panel installs at `window.DaimondWeb`. |
| 32 | #[wasm_bindgen(js_name = DaimondWeb)] |
| 33 | type Driver; |
| 34 | |
| 35 | /// Which driver is attached, what is open, and who is driving. |
| 36 | #[wasm_bindgen(method)] |
| 37 | fn status(this: &Driver) -> js_sys::Promise; |
| 38 | |
| 39 | /// Dock the panel and navigate it to `url`. |
| 40 | #[wasm_bindgen(method)] |
| 41 | fn open(this: &Driver, url: &str) -> js_sys::Promise; |
| 42 | |
| 43 | /// Read `url` through the gateway, without a driver. |
| 44 | #[wasm_bindgen(method)] |
| 45 | fn fetch(this: &Driver, url: &str) -> js_sys::Promise; |
| 46 | |
| 47 | /// The accessibility tree of the open page. |
| 48 | #[wasm_bindgen(method)] |
| 49 | fn snapshot(this: &Driver) -> js_sys::Promise; |
| 50 | |
| 51 | /// The rendered text of the open page. |
| 52 | #[wasm_bindgen(method)] |
| 53 | fn read(this: &Driver) -> js_sys::Promise; |
| 54 | |
| 55 | /// Click the node named by `node_ref`. |
| 56 | #[wasm_bindgen(method)] |
| 57 | fn click(this: &Driver, node_ref: u32) -> js_sys::Promise; |
| 58 | |
| 59 | /// Type into the node named by `node_ref` (`type` is a Rust keyword). |
| 60 | #[wasm_bindgen(method, js_name = "type")] |
| 61 | fn type_into(this: &Driver, node_ref: u32, text: &str, submit: bool) -> js_sys::Promise; |
| 62 | |
| 63 | /// Scroll the open page; `amount` may be `undefined` for the default. |
| 64 | #[wasm_bindgen(method)] |
| 65 | fn scroll(this: &Driver, dir: &str, amount: JsValue) -> js_sys::Promise; |
| 66 | |
| 67 | /// Undock the panel and drop the page. |
| 68 | #[wasm_bindgen(method)] |
| 69 | fn close(this: &Driver) -> js_sys::Promise; |
| 70 | } |
| 71 | |
| 72 | |
| 73 | #[wasm_bindgen] |
| 74 | extern "C" { |
| 75 | |
| 76 | /// The search half the app installs at `window.DaimondSearch`. |
| 77 | /// |
| 78 | /// A global of its own rather than a `DaimondWeb` method, because it holds something the |
| 79 | /// Web panel's driver knows nothing about: WHICH ENGINE the user chose, and the key that |
| 80 | /// pays for it. Keeping that on the driver would put the user's setting behind the thing |
| 81 | /// that owns pages, and searching needs no page at all. |
| 82 | #[wasm_bindgen(js_name = DaimondSearch)] |
| 83 | type Searcher; |
| 84 | |
| 85 | /// Run one query, with the engine and any key the user's settings supply. |
| 86 | /// |
| 87 | /// Renamed on the Rust side only because [`search`] below is the function callers use; the |
| 88 | /// JavaScript method is `search`, exactly as the contract names it. |
| 89 | #[wasm_bindgen(method, js_name = "search")] |
| 90 | fn run_query(this: &Searcher, query: &str, opts: &JsValue) -> js_sys::Promise; |
| 91 | } |
| 92 | |
| 93 | |
| 94 | /// Reach the driver object on `window`, or refuse in the model's language. |
| 95 | fn driver() -> Outcome<Driver> { |
| 96 | let win = res!(web_sys::window() |
| 97 | .ok_or_else(|| err!("The web tools need a browser window."; System, Missing))); |
| 98 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondWeb")) |
| 99 | .map_err(|e| err!("Reading window.DaimondWeb failed: {}.", js_str(&e); System, Missing))); |
| 100 | if obj.is_undefined() || obj.is_null() { |
| 101 | return Err(err!( |
| 102 | "The Web panel is not loaded in this page, so there is nothing to drive. \ |
| 103 | Tell the user, and carry on without the web tools."; |
| 104 | System, Missing)); |
| 105 | } |
| 106 | Ok(obj.unchecked_into::<Driver>()) |
| 107 | } |
| 108 | |
| 109 | /// The `message` of a rejected JS `Error`, verbatim, falling back to the |
| 110 | /// value's own rendering when it is not an `Error`. |
| 111 | fn refusal(e: &JsValue) -> String { |
| 112 | match js_sys::Reflect::get(e, &JsValue::from_str("message")) { |
| 113 | Ok(m) => m.as_string().unwrap_or_else(|| js_str(e)), |
| 114 | Err(_) => js_str(e), |
| 115 | } |
| 116 | } |
| 117 | |
| 118 | /// Render a resolved JS value as the JSON string the tool result carries. |
| 119 | fn stringify(v: &JsValue) -> Outcome<String> { |
| 120 | if v.is_undefined() || v.is_null() { |
| 121 | return Ok("{}".to_string()); |
| 122 | } |
| 123 | if let Some(s) = v.as_string() { |
| 124 | return Ok(s); // the driver resolved with JSON already |
| 125 | } |
| 126 | match js_sys::JSON::stringify(v) { |
| 127 | Ok(s) => Ok(String::from(s)), |
| 128 | Err(e) => Err(err!( |
| 129 | "The Web panel returned a result that cannot be read: {}.", refusal(&e); |
| 130 | Invalid, Data)), |
| 131 | } |
| 132 | } |
| 133 | |
| 134 | /// Await a driver promise, passing a refusal through untouched. |
| 135 | async fn settle(promise: js_sys::Promise) -> Outcome<String> { |
| 136 | match JsFuture::from(promise).await { |
| 137 | Ok(v) => stringify(&v), |
| 138 | Err(e) => Err(err!("{}", refusal(&e); Network, Invalid)), |
| 139 | } |
| 140 | } |
| 141 | |
| 142 | /// Which driver is attached, what is open, and who is driving. |
| 143 | pub async fn status() -> Outcome<String> { |
| 144 | let d = res!(driver()); |
| 145 | settle(d.status()).await |
| 146 | } |
| 147 | |
| 148 | /// The address of the page currently open, or empty when nothing says. |
| 149 | /// |
| 150 | /// Best effort: the driver reports its state as JSON, and the one field wanted here is the URL. |
| 151 | /// An action on a page has no address of its own, so this is what names the destination. |
| 152 | pub async fn current_url() -> String { |
| 153 | let st = match status().await { |
| 154 | Ok(s) => s, |
| 155 | Err(_) => return String::new(), |
| 156 | }; |
| 157 | let key = "\"url\":\""; |
| 158 | let from = match st.find(key) { |
| 159 | Some(i) => i + key.len(), |
| 160 | None => return String::new(), |
| 161 | }; |
| 162 | match st[from..].find('"') { |
| 163 | Some(end) => st[from..from + end].to_string(), |
| 164 | None => String::new(), |
| 165 | } |
| 166 | } |
| 167 | |
| 168 | /// Show `url` in the Web panel. |
| 169 | pub async fn open(url: &str) -> Outcome<String> { |
| 170 | let d = res!(driver()); |
| 171 | settle(d.open(url)).await |
| 172 | } |
| 173 | |
| 174 | /// Read `url` through the gateway, driver or no driver. |
| 175 | pub async fn fetch(url: &str) -> Outcome<String> { |
| 176 | let d = res!(driver()); |
| 177 | settle(d.fetch(url)).await |
| 178 | } |
| 179 | |
| 180 | // ── Searching ─────────────────────────────────────────────────────── |
| 181 | |
| 182 | /// Reach the search object on `window`, or refuse in the model's language. |
| 183 | /// |
| 184 | /// Separate from [`driver`] because the two can be absent independently: a build with a Web |
| 185 | /// panel and no search settings is a real state, and telling the model the Web panel is missing |
| 186 | /// would send it looking in the wrong place. |
| 187 | fn searcher() -> Outcome<Searcher> { |
| 188 | let win = res!(web_sys::window() |
| 189 | .ok_or_else(|| err!("Searching needs a browser window."; System, Missing))); |
| 190 | let obj = res!(js_sys::Reflect::get(&win, &JsValue::from_str("DaimondSearch")) |
| 191 | .map_err(|e| err!("Reading window.DaimondSearch failed: {}.", js_str(&e); |
| 192 | System, Missing))); |
| 193 | if obj.is_undefined() || obj.is_null() { |
| 194 | return Err(err!( |
| 195 | "Search is not set up in this page, so there is nothing to search with. \ |
| 196 | Tell the user, and carry on without it."; |
| 197 | System, Missing)); |
| 198 | } |
| 199 | Ok(obj.unchecked_into::<Searcher>()) |
| 200 | } |
| 201 | |
| 202 | /// Put one option on the object handed to the search driver. |
| 203 | /// |
| 204 | /// # Arguments |
| 205 | /// * `obj` - The options object being built. |
| 206 | /// * `key` - The option's name. |
| 207 | /// * `val` - Its value. |
| 208 | fn set(obj: &js_sys::Object, key: &str, val: JsValue) -> Outcome<()> { |
| 209 | match js_sys::Reflect::set(obj, &JsValue::from_str(key), &val) { |
| 210 | Ok(_) => Ok(()), |
| 211 | Err(e) => Err(err!("Building the search request failed at '{}': {}.", key, js_str(&e); |
| 212 | System, Invalid)), |
| 213 | } |
| 214 | } |
| 215 | |
| 216 | /// A string property of a JavaScript object, empty where it is absent or is not a string. |
| 217 | fn prop(v: &JsValue, key: &str) -> String { |
| 218 | match js_sys::Reflect::get(v, &JsValue::from_str(key)) { |
| 219 | Ok(x) => x.as_string().unwrap_or_default(), |
| 220 | Err(_) => String::new(), |
| 221 | } |
| 222 | } |
| 223 | |
| 224 | /// Read the driver's reply into the common shape. |
| 225 | /// |
| 226 | /// A result missing a title or a url is DROPPED rather than passed on empty, per the contract: a |
| 227 | /// row with no address is nothing the model can follow up, and a row with no title reads as a |
| 228 | /// blank line in the middle of the list. One bad row never fails the whole answer. |
| 229 | /// |
| 230 | /// # Arguments |
| 231 | /// * `v` - What the promise resolved with: the result object, or the JSON text of one. |
| 232 | /// * `asked` - The query as it was sent. |
| 233 | fn answer(v: &JsValue, asked: &str) -> Outcome<SearchAnswer> { |
| 234 | // A driver that resolves with JSON TEXT rather than an object is read anyway: the Web panel's |
| 235 | // driver already resolves both ways (see `stringify`), and a working search reported as a |
| 236 | // broken one because of which of the two it picked would be a poor trade for strictness. |
| 237 | let obj = match v.as_string() { |
| 238 | Some(s) => match js_sys::JSON::parse(&s) { |
| 239 | Ok(o) => o, |
| 240 | Err(_) => JsValue::NULL, |
| 241 | }, |
| 242 | None => v.clone(), |
| 243 | }; |
| 244 | let list = match js_sys::Reflect::get(&obj, &JsValue::from_str("results")) { |
| 245 | Ok(l) => l, |
| 246 | Err(_) => JsValue::UNDEFINED, |
| 247 | }; |
| 248 | // No list at all is a MALFORMED reply, and it is not the same thing as an empty one. Reporting |
| 249 | // it as "no results" would tell the model the web has nothing on the subject, which is a lie |
| 250 | // about a broken reply and one it cannot see through. |
| 251 | if !js_sys::Array::is_array(&list) { |
| 252 | return Err(err!( |
| 253 | "The search service answered without a result list, so nothing can be read from it."; |
| 254 | Invalid, Data)); |
| 255 | } |
| 256 | let mut out = SearchAnswer { |
| 257 | engine: prop(&obj, "engine"), |
| 258 | // OURS, not the reply's echo of it: see the doc comment on `search`. |
| 259 | query: asked.to_string(), |
| 260 | results: Vec::new(), |
| 261 | }; |
| 262 | for item in js_sys::Array::from(&list).iter() { |
| 263 | let title = prop(&item, "title"); |
| 264 | let url = prop(&item, "url"); |
| 265 | if title.trim().is_empty() || url.trim().is_empty() { |
| 266 | continue; |
| 267 | } |
| 268 | out.results.push(SearchHit { |
| 269 | title, |
| 270 | url, |
| 271 | snippet: prop(&item, "snippet"), |
| 272 | age: prop(&item, "age"), |
| 273 | }); |
| 274 | } |
| 275 | Ok(out) |
| 276 | } |
| 277 | |
| 278 | /// Search the web through the engine the USER chose. |
| 279 | /// |
| 280 | /// The engine, and any key of the user's own that pays for it, are supplied by the JavaScript |
| 281 | /// half and are deliberately not arguments: the choice is a setting, not the model's. |
| 282 | /// |
| 283 | /// **The query in the answer is the one that was ASKED, not the one the reply echoed.** The |
| 284 | /// origin line of the untrusted envelope is a record of what the model went looking for, and |
| 285 | /// nothing arriving from outside is entitled to rewrite that record. |
| 286 | /// |
| 287 | /// # Arguments |
| 288 | /// * `query` - What to search for. |
| 289 | /// * `kind` - `web`, `news` or `academic`, already checked by the tool. |
| 290 | /// * `limit` - How many results to ask for, left to the engine when unset. |
| 291 | pub async fn search(query: &str, kind: &str, limit: Option<u32>) -> Outcome<SearchAnswer> { |
| 292 | let s = res!(searcher()); |
| 293 | let opts = js_sys::Object::new(); |
| 294 | res!(set(&opts, "kind", JsValue::from_str(kind))); |
| 295 | if let Some(n) = limit { |
| 296 | res!(set(&opts, "limit", JsValue::from_f64(n as f64))); |
| 297 | } |
| 298 | let v = match JsFuture::from(s.run_query(query, opts.as_ref())).await { |
| 299 | Ok(v) => v, |
| 300 | Err(e) => return Err(err!("{}", refusal(&e); Network, Invalid)), |
| 301 | }; |
| 302 | answer(&v, query) |
| 303 | } |
| 304 | |
| 305 | /// The accessibility tree of the open page, whose refs the actions take. |
| 306 | pub async fn snapshot() -> Outcome<String> { |
| 307 | let d = res!(driver()); |
| 308 | settle(d.snapshot()).await |
| 309 | } |
| 310 | |
| 311 | /// The rendered text of the open page -- the way to READ its content. |
| 312 | pub async fn read() -> Outcome<String> { |
| 313 | let d = res!(driver()); |
| 314 | settle(d.read()).await |
| 315 | } |
| 316 | |
| 317 | /// Click the node named by `node_ref` from the latest snapshot. |
| 318 | pub async fn click(node_ref: u32) -> Outcome<String> { |
| 319 | let d = res!(driver()); |
| 320 | settle(d.click(node_ref)).await |
| 321 | } |
| 322 | |
| 323 | /// Type `text` into the node named by `node_ref`, optionally submitting. |
| 324 | pub async fn type_into(node_ref: u32, text: &str, submit: bool) -> Outcome<String> { |
| 325 | let d = res!(driver()); |
| 326 | settle(d.type_into(node_ref, text, submit)).await |
| 327 | } |
| 328 | |
| 329 | /// Scroll the open page, leaving `amount` to the driver when unset. |
| 330 | pub async fn scroll(dir: &str, amount: Option<u32>) -> Outcome<String> { |
| 331 | let d = res!(driver()); |
| 332 | let amt = match amount { |
| 333 | Some(n) => JsValue::from_f64(n as f64), |
| 334 | None => JsValue::UNDEFINED, |
| 335 | }; |
| 336 | settle(d.scroll(dir, amt)).await |
| 337 | } |
| 338 | |
| 339 | /// Close the Web panel and drop the page. |
| 340 | pub async fn close() -> Outcome<String> { |
| 341 | let d = res!(driver()); |
| 342 | settle(d.close()).await |
| 343 | } |
| 344 | |
| 345 | |
| 346 | // ── The egress gate's edge ────────────────────────────────────────── |
| 347 | |
| 348 | /// The global the JavaScript half installs to answer whether this turn may reach a destination. |
| 349 | /// |
| 350 | /// It is not a `DaimondWeb` method: the question is about the turn, not about the panel, and the |
| 351 | /// half that answers it owns the user's standing decisions rather than the browser driver. |
| 352 | const EGRESS_GLOBAL: &str = "__daimondEgressAllowed"; |
| 353 | |
| 354 | /// Ask the JavaScript half whether this conversation may reach `url`, and how far its answer |
| 355 | /// reaches. |
| 356 | /// |
| 357 | /// The payload is a JSON string, per the contract: |
| 358 | /// `{"tool":"web_fetch","url":"…","detail":"","alone":false,"granted":false}`. |
| 359 | /// |
| 360 | /// `granted` is the whole of what this adds, and it flows one way only. The SCOPE of the answer |
| 361 | /// is decided in Rust ([`crate::tools::web_step`]); the WORDING of the question lives on the page, |
| 362 | /// with the dialogs and the translations. So the page is told whether the conversation has |
| 363 | /// already said yes, and answers either the wide question or the one about a single overlong |
| 364 | /// address -- and says which, in the word it answers with (see [`crate::tools::EgressWord`]). |
| 365 | /// |
| 366 | /// `None` from the door is not a verdict, so it is read as a refusal spent on this one call. |
| 367 | /// |
| 368 | /// # Arguments |
| 369 | /// * `tool` - The wire name of the tool asking. |
| 370 | /// * `url` - The destination it wants. |
| 371 | /// * `granted` - Whether the conversation has already granted the web. |
| 372 | pub async fn egress_reach(tool: &str, url: &str, granted: bool) -> crate::tools::EgressWord { |
| 373 | let answer = egress_ask_raw(tool, url, "", false, granted).await; |
| 374 | crate::tools::egress_word(answer.as_deref()) |
| 375 | } |
| 376 | |
| 377 | /// As [`egress_reach`], with a `detail` the user should see -- the text about to be typed into a |
| 378 | /// page, say, which is the thing being sent and therefore the thing to look at. |
| 379 | /// |
| 380 | /// # Arguments |
| 381 | /// * `tool` - The wire name of the tool asking. |
| 382 | /// * `url` - The destination it wants. |
| 383 | /// * `detail` - What is being sent, when the tool sends something other than the address. |
| 384 | pub async fn egress_allowed_detail(tool: &str, url: &str, detail: &str) -> Option<Verdict> { |
| 385 | egress_allowed_act(tool, url, detail, false).await |
| 386 | } |
| 387 | |
| 388 | /// As [`egress_allowed_detail`], saying whether the actor is acting ALONE. |
| 389 | /// |
| 390 | /// `alone` does exactly one thing at the other end, and it is not about what the dialog says: it |
| 391 | /// stops the answer being remembered. The page keeps a per-host note of "acting here is |
| 392 | /// approved", which is right for a person clicking through a site and wrong for a dispatched |
| 393 | /// worker, where one yes about a host would license every later act on it. |
| 394 | /// |
| 395 | /// # Arguments |
| 396 | /// * `tool` - The wire name of the tool asking. |
| 397 | /// * `url` - The destination it wants. |
| 398 | /// * `detail` - What is being sent, when the tool sends something other than the address. |
| 399 | /// * `alone` - Whether the agent asking has nobody watching it. |
| 400 | pub async fn egress_allowed_act(tool: &str, url: &str, detail: &str, alone: bool) |
| 401 | -> Option<Verdict> |
| 402 | { |
| 403 | egress_ask_js(tool, url, detail, alone, crate::tools::EGRESS_ALLOW_WORD).await |
| 404 | } |
| 405 | |
| 406 | /// Ask whether a command on a turn that has read outside content may reach the network. |
| 407 | /// |
| 408 | /// The same door, the same payload and the same dialog machinery as every other question here -- |
| 409 | /// what differs is the word a yes comes back as (see [`crate::tools::RUN_NET_ALLOW_WORD`]), |
| 410 | /// because a command line resolves as a same-origin URL and the page's gate waves those through. |
| 411 | /// |
| 412 | /// # Arguments |
| 413 | /// * `cmd` - The command line, which is what the user is being asked about. |
| 414 | /// * `cwd` - The folder it would run in. |
| 415 | pub async fn egress_allowed_net(cmd: &str, cwd: &str) -> Option<Verdict> { |
| 416 | egress_ask_js(crate::tools::RUN_NET_TOOL, cmd, cwd, false, |
| 417 | crate::tools::RUN_NET_ALLOW_WORD).await |
| 418 | } |
| 419 | |
| 420 | /// Ask whether one publication on the Social panel may go out. |
| 421 | /// |
| 422 | /// The same door, the same payload and the same dialog machinery as every other question here. |
| 423 | /// Three things differ, and each is a decision rather than a shape: |
| 424 | /// |
| 425 | /// * the word a yes comes back as (see [`crate::tools::PUBLISH_ALLOW_WORD`]), because the forge |
| 426 | /// is reached through our own gateway and the page's gate waves same-origin addresses through; |
| 427 | /// * `url` carries the exact characters that would be published rather than an address, because |
| 428 | /// the address is not the question -- what the user is approving is the text; |
| 429 | /// * `alone` is always false, because a dispatched worker never gets here: it is refused by |
| 430 | /// [`crate::tools::social_send_step`] before anything is composed. Publishing in somebody's |
| 431 | /// name is not a question to be parked on a tile for whoever passes. |
| 432 | /// |
| 433 | /// # Arguments |
| 434 | /// * `shown` - Exactly what would be published, as the panel composed it. |
| 435 | pub async fn egress_allowed_publish(shown: &str) -> Option<Verdict> { |
| 436 | egress_ask_js(crate::tools::SOCIAL_SEND_TOOL, shown, "", false, |
| 437 | crate::tools::PUBLISH_ALLOW_WORD).await |
| 438 | } |
| 439 | |
| 440 | /// Put one question to the page's gate and read its answer. |
| 441 | /// |
| 442 | /// # Arguments |
| 443 | /// * `tool` - The wire name of the tool asking. |
| 444 | /// * `url` - The destination it wants, or the command it wants to run. |
| 445 | /// * `detail` - What is being sent, when the tool sends something other than the address. |
| 446 | /// * `alone` - Whether the agent asking has nobody watching it. |
| 447 | /// * `word` - The one string that means yes to this question. |
| 448 | async fn egress_ask_js(tool: &str, url: &str, detail: &str, alone: bool, word: &str) |
| 449 | -> Option<Verdict> |
| 450 | { |
| 451 | let answer = egress_ask_raw(tool, url, detail, alone, false).await; |
| 452 | answer.map(|s| crate::tools::verdict_of(Some(s.as_str()), word)) |
| 453 | } |
| 454 | |
| 455 | /// Put one question to the page's gate and read its answer back as the string the page resolved |
| 456 | /// with, or `None` where the question could not be put at all. |
| 457 | /// |
| 458 | /// The one place the payload is composed, so the two callers above cannot spell it differently. |
| 459 | /// |
| 460 | /// # Arguments |
| 461 | /// * `tool` - The wire name of the tool asking. |
| 462 | /// * `url` - The destination it wants, or the command it wants to run. |
| 463 | /// * `detail` - What is being sent, when the tool sends something other than the address. |
| 464 | /// * `alone` - Whether the agent asking has nobody watching it. |
| 465 | /// * `granted` - Whether the conversation has already granted the web (reaching questions only). |
| 466 | async fn egress_ask_raw(tool: &str, url: &str, detail: &str, alone: bool, granted: bool) |
| 467 | -> Option<String> |
| 468 | { |
| 469 | let win = match web_sys::window() { |
| 470 | Some(w) => w, |
| 471 | None => return None, |
| 472 | }; |
| 473 | let f = match js_sys::Reflect::get(&win, &JsValue::from_str(EGRESS_GLOBAL)) { |
| 474 | Ok(v) => v, |
| 475 | Err(_) => return None, |
| 476 | }; |
| 477 | if !f.is_function() { |
| 478 | return None; |
| 479 | } |
| 480 | let f = f.unchecked_into::<js_sys::Function>(); |
| 481 | let payload = fmt!( |
| 482 | "{{\"tool\":\"{}\",\"url\":\"{}\",\"detail\":\"{}\",\"alone\":{},\"granted\":{}}}", |
| 483 | json_escape(tool), json_escape(url), json_escape(detail), alone, granted); |
| 484 | let ret = match f.call1(&JsValue::NULL, &JsValue::from_str(&payload)) { |
| 485 | Ok(v) => v, |
| 486 | Err(_) => return None, |
| 487 | }; |
| 488 | // The contract says a Promise; a value returned outright is read anyway rather than refused |
| 489 | // on a technicality, since it is still an answer. |
| 490 | let promise = match ret.dyn_into::<js_sys::Promise>() { |
| 491 | Ok(p) => p, |
| 492 | Err(v) => return v.as_string(), |
| 493 | }; |
| 494 | match JsFuture::from(promise).await { |
| 495 | Ok(v) => v.as_string(), |
| 496 | Err(_) => None, |
| 497 | } |
| 498 | } |