Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .agents/skills/nemoclaw-user-manage-policy/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ Available presets:
| `brew` | Homebrew (Linuxbrew) package manager |
| `discord` | Discord webhook API |
| `github` | GitHub and GitHub REST API |
| `heygen` | HeyGen video API (`api.heygen.com`), OAuth token endpoints on `api2.heygen.com`, uploads and files |
| `huggingface` | Hugging Face Hub (download-only) and inference router |
| `jira` | Atlassian Jira API |
| `npm` | npm and Yarn registries |
Expand Down
1 change: 1 addition & 0 deletions docs/network-policy/customize-network-policy.md
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,7 @@ Available presets:
| `brew` | Homebrew (Linuxbrew) package manager |
| `discord` | Discord webhook API |
| `github` | GitHub and GitHub REST API |
| `heygen` | HeyGen video API (`api.heygen.com`), OAuth token endpoints on `api2.heygen.com`, uploads and files |
| `huggingface` | Hugging Face Hub (download-only) and inference router |
| `jira` | Atlassian Jira API |
| `npm` | npm and Yarn registries |
Expand Down
51 changes: 51 additions & 0 deletions nemoclaw-blueprint/policies/presets/heygen.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0

preset:
name: heygen
description: "HeyGen API (REST, OAuth token endpoints, uploads, streaming WebSocket)"

network_policies:
heygen:
name: heygen
endpoints:
- host: api.heygen.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- allow: { method: PATCH, path: "/**" }
- allow: { method: PUT, path: "/**" }
- allow: { method: DELETE, path: "/**" }
- host: api.heygen.com
port: 443
access: full
- host: api2.heygen.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: POST, path: "/v1/oauth/**" }
- allow: { method: GET, path: "/v1/user/**" }
- host: upload.heygen.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
- allow: { method: POST, path: "/**" }
- allow: { method: PUT, path: "/**" }
- host: files.heygen.com
port: 443
protocol: rest
enforcement: enforce
tls: terminate
rules:
- allow: { method: GET, path: "/**" }
binaries:
- { path: /usr/local/bin/node }
Comment on lines +50 to +51
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Add /usr/bin/node to the binaries allowlist to avoid runtime mismatches.

Line 50 currently allows only /usr/local/bin/node. On images where Node is invoked from /usr/bin/node, this preset won’t match and HeyGen traffic will be blocked.

Proposed fix
     binaries:
       - { path: /usr/local/bin/node }
+      - { path: /usr/bin/node }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
binaries:
- { path: /usr/local/bin/node }
binaries:
- { path: /usr/local/bin/node }
- { path: /usr/bin/node }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@nemoclaw-blueprint/policies/presets/heygen.yaml` around lines 50 - 51, The
binaries allowlist currently only includes the entry { path: /usr/local/bin/node
} under the binaries key, which causes HeyGen to be blocked when Node is
executed from /usr/bin/node; update the preset by adding an additional allowlist
entry for { path: /usr/bin/node } alongside the existing /usr/local/bin/node
entry so both locations are permitted.

17 changes: 17 additions & 0 deletions src/lib/onboard.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2712,6 +2712,9 @@ async function createSandbox(
// Additional credentials not in REMOTE_PROVIDER_CONFIG
"BEDROCK_API_KEY",
"DISCORD_BOT_TOKEN",
"HEYGEN_ACCESS_TOKEN",
"HEYGEN_API_KEY",
"HEYGEN_REFRESH_TOKEN",
"SLACK_BOT_TOKEN",
"SLACK_APP_TOKEN",
"TELEGRAM_BOT_TOKEN",
Expand Down Expand Up @@ -3924,6 +3927,20 @@ function getSuggestedPolicyPresets({ enabledChannels = null, webSearchConfig = n
maybeSuggestMessagingPreset("slack", "SLACK_BOT_TOKEN");
maybeSuggestMessagingPreset("discord", "DISCORD_BOT_TOKEN");

if (
getCredential("HEYGEN_API_KEY") ||
process.env.HEYGEN_API_KEY ||
getCredential("HEYGEN_ACCESS_TOKEN") ||
process.env.HEYGEN_ACCESS_TOKEN ||
getCredential("HEYGEN_REFRESH_TOKEN") ||
process.env.HEYGEN_REFRESH_TOKEN
) {
suggestions.push("heygen");
if (process.stdout.isTTY && !isNonInteractive() && process.env.CI !== "true") {
console.log(" Auto-detected: HeyGen credentials -> suggesting heygen preset");
}
}
Comment on lines +3930 to +3942
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Avoid raw process.env checks to prevent false-positive HeyGen preset suggestions.

getCredential() already checks env + stored creds and normalizes whitespace. The extra process.env.* checks can incorrectly suggest heygen when a var is set to whitespace.

♻️ Proposed fix
-  if (
-    getCredential("HEYGEN_API_KEY") ||
-    process.env.HEYGEN_API_KEY ||
-    getCredential("HEYGEN_ACCESS_TOKEN") ||
-    process.env.HEYGEN_ACCESS_TOKEN ||
-    getCredential("HEYGEN_REFRESH_TOKEN") ||
-    process.env.HEYGEN_REFRESH_TOKEN
-  ) {
+  const heygenCredentialKeys = [
+    "HEYGEN_API_KEY",
+    "HEYGEN_ACCESS_TOKEN",
+    "HEYGEN_REFRESH_TOKEN",
+  ];
+  if (heygenCredentialKeys.some((key) => getCredential(key))) {
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/onboard.ts` around lines 3930 - 3942, The conditional that
auto-suggests the "heygen" preset is using raw process.env checks which can be
whitespace-only and cause false positives; update the if() to rely solely on
getCredential("HEYGEN_API_KEY"), getCredential("HEYGEN_ACCESS_TOKEN"), and
getCredential("HEYGEN_REFRESH_TOKEN") (which already normalize/validate values)
instead of including process.env.HEYGEN_API_KEY / HEYGEN_ACCESS_TOKEN /
HEYGEN_REFRESH_TOKEN, leaving the suggestions.push("heygen") and the TTY/CI
logging logic (process.stdout.isTTY, isNonInteractive(), process.env.CI)
unchanged.


if (webSearchConfig) suggestions.push("brave");

return suggestions;
Expand Down
2 changes: 1 addition & 1 deletion src/lib/preflight.ts
Original file line number Diff line number Diff line change
Expand Up @@ -539,7 +539,7 @@ export async function checkPortAvailable(
// Retry with sudo -n to identify root-owned listeners before falling
// through to the net probe (which can only detect EADDRINUSE but not
// the owning process).
if (!o.lsofOutput) {
if (typeof o.lsofOutput !== "string") {
const sudoOut: string | undefined = runCapture(
`sudo -n lsof -i :${p} -sTCP:LISTEN -P -n 2>/dev/null`,
{ ignoreError: true },
Expand Down
3 changes: 3 additions & 0 deletions test/credential-exposure.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,9 @@ describe("credential exposure in process arguments", () => {
// Messaging and additional credentials are listed explicitly
expect(blocklist).toContain('"BEDROCK_API_KEY"');
expect(blocklist).toContain('"DISCORD_BOT_TOKEN"');
expect(blocklist).toContain('"HEYGEN_ACCESS_TOKEN"');
expect(blocklist).toContain('"HEYGEN_API_KEY"');
expect(blocklist).toContain('"HEYGEN_REFRESH_TOKEN"');
expect(blocklist).toContain('"SLACK_BOT_TOKEN"');
expect(blocklist).toContain('"SLACK_APP_TOKEN"');
expect(blocklist).toContain('"TELEGRAM_BOT_TOKEN"');
Expand Down
4 changes: 3 additions & 1 deletion test/legacy-path-guard.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,9 @@ function run(command: string, args: string[], cwd: string) {

function initTempRepo(prefix: string): string {
const repoDir = fs.mkdtempSync(path.join(os.tmpdir(), prefix));
run("git", ["init", "-b", "main"], repoDir);
// Portable default branch (git init -b requires Git 2.28+).
run("git", ["init"], repoDir);
run("git", ["symbolic-ref", "HEAD", "refs/heads/main"], repoDir);
run("git", ["config", "user.name", "Test User"], repoDir);
run("git", ["config", "user.email", "test@example.com"], repoDir);
return repoDir;
Expand Down
42 changes: 27 additions & 15 deletions test/policies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,10 +95,25 @@ selectFromList(items, options)
}

describe("policies", () => {
const EXPECTED_PRESET_NAMES = [
"brave",
"brew",
"discord",
"github",
"heygen",
"huggingface",
"jira",
"npm",
"outlook",
"pypi",
"slack",
"telegram",
];

describe("listPresets", () => {
it("returns all 11 presets", () => {
it("returns all expected presets", () => {
const presets = policies.listPresets();
expect(presets.length).toBe(11);
expect(presets.length).toBe(EXPECTED_PRESET_NAMES.length);
});

it("each preset has name and description", () => {
Expand All @@ -113,19 +128,7 @@ describe("policies", () => {
.listPresets()
.map((p) => p.name)
.sort();
const expected = [
"brave",
"brew",
"discord",
"github",
"huggingface",
"jira",
"npm",
"outlook",
"pypi",
"slack",
"telegram",
];
const expected = [...EXPECTED_PRESET_NAMES].sort();
expect(names).toEqual(expected);
});
});
Expand Down Expand Up @@ -171,6 +174,15 @@ describe("policies", () => {
expect(hosts).toEqual(["api.telegram.org"]);
});

it("extracts hosts from heygen preset including api2 for OAuth", () => {
const content = policies.loadPreset("heygen");
const hosts = policies.getPresetEndpoints(content);
expect(hosts.includes("api.heygen.com")).toBe(true);
expect(hosts.includes("api2.heygen.com")).toBe(true);
expect(hosts.includes("upload.heygen.com")).toBe(true);
expect(hosts.includes("files.heygen.com")).toBe(true);
});

it("every preset has at least one endpoint", () => {
for (const p of policies.listPresets()) {
const content = policies.loadPreset(p.name);
Expand Down