Skip to content

feat(plugin): request network access through policy local#3746

Open
cheese-head wants to merge 4 commits into
NVIDIA:mainfrom
cheese-head:policy-access/openclaw-network-tools
Open

feat(plugin): request network access through policy local#3746
cheese-head wants to merge 4 commits into
NVIDIA:mainfrom
cheese-head:policy-access/openclaw-network-tools

Conversation

@cheese-head

@cheese-head cheese-head commented May 18, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds OpenClaw plugin support for requesting OpenShell policy.local network access from inside the sandbox. This gives agents a structured way to list provider-backed presets, submit least-privilege access proposals, and poll proposal status.

Changes

  • Add nemoclaw/src/access-client.ts for policy.local proposal creation and status polling.
  • Register OpenClaw access tools in nemoclaw/src/index.ts.
  • Add plugin tests for preset listing, proposal submission, and status mapping.
  • Add live e2e helper scripts for the policy.local OpenClaw plugin flow.

Type of Change

  • Code change (feature, bug fix, or refactor)
  • Code change with doc updates
  • Doc only (prose changes, no code sample modifications)
  • Doc only (includes code sample changes)

Verification

  • npx prek run --all-files passes
  • npm test passes
  • Tests added or updated for new or changed behavior
  • No secrets, API keys, or credentials committed
  • Docs updated for user-facing behavior changes
  • make docs builds without warnings (doc changes only)
  • Doc pages follow the style guide (doc changes only)
  • New doc pages include SPDX header and frontmatter (new pages only)

Signed-off-by: Patrick Riel priel@nvidia.com

Summary by CodeRabbit

Release Notes

  • New Features

    • Integrated resource access request system enabling agents to request and monitor external service access through OpenShell policy management.
    • Added agent tools to list available resource presets, request resource access, and check request status.
    • Introduced provider profiles for multiple services: Slack, Jira, Discord, Hugging Face, PyPI, npm, Brave, Telegram, Homebrew, and local inference endpoints.
    • Automated provider profile onboarding during initial setup.
  • Documentation

    • Added integration guide documenting the end-to-end resource access workflow between agents and policy systems.

Review Change Stack

@copy-pr-bot

copy-pr-bot Bot commented May 18, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@coderabbitai

coderabbitai Bot commented May 18, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR implements NemoClaw's integration with OpenShell to enable agents to request external service access. It adds 10 provider profile configurations, an access client for OpenShell communication, three agent tools for resource access management, onboarding logic to import profiles, policy service wiring to synthesize presets from profiles, and comprehensive testing with e2e validation.

Changes

NemoClaw OpenShell Integration

Layer / File(s) Summary
Provider profile fixtures
nemoclaw-blueprint/provider-profiles/*.yaml (brave, brew, discord, huggingface, jira, local-inference, npm, pypi, slack, telegram)
Ten YAML configurations defining REST/WebSocket endpoints, enforcement rules, allowed paths, and required binary dependencies for external services like GitHub, Slack, Jira, PyPI, and others.
Access client implementation
nemoclaw/src/access-client.ts
Core HTTP client for OpenShell policy.local communication, including request/response types, provider profile ingestion with caching, rule synthesis (read vs read_write modes), HTTP proxy support, timeout handling, and preset derivation.
Access client unit tests
nemoclaw/src/access-client.test.ts
Vitest coverage for HTTP constraint validation, provider preset listing, dynamic environment-based preset augmentation, rule synthesis from provider profiles, and canonical request state tracking through approval lifecycle.
Agent tool definitions
nemoclaw/src/index.ts (lines 22–32, 114–124, 235, 361–473, 517–620)
New PluginToolDefinition and PluginToolResult types, registerTool API extension, and three agent tools: request_resource_access (creates request and waits), list_resource_access_presets (returns available presets), check_resource_access (fetches status with optional polling).
Agent tool registration tests
nemoclaw/src/register.test.ts
Tests verifying three tools are registered with correct metadata, mocked access-client invocation with expected parameters, and returned tool results for list/request/check flows.
Provider profile onboarding
src/lib/onboard/provider-profiles.ts, src/lib/onboard.ts
Scans YAML profiles directory, queries OpenShell for existing profiles, imports missing profiles via lint/import commands, detects unsupported subcommand fallback, logs results, and clears cached state. Wired into onboard() after gateway setup.
Policy service provider-profile support
src/lib/policy/index.ts
Extended with provider profile endpoint/binary types, OpenShell CLI querying (with env override), YAML preset synthesis, listPresets() refactored to merge built-in and provider-derived presets, cache clearing export.
Policy tests
test/policies.test.ts
Validates provider-backed presets appear in listPresets() and empty endpoints are skipped; loadPreset() synthesizes YAML with correct provider_profile and endpoint/binary shape.
Onboarding tests
test/provider-profile-onboard.test.ts
Validates shipped profile fixture IDs, selective import of missing profiles only, short-circuit when all present, and unsupported command fallback.
E2E runner and test script
test/e2e/nemoclaw-policy-local-runner.mjs, test/e2e/test-nemoclaw-policy-local-plugin.sh
Node CLI runner for local policy flow execution and bash harness that provisions sandbox, uploads plugin, enables agent policy, runs list/request/check commands over SSH, validates preset listing and approval state progression to applied.
Integration documentation
docs/reference/nemoclaw-openshell-integration.md
Reference guide describing NemoClaw↔OpenShell integration flow, agent tools, provider profile import idempotence, policy lifecycle, and fallback behavior with Mermaid diagram.

Sequence Diagram

sequenceDiagram
  participant Agent as NemoClaw Agent
  participant Policy as NemoClaw Plugin<br/>(Tools)
  participant AccessClient as Access Client<br/>(access-client.ts)
  participant OpenShell as OpenShell<br/>policy.local
  participant Sandbox as Agent Sandbox<br/>(HTTP)

  Agent->>Policy: execute list_resource_access_presets
  Policy->>AccessClient: listAccessPresets()
  AccessClient->>OpenShell: GET /openshell/presets (via HTTP)
  OpenShell->>AccessClient: built-in + provider presets
  AccessClient->>Policy: AccessPresetsResponse
  Policy->>Agent: tool result with preset list

  Agent->>Policy: execute request_resource_access<br/>(resource, mode)
  Policy->>AccessClient: createAccessRequest(preset, rules)
  AccessClient->>Sandbox: POST /policy/propose (JSON body)
  Sandbox->>OpenShell: forward proposal
  OpenShell->>Sandbox: accepted_chunk_id
  Sandbox->>AccessClient: response with chunk id
  AccessClient->>Policy: pending_approval status
  Policy->>AccessClient: waitAccessRequest(id)
  loop poll every N seconds
    AccessClient->>OpenShell: GET /proposal/{id}
  end
  OpenShell->>AccessClient: approved + policy_reloaded
  AccessClient->>Policy: applied status
  Policy->>Agent: tool result with applied status
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐰 A rabbit's ode to access requests

Profiles bundled, endpoints bound,
Access clients dance around,
Sandbox policies hear the plea,
Agent tools now run so free,
OpenShell and NemoClaw align,
Resource gating works just fine! 🔐

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 4.48% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat(plugin): request network access through policy local' directly describes the main feature addition—enabling plugin support for requesting network access through OpenShell policy.local from within the sandbox.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
⚔️ Resolve merge conflicts
  • Resolve merge conflict in branch policy-access/openclaw-network-tools

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 ESLint

If the error stems from missing dependencies, add them to the package.json file. For unrecoverable errors (e.g., due to private dependencies), disable the tool in the CodeRabbit configuration.

ESLint skipped: no ESLint configuration detected in root package.json. To enable, add eslint to devDependencies.


Comment @coderabbitai help to get the list of available commands and usage tips.

@wscurran

Copy link
Copy Markdown
Contributor

✨ Thanks for submitting this detailed PR to add OpenClaw plugin support for requesting OpenShell policy.local network access from inside the sandbox. This change aims to improve the security and usability of the sandbox environment by giving agents a structured way to request network access.

@cheese-head cheese-head marked this pull request as ready for review May 21, 2026 19:21

@coderabbitai coderabbitai Bot left a comment

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.

Actionable comments posted: 8

🧹 Nitpick comments (3)
src/lib/onboard.ts (1)

9751-9751: Run the onboarding E2E matrix for this flow change before merge.

This hook executes in the core onboarding path, so run the recommended targeted jobs (cloud-e2e, sandbox-operations-e2e, rebuild-openclaw-e2e, channels-stop-start-e2e, messaging-compatible-endpoint-e2e, hermes-discord-e2e, hermes-slack-e2e, openshell-gateway-upgrade-e2e).

As per coding guidelines, src/lib/onboard.ts “contains core onboarding logic” and includes the listed “E2E test recommendation” suite.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard.ts` at line 9751, The onboarding change adds a core hook call
ensureProviderProfilesAvailable() in src/lib/onboard.ts so you must run the
recommended targeted E2E matrix before merging: execute the cloud-e2e,
sandbox-operations-e2e, rebuild-openclaw-e2e, channels-stop-start-e2e,
messaging-compatible-endpoint-e2e, hermes-discord-e2e, hermes-slack-e2e and
openshell-gateway-upgrade-e2e pipelines against this branch, confirm all pass
(or capture/fix any failures), and attach the passing job artifacts to the PR;
do not merge the change to ensureProviderProfilesAvailable() until those E2E
jobs are green.
docs/reference/nemoclaw-openshell-integration.md (2)

68-68: ⚡ Quick win

Use one sentence per source line in prose sections.

Line 68 and Line 72 each contain multiple sentences on a single line.

As per coding guidelines "One sentence per line in source (makes diffs readable)."

Also applies to: 72-72

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/nemoclaw-openshell-integration.md` at line 68, The prose line
containing "Each agent adapter exposes the same tool names and response shape
through the harness-native mechanism. OpenClaw uses its plugin API. Hermes uses
its Python plugin API. Additional harnesses can implement the same contract
without changing the OpenShell policy proposal flow." should be split so each
sentence is on its own source line (one sentence per line); similarly split the
sentence group at the other occurrence starting with "OpenClaw" so that every
sentence occupies its own line to satisfy the "one sentence per line" guideline.

1-73: ⚡ Quick win

Add the required intro placement and a Next Steps section.

The page starts directly with a diagram instead of the required 1–2 sentence introduction, and it does not end with a Next Steps section linking related pages.

As per coding guidelines "Page starts with a one- or two-sentence introduction." and "A 'Next Steps' section at the bottom links to related pages."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/nemoclaw-openshell-integration.md` around lines 1 - 73, Add a
1–2 sentence intro above the mermaid diagram (locate the "# NemoClaw OpenShell
Integration" header or the "```mermaid" block) that briefly summarizes what this
page covers, and append a "Next Steps" section at the bottom (after the
"Provider Profiles" paragraph) with links to related docs such as the agent
tools and onboarding pages (eg. list_resource_access_presets,
request_resource_access, check_resource_access, and the NemoClaw
onboarding/import guidance) so readers can navigate to implementation and usage
details.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/reference/nemoclaw-openshell-integration.md`:
- Around line 1-2: Add a YAML frontmatter block at the top of the document
containing the required fields (title, description, keywords, topics, tags,
content type, difficulty, audience, status) and then insert the SPDX license
header immediately after the closing frontmatter delimiter; ensure the SPDX
header uses the project's standard SPDX string and that all frontmatter keys
match the docs schema exactly.

In `@nemoclaw/src/access-client.ts`:
- Around line 305-310: cleanProviderEndpoint currently uses
Number(endpoint.port) <= 0 which lets non-numeric ports like "abc" slip through
(NaN comparison is false); update the validation in cleanProviderEndpoint to
parse and validate the port explicitly (e.g., const p = Number(endpoint.port)),
then check Number.isFinite(p) && Number.isInteger(p) && p > 0 (optionally also p
<= 65535) and return null if that fails, otherwise set output.port = p; keep
references to ProviderProfileEndpoint and NetworkEndpoint and ensure invalid
ports are rejected and not serialized.
- Line 578: The proxy target construction is hard-coding policy.local:80, which
ignores the configured policyLocalUrl host and port when HTTP_PROXY is enabled.
Update the target logic in access-client.ts to derive the proxy destination from
the existing policyLocalUrl configuration (reusing the same parsed host/port
used in the non-proxy path) instead of a fixed hostname and port, and keep the
pathname/requestPath concatenation behavior unchanged.
- Around line 396-400: The preset merge in the listProviderProfilePresets()
handling is preserving the built-in preset object on name collisions, which
keeps the old rule and drops other provider_profile fields. Update the collision
path in the byName.set logic so a provider profile with the same preset name
replaces the preset’s provider_profile data from the imported preset instead of
only copying provider_profile; ensure the rule selection also follows the
provider-profile rule when a collision occurs.

In `@nemoclaw/src/index.ts`:
- Around line 384-403: The normalizeRequestedResource function currently
canonicalizes only GitHub hosts and returns other hostnames verbatim, causing
request_resource_access to send preset ids that list_resource_access_presets
never returns; update normalizeRequestedResource to either (A) validate inputs
against a closed set of supported provider host mappings (e.g., map pypi.org ->
pypi, registry.npmjs.org -> npm, api.telegram.org -> telegram, github.com ->
github) and return a canonical preset id for those hosts, or (B) throw an error
for any host not in that supported mapping so callers like
request_resource_access reject non-preset inputs up front; implement the chosen
approach inside normalizeRequestedResource (and update any callers if switching
to throwing) so preset ids always match what list_resource_access_presets
produces.

In `@src/lib/onboard.ts`:
- Around line 1795-1805: Move the wrapper/reporting logic out of
ensureProviderProfilesAvailable in onboard.ts into the provider-profiles module:
create a new exported helper in the provider-profiles module that calls
providerProfileOnboard.ensureNemoClawProviderProfiles(runOpenshell, { log: note
}) and performs the status branches (logging the "unsupported" message or the
"already-present" skipped list) and then calls
policies.clearProviderProfileCache(); then update
ensureProviderProfilesAvailable to simply call and return that new exported
helper (or inline a single short delegating call) so onboard.ts loses the extra
lines while keeping behavior identical; ensure imports/exports are updated to
reference the new helper and preserve the same runtime logging by reusing the
same runOpenshell, note, and policies symbols.

In `@src/lib/onboard/provider-profiles.ts`:
- Around line 116-123: The code currently treats any non-zero result from the
`provider list-profiles` invocation (the `list` variable and its `status` check)
as "unsupported"; change this so you only return {status: "unsupported", ...}
when the command output actually matches the unsupported-command patterns
(inspect `list.stdout` / `list.stderr` for those specific messages), and for any
other non-zero exit throw an Error that includes `list.stdout` and `list.stderr`
so callers can see the runtime failure details; update the branch that handles
`list.status !== 0` to perform the pattern match and either return the
unsupported response or throw with stderr/stdout accordingly.

In `@test/e2e/test-nemoclaw-policy-local-plugin.sh`:
- Around line 25-28: Before enabling agent_policy_proposals_enabled, read and
save the current global value by invoking "${OPENSHELL_BIN} settings get
--global --key agent_policy_proposals_enabled" (or equivalent) into a variable,
then set the flag as you do now; in cleanup(), restore the saved value by
calling "${OPENSHELL_BIN} settings set --global --key
agent_policy_proposals_enabled --value <saved_value>" (or delete/unset it if the
saved value indicates it was absent), ensuring the saved variable is in a scope
accessible to cleanup() so the original global setting is always restored even
on early exit.

---

Nitpick comments:
In `@docs/reference/nemoclaw-openshell-integration.md`:
- Line 68: The prose line containing "Each agent adapter exposes the same tool
names and response shape through the harness-native mechanism. OpenClaw uses its
plugin API. Hermes uses its Python plugin API. Additional harnesses can
implement the same contract without changing the OpenShell policy proposal
flow." should be split so each sentence is on its own source line (one sentence
per line); similarly split the sentence group at the other occurrence starting
with "OpenClaw" so that every sentence occupies its own line to satisfy the "one
sentence per line" guideline.
- Around line 1-73: Add a 1–2 sentence intro above the mermaid diagram (locate
the "# NemoClaw OpenShell Integration" header or the "```mermaid" block) that
briefly summarizes what this page covers, and append a "Next Steps" section at
the bottom (after the "Provider Profiles" paragraph) with links to related docs
such as the agent tools and onboarding pages (eg. list_resource_access_presets,
request_resource_access, check_resource_access, and the NemoClaw
onboarding/import guidance) so readers can navigate to implementation and usage
details.

In `@src/lib/onboard.ts`:
- Line 9751: The onboarding change adds a core hook call
ensureProviderProfilesAvailable() in src/lib/onboard.ts so you must run the
recommended targeted E2E matrix before merging: execute the cloud-e2e,
sandbox-operations-e2e, rebuild-openclaw-e2e, channels-stop-start-e2e,
messaging-compatible-endpoint-e2e, hermes-discord-e2e, hermes-slack-e2e and
openshell-gateway-upgrade-e2e pipelines against this branch, confirm all pass
(or capture/fix any failures), and attach the passing job artifacts to the PR;
do not merge the change to ensureProviderProfilesAvailable() until those E2E
jobs are green.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 28e5da0d-2364-4730-889a-72217ff1c00e

📥 Commits

Reviewing files that changed from the base of the PR and between d7bae57 and cdb8cfd.

📒 Files selected for processing (22)
  • docs/reference/nemoclaw-openshell-integration.md
  • nemoclaw-blueprint/provider-profiles/brave.yaml
  • nemoclaw-blueprint/provider-profiles/brew.yaml
  • nemoclaw-blueprint/provider-profiles/discord.yaml
  • nemoclaw-blueprint/provider-profiles/huggingface.yaml
  • nemoclaw-blueprint/provider-profiles/jira.yaml
  • nemoclaw-blueprint/provider-profiles/local-inference.yaml
  • nemoclaw-blueprint/provider-profiles/npm.yaml
  • nemoclaw-blueprint/provider-profiles/pypi.yaml
  • nemoclaw-blueprint/provider-profiles/slack.yaml
  • nemoclaw-blueprint/provider-profiles/telegram.yaml
  • nemoclaw/src/access-client.test.ts
  • nemoclaw/src/access-client.ts
  • nemoclaw/src/index.ts
  • nemoclaw/src/register.test.ts
  • src/lib/onboard.ts
  • src/lib/onboard/provider-profiles.ts
  • src/lib/policy/index.ts
  • test/e2e/nemoclaw-policy-local-runner.mjs
  • test/e2e/test-nemoclaw-policy-local-plugin.sh
  • test/policies.test.ts
  • test/provider-profile-onboard.test.ts

Comment on lines +1 to +2
# NemoClaw OpenShell Integration

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 | ⚡ Quick win

Add required docs frontmatter and SPDX placement.

This new docs page is missing required frontmatter fields, and the SPDX header is not present after frontmatter as required for docs pages.

As per coding guidelines "SPDX license header is present after frontmatter." and "Frontmatter includes title, description, keywords, topics, tags, content type, difficulty, audience, and status fields."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@docs/reference/nemoclaw-openshell-integration.md` around lines 1 - 2, Add a
YAML frontmatter block at the top of the document containing the required fields
(title, description, keywords, topics, tags, content type, difficulty, audience,
status) and then insert the SPDX license header immediately after the closing
frontmatter delimiter; ensure the SPDX header uses the project's standard SPDX
string and that all frontmatter keys match the docs schema exactly.

Comment on lines +305 to +310
function cleanProviderEndpoint(endpoint: ProviderProfileEndpoint): NetworkEndpoint | null {
if (typeof endpoint.host !== "string" || Number(endpoint.port) <= 0) return null;
const output: NetworkEndpoint = {
host: endpoint.host,
port: Number(endpoint.port),
};

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 | ⚡ Quick win

Reject malformed provider-profile ports here.

Number(endpoint.port) <= 0 lets values like "abc" through because NaN <= 0 is false. That leaves an unusable endpoint in the preset list and serializes port as null when a proposal is submitted.

Suggested fix
 function cleanProviderEndpoint(endpoint: ProviderProfileEndpoint): NetworkEndpoint | null {
-  if (typeof endpoint.host !== "string" || Number(endpoint.port) <= 0) return null;
+  const port = typeof endpoint.port === "number" ? endpoint.port : Number(endpoint.port);
+  if (
+    typeof endpoint.host !== "string" ||
+    endpoint.host.trim() === "" ||
+    !Number.isInteger(port) ||
+    port < 1 ||
+    port > 65535
+  ) {
+    return null;
+  }
   const output: NetworkEndpoint = {
     host: endpoint.host,
-    port: Number(endpoint.port),
+    port,
   };
📝 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
function cleanProviderEndpoint(endpoint: ProviderProfileEndpoint): NetworkEndpoint | null {
if (typeof endpoint.host !== "string" || Number(endpoint.port) <= 0) return null;
const output: NetworkEndpoint = {
host: endpoint.host,
port: Number(endpoint.port),
};
function cleanProviderEndpoint(endpoint: ProviderProfileEndpoint): NetworkEndpoint | null {
const port = typeof endpoint.port === "number" ? endpoint.port : Number(endpoint.port);
if (
typeof endpoint.host !== "string" ||
endpoint.host.trim() === "" ||
!Number.isInteger(port) ||
port < 1 ||
port > 65535
) {
return null;
}
const output: NetworkEndpoint = {
host: endpoint.host,
port,
};
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoclaw/src/access-client.ts` around lines 305 - 310, cleanProviderEndpoint
currently uses Number(endpoint.port) <= 0 which lets non-numeric ports like
"abc" slip through (NaN comparison is false); update the validation in
cleanProviderEndpoint to parse and validate the port explicitly (e.g., const p =
Number(endpoint.port)), then check Number.isFinite(p) && Number.isInteger(p) &&
p > 0 (optionally also p <= 65535) and return null if that fails, otherwise set
output.port = p; keep references to ProviderProfileEndpoint and NetworkEndpoint
and ensure invalid ports are rejected and not serialized.

Comment on lines +396 to +400
for (const preset of listProviderProfilePresets()) {
const existing = byName.get(preset.name);
byName.set(
preset.name,
existing ? { ...existing, provider_profile: preset.provider_profile } : preset,

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 | ⚡ Quick win

Use the provider-profile rule on preset name collisions.

When a provider profile reuses a built-in preset name, this merge keeps the baked-in rule and only copies provider_profile. That drops the imported profile’s endpoints, binaries, and extra endpoint fields, so proposals for common presets can drift from the OpenShell definition.

Suggested fix
   for (const preset of listProviderProfilePresets()) {
     const existing = byName.get(preset.name);
     byName.set(
       preset.name,
-      existing ? { ...existing, provider_profile: preset.provider_profile } : preset,
+      existing
+        ? {
+            ...preset,
+            description: preset.description || existing.description,
+          }
+        : preset,
     );
   }
📝 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
for (const preset of listProviderProfilePresets()) {
const existing = byName.get(preset.name);
byName.set(
preset.name,
existing ? { ...existing, provider_profile: preset.provider_profile } : preset,
for (const preset of listProviderProfilePresets()) {
const existing = byName.get(preset.name);
byName.set(
preset.name,
existing
? {
...preset,
description: preset.description || existing.description,
}
: preset,
);
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoclaw/src/access-client.ts` around lines 396 - 400, The preset merge in
the listProviderProfilePresets() handling is preserving the built-in preset
object on name collisions, which keeps the old rule and drops other
provider_profile fields. Update the collision path in the byName.set logic so a
provider profile with the same preset name replaces the preset’s
provider_profile data from the imported preset instead of only copying
provider_profile; ensure the rule selection also follows the provider-profile
rule when a collision occurs.

return Promise.reject(new Error("HTTP proxy is not configured."));
}

const target = `http://policy.local:80${base.pathname.replace(/\/$/, "")}${requestPath}`;

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 | ⚡ Quick win

Honor the configured policyLocalUrl host and port in proxy mode.

The proxy request target is hard-coded to policy.local:80, so a custom policyLocalUrl port works without a proxy but breaks as soon as HTTP_PROXY is present.

Suggested fix
-  const target = `http://policy.local:80${base.pathname.replace(/\/$/, "")}${requestPath}`;
+  const target = `${base.protocol}//${base.host}${base.pathname.replace(/\/$/, "")}${requestPath}`;
📝 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
const target = `http://policy.local:80${base.pathname.replace(/\/$/, "")}${requestPath}`;
const target = `${base.protocol}//${base.host}${base.pathname.replace(/\/$/, "")}${requestPath}`;
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoclaw/src/access-client.ts` at line 578, The proxy target construction is
hard-coding policy.local:80, which ignores the configured policyLocalUrl host
and port when HTTP_PROXY is enabled. Update the target logic in access-client.ts
to derive the proxy destination from the existing policyLocalUrl configuration
(reusing the same parsed host/port used in the non-proxy path) instead of a
fixed hostname and port, and keep the pathname/requestPath concatenation
behavior unchanged.

Comment thread nemoclaw/src/index.ts
Comment on lines +384 to +403
function normalizeRequestedResource(resource: string): string {
const normalized = resource.trim().toLowerCase();
const normalizedHost = (() => {
if (!normalized.includes("://")) {
return normalized;
}
try {
return new URL(normalized).hostname.toLowerCase();
} catch {
return normalized;
}
})();
if (
normalizedHost === "github" ||
normalizedHost === "github.com" ||
normalizedHost === "api.github.com"
) {
return "github";
}
return normalizedHost;

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 | ⚡ Quick win

Don't silently accept non-GitHub hostnames as preset ids.

Only GitHub inputs are canonicalized here. Other common provider-backed hosts like pypi.org, registry.npmjs.org, or api.telegram.org fall through as raw hostnames, so request_resource_access can submit preset ids that list_resource_access_presets never returns. Either reject non-preset inputs up front or normalize all supported provider-backed hosts before building the request.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@nemoclaw/src/index.ts` around lines 384 - 403, The normalizeRequestedResource
function currently canonicalizes only GitHub hosts and returns other hostnames
verbatim, causing request_resource_access to send preset ids that
list_resource_access_presets never returns; update normalizeRequestedResource to
either (A) validate inputs against a closed set of supported provider host
mappings (e.g., map pypi.org -> pypi, registry.npmjs.org -> npm,
api.telegram.org -> telegram, github.com -> github) and return a canonical
preset id for those hosts, or (B) throw an error for any host not in that
supported mapping so callers like request_resource_access reject non-preset
inputs up front; implement the chosen approach inside normalizeRequestedResource
(and update any callers if switching to throwing) so preset ids always match
what list_resource_access_presets produces.

Comment thread src/lib/onboard.ts
Comment on lines +1795 to +1805
function ensureProviderProfilesAvailable(): void {
const result = providerProfileOnboard.ensureNemoClawProviderProfiles(runOpenshell, {
log: note,
});
if (result.status === "unsupported") {
note(` ${result.message}`);
} else if (result.status === "already-present" && result.skipped.length > 0) {
note(` NemoClaw provider profiles already registered: ${result.skipped.join(", ")}`);
}
policies.clearProviderProfileCache();
}

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 | ⚡ Quick win

Onboard entrypoint budget regression is currently blocking CI.

This helper adds net lines in src/lib/onboard.ts, and the onboard-entrypoint-budget check is failing (+16 lines). Please move this wrapper/reporting logic into src/lib/onboard/provider-profiles.ts (or otherwise reduce net lines in onboard.ts) so the budget gate passes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard.ts` around lines 1795 - 1805, Move the wrapper/reporting
logic out of ensureProviderProfilesAvailable in onboard.ts into the
provider-profiles module: create a new exported helper in the provider-profiles
module that calls
providerProfileOnboard.ensureNemoClawProviderProfiles(runOpenshell, { log: note
}) and performs the status branches (logging the "unsupported" message or the
"already-present" skipped list) and then calls
policies.clearProviderProfileCache(); then update
ensureProviderProfilesAvailable to simply call and return that new exported
helper (or inline a single short delegating call) so onboard.ts loses the extra
lines while keeping behavior identical; ensure imports/exports are updated to
reference the new helper and preserve the same runtime logging by reusing the
same runOpenshell, note, and policies symbols.

Comment on lines +116 to +123
if (list.status !== 0) {
return {
status: "unsupported",
imported: [],
skipped: [],
message: "OpenShell provider profiles are not available; using local preset fallbacks.",
};
}

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 | ⚡ Quick win

Don’t classify all list-profiles failures as unsupported.

A non-zero exit from provider list-profiles is currently always mapped to unsupported, which can hide transient/runtime failures and silently force fallback behavior. Only return unsupported when the output matches unsupported-command patterns; otherwise throw with stderr/stdout details.

Proposed fix
   const list = runOpenshell(["provider", "list-profiles", "-o", "json"], {
     ignoreError: true,
     stdio: ["ignore", "pipe", "pipe"],
     suppressOutput: true,
     timeout: 10_000,
   });
   if (list.status !== 0) {
-    return {
-      status: "unsupported",
-      imported: [],
-      skipped: [],
-      message: "OpenShell provider profiles are not available; using local preset fallbacks.",
-    };
+    if (isUnsupportedProviderProfileCommand(list)) {
+      return {
+        status: "unsupported",
+        imported: [],
+        skipped: [],
+        message: "OpenShell provider profiles are not available; using local preset fallbacks.",
+      };
+    }
+    const details =
+      outputText(list.stderr) || outputText(list.stdout) || "provider list-profiles failed";
+    throw new Error(`NemoClaw provider profile listing failed: ${details.trim()}`);
   }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/lib/onboard/provider-profiles.ts` around lines 116 - 123, The code
currently treats any non-zero result from the `provider list-profiles`
invocation (the `list` variable and its `status` check) as "unsupported"; change
this so you only return {status: "unsupported", ...} when the command output
actually matches the unsupported-command patterns (inspect `list.stdout` /
`list.stderr` for those specific messages), and for any other non-zero exit
throw an Error that includes `list.stdout` and `list.stderr` so callers can see
the runtime failure details; update the branch that handles `list.status !== 0`
to perform the pattern match and either return the unsupported response or throw
with stderr/stdout accordingly.

Comment on lines +25 to +28
"${OPENSHELL_BIN}" settings set --global \
--key agent_policy_proposals_enabled \
--value true \
--yes

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 | ⚡ Quick win

Restore the previous global policy setting in cleanup().

This helper permanently mutates the caller's OpenShell config. If the script exits after enabling agent_policy_proposals_enabled, later local runs inherit that state unexpectedly. Please capture the existing value before settings set --global and restore it from cleanup().

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@test/e2e/test-nemoclaw-policy-local-plugin.sh` around lines 25 - 28, Before
enabling agent_policy_proposals_enabled, read and save the current global value
by invoking "${OPENSHELL_BIN} settings get --global --key
agent_policy_proposals_enabled" (or equivalent) into a variable, then set the
flag as you do now; in cleanup(), restore the saved value by calling
"${OPENSHELL_BIN} settings set --global --key agent_policy_proposals_enabled
--value <saved_value>" (or delete/unset it if the saved value indicates it was
absent), ensuring the saved variable is in a scope accessible to cleanup() so
the original global setting is always restored even on early exit.

@cv cv left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed with the PR review advisor rubric (static patch review only; not evaluating CI/mergeability). Requesting changes before merge:

  1. This PR includes the provider-profile layer from #3745, so the same blockers apply here: src/lib/onboard/provider-profiles.ts:116 should not classify every provider list-profiles non-zero exit as unsupported, and provider endpoint ports need finite integer 1..65535 validation.
  2. nemoclaw/src/access-client.ts:306 has the same port-validation issue in the in-sandbox OpenClaw client. Number(endpoint.port) <= 0 lets NaN through and then serializes it at line 309 into policy.local proposals. Please validate finite integer ports, including an upper bound.
  3. nemoclaw/src/access-client.ts:578 hardcodes proxy absolute-form requests to http://policy.local:80.... That ignores OPENSHELL_POLICY_LOCAL_URL host/port when proxy transport is used. Please derive the absolute target from the parsed policyLocalUrl while preserving the configured path prefix.
  4. nemoclaw/src/index.ts:384 only normalizes GitHub URLs to preset ids. A resource like https://pypi.org/project/x becomes pypi.org, but list_presets returns pypi. Please either require exact preset ids or map all supported provider hosts to their canonical preset ids before submitting proposals.
  5. nemoclaw/src/access-client.ts:398 preserves fallback rules on provider-profile preset collisions. If provider profiles are the source of truth when available, the provider profile's rule should replace the fallback rule.

Please add tests for invalid provider-profile ports, custom OPENSHELL_POLICY_LOCAL_URL with HTTP proxy, and non-GitHub URL normalization/rejection.

@wscurran wscurran added area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery feature PR adds or expands user-visible functionality and removed enhancement: feature labels Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: policy Network policy, egress rules, presets, or sandbox policy area: sandbox OpenShell sandbox lifecycle, runtime, config, or recovery feature PR adds or expands user-visible functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants