commit 1c47b197e9c53fd0beb193ca220700507cbb44b0
parent 045117aaf65aa29f19d4c83f74b82eab61263f2e
Author: triesap <tyson@radroots.org>
Date: Wed, 8 Apr 2026 17:25:52 +0000
stdio: add typed hyf wire codec
- add strict mojson-backed request and response envelopes
- add newline-delimited decode and encode helpers for stdio
- return explicit invalid_request and unsupported_capability wire errors
- keep pixi run run non-blocking when stdin is interactive
Diffstat:
4 files changed, 177 insertions(+), 18 deletions(-)
diff --git a/src/hyf_stdio/codec.mojo b/src/hyf_stdio/codec.mojo
@@ -0,0 +1,22 @@
+from mojson import dumps
+from mojson.deserialize import deserialize
+
+from hyf_stdio.envelope import (
+ WireErrorResponse,
+ WireRequest,
+ WireSuccessResponse,
+)
+
+
+def decode_request(line: String) raises -> WireRequest:
+ if line == "":
+ raise Error("request line must not be empty")
+ return deserialize[WireRequest](line)
+
+
+def encode_success(response: WireSuccessResponse) raises -> String:
+ return dumps(response.to_json_value())
+
+
+def encode_error(response: WireErrorResponse) raises -> String:
+ return dumps(response.to_json_value())
diff --git a/src/hyf_stdio/envelope.mojo b/src/hyf_stdio/envelope.mojo
@@ -1,3 +1,74 @@
-def envelope_module_name() -> String:
- return "hyf_stdio.envelope"
+from mojson import Value, loads
+from mojson.deserialize import Deserializable, get_string
+from hyf_stdio.errors import WireError
+
+
+def _require_object(value: Value, context: String) raises:
+ if not value.is_object():
+ raise Error(context + " must be a JSON object")
+
+
+def _require_non_empty(value: String, field_name: String) raises:
+ if value == "":
+ raise Error(
+ "request envelope field '" + field_name + "' must not be empty"
+ )
+
+
+def _require_request_keys(value: Value) raises:
+ for key in value.object_keys():
+ if key != "request_id" and key != "capability" and key != "input":
+ raise Error(
+ "request envelope contains unexpected field '" + key + "'"
+ )
+
+
+@fieldwise_init
+struct WireRequest(Deserializable, Copyable, Movable):
+ var request_id: String
+ var capability: String
+ var input: Value
+
+ @staticmethod
+ def from_json(json: Value) raises -> Self:
+ _require_object(json, "request envelope")
+ _require_request_keys(json)
+
+ var request_id = get_string(json, "request_id")
+ _require_non_empty(request_id, "request_id")
+
+ var capability = get_string(json, "capability")
+ _require_non_empty(capability, "capability")
+
+ return Self(
+ request_id=request_id,
+ capability=capability,
+ input=json["input"].clone(),
+ )
+
+
+@fieldwise_init
+struct WireSuccessResponse(Copyable, Movable):
+ var request_id: String
+ var output: Value
+
+ def to_json_value(self) raises -> Value:
+ var value = loads("{}")
+ value.set("request_id", Value(String(self.request_id)))
+ value.set("ok", Value(True))
+ value.set("output", self.output.clone())
+ return value^
+
+
+@fieldwise_init
+struct WireErrorResponse(Copyable, Movable):
+ var request_id: String
+ var error: WireError
+
+ def to_json_value(self) raises -> Value:
+ var value = loads("{}")
+ value.set("request_id", Value(String(self.request_id)))
+ value.set("ok", Value(False))
+ value.set("error", self.error.to_json_value())
+ return value^
diff --git a/src/hyf_stdio/errors.mojo b/src/hyf_stdio/errors.mojo
@@ -1,3 +1,28 @@
-def stdio_error_module_name() -> String:
- return "hyf_stdio.errors"
+from mojson import Value, loads
+
+@fieldwise_init
+struct WireError(Copyable, Movable):
+ var code: String
+ var message: String
+
+ def to_json_value(self) raises -> Value:
+ var value = loads("{}")
+ value.set("code", Value(String(self.code)))
+ value.set("message", Value(String(self.message)))
+ return value^
+
+
+def invalid_request_error(message: String) -> WireError:
+ return WireError(code="invalid_request", message=message)
+
+
+def unsupported_capability_error(capability: String) -> WireError:
+ return WireError(
+ code="unsupported_capability",
+ message="no handler registered for capability '" + capability + "'",
+ )
+
+
+def internal_error(message: String) -> WireError:
+ return WireError(code="internal_error", message=message)
diff --git a/src/hyf_stdio/server.mojo b/src/hyf_stdio/server.mojo
@@ -1,18 +1,59 @@
-from hyf_core.backends.null_backend import backend_name
-from hyf_core.capabilities.registry import bootstrap_capability_count
-from hyf_core.errors import core_error_module_name
-from hyf_core.provenance import provenance_module_name
-from hyf_core.request_context import request_context_module_name
-from hyf_stdio.envelope import envelope_module_name
-from hyf_stdio.errors import stdio_error_module_name
+from std.io.io import _fdopen
+from std.sys import stdin
+
+from hyf_stdio.codec import decode_request, encode_error
+from hyf_stdio.envelope import WireErrorResponse, WireRequest
+from hyf_stdio.errors import (
+ internal_error,
+ invalid_request_error,
+ unsupported_capability_error,
+)
+
+
+def _read_request_line() raises -> String:
+ with _fdopen["r"](stdin) as input_file:
+ return input_file.readline()
+
+
+def _unsupported_response(request: WireRequest) -> WireErrorResponse:
+ return WireErrorResponse(
+ request_id=String(request.request_id),
+ error=unsupported_capability_error(String(request.capability)),
+ )
+
+
+def _write_response(response: WireErrorResponse) raises:
+ print(encode_error(response))
def run_stdio_server() raises:
- _ = backend_name()
- _ = bootstrap_capability_count()
- _ = core_error_module_name()
- _ = provenance_module_name()
- _ = request_context_module_name()
- _ = envelope_module_name()
- _ = stdio_error_module_name()
+ if stdin.isatty():
+ return
+
+ try:
+ var line = _read_request_line()
+
+ try:
+ var request = decode_request(line)
+ var request_id = String(request.request_id)
+ try:
+ _write_response(_unsupported_response(request))
+ except e:
+ _write_response(
+ WireErrorResponse(
+ request_id=request_id,
+ error=internal_error(String(e)),
+ )
+ )
+ except e:
+ _write_response(
+ WireErrorResponse(
+ request_id="",
+ error=invalid_request_error(String(e)),
+ )
+ )
+ except e:
+ if String(e) == "EOF":
+ return
+ raise e^