- flake.nix: devShell, frontend package, backend package, combined default - spatial-office.nix: standalone NixOS module for importing into n97 config - packages/frontend: React + @react-three/fiber + Zustand (Vite build) - packages/backend: Node.js + ws + Zod (WebSocket server, port 18790) - pnpm-workspace.yaml: monorepo workspace config nix build .#frontend — bundled static site nix build .#backend — runnable WebSocket server nix build — full spatial-office application nix develop — dev shell with Node.js 22 + pnpm
184 lines
6.2 KiB
Nix
184 lines
6.2 KiB
Nix
# spatial-office.nix — NixOS module for the Spatial Office service
|
|
#
|
|
# Import into your NixOS configuration:
|
|
#
|
|
# imports = [ ./spatial-office.nix ];
|
|
# services.spatial-office.enable = true;
|
|
# services.spatial-office.port = 18790;
|
|
# services.spatial-office.openclawTokenFile = "/run/secrets/openclaw-token";
|
|
#
|
|
# This module provides:
|
|
# - A systemd service running the spatial-office backend
|
|
# - A dedicated spatial-office system user
|
|
# - Firewall port opening for the configured port
|
|
#
|
|
# Requirements:
|
|
# - The spatial-office flake must be available as an input in your flake.nix
|
|
# - Or the spatial-office package must be in your nixpkgs overlay
|
|
#
|
|
# Usage with flake-parts or raw NixOS config:
|
|
#
|
|
# { config, inputs, pkgs, ... }: {
|
|
# imports = [ inputs.spatial-office.nixosModules.spatial-office-service ];
|
|
# services.spatial-office = {
|
|
# enable = true;
|
|
# port = 18790;
|
|
# openclawTokenFile = config.sops.secrets.openclaw-token.path;
|
|
# };
|
|
# }
|
|
|
|
{ config, lib, pkgs, ... }:
|
|
|
|
let
|
|
cfg = config.services.spatial-office;
|
|
in
|
|
{
|
|
options.services.spatial-office = {
|
|
enable = lib.mkEnableOption "Spatial Office service — isometric agent workspace";
|
|
|
|
package = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.spatial-office-backend or (throw ''
|
|
spatial-office package not found in nixpkgs.
|
|
Add the spatial-office flake as an input and set:
|
|
services.spatial-office.package = inputs.spatial-office.packages.''${pkgs.system}.backend;
|
|
'');
|
|
description = "The spatial-office backend package to use";
|
|
};
|
|
|
|
frontendPackage = lib.mkOption {
|
|
type = lib.types.package;
|
|
default = pkgs.spatial-office-frontend or (throw ''
|
|
spatial-office frontend package not found in nixpkgs.
|
|
Add the spatial-office flake as an input and set:
|
|
services.spatial-office.frontendPackage = inputs.spatial-office.packages.''${pkgs.system}.frontend;
|
|
'');
|
|
description = "The spatial-office frontend package (static files)";
|
|
};
|
|
|
|
port = lib.mkOption {
|
|
type = lib.types.port;
|
|
default = 18790;
|
|
description = "WebSocket server port for the spatial-office backend";
|
|
};
|
|
|
|
host = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "127.0.0.1";
|
|
description = "Host address to bind the backend to";
|
|
};
|
|
|
|
openclawTokenFile = lib.mkOption {
|
|
type = lib.types.nullOr lib.types.path;
|
|
default = null;
|
|
description = ''
|
|
Path to a file containing the OpenClaw gateway shared token.
|
|
The file must be readable by the spatial-office user.
|
|
Use with sops-nix or agenix for secret management.
|
|
'';
|
|
};
|
|
|
|
openclawGatewayUrl = lib.mkOption {
|
|
type = lib.types.str;
|
|
default = "ws://127.0.0.1:18789";
|
|
description = "WebSocket URL of the OpenClaw gateway";
|
|
};
|
|
|
|
frontendDir = lib.mkOption {
|
|
type = lib.types.path;
|
|
default = cfg.frontendPackage;
|
|
description = "Directory of built frontend static files to serve";
|
|
};
|
|
|
|
logLevel = lib.mkOption {
|
|
type = lib.types.enum [ "debug" "info" "warn" "error" ];
|
|
default = "info";
|
|
description = "Log level for the spatial-office backend";
|
|
};
|
|
|
|
extraEnvironment = lib.mkOption {
|
|
type = lib.types.attrsOf lib.types.str;
|
|
default = { };
|
|
description = "Extra environment variables to pass to the service";
|
|
};
|
|
};
|
|
|
|
config = lib.mkIf cfg.enable {
|
|
# ── Systemd service ────────────────────────────────────────────
|
|
systemd.services.spatial-office = {
|
|
description = "Spatial Office — isometric agent workspace backend";
|
|
documentation = [ "https://os.scoble.me/forgejo/sscoble/spatial-office" ];
|
|
wantedBy = [ "multi-user.target" ];
|
|
after = [ "network.target" "network-online.target" ];
|
|
wants = [ "network-online.target" ];
|
|
|
|
# If a token file is configured, make sure it's available
|
|
unitConfig = lib.mkIf (cfg.openclawTokenFile != null) {
|
|
RequiresMountsFor = [ (builtins.dirOf cfg.openclawTokenFile) ];
|
|
};
|
|
|
|
environment = {
|
|
PORT = toString cfg.port;
|
|
HOST = cfg.host;
|
|
SPATIAL_OFFICE_FRONTEND_DIR = "${cfg.frontendDir}";
|
|
LOG_LEVEL = cfg.logLevel;
|
|
NODE_ENV = "production";
|
|
OPENCLAW_GATEWAY_URL = cfg.openclawGatewayUrl;
|
|
} // (lib.optionalAttrs (cfg.openclawTokenFile != null) {
|
|
OPENCLAW_TOKEN_FILE = cfg.openclawTokenFile;
|
|
}) // cfg.extraEnvironment;
|
|
|
|
serviceConfig = {
|
|
ExecStart = "${cfg.package}/bin/spatial-office-backend";
|
|
Restart = "always";
|
|
RestartSec = 5;
|
|
|
|
# Run as dedicated user
|
|
User = "spatial-office";
|
|
Group = "spatial-office";
|
|
|
|
# Security hardening
|
|
NoNewPrivileges = true;
|
|
ProtectSystem = "strict";
|
|
ProtectHome = true;
|
|
PrivateTmp = true;
|
|
PrivateDevices = true;
|
|
ProtectKernelTunables = true;
|
|
ProtectKernelModules = true;
|
|
ProtectControlGroups = true;
|
|
RestrictRealtime = true;
|
|
|
|
# Node.js needs W^X disabled for JIT
|
|
MemoryDenyWriteExecute = false;
|
|
|
|
# Allow network and basic syscalls
|
|
SystemCallFilter = [
|
|
"@system-service"
|
|
"~@privileged"
|
|
"~@resources"
|
|
];
|
|
|
|
# Restrict filesystem access
|
|
ReadWritePaths = [ ];
|
|
ReadOnlyPaths = lib.optionals (cfg.openclawTokenFile != null) [ cfg.openclawTokenFile ];
|
|
InaccessiblePaths = [ "/root" "/etc/ssh" ];
|
|
|
|
# Resource limits
|
|
LimitNOFILE = 4096;
|
|
};
|
|
};
|
|
|
|
# ── Service user ───────────────────────────────────────────────
|
|
users.users.spatial-office = {
|
|
isSystemUser = true;
|
|
group = "spatial-office";
|
|
description = "Spatial Office service user";
|
|
home = "/var/lib/spatial-office";
|
|
createHome = true;
|
|
};
|
|
users.groups.spatial-office = { };
|
|
|
|
# ── Firewall ───────────────────────────────────────────────────
|
|
networking.firewall.allowedTCPPorts = [ cfg.port ];
|
|
};
|
|
}
|