Skip to content

Commit 4e8b638

Browse files
authored
feat(chat ui): policy tab first pass (#12)
* First draft of design and implementation * feat(chat ui): policy tab first pass
1 parent 73149a5 commit 4e8b638

File tree

14 files changed

+4734
-39
lines changed

14 files changed

+4734
-39
lines changed
17.3 KB
Binary file not shown.

brev/welcome-ui/server.py

Lines changed: 295 additions & 19 deletions
Large diffs are not rendered by default.

sandboxes/nemoclaw/Dockerfile

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,17 @@ USER root
2020
COPY nemoclaw-start.sh /usr/local/bin/nemoclaw-start
2121
RUN chmod +x /usr/local/bin/nemoclaw-start
2222

23+
# Install the policy reverse proxy (sits in front of the OpenClaw gateway,
24+
# intercepts /api/policy to read/write the sandbox policy file) and its
25+
# runtime dependencies for gRPC gateway sync.
26+
COPY policy-proxy.js /usr/local/lib/policy-proxy.js
27+
COPY proto/ /usr/local/lib/nemoclaw-proto/
28+
RUN npm install -g @grpc/grpc-js @grpc/proto-loader js-yaml
29+
30+
# Allow the sandbox user to read the default policy (the startup script
31+
# copies it to a writable location; this chown covers non-Landlock envs)
32+
RUN chown -R sandbox:sandbox /etc/navigator
33+
2334
# Stage the NeMoClaw DevX extension source
2435
COPY nemoclaw-ui-extension/extension/ /opt/nemoclaw-devx/
2536

@@ -29,8 +40,11 @@ COPY nemoclaw-ui-extension/extension/ /opt/nemoclaw-devx/
2940
# add <script>/<link> tags to index.html.
3041
# API key placeholders (__NVIDIA_*_API_KEY__) stay as literal strings in the
3142
# bundle; they are substituted at container startup from environment variables.
43+
# js-yaml is installed locally so esbuild can bundle it for browser-side YAML
44+
# parsing in the policy page; the node_modules are removed after bundling.
3245
RUN set -e; \
3346
npm install -g esbuild; \
47+
cd /opt/nemoclaw-devx && npm install --production; \
3448
UI_DIR="$(npm root -g)/openclaw/dist/control-ui"; \
3549
esbuild /opt/nemoclaw-devx/index.ts \
3650
--bundle \
@@ -39,6 +53,7 @@ RUN set -e; \
3953
HASH=$(md5sum "$UI_DIR/assets/nemoclaw-devx.js" | cut -c1-8); \
4054
sed -i "s|</head>|<link rel=\"stylesheet\" href=\"./assets/nemoclaw-devx.css?v=${HASH}\">\n</head>|" "$UI_DIR/index.html"; \
4155
sed -i "s|</head>|<script type=\"module\" src=\"./assets/nemoclaw-devx.js?v=${HASH}\"></script>\n</head>|" "$UI_DIR/index.html"; \
42-
npm uninstall -g esbuild
56+
npm uninstall -g esbuild; \
57+
rm -rf /opt/nemoclaw-devx/node_modules
4358

4459
ENTRYPOINT ["/bin/bash"]

sandboxes/nemoclaw/nemoclaw-start.sh

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,14 @@ set -euo pipefail
4040
# that is blocked, we skip gracefully — users can still enter keys via
4141
# the API Keys page in the OpenClaw UI.
4242
# --------------------------------------------------------------------------
43+
if [ -z "${CHAT_UI_URL:-}" ]; then
44+
echo "Error: CHAT_UI_URL environment variable is required." >&2
45+
echo "Set it to the URL where the chat UI will be accessed, e.g.:" >&2
46+
echo " Local: CHAT_UI_URL=http://127.0.0.1:18789" >&2
47+
echo " Brev: CHAT_UI_URL=https://187890-<brev-id>.brevlab.com" >&2
48+
exit 1
49+
fi
50+
4351
BUNDLE="$(npm root -g)/openclaw/dist/control-ui/assets/nemoclaw-devx.js"
4452

4553
if [ -f "$BUNDLE" ]; then
@@ -74,26 +82,21 @@ openclaw onboard \
7482
--custom-api-key "not-used" \
7583
--secret-input-mode plaintext \
7684
--custom-compatibility openai \
77-
--gateway-port 18789 \
85+
--gateway-port 18788 \
7886
--gateway-bind loopback
7987

8088
export NVIDIA_API_KEY=" "
8189

82-
GATEWAY_PORT=18789
83-
84-
if [ -z "${CHAT_UI_URL:-}" ]; then
85-
echo "Error: CHAT_UI_URL environment variable is required." >&2
86-
echo "Set it to the URL where the chat UI will be accessed, e.g.:" >&2
87-
echo " Local: CHAT_UI_URL=http://127.0.0.1:18789" >&2
88-
echo " Brev: CHAT_UI_URL=https://187890-<brev-id>.brevlab.com" >&2
89-
exit 1
90-
fi
90+
INTERNAL_GATEWAY_PORT=18788
91+
PUBLIC_PORT=18789
9192

93+
# allowedOrigins must reference the PUBLIC port (18789) since that is the
94+
# origin the browser sends. The proxy on 18789 forwards to 18788 internally.
9295
python3 -c "
9396
import json, os
9497
from urllib.parse import urlparse
9598
cfg = json.load(open(os.environ['HOME'] + '/.openclaw/openclaw.json'))
96-
local = 'http://127.0.0.1:${GATEWAY_PORT}'
99+
local = 'http://127.0.0.1:${PUBLIC_PORT}'
97100
parsed = urlparse(os.environ['CHAT_UI_URL'])
98101
chat_origin = f'{parsed.scheme}://{parsed.netloc}'
99102
origins = [local]
@@ -108,6 +111,24 @@ json.dump(cfg, open(os.environ['HOME'] + '/.openclaw/openclaw.json', 'w'), inden
108111

109112
nohup openclaw gateway > /tmp/gateway.log 2>&1 &
110113

114+
# Copy the default policy to a writable location so that policy-proxy can
115+
# update it at runtime. /etc is read-only under Landlock, but /sandbox is
116+
# read-write, so we use /sandbox/.openclaw/ which is already owned by the
117+
# sandbox user.
118+
_POLICY_SRC="/etc/navigator/policy.yaml"
119+
_POLICY_DST="/sandbox/.openclaw/policy.yaml"
120+
if [ ! -f "$_POLICY_DST" ] && [ -f "$_POLICY_SRC" ]; then
121+
cp "$_POLICY_SRC" "$_POLICY_DST" 2>/dev/null || true
122+
fi
123+
_POLICY_PATH="${_POLICY_DST}"
124+
[ -f "$_POLICY_PATH" ] || _POLICY_PATH="$_POLICY_SRC"
125+
126+
# Start the policy reverse proxy on the public-facing port. It forwards all
127+
# traffic to the OpenClaw gateway on the internal port and intercepts
128+
# /api/policy requests to read/write the sandbox policy file.
129+
NODE_PATH=$(npm root -g) POLICY_PATH=${_POLICY_PATH} UPSTREAM_PORT=${INTERNAL_GATEWAY_PORT} LISTEN_PORT=${PUBLIC_PORT} \
130+
nohup node /usr/local/lib/policy-proxy.js >> /tmp/gateway.log 2>&1 &
131+
111132
# Auto-approve pending device pairing requests so the browser is paired
112133
# before the user notices the "pairing required" prompt in the Control UI.
113134
(

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/icons.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,30 @@ export const ICON_EYE = `<svg viewBox="0 0 24 24"><path d="M2 12s3-7 10-7 10 7 1
3030

3131
export const ICON_EYE_OFF = `<svg viewBox="0 0 24 24"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"/><line x1="1" y1="1" x2="23" y2="23"/></svg>`;
3232

33+
export const ICON_LOCK = `<svg viewBox="0 0 24 24"><rect width="18" height="11" x="3" y="11" rx="2" ry="2"/><path d="M7 11V7a5 5 0 0 1 10 0v4"/></svg>`;
34+
35+
export const ICON_PLUS = `<svg viewBox="0 0 24 24"><path d="M12 5v14"/><path d="M5 12h14"/></svg>`;
36+
37+
export const ICON_TRASH = `<svg viewBox="0 0 24 24"><path d="M3 6h18"/><path d="M19 6v14c0 1-1 2-2 2H7c-1 0-2-1-2-2V6"/><path d="M8 6V4c0-1 1-2 2-2h4c1 0 2 1 2 2v2"/><line x1="10" x2="10" y1="11" y2="17"/><line x1="14" x2="14" y1="11" y2="17"/></svg>`;
38+
39+
export const ICON_EDIT = `<svg viewBox="0 0 24 24"><path d="M17 3a2.85 2.83 0 1 1 4 4L7.5 20.5 2 22l1.5-5.5Z"/><path d="m15 5 4 4"/></svg>`;
40+
41+
export const ICON_INFO = `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/></svg>`;
42+
43+
export const ICON_GLOBE = `<svg viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/><path d="M12 2a14.5 14.5 0 0 0 0 20 14.5 14.5 0 0 0 0-20"/><path d="M2 12h20"/></svg>`;
44+
45+
export const ICON_TERMINAL = `<svg viewBox="0 0 24 24"><polyline points="4 17 10 11 4 5"/><line x1="12" x2="20" y1="19" y2="19"/></svg>`;
46+
47+
export const ICON_FOLDER = `<svg viewBox="0 0 24 24"><path d="M20 20a2 2 0 0 0 2-2V8a2 2 0 0 0-2-2h-7.9a2 2 0 0 1-1.69-.9L9.6 3.9A2 2 0 0 0 7.93 3H4a2 2 0 0 0-2 2v13a2 2 0 0 0 2 2Z"/></svg>`;
48+
49+
export const ICON_USER = `<svg viewBox="0 0 24 24"><path d="M19 21v-2a4 4 0 0 0-4-4H9a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/></svg>`;
50+
51+
export const ICON_CHEVRON_RIGHT = `<svg viewBox="0 0 24 24"><polyline points="9 18 15 12 9 6"/></svg>`;
52+
53+
export const ICON_SEARCH = `<svg viewBox="0 0 24 24"><circle cx="11" cy="11" r="8"/><path d="m21 21-4.3-4.3"/></svg>`;
54+
55+
export const ICON_WARNING = `<svg viewBox="0 0 24 24"><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3Z"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>`;
56+
3357
export const TARGET_ICONS: Record<string, string> = {
3458
"dgx-spark": ICON_CHIP,
3559
"dgx-station": ICON_SERVER,

sandboxes/nemoclaw/nemoclaw-ui-extension/extension/nav-group.ts

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import { ICON_SHIELD, ICON_ROUTE, ICON_KEY } from "./icons.ts";
99
import { renderApiKeysPage, areAllKeysConfigured, updateStatusDots } from "./api-keys-page.ts";
10+
import { renderPolicyPage } from "./policy-page.ts";
1011

1112
// ---------------------------------------------------------------------------
1213
// Page definitions
@@ -26,12 +27,12 @@ interface NemoClawPage {
2627
const NEMOCLAW_PAGES: NemoClawPage[] = [
2728
{
2829
id: "nemoclaw-policy",
29-
label: "Policy",
30+
label: "Sandbox Policy",
3031
icon: ICON_SHIELD,
31-
title: "Policy",
32-
subtitle: "Manage deployment policies and guardrails",
33-
emptyMessage:
34-
"Policy configuration is coming soon. You'll be able to define safety policies, rate limits, and access controls for your NeMoClaw deployments here.",
32+
title: "Sandbox Policy",
33+
subtitle: "View and manage sandbox security guardrails",
34+
emptyMessage: "",
35+
customRender: renderPolicyPage,
3536
},
3637
{
3738
id: "nemoclaw-inference-routes",
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
{
2+
"private": true,
3+
"dependencies": {
4+
"js-yaml": "^4.1.0"
5+
}
6+
}

0 commit comments

Comments
 (0)