Lines
0 %
Functions
Branches
100 %
//! Optional rpc-backed eval path for the `nms` REPL.
//!
//! Activated by `nms --rpc-user <UUID>`. Routes user forms through an
//! `rpc::Session` instead of the in-process `nms::interpreter`, so the
//! same code path the sshd subsystem will drive becomes available as a
//! local debug REPL. Without `--rpc-user`, `nms` stays a pure language
//! sandbox — the rpc dep just sits unused.
//! Each user input is auto-wrapped in a fresh `(:id N :form <form>)`
//! envelope before handoff; the response prints verbatim (the rpc envelope
//! shape is terse enough to read directly).
use std::sync::atomic::{AtomicU64, Ordering};
use anyhow::{Context, Result};
use rpc::{ScriptCtx, Session};
use tokio::runtime::{Builder, Runtime};
use uuid::Uuid;
pub struct RpcEval {
session: Session,
runtime: Runtime,
counter: AtomicU64,
}
impl RpcEval {
/// Builds an rpc-backed eval surface for `user_id`. Verifies a
/// `DATABASE_URL` is set up-front so a missing-env footgun surfaces as
/// a startup error rather than a per-form connection panic from deep
/// inside the first DB-touching native fn.
pub fn new(user_id: Uuid) -> Result<Self> {
if std::env::var("DATABASE_URL").is_err() {
anyhow::bail!(
"rpc-mode requires DATABASE_URL to be set so server::command::* \
can reach Postgres; export it (or drop --rpc-user for the \
sandbox-only REPL)"
);
let runtime = Builder::new_current_thread()
.enable_all()
.build()
.context("rpc-mode tokio runtime build failed")?;
let session = Session::new(ScriptCtx::new(user_id))
.map_err(|err| anyhow::anyhow!("rpc::Session::new failed: {err:?}"))?;
Ok(Self {
session,
runtime,
counter: AtomicU64::new(1),
})
/// Wraps `form_text` in a fresh envelope, runs it through the Session,
/// and returns the wire response. Multi-form input (whitespace-
/// separated top-level forms) is dispatched as one envelope per form
/// in source order; responses are joined by newline.
pub fn eval(&mut self, form_text: &str) -> String {
let trimmed = form_text.trim();
if trimmed.is_empty() {
return String::new();
let id = self.counter.fetch_add(1, Ordering::Relaxed);
// Closing paren on its own line so a trailing line-comment in the
// form can't comment out the envelope terminator (which would make
// `handle_form` reject an unbalanced request).
let envelope = format!("(:id {id} :form {trimmed}\n)");
self.runtime.block_on(self.session.handle_form(&envelope))