Skip to content

Commit ca10afe

Browse files
committed
fix: edited config for launchable & decoupled .yaml for Other Agents instructions
1 parent 4226d31 commit ca10afe

File tree

4 files changed

+247
-70
lines changed

4 files changed

+247
-70
lines changed
-17.3 KB
Binary file not shown.

brev/welcome-ui/index.html

Lines changed: 2 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -157,73 +157,8 @@ <h4 class="error-card__title">Something went wrong</h4>
157157
</div>
158158
</div>
159159

160-
<!-- Path 2 Modal: Other Agents -->
161-
<div class="overlay" id="overlay-instructions" hidden>
162-
<div class="modal modal--wide">
163-
<div class="modal__header">
164-
<h3 class="modal__title">Bring Your Own Agent</h3>
165-
<button class="modal__close" id="close-instructions">
166-
<svg viewBox="0 0 24 24"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
167-
</button>
168-
</div>
169-
<div class="modal__body">
170-
<p class="modal__text">
171-
Connect from your laptop, create a sandbox, and run any agent inside it.
172-
The NemoClaw TUI on your machine surfaces policy recommendations from the agent.
173-
You approve or deny &mdash; the sandbox boundary is never violated.
174-
</p>
175-
176-
<div class="instructions-section">
177-
<h4 class="instructions-section__title">1. Install NemoClaw CLI</h4>
178-
<div class="code-block">
179-
<span class="cmd">pip install nemoclaw</span>
180-
<button class="copy-btn" data-copy="pip install nemoclaw" aria-label="Copy">
181-
<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
182-
</button>
183-
</div>
184-
</div>
185-
186-
<div class="instructions-section">
187-
<h4 class="instructions-section__title">2. Connect to this VM</h4>
188-
<div class="code-block" id="connect-cmd-block">
189-
<span class="cmd" id="connect-cmd">nemoclaw cluster connect &lt;loading...&gt;</span>
190-
<button class="copy-btn" id="copy-connect" aria-label="Copy">
191-
<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
192-
</button>
193-
</div>
194-
</div>
195-
196-
<div class="instructions-section">
197-
<h4 class="instructions-section__title">3. Create a sandbox and run your agent</h4>
198-
<div class="code-block"><span class="comment"># Claude Code</span>
199-
<span class="cmd">nemoclaw sandbox create -- claude</span>
200-
201-
<span class="comment"># OpenCode</span>
202-
<span class="cmd">nemoclaw sandbox create -- opencode</span>
203-
204-
<span class="comment"># Any agent</span>
205-
<span class="cmd">nemoclaw sandbox create -- your-agent-command</span></div>
206-
</div>
207-
208-
<div class="instructions-section">
209-
<h4 class="instructions-section__title">4. Manage policies with the TUI</h4>
210-
<div class="code-block">
211-
<span class="cmd">nemoclaw term</span>
212-
<button class="copy-btn" data-copy="nemoclaw term" aria-label="Copy">
213-
<svg viewBox="0 0 24 24"><rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/></svg>
214-
</button>
215-
</div>
216-
<p class="modal__text" style="margin-top:10px">
217-
When your agent hits a policy denial, it reads the policy, diagnoses the block,
218-
and proposes an update. The TUI surfaces the recommendation:
219-
<em>"Your sandbox wants to connect to pypi.org. Recommended policy update:
220-
[allow pypi.org:443]. Approve?"</em>
221-
You approve &mdash; the policy hot-reloads &mdash; the agent continues.
222-
</p>
223-
</div>
224-
</div>
225-
</div>
226-
</div>
160+
<!-- Path 2 Modal: rendered from other-agents.yaml -->
161+
{{OTHER_AGENTS_MODAL}}
227162

228163
<!-- Footer -->
229164
<footer class="footer">

brev/welcome-ui/other-agents.yaml

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
# Content definition for the "Other Agents" modal.
2+
# Edit this file instead of raw HTML — the server renders it automatically.
3+
#
4+
# Structure:
5+
# title - Modal heading
6+
# intro - Introductory paragraph (supports raw HTML)
7+
# steps[] - Numbered instruction sections
8+
# title - Section heading (auto-numbered)
9+
# commands - List of commands to show in the code block
10+
# string → plain command
11+
# dict → { cmd, comment (optional), id (optional) }
12+
# copyable - Show a copy-to-clipboard button (default: false)
13+
# copy_button_id - HTML id for the copy button (for JS hooks)
14+
# block_id - HTML id for the code-block div (for JS hooks)
15+
# description - Paragraph below the code block (supports raw HTML)
16+
17+
title: Bring Your Own Agent
18+
19+
intro: >-
20+
Connect from your laptop, create a sandbox, and run any agent inside it.
21+
The NemoClaw TUI on your machine surfaces policy recommendations from the agent.
22+
You approve or deny &mdash; the sandbox boundary is never violated.
23+
24+
steps:
25+
- title: Install NemoClaw CLI
26+
commands:
27+
- pip install nemoclaw
28+
copyable: true
29+
30+
- title: Connect to this VM
31+
block_id: connect-cmd-block
32+
commands:
33+
- cmd: "nemoclaw cluster connect <loading...>"
34+
id: connect-cmd
35+
copy_button_id: copy-connect
36+
copyable: true
37+
38+
- title: Create a sandbox and run your agent
39+
commands:
40+
- comment: Claude Code
41+
cmd: "nemoclaw sandbox create -- claude"
42+
- comment: OpenCode
43+
cmd: "nemoclaw sandbox create -- opencode"
44+
- comment: Any agent
45+
cmd: "nemoclaw sandbox create -- your-agent-command"
46+
47+
- title: Manage policies with the TUI
48+
commands:
49+
- nemoclaw term
50+
copyable: true
51+
description: >-
52+
When your agent hits a policy denial, it reads the policy, diagnoses the block,
53+
and proposes an update. The TUI surfaces the recommendation:
54+
<em>&ldquo;Your sandbox wants to connect to pypi.org. Recommended policy update:
55+
[allow pypi.org:443]. Approve?&rdquo;</em>
56+
You approve &mdash; the policy hot-reloads &mdash; the agent continues.

brev/welcome-ui/server.py

Lines changed: 189 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
"""NemoClaw Welcome UI — HTTP server with sandbox lifecycle APIs."""
77

88
import hashlib
9+
import html as _html_mod
910
import http.client
1011
import http.server
1112
import json
@@ -27,8 +28,7 @@
2728
ROOT = os.path.dirname(os.path.abspath(__file__))
2829
REPO_ROOT = os.environ.get("REPO_ROOT", os.path.join(ROOT, "..", ".."))
2930
SANDBOX_DIR = os.path.join(REPO_ROOT, "sandboxes", "nemoclaw")
30-
# NEMOCLAW_IMAGE = "ghcr.io/nvidia/nemoclaw-community/sandboxes/nemoclaw:local"
31-
NEMOCLAW_IMAGE = "ghcr.io/nvidia/nemoclaw-community/sandboxes/nemoclaw:latest"
31+
NEMOCLAW_IMAGE = "ghcr.io/nvidia/nemoclaw-community/sandboxes/nemoclaw:local"
3232
POLICY_FILE = os.path.join(SANDBOX_DIR, "policy.yaml")
3333

3434
LOG_FILE = "/tmp/nemoclaw-sandbox-create.log"
@@ -40,6 +40,177 @@
4040

4141
_ANSI_RE = re.compile(r"\x1b\[[0-9;]*[a-zA-Z]")
4242

43+
OTHER_AGENTS_YAML = os.path.join(ROOT, "other-agents.yaml")
44+
45+
_COPY_BTN_SVG = (
46+
'<svg viewBox="0 0 24 24">'
47+
'<rect x="9" y="9" width="13" height="13" rx="2"/>'
48+
'<path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>'
49+
'</svg>'
50+
)
51+
52+
53+
def _render_other_agents_modal() -> str | None:
54+
"""Load other-agents.yaml and return the full modal overlay HTML."""
55+
if not os.path.isfile(OTHER_AGENTS_YAML):
56+
return None
57+
if _yaml is None:
58+
sys.stderr.write(
59+
"[welcome-ui] PyYAML not installed; other-agents.yaml ignored\n"
60+
)
61+
return None
62+
try:
63+
with open(OTHER_AGENTS_YAML) as f:
64+
data = _yaml.safe_load(f)
65+
except Exception as exc:
66+
sys.stderr.write(
67+
f"[welcome-ui] Failed to parse other-agents.yaml: {exc}\n"
68+
)
69+
return None
70+
71+
title = data.get("title", "Bring Your Own Agent")
72+
intro = (data.get("intro") or "").strip()
73+
steps = data.get("steps") or []
74+
75+
body_parts: list[str] = []
76+
if intro:
77+
body_parts.append(
78+
f' <p class="modal__text">\n'
79+
f" {intro}\n"
80+
f" </p>"
81+
)
82+
83+
for i, step in enumerate(steps, 1):
84+
step_title = step.get("title", "")
85+
commands = step.get("commands") or []
86+
copyable = step.get("copyable", False)
87+
block_id = step.get("block_id", "")
88+
copy_btn_id = step.get("copy_button_id", "")
89+
description = (step.get("description") or "").strip()
90+
91+
body_parts.append("")
92+
body_parts.append(' <div class="instructions-section">')
93+
body_parts.append(
94+
f' <h4 class="instructions-section__title">'
95+
f"{i}. {step_title}</h4>"
96+
)
97+
98+
groups: list[str] = []
99+
for entry in commands:
100+
lines: list[str] = []
101+
if isinstance(entry, str):
102+
lines.append(
103+
f'<span class="cmd">'
104+
f"{_html_mod.escape(entry)}</span>"
105+
)
106+
elif isinstance(entry, dict):
107+
comment = entry.get("comment", "")
108+
cmd = entry.get("cmd", "")
109+
cmd_id = entry.get("id", "")
110+
id_attr = f' id="{cmd_id}"' if cmd_id else ""
111+
if comment:
112+
lines.append(
113+
f'<span class="comment">'
114+
f"# {_html_mod.escape(comment)}</span>"
115+
)
116+
lines.append(
117+
f'<span class="cmd"{id_attr}>'
118+
f"{_html_mod.escape(cmd)}</span>"
119+
)
120+
groups.append("\n".join(lines))
121+
122+
cmd_html = "\n\n".join(groups)
123+
124+
copy_html = ""
125+
if copyable:
126+
if copy_btn_id:
127+
copy_html = (
128+
f'<button class="copy-btn" id="{copy_btn_id}" '
129+
f'aria-label="Copy">{_COPY_BTN_SVG}</button>'
130+
)
131+
elif len(commands) == 1:
132+
raw = (
133+
commands[0]
134+
if isinstance(commands[0], str)
135+
else commands[0].get("cmd", "")
136+
)
137+
copy_html = (
138+
f'<button class="copy-btn" '
139+
f'data-copy="{_html_mod.escape(raw)}" '
140+
f'aria-label="Copy">{_COPY_BTN_SVG}</button>'
141+
)
142+
else:
143+
copy_html = (
144+
f'<button class="copy-btn" '
145+
f'aria-label="Copy">{_COPY_BTN_SVG}</button>'
146+
)
147+
148+
block_id_attr = f' id="{block_id}"' if block_id else ""
149+
body_parts.append(
150+
f' <div class="code-block"{block_id_attr}>'
151+
f"{cmd_html}"
152+
f"{copy_html}"
153+
f"</div>"
154+
)
155+
156+
if description:
157+
body_parts.append(
158+
f' <p class="modal__text" style="margin-top:10px">'
159+
f"\n {description}\n"
160+
f" </p>"
161+
)
162+
163+
body_parts.append(" </div>")
164+
165+
body_html = "\n".join(body_parts)
166+
167+
return (
168+
f'<div class="overlay" id="overlay-instructions" hidden>\n'
169+
f' <div class="modal modal--wide">\n'
170+
f' <div class="modal__header">\n'
171+
f' <h3 class="modal__title">{title}</h3>\n'
172+
f' <button class="modal__close" id="close-instructions">\n'
173+
f' <svg viewBox="0 0 24 24">'
174+
f'<path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>\n'
175+
f" </button>\n"
176+
f" </div>\n"
177+
f' <div class="modal__body">\n'
178+
f"{body_html}\n"
179+
f" </div>\n"
180+
f" </div>\n"
181+
f" </div>"
182+
)
183+
184+
185+
_rendered_index: str | None = None
186+
187+
188+
def _get_rendered_index() -> str:
189+
"""Return index.html with the YAML-rendered modal injected."""
190+
global _rendered_index
191+
if _rendered_index is not None:
192+
return _rendered_index
193+
194+
index_path = os.path.join(ROOT, "index.html")
195+
with open(index_path) as f:
196+
template = f.read()
197+
198+
modal_html = _render_other_agents_modal()
199+
if modal_html:
200+
template = template.replace("{{OTHER_AGENTS_MODAL}}", modal_html)
201+
sys.stderr.write("[welcome-ui] Rendered other-agents.yaml into index.html\n")
202+
else:
203+
template = template.replace(
204+
"{{OTHER_AGENTS_MODAL}}",
205+
'<!-- other-agents.yaml not available -->',
206+
)
207+
sys.stderr.write(
208+
"[welcome-ui] WARNING: could not render other-agents.yaml\n"
209+
)
210+
211+
_rendered_index = template
212+
return _rendered_index
213+
43214
def _strip_ansi(text: str) -> str:
44215
return _ANSI_RE.sub("", text)
45216

@@ -285,7 +456,8 @@ def _run_sandbox_create():
285456
cmd = [
286457
"nemoclaw", "sandbox", "create",
287458
"--name", "nemoclaw",
288-
"--from", NEMOCLAW_IMAGE,
459+
# "--from", NEMOCLAW_IMAGE,
460+
"--from", "nemoclaw",
289461
"--forward", "18789",
290462
]
291463
if policy_path:
@@ -511,6 +683,8 @@ def _route(self):
511683
return self._proxy_to_sandbox()
512684

513685
if self.command in ("GET", "HEAD"):
686+
if path in ("", "/", "/index.html"):
687+
return self._serve_templated_index()
514688
return super().do_GET()
515689

516690
self.send_error(404)
@@ -982,6 +1156,18 @@ def _handle_connection_details(self):
9821156
},
9831157
})
9841158

1159+
# -- Templated index.html -------------------------------------------
1160+
1161+
def _serve_templated_index(self):
1162+
"""Serve index.html with the YAML-rendered Other Agents modal."""
1163+
content = _get_rendered_index().encode("utf-8")
1164+
self.send_response(200)
1165+
self.send_header("Content-Type", "text/html; charset=utf-8")
1166+
self.send_header("Content-Length", str(len(content)))
1167+
self.end_headers()
1168+
if self.command != "HEAD":
1169+
self.wfile.write(content)
1170+
9851171
# -- Helpers --------------------------------------------------------
9861172

9871173
def _json_response(self, status: int, body: dict):

0 commit comments

Comments
 (0)