7.8 KiB
API_TODO — trans-membrane OS surface expansion
Status: briefing / design spec for the next round of uce_host_* membrane work.
Implementation delegated to the pi agent + sub-agents.
Why
UCE units run as wasm in a per-request workspace; the only way they touch the
OS is via uce_host_* hostcalls (see docs/wasm-runtime-architecture.md). That
constrained, auditable surface is a feature — it's the security boundary
sysadmins can reason about. The current file API (file_get_contents /
file_put_contents / file_append / file_exists / file_unlink / ls /
file_mkdir / file_mtime / path_* / cwd_*) is simple and good but is
whole-file only and lacks several OS capabilities real apps need. This doc
widens what units can do within the existing path roots — not where.
Cross-cutting design principles (apply to EVERY new call)
- Requests must complete fast — never block indefinitely. Every potentially
blocking call takes an explicit timeout/deadline and fails fast on expiry
with a clear result, rather than spinning or blocking forever. In particular:
- File locks expire. Auto-locking (writes take exclusive, reads wait for
writers) must use a bounded wait (deadline, e.g. config
UCE_FILE_LOCK_TIMEOUT_MS, sane default ~2000ms). On timeout the op returns a clear "lock timeout" error — it does NOT hang the request. This applies tofile_put_contents/file_append/file_get_contentsand the new streaming handles below. http_request, blockingshell_exec,job_await, etc. all take timeouts.
- File locks expire. Auto-locking (writes take exclusive, reads wait for
writers) must use a bounded wait (deadline, e.g. config
- Fire-and-forget + check back later. Long work goes to the background and the request returns immediately with a job id; a later request (any worker) checks the outcome. A single host-side job registry backs this (see "Async jobs"). This is the model for background shell, async http, and any long op — not per-request blocking.
- Simple AND versatile. Prefer one small, composable primitive over many
special cases. Structured inputs/outputs are
DValue(UCEB across the membrane). Streaming uses opaque host handles (u64). Errors are returned values (no exceptions in the guest). - One capability per named hostcall (blocklist granularity — see Future). Don't bundle unrelated capabilities into one mega-hostcall.
- Always policy-gated. Every path goes through
resolve_guest_file/resolve_guest_write(roots); handles/stat/rename/etc. gated the same way. - No exceptions/RTTI in unit-facing code; return values + status DValues.
API groups to implement
1. Streaming / handle-based file I/O (P0 — the keystone)
Whole-file String I/O can't handle large or partial files. Add a handle API.
Locking stays automatic and lifetime-scoped (open-for-write = exclusive,
open-for-read = shared, released on file_close), with the bounded-wait rule
from principle 1 — this is NOT the old manual lock API, the user never calls a
lock function.
u64 file_open(String path, String mode) // "r" "w" "a" "r+"; 0 = error (incl. lock timeout)
String file_read(u64 h, u64 len) // sequential
String file_pread(u64 h, u64 offset, u64 len) // positional
u64 file_write(u64 h, String data) // sequential; returns bytes written
u64 file_pwrite(u64 h, u64 offset, String data)
s64 file_seek(u64 h, s64 offset, int whence) // + file_tell(h)
void file_close(u64 h) // releases lock + fd
Keep the existing whole-file file_get_contents/put_contents/append (now
auto-locking) as the easy path.
2. Metadata (P0/P1)
DValue file_stat(String path) // { exists, size, mtime, ctime, mode, is_dir, is_file, is_symlink }
DValue dir_list(String path) // [ { name, type, size, mtime } ... ]; optional glob/recursive flags
(file_mtime stays; dir_list is the richer ls so callers don't N+1 file_stat.)
3. Structural ops (P1)
bool file_rename(String from, String to) // atomic replace; enables write-temp-then-rename
bool file_copy(String from, String to) // host-side, preserves mode
bool file_truncate(String path, u64 size) // (or on a handle)
bool dir_remove(String path, bool recursive)
4. Niceties (P2)
String file_temp(String prefix) // race-free temp file under a write root (mkstemp)
bool file_chmod(String path, u32 mode)
bool file_symlink(String target, String linkpath); String file_readlink(String path)
// durability: file_put_contents/file_write gain an optional fsync flag/variant
5. Outbound HTTP (with TLS) (high value)
socket_* is raw TCP only — no secure outbound. Add a real client:
DValue http_request(DValue req) // req: { method, url, headers{}, body, timeout_ms, follow_redirects }
// -> { status, headers{}, body, error } (TLS/https supported)
u64 http_request_async(DValue req) // -> job_id (see Async jobs)
6. Crypto (extended) (high value)
hash.h has sha1 + rng only.
String sha256(String data); String sha256_hex(String data)
String hmac_sha256(String key, String data) // + _hex
String base64_encode(String data); String base64_decode(String b64)
String random_bytes(u64 n) // CSPRNG
bool crypto_equal(String a, String b) // constant-time compare
7. Shell (extended + background) (high value)
Current shell_exec is blocking + output-only.
DValue shell_exec(DValue spec) // spec: { cmd, stdin, env{}, timeout_ms } -> { exit_code, stdout, stderr, timed_out }
u64 shell_spawn(DValue spec) // background -> job_id; non-blocking; request returns immediately
(Keep a simple shell_exec(String) convenience overload returning stdout.)
8. Async jobs (the unifying "check back later" primitive)
Host-side job registry, survives the spawning request, reachable from any later request/worker by id.
DValue job_status(u64 job_id) // { state: pending|running|done|failed, ... }
DValue job_result(u64 job_id) // blocks up to a small default; { done, ... , result }
DValue job_await(u64 job_id, u64 timeout_ms) // bounded wait; returns even if not done (state=running)
bool job_cancel(u64 job_id)
shell_spawn and http_request_async produce job ids consumed here. Jobs have a
TTL and are reaped; results are retrievable until collected or TTL expires.
Explicitly OUT of scope (declined)
- DNS resolution API, UDP sockets,
setenv/ environment mutation.
Future (design influences now, NOT implemented here)
- Configurable trans-membrane blocklist. A server config will let sysadmins
disable specific hostcalls by name (e.g. turn off
shell_exec,http_request) to harden a deployment. This is WHY principle 4 (one capability per named hostcall) matters — keep the granularity clean so each is independently gateable. A blocked call should return a clear "disabled by policy" error. - Native app extensions. Longer term: let app developers ship their own
native compiled modules (
.soliving in the site tree) that units can call across the membrane — a per-app plugin mechanism. Out of scope now, but the hostcall/membrane shape should not preclude a future generic "call host extension" dispatch.
Implementation notes
- Each call = a discrete
uce_host_*hostcall: add tosrc/wasm/core_hostcalls.syms, register insrc/wasm/worker.cpp, declare/implement the wasm-core wrapper insrc/lib/sys.cpp(+ header) mirroring existing hostcalls (sized-buffer for bytes, sized-DValue/UCEB for structured returns). - Reuse native impls where they exist; policy-gate every path.
- Each group lands suite-gated (
run_cli_tests --include-wasm-kill, 0 failed) on the build host; add tests + doc pages per new function (function pages, and method/2_pages where a class gains methods).