$npx -y skills add pssah4/vault-operator --skill sandbox-environmentSandbox API reference, proven patterns, and library recommendations for evaluate_expression
| 1 | # Sandbox Environment Reference |
| 2 | |
| 3 | ## Available APIs |
| 4 | |
| 5 | ### ctx.vault |
| 6 | - `ctx.vault.read(path: string): Promise<string>` -- Read text file |
| 7 | - `ctx.vault.readBinary(path: string): Promise<ArrayBuffer>` -- Read binary file |
| 8 | - `ctx.vault.write(path: string, content: string): Promise<void>` -- Write text (max 10MB, no writes to .obsidian/) |
| 9 | - `ctx.vault.writeBinary(path: string, content: ArrayBuffer): Promise<void>` -- Write binary (max 10MB) |
| 10 | - `ctx.vault.list(path: string): Promise<string[]>` -- List folder contents |
| 11 | |
| 12 | ### ctx.requestUrl |
| 13 | - `ctx.requestUrl(url: string, options?: {method?: string, body?: string}): Promise<{status: number, text: string}>` |
| 14 | - HTTPS only. Allowed domains: esm.sh, cdn.jsdelivr.net, unpkg.com, registry.npmjs.org |
| 15 | - Rate limit: 5 requests/minute |
| 16 | |
| 17 | ### Standard Globals |
| 18 | Promise, JSON, Math, Date, Object (full), Array, Map, Set, RegExp, Number, String, Boolean, Symbol, setTimeout, clearTimeout, TextEncoder, TextDecoder, parseInt, parseFloat, isNaN, isFinite, encodeURIComponent, decodeURIComponent, Error, TypeError, RangeError |
| 19 | |
| 20 | ### TypedArrays (for binary data) |
| 21 | Uint8Array, Int8Array, Uint16Array, Int16Array, Uint32Array, Int32Array, Float32Array, Float64Array, ArrayBuffer, DataView |
| 22 | |
| 23 | ## NOT Available (will cause errors) |
| 24 | Buffer (Node.js), require(), dynamic import(), process, fs, path, __dirname, __filename, child_process, WebAssembly, eval(), new Function() |
| 25 | |
| 26 | `require`, `import()`, `process`, `globalThis`, `eval`, `new Function` and `WebAssembly` are not merely absent: the AstValidator rejects the script BEFORE it compiles if the source so much as contains them, and its comment stripper deliberately keeps string literals. A script that searches for these tokens must therefore assemble them at runtime (`'pro' + 'cess'`), or it rejects itself. `new RegExp` is allowed. |
| 27 | |
| 28 | ## The one rule that has no error message |
| 29 | |
| 30 | **A skill script is a single, self-contained file. No `import`, no `require`.** |
| 31 | |
| 32 | A static `import x from './y.js'` passes every check: the AstValidator only blocks `require(` and `import(`, and the repo has no other gate. But `esbuild.transform` (no bundling) rewrites it to `require()`, and there is no `require` here. The script dies on the user's first call, with an error that names none of this. Put everything in one file. |
| 33 | |
| 34 | ## What the environment actually is |
| 35 | |
| 36 | A Chromium iframe (`sandbox="allow-scripts"`, no `allow-same-origin`, CSP `default-src 'none'`), NOT a Node vm. The `vm` worker was removed after a confirmed RCE; `sandbox-worker.js` in the repo root is a dead build artefact. Browser globals therefore exist in the usual browser sense, but the CSP gives them nothing to talk to: there is no network for `fetch`, and `document` belongs to a blank, origin-less frame. Do not build on them. `ctx.vault` and `ctx.requestUrl` are the entire surface you are given, and they are enough. |
| 37 | |
| 38 | ## The return value is the only transport |
| 39 | |
| 40 | The agent sees exactly one thing: the value your `execute` returns, JSON-stringified. A thrown error becomes `Script execution error: {message}`, so put the recovery step in the message. Everything the agent must know goes in the return value. |
| 41 | |
| 42 | `console.log` is not a no-op, despite what this file used to claim. It works, and the output is visible in the Electron DevTools console. It simply is not a transport: nothing can read a child realm's console from the parent, and across an opaque origin the parent cannot even patch it. Use it to debug with DevTools open; never to return a result. |
| 43 | |
| 44 | ## There is no network, and this one is absolute |
| 45 | |
| 46 | `fetch`, `XMLHttpRequest`, `WebSocket`, `EventSource` and `navigator.sendBeacon` all EXIST as globals. Every call fails. The CSP is `default-src 'none'` with no `connect-src`, `connect-src` falls back to `default-src` by spec, and `'none'` matches no URL at all: not a remote one, not a relative one, not a same-origin one. `fetch()` rejects with `TypeError: Failed to fetch` before a packet leaves the process. There is no URL, no retry and no fallback that changes this. |
| 47 | |
| 48 | Dynamic `import('https://esm.sh/...')` is dead for the same reason: a script fetch falls to `script-src`, which grants `'unsafe-inline' 'unsafe-eval'` and **no URL sources**. `'unsafe-eval'` permits `new Function`; it does not permit loading code from a URL. |
| 49 | |
| 50 | `ctx.requestUrl` is the only way out, and it works because the PARENT makes the request, not you. |
| 51 | |
| 52 | ## Storage throws, it does not fail softly |
| 53 | |
| 54 | `localStorage`, `sessionStorage`, `document.cookie` and `indexedDB` are blocked by the opaque origin. Reading the property THROWS a `SecurityError`; even `typeof localStorage` throws. An unguarded reference kills the script at the point of access, which is harsher than the network case. Persist through `ctx.vault` |