Skip to content

Extending uvscope

uvscope does not load native plugins in 1.x. Extensibility is provided by four stable, documented surfaces: environment variables, request scripts, the command library and the HTTP API. The child-process IPC protocol is an internal boundary between uvscope processes, not a public ABI.

Environment variables

Environments live in the HTTP workspace (GET/PUT /api/http/workspace) and are selected per workspace. Variables use double braces and are substituted in the request URL, header names and values, and the request body:

{{base_url}}/v1/users
Authorization: Bearer {{token}}

The substitution pattern is {{ name }} with [\w.-]+ names. Unknown variables are left literal so a typo is visible in the outgoing request. The active environment can be changed without re-saving a request; the raw request text is preserved.

Pre-request and post-response scripts

Scripts run in the browser as async functions with "use strict". They are trusted local code: unlike the response preview, they are not sandboxed and can call browser APIs. The workspace request and response are passed through the uvscope argument.

Pre-request

const url = new URL(uvscope.request.url)
url.searchParams.set('trace', '1')
uvscope.request.url = url.toString()
uvscope.request.headers.push({ name: 'X-Debug', value: '1' })
uvscope.env.set('last_trace', '1')
Surface Description
uvscope.request.method Mutable method
uvscope.request.url Mutable URL (variables already substituted)
uvscope.request.headers Mutable [{ name, value }] array
uvscope.request.body Mutable body text
uvscope.env.get(name) / uvscope.env.set(name, value) Read or write the active environment
uvscope.variables Read-only snapshot of the active environment

Returning normally sends the (possibly modified) request. A thrown error aborts the send and is shown in the UI. The script may be async; await is supported.

Post-response

uvscope.test('status is 200', () => {
  if (uvscope.response.status !== 200) throw new Error('unexpected status')
})
uvscope.test('json has items', () => {
  if (!Array.isArray(uvscope.response.json().items)) throw new Error('items missing')
})
if (uvscope.response.status === 401) uvscope.env.set('token', '')
Surface Description
uvscope.response.status Numeric status
uvscope.response.headers [{ name, value }]
uvscope.response.text Body text
uvscope.response.contentType Content-Type header or ""
uvscope.response.json() Parsed JSON (throws on invalid JSON)
uvscope.test(name, fn) Runs fn; a throw records a failed assertion
uvscope.env.get/set, uvscope.variables Same as pre-request

Post-response scripts run only for successful requests (res.ok). Script source is stored in browser localStorage per workspace; it is not written to SQLite and is not sent to the daemon.

Command library

The terminal command catalog is seeded into SQLite on first migration. Custom entries are created with POST /api/terminal/commands:

{
  "name": "Tail app log",
  "command": "tail -f /var/log/app.log",
  "category": "custom",
  "platform": "linux",
  "description": "Follow the application log",
  "quick": true
}

platform is linux, windows or empty (both), category groups the list, description is shown in the command detail, and quick: true pins the entry to the quick bar. Admins can edit or delete any entry and restore deleted built-ins with POST /api/terminal/commands/restore. Commands are plain text; uvscope never executes a command without an explicit user action in a terminal session.

Proxy destination variables

Rule destinations (redirect / rewrite) support the variables documented in the HTTP API: $uri, $args, $request_uri, $rest, $1–$9, $host and $scheme. Unknown variables are left literal. This is the supported way to express routing logic without a plugin.

HTTP API

Every UI feature is backed by the same JSON API documented in API.md. Responses use { ok, ... } with a typed code on errors, timestamps are ISO-8601 UTC, and event streams are sequenced for incremental polling. Event and payload compatibility rules are in EVENTS.md. Integrations should use the API rather than driving the UI.

Child-process IPC (internal)

Isolated services are launched with hidden flags (--proxy-child, --listener-child, --socks5-child, --terminal-child, --ipc-child). The parent and child exchange frames over a Unix domain socket (Windows named pipe):

4-byte big-endian length | UTF-8 JSON payload (maximum 4 MiB)

Each service defines its own message types (start, input, resize, redraw, close, stop, snapshot, output, events, send, session-events, …) and an initial ready/started handshake. The protocol is versioned with the daemon: a child is always the same executable and build as its parent. It is documented here for operators and contributors, but it is not a stable third-party plugin ABI and may change between minor releases.

What is not in 1.x

  • A native plugin ABI or dynamically loaded modules.
  • User-authored server-side code execution.
  • A remote API authentication mode other than the workbench session cookie.

These are tracked as post-1.0 work in the roadmap.