diff --git a/REUSE.toml b/REUSE.toml
index bf435ec4..8f345c27 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -64,6 +64,7 @@ SPDX-License-Identifier = "Apache-2.0"
[[annotations]]
path = [
"docs/security/dstack-audit.pdf",
+ "dstack_Technical_Charter_Final_10-17-2025.pdf",
"sdk/simulator/quote.hex",
"ra-tls/assets/tdx_quote",
"cc-eventlog/samples/ccel.bin",
diff --git a/dstack-util/src/system_setup.rs b/dstack-util/src/system_setup.rs
index 697f18b2..6e568702 100644
--- a/dstack-util/src/system_setup.rs
+++ b/dstack-util/src/system_setup.rs
@@ -1124,7 +1124,7 @@ impl Stage1<'_> {
}
async fn setup(&self) -> Result<()> {
- let envs = self.unseal_env_vars()?;
+ let _envs = self.unseal_env_vars()?;
self.link_files()?;
self.setup_guest_agent_config()?;
self.vmm
diff --git a/dstack_Technical_Charter_Final_10-17-2025.pdf b/dstack_Technical_Charter_Final_10-17-2025.pdf
new file mode 100644
index 00000000..e73d6e12
Binary files /dev/null and b/dstack_Technical_Charter_Final_10-17-2025.pdf differ
diff --git a/kms/README.md b/kms/README.md
index 7fa27dd8..19130c92 100644
--- a/kms/README.md
+++ b/kms/README.md
@@ -150,7 +150,7 @@ The verification process follows these steps:
## The RPC Interface
-The KMS RPC interface is defined in [kms.proto](rpc/proto/kms.proto).
+The KMS RPC interface is defined in [kms_rpc.proto](rpc/proto/kms_rpc.proto).
The core interface serving the dstack app are:
- `GetAppKey`: Requests an app key using the app ID and TDX quote
@@ -171,7 +171,7 @@ The `GetAppKey` RPC is used by the dstack app to request an app key. In this RPC
Note:
-There are multiple keys derived for different usage, see [kms.proto](rpc/proto/kms.proto) for more details.
+There are multiple keys derived for different usage, see [kms_rpc.proto](rpc/proto/kms_rpc.proto) for more details.
The root key is generated by a genesis KMS node in TEE and would be stored in the KMS node's encrypted local disk, replicated to other KMS nodes.
The keys are derived with app id which guarantees apps can not get the keys from other apps.
diff --git a/kms/auth-eth-bun/package.json b/kms/auth-eth-bun/package.json
index febd26eb..3bef0eb8 100644
--- a/kms/auth-eth-bun/package.json
+++ b/kms/auth-eth-bun/package.json
@@ -15,7 +15,7 @@
"check": "bun run lint && bun run test:run"
},
"dependencies": {
- "hono": "4.9.7",
+ "hono": "4.10.3",
"@hono/zod-validator": "0.2.2",
"zod": "3.25.76",
"viem": "2.31.7"
diff --git a/kms/auth-mock/package.json b/kms/auth-mock/package.json
index a2c38999..3c62e80e 100644
--- a/kms/auth-mock/package.json
+++ b/kms/auth-mock/package.json
@@ -15,7 +15,7 @@
"check": "bun run lint && bun run test:run"
},
"dependencies": {
- "hono": "4.9.6",
+ "hono": "4.10.3",
"@hono/zod-validator": "0.2.2",
"zod": "3.25.76"
},
diff --git a/vmm/src/config.rs b/vmm/src/config.rs
index 999b4a21..34365226 100644
--- a/vmm/src/config.rs
+++ b/vmm/src/config.rs
@@ -265,6 +265,10 @@ pub struct Config {
/// The URL of the KMS server
pub kms_url: String,
+ /// Node name (optional, used as prefix in UI title)
+ #[serde(default)]
+ pub node_name: String,
+
/// CVM configuration
pub cvm: CvmConfig,
/// Gateway configuration
diff --git a/vmm/src/console.html b/vmm/src/console.html
index c07a6f4e..f9d0bd78 100644
--- a/vmm/src/console.html
+++ b/vmm/src/console.html
@@ -10,7 +10,7 @@
- Codestin Search App
+ Codestin Search App
(ContentType, String) {
- (ContentType::HTML, file_or_include_str!("console.html"))
+async fn index(app: &State) -> (ContentType, String) {
+ let html = file_or_include_str!("console.html");
+ let title = if app.config.node_name.is_empty() {
+ "dstack VM Management Console".to_string()
+ } else {
+ format!("{} - dstack VM Management Console", app.config.node_name)
+ };
+ let html = html.replace("{{TITLE}}", &title);
+ (ContentType::HTML, html)
}
#[get("/res/")]
diff --git a/vmm/src/vmm-cli.py b/vmm/src/vmm-cli.py
index bbdac681..36e9288f 100755
--- a/vmm/src/vmm-cli.py
+++ b/vmm/src/vmm-cli.py
@@ -123,7 +123,6 @@ def read_utf8(filepath: str) -> str:
with open(filepath, 'rb') as f:
return f.read().decode('utf-8')
-
class UnixSocketHTTPConnection(http.client.HTTPConnection):
"""HTTPConnection that connects to a Unix domain socket."""
@@ -332,6 +331,33 @@ def remove_vm(self, vm_id: str) -> None:
self.rpc_call('RemoveVm', {'id': vm_id})
print(f"Removed VM {vm_id}")
+ def resize_vm(
+ self,
+ vm_id: str,
+ vcpu: Optional[int] = None,
+ memory: Optional[int] = None,
+ disk_size: Optional[int] = None,
+ image: Optional[str] = None,
+ ) -> None:
+ """Resize a VM"""
+ params = {"id": vm_id}
+ if vcpu is not None:
+ params["vcpu"] = vcpu
+ if memory is not None:
+ params["memory"] = memory
+ if disk_size is not None:
+ params["disk_size"] = disk_size
+ if image is not None:
+ params["image"] = image
+
+ if len(params) == 1:
+ raise Exception(
+ "at least one parameter must be specified for resize: --vcpu, --memory, --disk, or --image"
+ )
+
+ self.rpc_call("ResizeVm", params)
+ print(f"Resized VM {vm_id}")
+
def show_logs(self, vm_id: str, lines: int = 20, follow: bool = False) -> None:
"""Show VM logs"""
path = f"/logs?id={vm_id}&follow={str(follow).lower()}&ansi=false&lines={lines}"
@@ -609,6 +635,15 @@ def update_vm_app_compose(self, vm_id: str, app_compose: str) -> None:
self.rpc_call('UpgradeApp', {'id': vm_id,
'compose_file': app_compose})
print(f"App compose updated for VM {vm_id}")
+
+ def update_vm_ports(self, vm_id: str, ports: List[str]) -> None:
+ """Update port mapping for a VM"""
+ port_mappings = [parse_port_mapping(port) for port in ports]
+ self.rpc_call(
+ "UpgradeApp", {"id": vm_id,
+ "update_ports": True, "ports": port_mappings}
+ )
+ print(f"Port mapping updated for VM {vm_id}")
def list_gpus(self, json_output: bool = False) -> None:
"""List all available GPUs"""
@@ -884,6 +919,18 @@ def main():
remove_parser = subparsers.add_parser('remove', help='Remove a VM')
remove_parser.add_argument('vm_id', help='VM ID to remove')
+ # Resize command
+ resize_parser = subparsers.add_parser("resize", help="Resize a VM")
+ resize_parser.add_argument("vm_id", help="VM ID to resize")
+ resize_parser.add_argument("--vcpu", type=int, help="Number of vCPUs")
+ resize_parser.add_argument(
+ "--memory", type=parse_memory_size, help="Memory size (e.g. 1G, 100M)"
+ )
+ resize_parser.add_argument(
+ "--disk", type=parse_disk_size, help="Disk size (e.g. 20G, 1T)"
+ )
+ resize_parser.add_argument("--image", type=str, help="Image name")
+
# Logs command
logs_parser = subparsers.add_parser('logs', help='Show VM logs')
logs_parser.add_argument('vm_id', help='VM ID to show logs for')
@@ -1016,6 +1063,19 @@ def main():
update_user_config_parser.add_argument(
'user_config', help='Path to user config file')
+ # Update port mapping
+ update_ports_parser = subparsers.add_parser(
+ "update-ports", help="Update port mapping for a VM"
+ )
+ update_ports_parser.add_argument("vm_id", help="VM ID to update")
+ update_ports_parser.add_argument(
+ "--port",
+ action="append",
+ type=str,
+ required=True,
+ help="Port mapping in format: protocol[:address]:from:to (can be used multiple times)",
+ )
+
args = parser.parse_args()
cli = VmmCLI(args.url, args.auth_user, args.auth_password)
@@ -1028,6 +1088,14 @@ def main():
cli.stop_vm(args.vm_id, args.force)
elif args.command == 'remove':
cli.remove_vm(args.vm_id)
+ elif args.command == 'resize':
+ cli.resize_vm(
+ args.vm_id,
+ vcpu=args.vcpu,
+ memory=args.memory,
+ disk_size=args.disk,
+ image=args.image,
+ )
elif args.command == 'logs':
cli.show_logs(args.vm_id, args.lines, args.follow)
elif args.command == 'compose':
@@ -1046,6 +1114,8 @@ def main():
args.vm_id, open(args.user_config, 'r').read())
elif args.command == 'update-app-compose':
cli.update_vm_app_compose(args.vm_id, open(args.compose, 'r').read())
+ elif args.command == "update-ports":
+ cli.update_vm_ports(args.vm_id, args.port)
elif args.command == 'kms':
if not args.kms_action:
kms_parser.print_help()