diff --git a/docs/assets/robotics-policy-prover/action-governance-architecture.svg b/docs/assets/robotics-policy-prover/action-governance-architecture.svg
new file mode 100644
index 00000000..192d02dc
--- /dev/null
+++ b/docs/assets/robotics-policy-prover/action-governance-architecture.svg
@@ -0,0 +1,109 @@
+
diff --git a/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 b/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4
new file mode 100644
index 00000000..32a1cf73
Binary files /dev/null and b/docs/assets/robotics-policy-prover/openshell-robotics-prover-demo.mp4 differ
diff --git a/docs/assets/robotics-policy-prover/policy-latency-scaling.svg b/docs/assets/robotics-policy-prover/policy-latency-scaling.svg
new file mode 100644
index 00000000..9cafd467
--- /dev/null
+++ b/docs/assets/robotics-policy-prover/policy-latency-scaling.svg
@@ -0,0 +1,59 @@
+
diff --git a/docs/assets/robotics-policy-prover/robotics-policy-prover-hero.png b/docs/assets/robotics-policy-prover/robotics-policy-prover-hero.png
new file mode 100644
index 00000000..ef54c4cc
Binary files /dev/null and b/docs/assets/robotics-policy-prover/robotics-policy-prover-hero.png differ
diff --git a/docs/assets/robotics-policy-prover/terra-planning-loop-variance.svg b/docs/assets/robotics-policy-prover/terra-planning-loop-variance.svg
new file mode 100644
index 00000000..33061b94
--- /dev/null
+++ b/docs/assets/robotics-policy-prover/terra-planning-loop-variance.svg
@@ -0,0 +1,66 @@
+
diff --git a/docs/dev-notes/index.md b/docs/dev-notes/index.md
index f0cc7b8a..d35e81cc 100644
--- a/docs/dev-notes/index.md
+++ b/docs/dev-notes/index.md
@@ -31,10 +31,44 @@ hide:
Featured note
Latest from the team
-
+
+
+
+

+
+
+
+
+ Physical AI
+
+
Can Formal Methods Govern AI-Generated Robot Actions?
+
A robotics experiment asks whether an independent, SMT-backed policy boundary can efficiently govern AI-generated plans before they reach a simulated or physical robot.
+
+ physical-ai
+ formal-methods
+ policy
+
+
+
+
+
+
+
+
+
Recent notes
+ The working archive
+
+
diff --git a/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md b/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md
new file mode 100644
index 00000000..da00d1f1
--- /dev/null
+++ b/docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md
@@ -0,0 +1,392 @@
+---
+title: "Can Formal Methods Govern AI-Generated Robot Actions?"
+date: 2026-08-07
+updated: 2026-08-10
+description: "A robotics experiment asks whether an independent, SMT-backed policy boundary can efficiently govern AI-generated plans before they reach a simulated or physical robot."
+agent_markdown: true
+hero_image: "../../assets/robotics-policy-prover/robotics-policy-prover-hero.png"
+categories:
+ - Physical AI
+tags:
+ - openshell
+ - robotics
+ - physical-ai
+ - formal-methods
+ - policy
+ - agents
+authors:
+ - zredlined
+card_tags:
+ - physical-ai
+ - formal-methods
+ - policy
+---
+
+# Can Formal Methods Govern AI-Generated Robot Actions?
+
+
+
+
+
+ Dev Note
+
+ Physical AI
+
+
+
+
+
+*We built a small robotics experiment to test whether OpenShell's formal
+methods-based policy enforcement can govern robot plans generated by local and
+frontier AI models.*
+
+
+
+
+
+OpenShell uses formal methods-based policy enforcement to constrain what AI
+agents can do in digital environments. We wanted to see whether a similar
+approach could govern the higher-level plans local and frontier models generate
+for robots.
+
+To explore that question, we built a small robotics pick-and-place experiment.
+The task is intentionally simple: move a green block into a blue tray. An AI
+planner proposes a sequence of 3D waypoints, and a policy prover checks the path
+before the simulated tool head moves. The checks cover workspace limits, a
+restricted volume, human proximity, sensor freshness, delegated authority,
+force, and task budget.
+
+Formal methods are attracting renewed attention as AI systems become agents.
+Recent work is exploring
+[behavioral contracts](https://arxiv.org/abs/2602.22302),
+[runtime compliance over agent traces](https://arxiv.org/abs/2606.19242), and
+other ways to turn desired behavior into specifications an external system can
+check. Many familiar approaches focus on making the model itself more reliable:
+better instructions, safer training, stronger evaluations, or another model
+acting as a judge. Those remain important layers. This experiment looks at a
+complementary question: can a separate system check a proposed physical action
+before it reaches a simulator or robot?
+
+For this prototype, we split responsibility this way:
+
+- The agent proposes a plan it believes will accomplish the task.
+- An independent, deterministic policy boundary sits between that plan and the
+ simulated actuator.
+- A rejection returns structured evidence the agent can use to revise its plan.
+- Only a version admitted by the policy boundary reaches the executor, with any
+ returned constraints and runtime obligations attached.
+
+We set out to learn whether that check could return useful feedback to the
+planner and run quickly enough to fit a robot-planning loop driven by an AI
+model.
+
+
+
+
+
+---
+
+## The Experiment
+
+
+
+The recorded run follows four steps: propose, check, adapt, execute.
+
+First, GPT-5.6 Terra proposes a waypoint path whose direct route crosses the red
+restricted volume. The policy service denies the action, identifies
+`restricted_zone_intersection`, returns an approximate counterexample point,
+and supplies a minimum bypass height. Nothing moves.
+
+The planning loop sends that decision packet back to the model, which proposes
+a route around the restricted volume. Before execution, the world changes: a
+human enters the caution radius. The policy service evaluates the action again
+and changes the contract. The path may proceed, but only at a speed of 0.08 m/s
+or less, with an obligation to pause if the human gets closer.
+
+The executor runs the revised path with the returned speed limit. In this run,
+the planning loop could propose and revise a path, while the policy boundary
+determined which version reached the simulated actuator.
+
+Here is an example decision packet:
+
+```json
+{
+ "decision": "deny",
+ "violations": ["restricted_zone_intersection"],
+ "constraints": {
+ "bypass_z_min": 0.55
+ },
+ "obligations": ["emit_audit_event"],
+ "counterexample": {
+ "segment_id": "proposed_segment",
+ "zone_id": "restricted_zone.alpha",
+ "reason": "restricted_zone_intersection",
+ "point": [-0.18, 0.35, -0.20]
+ }
+}
+```
+
+In this demo, that response gives the planner more to work with than a generic
+"unsafe" error. It serves as both an enforcement decision for the executor and
+machine-readable feedback for replanning.
+
+---
+
+## Why Check the Proposed Action?
+
+This experiment applies formal methods to the concrete action proposed by the
+planner.
+
+This resembles the formal-methods idea of a *shield*. In
+[safe reinforcement learning via shielding](https://ojs.aaai.org/index.php/AAAI/article/view/11797),
+a learned policy proposes actions while a reactive system monitors them and
+intervenes when one would violate a formal specification. Our prototype tests a
+similar division of work with an agent-generated robot path.
+
+Agent systems broaden that idea. A proposed action may be a shell command, a
+network request, a delegated capability, a financial transaction, or a robot
+trajectory. One useful formal object is a typed description of the action, the
+relevant state, and the invariant the surrounding system is expected to
+enforce—not the model's prose or chain of thought.
+
+The design also resembles the reference-monitor pattern emphasized in recent
+agent-security research. For example, [VeriGuard](https://research.google/pubs/veriguard-enhancing-llm-agent-safety-via-verified-code-generation/),
+work by Dj Dvijotham and collaborators at Google DeepMind, separates rigorous
+policy validation from a lightweight runtime monitor that checks each proposed
+action before execution. Applied to robotics, the open systems question is
+whether the monitor can cover the relevant paths from a planner's output to an
+actuator command.
+
+A robotics-focused prototype gives us a few key design goals to investigate:
+
+- **Close to the effect.** Check the plan after intent becomes a concrete
+ action, but before the simulator or robot moves.
+- **Compositional.** Evaluate overlapping concerns such as workspace,
+ authority, sensor freshness, human proximity, budget, speed, and force
+ together.
+- **Constructive.** Return a failed invariant, counterexample, or narrower
+ contract so the agent can improve its plan rather than guess.
+
+Any guarantee from the prototype remains relative to its specification and
+world model. Work
+on [safe reinforcement learning through proof and learning](https://ojs.aaai.org/index.php/AAAI/article/view/12107)
+makes the same essential point: formal verification provides confidence relative
+to a model, and cyber-physical reality will always test the completeness of that
+model. For this experiment, that means stating the property, assumptions, and
+enforcement point precisely, then testing where each breaks down as the
+environment becomes more realistic.
+
+---
+
+## How the Prototype Works
+
+OpenShell's architecture is designed to move security policy out of the agent
+and into the environment that mediates its actions. A sandboxed agent can
+reason, write code, call tools, and delegate work, but filesystem, network,
+process, and inference authority are enforced by infrastructure the agent does
+not control.
+
+This experiment explores whether the same architectural move can extend from
+the digital domain—such as coding or cybersecurity—to physical actions. The
+question becomes: can this particular motion safely run, with this object, at
+this speed, given the world state we have now?
+
+The prototype expresses each request as an action envelope with multiple,
+often-overlapping rules:
+
+```text
+actor and delegated subagent
+action and target resource
+start, end, and waypoint path
+requested speed and force
+object identity and class
+capability grant
+human distance and sensor age
+restricted and caution volumes
+remaining task budget
+```
+
+The service turns that envelope into one of four decisions:
+
+- `allow`: execute the proposed action.
+- `deny`: do not execute; return violations and a counterexample when possible.
+- `allow_with_constraints`: execute only inside narrower speed, force, or path
+ bounds.
+- `approval_required`: stop at a human decision point.
+
+The result also contains obligations for the executor. In this prototype, the
+executor applies returned speed and force limits, emits an audit event, and
+checks the human-distance pause condition immediately before motion. Continuous
+monitoring during execution is part of the next simulator integration, not a
+property of this first version.
+
+The prototype divides responsibility this way:
+
+```text
+agent runtime propose and revise the plan
+policy service decide and return an action contract
+executor enforce the contract and emit evidence
+```
+
+The policy check admits a plan or short-horizon physical action before execution.
+Once admitted, the robot's existing controller remains responsible for the
+low-level control loop. The solver is an action-admission boundary; it is not a
+motor controller.
+
+---
+
+## Prover Latency
+
+One way to review an AI-generated robot plan is to ask another model whether it
+looks safe. An LLM-as-judge can add useful semantic review, but it is another
+probabilistic inference call. If it takes as long as the planning model, it can
+nearly double the inference time.
+
+The first Terra path was invalid and had to be regenerated regardless of which
+review method caught it. The useful comparison is therefore the time spent
+checking the plans, not one plan versus two.
+
+The prover runs locally and has a narrower job: evaluate explicit invariants
+and return a deterministic admission decision. In ten complete browser
+missions, **`openai/openai/gpt-5.6-terra`** generated both the initial and
+corrected plans. All ten completed the deny, replan, constrain, and execute flow
+without a fixture fallback.
+
+Generating the two plans took **7.53 to 16.90 seconds** per mission. Proving
+both plans took **5.76 to 26.23 ms**, with a **17.30 ms median**—or
+**0.05–0.26%** of the observed inference time. Model planning varied by seconds;
+policy admission remained in milliseconds.
+
+
+
+ Policy-prover time as a share of AI inference across ten sequential Terra missions. This is not a general benchmark of the model or serving endpoint.
+
+
+We also isolated the policy decision in a release-mode benchmark on an **NVIDIA
+DGX Spark**. Across allow, deny, and constrained outcomes with 3 to 48
+waypoints, p95 latency was **1.006–1.250 ms**. Each of the 15 cases used 5,000
+measured decisions after 1,000 warm-ups. Against an illustrative 100 ms model
+call, the slowest p95 would add about **1.25%**. Against the multi-second
+calls observed in this experiment, the share is much smaller.
+
+
+
+ Release-mode p95 policy latency on NVIDIA DGX Spark.
+
+
+The Spark benchmark isolates geometry checks and Z3 setup/checking; it excludes
+model inference, transport, rendering, and execution. It supports using the
+prover at an AI model's planning cadence, where a model proposes a path or
+short-horizon action. It does not establish a hard real-time guarantee, and it
+does not move balance, torque, or joint control out of the robot's dedicated
+controllers and safety systems.
+
+---
+
+## What the Prototype Verifies
+
+The current implementation combines deterministic Rust checks with an
+SMT-backed policy check using Z3. Rust derives facts about workspace bounds,
+sampled segment/zone intersections, sensor freshness, budget, authority, human
+proximity, speed, and force. Z3 composes those Boolean facts into exactly one of
+the four policy outcomes with a bounded timeout. The model returned by Z3 is the
+source of the verdict; an unknown, inconsistent, or ambiguous result fails
+closed. Malformed action envelopes are rejected before they reach the solver.
+Rust then constructs the typed decision packet and supporting evidence.
+
+The current scope is a sampled tool-head path. It does not yet encode full-body
+geometry, kinematics, dynamics, braking distance, or perception uncertainty.
+Its guarantee also depends on complete mediation and accurate world state; an
+unobserved route to the actuator or a bad sensor value can invalidate the
+decision.
+
+This is a research prototype, not a safety-rated system or proof that a real
+trajectory is collision-free. The next steps are richer symbolic trajectory
+constraints, a real motion planner, and continuous runtime monitoring.
+
+---
+
+## Next Experiments
+
+The first results are encouraging, and this is an area we are actively exploring
+with partners around [OpenShell](https://github.com/NVIDIA/OpenShell), an
+Apache-2.0, community-driven project.
+
+Version one uses a Three.js workcell deliberately. Keeping the environment
+small let us focus on the policy contract and prover rather than simulator
+integration. The next parts of this research series will carry the boundary
+into NVIDIA Isaac Sim and MuJoCo, where we can test richer robot geometry,
+dynamics, perception, trajectory constraints, and changing world state before
+moving to a small physical platform.
+
+Physical perception also makes some policy predicates probabilistic rather than
+crisp. A human detector, distance estimate, occupancy map, or object classifier
+can be wrong, and their errors may be correlated. Recent work by Dvijotham and
+collaborators on
+[sound probabilistic verification for AI agents](https://arxiv.org/abs/2606.20510)
+offers an interesting direction for representing that uncertainty while
+retaining the deterministic envelope as a separate layer.
+
+Related research such as
+[ENPIRE](https://research.nvidia.com/labs/gear/enpire/) shows coding agents
+managing repeated real-world robot-policy improvement across reset,
+verification, rollout, and evolution. That raises a useful question for future
+experiments:
+
+> Can an agent optimize a robot policy while the governing invariants remain
+> outside the agent's control?
+
+We want to test that pattern with richer dynamics, uncertain perception, longer
+plans, and eventually real hardware.
+
+---
+
+## Research Questions and Collaboration
+
+We would like to study how formal specifications hold up under changing world
+state, whether constructive counterexamples improve replanning, and how an
+admitted contract can be carried reliably into a simulator or actuator.
+
+If you are working on SMT, temporal logic, runtime verification, control barrier
+functions, motion planning, digital twins, robot learning, or runtime assurance
+for agents, we would like to compare models and workloads. Some useful next
+experiments may come from connecting these communities rather than treating
+agent security and physical safety as separate problems.
+
+---
+
+## Run and Extend the Experiment
+
+The complete prototype, local setup, benchmark harness, machine-readable
+results, and Dev Note live together in the
+[OpenShell Research repository](https://github.com/NVIDIA/OpenShell-Research/tree/main/projects/robotics-policy-prover).
+The default fixture mode reproduces the interaction without model credentials;
+the optional agent mode can be used to explore different planners. We welcome
+new policy encodings, adversarial workloads, simulator adapters, and benchmark
+results through the normal OpenShell Research contribution process.
+
+Resources:
+
+1. [Robotics policy-prover project source](https://github.com/NVIDIA/OpenShell-Research/tree/main/projects/robotics-policy-prover)
+2. [Machine-readable DGX Spark benchmark results](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/robotics-policy-prover/benchmarks/policy-latency.json)
+3. [Machine-readable Terra planning-loop sample](https://github.com/NVIDIA/OpenShell-Research/blob/main/projects/robotics-policy-prover/benchmarks/terra-planning-loop.json)
+4. [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell)
+5. [ENPIRE: Agentic Robot Policy Self-Improvement in the Real World](https://research.nvidia.com/labs/gear/enpire/)
+6. [Z3 theorem prover](https://github.com/Z3Prover/z3)
+7. [Safe Reinforcement Learning via Shielding](https://ojs.aaai.org/index.php/AAAI/article/view/11797)
+8. [Safe Reinforcement Learning via Formal Methods: Toward Safe Control Through Proof and Learning](https://ojs.aaai.org/index.php/AAAI/article/view/12107)
+9. [Agent Behavioral Contracts](https://arxiv.org/abs/2602.22302)
+10. [Runtime Compliance Verification for AI Agents](https://arxiv.org/abs/2606.19242)
+11. [VeriGuard: Enhancing LLM Agent Safety via Verified Code Generation](https://research.google/pubs/veriguard-enhancing-llm-agent-safety-via-verified-code-generation/)
+12. [Efficient and Sound Probabilistic Verification for AI Agents](https://arxiv.org/abs/2606.20510)
diff --git a/docs/stylesheets/dev-notes.css b/docs/stylesheets/dev-notes.css
index ae1eea31..929e033e 100644
--- a/docs/stylesheets/dev-notes.css
+++ b/docs/stylesheets/dev-notes.css
@@ -1426,6 +1426,10 @@ body[data-md-color-scheme="slate"] .openshell-home-brand__dark {
aspect-ratio: 1200 / 630;
}
+.md-typeset .dev-note-figure--native-aspect img {
+ aspect-ratio: auto;
+}
+
.md-typeset .dev-note-figure figcaption {
margin: 0.7rem auto 0;
max-width: 42rem;
diff --git a/projects/robotics-policy-prover/.env.example b/projects/robotics-policy-prover/.env.example
new file mode 100644
index 00000000..bba09f40
--- /dev/null
+++ b/projects/robotics-policy-prover/.env.example
@@ -0,0 +1,15 @@
+# Required only for model-backed planning. Fixture mode needs no credentials.
+OPENAI_API_KEY=
+
+# The official OpenAI URL uses the Responses API. Other OpenAI-compatible URLs
+# use the /chat/completions endpoint.
+OPENAI_BASE_URL=https://api.openai.com/v1
+
+# Preferred model setting. If omitted, the service checks LLM_MODEL, then
+# FAST_MODEL, and finally defaults to gpt-4.1-mini.
+OPENAI_MODEL=gpt-4.1-mini
+# LLM_MODEL=
+# FAST_MODEL=
+
+# Optional. The secure default is 127.0.0.1:8787.
+# POLICY_PROVER_BIND_ADDR=127.0.0.1:8787
diff --git a/projects/robotics-policy-prover/.env.sample b/projects/robotics-policy-prover/.env.sample
new file mode 100644
index 00000000..bba09f40
--- /dev/null
+++ b/projects/robotics-policy-prover/.env.sample
@@ -0,0 +1,15 @@
+# Required only for model-backed planning. Fixture mode needs no credentials.
+OPENAI_API_KEY=
+
+# The official OpenAI URL uses the Responses API. Other OpenAI-compatible URLs
+# use the /chat/completions endpoint.
+OPENAI_BASE_URL=https://api.openai.com/v1
+
+# Preferred model setting. If omitted, the service checks LLM_MODEL, then
+# FAST_MODEL, and finally defaults to gpt-4.1-mini.
+OPENAI_MODEL=gpt-4.1-mini
+# LLM_MODEL=
+# FAST_MODEL=
+
+# Optional. The secure default is 127.0.0.1:8787.
+# POLICY_PROVER_BIND_ADDR=127.0.0.1:8787
diff --git a/projects/robotics-policy-prover/.gitignore b/projects/robotics-policy-prover/.gitignore
new file mode 100644
index 00000000..1d3ca718
--- /dev/null
+++ b/projects/robotics-policy-prover/.gitignore
@@ -0,0 +1,15 @@
+node_modules/
+dist/
+target/
+policy-prover-service/target/
+.env
+.env.*
+!.env.example
+!.env.sample
+.z3-trace
+**/.z3-trace
+.DS_Store
+coverage/
+playwright-report/
+test-results/
+screenshots/
diff --git a/projects/robotics-policy-prover/ARCHITECTURE.md b/projects/robotics-policy-prover/ARCHITECTURE.md
new file mode 100644
index 00000000..4b37a0ae
--- /dev/null
+++ b/projects/robotics-policy-prover/ARCHITECTURE.md
@@ -0,0 +1,40 @@
+# Robotics Policy-Prover Architecture
+
+This demo is intentionally shaped like a future OpenShell runtime integration,
+without depending on OpenShell internals yet.
+
+## Runtime Boundary
+
+The browser simulation plays the role of a physical actuator and operator view.
+The agent proposes `ActionEnvelope` values. The policy-prover service accepts
+those envelopes through `/api/decide`, runs bounded Z3 checks, and returns a
+`ProverDecision` contract:
+
+- `decision`: allow, deny, constrain, or require approval
+- `violations`: invariant ids that failed
+- `constraints`: narrower path, speed, or force bounds the executor must use
+- `obligations`: runtime monitor rules the executor must enforce
+- `counterexample`: proof-relevant coordinates for a denial
+- `solver_ms`: decision latency
+
+That boundary is the intended OpenShell transition point. In a later integration,
+the planner agent can run inside an OpenShell sandbox, submit action envelopes to
+an OpenShell-hosted policy-prover service, and receive the same decision contract before
+any MCP tool, A2A delegation, sandbox capability, or physical actuator command is
+allowed to execute.
+
+## Cadence
+
+Z3 is not a servo controller. The solver runs before each physical action or
+short-horizon segment. The executor then enforces returned obligations every sim
+tick. This mirrors an OpenShell runtime split:
+
+- agent runtime: propose/replan
+- policy prover: prove and return a contract
+- executor/proxy: enforce contract and stream audit events
+
+## Demo Surface
+
+The transcript is the audit stream. Each `agent_plan`, `prover_decision`,
+`execution_update`, and `world_event` is a future-compatible explanation surface
+for OpenShell operators and agents.
diff --git a/projects/robotics-policy-prover/LICENSE b/projects/robotics-policy-prover/LICENSE
new file mode 100644
index 00000000..261eeb9e
--- /dev/null
+++ b/projects/robotics-policy-prover/LICENSE
@@ -0,0 +1,201 @@
+ Apache License
+ Version 2.0, January 2004
+ http://www.apache.org/licenses/
+
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+ 1. Definitions.
+
+ "License" shall mean the terms and conditions for use, reproduction,
+ and distribution as defined by Sections 1 through 9 of this document.
+
+ "Licensor" shall mean the copyright owner or entity authorized by
+ the copyright owner that is granting the License.
+
+ "Legal Entity" shall mean the union of the acting entity and all
+ other entities that control, are controlled by, or are under common
+ control with that entity. For the purposes of this definition,
+ "control" means (i) the power, direct or indirect, to cause the
+ direction or management of such entity, whether by contract or
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
+ outstanding shares, or (iii) beneficial ownership of such entity.
+
+ "You" (or "Your") shall mean an individual or Legal Entity
+ exercising permissions granted by this License.
+
+ "Source" form shall mean the preferred form for making modifications,
+ including but not limited to software source code, documentation
+ source, and configuration files.
+
+ "Object" form shall mean any form resulting from mechanical
+ transformation or translation of a Source form, including but
+ not limited to compiled object code, generated documentation,
+ and conversions to other media types.
+
+ "Work" shall mean the work of authorship, whether in Source or
+ Object form, made available under the License, as indicated by a
+ copyright notice that is included in or attached to the work
+ (an example is provided in the Appendix below).
+
+ "Derivative Works" shall mean any work, whether in Source or Object
+ form, that is based on (or derived from) the Work and for which the
+ editorial revisions, annotations, elaborations, or other modifications
+ represent, as a whole, an original work of authorship. For the purposes
+ of this License, Derivative Works shall not include works that remain
+ separable from, or merely link (or bind by name) to the interfaces of,
+ the Work and Derivative Works thereof.
+
+ "Contribution" shall mean any work of authorship, including
+ the original version of the Work and any modifications or additions
+ to that Work or Derivative Works thereof, that is intentionally
+ submitted to Licensor for inclusion in the Work by the copyright owner
+ or by an individual or Legal Entity authorized to submit on behalf of
+ the copyright owner. For the purposes of this definition, "submitted"
+ means any form of electronic, verbal, or written communication sent
+ to the Licensor or its representatives, including but not limited to
+ communication on electronic mailing lists, source code control systems,
+ and issue tracking systems that are managed by, or on behalf of, the
+ Licensor for the purpose of discussing and improving the Work, but
+ excluding communication that is conspicuously marked or otherwise
+ designated in writing by the copyright owner as "Not a Contribution."
+
+ "Contributor" shall mean Licensor and any individual or Legal Entity
+ on behalf of whom a Contribution has been received by Licensor and
+ subsequently incorporated within the Work.
+
+ 2. Grant of Copyright License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ copyright license to reproduce, prepare Derivative Works of,
+ publicly display, publicly perform, sublicense, and distribute the
+ Work and such Derivative Works in Source or Object form.
+
+ 3. Grant of Patent License. Subject to the terms and conditions of
+ this License, each Contributor hereby grants to You a perpetual,
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+ (except as stated in this section) patent license to make, have made,
+ use, offer to sell, sell, import, and otherwise transfer the Work,
+ where such license applies only to those patent claims licensable
+ by such Contributor that are necessarily infringed by their
+ Contribution(s) alone or by combination of their Contribution(s)
+ with the Work to which such Contribution(s) was submitted. If You
+ institute patent litigation against any entity (including a
+ cross-claim or counterclaim in a lawsuit) alleging that the Work
+ or a Contribution incorporated within the Work constitutes direct
+ or contributory patent infringement, then any patent licenses
+ granted to You under this License for that Work shall terminate
+ as of the date such litigation is filed.
+
+ 4. Redistribution. You may reproduce and distribute copies of the
+ Work or Derivative Works thereof in any medium, with or without
+ modifications, and in Source or Object form, provided that You
+ meet the following conditions:
+
+ (a) You must give any other recipients of the Work or
+ Derivative Works a copy of this License; and
+
+ (b) You must cause any modified files to carry prominent notices
+ stating that You changed the files; and
+
+ (c) You must retain, in the Source form of any Derivative Works
+ that You distribute, all copyright, patent, trademark, and
+ attribution notices from the Source form of the Work,
+ excluding those notices that do not pertain to any part of
+ the Derivative Works; and
+
+ (d) If the Work includes a "NOTICE" text file as part of its
+ distribution, then any Derivative Works that You distribute must
+ include a readable copy of the attribution notices contained
+ within such NOTICE file, excluding those notices that do not
+ pertain to any part of the Derivative Works, in at least one
+ of the following places: within a NOTICE text file distributed
+ as part of the Derivative Works; within the Source form or
+ documentation, if provided along with the Derivative Works; or,
+ within a display generated by the Derivative Works, if and
+ wherever such third-party notices normally appear. The contents
+ of the NOTICE file are for informational purposes only and
+ do not modify the License. You may add Your own attribution
+ notices within Derivative Works that You distribute, alongside
+ or as an addendum to the NOTICE text from the Work, provided
+ that such additional attribution notices cannot be construed
+ as modifying the License.
+
+ You may add Your own copyright statement to Your modifications and
+ may provide additional or different license terms and conditions
+ for use, reproduction, or distribution of Your modifications, or
+ for any such Derivative Works as a whole, provided Your use,
+ reproduction, and distribution of the Work otherwise complies with
+ the conditions stated in this License.
+
+ 5. Submission of Contributions. Unless You explicitly state otherwise,
+ any Contribution intentionally submitted for inclusion in the Work
+ by You to the Licensor shall be under the terms and conditions of
+ this License, without any additional terms or conditions.
+ Notwithstanding the above, nothing herein shall supersede or modify
+ the terms of any separate license agreement you may have executed
+ with Licensor regarding such Contributions.
+
+ 6. Trademarks. This License does not grant permission to use the trade
+ names, trademarks, service marks, or product names of the Licensor,
+ except as required for reasonable and customary use in describing the
+ origin of the Work and reproducing the content of the NOTICE file.
+
+ 7. Disclaimer of Warranty. Unless required by applicable law or
+ agreed to in writing, Licensor provides the Work (and each
+ Contributor provides its Contributions) on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+ implied, including, without limitation, any warranties or conditions
+ of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+ PARTICULAR PURPOSE. You are solely responsible for determining the
+ appropriateness of using or redistributing the Work and assume any
+ risks associated with Your exercise of permissions under this License.
+
+ 8. Limitation of Liability. In no event and under no legal theory,
+ whether in tort (including negligence), contract, or otherwise,
+ unless required by applicable law (such as deliberate and grossly
+ negligent acts) or agreed to in writing, shall any Contributor be
+ liable to You for damages, including any direct, indirect, special,
+ incidental, or consequential damages of any character arising as a
+ result of this License or out of the use or inability to use the
+ Work (including but not limited to damages for loss of goodwill,
+ work stoppage, computer failure or malfunction, or any and all
+ other commercial damages or losses), even if such Contributor
+ has been advised of the possibility of such damages.
+
+ 9. Accepting Warranty or Additional Liability. While redistributing
+ the Work or Derivative Works thereof, You may choose to offer,
+ and charge a fee for, acceptance of support, warranty, indemnity,
+ or other liability obligations and/or rights consistent with this
+ License. However, in accepting such obligations, You may act only
+ on Your own behalf and on Your sole responsibility, not on behalf
+ of any other Contributor, and only if You agree to indemnify,
+ defend, and hold each Contributor harmless for any liability
+ incurred by, or claims asserted against, such Contributor by reason
+ of your accepting any such warranty or additional liability.
+
+ END OF TERMS AND CONDITIONS
+
+ APPENDIX: How to apply the Apache License to your work.
+
+ To apply the Apache License to your work, attach the following
+ boilerplate notice, with the fields enclosed by brackets "[]"
+ replaced with your own identifying information. (Don't include
+ the brackets!) The text should be enclosed in the appropriate
+ comment syntax for the file format. We also recommend that a
+ file or class name and description of purpose be included on the
+ same "printed page" as the copyright notice for easier
+ identification within third-party archives.
+
+ Copyright [yyyy] [name of copyright owner]
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
diff --git a/projects/robotics-policy-prover/README.md b/projects/robotics-policy-prover/README.md
new file mode 100644
index 00000000..522ac7f9
--- /dev/null
+++ b/projects/robotics-policy-prover/README.md
@@ -0,0 +1,101 @@
+# OpenShell Robotics Prover Demo
+
+This is a small robotics demo for showing how an AI agent and a formal policy prover can work together.
+
+For the experiment, architecture, and DGX Spark latency results, read the
+[Dev Note](../../docs/dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md).
+
+The scene is simple on purpose: an agent proposes a tool-head path to pick up the green block and move it into the blue tray. Before the robot can move, the policy-prover service checks the proposed path against hard rules:
+
+- the path must stay inside the workspace
+- the path must not enter the red restricted zone
+- nearby humans clamp the allowed speed
+- approved moves emit runtime obligations
+
+Rust derives concrete geometry and state facts, and Z3 selects exactly one
+policy outcome: allow, deny, allow with constraints, or require approval. If the
+path violates a rule, the service returns invariant ids, counterexample
+coordinates, constraints, obligations, and solver latency. The agent can use
+that packet to replan, but only an allowed outcome can reach the executor.
+
+The executor applies returned speed and force limits to the effective action and
+checks the human-distance pause obligation again immediately before motion.
+Malformed envelopes, solver failures, denials, and approval-required outcomes
+fail closed.
+
+The point of the experiment is to make the OpenShell runtime idea visible: an autonomous agent may plan freely, but every tool, delegation, or physical action can be checked against a policy-prover contract before execution.
+
+## Run It
+
+Install dependencies:
+
+```shell
+npm install
+```
+
+Start the policy-prover service:
+
+```shell
+npm run dev:prover
+```
+
+In another terminal, start the web app:
+
+```shell
+npm run dev -- --port 5173
+```
+
+Open:
+
+```shell
+http://localhost:5173/
+```
+
+Click **Run Experiment**.
+
+## Agent Modes
+
+The app can run in two modes:
+
+- **Fixture**: deterministic and repeatable; good for presenting the demo.
+- **OpenAI-compatible**: uses `OPENAI_API_KEY` and the configured model endpoint
+ from `.env`; good for showing real model variability. The event stream records
+ the exact model setting and request latency for each successful plan.
+
+Copy the sample env file if you want to use the OpenAI-compatible mode:
+
+```shell
+cp .env.sample .env
+```
+
+Then fill in your local values. Do not commit `.env`.
+
+## Test
+
+```shell
+cargo test --manifest-path policy-prover-service/Cargo.toml
+npm run build
+npm run verify:visual
+```
+
+## Benchmark the Policy Decision
+
+The benchmark calls the in-process policy decision directly in a release build.
+It covers allow, deny, and constrained outcomes at 3, 6, 12, 24, and 48
+waypoints. It intentionally excludes model inference, HTTP transport, rendering,
+and robot execution. This is an action-admission benchmark for agent-generated
+plans, not a benchmark for servo, balance, torque, or other low-level robot
+control loops.
+
+```shell
+POLICY_PROVER_BENCHMARK=1 \
+POLICY_PROVER_BENCHMARK_SAMPLES=5000 \
+POLICY_PROVER_BENCHMARK_WARMUP=1000 \
+POLICY_PROVER_BENCHMARK_OUTPUT=benchmarks/policy-latency.json \
+cargo run --release --manifest-path policy-prover-service/Cargo.toml
+```
+
+The command writes p50, p95, p99, mean, minimum, maximum, and throughput for
+each case. Add `POLICY_PROVER_BENCHMARK_PLATFORM` and
+`POLICY_PROVER_BENCHMARK_REVISION` when publishing results so the run has useful
+provenance.
diff --git a/projects/robotics-policy-prover/benchmarks/policy-latency.json b/projects/robotics-policy-prover/benchmarks/policy-latency.json
new file mode 100644
index 00000000..8ad8db14
--- /dev/null
+++ b/projects/robotics-policy-prover/benchmarks/policy-latency.json
@@ -0,0 +1,210 @@
+{
+ "schema_version": 1,
+ "generated_unix_seconds": 1786381013,
+ "platform": "NVIDIA DGX Spark (GB10, 20-core Arm), Ubuntu 24.04; power mode uncontrolled",
+ "rustc_version": "rustc 1.89.0",
+ "z3_crate_version": "0.19.15 (bundled Z3 4.16.0)",
+ "source_revision": "sha256:1ee3a1c5cfe3f1b7358ab3b69d0f12dd4ed8084f11ba90311c5054f0e4131a29",
+ "build_profile": "release",
+ "samples_per_case": 5000,
+ "warmup_per_case": 1000,
+ "z3_timeout_ms": 100,
+ "geometry_samples_per_segment": 33,
+ "cases": [
+ {
+ "outcome": "allow",
+ "waypoints": 3,
+ "segments": 2,
+ "samples": 5000,
+ "p50_ms": 0.770947,
+ "p95_ms": 1.232181,
+ "p99_ms": 1.2510290000000002,
+ "mean_ms": 0.8707483875999984,
+ "min_ms": 0.735587,
+ "max_ms": 1.4447100000000002,
+ "decisions_per_second": 1148.3439826394404
+ },
+ {
+ "outcome": "allow",
+ "waypoints": 6,
+ "segments": 5,
+ "samples": 5000,
+ "p50_ms": 0.862963,
+ "p95_ms": 1.242406,
+ "p99_ms": 1.280789,
+ "mean_ms": 0.9177644591999998,
+ "min_ms": 0.7382909999999999,
+ "max_ms": 1.4228699999999999,
+ "decisions_per_second": 1089.5182747942167
+ },
+ {
+ "outcome": "allow",
+ "waypoints": 12,
+ "segments": 11,
+ "samples": 5000,
+ "p50_ms": 0.868099,
+ "p95_ms": 1.2403099999999998,
+ "p99_ms": 1.279301,
+ "mean_ms": 0.9166326259999987,
+ "min_ms": 0.7364029999999999,
+ "max_ms": 1.398582,
+ "decisions_per_second": 1090.8630648179826
+ },
+ {
+ "outcome": "allow",
+ "waypoints": 24,
+ "segments": 23,
+ "samples": 5000,
+ "p50_ms": 0.9863409999999999,
+ "p95_ms": 1.244581,
+ "p99_ms": 1.3013180000000002,
+ "mean_ms": 0.9738059153999975,
+ "min_ms": 0.739156,
+ "max_ms": 1.432694,
+ "decisions_per_second": 1026.825085344885
+ },
+ {
+ "outcome": "allow",
+ "waypoints": 48,
+ "segments": 47,
+ "samples": 5000,
+ "p50_ms": 0.988708,
+ "p95_ms": 1.247973,
+ "p99_ms": 1.276646,
+ "mean_ms": 0.9903128707999985,
+ "min_ms": 0.739892,
+ "max_ms": 1.4373660000000001,
+ "decisions_per_second": 1009.7092959294253
+ },
+ {
+ "outcome": "deny",
+ "waypoints": 3,
+ "segments": 2,
+ "samples": 5000,
+ "p50_ms": 0.9839239999999999,
+ "p95_ms": 1.240213,
+ "p99_ms": 1.271749,
+ "mean_ms": 0.9587258604000006,
+ "min_ms": 0.736163,
+ "max_ms": 1.574167,
+ "decisions_per_second": 1042.9146304808373
+ },
+ {
+ "outcome": "deny",
+ "waypoints": 6,
+ "segments": 5,
+ "samples": 5000,
+ "p50_ms": 0.99362,
+ "p95_ms": 1.2492370000000002,
+ "p99_ms": 1.4515419999999999,
+ "mean_ms": 0.9991110019999998,
+ "min_ms": 0.7388359999999999,
+ "max_ms": 2.981629,
+ "decisions_per_second": 1000.7515097427845
+ },
+ {
+ "outcome": "deny",
+ "waypoints": 12,
+ "segments": 11,
+ "samples": 5000,
+ "p50_ms": 0.853364,
+ "p95_ms": 1.241622,
+ "p99_ms": 1.26271,
+ "mean_ms": 0.9166418470000017,
+ "min_ms": 0.739603,
+ "max_ms": 1.451431,
+ "decisions_per_second": 1090.7636984543485
+ },
+ {
+ "outcome": "deny",
+ "waypoints": 24,
+ "segments": 23,
+ "samples": 5000,
+ "p50_ms": 0.7513949999999999,
+ "p95_ms": 1.00874,
+ "p99_ms": 1.1879410000000001,
+ "mean_ms": 0.8078347539999984,
+ "min_ms": 0.735699,
+ "max_ms": 1.361014,
+ "decisions_per_second": 1237.6208711223312
+ },
+ {
+ "outcome": "deny",
+ "waypoints": 48,
+ "segments": 47,
+ "samples": 5000,
+ "p50_ms": 0.752003,
+ "p95_ms": 1.006069,
+ "p99_ms": 1.1888050000000001,
+ "mean_ms": 0.806967362799999,
+ "min_ms": 0.737667,
+ "max_ms": 1.4093980000000002,
+ "decisions_per_second": 1238.9619685719033
+ },
+ {
+ "outcome": "constrain",
+ "waypoints": 3,
+ "segments": 2,
+ "samples": 5000,
+ "p50_ms": 0.752227,
+ "p95_ms": 1.086293,
+ "p99_ms": 1.2501490000000002,
+ "mean_ms": 0.8216954929999982,
+ "min_ms": 0.73506,
+ "max_ms": 1.405718,
+ "decisions_per_second": 1216.782316098823
+ },
+ {
+ "outcome": "constrain",
+ "waypoints": 6,
+ "segments": 5,
+ "samples": 5000,
+ "p50_ms": 1.0015239999999999,
+ "p95_ms": 1.2484060000000001,
+ "p99_ms": 1.2727249999999999,
+ "mean_ms": 0.984540276999999,
+ "min_ms": 0.740499,
+ "max_ms": 1.5234779999999999,
+ "decisions_per_second": 1015.5512494146965
+ },
+ {
+ "outcome": "constrain",
+ "waypoints": 12,
+ "segments": 11,
+ "samples": 5000,
+ "p50_ms": 0.99778,
+ "p95_ms": 1.2504689999999998,
+ "p99_ms": 1.2745980000000001,
+ "mean_ms": 0.987746929999998,
+ "min_ms": 0.740803,
+ "max_ms": 1.510295,
+ "decisions_per_second": 1012.2630176093288
+ },
+ {
+ "outcome": "constrain",
+ "waypoints": 24,
+ "segments": 23,
+ "samples": 5000,
+ "p50_ms": 0.9850919999999999,
+ "p95_ms": 1.2458770000000001,
+ "p99_ms": 1.260486,
+ "mean_ms": 0.9455710729999997,
+ "min_ms": 0.7428680000000001,
+ "max_ms": 1.474999,
+ "decisions_per_second": 1057.409593498351
+ },
+ {
+ "outcome": "constrain",
+ "waypoints": 48,
+ "segments": 47,
+ "samples": 5000,
+ "p50_ms": 0.758036,
+ "p95_ms": 1.054388,
+ "p99_ms": 1.208918,
+ "mean_ms": 0.8354936192000018,
+ "min_ms": 0.7408990000000001,
+ "max_ms": 1.609767,
+ "decisions_per_second": 1196.6963110338188
+ }
+ ]
+}
diff --git a/projects/robotics-policy-prover/benchmarks/terra-planning-loop.json b/projects/robotics-policy-prover/benchmarks/terra-planning-loop.json
new file mode 100644
index 00000000..ff3bbb71
--- /dev/null
+++ b/projects/robotics-policy-prover/benchmarks/terra-planning-loop.json
@@ -0,0 +1,55 @@
+{
+ "schema_version": 1,
+ "date": "2026-08-10",
+ "description": "Ten complete browser-driven propose-check-replan-check-execute runs of the robotics policy-prover demo.",
+ "model": "openai/openai/gpt-5.6-terra",
+ "endpoint_type": "OpenAI-compatible chat completions",
+ "seed": 42,
+ "runs": 10,
+ "successful_runs": 10,
+ "fixture_fallbacks": 0,
+ "summary": {
+ "initial_inference_ms": {
+ "min": 2283,
+ "median": 2541.5,
+ "max": 10378,
+ "mean": 3434.3
+ },
+ "replan_inference_ms": {
+ "min": 4971,
+ "median": 6594,
+ "max": 10640,
+ "mean": 7532.8
+ },
+ "total_inference_ms": {
+ "min": 7534,
+ "median": 10262,
+ "max": 16901,
+ "mean": 10967.1
+ },
+ "total_policy_ms": {
+ "min": 5.76,
+ "median": 17.295,
+ "max": 26.23,
+ "mean": 15.394
+ },
+ "policy_share_of_inference_percent": {
+ "min": 0.0473,
+ "median": 0.1585,
+ "max": 0.264,
+ "mean": 0.1516
+ }
+ },
+ "measurements": [
+ { "run": 1, "initial_inference_ms": 10378, "replan_inference_ms": 6523, "deny_policy_ms": 11.75, "constrain_policy_ms": 3.15, "completed": true },
+ { "run": 2, "initial_inference_ms": 2520, "replan_inference_ms": 6556, "deny_policy_ms": 17.34, "constrain_policy_ms": 4.01, "completed": true },
+ { "run": 3, "initial_inference_ms": 2318, "replan_inference_ms": 10640, "deny_policy_ms": 3.91, "constrain_policy_ms": 22.32, "completed": true },
+ { "run": 4, "initial_inference_ms": 2563, "replan_inference_ms": 4971, "deny_policy_ms": 14.69, "constrain_policy_ms": 5.2, "completed": true },
+ { "run": 5, "initial_inference_ms": 2322, "replan_inference_ms": 9859, "deny_policy_ms": 2.88, "constrain_policy_ms": 2.88, "completed": true },
+ { "run": 6, "initial_inference_ms": 3071, "replan_inference_ms": 9165, "deny_policy_ms": 2.78, "constrain_policy_ms": 3.68, "completed": true },
+ { "run": 7, "initial_inference_ms": 2876, "replan_inference_ms": 6470, "deny_policy_ms": 8.83, "constrain_policy_ms": 14.59, "completed": true },
+ { "run": 8, "initial_inference_ms": 2434, "replan_inference_ms": 8506, "deny_policy_ms": 3.12, "constrain_policy_ms": 2.91, "completed": true },
+ { "run": 9, "initial_inference_ms": 3578, "replan_inference_ms": 6006, "deny_policy_ms": 16.62, "constrain_policy_ms": 3.07, "completed": true },
+ { "run": 10, "initial_inference_ms": 2283, "replan_inference_ms": 6632, "deny_policy_ms": 7.89, "constrain_policy_ms": 2.32, "completed": true }
+ ]
+}
diff --git a/projects/robotics-policy-prover/index.html b/projects/robotics-policy-prover/index.html
new file mode 100644
index 00000000..4733559e
--- /dev/null
+++ b/projects/robotics-policy-prover/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ OpenShell Robot Governor
+
+
+
+
+
+
diff --git a/projects/robotics-policy-prover/package-lock.json b/projects/robotics-policy-prover/package-lock.json
new file mode 100644
index 00000000..c449845b
--- /dev/null
+++ b/projects/robotics-policy-prover/package-lock.json
@@ -0,0 +1,1636 @@
+{
+ "name": "openshell-robotics-prover-demo",
+ "version": "0.1.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "openshell-robotics-prover-demo",
+ "version": "0.1.0",
+ "dependencies": {
+ "@react-three/drei": "^10.7.7",
+ "@react-three/fiber": "^9.6.1",
+ "lucide-react": "^1.17.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "three": "^0.184.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^6.0.2",
+ "playwright": "^1.60.0",
+ "vite": "^8.0.14"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.29.7",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz",
+ "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/@dimforge/rapier3d-compat": {
+ "version": "0.12.0",
+ "resolved": "https://registry.npmjs.org/@dimforge/rapier3d-compat/-/rapier3d-compat-0.12.0.tgz",
+ "integrity": "sha512-uekIGetywIgopfD97oDL5PfeezkFpNhwlzlaEYNOA0N6ghdsOvh/HYjSMek5Q2O1PYvRSDFcqFVJl4r4ZBwOow==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@mediapipe/tasks-vision": {
+ "version": "0.10.17",
+ "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.17.tgz",
+ "integrity": "sha512-CZWV/q6TTe8ta61cZXjfnnHsfWIdFhms03M9T7Cnd5y2mdpylJM0rF1qRq+wsQVRMLz1OYPVEBU9ph2Bx8cxrg==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/@monogrid/gainmap-js": {
+ "version": "3.4.0",
+ "resolved": "https://registry.npmjs.org/@monogrid/gainmap-js/-/gainmap-js-3.4.0.tgz",
+ "integrity": "sha512-2Z0FATFHaoYJ8b+Y4y4Hgfn3FRFwuU5zRrk+9dFWp4uGAdHGqVEdP7HP+gLA3X469KXHmfupJaUbKo1b/aDKIg==",
+ "license": "MIT",
+ "dependencies": {
+ "promise-worker-transferable": "^1.0.4"
+ },
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/@oxc-project/types": {
+ "version": "0.143.0",
+ "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.143.0.tgz",
+ "integrity": "sha512-u6JZdLBTLotrNC9Vd6vPssINdzcCzleKAH6EJKImQb7GtYvX5keN2dxkoK44stCc4tffE6QQRtZTXVSzsLUlWA==",
+ "dev": true,
+ "license": "MIT",
+ "funding": {
+ "url": "https://github.com/sponsors/Boshen"
+ }
+ },
+ "node_modules/@react-three/drei": {
+ "version": "10.7.7",
+ "resolved": "https://registry.npmjs.org/@react-three/drei/-/drei-10.7.7.tgz",
+ "integrity": "sha512-ff+J5iloR0k4tC++QtD/j9u3w5fzfgFAWDtAGQah9pF2B1YgOq/5JxqY0/aVoQG5r3xSZz0cv5tk2YuBob4xEQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.26.0",
+ "@mediapipe/tasks-vision": "0.10.17",
+ "@monogrid/gainmap-js": "^3.0.6",
+ "@use-gesture/react": "^10.3.1",
+ "camera-controls": "^3.1.0",
+ "cross-env": "^7.0.3",
+ "detect-gpu": "^5.0.56",
+ "glsl-noise": "^0.0.0",
+ "hls.js": "^1.5.17",
+ "maath": "^0.10.8",
+ "meshline": "^3.3.1",
+ "stats-gl": "^2.2.8",
+ "stats.js": "^0.17.0",
+ "suspend-react": "^0.1.3",
+ "three-mesh-bvh": "^0.8.3",
+ "three-stdlib": "^2.35.6",
+ "troika-three-text": "^0.52.4",
+ "tunnel-rat": "^0.1.2",
+ "use-sync-external-store": "^1.4.0",
+ "utility-types": "^3.11.0",
+ "zustand": "^5.0.1"
+ },
+ "peerDependencies": {
+ "@react-three/fiber": "^9.0.0",
+ "react": "^19",
+ "react-dom": "^19",
+ "three": ">=0.159"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@react-three/fiber": {
+ "version": "9.6.1",
+ "resolved": "https://registry.npmjs.org/@react-three/fiber/-/fiber-9.6.1.tgz",
+ "integrity": "sha512-zF0rsKcVYpcJwbFEnv2HkHX9cvOEgsfQo/X8lwmR2dn13S4qEQJXir9fxf5js2LQFoXqxOY7MDkOkYx2uZ4gSg==",
+ "license": "MIT",
+ "dependencies": {
+ "@babel/runtime": "^7.17.8",
+ "@types/webxr": "*",
+ "base64-js": "^1.5.1",
+ "buffer": "^6.0.3",
+ "its-fine": "^2.0.0",
+ "react-use-measure": "^2.1.7",
+ "scheduler": "^0.27.0",
+ "suspend-react": "^0.1.3",
+ "use-sync-external-store": "^1.4.0",
+ "zustand": "^5.0.3"
+ },
+ "peerDependencies": {
+ "expo": ">=43.0",
+ "expo-asset": ">=8.4",
+ "expo-file-system": ">=11.0",
+ "expo-gl": ">=11.0",
+ "react": ">=19 <19.3",
+ "react-dom": ">=19 <19.3",
+ "react-native": ">=0.78",
+ "three": ">=0.156"
+ },
+ "peerDependenciesMeta": {
+ "expo": {
+ "optional": true
+ },
+ "expo-asset": {
+ "optional": true
+ },
+ "expo-file-system": {
+ "optional": true
+ },
+ "expo-gl": {
+ "optional": true
+ },
+ "react-dom": {
+ "optional": true
+ },
+ "react-native": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/@rolldown/binding-android-arm64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.3.tgz",
+ "integrity": "sha512-zrJtHDcaZJ1Fp7xf4hNl+7seH9Cn/N5TwLYkhgXREtBwAd/jaqW3uqeHxpDugJLVICWg4eW44kOQEGJ1r6jCGw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-arm64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.3.tgz",
+ "integrity": "sha512-ieIiibVCp0tX7TLu2cafoNPv8wJyYi01ekXpbf8q2j7F4rGAhhXb/eQh7ge9DRBY78GwmRQtvjZDux7EDbA8kA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-darwin-x64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.3.tgz",
+ "integrity": "sha512-Zh9tCon19eDXJoihx0rqKhMUlMYqzwj3aPsSuHmI4RWZh62dWUL+DJN4C5YQya5TcQBJU/Fe8+rY0jhXTQITqA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-freebsd-x64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.3.tgz",
+ "integrity": "sha512-nGbJWewA1wrXXZiQhjAT5rhibGfns5ZNkDVqxsO6zJ3f3YvpoDNNmGMSbbhLuXKjNScaBJVOAboztAWVespQMg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm-gnueabihf": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.3.tgz",
+ "integrity": "sha512-QNniJr5Kml0kDEB98jiDOJjXNroxIIi0IXIbdYzY26Xt1pVbeP62+KnoIZLwirOymX/0jDk/2gI/bNUv7A7OIw==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-gnu": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.3.tgz",
+ "integrity": "sha512-TkqEAcmmvH3I/q4114NB4RVt6241Dao48pF45uLcFGrwAaIn0iITgTAKP/dLjbN0R4buJjGb91+UHSoFmpgIWw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-arm64-musl": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.3.tgz",
+ "integrity": "sha512-NHqjnxpsndf4MPymxteFAWHHfkTL8HjWh1KB7z23ofZ6QO2euONuxDXjat69dKZRALnGypg8k8SsK8vZJoXv1Q==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-ppc64-gnu": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.3.tgz",
+ "integrity": "sha512-6tbrbwfz5GB9DQ4Jwo6hy9v+vR31xZlvzZ6n5Xut6Hhx5PvrA9q/HsK8KMaYQp063iqZGXwNvZtYNLD7EM/x0w==",
+ "cpu": [
+ "ppc64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-s390x-gnu": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.3.tgz",
+ "integrity": "sha512-oyuXxXmoZHjXC917IAPFAAv4wWAa0cM9afk8nx1+9/jNNOX1uPf8yDA6p7G0RypOfw/X0PQt5IfoquY1um+zSg==",
+ "cpu": [
+ "s390x"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-gnu": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.3.tgz",
+ "integrity": "sha512-TytMwF2KVGqP2tgd0I1OY0PAv78dZRAYcF5ssDzjM34SUXCED3uXvSd5+lHoC0bTD6eEdFz7LdQNCO1y0oVk9w==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-linux-x64-musl": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.3.tgz",
+ "integrity": "sha512-/E9m3qstrJFVPoULV25mVQblSNExY2+kBsYe4sy0Tn0yOOgJ8wZbZt3KnRbF/XeU2Gl1STKUQnDNTqhIE5MD4A==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-openharmony-arm64": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.3.tgz",
+ "integrity": "sha512-Kr0OcsoQI816i6HOl3vFHpd1K0eZyh76zgfj4c1nTyaTsd5r2Mj1lwM4R90y/qaCfmTn9eHy0SKwi98eitRxug==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "openharmony"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-arm64-msvc": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.3.tgz",
+ "integrity": "sha512-hOtMwTqnME+/gJcH/PCZ0wn0zPUjiWOgkHpxbSJpfGKMezHltx1S7/k1SitzVa7Ww2cqrDDaFbZEhcJZO8o+Jw==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/binding-win32-x64-msvc": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.3.tgz",
+ "integrity": "sha512-ekcqMMkI2PlhYnfzQnB/cEdYUVVJViWvoUyLrbzgDoi3Snfc1mVBwdnc306ufA5ejy8JSPjT2RlW1nQSjW7efg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ }
+ },
+ "node_modules/@rolldown/pluginutils": {
+ "version": "1.0.1",
+ "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz",
+ "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@tweenjs/tween.js": {
+ "version": "23.1.3",
+ "resolved": "https://registry.npmjs.org/@tweenjs/tween.js/-/tween.js-23.1.3.tgz",
+ "integrity": "sha512-vJmvvwFxYuGnF2axRtPYocag6Clbb5YS7kLL+SO/TeVFzHqDIWrNKYtcsPMibjDx9O+bu+psAy9NKfWklassUA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/draco3d": {
+ "version": "1.4.10",
+ "resolved": "https://registry.npmjs.org/@types/draco3d/-/draco3d-1.4.10.tgz",
+ "integrity": "sha512-AX22jp8Y7wwaBgAixaSvkoG4M/+PlAcm3Qs4OW8yT9DM4xUpWKeFhLueTAyZF39pviAdcDdeJoACapiAceqNcw==",
+ "license": "MIT"
+ },
+ "node_modules/@types/offscreencanvas": {
+ "version": "2019.7.3",
+ "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
+ "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A==",
+ "license": "MIT"
+ },
+ "node_modules/@types/react": {
+ "version": "19.2.15",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.15.tgz",
+ "integrity": "sha512-eRwcGNHve+E8qtEQSSRl6urh+rFop4v8gm6O8rGv25CodbvFdLjA1vVQ1KkiFE0w0UPOnb8tDiFKL5lp0rtY5Q==",
+ "license": "MIT",
+ "peer": true,
+ "dependencies": {
+ "csstype": "^3.2.2"
+ }
+ },
+ "node_modules/@types/react-reconciler": {
+ "version": "0.28.9",
+ "resolved": "https://registry.npmjs.org/@types/react-reconciler/-/react-reconciler-0.28.9.tgz",
+ "integrity": "sha512-HHM3nxyUZ3zAylX8ZEyrDNd2XZOnQ0D5XfunJF5FLQnZbHHYq4UWvW1QfelQNXv1ICNkwYhfxjwfnqivYB6bFg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/react": "*"
+ }
+ },
+ "node_modules/@types/stats.js": {
+ "version": "0.17.4",
+ "resolved": "https://registry.npmjs.org/@types/stats.js/-/stats.js-0.17.4.tgz",
+ "integrity": "sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==",
+ "license": "MIT"
+ },
+ "node_modules/@types/three": {
+ "version": "0.184.1",
+ "resolved": "https://registry.npmjs.org/@types/three/-/three-0.184.1.tgz",
+ "integrity": "sha512-6q4VdiqVsrTRqmk62/BnlcAvIrnDM0zf2ZDVKI5kZiniWrSaOHaQzmbp+BNzoggc/8tgW412pL//wZIxu2PPTA==",
+ "license": "MIT",
+ "dependencies": {
+ "@dimforge/rapier3d-compat": "~0.12.0",
+ "@tweenjs/tween.js": "~23.1.3",
+ "@types/stats.js": "*",
+ "@types/webxr": ">=0.5.17",
+ "fflate": "~0.8.2",
+ "meshoptimizer": "~1.1.1"
+ }
+ },
+ "node_modules/@types/webxr": {
+ "version": "0.5.24",
+ "resolved": "https://registry.npmjs.org/@types/webxr/-/webxr-0.5.24.tgz",
+ "integrity": "sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/core": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/core/-/core-10.3.1.tgz",
+ "integrity": "sha512-WcINiDt8WjqBdUXye25anHiNxPc0VOrlT8F6LLkU6cycrOGUDyY/yyFmsg3k8i5OLvv25llc0QC45GhR/C8llw==",
+ "license": "MIT"
+ },
+ "node_modules/@use-gesture/react": {
+ "version": "10.3.1",
+ "resolved": "https://registry.npmjs.org/@use-gesture/react/-/react-10.3.1.tgz",
+ "integrity": "sha512-Yy19y6O2GJq8f7CHf7L0nxL8bf4PZCPaVOCgJrusOeFHY1LvHgYXnmnXg6N5iwAnbgbZCDjo60SiM6IPJi9C5g==",
+ "license": "MIT",
+ "dependencies": {
+ "@use-gesture/core": "10.3.1"
+ },
+ "peerDependencies": {
+ "react": ">= 16.8.0"
+ }
+ },
+ "node_modules/@vitejs/plugin-react": {
+ "version": "6.0.2",
+ "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-6.0.2.tgz",
+ "integrity": "sha512-DlSMqo4WhThw4vB8Mpn0Woe9J+Jfq1geJ61AKW0QEgLzGMNwtIMdxbDUzLxcun8W7NbJO0e2Jg/Nxm3cCSVzzg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "peerDependencies": {
+ "@rolldown/plugin-babel": "^0.1.7 || ^0.2.0",
+ "babel-plugin-react-compiler": "^1.0.0",
+ "vite": "^8.0.0"
+ },
+ "peerDependenciesMeta": {
+ "@rolldown/plugin-babel": {
+ "optional": true
+ },
+ "babel-plugin-react-compiler": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/base64-js": {
+ "version": "1.5.1",
+ "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
+ "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT"
+ },
+ "node_modules/bidi-js": {
+ "version": "1.0.3",
+ "resolved": "https://registry.npmjs.org/bidi-js/-/bidi-js-1.0.3.tgz",
+ "integrity": "sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==",
+ "license": "MIT",
+ "dependencies": {
+ "require-from-string": "^2.0.2"
+ }
+ },
+ "node_modules/buffer": {
+ "version": "6.0.3",
+ "resolved": "https://registry.npmjs.org/buffer/-/buffer-6.0.3.tgz",
+ "integrity": "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "base64-js": "^1.3.1",
+ "ieee754": "^1.2.1"
+ }
+ },
+ "node_modules/camera-controls": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/camera-controls/-/camera-controls-3.1.2.tgz",
+ "integrity": "sha512-xkxfpG2ECZ6Ww5/9+kf4mfg1VEYAoe9aDSY+IwF0UEs7qEzwy0aVRfs2grImIECs/PoBtWFrh7RXsQkwG922JA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=22.0.0",
+ "npm": ">=10.5.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.126.1"
+ }
+ },
+ "node_modules/cross-env": {
+ "version": "7.0.3",
+ "resolved": "https://registry.npmjs.org/cross-env/-/cross-env-7.0.3.tgz",
+ "integrity": "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==",
+ "license": "MIT",
+ "dependencies": {
+ "cross-spawn": "^7.0.1"
+ },
+ "bin": {
+ "cross-env": "src/bin/cross-env.js",
+ "cross-env-shell": "src/bin/cross-env-shell.js"
+ },
+ "engines": {
+ "node": ">=10.14",
+ "npm": ">=6",
+ "yarn": ">=1"
+ }
+ },
+ "node_modules/cross-spawn": {
+ "version": "7.0.6",
+ "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
+ "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==",
+ "license": "MIT",
+ "dependencies": {
+ "path-key": "^3.1.0",
+ "shebang-command": "^2.0.0",
+ "which": "^2.0.1"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/csstype": {
+ "version": "3.2.3",
+ "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz",
+ "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==",
+ "license": "MIT",
+ "peer": true
+ },
+ "node_modules/detect-gpu": {
+ "version": "5.0.70",
+ "resolved": "https://registry.npmjs.org/detect-gpu/-/detect-gpu-5.0.70.tgz",
+ "integrity": "sha512-bqerEP1Ese6nt3rFkwPnGbsUF9a4q+gMmpTVVOEzoCyeCc+y7/RvJnQZJx1JwhgQI5Ntg0Kgat8Uu7XpBqnz1w==",
+ "license": "MIT",
+ "dependencies": {
+ "webgl-constants": "^1.1.1"
+ }
+ },
+ "node_modules/detect-libc": {
+ "version": "2.1.2",
+ "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz",
+ "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/draco3d": {
+ "version": "1.5.7",
+ "resolved": "https://registry.npmjs.org/draco3d/-/draco3d-1.5.7.tgz",
+ "integrity": "sha512-m6WCKt/erDXcw+70IJXnG7M3awwQPAsZvJGX5zY7beBqpELw6RDGkYVU0W43AFxye4pDZ5i2Lbyc/NNGqwjUVQ==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/fdir": {
+ "version": "6.5.0",
+ "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz",
+ "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "peerDependencies": {
+ "picomatch": "^3 || ^4"
+ },
+ "peerDependenciesMeta": {
+ "picomatch": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/fflate": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.8.3.tgz",
+ "integrity": "sha512-tbZNuJrLwGUp3zshBtdy4W+ORxZuIh8a5ilyIEQDC5rY1f3U20JMry0Ll3WBzU58EZKsEuJFXhb5gwv8CsPvgA==",
+ "license": "MIT"
+ },
+ "node_modules/fsevents": {
+ "version": "2.3.2",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
+ "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/glsl-noise": {
+ "version": "0.0.0",
+ "resolved": "https://registry.npmjs.org/glsl-noise/-/glsl-noise-0.0.0.tgz",
+ "integrity": "sha512-b/ZCF6amfAUb7dJM/MxRs7AetQEahYzJ8PtgfrmEdtw6uyGOr+ZSGtgjFm6mfsBkxJ4d2W7kg+Nlqzqvn3Bc0w==",
+ "license": "MIT"
+ },
+ "node_modules/hls.js": {
+ "version": "1.6.16",
+ "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.6.16.tgz",
+ "integrity": "sha512-VSIRpLfRwlAAdGL4wiTucx2ScRipo0ed1FBatWkyt832jC4CReKstga6yIhYVwGu9LOBjuX9wzmRMeQdBJtzEA==",
+ "license": "Apache-2.0"
+ },
+ "node_modules/ieee754": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz",
+ "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==",
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/feross"
+ },
+ {
+ "type": "patreon",
+ "url": "https://www.patreon.com/feross"
+ },
+ {
+ "type": "consulting",
+ "url": "https://feross.org/support"
+ }
+ ],
+ "license": "BSD-3-Clause"
+ },
+ "node_modules/immediate": {
+ "version": "3.0.6",
+ "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz",
+ "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==",
+ "license": "MIT"
+ },
+ "node_modules/is-promise": {
+ "version": "2.2.2",
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-2.2.2.tgz",
+ "integrity": "sha512-+lP4/6lKUBfQjZ2pdxThZvLUAafmZb8OAxFb8XXtiQmS35INgr85hdOGoEs124ez1FCnZJt6jau/T+alh58QFQ==",
+ "license": "MIT"
+ },
+ "node_modules/isexe": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
+ "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==",
+ "license": "ISC"
+ },
+ "node_modules/its-fine": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/its-fine/-/its-fine-2.0.0.tgz",
+ "integrity": "sha512-KLViCmWx94zOvpLwSlsx6yOCeMhZYaxrJV87Po5k/FoZzcPSahvK5qJ7fYhS61sZi5ikmh2S3Hz55A2l3U69ng==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/react-reconciler": "^0.28.9"
+ },
+ "peerDependencies": {
+ "react": "^19.0.0"
+ }
+ },
+ "node_modules/lie": {
+ "version": "3.3.0",
+ "resolved": "https://registry.npmjs.org/lie/-/lie-3.3.0.tgz",
+ "integrity": "sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==",
+ "license": "MIT",
+ "dependencies": {
+ "immediate": "~3.0.5"
+ }
+ },
+ "node_modules/lightningcss": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz",
+ "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==",
+ "dev": true,
+ "license": "MPL-2.0",
+ "dependencies": {
+ "detect-libc": "^2.0.3"
+ },
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ },
+ "optionalDependencies": {
+ "lightningcss-android-arm64": "1.33.0",
+ "lightningcss-darwin-arm64": "1.33.0",
+ "lightningcss-darwin-x64": "1.33.0",
+ "lightningcss-freebsd-x64": "1.33.0",
+ "lightningcss-linux-arm-gnueabihf": "1.33.0",
+ "lightningcss-linux-arm64-gnu": "1.33.0",
+ "lightningcss-linux-arm64-musl": "1.33.0",
+ "lightningcss-linux-x64-gnu": "1.33.0",
+ "lightningcss-linux-x64-musl": "1.33.0",
+ "lightningcss-win32-arm64-msvc": "1.33.0",
+ "lightningcss-win32-x64-msvc": "1.33.0"
+ }
+ },
+ "node_modules/lightningcss-android-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz",
+ "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "android"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-arm64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz",
+ "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-darwin-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz",
+ "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-freebsd-x64": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz",
+ "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "freebsd"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm-gnueabihf": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz",
+ "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==",
+ "cpu": [
+ "arm"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz",
+ "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-arm64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz",
+ "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-gnu": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz",
+ "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "glibc"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-linux-x64-musl": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz",
+ "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "libc": [
+ "musl"
+ ],
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "linux"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-arm64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz",
+ "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==",
+ "cpu": [
+ "arm64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lightningcss-win32-x64-msvc": {
+ "version": "1.33.0",
+ "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz",
+ "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==",
+ "cpu": [
+ "x64"
+ ],
+ "dev": true,
+ "license": "MPL-2.0",
+ "optional": true,
+ "os": [
+ "win32"
+ ],
+ "engines": {
+ "node": ">= 12.0.0"
+ },
+ "funding": {
+ "type": "opencollective",
+ "url": "https://opencollective.com/parcel"
+ }
+ },
+ "node_modules/lucide-react": {
+ "version": "1.17.0",
+ "resolved": "https://registry.npmjs.org/lucide-react/-/lucide-react-1.17.0.tgz",
+ "integrity": "sha512-9FA9evdox/JQL5PT57fdA1x/yg8T7knJ98+zjTL3UfKza6pflQUUh3XtaQIHKvnsJw1lmsEyHVlt5jchYxOQ5w==",
+ "license": "ISC",
+ "peerDependencies": {
+ "react": "^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/maath": {
+ "version": "0.10.8",
+ "resolved": "https://registry.npmjs.org/maath/-/maath-0.10.8.tgz",
+ "integrity": "sha512-tRvbDF0Pgqz+9XUa4jjfgAQ8/aPKmQdWXilFu2tMy4GWj4NOsx99HlULO4IeREfbO3a0sA145DZYyvXPkybm0g==",
+ "license": "MIT",
+ "peerDependencies": {
+ "@types/three": ">=0.134.0",
+ "three": ">=0.134.0"
+ }
+ },
+ "node_modules/meshline": {
+ "version": "3.3.1",
+ "resolved": "https://registry.npmjs.org/meshline/-/meshline-3.3.1.tgz",
+ "integrity": "sha512-/TQj+JdZkeSUOl5Mk2J7eLcYTLiQm2IDzmlSvYm7ov15anEcDJ92GHqqazxTSreeNgfnYu24kiEvvv0WlbCdFQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.137"
+ }
+ },
+ "node_modules/meshoptimizer": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/meshoptimizer/-/meshoptimizer-1.1.1.tgz",
+ "integrity": "sha512-oRFNWJRDA/WTrVj7NWvqa5HqE1t9MYDj2VaWirQCzCCrAd2GHrqR/sQezCxiWATPNlKTcRaPRHPJwIRoPBAp5g==",
+ "license": "MIT"
+ },
+ "node_modules/nanoid": {
+ "version": "3.3.18",
+ "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
+ "integrity": "sha512-DTg4MJbGMWkfi6VZFdNt2/caMbQy4Ou+Op/hJQvGEWcnVfoA1QA+xzRKAzw9jD6+GVOOeYr/mIcuDSdug6F6+w==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "bin": {
+ "nanoid": "bin/nanoid.cjs"
+ },
+ "engines": {
+ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
+ }
+ },
+ "node_modules/path-key": {
+ "version": "3.1.1",
+ "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz",
+ "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/picocolors": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+ "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
+ "dev": true,
+ "license": "ISC"
+ },
+ "node_modules/picomatch": {
+ "version": "4.0.5",
+ "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.5.tgz",
+ "integrity": "sha512-RvwwcruNjI1ncT5xRakeyS9Lf8lcItv34KD+aif+VH9kduAyfYBipGh12274xtenIPZ119/R9BdTBa8gAwSh0A==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=12"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/jonschlinkert"
+ }
+ },
+ "node_modules/playwright": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.60.0.tgz",
+ "integrity": "sha512-hheHdokM8cdqCb0lcE3s+zT4t4W+vvjpGxsZlDnikarzx8tSzMebh3UiFtgqwFwnTnjYQcsyMF8ei2mCO/tpeA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "dependencies": {
+ "playwright-core": "1.60.0"
+ },
+ "bin": {
+ "playwright": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ },
+ "optionalDependencies": {
+ "fsevents": "2.3.2"
+ }
+ },
+ "node_modules/playwright-core": {
+ "version": "1.60.0",
+ "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.60.0.tgz",
+ "integrity": "sha512-9bW6zvX/m0lEbgTKJ6YppOKx8H3VOPBMOCFh2irXFOT4BbHgrx5hPjwJYLT40Lu+4qtD36qKc/Hn56StUW57IA==",
+ "dev": true,
+ "license": "Apache-2.0",
+ "bin": {
+ "playwright-core": "cli.js"
+ },
+ "engines": {
+ "node": ">=18"
+ }
+ },
+ "node_modules/postcss": {
+ "version": "8.5.26",
+ "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.26.tgz",
+ "integrity": "sha512-u82N74LFzG8ca+dD8puPnplTXoGH4fTPpVGuIbt36G3qvNlkvfD0lEAZSxaly3KX8TS/L1A1gsCEmvKmBcVbkQ==",
+ "dev": true,
+ "funding": [
+ {
+ "type": "opencollective",
+ "url": "https://opencollective.com/postcss/"
+ },
+ {
+ "type": "tidelift",
+ "url": "https://tidelift.com/funding/github/npm/postcss"
+ },
+ {
+ "type": "github",
+ "url": "https://github.com/sponsors/ai"
+ }
+ ],
+ "license": "MIT",
+ "dependencies": {
+ "nanoid": "^3.3.17",
+ "picocolors": "^1.1.1",
+ "source-map-js": "^1.2.1"
+ },
+ "engines": {
+ "node": "^10 || ^12 || >=14"
+ }
+ },
+ "node_modules/potpack": {
+ "version": "1.0.2",
+ "resolved": "https://registry.npmjs.org/potpack/-/potpack-1.0.2.tgz",
+ "integrity": "sha512-choctRBIV9EMT9WGAZHn3V7t0Z2pMQyl0EZE6pFc/6ml3ssw7Dlf/oAOvFwjm1HVsqfQN8GfeFyJ+d8tRzqueQ==",
+ "license": "ISC"
+ },
+ "node_modules/promise-worker-transferable": {
+ "version": "1.0.4",
+ "resolved": "https://registry.npmjs.org/promise-worker-transferable/-/promise-worker-transferable-1.0.4.tgz",
+ "integrity": "sha512-bN+0ehEnrXfxV2ZQvU2PetO0n4gqBD4ulq3MI1WOPLgr7/Mg9yRQkX5+0v1vagr74ZTsl7XtzlaYDo2EuCeYJw==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "is-promise": "^2.1.0",
+ "lie": "^3.0.2"
+ }
+ },
+ "node_modules/react": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.6.tgz",
+ "integrity": "sha512-sfWGGfavi0xr8Pg0sVsyHMAOziVYKgPLNrS7ig+ivMNb3wbCBw3KxtflsGBAwD3gYQlE/AEZsTLgToRrSCjb0Q==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/react-dom": {
+ "version": "19.2.6",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.6.tgz",
+ "integrity": "sha512-0prMI+hvBbPjsWnxDLxlCGyM8PN6UuWjEUCYmZhO67xIV9Xasa/r/vDnq+Xyq4Lo27g8QSbO5YzARu0D1Sps3g==",
+ "license": "MIT",
+ "dependencies": {
+ "scheduler": "^0.27.0"
+ },
+ "peerDependencies": {
+ "react": "^19.2.6"
+ }
+ },
+ "node_modules/react-use-measure": {
+ "version": "2.1.7",
+ "resolved": "https://registry.npmjs.org/react-use-measure/-/react-use-measure-2.1.7.tgz",
+ "integrity": "sha512-KrvcAo13I/60HpwGO5jpW7E9DfusKyLPLvuHlUyP5zqnmAPhNc6qTRjUQrdTADl0lpPpDVU2/Gg51UlOGHXbdg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=16.13",
+ "react-dom": ">=16.13"
+ },
+ "peerDependenciesMeta": {
+ "react-dom": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/require-from-string": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
+ "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/rolldown": {
+ "version": "1.2.3",
+ "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.3.tgz",
+ "integrity": "sha512-rn9wpmxplLf7NLNyCk9FyWh3FM43DbY8jOzCdEPzH7uflhTftRbCEpqi6Ly2osgoU8OwObtmavMbWLaWy4LX7A==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@oxc-project/types": "=0.143.0",
+ "@rolldown/pluginutils": "^1.0.0"
+ },
+ "bin": {
+ "rolldown": "bin/cli.mjs"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "optionalDependencies": {
+ "@rolldown/binding-android-arm64": "1.2.3",
+ "@rolldown/binding-darwin-arm64": "1.2.3",
+ "@rolldown/binding-darwin-x64": "1.2.3",
+ "@rolldown/binding-freebsd-x64": "1.2.3",
+ "@rolldown/binding-linux-arm-gnueabihf": "1.2.3",
+ "@rolldown/binding-linux-arm64-gnu": "1.2.3",
+ "@rolldown/binding-linux-arm64-musl": "1.2.3",
+ "@rolldown/binding-linux-ppc64-gnu": "1.2.3",
+ "@rolldown/binding-linux-s390x-gnu": "1.2.3",
+ "@rolldown/binding-linux-x64-gnu": "1.2.3",
+ "@rolldown/binding-linux-x64-musl": "1.2.3",
+ "@rolldown/binding-openharmony-arm64": "1.2.3",
+ "@rolldown/binding-win32-arm64-msvc": "1.2.3",
+ "@rolldown/binding-win32-x64-msvc": "1.2.3"
+ }
+ },
+ "node_modules/scheduler": {
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
+ },
+ "node_modules/shebang-command": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
+ "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==",
+ "license": "MIT",
+ "dependencies": {
+ "shebang-regex": "^3.0.0"
+ },
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/shebang-regex": {
+ "version": "3.0.0",
+ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz",
+ "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=8"
+ }
+ },
+ "node_modules/source-map-js": {
+ "version": "1.2.1",
+ "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
+ "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/stats-gl": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/stats-gl/-/stats-gl-2.4.2.tgz",
+ "integrity": "sha512-g5O9B0hm9CvnM36+v7SFl39T7hmAlv541tU81ME8YeSb3i1CIP5/QdDeSB3A0la0bKNHpxpwxOVRo2wFTYEosQ==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/three": "*",
+ "three": "^0.170.0"
+ },
+ "peerDependencies": {
+ "@types/three": "*",
+ "three": "*"
+ }
+ },
+ "node_modules/stats-gl/node_modules/three": {
+ "version": "0.170.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.170.0.tgz",
+ "integrity": "sha512-FQK+LEpYc0fBD+J8g6oSEyyNzjp+Q7Ks1C568WWaoMRLW+TkNNWmenWeGgJjV105Gd+p/2ql1ZcjYvNiPZBhuQ==",
+ "license": "MIT"
+ },
+ "node_modules/stats.js": {
+ "version": "0.17.0",
+ "resolved": "https://registry.npmjs.org/stats.js/-/stats.js-0.17.0.tgz",
+ "integrity": "sha512-hNKz8phvYLPEcRkeG1rsGmV5ChMjKDAWU7/OJJdDErPBNChQXxCo3WZurGpnWc6gZhAzEPFad1aVgyOANH1sMw==",
+ "license": "MIT"
+ },
+ "node_modules/suspend-react": {
+ "version": "0.1.3",
+ "resolved": "https://registry.npmjs.org/suspend-react/-/suspend-react-0.1.3.tgz",
+ "integrity": "sha512-aqldKgX9aZqpoDp3e8/BZ8Dm7x1pJl+qI3ZKxDN0i/IQTWUwBx/ManmlVJ3wowqbno6c2bmiIfs+Um6LbsjJyQ==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": ">=17.0"
+ }
+ },
+ "node_modules/three": {
+ "version": "0.184.0",
+ "resolved": "https://registry.npmjs.org/three/-/three-0.184.0.tgz",
+ "integrity": "sha512-wtTRjG92pM5eUg/KuUnHsqSAlPM296brTOcLgMRqEeylYTh/CdtvKUvCyyCQTzFuStieWxvZb8mVTMvdPyUpxg==",
+ "license": "MIT"
+ },
+ "node_modules/three-mesh-bvh": {
+ "version": "0.8.3",
+ "resolved": "https://registry.npmjs.org/three-mesh-bvh/-/three-mesh-bvh-0.8.3.tgz",
+ "integrity": "sha512-4G5lBaF+g2auKX3P0yqx+MJC6oVt6sB5k+CchS6Ob0qvH0YIhuUk1eYr7ktsIpY+albCqE80/FVQGV190PmiAg==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">= 0.159.0"
+ }
+ },
+ "node_modules/three-stdlib": {
+ "version": "2.36.1",
+ "resolved": "https://registry.npmjs.org/three-stdlib/-/three-stdlib-2.36.1.tgz",
+ "integrity": "sha512-XyGQrFmNQ5O/IoKm556ftwKsBg11TIb301MB5dWNicziQBEs2g3gtOYIf7pFiLa0zI2gUwhtCjv9fmjnxKZ1Cg==",
+ "license": "MIT",
+ "dependencies": {
+ "@types/draco3d": "^1.4.0",
+ "@types/offscreencanvas": "^2019.6.4",
+ "@types/webxr": "^0.5.2",
+ "draco3d": "^1.4.1",
+ "fflate": "^0.6.9",
+ "potpack": "^1.0.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.128.0"
+ }
+ },
+ "node_modules/three-stdlib/node_modules/fflate": {
+ "version": "0.6.10",
+ "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.6.10.tgz",
+ "integrity": "sha512-IQrh3lEPM93wVCEczc9SaAOvkmcoQn/G8Bo1e8ZPlY3X3bnAxWaBdvTdvM1hP62iZp0BXWDy4vTAy4fF0+Dlpg==",
+ "license": "MIT"
+ },
+ "node_modules/tinyglobby": {
+ "version": "0.2.17",
+ "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz",
+ "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "fdir": "^6.5.0",
+ "picomatch": "^4.0.4"
+ },
+ "engines": {
+ "node": ">=12.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/sponsors/SuperchupuDev"
+ }
+ },
+ "node_modules/troika-three-text": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-text/-/troika-three-text-0.52.4.tgz",
+ "integrity": "sha512-V50EwcYGruV5rUZ9F4aNsrytGdKcXKALjEtQXIOBfhVoZU9VAqZNIoGQ3TMiooVqFAbR1w15T+f+8gkzoFzawg==",
+ "license": "MIT",
+ "dependencies": {
+ "bidi-js": "^1.0.2",
+ "troika-three-utils": "^0.52.4",
+ "troika-worker-utils": "^0.52.0",
+ "webgl-sdf-generator": "1.1.1"
+ },
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-three-utils": {
+ "version": "0.52.4",
+ "resolved": "https://registry.npmjs.org/troika-three-utils/-/troika-three-utils-0.52.4.tgz",
+ "integrity": "sha512-NORAStSVa/BDiG52Mfudk4j1FG4jC4ILutB3foPnfGbOeIs9+G5vZLa0pnmnaftZUGm4UwSoqEpWdqvC7zms3A==",
+ "license": "MIT",
+ "peerDependencies": {
+ "three": ">=0.125.0"
+ }
+ },
+ "node_modules/troika-worker-utils": {
+ "version": "0.52.0",
+ "resolved": "https://registry.npmjs.org/troika-worker-utils/-/troika-worker-utils-0.52.0.tgz",
+ "integrity": "sha512-W1CpvTHykaPH5brv5VHLfQo9D1OYuo0cSBEUQFFT/nBUzM8iD6Lq2/tgG/f1OelbAS1WtaTPQzE5uM49egnngw==",
+ "license": "MIT"
+ },
+ "node_modules/tunnel-rat": {
+ "version": "0.1.2",
+ "resolved": "https://registry.npmjs.org/tunnel-rat/-/tunnel-rat-0.1.2.tgz",
+ "integrity": "sha512-lR5VHmkPhzdhrM092lI2nACsLO4QubF0/yoOhzX7c+wIpbN1GjHNzCc91QlpxBi+cnx8vVJ+Ur6vL5cEoQPFpQ==",
+ "license": "MIT",
+ "dependencies": {
+ "zustand": "^4.3.2"
+ }
+ },
+ "node_modules/tunnel-rat/node_modules/zustand": {
+ "version": "4.5.7",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-4.5.7.tgz",
+ "integrity": "sha512-CHOUy7mu3lbD6o6LJLfllpjkzhHXSBlX8B9+qPddUsIfeF5S/UZ5q0kmCsnRqT1UHFQZchNFDDzMbQsuesHWlw==",
+ "license": "MIT",
+ "dependencies": {
+ "use-sync-external-store": "^1.2.2"
+ },
+ "engines": {
+ "node": ">=12.7.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=16.8",
+ "immer": ">=9.0.6",
+ "react": ">=16.8"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/use-sync-external-store": {
+ "version": "1.6.0",
+ "resolved": "https://registry.npmjs.org/use-sync-external-store/-/use-sync-external-store-1.6.0.tgz",
+ "integrity": "sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==",
+ "license": "MIT",
+ "peerDependencies": {
+ "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
+ }
+ },
+ "node_modules/utility-types": {
+ "version": "3.11.0",
+ "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz",
+ "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 4"
+ }
+ },
+ "node_modules/vite": {
+ "version": "8.2.1",
+ "resolved": "https://registry.npmjs.org/vite/-/vite-8.2.1.tgz",
+ "integrity": "sha512-EU/eS7BH3XROHh2YnBefjM6DBKA6ZeMZEYQbj7NLWg5wHYlhB8B/Mayd5XsgWq+NFYccDOTemRpdETWR6Ka/lw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "lightningcss": "^1.33.0",
+ "picomatch": "^4.0.5",
+ "postcss": "^8.5.25",
+ "rolldown": "~1.2.1",
+ "tinyglobby": "^0.2.17"
+ },
+ "bin": {
+ "vite": "bin/vite.js"
+ },
+ "engines": {
+ "node": "^20.19.0 || >=22.12.0"
+ },
+ "funding": {
+ "url": "https://github.com/vitejs/vite?sponsor=1"
+ },
+ "optionalDependencies": {
+ "fsevents": "~2.3.3"
+ },
+ "peerDependencies": {
+ "@types/node": "^20.19.0 || >=22.12.0",
+ "@vitejs/devtools": "^0.4.0",
+ "esbuild": "^0.27.0 || ^0.28.0",
+ "jiti": ">=1.21.0",
+ "less": "^4.0.0",
+ "sass": "^1.70.0",
+ "sass-embedded": "^1.70.0",
+ "stylus": ">=0.54.8",
+ "sugarss": "^5.0.0",
+ "terser": "^5.16.0",
+ "tsx": "^4.8.1",
+ "yaml": "^2.4.2"
+ },
+ "peerDependenciesMeta": {
+ "@types/node": {
+ "optional": true
+ },
+ "@vitejs/devtools": {
+ "optional": true
+ },
+ "esbuild": {
+ "optional": true
+ },
+ "jiti": {
+ "optional": true
+ },
+ "less": {
+ "optional": true
+ },
+ "sass": {
+ "optional": true
+ },
+ "sass-embedded": {
+ "optional": true
+ },
+ "stylus": {
+ "optional": true
+ },
+ "sugarss": {
+ "optional": true
+ },
+ "terser": {
+ "optional": true
+ },
+ "tsx": {
+ "optional": true
+ },
+ "yaml": {
+ "optional": true
+ }
+ }
+ },
+ "node_modules/vite/node_modules/fsevents": {
+ "version": "2.3.3",
+ "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
+ "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
+ "dev": true,
+ "hasInstallScript": true,
+ "license": "MIT",
+ "optional": true,
+ "os": [
+ "darwin"
+ ],
+ "engines": {
+ "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
+ }
+ },
+ "node_modules/webgl-constants": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-constants/-/webgl-constants-1.1.1.tgz",
+ "integrity": "sha512-LkBXKjU5r9vAW7Gcu3T5u+5cvSvh5WwINdr0C+9jpzVB41cjQAP5ePArDtk/WHYdVj0GefCgM73BA7FlIiNtdg=="
+ },
+ "node_modules/webgl-sdf-generator": {
+ "version": "1.1.1",
+ "resolved": "https://registry.npmjs.org/webgl-sdf-generator/-/webgl-sdf-generator-1.1.1.tgz",
+ "integrity": "sha512-9Z0JcMTFxeE+b2x1LJTdnaT8rT8aEp7MVxkNwoycNmJWwPdzoXzMh0BjJSh/AEFP+KPYZUli814h8bJZFIZ2jA==",
+ "license": "MIT"
+ },
+ "node_modules/which": {
+ "version": "2.0.2",
+ "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
+ "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==",
+ "license": "ISC",
+ "dependencies": {
+ "isexe": "^2.0.0"
+ },
+ "bin": {
+ "node-which": "bin/node-which"
+ },
+ "engines": {
+ "node": ">= 8"
+ }
+ },
+ "node_modules/zustand": {
+ "version": "5.0.14",
+ "resolved": "https://registry.npmjs.org/zustand/-/zustand-5.0.14.tgz",
+ "integrity": "sha512-/8tAspM5LMPr28b3fwLYrtdj77ECpfZviaP75CMTnwO8ISyaE4GDIG/9rDDYq/cH9D2Xw2A2RXglLInmVBQB/g==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=12.20.0"
+ },
+ "peerDependencies": {
+ "@types/react": ">=18.0.0",
+ "immer": ">=9.0.6",
+ "react": ">=18.0.0",
+ "use-sync-external-store": ">=1.2.0"
+ },
+ "peerDependenciesMeta": {
+ "@types/react": {
+ "optional": true
+ },
+ "immer": {
+ "optional": true
+ },
+ "react": {
+ "optional": true
+ },
+ "use-sync-external-store": {
+ "optional": true
+ }
+ }
+ }
+ }
+}
diff --git a/projects/robotics-policy-prover/package.json b/projects/robotics-policy-prover/package.json
new file mode 100644
index 00000000..15598eb7
--- /dev/null
+++ b/projects/robotics-policy-prover/package.json
@@ -0,0 +1,26 @@
+{
+ "name": "openshell-robotics-prover-demo",
+ "private": true,
+ "version": "0.1.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite --host 0.0.0.0",
+ "dev:prover": "cargo run --manifest-path policy-prover-service/Cargo.toml",
+ "build": "vite build",
+ "preview": "vite preview --host 0.0.0.0",
+ "verify:visual": "node scripts/verify-visual.mjs"
+ },
+ "dependencies": {
+ "@react-three/drei": "^10.7.7",
+ "@react-three/fiber": "^9.6.1",
+ "lucide-react": "^1.17.0",
+ "react": "^19.2.6",
+ "react-dom": "^19.2.6",
+ "three": "^0.184.0"
+ },
+ "devDependencies": {
+ "@vitejs/plugin-react": "^6.0.2",
+ "playwright": "^1.60.0",
+ "vite": "^8.0.14"
+ }
+}
diff --git a/projects/robotics-policy-prover/policy-prover-service/Cargo.lock b/projects/robotics-policy-prover/policy-prover-service/Cargo.lock
new file mode 100644
index 00000000..23a4592c
--- /dev/null
+++ b/projects/robotics-policy-prover/policy-prover-service/Cargo.lock
@@ -0,0 +1,2382 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aes"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1fc76eaeac4c9164506c466d4ffdd8ec9d0c5bf57ee97177c4d8eceb3a0e138"
+dependencies = [
+ "cipher",
+ "cpubits",
+ "cpufeatures",
+]
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "axum"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
+dependencies = [
+ "axum-core",
+ "bytes",
+ "form_urlencoded",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "itoa",
+ "matchit",
+ "memchr",
+ "mime",
+ "percent-encoding",
+ "pin-project-lite",
+ "serde_core",
+ "serde_json",
+ "serde_path_to_error",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "axum-core"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "http-body-util",
+ "mime",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bindgen"
+version = "0.72.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex 1.3.0",
+ "syn",
+]
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+dependencies = [
+ "hybrid-array",
+ "zeroize",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bzip2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
+dependencies = [
+ "libbz2-rs-sys",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex 2.0.1",
+]
+
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "cfg_aliases"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724"
+
+[[package]]
+name = "cipher"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8cf2a2c93cd704877c0858356ed03480ff301ee950b43f1cbe4573b088bfa6c"
+dependencies = [
+ "crypto-common",
+ "inout",
+]
+
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
+[[package]]
+name = "cmake"
+version = "0.1.58"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0f78a02292a74a88ac736019ab962ece0bc380e3f977bf72e376c5d78ff0678"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "cmov"
+version = "0.5.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c9ea0ac24bc397ab3c98583a3c9ba74fa56b09a4449bbe172b9b1ddb016027a"
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "constant_time_eq"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d52eff69cd5e647efe296129160853a42795992097e8af39800e1060caeea9b"
+
+[[package]]
+name = "cpubits"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b85f9c39137c3a891689859392b1bd49812121d0d61c9caf00d46ed5ce06ae"
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "ctutils"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d5515a3834141de9eafb9717ad39eea8247b5674e6066c404e8c4b365d2a29e"
+dependencies = [
+ "cmov",
+]
+
+[[package]]
+name = "deflate64"
+version = "0.1.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac6b926516df9c60bfa16e107b21086399f8285a44ca9711344b9e553c5146e2"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer",
+ "const-oid",
+ "crypto-common",
+ "ctutils",
+ "zeroize",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "dotenvy"
+version = "0.15.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "futures-channel"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
+dependencies = [
+ "futures-core",
+ "futures-sink",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-io"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
+
+[[package]]
+name = "futures-macro"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "futures-sink"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-io",
+ "futures-macro",
+ "futures-sink",
+ "futures-task",
+ "memchr",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "wasi",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hmac"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6303bc9732ae41b04cb554b844a762b4115a61bfaa81e3e83050991eeb56863f"
+dependencies = [
+ "digest",
+]
+
+[[package]]
+name = "http"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "httpdate"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "hyper"
+version = "1.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eb92f162bf56536459fc83c79b974bb12837acfed43d6bc370a7916d0ae15ecc"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "http",
+ "http-body",
+ "httparse",
+ "httpdate",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33ca68d021ef39cf6463ab54c1d0f5daf03377b70561305bb89a8f83aab66e0f"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+ "webpki-roots",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "tokio",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "inout"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4250ce6452e92010fdf7268ccc5d14faa80bb12fc741938534c58f16804e03c7"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libbz2-rs-sys"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libloading"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7c4b02199fee7c5d21a5ae7d8cfa79a6ef5bb2fc834d6e9058e89c825efdc55"
+dependencies = [
+ "cfg-if",
+ "windows-link",
+]
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "log"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
+
+[[package]]
+name = "lru-slab"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112b39cec0b298b6c1999fee3e31427f74f676e4cb9879ed1a121b43661a4154"
+
+[[package]]
+name = "lzma-rust2"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20"
+dependencies = [
+ "sha2",
+]
+
+[[package]]
+name = "matchit"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "mio"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
+[[package]]
+name = "num"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23"
+dependencies = [
+ "num-bigint",
+ "num-complex",
+ "num-integer",
+ "num-iter",
+ "num-rational",
+ "num-traits",
+]
+
+[[package]]
+name = "num-bigint"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9"
+dependencies = [
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-complex"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-integer"
+version = "0.1.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "num-iter"
+version = "0.1.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf"
+dependencies = [
+ "autocfg",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-rational"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
+dependencies = [
+ "num-bigint",
+ "num-integer",
+ "num-traits",
+]
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "pbkdf2"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "112d82ceb8c5bf524d9af484d4e4970c9fd5a0cc15ba14ad93dccd28873b0629"
+dependencies = [
+ "digest",
+ "hmac",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "policy-prover-service"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "axum",
+ "dotenvy",
+ "futures-util",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "tokio",
+ "tokio-stream",
+ "uuid",
+ "z3",
+]
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "ppmd-rust"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "efca4c95a19a79d1c98f791f10aebd5c1363b473244630bb7dbde1dc98455a24"
+
+[[package]]
+name = "ppv-lite86"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
+dependencies = [
+ "zerocopy",
+]
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "quinn"
+version = "0.11.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e20a958963c291dc322d98411f541009df2ced7b5a4f2bd52337638cfccf20"
+dependencies = [
+ "bytes",
+ "cfg_aliases",
+ "pin-project-lite",
+ "quinn-proto",
+ "quinn-udp",
+ "rustc-hash",
+ "rustls",
+ "socket2",
+ "thiserror",
+ "tokio",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-proto"
+version = "0.11.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "434b42fec591c96ef50e21e886936e66d3cc3f737104fdb9b737c40ffb94c098"
+dependencies = [
+ "bytes",
+ "getrandom 0.3.4",
+ "lru-slab",
+ "rand",
+ "ring",
+ "rustc-hash",
+ "rustls",
+ "rustls-pki-types",
+ "slab",
+ "thiserror",
+ "tinyvec",
+ "tracing",
+ "web-time",
+]
+
+[[package]]
+name = "quinn-udp"
+version = "0.5.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "addec6a0dcad8a8d96a771f815f0eaf55f9d1805756410b39f5fa81332574cbd"
+dependencies = [
+ "cfg_aliases",
+ "libc",
+ "once_cell",
+ "socket2",
+ "tracing",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44c5af06bb1b7d3216d91932aed5265164bf384dc89cd6ba05cf59a35f5f76ea"
+dependencies = [
+ "rand_chacha",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_chacha"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
+dependencies = [
+ "ppv-lite86",
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c"
+dependencies = [
+ "getrandom 0.3.4",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "reqwest"
+version = "0.12.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddd3ca559203180a307f12d114c268abf583f59b03cb906fd0b3ff8646c1147"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "percent-encoding",
+ "pin-project-lite",
+ "quinn",
+ "rustls",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-rustls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "web-time",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ryu"
+version = "1.0.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_path_to_error"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
+dependencies = [
+ "itoa",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "sha1"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aacc4cc499359472b4abe1bf11d0b12e688af9a805fa5e3016f9a386dc2d0214"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures",
+ "digest",
+]
+
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "socket2"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "js-sys",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "tinyvec"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e61e67053d25a4e82c844e8424039d9745781b3fc4f32b8d55ed50f5f667ef3"
+dependencies = [
+ "tinyvec_macros",
+]
+
+[[package]]
+name = "tinyvec_macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
+
+[[package]]
+name = "tokio"
+version = "1.52.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
+dependencies = [
+ "bytes",
+ "libc",
+ "mio",
+ "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "385a6cb71ab9ab790c5fe8d67f1645e6c450a7ce006a33de03daa956cf70a496"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+ "tokio-util",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tower"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
+dependencies = [
+ "bitflags",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+ "url",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "log",
+ "pin-project-lite",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typed-path"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "uuid"
+version = "1.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "serde_core",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.72"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.5",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm 0.52.6",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "z3"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "107cca65ed27d28b11f7c492298a51383333fd48ba6ebe49a432aba96162f678"
+dependencies = [
+ "log",
+ "num",
+ "z3-sys",
+]
+
+[[package]]
+name = "z3-sys"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82b97329d02d87da6802ed9fda083f1b255d822ab13d5b1fb961196b58a69a1"
+dependencies = [
+ "bindgen",
+ "cmake",
+ "pkg-config",
+ "reqwest",
+ "serde_json",
+ "zip",
+]
+
+[[package]]
+name = "zerocopy"
+version = "0.8.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bce33a6288fa3f072a8c2c7d0f2fdbb90e28298f0135c1f99b96c3db2efcc60b"
+dependencies = [
+ "zerocopy-derive",
+]
+
+[[package]]
+name = "zerocopy-derive"
+version = "0.8.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd425244944f4ab65ccff928e7323354c5a018c75838362fdce749dfad2ee1e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zip"
+version = "8.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
+dependencies = [
+ "aes",
+ "bzip2",
+ "constant_time_eq",
+ "crc32fast",
+ "deflate64",
+ "flate2",
+ "getrandom 0.4.2",
+ "hmac",
+ "indexmap",
+ "lzma-rust2",
+ "memchr",
+ "pbkdf2",
+ "ppmd-rust",
+ "sha1",
+ "time",
+ "typed-path",
+ "zeroize",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/projects/robotics-policy-prover/policy-prover-service/Cargo.toml b/projects/robotics-policy-prover/policy-prover-service/Cargo.toml
new file mode 100644
index 00000000..a000e041
--- /dev/null
+++ b/projects/robotics-policy-prover/policy-prover-service/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "policy-prover-service"
+version = "0.1.0"
+edition = "2021"
+
+[dependencies]
+anyhow = "1"
+axum = "0.8"
+dotenvy = "0.15"
+futures-util = "0.3"
+reqwest = { version = "0.12", features = ["json", "rustls-tls"], default-features = false }
+serde = { version = "1", features = ["derive"] }
+serde_json = "1"
+tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync", "time"] }
+tokio-stream = { version = "0.1", features = ["sync"] }
+uuid = { version = "1", features = ["v4", "serde"] }
+z3 = { version = "0.19", features = ["bundled"] }
diff --git a/projects/robotics-policy-prover/policy-prover-service/src/main.rs b/projects/robotics-policy-prover/policy-prover-service/src/main.rs
new file mode 100644
index 00000000..31c12814
--- /dev/null
+++ b/projects/robotics-policy-prover/policy-prover-service/src/main.rs
@@ -0,0 +1,1843 @@
+use std::collections::HashMap;
+use std::convert::Infallible;
+use std::env;
+use std::fs;
+use std::hint::black_box;
+use std::net::SocketAddr;
+use std::sync::Arc;
+use std::time::{Instant, SystemTime, UNIX_EPOCH};
+
+use anyhow::{Context, Result};
+use axum::extract::{Path, State};
+use axum::response::sse::{Event, KeepAlive, Sse};
+use axum::routing::{get, post};
+use axum::{Json, Router};
+use futures_util::StreamExt;
+use reqwest::Client;
+use serde::{Deserialize, Serialize};
+use tokio::sync::{broadcast, Mutex};
+use tokio::time::{sleep, Duration};
+use tokio_stream::wrappers::BroadcastStream;
+use uuid::Uuid;
+use z3::ast::Bool;
+use z3::{Config, SatResult, Solver};
+
+const WORKSPACE_MIN: Vec3 = [-1.35, 0.02, -0.95];
+const WORKSPACE_MAX: Vec3 = [1.25, 1.05, 0.85];
+const HOME: Vec3 = [-1.08, 0.78, 0.58];
+const POLICY_SOLVER_TIMEOUT_MS: u64 = 100;
+
+type Vec3 = [f64; 3];
+
+#[derive(Clone)]
+struct AppState {
+ sessions: Arc>>,
+}
+
+#[derive(Clone)]
+struct SessionHandle {
+ tx: broadcast::Sender,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct StartSessionRequest {
+ goal: Option,
+ seed: Option,
+ agent_mode: Option,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+enum AgentMode {
+ Fixture,
+ Openai,
+}
+
+#[derive(Debug, Clone, Serialize)]
+struct StartSessionResponse {
+ session_id: Uuid,
+ agent_mode: AgentMode,
+ seed: u64,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+struct InjectRequest {
+ event: InjectEvent,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+#[serde(rename_all = "snake_case")]
+enum InjectEvent {
+ HumanEntersWorkspace,
+ SensorStale,
+ BudgetLow,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct ActionEnvelope {
+ actor: String,
+ subagent: String,
+ action: String,
+ resource: String,
+ from: Vec3,
+ to: Vec3,
+ path: Vec,
+ speed_mps: f64,
+ force_n: f64,
+ object_id: String,
+ object_class: String,
+ capability: String,
+ context: ActionContext,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct ActionContext {
+ human_distance_m: f64,
+ sensor_age_ms: u64,
+ remaining_budget_ms: u64,
+ restricted_zones: Vec,
+ caution_zones: Vec,
+ parent_speed_cap_mps: f64,
+ requested_speed_cap_mps: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct ProverDecision {
+ decision: Decision,
+ solver_ms: f64,
+ violations: Vec,
+ constraints: DecisionConstraints,
+ obligations: Vec,
+ counterexample: Option,
+}
+
+#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
+#[serde(rename_all = "snake_case")]
+enum Decision {
+ Allow,
+ Deny,
+ AllowWithConstraints,
+ ApprovalRequired,
+}
+
+#[derive(Debug, Clone, Default, Serialize, Deserialize)]
+struct DecisionConstraints {
+ #[serde(skip_serializing_if = "Option::is_none")]
+ speed_mps_max: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ force_n_max: Option,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ bypass_z_min: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct Counterexample {
+ segment_id: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ zone_id: Option,
+ reason: String,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ point: Option,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct WorldState {
+ goal: String,
+ seed: u64,
+ objects: Vec,
+ zones: Vec,
+ human: HumanState,
+ metrics: Metrics,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct SceneObject {
+ id: String,
+ label: String,
+ class_name: String,
+ color: String,
+ position: Vec3,
+ size: Vec3,
+ sorted: bool,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct Zone {
+ id: String,
+ label: String,
+ color: String,
+ position: Vec3,
+ size: Vec3,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct HumanState {
+ position: Vec3,
+ radius: f64,
+ caution: f64,
+ distance_m: f64,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct Metrics {
+ fps: u32,
+ decision_count: u32,
+ p95_solver_ms: f64,
+ task_budget_pct: u8,
+ compute_budget_pct: u8,
+ actions_left: u8,
+ sensor_age_ms: u64,
+}
+
+#[derive(Debug, Clone, Serialize)]
+struct DemoEvent {
+ id: String,
+ kind: String,
+ timestamp_ms: u64,
+ actor: String,
+ summary: String,
+ world: Option,
+ action: Option,
+ decision: Option,
+ proposed_path: Option>,
+ approved_path: Option>,
+ highlight: Option,
+}
+
+#[derive(Debug, Clone, Serialize)]
+struct Highlight {
+ kind: String,
+ target: Option,
+}
+
+#[derive(Debug, Clone)]
+struct MissionRuntime {
+ tx: broadcast::Sender,
+ world: WorldState,
+ started: Instant,
+ solver_samples: Vec,
+ agent_mode: AgentMode,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize)]
+struct AgentPlan {
+ path: Vec,
+ speed_mps: f64,
+ rationale: String,
+ #[serde(default)]
+ source: String,
+ #[serde(default)]
+ inference_ms: Option,
+}
+
+#[tokio::main(flavor = "current_thread")]
+async fn main() -> Result<()> {
+ dotenvy::dotenv().ok();
+
+ if env::var("POLICY_PROVER_BENCHMARK").as_deref() == Ok("1") {
+ run_policy_benchmark()?;
+ return Ok(());
+ }
+
+ // Pay one-time Z3 initialization costs before accepting interactive sessions.
+ for outcome in ["allow", "deny", "constrain"] {
+ for _ in 0..3 {
+ black_box(check_action(&benchmark_action(outcome, 6)));
+ }
+ }
+
+ let state = AppState {
+ sessions: Arc::new(Mutex::new(HashMap::new())),
+ };
+
+ let app = Router::new()
+ .route(
+ "/api/health",
+ get(|| async { Json(serde_json::json!({ "ok": true })) }),
+ )
+ .route("/api/sessions", post(start_session))
+ .route("/api/sessions/{id}/events", get(session_events))
+ .route("/api/sessions/{id}/inject", post(inject_event))
+ .route("/api/decide", post(decide))
+ .with_state(state);
+
+ let addr: SocketAddr = env::var("POLICY_PROVER_BIND_ADDR")
+ .unwrap_or_else(|_| "127.0.0.1:8787".to_owned())
+ .parse()?;
+ println!("policy-prover service listening on http://{addr}");
+ let listener = tokio::net::TcpListener::bind(addr).await?;
+ axum::serve(listener, app).await?;
+ Ok(())
+}
+
+async fn start_session(
+ State(state): State,
+ Json(request): Json,
+) -> Json {
+ let session_id = Uuid::new_v4();
+ let seed = request.seed.unwrap_or(42);
+ let requested_mode = request.agent_mode.unwrap_or(AgentMode::Fixture);
+ let agent_mode = if requested_mode == AgentMode::Openai && env::var("OPENAI_API_KEY").is_ok() {
+ AgentMode::Openai
+ } else {
+ AgentMode::Fixture
+ };
+ let goal = request
+ .goal
+ .unwrap_or_else(|| "Sort all lab samples into the correct trays.".to_owned());
+ let (tx, _) = broadcast::channel(256);
+ state
+ .sessions
+ .lock()
+ .await
+ .insert(session_id, SessionHandle { tx: tx.clone() });
+
+ let runtime = MissionRuntime {
+ tx,
+ world: seeded_world(seed, goal),
+ started: Instant::now(),
+ solver_samples: Vec::new(),
+ agent_mode,
+ };
+ tokio::spawn(async move {
+ if let Err(err) = run_mission(runtime).await {
+ eprintln!("mission {session_id} failed: {err:#}");
+ }
+ });
+
+ Json(StartSessionResponse {
+ session_id,
+ agent_mode,
+ seed,
+ })
+}
+
+async fn session_events(
+ State(state): State,
+ Path(id): Path,
+) -> Sse>> {
+ let rx = state
+ .sessions
+ .lock()
+ .await
+ .get(&id)
+ .map(|handle| handle.tx.subscribe());
+
+ let stream = match rx {
+ Some(rx) => BroadcastStream::new(rx)
+ .filter_map(|message| async move { message.ok() })
+ .map(|event| {
+ let data = serde_json::to_string(&event).unwrap_or_else(|_| "{}".to_owned());
+ Ok(Event::default().event(event.kind).id(event.id).data(data))
+ })
+ .boxed(),
+ None => {
+ let event = DemoEvent::system("session_missing", "Session not found", None);
+ tokio_stream::iter([Ok(Event::default()
+ .event("error")
+ .id(event.id.clone())
+ .data(serde_json::to_string(&event).unwrap()))])
+ .boxed()
+ }
+ };
+
+ Sse::new(stream).keep_alive(KeepAlive::default())
+}
+
+async fn inject_event(
+ State(state): State,
+ Path(id): Path,
+ Json(request): Json,
+) -> Json {
+ let Some(handle) = state.sessions.lock().await.get(&id).cloned() else {
+ return Json(serde_json::json!({ "ok": false, "error": "session_not_found" }));
+ };
+ let summary = match request.event {
+ InjectEvent::HumanEntersWorkspace => "Injected: human entered the workspace",
+ InjectEvent::SensorStale => "Injected: sensor state is stale",
+ InjectEvent::BudgetLow => "Injected: task budget is almost exhausted",
+ };
+ let _ = handle
+ .tx
+ .send(DemoEvent::system("world_event", summary, None));
+ Json(serde_json::json!({ "ok": true }))
+}
+
+async fn decide(Json(action): Json) -> Json {
+ Json(check_action(&action))
+}
+
+async fn run_mission(mut runtime: MissionRuntime) -> Result<()> {
+ sleep(Duration::from_millis(250)).await;
+ runtime.emit_world(
+ "world_event",
+ "Experiment started: move the green block into the blue tray.",
+ );
+ sleep(Duration::from_millis(700)).await;
+
+ let initial_plan = runtime
+ .choose_plan(None)
+ .await
+ .unwrap_or_else(|| fixture_plan(&runtime.world, None));
+ let proposal = action_from_plan(&runtime.world, &initial_plan);
+
+ runtime
+ .agent_plan(
+ "Agent proposed a waypoint path",
+ &proposal,
+ None,
+ &initial_plan,
+ )
+ .await;
+ let first_decision = runtime.check_only(proposal.clone()).await?;
+
+ if first_decision.decision == Decision::Deny {
+ sleep(Duration::from_millis(900)).await;
+ let mut repair_plan = runtime
+ .choose_plan(Some(&first_decision))
+ .await
+ .unwrap_or_else(|| fixture_plan(&runtime.world, Some(&first_decision)));
+ let mut repair = action_from_plan(&runtime.world, &repair_plan);
+ if runtime.agent_mode == AgentMode::Fixture
+ && first_path_intersection(&repair.path, &repair.context.restricted_zones).is_some()
+ {
+ repair_plan = fixture_plan(&runtime.world, Some(&first_decision));
+ repair = action_from_plan(&runtime.world, &repair_plan);
+ }
+
+ runtime
+ .agent_plan(
+ "Agent proposed a revised waypoint path",
+ &repair,
+ Some(&first_decision),
+ &repair_plan,
+ )
+ .await;
+
+ runtime.world.human.position = [0.12, 0.08, -0.22];
+ runtime.world.human.distance_m = 0.58;
+ runtime.emit_world(
+ "world_event",
+ "A human enters the caution radius before execution.",
+ );
+ sleep(Duration::from_millis(500)).await;
+ let mut repair_with_current_world = action_for(
+ &runtime.world,
+ "green_vial",
+ repair.path.clone(),
+ repair.speed_mps,
+ repair.force_n,
+ "move_lab_samples",
+ );
+ repair_with_current_world.speed_mps = repair.speed_mps;
+ runtime
+ .check_and_execute(
+ repair_with_current_world,
+ Some("Executor runs the revised path with the prover's speed limit"),
+ )
+ .await?;
+ } else if decision_is_executable(first_decision.decision) {
+ runtime
+ .execute_decision(
+ proposal,
+ first_decision,
+ Some("Executor runs the agent path"),
+ )
+ .await?;
+ }
+
+ runtime.emit_world(
+ "execution_update",
+ "Experiment complete: only the policy-prover-approved path was executed.",
+ );
+ Ok(())
+}
+
+impl MissionRuntime {
+ fn elapsed_ms(&self) -> u64 {
+ self.started.elapsed().as_millis() as u64
+ }
+
+ fn emit(&self, mut event: DemoEvent) {
+ event.timestamp_ms = self.elapsed_ms();
+ let _ = self.tx.send(event);
+ }
+
+ fn emit_world(&self, kind: &str, summary: &str) {
+ self.emit(DemoEvent {
+ id: Uuid::new_v4().to_string(),
+ kind: kind.to_owned(),
+ timestamp_ms: 0,
+ actor: "world".to_owned(),
+ summary: summary.to_owned(),
+ world: Some(self.world.clone()),
+ action: None,
+ decision: None,
+ proposed_path: None,
+ approved_path: None,
+ highlight: Some(Highlight {
+ kind: "world".to_owned(),
+ target: None,
+ }),
+ });
+ }
+
+ async fn agent_plan(
+ &self,
+ summary: &str,
+ action: &ActionEnvelope,
+ _prior: Option<&ProverDecision>,
+ plan: &AgentPlan,
+ ) {
+ let latency = plan
+ .inference_ms
+ .map(|value| format!(" in {value:.0} ms"))
+ .unwrap_or_default();
+ self.emit(DemoEvent {
+ id: Uuid::new_v4().to_string(),
+ kind: "agent_plan".to_owned(),
+ timestamp_ms: 0,
+ actor: "agent".to_owned(),
+ summary: format!(
+ "{summary}: {} waypoints at {:.2} m/s. Source: {}{}.",
+ action.path.len(),
+ action.speed_mps,
+ plan.source,
+ latency
+ ),
+ world: Some(self.world.clone()),
+ action: Some(action.clone()),
+ decision: None,
+ proposed_path: Some(action.path.clone()),
+ approved_path: None,
+ highlight: Some(Highlight {
+ kind: "proposal".to_owned(),
+ target: Some(action.object_id.clone()),
+ }),
+ });
+ }
+
+ async fn choose_plan(&self, prior: Option<&ProverDecision>) -> Option {
+ if self.agent_mode != AgentMode::Openai {
+ return Some(fixture_plan(&self.world, prior));
+ }
+ match ask_openai_plan(&self.world, prior).await {
+ Ok(plan) => Some(plan),
+ Err(error) => {
+ eprintln!("model planner failed; using fixture fallback: {error:#}");
+ None
+ }
+ }
+ }
+
+ async fn check_only(&mut self, action: ActionEnvelope) -> Result {
+ let decision = check_action(&action);
+ self.solver_samples.push(decision.solver_ms);
+ self.world.metrics.decision_count += 1;
+ self.world.metrics.p95_solver_ms = p95(&self.solver_samples);
+
+ let approved_path = match decision.decision {
+ Decision::Allow | Decision::AllowWithConstraints => Some(action.path.clone()),
+ Decision::Deny | Decision::ApprovalRequired => None,
+ };
+ self.emit(DemoEvent {
+ id: Uuid::new_v4().to_string(),
+ kind: "prover_decision".to_owned(),
+ timestamp_ms: 0,
+ actor: "policy-prover".to_owned(),
+ summary: decision_summary(&decision),
+ world: Some(self.world.clone()),
+ action: Some(action.clone()),
+ decision: Some(decision.clone()),
+ proposed_path: Some(action.path.clone()),
+ approved_path,
+ highlight: Some(Highlight {
+ kind: format!("{:?}", decision.decision).to_lowercase(),
+ target: decision
+ .counterexample
+ .as_ref()
+ .and_then(|counterexample| counterexample.zone_id.clone())
+ .or_else(|| Some(action.object_id.clone())),
+ }),
+ });
+ Ok(decision)
+ }
+
+ async fn check_and_execute(
+ &mut self,
+ action: ActionEnvelope,
+ executor_note: Option<&str>,
+ ) -> Result<()> {
+ let decision = self.check_only(action.clone()).await?;
+ if !decision_is_executable(decision.decision) {
+ return Ok(());
+ }
+ self.execute_decision(action, decision, executor_note).await
+ }
+
+ async fn execute_decision(
+ &mut self,
+ action: ActionEnvelope,
+ decision: ProverDecision,
+ executor_note: Option<&str>,
+ ) -> Result<()> {
+ anyhow::ensure!(
+ decision_is_executable(decision.decision),
+ "executor received a non-executable {:?} decision",
+ decision.decision
+ );
+ sleep(Duration::from_millis(600)).await;
+ let mut effective_action = apply_decision_contract(action, &decision);
+ effective_action.context.human_distance_m = self.world.human.distance_m;
+ effective_action.context.sensor_age_ms = self.world.metrics.sensor_age_ms;
+
+ if let Some(obligation) = unmet_execution_obligation(&effective_action, &decision) {
+ self.emit(DemoEvent {
+ id: Uuid::new_v4().to_string(),
+ kind: "execution_blocked".to_owned(),
+ timestamp_ms: 0,
+ actor: "executor".to_owned(),
+ summary: format!("Executor paused: unmet obligation {obligation}"),
+ world: Some(self.world.clone()),
+ action: Some(effective_action.clone()),
+ decision: Some(decision),
+ proposed_path: Some(effective_action.path.clone()),
+ approved_path: None,
+ highlight: Some(Highlight {
+ kind: "blocked".to_owned(),
+ target: Some(effective_action.object_id),
+ }),
+ });
+ return Ok(());
+ }
+
+ let contract =
+ if effective_action.speed_mps < effective_action.context.requested_speed_cap_mps {
+ format!(" at {:.2} m/s", effective_action.speed_mps)
+ } else {
+ String::new()
+ };
+ let note = executor_note.unwrap_or("Executor running approved segment");
+ self.emit(DemoEvent {
+ id: Uuid::new_v4().to_string(),
+ kind: "execution_update".to_owned(),
+ timestamp_ms: 0,
+ actor: "executor".to_owned(),
+ summary: format!("{note}{contract}"),
+ world: Some(self.world.clone()),
+ action: Some(effective_action.clone()),
+ decision: Some(decision),
+ proposed_path: Some(effective_action.path.clone()),
+ approved_path: Some(effective_action.path.clone()),
+ highlight: Some(Highlight {
+ kind: "execution".to_owned(),
+ target: Some(effective_action.object_id.clone()),
+ }),
+ });
+ sleep(Duration::from_millis(5_600)).await;
+ if let Some(object) = self
+ .world
+ .objects
+ .iter_mut()
+ .find(|object| object.id == effective_action.object_id)
+ {
+ object.position = place_point();
+ object.sorted = true;
+ }
+ Ok(())
+ }
+}
+
+impl DemoEvent {
+ fn system(kind: &str, summary: &str, world: Option) -> Self {
+ Self {
+ id: Uuid::new_v4().to_string(),
+ kind: kind.to_owned(),
+ timestamp_ms: 0,
+ actor: "system".to_owned(),
+ summary: summary.to_owned(),
+ world,
+ action: None,
+ decision: None,
+ proposed_path: None,
+ approved_path: None,
+ highlight: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone)]
+struct PolicyFacts {
+ out_of_bounds: bool,
+ restricted_hit: Option<(Zone, Vec3)>,
+ stale_sensor: bool,
+ budget_blocked: bool,
+ capability_expansion: bool,
+ human_speed_cap: bool,
+ fragile_force_cap: bool,
+}
+
+fn derive_policy_facts(action: &ActionEnvelope) -> PolicyFacts {
+ PolicyFacts {
+ out_of_bounds: action.path.iter().any(|point| !inside_workspace(*point)),
+ restricted_hit: first_path_intersection(&action.path, &action.context.restricted_zones),
+ stale_sensor: action.context.sensor_age_ms > 250,
+ budget_blocked: action.context.remaining_budget_ms < 1_200
+ && action.action != "arm.move_segment",
+ capability_expansion: action.action == "grant.capability"
+ && (action.capability == "unrestricted_motion"
+ || action.context.requested_speed_cap_mps > action.context.parent_speed_cap_mps),
+ human_speed_cap: action.context.human_distance_m < 0.75 && action.speed_mps > 0.08,
+ fragile_force_cap: matches!(action.object_class.as_str(), "fragile" | "hazardous")
+ && action.force_n > 2.0,
+ }
+}
+
+fn assert_fact(solver: &Solver, name: &str, value: bool) -> Bool {
+ let fact = Bool::new_const(name);
+ if value {
+ solver.assert(&fact);
+ } else {
+ solver.assert(&!fact.clone());
+ }
+ fact
+}
+
+fn solve_policy(facts: &PolicyFacts) -> std::result::Result {
+ let mut config = Config::new();
+ config.set_timeout_msec(POLICY_SOLVER_TIMEOUT_MS);
+ z3::with_z3_config(&config, || {
+ let solver = Solver::new();
+ let out_of_bounds = assert_fact(&solver, "workspace_bounds", facts.out_of_bounds);
+ let restricted = assert_fact(
+ &solver,
+ "restricted_zone_intersection",
+ facts.restricted_hit.is_some(),
+ );
+ let stale = assert_fact(&solver, "sensor_stale", facts.stale_sensor);
+ let budget = assert_fact(&solver, "budget_requires_approval", facts.budget_blocked);
+ let capability = assert_fact(&solver, "capability_expansion", facts.capability_expansion);
+ let human_cap = assert_fact(&solver, "human_speed_cap", facts.human_speed_cap);
+ let fragile_cap = assert_fact(&solver, "fragile_force_cap", facts.fragile_force_cap);
+
+ let hard_deny = Bool::or(&[out_of_bounds, restricted, stale, capability]);
+ let constrained = Bool::or(&[human_cap, fragile_cap]);
+ let deny = Bool::new_const("outcome_deny");
+ let approval = Bool::new_const("outcome_approval");
+ let constrain = Bool::new_const("outcome_constrain");
+ let allow = Bool::new_const("outcome_allow");
+
+ solver.assert(&deny.iff(&hard_deny));
+ solver.assert(&approval.iff(&Bool::and(&[!hard_deny.clone(), budget.clone()])));
+ solver.assert(&constrain.iff(&Bool::and(&[
+ !hard_deny.clone(),
+ !budget.clone(),
+ constrained.clone(),
+ ])));
+ solver.assert(&allow.iff(&Bool::and(&[!hard_deny, !budget, !constrained])));
+
+ match solver.check() {
+ SatResult::Sat => {
+ let model = solver.get_model().ok_or("policy_solver_missing_model")?;
+ let selected = [
+ (Decision::Deny, deny),
+ (Decision::ApprovalRequired, approval),
+ (Decision::AllowWithConstraints, constrain),
+ (Decision::Allow, allow),
+ ]
+ .into_iter()
+ .filter(|(_, outcome)| {
+ model.eval(outcome, true).and_then(|value| value.as_bool()) == Some(true)
+ })
+ .map(|(decision, _)| decision)
+ .collect::>();
+ match selected.as_slice() {
+ [decision] => Ok(*decision),
+ _ => Err("policy_solver_ambiguous_outcome"),
+ }
+ }
+ SatResult::Unsat => Err("policy_solver_inconsistent"),
+ SatResult::Unknown => Err("policy_solver_unknown"),
+ }
+ })
+}
+
+fn malformed_action_reasons(action: &ActionEnvelope) -> Vec {
+ let mut reasons = Vec::new();
+ if action.actor.trim().is_empty()
+ || action.action.trim().is_empty()
+ || action.resource.trim().is_empty()
+ || action.capability.trim().is_empty()
+ {
+ reasons.push("missing_required_identity".to_owned());
+ }
+ if action.path.len() < 2 {
+ reasons.push("path_requires_two_waypoints".to_owned());
+ }
+ if !finite_vec(action.from)
+ || !finite_vec(action.to)
+ || action.path.iter().any(|point| !finite_vec(*point))
+ {
+ reasons.push("non_finite_path_coordinate".to_owned());
+ }
+ if action
+ .path
+ .first()
+ .is_some_and(|point| distance(*point, action.from) > 1e-6)
+ || action
+ .path
+ .last()
+ .is_some_and(|point| distance(*point, action.to) > 1e-6)
+ {
+ reasons.push("path_endpoints_do_not_match_envelope".to_owned());
+ }
+ if !action.speed_mps.is_finite() || action.speed_mps <= 0.0 {
+ reasons.push("invalid_speed".to_owned());
+ }
+ if !action.force_n.is_finite() || action.force_n < 0.0 {
+ reasons.push("invalid_force".to_owned());
+ }
+ if !action.context.human_distance_m.is_finite()
+ || action.context.human_distance_m < 0.0
+ || !action.context.parent_speed_cap_mps.is_finite()
+ || action.context.parent_speed_cap_mps < 0.0
+ || !action.context.requested_speed_cap_mps.is_finite()
+ || action.context.requested_speed_cap_mps < 0.0
+ {
+ reasons.push("invalid_action_context".to_owned());
+ }
+ if action
+ .context
+ .restricted_zones
+ .iter()
+ .chain(action.context.caution_zones.iter())
+ .any(|zone| {
+ !finite_vec(zone.position)
+ || !finite_vec(zone.size)
+ || zone.size.iter().any(|value| *value <= 0.0)
+ })
+ {
+ reasons.push("invalid_zone_geometry".to_owned());
+ }
+ reasons
+}
+
+fn finite_vec(point: Vec3) -> bool {
+ point.iter().all(|value| value.is_finite())
+}
+
+fn decision_is_executable(decision: Decision) -> bool {
+ matches!(decision, Decision::Allow | Decision::AllowWithConstraints)
+}
+
+fn apply_decision_contract(
+ mut action: ActionEnvelope,
+ decision: &ProverDecision,
+) -> ActionEnvelope {
+ if let Some(speed_max) = decision.constraints.speed_mps_max {
+ action.speed_mps = action.speed_mps.min(speed_max);
+ }
+ if let Some(force_max) = decision.constraints.force_n_max {
+ action.force_n = action.force_n.min(force_max);
+ }
+ action
+}
+
+fn unmet_execution_obligation(
+ action: &ActionEnvelope,
+ decision: &ProverDecision,
+) -> Option<&'static str> {
+ let pause_for_human = decision
+ .obligations
+ .iter()
+ .any(|value| value == "pause_if_human_distance_below_0_5m")
+ && action.context.human_distance_m < 0.5;
+ pause_for_human.then_some("pause_if_human_distance_below_0_5m")
+}
+
+fn check_action(action: &ActionEnvelope) -> ProverDecision {
+ let start = Instant::now();
+ let malformed = malformed_action_reasons(action);
+ if !malformed.is_empty() {
+ let mut violations = vec!["malformed_action_envelope".to_owned()];
+ violations.extend(malformed);
+ return ProverDecision {
+ decision: Decision::Deny,
+ solver_ms: start.elapsed().as_secs_f64() * 1000.0,
+ violations,
+ constraints: DecisionConstraints::default(),
+ obligations: vec!["emit_audit_event".to_owned()],
+ counterexample: None,
+ };
+ }
+
+ let facts = derive_policy_facts(action);
+ let solved = solve_policy(&facts);
+ let decision = solved.unwrap_or(Decision::Deny);
+ let mut violations = Vec::new();
+ if facts.out_of_bounds {
+ violations.push("workspace_bounds".to_owned());
+ }
+ if facts.restricted_hit.is_some() {
+ violations.push("restricted_zone_intersection".to_owned());
+ }
+ if facts.stale_sensor {
+ violations.push("sensor_stale".to_owned());
+ }
+ if facts.capability_expansion {
+ violations.push("capability_expansion".to_owned());
+ }
+ if facts.budget_blocked {
+ violations.push("budget_requires_approval".to_owned());
+ }
+ if facts.human_speed_cap {
+ violations.push("human_speed_cap".to_owned());
+ }
+ if facts.fragile_force_cap {
+ violations.push("fragile_force_cap".to_owned());
+ }
+ if let Err(reason) = solved {
+ violations.push(reason.to_owned());
+ }
+
+ let mut constraints = DecisionConstraints::default();
+ if facts.human_speed_cap {
+ constraints.speed_mps_max = Some(0.08);
+ }
+ if facts.fragile_force_cap {
+ constraints.force_n_max = Some(2.0);
+ }
+ if facts.restricted_hit.is_some() {
+ constraints.bypass_z_min = Some(0.55);
+ }
+
+ let mut obligations = vec!["emit_audit_event".to_owned()];
+ if action.context.human_distance_m < 0.75 {
+ obligations.push("pause_if_human_distance_below_0_5m".to_owned());
+ }
+ let counterexample = facts.restricted_hit.map(|(zone, point)| Counterexample {
+ segment_id: "proposed_segment".to_owned(),
+ zone_id: Some(zone.id),
+ reason: "restricted_zone_intersection".to_owned(),
+ point: Some(point),
+ });
+
+ ProverDecision {
+ decision,
+ solver_ms: start.elapsed().as_secs_f64() * 1000.0,
+ violations,
+ constraints,
+ obligations,
+ counterexample,
+ }
+}
+
+fn seeded_world(seed: u64, goal: String) -> WorldState {
+ let mut rng = TinyRng::new(seed);
+ let restricted_x = 0.15 + rng.range(-0.08, 0.08);
+ WorldState {
+ goal,
+ seed,
+ objects: vec![
+ SceneObject {
+ id: "green_vial".to_owned(),
+ label: "Green block".to_owned(),
+ class_name: "standard".to_owned(),
+ color: "#31b66b".to_owned(),
+ position: [-0.72 + rng.range(-0.05, 0.05), 0.18, -0.46],
+ size: [0.18, 0.26, 0.18],
+ sorted: false,
+ },
+ SceneObject {
+ id: "blue_tray".to_owned(),
+ label: "Blue tray".to_owned(),
+ class_name: "tray".to_owned(),
+ color: "#3f7dda".to_owned(),
+ position: [0.83, 0.08, 0.28],
+ size: [0.62, 0.08, 0.42],
+ sorted: true,
+ },
+ ],
+ zones: vec![
+ Zone {
+ id: "restricted_zone.alpha".to_owned(),
+ label: "Restricted".to_owned(),
+ color: "#e44f5e".to_owned(),
+ position: [restricted_x, 0.52, 0.04],
+ size: [0.56, 1.04, 0.72],
+ },
+ Zone {
+ id: "caution_zone.human".to_owned(),
+ label: "Caution".to_owned(),
+ color: "#e8b630".to_owned(),
+ position: [-0.12, 0.15, -0.46],
+ size: [0.96, 0.3, 0.56],
+ },
+ ],
+ human: HumanState {
+ position: [1.08, 0.08, -0.72],
+ radius: 0.24,
+ caution: 0.54,
+ distance_m: 1.42,
+ },
+ metrics: Metrics {
+ fps: 60,
+ decision_count: 0,
+ p95_solver_ms: 0.0,
+ task_budget_pct: 84,
+ compute_budget_pct: 21,
+ actions_left: 11,
+ sensor_age_ms: 72,
+ },
+ }
+}
+
+fn action_for(
+ world: &WorldState,
+ object_id: &str,
+ path: Vec,
+ speed_mps: f64,
+ force_n: f64,
+ capability: &str,
+) -> ActionEnvelope {
+ let object = world
+ .objects
+ .iter()
+ .find(|object| object.id == object_id)
+ .cloned()
+ .unwrap_or_else(|| world.objects[0].clone());
+ ActionEnvelope {
+ actor: "planner-agent".to_owned(),
+ subagent: "motion-agent".to_owned(),
+ action: "arm.move_segment".to_owned(),
+ resource: "so101.sim.arm".to_owned(),
+ from: path.first().copied().unwrap_or(HOME),
+ to: path.last().copied().unwrap_or(HOME),
+ path,
+ speed_mps,
+ force_n,
+ object_id: object.id,
+ object_class: object.class_name,
+ capability: capability.to_owned(),
+ context: context_for(world, speed_mps),
+ }
+}
+
+fn action_from_plan(world: &WorldState, plan: &AgentPlan) -> ActionEnvelope {
+ let plan = normalize_plan(plan.clone(), world);
+ action_for(
+ world,
+ "green_vial",
+ plan.path,
+ plan.speed_mps,
+ 1.8,
+ "move_lab_samples",
+ )
+}
+
+fn pickup_point(world: &WorldState) -> Vec3 {
+ world
+ .objects
+ .iter()
+ .find(|object| object.id == "green_vial")
+ .map(|object| {
+ [
+ object.position[0],
+ object.position[1] + object.size[1] * 0.65,
+ object.position[2],
+ ]
+ })
+ .unwrap_or([-0.72, 0.35, -0.46])
+}
+
+fn place_point() -> Vec3 {
+ [0.84, 0.34, 0.3]
+}
+
+fn direct_path(world: &WorldState) -> Vec {
+ let pickup = pickup_point(world);
+ vec![
+ HOME,
+ pickup,
+ [-0.42, 0.52, -0.22],
+ [0.24, 0.31, 0.04],
+ place_point(),
+ ]
+}
+
+fn north_bypass_path(world: &WorldState) -> Vec {
+ let pickup = pickup_point(world);
+ vec![
+ HOME,
+ pickup,
+ [-0.92, 0.72, 0.72],
+ [-0.18, 0.72, 0.72],
+ [0.68, 0.54, 0.66],
+ place_point(),
+ ]
+}
+
+fn normalize_plan(mut plan: AgentPlan, world: &WorldState) -> AgentPlan {
+ plan.path
+ .retain(|point| point.iter().all(|value| value.is_finite()));
+ if plan.path.len() < 2 {
+ plan.path = direct_path(world);
+ }
+ if distance(*plan.path.first().unwrap_or(&HOME), HOME) > 0.08 {
+ plan.path.insert(0, HOME);
+ }
+ let pickup = pickup_point(world);
+ let target = place_point();
+ let pickup_index = plan
+ .path
+ .iter()
+ .position(|point| distance(*point, pickup) <= 0.18);
+ match pickup_index {
+ Some(index) if index <= 2 => {
+ plan.path[index] = pickup;
+ }
+ Some(index) => {
+ plan.path.remove(index);
+ plan.path.insert(1, pickup);
+ }
+ None => {
+ plan.path.insert(1, pickup);
+ }
+ }
+ if plan.path.len() > 7 {
+ let last = plan.path.last().copied().unwrap_or(target);
+ plan.path.truncate(7);
+ if distance(last, target) <= 0.12 {
+ let final_index = plan.path.len().saturating_sub(1);
+ plan.path[final_index] = target;
+ }
+ }
+ if distance(*plan.path.last().unwrap_or(&target), target) > 0.12 {
+ plan.path.push(target);
+ } else if let Some(last) = plan.path.last_mut() {
+ *last = target;
+ }
+ plan.speed_mps = plan.speed_mps.clamp(0.04, 0.35);
+ if plan.rationale.trim().is_empty() {
+ plan.rationale = "Proposed waypoint path.".to_owned();
+ }
+ plan
+}
+
+fn distance(a: Vec3, b: Vec3) -> f64 {
+ ((a[0] - b[0]).powi(2) + (a[1] - b[1]).powi(2) + (a[2] - b[2]).powi(2)).sqrt()
+}
+
+fn context_for(world: &WorldState, requested_speed_cap_mps: f64) -> ActionContext {
+ ActionContext {
+ human_distance_m: world.human.distance_m,
+ sensor_age_ms: world.metrics.sensor_age_ms,
+ remaining_budget_ms: (world.metrics.task_budget_pct as u64) * 180,
+ restricted_zones: world
+ .zones
+ .iter()
+ .filter(|zone| zone.id.starts_with("restricted"))
+ .cloned()
+ .collect(),
+ caution_zones: world
+ .zones
+ .iter()
+ .filter(|zone| zone.id.starts_with("caution"))
+ .cloned()
+ .collect(),
+ parent_speed_cap_mps: 0.2,
+ requested_speed_cap_mps,
+ }
+}
+
+fn inside_workspace(point: Vec3) -> bool {
+ (0..3).all(|idx| point[idx] >= WORKSPACE_MIN[idx] && point[idx] <= WORKSPACE_MAX[idx])
+}
+
+fn first_path_intersection(path: &[Vec3], zones: &[Zone]) -> Option<(Zone, Vec3)> {
+ if path.len() < 2 {
+ return None;
+ }
+ for segment in path.windows(2) {
+ if let Some(hit) = first_zone_intersection(segment[0], segment[1], zones) {
+ return Some(hit);
+ }
+ }
+ None
+}
+
+fn first_zone_intersection(from: Vec3, to: Vec3, zones: &[Zone]) -> Option<(Zone, Vec3)> {
+ for zone in zones {
+ for step in 0..=32 {
+ let t = step as f64 / 32.0;
+ let point = [
+ from[0] + (to[0] - from[0]) * t,
+ from[1] + (to[1] - from[1]) * t,
+ from[2] + (to[2] - from[2]) * t,
+ ];
+ if point_inside_zone(point, zone) {
+ return Some((zone.clone(), point));
+ }
+ }
+ }
+ None
+}
+
+fn point_inside_zone(point: Vec3, zone: &Zone) -> bool {
+ (0..3).all(|idx| {
+ let half = zone.size[idx] / 2.0;
+ point[idx] >= zone.position[idx] - half && point[idx] <= zone.position[idx] + half
+ })
+}
+
+fn decision_summary(decision: &ProverDecision) -> String {
+ let verdict = match decision.decision {
+ Decision::Allow => "ALLOW",
+ Decision::Deny => "DENY",
+ Decision::AllowWithConstraints => "CONSTRAIN",
+ Decision::ApprovalRequired => "APPROVAL",
+ };
+ let invariant = decision
+ .violations
+ .first()
+ .map(String::as_str)
+ .unwrap_or("policy_satisfied");
+ let detail = match decision.decision {
+ Decision::Deny => {
+ let point = decision
+ .counterexample
+ .as_ref()
+ .and_then(|counterexample| counterexample.point)
+ .map(|point| format!(" @ [{:.2}, {:.2}, {:.2}]", point[0], point[1], point[2]))
+ .unwrap_or_default();
+ let bypass = decision
+ .constraints
+ .bypass_z_min
+ .map(|value| format!("; bypass_z_min={value:.2}"))
+ .unwrap_or_default();
+ format!("{invariant}{point}{bypass}")
+ }
+ Decision::AllowWithConstraints => constraint_summary(&decision.constraints),
+ Decision::ApprovalRequired => invariant.to_owned(),
+ Decision::Allow => invariant.to_owned(),
+ };
+ format!("{verdict} · {detail} · {:.2} ms", decision.solver_ms)
+}
+
+fn constraint_summary(constraints: &DecisionConstraints) -> String {
+ let mut parts = Vec::new();
+ if let Some(value) = constraints.speed_mps_max {
+ parts.push(format!("speed_mps_max={value:.2}"));
+ }
+ if let Some(value) = constraints.force_n_max {
+ parts.push(format!("force_n_max={value:.1}"));
+ }
+ if let Some(value) = constraints.bypass_z_min {
+ parts.push(format!("bypass_z_min={value:.2}"));
+ }
+ if parts.is_empty() {
+ "no_constraints".to_owned()
+ } else {
+ parts.join("; ")
+ }
+}
+
+fn p95(samples: &[f64]) -> f64 {
+ if samples.is_empty() {
+ return 0.0;
+ }
+ let mut sorted = samples.to_vec();
+ sorted.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+ let index = ((sorted.len() as f64 * 0.95).ceil() as usize).saturating_sub(1);
+ sorted[index]
+}
+
+#[derive(Debug, Serialize)]
+struct BenchmarkReport {
+ schema_version: u8,
+ generated_unix_seconds: u64,
+ platform: String,
+ rustc_version: String,
+ z3_crate_version: String,
+ source_revision: String,
+ build_profile: String,
+ samples_per_case: usize,
+ warmup_per_case: usize,
+ z3_timeout_ms: u64,
+ geometry_samples_per_segment: usize,
+ cases: Vec,
+}
+
+#[derive(Debug, Serialize)]
+struct BenchmarkCase {
+ outcome: String,
+ waypoints: usize,
+ segments: usize,
+ samples: usize,
+ p50_ms: f64,
+ p95_ms: f64,
+ p99_ms: f64,
+ mean_ms: f64,
+ min_ms: f64,
+ max_ms: f64,
+ decisions_per_second: f64,
+}
+
+fn run_policy_benchmark() -> Result<()> {
+ let samples_per_case = env::var("POLICY_PROVER_BENCHMARK_SAMPLES")
+ .ok()
+ .and_then(|value| value.parse().ok())
+ .unwrap_or(500);
+ let warmup_per_case = env::var("POLICY_PROVER_BENCHMARK_WARMUP")
+ .ok()
+ .and_then(|value| value.parse().ok())
+ .unwrap_or(100);
+ let platform =
+ env::var("POLICY_PROVER_BENCHMARK_PLATFORM").unwrap_or_else(|_| "unspecified".to_owned());
+ let output_path = env::var("POLICY_PROVER_BENCHMARK_OUTPUT").ok();
+ let rustc_version =
+ env::var("POLICY_PROVER_BENCHMARK_RUSTC").unwrap_or_else(|_| "rustc 1.89.0".to_owned());
+ let source_revision = env::var("POLICY_PROVER_BENCHMARK_REVISION")
+ .unwrap_or_else(|_| "local-uncommitted".to_owned());
+ let waypoint_counts = [3, 6, 12, 24, 48];
+ let mut cases = Vec::new();
+
+ for outcome in ["allow", "deny", "constrain"] {
+ for waypoints in waypoint_counts {
+ let action = benchmark_action(outcome, waypoints);
+ let expected = match outcome {
+ "allow" => Decision::Allow,
+ "deny" => Decision::Deny,
+ "constrain" => Decision::AllowWithConstraints,
+ _ => unreachable!(),
+ };
+ let observed = check_action(&action);
+ anyhow::ensure!(
+ observed.decision == expected,
+ "benchmark case {outcome}/{waypoints} returned {:?}",
+ observed.decision
+ );
+
+ for _ in 0..warmup_per_case {
+ black_box(check_action(black_box(&action)));
+ }
+
+ let started = Instant::now();
+ let mut timings = Vec::with_capacity(samples_per_case);
+ for _ in 0..samples_per_case {
+ let decision = black_box(check_action(black_box(&action)));
+ timings.push(decision.solver_ms);
+ }
+ let wall_seconds = started.elapsed().as_secs_f64();
+ timings.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
+ let mean_ms = timings.iter().sum::() / timings.len() as f64;
+ cases.push(BenchmarkCase {
+ outcome: outcome.to_owned(),
+ waypoints,
+ segments: waypoints - 1,
+ samples: samples_per_case,
+ p50_ms: percentile(&timings, 0.50),
+ p95_ms: percentile(&timings, 0.95),
+ p99_ms: percentile(&timings, 0.99),
+ mean_ms,
+ min_ms: timings.first().copied().unwrap_or(0.0),
+ max_ms: timings.last().copied().unwrap_or(0.0),
+ decisions_per_second: samples_per_case as f64 / wall_seconds,
+ });
+ }
+ }
+
+ let report = BenchmarkReport {
+ schema_version: 1,
+ generated_unix_seconds: SystemTime::now().duration_since(UNIX_EPOCH)?.as_secs(),
+ platform,
+ rustc_version,
+ z3_crate_version: "0.19.15 (bundled Z3 4.16.0)".to_owned(),
+ source_revision,
+ build_profile: "release".to_owned(),
+ samples_per_case,
+ warmup_per_case,
+ z3_timeout_ms: POLICY_SOLVER_TIMEOUT_MS,
+ geometry_samples_per_segment: 33,
+ cases,
+ };
+ let json = serde_json::to_string_pretty(&report)?;
+ if let Some(path) = output_path {
+ if let Some(parent) = std::path::Path::new(&path).parent() {
+ fs::create_dir_all(parent)?;
+ }
+ fs::write(path, format!("{json}\n"))?;
+ } else {
+ println!("{json}");
+ }
+ Ok(())
+}
+
+fn benchmark_action(outcome: &str, waypoint_count: usize) -> ActionEnvelope {
+ let mut world = seeded_world(42, "benchmark".to_owned());
+ let safe_path = [HOME, [-0.18, 0.72, 0.72], place_point()];
+ let denied_path = [HOME, [0.24, 0.31, 0.04], place_point()];
+ let path = match outcome {
+ "deny" => densify_path(&denied_path, waypoint_count),
+ _ => densify_path(&safe_path, waypoint_count),
+ };
+ if outcome == "constrain" {
+ world.human.distance_m = 0.48;
+ }
+ let mut action = action_for(
+ &world,
+ "green_vial",
+ path,
+ if outcome == "constrain" { 0.2 } else { 0.08 },
+ if outcome == "constrain" { 3.0 } else { 1.8 },
+ "move_lab_samples",
+ );
+ if outcome == "constrain" {
+ action.object_class = "fragile".to_owned();
+ }
+ action
+}
+
+fn densify_path(points: &[Vec3], waypoint_count: usize) -> Vec {
+ assert!(points.len() >= 2);
+ assert!(waypoint_count >= points.len());
+ let segments = points.len() - 1;
+ let total_edges = waypoint_count - 1;
+ let base_edges = total_edges / segments;
+ let remainder = total_edges % segments;
+ let mut result = Vec::with_capacity(waypoint_count);
+ result.push(points[0]);
+
+ for segment_index in 0..segments {
+ let edges = base_edges + usize::from(segment_index < remainder);
+ for edge in 1..=edges {
+ let t = edge as f64 / edges as f64;
+ result.push([
+ points[segment_index][0]
+ + (points[segment_index + 1][0] - points[segment_index][0]) * t,
+ points[segment_index][1]
+ + (points[segment_index + 1][1] - points[segment_index][1]) * t,
+ points[segment_index][2]
+ + (points[segment_index + 1][2] - points[segment_index][2]) * t,
+ ]);
+ }
+ }
+ result
+}
+
+fn percentile(sorted_samples: &[f64], quantile: f64) -> f64 {
+ if sorted_samples.is_empty() {
+ return 0.0;
+ }
+ let index = ((sorted_samples.len() as f64 * quantile).ceil() as usize)
+ .saturating_sub(1)
+ .min(sorted_samples.len() - 1);
+ sorted_samples[index]
+}
+
+fn fixture_plan(world: &WorldState, prior: Option<&ProverDecision>) -> AgentPlan {
+ let (path, speed_mps, rationale) = if prior.is_some() {
+ (
+ north_bypass_path(world),
+ 0.18,
+ "Revised waypoint path from prover feedback.".to_owned(),
+ )
+ } else {
+ (
+ direct_path(world),
+ 0.32,
+ "Shortest efficient waypoint path.".to_owned(),
+ )
+ };
+ AgentPlan {
+ path,
+ speed_mps,
+ rationale,
+ source: "Fixture agent".to_owned(),
+ inference_ms: None,
+ }
+}
+
+fn normalize_agent_plan(
+ mut plan: AgentPlan,
+ source: &str,
+ inference_ms: f64,
+ world: &WorldState,
+) -> AgentPlan {
+ plan.source = source.to_owned();
+ plan.inference_ms = Some(inference_ms);
+ let trimmed = plan.rationale.trim();
+ plan.rationale = if trimmed.chars().count() > 160 {
+ format!("{}...", trimmed.chars().take(157).collect::())
+ } else {
+ trimmed.to_owned()
+ };
+ normalize_plan(plan, world)
+}
+
+async fn ask_openai_plan(world: &WorldState, prior: Option<&ProverDecision>) -> Result {
+ let key = env::var("OPENAI_API_KEY").context("OPENAI_API_KEY not set")?;
+ let model = env::var("OPENAI_MODEL")
+ .or_else(|_| env::var("LLM_MODEL"))
+ .or_else(|_| env::var("FAST_MODEL"))
+ .unwrap_or_else(|_| "gpt-4.1-mini".to_owned());
+ let base_url =
+ env::var("OPENAI_BASE_URL").unwrap_or_else(|_| "https://api.openai.com/v1".to_owned());
+ let client = Client::new();
+ let inference_started = Instant::now();
+ let agent_observation = serde_json::json!({
+ "seed": world.seed,
+ "goal": world.goal,
+ "objects": world.objects,
+ "policy_feedback_geometry": if prior.is_some() {
+ serde_json::json!({ "restricted_zones": world.zones.iter().filter(|zone| zone.id.starts_with("restricted")).collect::>() })
+ } else {
+ serde_json::Value::Null
+ },
+ "home": HOME,
+ "pickup": pickup_point(world),
+ "target": [0.84, 0.34, 0.3],
+ "note": "The planner sees task geometry. Formal policy constraints are checked separately by the policy prover."
+ });
+ let prompt = serde_json::json!({
+ "goal": world.goal,
+ "agent_observation": agent_observation,
+ "prior_prover_decision": prior,
+ "coordinate_system": {
+ "x": "left to right across the table",
+ "y": "height above the table",
+ "z": "front/back across the table",
+ "home": HOME,
+ "pickup": pickup_point(world),
+ "target": [0.84, 0.34, 0.3]
+ },
+ "instruction": "Return the next tool-head waypoint path for moving the green block to the blue tray. The path must start at home, visit pickup before crossing the table, then end at target. For the first proposal, optimize for the shortest efficient path; do not invent or apply safety policies. If prior_prover_decision is present, use only its violation ids, counterexample point, constraints, and policy_feedback_geometry to revise the path while preserving the pickup waypoint. Return JSON only with path, speed_mps, and rationale. Do not claim the path is safe."
+ });
+ let schema = serde_json::json!({
+ "type": "object",
+ "properties": {
+ "path": {
+ "type": "array",
+ "minItems": 2,
+ "maxItems": 8,
+ "items": {
+ "type": "array",
+ "minItems": 3,
+ "maxItems": 3,
+ "items": { "type": "number" }
+ }
+ },
+ "speed_mps": { "type": "number", "minimum": 0.04, "maximum": 0.35 },
+ "rationale": { "type": "string" }
+ },
+ "required": ["path", "speed_mps", "rationale"],
+ "additionalProperties": false
+ });
+
+ if !base_url.contains("api.openai.com") {
+ let body = serde_json::json!({
+ "model": model.clone(),
+ "messages": [
+ { "role": "system", "content": "You are a robotics path planner. Return only JSON with path, speed_mps, and rationale. Plan waypoints, not safety judgments; the OpenShell policy prover has final authority." },
+ { "role": "user", "content": prompt.to_string() }
+ ]
+ });
+ let response = client
+ .post(format!(
+ "{}/chat/completions",
+ base_url.trim_end_matches('/')
+ ))
+ .bearer_auth(key)
+ .json(&body)
+ .send()
+ .await?;
+ let status = response.status();
+ let response: serde_json::Value = response.json().await?;
+ if !status.is_success() {
+ let detail = response
+ .pointer("/error/message")
+ .and_then(|value| value.as_str())
+ .or_else(|| response.get("detail").and_then(|value| value.as_str()))
+ .unwrap_or("no error detail returned");
+ anyhow::bail!("planner endpoint returned {status}: {detail}");
+ }
+ let response_model = response
+ .get("model")
+ .and_then(|value| value.as_str())
+ .unwrap_or(&model)
+ .to_owned();
+ let text = response
+ .pointer("/choices/0/message/content")
+ .and_then(|value| value.as_str())
+ .context("missing chat completion content")?;
+ let source = format!("{response_model} via OpenAI-compatible endpoint");
+ return Ok(normalize_agent_plan(
+ parse_plan_text(text)?,
+ &source,
+ inference_started.elapsed().as_secs_f64() * 1000.0,
+ world,
+ ));
+ }
+
+ let body = serde_json::json!({
+ "model": model.clone(),
+ "input": [
+ { "role": "system", "content": "You are a robotics path planner. Return path waypoints, not safety judgments; the OpenShell policy prover has final authority." },
+ { "role": "user", "content": prompt.to_string() }
+ ],
+ "text": {
+ "format": {
+ "type": "json_schema",
+ "name": "robot_path_plan",
+ "schema": schema,
+ "strict": true
+ }
+ }
+ });
+ let response: serde_json::Value = client
+ .post(format!("{}/responses", base_url.trim_end_matches('/')))
+ .bearer_auth(key)
+ .json(&body)
+ .send()
+ .await?
+ .error_for_status()?
+ .json()
+ .await?;
+ let response_model = response
+ .get("model")
+ .and_then(|value| value.as_str())
+ .unwrap_or(&model)
+ .to_owned();
+ let text = response
+ .pointer("/output/0/content/0/text")
+ .and_then(|value| value.as_str())
+ .or_else(|| response.get("output_text").and_then(|value| value.as_str()))
+ .context("missing structured output text")?;
+ let source = format!("{response_model} via OpenAI Responses API");
+ Ok(normalize_agent_plan(
+ parse_plan_text(text)?,
+ &source,
+ inference_started.elapsed().as_secs_f64() * 1000.0,
+ world,
+ ))
+}
+
+fn parse_plan_text(text: &str) -> Result {
+ if let Ok(plan) = serde_json::from_str(text) {
+ return Ok(plan);
+ }
+ let start = text.find('{').context("missing JSON object start")?;
+ let end = text.rfind('}').context("missing JSON object end")?;
+ let plan = serde_json::from_str(&text[start..=end])?;
+ Ok(plan)
+}
+
+struct TinyRng {
+ state: u64,
+}
+
+impl TinyRng {
+ fn new(seed: u64) -> Self {
+ Self { state: seed.max(1) }
+ }
+
+ fn next(&mut self) -> f64 {
+ self.state ^= self.state << 13;
+ self.state ^= self.state >> 7;
+ self.state ^= self.state << 17;
+ (self.state as f64 / u64::MAX as f64).clamp(0.0, 1.0)
+ }
+
+ fn range(&mut self, min: f64, max: f64) -> f64 {
+ min + (max - min) * self.next()
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn denies_restricted_zone_crossing() {
+ let world = seeded_world(42, "test".to_owned());
+ let action = action_for(
+ &world,
+ "green_vial",
+ vec![HOME, [0.24, 0.31, 0.04], [0.84, 0.34, 0.3]],
+ 0.2,
+ 2.0,
+ "move_lab_samples",
+ );
+ let decision = check_action(&action);
+ assert_eq!(decision.decision, Decision::Deny);
+ assert!(decision
+ .violations
+ .contains(&"restricted_zone_intersection".to_owned()));
+ }
+
+ #[test]
+ fn constrains_fragile_sample_near_human() {
+ let mut world = seeded_world(7, "test".to_owned());
+ world.human.distance_m = 0.48;
+ let mut action = action_for(
+ &world,
+ "green_vial",
+ vec![
+ HOME,
+ [-0.94, 0.82, 0.72],
+ [-0.15, 0.74, 0.7],
+ [0.7, 0.56, 0.58],
+ [0.84, 0.34, 0.3],
+ ],
+ 0.2,
+ 3.0,
+ "move_lab_samples",
+ );
+ action.object_class = "fragile".to_owned();
+ let decision = check_action(&action);
+ assert_eq!(decision.decision, Decision::AllowWithConstraints);
+ assert_eq!(decision.constraints.speed_mps_max, Some(0.08));
+ assert_eq!(decision.constraints.force_n_max, Some(2.0));
+ }
+
+ #[test]
+ fn denies_capability_expansion() {
+ let world = seeded_world(9, "test".to_owned());
+ let action = ActionEnvelope {
+ actor: "planner-agent".to_owned(),
+ subagent: "optimizer-subagent".to_owned(),
+ action: "grant.capability".to_owned(),
+ resource: "delegated-motion-capability".to_owned(),
+ from: HOME,
+ to: [0.4, 0.4, 0.2],
+ path: vec![HOME, [0.4, 0.4, 0.2]],
+ speed_mps: 0.5,
+ force_n: 1.0,
+ object_id: "mission-budget".to_owned(),
+ object_class: "governance".to_owned(),
+ capability: "unrestricted_motion".to_owned(),
+ context: context_for(&world, 0.5),
+ };
+ let decision = check_action(&action);
+ assert_eq!(decision.decision, Decision::Deny);
+ assert!(decision
+ .violations
+ .contains(&"capability_expansion".to_owned()));
+ }
+
+ #[test]
+ fn denies_malformed_action_envelope() {
+ let world = seeded_world(11, "test".to_owned());
+ let mut action = action_for(
+ &world,
+ "green_vial",
+ vec![HOME, [-0.18, 0.72, 0.72], place_point()],
+ 0.08,
+ 1.0,
+ "move_lab_samples",
+ );
+ action.path.truncate(1);
+ let decision = check_action(&action);
+ assert_eq!(decision.decision, Decision::Deny);
+ assert!(decision
+ .violations
+ .contains(&"malformed_action_envelope".to_owned()));
+ }
+
+ #[test]
+ fn applies_constraints_to_effective_action() {
+ let world = seeded_world(12, "test".to_owned());
+ let action = action_for(
+ &world,
+ "green_vial",
+ vec![HOME, [-0.18, 0.72, 0.72], place_point()],
+ 0.2,
+ 3.0,
+ "move_lab_samples",
+ );
+ let decision = ProverDecision {
+ decision: Decision::AllowWithConstraints,
+ solver_ms: 0.0,
+ violations: Vec::new(),
+ constraints: DecisionConstraints {
+ speed_mps_max: Some(0.08),
+ force_n_max: Some(2.0),
+ bypass_z_min: None,
+ },
+ obligations: Vec::new(),
+ counterexample: None,
+ };
+ let effective = apply_decision_contract(action, &decision);
+ assert_eq!(effective.speed_mps, 0.08);
+ assert_eq!(effective.force_n, 2.0);
+ }
+
+ #[test]
+ fn pause_obligation_is_checked_before_execution() {
+ let mut world = seeded_world(13, "test".to_owned());
+ world.human.distance_m = 0.48;
+ let action = action_for(
+ &world,
+ "green_vial",
+ vec![HOME, [-0.18, 0.72, 0.72], place_point()],
+ 0.2,
+ 1.0,
+ "move_lab_samples",
+ );
+ let decision = check_action(&action);
+ assert_eq!(decision.decision, Decision::AllowWithConstraints);
+ assert_eq!(
+ unmet_execution_obligation(&action, &decision),
+ Some("pause_if_human_distance_below_0_5m")
+ );
+ }
+
+ #[tokio::test]
+ async fn approval_required_never_reaches_executor() {
+ let world = seeded_world(14, "test".to_owned());
+ let (tx, mut rx) = broadcast::channel(8);
+ let mut runtime = MissionRuntime {
+ tx,
+ world: world.clone(),
+ started: Instant::now(),
+ solver_samples: Vec::new(),
+ agent_mode: AgentMode::Fixture,
+ };
+ let mut action = action_for(
+ &world,
+ "green_vial",
+ vec![HOME, [-0.18, 0.72, 0.72], place_point()],
+ 0.08,
+ 1.0,
+ "move_lab_samples",
+ );
+ action.action = "mission.reserve_budget".to_owned();
+ action.context.remaining_budget_ms = 100;
+
+ runtime.check_and_execute(action, None).await.unwrap();
+
+ let events = std::iter::from_fn(|| rx.try_recv().ok()).collect::>();
+ assert_eq!(events.len(), 1);
+ assert_eq!(
+ events[0].decision.as_ref().map(|value| value.decision),
+ Some(Decision::ApprovalRequired)
+ );
+ assert!(events[0].approved_path.is_none());
+ assert!(!events.iter().any(|event| event.kind == "execution_update"));
+ }
+
+ #[test]
+ fn normalizes_plan_to_visit_pickup_before_place() {
+ let world = seeded_world(42, "test".to_owned());
+ let plan = AgentPlan {
+ path: vec![HOME, [0.72, 0.5, 0.62], place_point()],
+ speed_mps: 0.2,
+ rationale: "bad repair skipped pickup".to_owned(),
+ source: "test".to_owned(),
+ inference_ms: None,
+ };
+ let action = action_from_plan(&world, &plan);
+ assert!(distance(action.path[1], pickup_point(&world)) <= 0.01);
+ assert_eq!(action.path.last().copied(), Some(place_point()));
+ }
+}
diff --git a/projects/robotics-policy-prover/scripts/verify-visual.mjs b/projects/robotics-policy-prover/scripts/verify-visual.mjs
new file mode 100644
index 00000000..d869a6f3
--- /dev/null
+++ b/projects/robotics-policy-prover/scripts/verify-visual.mjs
@@ -0,0 +1,74 @@
+import { mkdir } from "node:fs/promises";
+import { chromium } from "playwright";
+
+const url = process.env.DEMO_URL ?? "http://127.0.0.1:5173";
+const viewports = [
+ { name: "desktop", width: 1440, height: 900 },
+ { name: "mobile", width: 390, height: 844 },
+];
+
+function assert(condition, message) {
+ if (!condition) {
+ throw new Error(message);
+ }
+}
+
+await mkdir("screenshots", { recursive: true });
+
+const browser = await chromium.launch({
+ executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH || undefined,
+});
+try {
+ for (const viewport of viewports) {
+ const page = await browser.newPage({ viewport });
+ await page.goto(url, { waitUntil: "domcontentloaded" });
+ await page.waitForSelector("canvas");
+ await page.waitForTimeout(1400);
+
+ const stats = await page.evaluate(() => {
+ const canvas = document.querySelector("canvas");
+ const sample = document.createElement("canvas");
+ sample.width = 120;
+ sample.height = 80;
+ const context = sample.getContext("2d", { willReadFrequently: true });
+ context.drawImage(canvas, 0, 0, sample.width, sample.height);
+ const image = context.getImageData(0, 0, sample.width, sample.height).data;
+ let brightPixels = 0;
+ let darkPixels = 0;
+ let colorSpread = 0;
+
+ for (let index = 0; index < image.length; index += 4) {
+ const red = image[index];
+ const green = image[index + 1];
+ const blue = image[index + 2];
+ const luma = red * 0.2126 + green * 0.7152 + blue * 0.0722;
+ if (luma > 180) brightPixels += 1;
+ if (luma < 110) darkPixels += 1;
+ if (Math.abs(red - green) + Math.abs(green - blue) > 35) colorSpread += 1;
+ }
+
+ return {
+ width: canvas.width,
+ height: canvas.height,
+ brightPixels,
+ darkPixels,
+ colorSpread,
+ };
+ });
+
+ assert(stats.width > 0 && stats.height > 0, `${viewport.name}: canvas has no size`);
+ assert(stats.brightPixels > 150, `${viewport.name}: canvas lacks lit geometry`);
+ assert(stats.darkPixels > 20, `${viewport.name}: canvas lacks shaded geometry`);
+ assert(stats.colorSpread > 60, `${viewport.name}: canvas lacks colored policy elements`);
+
+ await page.screenshot({
+ path: `screenshots/${viewport.name}.png`,
+ fullPage: true,
+ });
+ await page.close();
+ }
+} finally {
+ await browser.close();
+}
+
+console.log(`Visual verification passed for ${viewports.length} viewports at ${url}`);
diff --git a/projects/robotics-policy-prover/src/App.jsx b/projects/robotics-policy-prover/src/App.jsx
new file mode 100644
index 00000000..d79b1efd
--- /dev/null
+++ b/projects/robotics-policy-prover/src/App.jsx
@@ -0,0 +1,364 @@
+import {
+ AlertTriangle,
+ CheckCircle2,
+ ChevronDown,
+ Clock3,
+ Gauge,
+ Play,
+ Radar,
+ RefreshCcw,
+ ShieldCheck,
+} from "lucide-react";
+import { useCallback, useEffect, useMemo, useRef, useState } from "react";
+import RobotScene from "./RobotScene.jsx";
+import { homePosition } from "./scenarios.js";
+
+const DEFAULT_GOAL = "Move the green block into the blue tray.";
+
+const fallbackWorld = {
+ goal: DEFAULT_GOAL,
+ seed: 42,
+ objects: [
+ {
+ id: "green_vial",
+ label: "Green block",
+ class_name: "standard",
+ color: "#31b66b",
+ position: [-0.72, 0.18, -0.46],
+ size: [0.18, 0.26, 0.18],
+ sorted: false,
+ },
+ {
+ id: "blue_tray",
+ label: "Blue tray",
+ class_name: "tray",
+ color: "#3f7dda",
+ position: [0.83, 0.08, 0.28],
+ size: [0.62, 0.08, 0.42],
+ sorted: true,
+ },
+ ],
+ zones: [
+ {
+ id: "restricted_zone.alpha",
+ label: "Restricted",
+ color: "#e44f5e",
+ position: [0.15, 0.52, 0.04],
+ size: [0.56, 1.04, 0.72],
+ },
+ {
+ id: "caution_zone.human",
+ label: "Caution",
+ color: "#e8b630",
+ position: [-0.12, 0.15, -0.46],
+ size: [0.96, 0.3, 0.56],
+ },
+ ],
+ human: {
+ position: [1.08, 0.08, -0.72],
+ radius: 0.24,
+ caution: 0.54,
+ distance_m: 1.42,
+ },
+ metrics: {
+ fps: 60,
+ decision_count: 0,
+ p95_solver_ms: 0,
+ task_budget_pct: 84,
+ compute_budget_pct: 21,
+ actions_left: 3,
+ sensor_age_ms: 72,
+ },
+};
+
+const decisionMeta = {
+ allow: { label: "Allowed", className: "decision-allow", icon: CheckCircle2 },
+ allow_with_constraints: {
+ label: "Constrained",
+ className: "decision-constrain",
+ icon: ShieldCheck,
+ },
+ deny: { label: "Denied", className: "decision-deny", icon: AlertTriangle },
+ approval_required: { label: "Needs Approval", className: "decision-approval", icon: Clock3 },
+ pending: { label: "Ready", className: "decision-pending", icon: Radar },
+};
+
+function normalizeStep(event, previous) {
+ const decision = event?.decision?.decision ?? previous?.decision ?? "pending";
+ let approvedPath = previous?.approvedPath ?? [homePosition];
+ if (event && Object.hasOwn(event, "approved_path")) {
+ const blocked = decision === "deny" || decision === "approval_required";
+ approvedPath = event.approved_path ?? (blocked ? [homePosition] : approvedPath);
+ }
+
+ return {
+ id: event?.id ?? previous?.id ?? "idle",
+ title: event?.summary ?? previous?.title ?? "Click Run Experiment",
+ decision,
+ action: event?.action ?? previous?.action,
+ proposedPath: event?.proposed_path ?? previous?.proposedPath ?? [homePosition],
+ approvedPath,
+ highlight: event?.highlight ?? previous?.highlight,
+ executing:
+ event?.kind === "execution_update" &&
+ event?.highlight?.kind === "execution" &&
+ (event?.approved_path?.length ?? 0) > 1,
+ };
+}
+
+function DecisionBadge({ decision }) {
+ const meta = decisionMeta[decision] ?? decisionMeta.pending;
+ const Icon = meta.icon;
+ return (
+
+
+ {meta.label}
+
+ );
+}
+
+function storyLabel(event) {
+ if (event.kind === "agent_plan") return "Agent";
+ if (event.kind === "prover_decision") return "Policy prover";
+ if (event.kind === "execution_update" || event.kind === "execution_blocked") return "Executor";
+ return "World";
+}
+
+function storyTone(event) {
+ if (event.kind === "agent_plan") return "agent";
+ if (event.kind === "prover_decision") return event.decision?.decision ?? "prover";
+ if (event.kind === "execution_update" || event.kind === "execution_blocked") return "executor";
+ return "world";
+}
+
+function compactSummary(event) {
+ return event.summary;
+}
+
+function sanitizeDecision(decision) {
+ if (!decision) return null;
+ return {
+ decision: decision.decision,
+ solver_ms: Number(decision.solver_ms?.toFixed?.(2) ?? decision.solver_ms),
+ violations: decision.violations,
+ constraints: decision.constraints,
+ obligations: decision.obligations,
+ counterexample: decision.counterexample,
+ };
+}
+
+function App() {
+ const [seed, setSeed] = useState(42);
+ const [agentMode, setAgentMode] = useState("openai");
+ const [world, setWorld] = useState(fallbackWorld);
+ const [events, setEvents] = useState([]);
+ const [step, setStep] = useState(() => normalizeStep(null, null));
+ const [playbackSpeed, setPlaybackSpeed] = useState(1);
+ const [session, setSession] = useState(null);
+ const sourceRef = useRef(null);
+
+ const storyEvents = useMemo(
+ () => events.filter((event) => ["agent_plan", "prover_decision", "world_event", "execution_update", "execution_blocked"].includes(event.kind)),
+ [events],
+ );
+ const latestDecision = step.decision ?? "pending";
+ const latestDecisionEvent = [...events].reverse().find((event) => event.decision);
+
+ const attachEventSource = useCallback((sessionId) => {
+ sourceRef.current?.close();
+ const source = new EventSource(`/api/sessions/${sessionId}/events`);
+ sourceRef.current = source;
+
+ const handle = (message) => {
+ const event = JSON.parse(message.data);
+ setEvents((items) => [...items.slice(-40), event]);
+ if (event.world) setWorld(event.world);
+ setStep((previous) => normalizeStep(event, previous));
+ };
+
+ ["world_event", "agent_plan", "prover_decision", "execution_update", "execution_blocked", "error"].forEach((kind) =>
+ source.addEventListener(kind, handle),
+ );
+ }, []);
+
+ const startExperiment = useCallback(async () => {
+ setEvents([]);
+ setStep(normalizeStep(null, null));
+ const response = await fetch("/api/sessions", {
+ method: "POST",
+ headers: { "Content-Type": "application/json" },
+ body: JSON.stringify({ goal: DEFAULT_GOAL, seed: Number(seed), agent_mode: agentMode }),
+ });
+ const data = await response.json();
+ setSession(data);
+ setAgentMode(data.agent_mode);
+ attachEventSource(data.session_id);
+ }, [agentMode, attachEventSource, seed]);
+
+ useEffect(() => () => sourceRef.current?.close(), []);
+
+ return (
+
+
+
+
+
+
+
+
Agent Path vs Policy Prover
+
One tool-path experiment: propose, prove, adapt, execute.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {step.title}
+
+
+
+ Agent path
+ Admitted path
+ Restricted
+ Human
+
+
+
+
+
+
+ );
+}
+
+export default App;
diff --git a/projects/robotics-policy-prover/src/RobotScene.jsx b/projects/robotics-policy-prover/src/RobotScene.jsx
new file mode 100644
index 00000000..d0fd424e
--- /dev/null
+++ b/projects/robotics-policy-prover/src/RobotScene.jsx
@@ -0,0 +1,238 @@
+import { Canvas, useFrame } from "@react-three/fiber";
+import { ContactShadows, Edges, Line, OrbitControls } from "@react-three/drei";
+import { Suspense, useMemo, useRef, useState } from "react";
+import * as THREE from "three";
+import { homePosition } from "./scenarios.js";
+
+const gantryHome = new THREE.Vector3(-1.08, 0.98, 0.58);
+
+function toVector(point) {
+ return new THREE.Vector3(point[0], point[1], point[2]);
+}
+
+function samplePath(points, progress) {
+ if (!points.length) return toVector(homePosition);
+ if (points.length === 1) return toVector(points[0]);
+
+ const scaled = THREE.MathUtils.clamp(progress, 0, 1) * (points.length - 1);
+ const index = Math.min(Math.floor(scaled), points.length - 2);
+ const local = scaled - index;
+ return toVector(points[index]).lerp(toVector(points[index + 1]), local);
+}
+
+function segmentQuaternion(start, end) {
+ const direction = new THREE.Vector3().subVectors(end, start);
+ const quaternion = new THREE.Quaternion();
+ quaternion.setFromUnitVectors(new THREE.Vector3(0, 1, 0), direction.clone().normalize());
+ return quaternion;
+}
+
+function Segment({ start, end, radius = 0.045, color = "#54616f" }) {
+ const startVec = useMemo(() => toVector(start), [start]);
+ const endVec = useMemo(() => toVector(end), [end]);
+ const midpoint = useMemo(() => startVec.clone().lerp(endVec, 0.5), [startVec, endVec]);
+ const length = useMemo(() => startVec.distanceTo(endVec), [startVec, endVec]);
+ const quaternion = useMemo(() => segmentQuaternion(startVec, endVec), [startVec, endVec]);
+
+ return (
+
+
+
+
+ );
+}
+
+function ToolHead({ step, playbackSpeed }) {
+ const path = step.approvedPath?.length > 1 ? step.approvedPath : [homePosition];
+ const [tip, setTip] = useState(() => toVector(path[0]));
+ const progress = useRef(0);
+ const pausePulse = !step.executing;
+
+ useFrame((_, delta) => {
+ const pace = step.executing ? playbackSpeed : 0;
+ progress.current = (progress.current + delta * 0.18 * pace) % 1;
+ const next = samplePath(path, pausePulse ? 0 : progress.current);
+ setTip(next);
+ });
+
+ const lift = useMemo(() => tip.clone().setY(gantryHome.y), [tip]);
+ const tipArray = tip.toArray();
+ const liftArray = lift.toArray();
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function carriedPosition(object, path, progress) {
+ if (object.id !== "green_vial" || !path?.length || path.length < 2) return object.position;
+ const original = toVector(object.position);
+ const pickupIndex = path.reduce(
+ (best, point, index) => {
+ const distance = toVector(point).distanceTo(original);
+ return distance < best.distance ? { index, distance } : best;
+ },
+ { index: 0, distance: Infinity },
+ );
+ const scaled = THREE.MathUtils.clamp(progress, 0, 1) * (path.length - 1);
+ if (scaled < pickupIndex.index) return object.position;
+ const tip = samplePath(path, progress);
+ return [tip.x, Math.max(object.position[1], tip.y - 0.18), tip.z];
+}
+
+function WorkcellObjects({ objects, step, playbackSpeed }) {
+ const path = step.approvedPath?.length > 1 ? step.approvedPath : [];
+ const [progress, setProgress] = useState(0);
+ const progressRef = useRef(0);
+
+ useFrame((_, delta) => {
+ const shouldMove = path.length > 1 && step.executing;
+ const pace = shouldMove ? playbackSpeed : 0;
+ progressRef.current = (progressRef.current + delta * 0.18 * pace) % 1;
+ setProgress(progressRef.current);
+ });
+
+ return (
+
+ {objects.map((object) => (
+
+
+
+
+
+ ))}
+
+ );
+}
+
+function Zone({ zone }) {
+ return (
+
+
+
+
+
+ );
+}
+
+function MotionPath({ points, color, opacity = 1, dashed = false }) {
+ if (!points?.length || points.length < 2) return null;
+ return (
+
+ );
+}
+
+function HumanPresence({ human, decision }) {
+ const warning = decision === "allow_with_constraints" || decision === "approval_required";
+ return (
+
+
+
+
+
+
+
+
+
+
+ );
+}
+
+function SceneContent({ world, step, playbackSpeed }) {
+ const approvedColor =
+ step.decision === "deny"
+ ? "#e45454"
+ : step.decision === "approval_required" || step.decision === "allow_with_constraints"
+ ? "#e8b630"
+ : "#32bd77";
+
+ return (
+ <>
+
+
+
+
+
+
+
+
+
+
+ {world.zones.map((zone) => (
+
+ ))}
+
+
+
+
+
+
+
+
+ >
+ );
+}
+
+export default function RobotScene({ world, step, playbackSpeed }) {
+ return (
+
+ );
+}
diff --git a/projects/robotics-policy-prover/src/main.jsx b/projects/robotics-policy-prover/src/main.jsx
new file mode 100644
index 00000000..9b3fe1ce
--- /dev/null
+++ b/projects/robotics-policy-prover/src/main.jsx
@@ -0,0 +1,10 @@
+import React from "react";
+import { createRoot } from "react-dom/client";
+import App from "./App.jsx";
+import "./styles.css";
+
+createRoot(document.getElementById("root")).render(
+
+
+ ,
+);
diff --git a/projects/robotics-policy-prover/src/scenarios.js b/projects/robotics-policy-prover/src/scenarios.js
new file mode 100644
index 00000000..9deef430
--- /dev/null
+++ b/projects/robotics-policy-prover/src/scenarios.js
@@ -0,0 +1,297 @@
+export const homePosition = [-1.08, 0.78, 0.58];
+
+export const policyPacks = [
+ {
+ id: "workspace",
+ name: "Workspace bounds",
+ tone: "green",
+ invariant: "No path segment may leave the configured workcell.",
+ },
+ {
+ id: "human",
+ name: "Human proximity",
+ tone: "amber",
+ invariant: "Motion must slow near humans and stop inside the hard radius.",
+ },
+ {
+ id: "materials",
+ name: "Sample handling",
+ tone: "cyan",
+ invariant: "Fragile or hazardous samples require reduced speed and force.",
+ },
+ {
+ id: "delegation",
+ name: "Delegation",
+ tone: "violet",
+ invariant: "Subagents cannot expand authority beyond the parent grant.",
+ },
+ {
+ id: "budget",
+ name: "Budget envelope",
+ tone: "rose",
+ invariant: "Plans must stay inside time, action, and compute limits.",
+ },
+ {
+ id: "audit",
+ name: "Audit obligations",
+ tone: "blue",
+ invariant: "Every physical action must emit a durable decision record.",
+ },
+];
+
+export const objects = [
+ {
+ id: "green-vial",
+ label: "Green vial",
+ color: "#31b66b",
+ position: [-0.72, 0.18, -0.46],
+ size: [0.18, 0.26, 0.18],
+ },
+ {
+ id: "red-sample",
+ label: "Red sample",
+ color: "#e45454",
+ position: [-0.22, 0.15, -0.68],
+ size: [0.22, 0.2, 0.22],
+ },
+ {
+ id: "yellow-core",
+ label: "Yellow core",
+ color: "#e7b92f",
+ position: [0.26, 0.16, -0.48],
+ size: [0.2, 0.22, 0.2],
+ },
+ {
+ id: "blue-tray",
+ label: "Blue tray",
+ color: "#3f7dda",
+ position: [0.83, 0.08, 0.28],
+ size: [0.62, 0.08, 0.42],
+ },
+];
+
+export const zones = [
+ {
+ id: "restricted",
+ label: "Restricted",
+ color: "#e44f5e",
+ position: [0.22, 0.24, 0.04],
+ size: [0.56, 0.48, 0.72],
+ },
+ {
+ id: "caution",
+ label: "Caution",
+ color: "#e8b630",
+ position: [-0.12, 0.15, -0.46],
+ size: [0.96, 0.3, 0.56],
+ },
+];
+
+export const scenarios = [
+ {
+ id: "normal-autonomy",
+ label: "Normal",
+ title: "Normal Autonomy",
+ actor: "planner-agent",
+ subagent: "motion-agent",
+ action: "arm.move_path",
+ object: "green-vial",
+ resource: "so101.sim.arm",
+ decision: "allow",
+ risk: "Low",
+ speed: "0.18 m/s",
+ force: "2.1 N",
+ solverMs: 6,
+ budget: 84,
+ compute: 21,
+ remainingActions: 11,
+ humanDistance: "1.42 m",
+ sensorAge: "72 ms",
+ proposedPath: [
+ homePosition,
+ [-0.86, 0.72, 0.12],
+ [-0.72, 0.38, -0.46],
+ [0.58, 0.46, 0.2],
+ [0.83, 0.34, 0.28],
+ ],
+ approvedPath: [
+ homePosition,
+ [-0.86, 0.72, 0.12],
+ [-0.72, 0.38, -0.46],
+ [0.58, 0.46, 0.2],
+ [0.83, 0.34, 0.28],
+ ],
+ human: { position: [1.1, 0.08, -0.72], radius: 0.22, caution: 0.52 },
+ activePolicies: ["workspace", "audit"],
+ constraints: ["Path remains inside workcell", "Decision record required"],
+ obligations: ["Emit audit event", "Expire capability after 4s"],
+ explanation:
+ "The proposed move stays inside workspace bounds and uses an unprivileged object-handling grant.",
+ },
+ {
+ id: "human-composition",
+ label: "Human near",
+ title: "Human Proximity Composition",
+ actor: "planner-agent",
+ subagent: "motion-agent",
+ action: "arm.move_path",
+ object: "red-sample",
+ resource: "so101.sim.arm",
+ decision: "constrain",
+ risk: "Medium",
+ speed: "0.42 -> 0.08 m/s",
+ force: "3.8 -> 2.0 N",
+ solverMs: 13,
+ budget: 67,
+ compute: 38,
+ remainingActions: 7,
+ humanDistance: "0.48 m",
+ sensorAge: "88 ms",
+ proposedPath: [
+ homePosition,
+ [-0.55, 0.62, -0.1],
+ [-0.22, 0.34, -0.68],
+ [0.34, 0.32, -0.12],
+ [0.83, 0.34, 0.28],
+ ],
+ approvedPath: [
+ homePosition,
+ [-0.72, 0.82, 0.0],
+ [-0.36, 0.62, -0.62],
+ [0.12, 0.58, -0.5],
+ [0.68, 0.52, 0.08],
+ [0.83, 0.34, 0.28],
+ ],
+ human: { position: [0.12, 0.08, -0.18], radius: 0.28, caution: 0.68 },
+ activePolicies: ["workspace", "human", "materials", "audit"],
+ constraints: [
+ "Clamp speed to <= 0.08 m/s",
+ "Clamp gripper force to <= 2.0 N",
+ "Route outside caution overlap",
+ ],
+ obligations: [
+ "Pause if human_distance < 0.5 m",
+ "Recheck before gripper.close",
+ "Emit audit event",
+ ],
+ explanation:
+ "Human proximity and fragile-sample policies both apply, so the action is allowed only as a slower alternate path.",
+ },
+ {
+ id: "restricted-zone",
+ label: "No-go zone",
+ title: "Restricted Zone Denial",
+ actor: "planner-agent",
+ subagent: "motion-agent",
+ action: "arm.move_path",
+ object: "yellow-core",
+ resource: "so101.sim.arm",
+ decision: "deny",
+ risk: "High",
+ speed: "0.25 m/s",
+ force: "3.2 N",
+ solverMs: 10,
+ budget: 59,
+ compute: 44,
+ remainingActions: 6,
+ humanDistance: "1.08 m",
+ sensorAge: "64 ms",
+ proposedPath: [
+ homePosition,
+ [-0.44, 0.58, 0.08],
+ [0.22, 0.34, 0.04],
+ [0.7, 0.34, 0.22],
+ ],
+ approvedPath: [homePosition],
+ human: { position: [1.08, 0.08, -0.62], radius: 0.24, caution: 0.52 },
+ activePolicies: ["workspace", "materials", "audit"],
+ constraints: ["No feasible path through restricted_zone"],
+ obligations: ["Keep prior capability unchanged", "Record denial"],
+ explanation:
+ "The candidate trajectory intersects the restricted volume, and no override grant is present for that region.",
+ },
+ {
+ id: "delegation",
+ label: "Delegation",
+ title: "Subagent Delegation Check",
+ actor: "planner-agent",
+ subagent: "cleanup-subagent",
+ action: "grant.capability",
+ object: "so101.sim.arm",
+ resource: "delegated-motion-capability",
+ decision: "deny",
+ risk: "High",
+ speed: "n/a",
+ force: "n/a",
+ solverMs: 17,
+ budget: 46,
+ compute: 56,
+ remainingActions: 4,
+ humanDistance: "0.93 m",
+ sensorAge: "101 ms",
+ proposedPath: [
+ homePosition,
+ [-0.62, 0.68, 0.32],
+ [0.38, 0.44, -0.02],
+ [0.92, 0.38, -0.42],
+ ],
+ approvedPath: [homePosition],
+ human: { position: [0.92, 0.08, -0.48], radius: 0.24, caution: 0.52 },
+ activePolicies: ["delegation", "human", "audit"],
+ constraints: [
+ "Parent grant excludes restricted_zone",
+ "Child grant requested unrestricted move_path",
+ "Delegated speed cap exceeds parent speed cap",
+ ],
+ obligations: ["Reject broader grant", "Record attempted escalation"],
+ explanation:
+ "The subagent requested a capability that is broader than its parent envelope, including restricted-zone access and a higher speed cap.",
+ },
+ {
+ id: "budget",
+ label: "Budget",
+ title: "Budget-Constrained Autonomy",
+ actor: "planner-agent",
+ subagent: "optimizer-subagent",
+ action: "plan.extend",
+ object: "sorting-task",
+ resource: "mission-budget",
+ decision: "approval",
+ risk: "Medium",
+ speed: "0.12 m/s",
+ force: "1.7 N",
+ solverMs: 9,
+ budget: 8,
+ compute: 86,
+ remainingActions: 1,
+ humanDistance: "0.81 m",
+ sensorAge: "79 ms",
+ proposedPath: [
+ homePosition,
+ [-0.56, 0.8, -0.12],
+ [-0.12, 0.56, -0.64],
+ [0.68, 0.52, 0.12],
+ [0.92, 0.34, 0.34],
+ ],
+ approvedPath: [
+ homePosition,
+ [-0.72, 0.72, -0.02],
+ [0.42, 0.5, 0.12],
+ [0.83, 0.34, 0.28],
+ ],
+ human: { position: [0.64, 0.08, -0.58], radius: 0.25, caution: 0.54 },
+ activePolicies: ["budget", "workspace", "audit"],
+ constraints: [
+ "One final move may execute",
+ "No replanning loop without supervisor approval",
+ "Compute budget remains below 90%",
+ ],
+ obligations: ["Request supervisor approval for extension", "Close task after final move"],
+ explanation:
+ "The current task has enough budget for one bounded action, but continued optimization requires approval.",
+ },
+];
+
+export function policyTone(id) {
+ return policyPacks.find((policy) => policy.id === id)?.tone ?? "blue";
+}
diff --git a/projects/robotics-policy-prover/src/styles.css b/projects/robotics-policy-prover/src/styles.css
new file mode 100644
index 00000000..9ff99a1c
--- /dev/null
+++ b/projects/robotics-policy-prover/src/styles.css
@@ -0,0 +1,1068 @@
+:root {
+ color-scheme: light;
+ font-family:
+ Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
+ background: #eef1ed;
+ color: #202328;
+ font-synthesis: none;
+ text-rendering: optimizeLegibility;
+ -webkit-font-smoothing: antialiased;
+}
+
+* {
+ box-sizing: border-box;
+}
+
+body {
+ margin: 0;
+ min-width: 320px;
+ min-height: 100vh;
+ background:
+ linear-gradient(180deg, rgba(255, 255, 255, 0.52), rgba(255, 255, 255, 0)),
+ #eef1ed;
+}
+
+button,
+input,
+select {
+ font: inherit;
+}
+
+.app-shell {
+ width: min(100%, 1680px);
+ min-height: 100vh;
+ margin: 0 auto;
+ padding: 18px;
+ display: grid;
+ grid-template-rows: auto minmax(0, 1fr) auto;
+ gap: 14px;
+}
+
+.topbar {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+ min-height: 68px;
+}
+
+.brand-lockup {
+ display: flex;
+ align-items: center;
+ gap: 13px;
+ min-width: 0;
+}
+
+.brand-mark,
+.icon-button {
+ width: 42px;
+ height: 42px;
+ display: inline-grid;
+ place-items: center;
+ border-radius: 8px;
+ border: 1px solid rgba(31, 35, 40, 0.12);
+ background: #ffffff;
+ color: #253038;
+ box-shadow: 0 10px 24px rgba(31, 35, 40, 0.08);
+}
+
+.brand-mark {
+ color: #13875a;
+}
+
+h1,
+h2,
+p,
+dl {
+ margin: 0;
+}
+
+h1 {
+ font-size: clamp(1.15rem, 2vw, 1.55rem);
+ line-height: 1.08;
+ letter-spacing: 0;
+}
+
+.brand-lockup p {
+ margin-top: 4px;
+ font-size: 0.9rem;
+ color: #667076;
+}
+
+.topbar-actions {
+ display: flex;
+ align-items: center;
+ justify-content: flex-end;
+ gap: 9px;
+ flex-wrap: wrap;
+}
+
+.icon-button {
+ cursor: pointer;
+ transition:
+ transform 160ms ease,
+ border-color 160ms ease,
+ background-color 160ms ease;
+}
+
+.icon-button:hover,
+.icon-button.is-active {
+ transform: translateY(-1px);
+ border-color: rgba(27, 126, 90, 0.42);
+ background: #f7fbf8;
+}
+
+.run-button {
+ height: 42px;
+ display: inline-flex;
+ align-items: center;
+ justify-content: center;
+ gap: 8px;
+ padding: 0 14px;
+ border: 1px solid rgba(19, 135, 90, 0.38);
+ border-radius: 8px;
+ background: #17875d;
+ color: #ffffff;
+ box-shadow: 0 12px 26px rgba(19, 135, 90, 0.2);
+ cursor: pointer;
+ font-weight: 900;
+}
+
+.run-button:hover {
+ background: #126f4c;
+}
+
+.speed-control {
+ height: 42px;
+ display: grid;
+ grid-template-columns: auto auto minmax(96px, 140px) 48px;
+ align-items: center;
+ gap: 9px;
+ padding: 0 11px;
+ border: 1px solid rgba(31, 35, 40, 0.12);
+ border-radius: 8px;
+ background: #ffffff;
+ color: #4d5962;
+ box-shadow: 0 10px 24px rgba(31, 35, 40, 0.07);
+}
+
+.seed-control {
+ height: 42px;
+ display: grid;
+ grid-template-columns: auto minmax(76px, 118px);
+ align-items: center;
+ gap: 8px;
+ padding: 0 10px;
+ border: 1px solid rgba(31, 35, 40, 0.12);
+ border-radius: 8px;
+ background: #ffffff;
+ color: #4d5962;
+ box-shadow: 0 10px 24px rgba(31, 35, 40, 0.07);
+}
+
+.seed-control span {
+ font-size: 0.78rem;
+ font-weight: 800;
+}
+
+.seed-control input,
+.seed-control select {
+ width: 100%;
+ min-width: 0;
+ border: 1px solid rgba(31, 35, 40, 0.13);
+ border-radius: 7px;
+ background: #f7f9f7;
+ color: #202328;
+ padding: 6px 7px;
+ font-size: 0.8rem;
+}
+
+.speed-control input {
+ width: 100%;
+ accent-color: #19865f;
+}
+
+.speed-control strong {
+ font-size: 0.78rem;
+ color: #202328;
+ text-align: right;
+}
+
+.demo-layout {
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(360px, 420px);
+ gap: 14px;
+}
+
+.simple-shell {
+ grid-template-rows: auto minmax(0, 1fr);
+}
+
+.simple-layout {
+ min-height: 0;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) minmax(310px, 340px);
+ align-items: start;
+ gap: 14px;
+}
+
+.story-panel {
+ min-height: 0;
+ overflow: auto;
+ display: grid;
+ align-content: start;
+ gap: 10px;
+}
+
+.explain-strip {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 10px;
+}
+
+.explain-strip div {
+ min-width: 0;
+ display: grid;
+ gap: 4px;
+ padding: 11px 12px;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.74);
+ box-shadow: 0 12px 28px rgba(31, 35, 40, 0.07);
+}
+
+.explain-strip strong {
+ color: #253038;
+ font-size: 0.84rem;
+}
+
+.explain-strip span {
+ color: #647079;
+ font-size: 0.78rem;
+ line-height: 1.3;
+}
+
+.scene-panel,
+.inspector-section,
+.scenario-rail {
+ border: 1px solid rgba(31, 35, 40, 0.12);
+ background: rgba(255, 255, 255, 0.78);
+ box-shadow: 0 18px 42px rgba(31, 35, 40, 0.09);
+}
+
+.scene-panel {
+ position: relative;
+ width: 100%;
+ min-height: 0;
+ aspect-ratio: 16 / 9;
+ overflow: hidden;
+ border-radius: 8px;
+}
+
+.scene-panel canvas {
+ display: block;
+}
+
+.scene-status {
+ position: absolute;
+ z-index: 4;
+ top: 16px;
+ left: 16px;
+ right: 16px;
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ pointer-events: none;
+}
+
+.scene-title {
+ min-width: 0;
+ padding: 9px 11px;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.84);
+ color: #34404a;
+ font-size: 0.86rem;
+ font-weight: 700;
+ box-shadow: 0 10px 22px rgba(31, 35, 40, 0.08);
+}
+
+.legend-strip {
+ position: absolute;
+ z-index: 4;
+ left: 16px;
+ right: 16px;
+ bottom: 16px;
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: 8px;
+ pointer-events: none;
+}
+
+.legend-strip span {
+ min-width: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ gap: 7px;
+ padding: 8px 9px;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.82);
+ color: #3e474f;
+ font-size: 0.78rem;
+ font-weight: 700;
+}
+
+.legend-dot {
+ width: 9px;
+ height: 9px;
+ border-radius: 50%;
+ flex: 0 0 auto;
+}
+
+.legend-dot.proposed {
+ background: #e45454;
+}
+
+.legend-dot.approved {
+ background: #32bd77;
+}
+
+.legend-dot.restricted {
+ background: #e44f5e;
+}
+
+.legend-dot.human {
+ background: #e8b630;
+}
+
+.inspector {
+ min-height: 0;
+ overflow: auto;
+ display: grid;
+ align-content: start;
+ gap: 10px;
+ padding-right: 2px;
+}
+
+.inspector-section {
+ border-radius: 8px;
+ padding: 14px;
+}
+
+.section-heading {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ margin-bottom: 11px;
+ color: #38434c;
+}
+
+.section-heading h2 {
+ font-size: 0.86rem;
+ line-height: 1.1;
+ letter-spacing: 0;
+}
+
+.primary-decision {
+ border-color: rgba(19, 135, 90, 0.22);
+}
+
+.decision-badge {
+ width: fit-content;
+ max-width: 100%;
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ padding: 8px 10px;
+ border-radius: 8px;
+ font-size: 0.82rem;
+ font-weight: 800;
+ border: 1px solid transparent;
+}
+
+.decision-allow {
+ background: #e9f8ef;
+ color: #137a51;
+ border-color: #bfe8d0;
+}
+
+.decision-constrain {
+ background: #fff6d7;
+ color: #8d640e;
+ border-color: #ead07b;
+}
+
+.decision-deny {
+ background: #ffe7e7;
+ color: #a43a3a;
+ border-color: #efb9b9;
+}
+
+.decision-approval {
+ background: #e9f4ff;
+ color: #2c638f;
+ border-color: #bfdcf4;
+}
+
+.decision-pending {
+ background: #f1f4f1;
+ color: #5d6870;
+ border-color: #d8ded8;
+}
+
+.decision-copy {
+ margin-top: 11px;
+ color: #49545d;
+ font-size: 0.92rem;
+ line-height: 1.45;
+}
+
+.compact-copy {
+ margin-top: 0;
+ margin-bottom: 10px;
+ font-size: 0.82rem;
+}
+
+.metric-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+ margin-top: 12px;
+}
+
+.metric {
+ min-width: 0;
+ display: grid;
+ grid-template-columns: auto minmax(0, 1fr) auto;
+ align-items: center;
+ gap: 7px;
+ padding: 9px;
+ border-radius: 8px;
+ background: #f6f8f6;
+ color: #5b6871;
+ font-size: 0.78rem;
+}
+
+.metric strong {
+ min-width: 0;
+ color: #202328;
+ font-size: 0.78rem;
+ text-align: right;
+ white-space: nowrap;
+}
+
+.risk-low strong {
+ color: #137a51;
+}
+
+.risk-medium strong {
+ color: #92650b;
+}
+
+.risk-high strong {
+ color: #a43a3a;
+}
+
+.action-grid {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 8px;
+}
+
+.action-grid div {
+ min-width: 0;
+ padding: 9px;
+ border-radius: 8px;
+ background: #f6f8f6;
+}
+
+.action-grid dt {
+ color: #738087;
+ font-size: 0.72rem;
+ font-weight: 800;
+ text-transform: uppercase;
+}
+
+.action-grid dd {
+ margin: 4px 0 0;
+ min-width: 0;
+ color: #253038;
+ font-size: 0.82rem;
+ font-weight: 700;
+ overflow-wrap: anywhere;
+}
+
+.policy-chip-row {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 7px;
+}
+
+.policy-chip {
+ display: inline-flex;
+ max-width: 100%;
+ align-items: center;
+ padding: 7px 9px;
+ border-radius: 8px;
+ font-size: 0.76rem;
+ font-weight: 800;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+}
+
+.tone-green {
+ background: #e9f8ef;
+ color: #137a51;
+}
+
+.tone-amber {
+ background: #fff6d7;
+ color: #8d640e;
+}
+
+.tone-cyan {
+ background: #e8fbfb;
+ color: #087276;
+}
+
+.tone-violet {
+ background: #f1eaff;
+ color: #6c4ca3;
+}
+
+.tone-rose {
+ background: #ffe8ef;
+ color: #a13f63;
+}
+
+.tone-blue {
+ background: #e9f4ff;
+ color: #2c638f;
+}
+
+.policy-detail-list {
+ display: grid;
+ gap: 8px;
+ margin-top: 10px;
+}
+
+.policy-detail-list p {
+ display: grid;
+ gap: 3px;
+ padding: 9px;
+ border-radius: 8px;
+ background: #f6f8f6;
+ color: #54616a;
+ font-size: 0.78rem;
+ line-height: 1.34;
+}
+
+.policy-detail-list strong {
+ color: #2e3840;
+}
+
+.policy-stack {
+ display: grid;
+ gap: 7px;
+}
+
+.policy-stack div {
+ display: grid;
+ gap: 3px;
+ padding: 8px 9px;
+ border-radius: 8px;
+ background: #f6f8f6;
+}
+
+.policy-stack strong {
+ color: #2e3840;
+ font-size: 0.78rem;
+}
+
+.policy-stack span {
+ color: #60707a;
+ font-size: 0.76rem;
+ line-height: 1.28;
+}
+
+.terminal-section {
+ max-height: 350px;
+}
+
+.terminal-log {
+ height: 286px;
+ overflow: auto;
+ display: grid;
+ align-content: start;
+ gap: 7px;
+ padding: 8px;
+ border-radius: 8px;
+ background: #202328;
+ color: #f4f7f4;
+}
+
+.terminal-row {
+ width: 100%;
+ display: grid;
+ grid-template-columns: 94px minmax(0, 1fr);
+ gap: 8px;
+ align-items: start;
+ padding: 8px;
+ border: 1px solid rgba(255, 255, 255, 0.08);
+ border-radius: 7px;
+ background: rgba(255, 255, 255, 0.05);
+ color: #f4f7f4;
+ text-align: left;
+ cursor: pointer;
+}
+
+.terminal-row:hover {
+ border-color: rgba(255, 255, 255, 0.25);
+}
+
+.terminal-meta {
+ color: #aeb8b2;
+ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", monospace;
+ font-size: 0.72rem;
+ white-space: nowrap;
+}
+
+.terminal-row > span:last-child {
+ min-width: 0;
+ font-size: 0.78rem;
+ line-height: 1.35;
+ overflow-wrap: anywhere;
+}
+
+.terminal-empty {
+ color: #aeb8b2;
+ font-size: 0.82rem;
+ padding: 10px;
+}
+
+.watch-steps {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ gap: 7px;
+ margin-top: 12px;
+}
+
+.watch-steps span {
+ padding: 8px 9px;
+ border-radius: 8px;
+ background: #f6f8f6;
+ color: #34404a;
+ font-size: 0.78rem;
+ font-weight: 900;
+}
+
+.story-section {
+ max-height: none;
+}
+
+.story-list {
+ display: grid;
+ gap: 8px;
+}
+
+.story-row {
+ width: 100%;
+ display: grid;
+ gap: 4px;
+ padding: 10px;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-left-width: 4px;
+ border-radius: 8px;
+ background: #f8faf8;
+ color: #2f3840;
+ cursor: pointer;
+ text-align: left;
+}
+
+.story-row:hover {
+ background: #ffffff;
+ border-color: rgba(31, 35, 40, 0.18);
+}
+
+.story-row strong {
+ font-size: 0.78rem;
+ text-transform: uppercase;
+ color: #5c6870;
+}
+
+.story-row span {
+ min-width: 0;
+ font-size: 0.86rem;
+ line-height: 1.36;
+ overflow-wrap: anywhere;
+}
+
+.story-row.tone-agent {
+ border-left-color: #3f7dda;
+}
+
+.story-row.tone-allow {
+ border-left-color: #32bd77;
+}
+
+.story-row.tone-allow_with_constraints {
+ border-left-color: #e8b630;
+}
+
+.story-row.tone-deny {
+ border-left-color: #e45454;
+}
+
+.story-row.tone-executor {
+ border-left-color: #9aa59d;
+}
+
+.story-row.tone-world,
+.story-row.tone-prover {
+ border-left-color: #7d67b8;
+}
+
+.details-panel {
+ display: grid;
+ gap: 8px;
+}
+
+.details-panel details {
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-radius: 8px;
+ background: rgba(255, 255, 255, 0.78);
+}
+
+.details-panel summary {
+ min-height: 42px;
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ padding: 0 11px;
+ cursor: pointer;
+ color: #34404a;
+ font-size: 0.82rem;
+ font-weight: 900;
+}
+
+.details-panel details[open] summary svg {
+ transform: rotate(180deg);
+}
+
+.details-panel ul,
+.details-panel p,
+.details-panel pre {
+ margin: 0;
+ padding: 0 12px 12px;
+}
+
+.details-panel ul {
+ display: grid;
+ gap: 6px;
+ padding-left: 28px;
+ color: #52606a;
+ font-size: 0.8rem;
+ line-height: 1.34;
+}
+
+.details-panel p {
+ color: #52606a;
+ font-size: 0.8rem;
+ line-height: 1.4;
+}
+
+.details-panel pre {
+ max-height: 240px;
+ overflow: auto;
+ color: #f4f7f4;
+ background: #202328;
+ border-radius: 0 0 8px 8px;
+ padding-top: 12px;
+ font-size: 0.72rem;
+ line-height: 1.38;
+}
+
+.simple-speed {
+ width: 100%;
+}
+
+.terminal-row.tone-agent {
+ border-left: 3px solid #63b3ed;
+}
+
+.terminal-row.tone-allow {
+ border-left: 3px solid #32bd77;
+}
+
+.terminal-row.tone-allow_with_constraints {
+ border-left: 3px solid #e8b630;
+}
+
+.terminal-row.tone-deny {
+ border-left: 3px solid #e45454;
+}
+
+.terminal-row.tone-approval_required {
+ border-left: 3px solid #3f7dda;
+}
+
+.terminal-row.tone-executor {
+ border-left: 3px solid #9aa59d;
+}
+
+.terminal-row.tone-world,
+.terminal-row.tone-prover {
+ border-left: 3px solid #b894e8;
+}
+
+.plain-list {
+ margin: 0;
+ padding: 0;
+ display: grid;
+ gap: 7px;
+ list-style: none;
+}
+
+.plain-list + .plain-list {
+ margin-top: 9px;
+}
+
+.plain-list li {
+ position: relative;
+ padding: 8px 9px 8px 25px;
+ border-radius: 8px;
+ background: #f6f8f6;
+ color: #43505a;
+ font-size: 0.82rem;
+ line-height: 1.35;
+}
+
+.plain-list li::before {
+ content: "";
+ position: absolute;
+ left: 10px;
+ top: 14px;
+ width: 6px;
+ height: 6px;
+ border-radius: 50%;
+ background: #1f9d68;
+}
+
+.plain-list.obligations li::before {
+ background: #2d74aa;
+}
+
+.budget-row {
+ display: grid;
+ gap: 7px;
+ margin-bottom: 11px;
+}
+
+.budget-label {
+ display: flex;
+ justify-content: space-between;
+ gap: 8px;
+ color: #55616a;
+ font-size: 0.8rem;
+ font-weight: 800;
+}
+
+.budget-track {
+ height: 9px;
+ overflow: hidden;
+ border-radius: 999px;
+ background: #e3e8e4;
+}
+
+.budget-fill {
+ display: block;
+ height: 100%;
+ border-radius: inherit;
+}
+
+.budget-fill.steady {
+ background: #27a96d;
+}
+
+.budget-fill.cool {
+ background: #2d82c7;
+}
+
+.budget-fill.warning {
+ background: #e6a51f;
+}
+
+.budget-fill.danger {
+ background: #d84f5d;
+}
+
+.scenario-rail {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: 8px;
+ padding: 8px;
+ border-radius: 8px;
+}
+
+.mission-controls {
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+}
+
+.scenario-tab {
+ min-width: 0;
+ min-height: 56px;
+ display: grid;
+ align-content: center;
+ gap: 4px;
+ padding: 8px 10px;
+ border: 1px solid rgba(31, 35, 40, 0.1);
+ border-radius: 8px;
+ background: #f7f9f7;
+ color: #2f3840;
+ text-align: left;
+ transition:
+ transform 150ms ease,
+ background-color 150ms ease,
+ border-color 150ms ease;
+}
+
+button.scenario-tab {
+ cursor: pointer;
+}
+
+.scenario-tab:hover,
+.scenario-tab.is-selected {
+ transform: translateY(-1px);
+ background: #ffffff;
+ border-color: rgba(19, 135, 90, 0.38);
+}
+
+.scenario-tab span {
+ min-width: 0;
+ font-size: 0.86rem;
+ font-weight: 900;
+ overflow-wrap: anywhere;
+}
+
+.scenario-tab small {
+ min-width: 0;
+ color: #68747c;
+ font-size: 0.72rem;
+ font-weight: 800;
+ overflow-wrap: anywhere;
+}
+
+@media (max-width: 1080px) {
+ .demo-layout {
+ grid-template-columns: 1fr;
+ }
+
+ .simple-layout {
+ grid-template-columns: 1fr;
+ align-items: stretch;
+ }
+
+ .inspector {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ overflow: visible;
+ padding-right: 0;
+ }
+
+ .explain-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .primary-decision {
+ grid-column: 1 / -1;
+ }
+
+ .story-panel {
+ overflow: visible;
+ }
+}
+
+@media (max-width: 720px) {
+ .app-shell {
+ padding: 10px;
+ }
+
+ .topbar {
+ align-items: stretch;
+ flex-direction: column;
+ }
+
+ .topbar-actions {
+ justify-content: stretch;
+ }
+
+ .speed-control {
+ flex: 1 1 210px;
+ }
+
+ .seed-control {
+ flex: 1 1 160px;
+ }
+
+ .scene-panel {
+ aspect-ratio: auto;
+ min-height: 430px;
+ }
+
+ .legend-strip {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .inspector {
+ grid-template-columns: 1fr;
+ }
+
+ .metric-grid,
+ .action-grid {
+ grid-template-columns: 1fr;
+ }
+
+ .scenario-rail {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .mission-controls {
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ }
+
+ .terminal-row {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 560px) {
+ .explain-strip {
+ grid-template-columns: 1fr;
+ }
+}
+
+@media (max-width: 430px) {
+ .scene-status {
+ align-items: flex-start;
+ flex-direction: column;
+ }
+
+ .legend-strip {
+ left: 10px;
+ right: 10px;
+ bottom: 10px;
+ gap: 6px;
+ }
+
+ .legend-strip span {
+ justify-content: flex-start;
+ font-size: 0.71rem;
+ padding: 7px;
+ }
+
+ .scene-panel {
+ min-height: 380px;
+ }
+}
diff --git a/projects/robotics-policy-prover/vite.config.js b/projects/robotics-policy-prover/vite.config.js
new file mode 100644
index 00000000..ac9821e3
--- /dev/null
+++ b/projects/robotics-policy-prover/vite.config.js
@@ -0,0 +1,11 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ proxy: {
+ "/api": "http://127.0.0.1:8787",
+ },
+ },
+});
diff --git a/zensical.toml b/zensical.toml
index d9c7e499..3003ecca 100644
--- a/zensical.toml
+++ b/zensical.toml
@@ -19,6 +19,7 @@ nav = [
"dev-notes/index.md",
{"Posts" = [
# dev-notes:nav:start
+ {"Can Formal Methods Govern AI-Generated Robot Actions?" = "dev-notes/posts/2026-08-07-formal-methods-ai-generated-robot-actions.md"},
{"Bringing Privacy and Security to the Edge with OpenShell" = "dev-notes/posts/2026-07-20-policy-controlling-reachy-mini-with-openshell.md"}
# dev-notes:nav:end
]}