diff --git a/xbox/runtime/CMakeLists.txt b/xbox/runtime/CMakeLists.txt index abb5e98f7d..326a22fcfe 100644 --- a/xbox/runtime/CMakeLists.txt +++ b/xbox/runtime/CMakeLists.txt @@ -5,3 +5,7 @@ target_include_directories(runtime_contract PRIVATE include) target_compile_features(runtime_contract PRIVATE cxx_std_20) enable_testing() add_test(NAME runtime_contract COMMAND runtime_contract) +add_executable(piece_supervisor_contract tests/piece_supervisor_contract.cpp) +target_include_directories(piece_supervisor_contract PRIVATE include) +target_compile_features(piece_supervisor_contract PRIVATE cxx_std_20) +add_test(NAME piece_supervisor_contract COMMAND piece_supervisor_contract) diff --git a/xbox/runtime/README.md b/xbox/runtime/README.md index 6925191e51..6e1df2e242 100644 --- a/xbox/runtime/README.md +++ b/xbox/runtime/README.md @@ -1,8 +1,10 @@ # Aesthetic Computer Xbox native runtime -This directory is the portability seam between AC game logic and the Xbox UWP -host. It does not replace the latency probe or the WebView app. It defines the -small native API that future C++ ports—starting with the `nom` family—can share. +This directory is the portability seam for the native Xbox AC BIOS. The target +does **not** host a WebView: Direct3D renders AC drawing commands, XAudio2 plays +sound, Windows.Gaming.Input supplies input, and an interpreter compiled into +the package runs downloaded AC JavaScript pieces. The WebView dynamic shell is +an experimental comparison target, not the production architecture. ## Compatibility target @@ -32,9 +34,38 @@ game rules but use native text and basic oscillator cues. 5. Enqueue `Sound::synth` directly to a preallocated XAudio2 source voice. 6. Poll the outbound `ControlChannel` and emit timestamped telemetry. -The control channel accepts only precompiled piece/probe identifiers and JSON -configuration. It must not evaluate downloaded native code. A hosted WebView can -remain available when arbitrary JS iteration is useful. +## Live piece loading + +`JsEngine` is the narrow adapter for an embedded interpreter. The intended +first engine is QuickJS: it is MIT licensed, small, has no required external +dependencies, and is an interpreter (so it does not require runtime code +generation/JIT privileges). Pin and vendor an audited release in the build; +never download the engine itself at runtime. + +The AC endpoint returns a bounded `PieceBundle` containing `slug`, immutable +`version`, UTF-8 `source`, and SHA-256. The host downloads it with +`Windows.Web.Http.HttpClient`, requires HTTPS, verifies the digest, then asks +`PieceSupervisor` to stage it. Staging creates a fresh JS runtime with only AC +bindings, compiles and calls `boot`; activation happens at a frame boundary. +The prior context remains the last-known-good fallback until the new generation +has survived its probation window. Syntax/boot/runtime/watchdog failures emit +telemetry and roll back without restarting the binary. + +The JS global surface must not expose WinRT, filesystem/process APIs, `eval` of +untrusted secondary sources, raw sockets, or a general-purpose browser API. +Heap, source size, stack, callback time, and pending-job counts are bounded. +Network access is host-mediated and restricted to declared AC endpoints. + +This dynamic model must remain consistent with the product's declared purpose. +Microsoft Store policy 10.2.2 disallows using downloaded code to fundamentally +change or extend an app's described functionality. AC pieces therefore need to +be presented and certified as the title's normal content/runtime model, not as +a way to install unrelated programs. Dev Mode testing is feasible now; retail +certification needs an explicit policy review before relying on remote source. + +The control channel accepts configuration, piece source, and probe identifiers. +It must never accept downloaded native code. Native BIOS changes still require +a rebuilt and signed package. ## Nom porting plan diff --git a/xbox/runtime/include/ac/runtime.hpp b/xbox/runtime/include/ac/runtime.hpp index f3fed86e54..596bb7c136 100644 --- a/xbox/runtime/include/ac/runtime.hpp +++ b/xbox/runtime/include/ac/runtime.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -102,8 +103,98 @@ class Piece { virtual void leave(Api&) {} }; -// Control-plane commands are deliberately data/configuration, never downloaded -// executable code. A shim can change pieces, settings, and probes remotely. +// A downloaded piece is JavaScript source evaluated by an embedded engine. It +// is data to the UWP package: no PE/DLL/native code is accepted by this API. +// sha256 is supplied by the AC endpoint and verified by the platform host +// before stage() is called. +struct PieceBundle { + std::string slug; + std::string version; + std::string source; + std::string sha256; +}; + +struct JsLimits { + std::size_t max_source_bytes = 2 * 1024 * 1024; + std::size_t max_heap_bytes = 32 * 1024 * 1024; + std::uint64_t max_callback_us = 8'000; +}; + +// Adapter seam for QuickJS (or another interpreter compiled into the app). +// The engine exposes only the AC Api bindings; browser, filesystem, process, +// WinRT and arbitrary network globals are intentionally absent. +class JsPiece : public Piece { + public: + ~JsPiece() override = default; + [[nodiscard]] virtual std::string_view slug() const noexcept = 0; + [[nodiscard]] virtual std::string_view version() const noexcept = 0; +}; + +class JsEngine { + public: + virtual ~JsEngine() = default; + virtual std::unique_ptr compile(const PieceBundle&, const JsLimits&, + std::string& error) = 0; +}; + +// Owns the active and last-known-good JS contexts. Reload is transactional: +// compile and boot the candidate first, then swap at a frame boundary. If a +// callback throws or exceeds its budget the previous generation is restored. +class PieceSupervisor { + public: + explicit PieceSupervisor(JsEngine& engine, JsLimits limits = {}) + : engine_(engine), limits_(limits) {} + + bool stage(const PieceBundle& bundle, Api& api, std::string& error) { + if (bundle.source.size() > limits_.max_source_bytes) { + error = "piece source exceeds configured byte limit"; + return false; + } + auto candidate = engine_.compile(bundle, limits_, error); + if (!candidate) return false; + try { + candidate->boot(api); + } catch (...) { + error = "piece boot failed"; + return false; + } + staged_ = std::move(candidate); + return true; + } + + bool activate(Api& api) { + if (!staged_) return false; + if (active_) { + active_->leave(api); + fallback_ = std::move(active_); + } + active_ = std::move(staged_); + ++generation_; + return true; + } + + bool rollback(Api& api) { + if (!fallback_) return false; + if (active_) active_->leave(api); + active_ = std::move(fallback_); + ++generation_; + return true; + } + + [[nodiscard]] Piece* active() const noexcept { return active_.get(); } + [[nodiscard]] std::uint64_t generation() const noexcept { return generation_; } + + private: + JsEngine& engine_; + JsLimits limits_; + std::unique_ptr active_; + std::unique_ptr staged_; + std::unique_ptr fallback_; + std::uint64_t generation_ = 0; +}; + +// Control-plane commands are data/configuration or sandboxed JS piece source, +// never downloaded native executable code. enum class CommandKind { load_piece, configure, run_probe, reload, ping }; struct Command { CommandKind kind; diff --git a/xbox/runtime/tests/piece_supervisor_contract.cpp b/xbox/runtime/tests/piece_supervisor_contract.cpp new file mode 100644 index 0000000000..2cdb77ca5f --- /dev/null +++ b/xbox/runtime/tests/piece_supervisor_contract.cpp @@ -0,0 +1,29 @@ +#include "ac/runtime.hpp" +#include +#include +#include +using namespace ac::xbox; +namespace { +class NullGraphics final : public Graphics { public: void wipe(Color) override {} void box(const Rect&) override {} void line(const Line&) override {} void write(const Text&) override {} }; +class NullSound final : public Sound { public: void synth(const SynthVoice&) override {} void stop_all() override {} int sample_rate() const override { return 48000; } }; +class FakePiece final : public JsPiece { + public: + FakePiece(std::string s, std::string v, bool fail) : slug_(std::move(s)), version_(std::move(v)), fail_(fail) {} + std::string_view slug() const noexcept override { return slug_; } + std::string_view version() const noexcept override { return version_; } + void boot(Api&) override { if (fail_) throw std::runtime_error("boot"); } + void sim(Api&) override {} void paint(Api&) override {} void act(Api&, const Event&) override {} + private: std::string slug_, version_; bool fail_; +}; +class FakeEngine final : public JsEngine { public: std::unique_ptr compile(const PieceBundle& b, const JsLimits&, std::string& error) override { if (b.source == "syntax error") { error = "compile failed"; return {}; } return std::make_unique(b.slug, b.version, b.source == "boot error"); } }; +} +int main() { + NullGraphics graphics; NullSound sound; Api api{{}, {}, {}, {}, graphics, sound}; + FakeEngine engine; PieceSupervisor supervisor(engine, JsLimits{64}); std::string error; + assert(supervisor.stage({"fight", "v1", "ok", "hash"}, api, error)); assert(supervisor.activate(api)); assert(supervisor.generation() == 1); + assert(!supervisor.stage({"fight", "bad", "syntax error", "hash"}, api, error)); assert(supervisor.generation() == 1); + assert(!supervisor.stage({"fight", "bad", "boot error", "hash"}, api, error)); + assert(supervisor.stage({"fight", "v2", "ok", "hash"}, api, error)); assert(supervisor.activate(api)); assert(supervisor.generation() == 2); + assert(supervisor.rollback(api)); assert(supervisor.generation() == 3); + assert(!supervisor.stage({"fight", "huge", std::string(65, 'x'), "hash"}, api, error)); +}