diff --git a/.env.example b/.env.example
index 0549a5b..940108b 100644
--- a/.env.example
+++ b/.env.example
@@ -40,6 +40,9 @@ LAZYBOY_API_KEY=
# 正式 UI:先 `cd web && npm run build`,再指到編譯結果。開發用 Vite 時不必設。
# LAZYBOY_WEB_DIST=web/dist
+# 關於畫面與設定裡顯示的版號。改了要重開 Vite 或重新 `npm run build`。
+LAZYBOY_VERSION=0.1.0
+
# =============================================================================
# 公開搜尋 / 抓頁
# =============================================================================
diff --git a/Cargo.lock b/Cargo.lock
index 0095abc..fd8ce7c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -59,6 +59,45 @@ version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
+[[package]]
+name = "asn1-rs"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5493c3bedbacf7fd7382c6346bbd66687d12bbaad3a89a2d2c303ee6cf20b048"
+dependencies = [
+ "asn1-rs-derive",
+ "asn1-rs-impl",
+ "displaydoc",
+ "nom",
+ "num-traits",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
+[[package]]
+name = "asn1-rs-derive"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "965c2d33e53cb6b267e148a4cb0760bc01f4904c1cd4bb4002a085bb016d1490"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+ "synstructure",
+]
+
+[[package]]
+name = "asn1-rs-impl"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "async-compression"
version = "0.4.47"
@@ -125,7 +164,7 @@ dependencies = [
"sync_wrapper",
"tokio",
"tokio-tungstenite",
- "tower",
+ "tower 0.5.3",
"tower-layer",
"tower-service",
]
@@ -328,6 +367,26 @@ version = "2.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4583a4551df46e2792f82ceeac45e850d2e2d5debba0b91f102385cda5b11f06"
+[[package]]
+name = "der-parser"
+version = "9.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5cd0a5c643689626bec213c4d8bd4d96acc8ffdb4ad4bb6bc16abf27d5f4b553"
+dependencies = [
+ "asn1-rs",
+ "displaydoc",
+ "nom",
+ "num-bigint",
+ "num-traits",
+ "rusticata-macros",
+]
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -817,6 +876,12 @@ dependencies = [
"wasm-bindgen",
]
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
[[package]]
name = "lazyboy"
version = "0.1.0"
@@ -833,19 +898,27 @@ version = "0.1.0"
dependencies = [
"anyhow",
"axum",
+ "base64",
"chrono",
"encoding_rs",
"futures-util",
"html2text",
+ "hyper",
+ "hyper-util",
"libc",
+ "rcgen",
"regex",
"reqwest",
"rusqlite",
+ "rustls",
+ "rustls-pemfile",
"serde",
"serde_json",
"thiserror 2.0.20",
"tokio",
+ "tokio-rustls",
"tokio-tungstenite",
+ "tower 0.4.13",
"tower-http 0.5.2",
"uuid",
]
@@ -933,6 +1006,12 @@ dependencies = [
"unicase",
]
+[[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.9.1"
@@ -987,6 +1066,41 @@ version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
+[[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-bigint"
+version = "0.4.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367"
+dependencies = [
+ "num-integer",
+ "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.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ce2d95d4b3734dc35aa2f45e1aa22cd416814592a4f9d9205e11affd5b8e10b"
+dependencies = [
+ "num-traits",
+]
+
[[package]]
name = "num-traits"
version = "0.2.19"
@@ -996,6 +1110,15 @@ dependencies = [
"autocfg",
]
+[[package]]
+name = "oid-registry"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8d8034d9489cdaf79228eb9f6a3b8d7bb32ba00d6645ebd48eef4077ceb5bd9"
+dependencies = [
+ "asn1-rs",
+]
+
[[package]]
name = "once_cell"
version = "1.21.4"
@@ -1025,6 +1148,16 @@ dependencies = [
"windows-link",
]
+[[package]]
+name = "pem"
+version = "3.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be"
+dependencies = [
+ "base64",
+ "serde_core",
+]
+
[[package]]
name = "percent-encoding"
version = "2.3.2"
@@ -1070,6 +1203,26 @@ dependencies = [
"siphasher",
]
+[[package]]
+name = "pin-project"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924"
+dependencies = [
+ "pin-project-internal",
+]
+
+[[package]]
+name = "pin-project-internal"
+version = "1.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.119",
+]
+
[[package]]
name = "pin-project-lite"
version = "0.2.17"
@@ -1091,6 +1244,12 @@ dependencies = [
"zerovec",
]
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
[[package]]
name = "ppv-lite86"
version = "0.2.21"
@@ -1242,6 +1401,20 @@ dependencies = [
"rand_core 0.10.1",
]
+[[package]]
+name = "rcgen"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75e669e5202259b5314d1ea5397316ad400819437857b90861765f24c4cf80a2"
+dependencies = [
+ "pem",
+ "ring",
+ "rustls-pki-types",
+ "time",
+ "x509-parser",
+ "yasna",
+]
+
[[package]]
name = "redox_syscall"
version = "0.5.18"
@@ -1310,7 +1483,7 @@ dependencies = [
"tokio",
"tokio-rustls",
"tokio-util",
- "tower",
+ "tower 0.5.3",
"tower-http 0.6.11",
"tower-service",
"url",
@@ -1355,6 +1528,15 @@ version = "2.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
+[[package]]
+name = "rusticata-macros"
+version = "4.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632"
+dependencies = [
+ "nom",
+]
+
[[package]]
name = "rustls"
version = "0.23.44"
@@ -1369,6 +1551,15 @@ dependencies = [
"zeroize",
]
+[[package]]
+name = "rustls-pemfile"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50"
+dependencies = [
+ "rustls-pki-types",
+]
+
[[package]]
name = "rustls-pki-types"
version = "1.15.1"
@@ -1668,6 +1859,36 @@ dependencies = [
"syn 3.0.5",
]
+[[package]]
+name = "time"
+version = "0.3.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
+dependencies = [
+ "deranged",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
+
+[[package]]
+name = "time-macros"
+version = "0.2.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
[[package]]
name = "tinystr"
version = "0.8.4"
@@ -1755,6 +1976,21 @@ dependencies = [
"tokio",
]
+[[package]]
+name = "tower"
+version = "0.4.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project",
+ "pin-project-lite",
+ "tower-layer",
+ "tower-service",
+ "tracing",
+]
+
[[package]]
name = "tower"
version = "0.5.3"
@@ -1812,7 +2048,7 @@ dependencies = [
"pin-project-lite",
"tokio",
"tokio-util",
- "tower",
+ "tower 0.5.3",
"tower-layer",
"tower-service",
"url",
@@ -1836,6 +2072,7 @@ version = "0.1.44"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
dependencies = [
+ "log",
"pin-project-lite",
"tracing-core",
]
@@ -2222,6 +2459,33 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc"
+[[package]]
+name = "x509-parser"
+version = "0.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcbc162f30700d6f3f82a24bf7cc62ffe7caea42c0b2cba8bf7f3ae50cf51f69"
+dependencies = [
+ "asn1-rs",
+ "data-encoding",
+ "der-parser",
+ "lazy_static",
+ "nom",
+ "oid-registry",
+ "ring",
+ "rusticata-macros",
+ "thiserror 1.0.69",
+ "time",
+]
+
+[[package]]
+name = "yasna"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e17bb3549cc1321ae1296b9cdc2698e2b6cb1992adfa19a8c72e5b7a738f44cd"
+dependencies = [
+ "time",
+]
+
[[package]]
name = "yoke"
version = "0.8.3"
diff --git a/box/Dockerfile b/box/Dockerfile
index 392eeb9..4be1608 100644
--- a/box/Dockerfile
+++ b/box/Dockerfile
@@ -1,8 +1,7 @@
FROM debian:bookworm-slim
ENV DEBIAN_FRONTEND=noninteractive
-LABEL lazyboy.box.revision=computer-use-3
RUN apt-get update && apt-get install -y --no-install-recommends \
- xvfb x11-utils x11vnc novnc websockify imagemagick xdotool \
+ xvfb x11-utils x11-xserver-utils x11vnc novnc websockify autocutsel xclip imagemagick xdotool \
dbus-x11 xfce4-session xfce4-panel xfwm4 xfdesktop4 xfce4-settings \
xfce4-terminal thunar \
fonts-noto-core fonts-noto-cjk \
@@ -22,5 +21,6 @@ RUN chmod +x /usr/local/bin/start-desktop.sh /usr/local/bin/box-chrome
ENV DISPLAY=:1
WORKDIR /workspace
EXPOSE 6080
+LABEL lazyboy.box.revision=computer-use-5
ENTRYPOINT ["/usr/bin/tini", "--"]
CMD ["/usr/local/bin/start-desktop.sh"]
diff --git a/box/start-desktop.sh b/box/start-desktop.sh
index c621b8e..eacda0b 100644
--- a/box/start-desktop.sh
+++ b/box/start-desktop.sh
@@ -45,7 +45,11 @@ startxfce4 >/tmp/xfce.log 2>&1 &
# Only websockify (same container) talks to VNC; keep the passwordless port
# off the Docker bridge so sibling containers cannot attach. (libvncserver
# still binds [::]:5900, but the container has no routable IPv6 address.)
-x11vnc -display "$DISPLAY" -forever -shared -nopw -rfbport 5900 -listen localhost >/tmp/x11vnc.log 2>&1 &
+x11vnc -display "$DISPLAY" -forever -shared -nopw -rfbport 5900 -listen localhost -xkb -repeat -noxdamage >/tmp/x11vnc.log 2>&1 &
+if command -v autocutsel >/dev/null 2>&1; then
+ autocutsel -fork -selection CLIPBOARD >/dev/null 2>&1 || true
+ autocutsel -fork -selection PRIMARY >/dev/null 2>&1 || true
+fi
websockify --web /usr/share/novnc 6080 localhost:5900 >/tmp/novnc.log 2>&1 &
echo "[lazyboy-box] desktop up display=$DISPLAY vnc=6080 xfce chrome terminal" >/tmp/start-desktop.log
diff --git a/crates/lazyboy-core/Cargo.toml b/crates/lazyboy-core/Cargo.toml
index d8cede3..79dcbb4 100644
--- a/crates/lazyboy-core/Cargo.toml
+++ b/crates/lazyboy-core/Cargo.toml
@@ -8,18 +8,26 @@ description = "Minimal GrokBot-like agent core (chat + tools + ReAct)"
[dependencies]
anyhow.workspace = true
axum.workspace = true
+base64 = "0.22"
chrono.workspace = true
encoding_rs = "0.8"
futures-util.workspace = true
html2text = "0.17"
+hyper = { version = "1", features = ["http1", "server"] }
+hyper-util = { version = "0.1", features = ["tokio", "http1", "server"] }
+rcgen = { version = "0.13", features = ["x509-parser"] }
regex = "1"
reqwest.workspace = true
rusqlite = { version = "0.32", features = ["bundled"] }
+rustls = { version = "0.23", default-features = false, features = ["std", "tls12", "ring"] }
+rustls-pemfile = "2"
serde.workspace = true
serde_json.workspace = true
thiserror.workspace = true
tokio.workspace = true
+tokio-rustls = { version = "0.26", default-features = false, features = ["ring", "tls12"] }
tokio-tungstenite.workspace = true
+tower = { version = "0.4", features = ["util"] }
tower-http.workspace = true
uuid.workspace = true
diff --git a/crates/lazyboy-core/src/box_runtime.rs b/crates/lazyboy-core/src/box_runtime.rs
index 9902c8f..32b4030 100644
--- a/crates/lazyboy-core/src/box_runtime.rs
+++ b/crates/lazyboy-core/src/box_runtime.rs
@@ -16,7 +16,7 @@ use tokio::sync::Mutex;
const IMAGE: &str = "lazyboy-box:local";
const CONTAINER: &str = "lazyboy-box";
const VIEWER_PORT: u16 = 6080;
-const BOX_REVISION: &str = "computer-use-3";
+const BOX_REVISION: &str = "computer-use-5";
const BROWSER_PROFILE: &str = "/home/box/chrome-profile";
/// Per-stream cap on shell output returned to the model; the middle is elided.
const JOB_OUTPUT_HEAD_CHARS: usize = 16_000;
diff --git a/crates/lazyboy-core/src/lib.rs b/crates/lazyboy-core/src/lib.rs
index 5279328..0220955 100644
--- a/crates/lazyboy-core/src/lib.rs
+++ b/crates/lazyboy-core/src/lib.rs
@@ -19,6 +19,8 @@ mod model;
mod session;
mod tools;
mod web;
+mod local_tls;
+mod novnc_skin;
mod web_server;
pub use agent::{
@@ -42,7 +44,7 @@ pub use model::{
};
pub use session::{
list_sessions, load_or_create, load_session, public_transcript, public_transcript_from_messages,
- save_session, session_preview, session_preview_from_messages, sessions_dir, Session,
+ save_session, session_last_at_from_messages, session_preview, session_preview_from_messages, sessions_dir, Session,
SessionSummary,
};
pub use web_server::{serve_http, serve_web, WebListen};
diff --git a/crates/lazyboy-core/src/local_tls.rs b/crates/lazyboy-core/src/local_tls.rs
new file mode 100644
index 0000000..ab45858
--- /dev/null
+++ b/crates/lazyboy-core/src/local_tls.rs
@@ -0,0 +1,365 @@
+//! Local HTTPS for the web UI. VNC stays on 127.0.0.1; browsers talk TLS to LazyBoy.
+
+use anyhow::{Context, Result};
+use rcgen::{
+ BasicConstraints, CertificateParams, DistinguishedName, DnType, ExtendedKeyUsagePurpose,
+ IsCa, KeyPair, KeyUsagePurpose, SanType,
+};
+use rustls::pki_types::{CertificateDer, PrivateKeyDer};
+use rustls::ServerConfig;
+use std::net::IpAddr;
+use std::path::{Path, PathBuf};
+use std::sync::Arc;
+
+pub struct TlsMaterial {
+ pub ca_pem: String,
+ pub cert_pem: String,
+ pub key_pem: String,
+ pub names: Vec