[{"data":1,"prerenderedAt":55},["ShallowReactive",2],{"doc:en:plugins\u002Fcalls":3},{"title":4,"lede":5,"html":6,"headings":7},"The calls, in TS and in Rust","Each method of the host, with a short example in both languages: the network, the sockets, the timers, the files, the programs, the secrets, the settings and the journal. For the form of the messages, see the protocol.","\u003Ch2 id=\"how-to-read-this-page\">How to read this page\u003C\u002Fh2>\n\u003Cp>The examples come from two plugins of Escouade’s repository that do the same thing, \u003Ccode class=\"key\">sdk\u002Fdoc-examples\u002Fts\u003C\u002Fcode> and \u003Ccode>sdk\u002Fdoc-examples\u002Frust\u003C\u002Fcode>. They are compiled and pass the \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Ftesting\u002F\">bench\u003C\u002Fa> at each change.\u003C\u002Fp>\n\u003Cul>\n\u003Cli>\u003Cstrong>In TypeScript\u003C\u002Fstrong>, \u003Ccode class=\"key\">p\u003C\u002Fcode> is the SDK’s \u003Ccode class=\"key\">Plugin\u003C\u002Fcode>: each function gets the one from \u003Ccode class=\"key\">createPlugin()\u003C\u002Fcode>.\u003C\u002Fli>\n\u003Cli>\u003Cstrong>In Rust\u003C\u002Fstrong>, \u003Ccode class=\"key\">cx\u003C\u002Fcode> is the kit’s \u003Ccode class=\"key\">Cx\u003C\u002Fcode>, which each call gets. \u003Ccode class=\"key\">cx.request\u003C\u002Fcode> sends a method and waits for its answer, \u003Ccode class=\"key\">cx.notify\u003C\u002Fcode> sends a notification. What the host sends by itself, like \u003Ccode class=\"key\">net\u002Fchunk\u003C\u002Fcode>, comes to \u003Ccode class=\"key\">Plugin::call\u003C\u002Fcode>, like any call:\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">                \u002F\u002F What the host sends by itself comes here, like any call.\n                &quot;net\u002Fchunk&quot; =&gt; net::chunk(cx, &amp;params),\n                &quot;net\u002Fend&quot; =&gt; net::end(cx, &amp;params),\n                &quot;ws\u002Fmessage&quot; =&gt; socket::message(cx, &amp;params),\n                &quot;ws\u002Fclosed&quot; =&gt; socket::closed(cx, &amp;params),\n                &quot;proc\u002Fout&quot; =&gt; procs::out(cx, &amp;params),\n                &quot;proc\u002Fexit&quot; =&gt; procs::exit(cx, &amp;params),\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A refused call answers an error: \u003Ccode class=\"key\">RpcError\u003C\u002Fcode> in TypeScript, \u003Ccode class=\"key\">Refusal\u003C\u002Fcode> in Rust, with the same code (\u003Ccode class=\"key\">-32003\u003C\u002Fcode> for what is refused, \u003Ccode class=\"key\">-32004\u003C\u002Fcode> for what fails on the way, \u003Ccode class=\"key\">-32005\u003C\u002Fcode> for a missing file, \u003Ccode class=\"key\">-32602\u003C\u002Fcode> for parameters that do not hold). What each capability allows is in \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fcapabilities\u002F\">the host’s capabilities\u003C\u002Fa>.\u003C\u002Fp>\n\u003Ch2 id=\"the-events\">The events\u003C\u002Fh2>\n\u003Cp>The plugin gets \u003Ccode class=\"key\">events\u002Ffact\u003C\u002Fcode> for each fact of \u003Ccode class=\"key\">permissions.events\u003C\u002Fcode>, and \u003Ccode class=\"key\">permissions\u002Fdecide\u003C\u002Fcode> if it has \u003Ccode class=\"key\">permissions.decide\u003C\u002Fcode>. The list of facts is in \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fevents\u002F\">the events\u003C\u002Fa>, the rules of a decision in \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fpermission-rules\u002F\">the permission rules\u003C\u002Fa>. The plugin of both templates of \u003Ccode>npm create escouade-plugin\u003C\u002Fcode> does both:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Fsrc\u002Fplugin.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F A fact of permissions.events: a ticket changed columns (its data says from and to).\nplugin.onEvent(&#39;ticket.column&#39;, (fact, p) =&gt; {\n  \u002F\u002F A ticket&#39;s fact says its ticket; the fact&#39;s type, one for every fact, leaves it optional.\n  if (fact.data.to === &#39;review&#39;) p.log.info(`${fact.ticket?.key ?? &#39;&#39;} is ready to test (${fact.project?.name ?? &#39;&#39;})`);\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F A fact of `permissions.events`: a ticket changed columns (its data says from and to).\nfn fact(cx: &amp;Cx, fact: &amp;Value) {\n    if fact[&quot;name&quot;] == &quot;ticket.column&quot; &amp;&amp; fact[&quot;data&quot;][&quot;to&quot;] == &quot;review&quot; {\n        let key = fact[&quot;ticket&quot;][&quot;key&quot;].as_str().unwrap_or_default();\n        let project = fact[&quot;project&quot;][&quot;name&quot;].as_str().unwrap_or_default();\n        cx.log(&quot;info&quot;, &amp;format!(&quot;{key} is ready to test ({project})&quot;));\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>A decision answers within 500 ms:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Fprogram\u002Fsrc\u002Fplugin.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F A permission an agent asks for, before the user is asked (permissions.decide). Answer within 500 ms.\nplugin.decidePermission(({ request }) =&gt; {\n  const command = typeof request.input?.command === &#39;string&#39; ? request.input.command : &#39;&#39;;\n  if (request.kind === &#39;command&#39; &amp;&amp; \u002F\\bgit\\s+push\\b.*(--force\\b|\\s-f\\b)\u002F.test(command)) {\n    return { decision: &#39;deny&#39;, message: &#39;No force push in this team: push a new commit instead.&#39; };\n  }\n  \u002F\u002F Leaves it to the user, as without the plugin.\n  return { decision: &#39;ask&#39; };\n});\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fcreate-escouade-plugin\u002Ftemplates\u002Frust\u002Fsrc\u002Flib.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F A permission an agent asks for, before the user is asked (`permissions.decide`). Answer within 500 ms.\nfn decide(params: &amp;Value) -&gt; Value {\n    let request = &amp;params[&quot;request&quot;];\n    let command = request[&quot;input&quot;][&quot;command&quot;].as_str().unwrap_or_default();\n    if request[&quot;kind&quot;] == &quot;command&quot; &amp;&amp; is_force_push(command) {\n        return json!({\n            &quot;decision&quot;: &quot;deny&quot;,\n            &quot;message&quot;: &quot;No force push in this team: push a new commit instead.&quot;,\n        });\n    }\n    \u002F\u002F Leaves it to the user, as without the plugin.\n    json!({ &quot;decision&quot;: &quot;ask&quot; })\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-network\">The network\u003C\u002Fh2>\n\u003Ch3 id=\"net-fetch\">\u003Ccode class=\"key\">net\u002Ffetch\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>\u003Ccode>{ url, method?, headers?, body? | bodyBase64? }\u003C\u002Fcode> → \u003Ccode>{ status, headers, body | bodyBase64 }\u003C\u002Fcode>. A UTF-8 response comes in \u003Ccode class=\"key\">body\u003C\u002Fcode>, any other, whole, in \u003Ccode class=\"key\">bodyBase64\u003C\u002Fcode>. 16 calls at once per plugin: the next ones wait for their turn.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fnet.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F The standard fetch, which goes through Escouade in WASM.\nexport async function issue(p: Plugin, id: string) {\n  const token = await p.secret(&#39;token&#39;);\n  const res = await fetch(`https:\u002F\u002Fapi.example.dev\u002Fissues\u002F${id}`, {\n    headers: { authorization: `Bearer ${token ?? &#39;&#39;}` },\n  });\n  return { status: res.status, body: await res.text() };\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fnet.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F reqwest on your machine, `net\u002Ffetch` in the module: same source.\npub async fn issue(cx: &amp;Cx, id: &amp;str) -&gt; Result&lt;Value, Refusal&gt; {\n    let refused = |e: reqwest::Error| Refusal {\n        code: -32004,\n        message: e.to_string(),\n    };\n    let token = cx.secret(&quot;token&quot;).await?.unwrap_or_default();\n    let response = reqwest::Client::new()\n        .get(format!(&quot;https:\u002F\u002Fapi.example.dev\u002Fissues\u002F{id}&quot;))\n        .bearer_auth(token)\n        .send()\n        .await\n        .map_err(refused)?;\n    let status = response.status().as_u16();\n    let body = response.text().await.map_err(refused)?;\n    Ok(json!({ &quot;status&quot;: status, &quot;body&quot;: body }))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"net-stream\">\u003Ccode class=\"key\">net\u002Fstream\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>A response read as it comes. \u003Ccode>net\u002Fstream { url, method?, headers?, body?, sse? }\u003C\u002Fcode> → \u003Ccode>{ stream, status, headers }\u003C\u002Fcode>, then the notifications \u003Ccode>net\u002Fchunk { stream, data | dataBase64 }\u003C\u002Fcode> and \u003Ccode>net\u002Fend { stream, error? }\u003C\u002Fcode>. The plugin gives back what it read by \u003Ccode>net\u002Fcredit { stream, bytes }\u003C\u002Fcode>, or lets the stream go by \u003Ccode>net\u002Fcancel { stream }\u003C\u002Fcode>. Cutting SSE into events stays with the plugin.\u003C\u002Fp>\n\u003Cp>The SDK’s \u003Ccode class=\"key\">openStream\u003C\u002Fcode> does all that:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fnet.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Each piece taken gives its bytes back to Escouade, which reads no further than 256 KiB ahead of the plugin.\nexport async function follow(p: Plugin, url: string) {\n  const stream = await openStream(p, url, { sse: true });\n  for await (const piece of stream) p.log.debug(`${piece.data?.length ?? 0} characters`);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>In Rust, you write it by hand. Without credit given back, the host stops reading at 256 KiB:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fnet.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Opens a stream: its pieces come as `net\u002Fchunk`, its end as `net\u002Fend`.\npub async fn follow(cx: &amp;Cx, url: &amp;str) -&gt; Result&lt;Value, Refusal&gt; {\n    cx.request(&quot;net\u002Fstream&quot;, json!({ &quot;url&quot;: url, &quot;sse&quot;: true }))\n        .await\n}\n\n\u002F\u002F\u002F A piece, read: its bytes go back to Escouade, which reads no further than 256 KiB ahead of the plugin.\npub fn chunk(cx: &amp;Cx, piece: &amp;Value) {\n    let text = piece[&quot;data&quot;].as_str().map(str::len);\n    let bytes = piece[&quot;dataBase64&quot;].as_str().map(|b64| b64.len() \u002F 4 * 3);\n    let bytes = text.or(bytes).unwrap_or_default();\n    cx.log(&quot;debug&quot;, &amp;format!(&quot;{bytes} bytes&quot;));\n    cx.notify(\n        &quot;net\u002Fcredit&quot;,\n        json!({ &quot;stream&quot;: piece[&quot;stream&quot;], &quot;bytes&quot;: bytes }),\n    );\n}\n\npub fn end(cx: &amp;Cx, end: &amp;Value) {\n    if let Some(error) = end[&quot;error&quot;].as_str() {\n        cx.log(&quot;warn&quot;, error);\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-sockets-ws\">The sockets: \u003Ccode class=\"key\">ws\u002F*\u003C\u002Fcode>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>ws\u002Fopen { url, headers? }\u003C\u002Fcode> → \u003Ccode>{ socket }\u003C\u002Fcode>, \u003Ccode>ws\u002Fsend { socket, text | dataBase64 }\u003C\u002Fcode>, \u003Ccode>ws\u002Fclose { socket }\u003C\u002Fcode>, then the notifications \u003Ccode>ws\u002Fmessage { socket, text | dataBase64 }\u003C\u002Fcode> and \u003Ccode>ws\u002Fclosed { socket, code, reason }\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fsocket.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Its messages as they come, until it closes.\nexport async function live(p: Plugin) {\n  const token = await p.secret(&#39;token&#39;);\n  const socket = await openSocket(p, &#39;wss:\u002F\u002Fapi.example.dev\u002Flive&#39;, { authorization: `Bearer ${token ?? &#39;&#39;}` });\n  await socket.send({ text: &#39;hello&#39; });\n  for await (const message of socket) {\n    p.log.info(`the socket says ${message.text ?? &#39;&#39;}`);\n    await socket.close();\n  }\n  return (await socket.closed).code;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fsocket.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Opens a socket and says hello. What comes back arrives as `ws\u002Fmessage`, its end as `ws\u002Fclosed`.\npub async fn open(cx: &amp;Cx) -&gt; Result&lt;Value, Refusal&gt; {\n    let token = cx.secret(&quot;token&quot;).await?.unwrap_or_default();\n    let opened = cx\n        .request(\n            &quot;ws\u002Fopen&quot;,\n            json!({\n                &quot;url&quot;: &quot;wss:\u002F\u002Fapi.example.dev\u002Flive&quot;,\n                &quot;headers&quot;: { &quot;authorization&quot;: format!(&quot;Bearer {token}&quot;) },\n            }),\n        )\n        .await?;\n    let socket = &amp;opened[&quot;socket&quot;];\n    cx.request(&quot;ws\u002Fsend&quot;, json!({ &quot;socket&quot;: socket, &quot;text&quot;: &quot;hello&quot; }))\n        .await?;\n    Ok(socket.clone())\n}\n\npub fn message(cx: &amp;Cx, message: &amp;Value) {\n    let text = message[&quot;text&quot;].as_str().unwrap_or_default();\n    cx.log(&quot;info&quot;, &amp;format!(&quot;the socket says {text}&quot;));\n}\n\npub fn closed(cx: &amp;Cx, closed: &amp;Value) {\n    cx.log(&quot;info&quot;, &amp;format!(&quot;closed: {}&quot;, closed[&quot;code&quot;]));\n}\n\npub async fn close(cx: &amp;Cx, socket: &amp;Value) -&gt; Result&lt;Value, Refusal&gt; {\n    cx.request(&quot;ws\u002Fclose&quot;, json!({ &quot;socket&quot;: socket })).await\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-timers-timer\">The timers: \u003Ccode class=\"key\">timer\u002F*\u003C\u002Fcode>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>timer\u002Fset { ms }\u003C\u002Fcode> → \u003Ccode>{ timer }\u003C\u002Fcode>, \u003Ccode>timer\u002Fcancel { timer }\u003C\u002Fcode>, then the notification \u003Ccode>timer\u002Ffired { timer }\u003C\u002Fcode>. You do not call them yourself: \u003Ccode class=\"key\">setTimeout\u003C\u002Fcode> and \u003Ccode class=\"key\">clearTimeout\u003C\u002Fcode> exist in JS, \u003Ccode class=\"key\">sleep\u003C\u002Fcode> and \u003Ccode class=\"key\">timeout\u003C\u002Fcode> in the kit. Waiting gives the hand back to Escouade, and the other calls go on.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Ftimers.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Other calls go on while this one waits.\nexport async function later(p: Plugin) {\n  await new Promise((resolve) =&gt; setTimeout(resolve, 1000));\n  p.log.info(&#39;one second later&#39;);\n  return &#39;rested&#39;;\n}\n\n\u002F\u002F A timeout on anything that waits.\nexport async function patient(p: Plugin) {\n  let timer: ReturnType&lt;typeof setTimeout&gt; | undefined;\n  const late = new Promise&lt;never&gt;((_, reject) =&gt; {\n    timer = setTimeout(() =&gt; reject(new Error(&#39;timed out&#39;)), 2000);\n  });\n  try {\n    return await Promise.race([p.secret(&#39;token&#39;), late]);\n  } finally {\n    clearTimeout(timer);\n  }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Ftimers.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Other calls go on while this one waits.\npub async fn later(cx: &amp;Cx) -&gt; Value {\n    sleep(Duration::from_secs(1)).await;\n    cx.log(&quot;info&quot;, &quot;one second later&quot;);\n    json!(&quot;rested&quot;)\n}\n\n\u002F\u002F\u002F A timeout on anything that waits.\npub async fn patient(cx: &amp;Cx) -&gt; Result&lt;Option&lt;String&gt;, Refusal&gt; {\n    match timeout(Duration::from_secs(2), cx.secret(&quot;token&quot;)).await {\n        Ok(secret) =&gt; secret,\n        Err(elapsed) =&gt; Err(Refusal {\n            code: -32004,\n            message: elapsed.to_string(),\n        }),\n    }\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-files\">The files\u003C\u002Fh2>\n\u003Ch3 id=\"fs\">\u003Ccode class=\"key\">fs\u002F*\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>A root of \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode> and a relative path, or a path of the host under a root of an agent, with \u003Ccode class=\"key\">agent\u003C\u002Fcode>. \u003Ccode class=\"key\">.\u003C\u002Fcode> names the root.\u003C\u002Fp>\n\u003Cdiv class=\"table\">\u003Ctable>\n\u003Cthead>\n\u003Ctr>\n\u003Cth>Method\u003C\u002Fth>\n\u003Cth>More parameters\u003C\u002Fth>\n\u003Cth>Answer\u003C\u002Fth>\n\u003C\u002Ftr>\n\u003C\u002Fthead>\n\u003Ctbody>\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">fs\u002Fread\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ text }\u003C\u002Fcode>, or \u003Ccode>{ dataBase64 }\u003C\u002Fcode> for what is not UTF-8\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">fs\u002Fwrite\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode class=\"key\">text\u003C\u002Fcode> or \u003Ccode class=\"key\">dataBase64\u003C\u002Fcode>, \u003Ccode class=\"key\">append\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ bytes, path }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">fs\u002Fstat\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ kind, bytes, modified }\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">fs\u002Flist\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ entries, truncated }\u003C\u002Fcode>, each entry with \u003Ccode class=\"key\">name\u003C\u002Fcode>, \u003Ccode class=\"key\">kind\u003C\u002Fcode> and \u003Ccode class=\"key\">bytes\u003C\u002Fcode>\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003Ctr>\n\u003Ctd>\u003Ccode class=\"key\">fs\u002Fcanon\u003C\u002Fcode>\u003C\u002Ftd>\n\u003Ctd>\u003C\u002Ftd>\n\u003Ctd>\u003Ccode>{ path }\u003C\u002Fcode>, the path of the host\u003C\u002Ftd>\n\u003C\u002Ftr>\n\u003C\u002Ftbody>\u003C\u002Ftable>\u003C\u002Fdiv>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Ffiles.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Writes in its own temporary folder (`permissions.files`, root `temp`).\nexport function keep(p: Plugin, text: string) {\n  return p.fs.write({ root: &#39;temp&#39;, path: &#39;notes.txt&#39;, text, append: true });\n}\n\n\u002F\u002F Reads it back, what is there and what the host calls it.\nexport async function notes(p: Plugin) {\n  const file = { root: &#39;temp&#39;, path: &#39;notes.txt&#39; } as const;\n  return {\n    text: await p.fs.text(file),\n    stat: await p.fs.stat(file),\n    canon: await p.fs.canon(file),\n    list: await p.fs.list({ root: &#39;temp&#39;, path: &#39;.&#39; }),\n  };\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Ffiles.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Writes in its own temporary folder (`permissions.files`, root `temp`).\npub async fn keep(cx: &amp;Cx, text: &amp;str) -&gt; Result&lt;Value, Refusal&gt; {\n    let note = json!({ &quot;root&quot;: &quot;temp&quot;, &quot;path&quot;: &quot;notes.txt&quot;, &quot;text&quot;: text, &quot;append&quot;: true });\n    cx.request(&quot;fs\u002Fwrite&quot;, note).await\n}\n\n\u002F\u002F\u002F Reads it back, what is there and what the host calls it.\npub async fn notes(cx: &amp;Cx) -&gt; Result&lt;Value, Refusal&gt; {\n    let file = json!({ &quot;root&quot;: &quot;temp&quot;, &quot;path&quot;: &quot;notes.txt&quot; });\n    let read = cx.request(&quot;fs\u002Fread&quot;, file.clone()).await?;\n    let stat = cx.request(&quot;fs\u002Fstat&quot;, file.clone()).await?;\n    let canon = cx.request(&quot;fs\u002Fcanon&quot;, file).await?;\n    let list = cx\n        .request(&quot;fs\u002Flist&quot;, json!({ &quot;root&quot;: &quot;temp&quot;, &quot;path&quot;: &quot;.&quot; }))\n        .await?;\n    Ok(json!({ &quot;read&quot;: read, &quot;stat&quot;: stat, &quot;canon&quot;: canon, &quot;list&quot;: list }))\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"files-read\">\u003Ccode class=\"key\">files\u002Fread\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>A file of its own folder, without \u003Ccode class=\"key\">permissions.files\u003C\u002Fcode>: as UTF-8 text, 256 KiB at most. Never its manifest, a hidden name (\u003Ccode class=\"key\">.env\u003C\u002Fcode>) or a path that leaves the folder.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Ffiles.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F A file of the plugin&#39;s own folder.\nexport function words(p: Plugin) {\n  return p.file(&#39;data\u002Fwords.txt&#39;);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Ffiles.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F A file of the plugin&#39;s own folder.\npub async fn words(cx: &amp;Cx) -&gt; Result&lt;Value, Refusal&gt; {\n    cx.request(&quot;files\u002Fread&quot;, json!({ &quot;path&quot;: &quot;data\u002Fwords.txt&quot; }))\n        .await\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-programs-proc\">The programs: \u003Ccode class=\"key\">proc\u002F*\u003C\u002Fcode>\u003C\u002Fh2>\n\u003Cp>\u003Ccode>proc\u002Fspawn { command, args?, dir?, agent?, project?, env? }\u003C\u002Fcode> → \u003Ccode>{ proc }\u003C\u002Fcode>, \u003Ccode>proc\u002Fwrite { proc, data, close? }\u003C\u002Fcode>, \u003Ccode>proc\u002Fkill { proc }\u003C\u002Fcode>, then the notifications \u003Ccode>proc\u002Fout { proc, stream, data }\u003C\u002Fcode> and \u003Ccode>proc\u002Fexit { proc, code }\u003C\u002Fcode>. \u003Ccode class=\"key\">proc\u002Fout\u003C\u002Fcode> may come before the answer to \u003Ccode class=\"key\">proc\u002Fspawn\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Ccode class=\"key\">command\u003C\u002Fcode> and \u003Ccode class=\"key\">args\u003C\u002Fcode> must be a line of \u003Ccode class=\"key\">permissions.commands\u003C\u002Fcode>:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fescouade-plugin.json\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-json\">    &quot;commands&quot;: [&quot;git status --short&quot;, &quot;git log *&quot;]\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fprocs.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Runs a line of `permissions.commands` in a project: what it writes comes to the function, `exited` says how it ended.\nexport function status(p: Plugin, project: string) {\n  return p.runProgram({ command: &#39;git&#39;, args: [&#39;status&#39;, &#39;--short&#39;], dir: &#39;project&#39;, project }, (data, stream) =&gt;\n    p.log.info(`${stream}: ${data}`),\n  );\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fprocs.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Runs a line of `permissions.commands` in a project: what it writes comes as `proc\u002Fout`, its end as `proc\u002Fexit`.\npub async fn status(cx: &amp;Cx, project: &amp;str) -&gt; Result&lt;Value, Refusal&gt; {\n    let spawned = cx\n        .request(\n            &quot;proc\u002Fspawn&quot;,\n            json!({\n                &quot;command&quot;: &quot;git&quot;,\n                &quot;args&quot;: [&quot;status&quot;, &quot;--short&quot;],\n                &quot;dir&quot;: &quot;project&quot;,\n                &quot;project&quot;: project,\n            }),\n        )\n        .await?;\n    Ok(spawned[&quot;proc&quot;].clone())\n}\n\npub fn out(cx: &amp;Cx, out: &amp;Value) {\n    let data = out[&quot;data&quot;].as_str().unwrap_or_default();\n    cx.log(&quot;info&quot;, &amp;format!(&quot;{}: {data}&quot;, out[&quot;stream&quot;]));\n}\n\npub fn exit(cx: &amp;Cx, exit: &amp;Value) {\n    cx.log(&quot;info&quot;, &amp;format!(&quot;it ended: {}&quot;, exit[&quot;code&quot;]));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode class=\"key\">proc\u002Fwrite\u003C\u002Fcode> writes on its input, and \u003Ccode class=\"key\">proc\u002Fkill\u003C\u002Fcode> stops it with what it started:\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fprocs.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F What it reads on its input; `true` ends that input.\nexport async function input(p: Plugin) {\n  const program = await p.runProgram({ command: &#39;git&#39;, args: [&#39;log&#39;, &#39;--oneline&#39;] });\n  await program.write(&#39;yes\\n&#39;, true);\n  return program.exited;\n}\n\nexport async function stop(p: Plugin) {\n  const program = await p.runProgram({ command: &#39;git&#39;, args: [&#39;log&#39;, &#39;--oneline&#39;] });\n  await program.kill();\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fprocs.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F What it reads on its input; `close` ends that input.\npub async fn feed(cx: &amp;Cx, proc: &amp;Value) -&gt; Result&lt;Value, Refusal&gt; {\n    let input = json!({ &quot;proc&quot;: proc, &quot;data&quot;: &quot;yes\\n&quot;, &quot;close&quot;: true });\n    cx.request(&quot;proc\u002Fwrite&quot;, input).await\n}\n\npub async fn stop(cx: &amp;Cx, proc: &amp;Value) -&gt; Result&lt;Value, Refusal&gt; {\n    cx.request(&quot;proc\u002Fkill&quot;, json!({ &quot;proc&quot;: proc })).await\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>Running a program takes time: do not do it in a decision, which has 500 ms.\u003C\u002Fp>\n\u003Ch2 id=\"the-secrets-and-the-settings\">The secrets and the settings\u003C\u002Fh2>\n\u003Ch3 id=\"secrets-get\">\u003Ccode class=\"key\">secrets\u002Fget\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>\u003Ccode>{ key }\u003C\u002Fcode> → the secret, or \u003Ccode class=\"key\">null\u003C\u002Fcode> when none is kept. The key must be in \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode>, and declared in \u003Ccode class=\"key\">contributes.secrets\u003C\u002Fcode>. Another one answers \u003Ccode class=\"key\">-32001\u003C\u002Fcode>. For an account: \u003Ccode class=\"key\">account:&lt;name&gt;\u003C\u002Fcode>, with \u003Ccode class=\"key\">account:*\u003C\u002Fcode> in \u003Ccode class=\"key\">permissions.secrets\u003C\u002Fcode>.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fsettings.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F One of `permissions.secrets`; null while the user has not given it.\nexport function token(p: Plugin) {\n  return p.secret(&#39;token&#39;);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fsettings.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F One of `permissions.secrets`; none while the user has not given it.\npub async fn token(cx: &amp;Cx) -&gt; Result&lt;Option&lt;String&gt;, Refusal&gt; {\n    cx.secret(&quot;token&quot;).await\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3 id=\"settings-get\">\u003Ccode class=\"key\">settings\u002Fget\u003C\u002Fcode>\u003C\u002Fh3>\n\u003Cp>\u003Ccode class=\"key\">{}\u003C\u002Fcode> or \u003Ccode>{ project }\u003C\u002Fcode> → an object by key of \u003Ccode class=\"key\">contributes.settings\u003C\u002Fcode>, without those left at their default. With a project, what its \u003Ccode class=\"key\">.escouade.yml\u003C\u002Fcode> gives it under \u003Ccode class=\"key\">plugins.&lt;id&gt;\u003C\u002Fcode> comes first.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fsettings.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F Its settings: with a project, what its `.escouade.yml` gives over what the user chose.\nexport async function channel(p: Plugin, project: string) {\n  const settings = await p.settings(project);\n  return settings.channel;\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fsettings.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F Its settings: with a project, what its `.escouade.yml` gives over what the user chose.\npub async fn channel(cx: &amp;Cx, project: &amp;Value) -&gt; Result&lt;Value, Refusal&gt; {\n    let settings = cx\n        .request(&quot;settings\u002Fget&quot;, json!({ &quot;project&quot;: project }))\n        .await?;\n    Ok(settings[&quot;channel&quot;].clone())\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"the-journal\">The journal\u003C\u002Fh2>\n\u003Cp>\u003Ccode>log { level, message }\u003C\u002Fcode>, a notification: \u003Ccode class=\"key\">debug\u003C\u002Fcode>, \u003Ccode class=\"key\">info\u003C\u002Fcode>, \u003Ccode class=\"key\">warn\u003C\u002Fcode> or \u003Ccode class=\"key\">error\u003C\u002Fcode>. The journal is in English, like the app’s, and each secret given to the plugin is hidden there.\u003C\u002Fp>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Fts\u002Fsrc\u002Fsettings.ts\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-ts\">\u002F\u002F A line of its journal: `debug`, `info`, `warn` or `error`.\nexport function journal(fact: Fact, p: Plugin) {\n  p.log.info(`${fact.name} received`);\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>\u003Ccode>sdk\u002Fdoc-examples\u002Frust\u002Fsrc\u002Fsettings.rs\u003C\u002Fcode> (excerpt)\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-rust\">\u002F\u002F\u002F A line of its journal: `debug`, `info`, `warn` or `error`.\npub fn journal(cx: &amp;Cx, fact: &amp;Value) {\n    cx.log(&quot;info&quot;, &amp;format!(&quot;{} received&quot;, fact[&quot;name&quot;]));\n}\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>What the plugin writes on its standard error goes there too: see \u003Ca href=\"\u002Fen\u002Fdocs\u002Fplugins\u002Fapi\u002F#the-journal\">the journal\u003C\u002Fa>.\u003C\u002Fp>\n",[8,12,15,18,22,25,28,31,34,37,40,43,46,49,52],{"id":9,"text":10,"depth":11},"how-to-read-this-page","How to read this page",2,{"id":13,"text":14,"depth":11},"the-events","The events",{"id":16,"text":17,"depth":11},"the-network","The network",{"id":19,"text":20,"depth":21},"net-fetch","net\u002Ffetch",3,{"id":23,"text":24,"depth":21},"net-stream","net\u002Fstream",{"id":26,"text":27,"depth":11},"the-sockets-ws","The sockets: ws\u002F*",{"id":29,"text":30,"depth":11},"the-timers-timer","The timers: timer\u002F*",{"id":32,"text":33,"depth":11},"the-files","The files",{"id":35,"text":36,"depth":21},"fs","fs\u002F*",{"id":38,"text":39,"depth":21},"files-read","files\u002Fread",{"id":41,"text":42,"depth":11},"the-programs-proc","The programs: proc\u002F*",{"id":44,"text":45,"depth":11},"the-secrets-and-the-settings","The secrets and the settings",{"id":47,"text":48,"depth":21},"secrets-get","secrets\u002Fget",{"id":50,"text":51,"depth":21},"settings-get","settings\u002Fget",{"id":53,"text":54,"depth":11},"the-journal","The journal",1793889382253]