diff --git a/.agents/skills/update-docs-from-commits/SKILL.md b/.agents/skills/update-docs/SKILL.md similarity index 99% rename from .agents/skills/update-docs-from-commits/SKILL.md rename to .agents/skills/update-docs/SKILL.md index 83cd9cac0c4..19065d64612 100644 --- a/.agents/skills/update-docs-from-commits/SKILL.md +++ b/.agents/skills/update-docs/SKILL.md @@ -1,5 +1,5 @@ --- -name: update-docs-from-commits +name: update-docs description: Scan recent git commits for changes that affect user-facing behavior, then draft or update the corresponding documentation pages. Use when docs have fallen behind code changes, after a batch of features lands, or when preparing a release. Trigger keywords - update docs, draft docs, docs from commits, sync docs, catch up docs, doc debt, docs behind, docs drift. --- diff --git a/.coderabbit.yaml b/.coderabbit.yaml new file mode 100644 index 00000000000..058a1f271fd --- /dev/null +++ b/.coderabbit.yaml @@ -0,0 +1,114 @@ +# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json +language: "en-US" +early_access: false +reviews: + profile: "chill" + request_changes_workflow: false + high_level_summary: true + poem: true + review_status: true + review_details: false + auto_review: + enabled: true + drafts: false + path_instructions: + - path: "docs/**" + instructions: | + You are an editorial reviewer for NemoClaw documentation. + Review every docs change against the style guide below. Flag violations + inline as review comments. Do not rewrite the content — point out the + issue and cite the relevant rule so the author can fix it. + + ## Voice and Tone + + - Active voice required. Flag passive constructions. + Bad: "A gateway is created by the CLI." + Good: "The CLI creates a gateway." + - Second person ("you") when addressing the reader. + - Present tense. Flag future tense ("will") in descriptions of current behavior. + - No hedge words: flag "simply," "just," "easily," "of course." + + ## LLM-Generated Patterns (Flag These) + + These are common in AI-generated text and erode trust with technical readers. + Flag every occurrence: + + - Unnecessary bold on routine instructions ("This is a **critical** step" + when nothing is actually critical). Bold is reserved for UI labels, + parameter names, and genuine warnings. + - Excessive em dashes. One per paragraph is fine; multiple per paragraph + or em dashes used instead of commas/periods should be flagged. + - Superlatives and marketing language ("powerful," "robust," "seamless," + "cutting-edge"). Say what it does, not how great it is. + - Emoji in documentation prose. + - Rhetorical questions ("Want to secure your agents? Look no further!"). + State the purpose directly. + - Filler introductions ("In this section, we will explore..."). Start + with the content. + + ## Formatting Rules + + - Every sentence must end with a period. + - One sentence per line in source (makes diffs readable). Flag paragraphs + where multiple sentences appear on the same line. + - CLI commands, file paths, flags, parameter names, and values must use + inline `code` formatting. + - CLI code blocks must use the `console` language tag with `$` prompt + prefix. Flag ```bash or ```shell for CLI examples. + - Use MyST admonitions (:::{tip}, :::{note}, :::{warning}) for callouts, + not bold text or blockquotes. + - No nested admonitions. + - Do not number section titles. Flag "Section 1: ...", "Step 3: ...", etc. + - No colons in titles. Flag "Inference: Cloud and Local" — should be + "Cloud and Local Inference." + - Colons should only introduce a list. Flag colons used as general + punctuation between clauses. + + ## Word List (Flag Incorrect Usage) + + | Correct | Incorrect (flag these) | + |----------------|------------------------------------------------------| + | NVIDIA | Nvidia, nvidia | + | NemoClaw | nemoclaw (in prose), Nemoclaw | + | OpenClaw | openclaw (in prose), Openclaw | + | OpenShell | Open Shell, openShell, Openshell, openshell (in prose)| + | CLI | cli, Cli | + | API key | api key, API Key | + | mTLS | MTLS, mtls | + | YAML | yaml (in prose), Yaml | + | gateway | Gateway (unless starting a sentence) | + | sandbox | Sandbox (unless starting a sentence) | + + Words inside code blocks or inline code spans are exempt from the word + list (e.g., `nemoclaw onboard` is correct). + + ## Page Structure + + When reviewing new pages, verify: + - SPDX license header is present after frontmatter. + - Frontmatter includes title, description, keywords, topics, tags, + content type, difficulty, audience, and status fields. + - H1 heading matches the `title.page` frontmatter value. + - Page starts with a one- or two-sentence introduction. + - Sections use H2 and H3, each starting with an introductory sentence. + - A "Next Steps" section at the bottom links to related pages. + + ## Severity + + - Word list and voice violations: flag as suggestions. + - Missing SPDX header, broken cross-references, or incorrect code + block language: flag as issues. + - LLM-generated patterns (bold overuse, em dashes, superlatives, + hedge words): flag as suggestions with the note "LLM pattern detected." + - path: "**/*.md" + instructions: | + This rule applies to all Markdown files project-wide. For files + under docs/, the stricter docs/** rules defined earlier in this + file take precedence; this rule adds baseline checks for every + other Markdown file (README, CONTRIBUTING, architecture docs, etc.). + + - NVIDIA must be all caps (not Nvidia, nvidia). + - NemoClaw, OpenClaw, and OpenShell must use correct casing. + - No emoji in technical prose. +chat: + auto_reply: true diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..1a9be7a3eba --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,55 @@ +name: Bug Report +description: Report a bug you confirmed and could not fix. +labels: ["bug", "status: triage"] +body: + - type: textarea + id: description + attributes: + label: Description + description: What happened? What did you expect to happen? + validations: + required: true + + - type: textarea + id: reproduction + attributes: + label: Reproduction Steps + description: Minimal steps to reproduce the issue. + placeholder: | + 1. Run `nemoclaw onboard` + 2. Select "local" inference + 3. ... + validations: + required: true + + - type: textarea + id: environment + attributes: + label: Environment + description: OS, Node.js version, Docker version, NemoClaw version, and any other relevant details. + placeholder: | + - OS: macOS 15.2 / Ubuntu 24.04 / Windows 11 + WSL2 + - Node.js: v20.x + - Docker: Docker Desktop 4.x / Docker Engine 27.x + - NemoClaw: v0.x.x (output of `nemoclaw --version`) + validations: + required: true + + - type: textarea + id: logs + attributes: + label: Logs + description: Relevant log output, error messages, or stack traces. + render: shell + validations: + required: false + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I confirmed this bug is reproducible + required: true + - label: I searched existing issues and this is not a duplicate + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..a6cc0194935 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,7 @@ +blank_issues_enabled: false +contact_links: + - name: Security vulnerability? + url: https://github.com/NVIDIA/NemoClaw/blob/main/SECURITY.md + about: > + Do not report security vulnerabilities through GitHub issues. Follow + the disclosure process in SECURITY.md. diff --git a/.github/ISSUE_TEMPLATE/doc_issue.yml b/.github/ISSUE_TEMPLATE/doc_issue.yml new file mode 100644 index 00000000000..823fcef14e4 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/doc_issue.yml @@ -0,0 +1,42 @@ +name: Documentation Issue +description: Report incorrect, missing, or unclear documentation. +labels: ["documentation", "status: triage"] +body: + - type: textarea + id: description + attributes: + label: Description + description: What is wrong, missing, or unclear in the docs? + validations: + required: true + + - type: input + id: page + attributes: + label: Affected Page + description: URL or file path of the doc page (e.g., docs/get-started/quickstart.md). + placeholder: docs/reference/commands.md + validations: + required: false + + - type: dropdown + id: issue-type + attributes: + label: Issue Type + options: + - "Inaccurate information" + - "Missing documentation" + - "Unclear or confusing" + - "Broken link or cross-reference" + - "Formatting or rendering problem" + - "Other" + validations: + required: true + + - type: textarea + id: suggestion + attributes: + label: Suggested Fix + description: If you know what the correct content should be, describe it here. + validations: + required: false diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 00000000000..c2bdfae94c7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,51 @@ +name: Feature Request +description: Propose a feature with a design. Not a "please build this" request. +labels: ["enhancement", "status: triage"] +body: + - type: textarea + id: problem + attributes: + label: Problem Statement + description: What problem does this solve? Why does it matter? + validations: + required: true + + - type: textarea + id: design + attributes: + label: Proposed Design + description: | + How should this work? Describe the system behavior, components involved, and user-facing interface. + This should be a design, not a wish list. + validations: + required: true + + - type: textarea + id: alternatives + attributes: + label: Alternatives Considered + description: What other approaches did you evaluate? Why is the proposed design better? + validations: + required: false + + - type: dropdown + id: category + attributes: + label: Category + options: + - "enhancement: feature" + - "enhancement: provider" + - "enhancement: platform" + - "enhancement: testing" + validations: + required: true + + - type: checkboxes + id: checklist + attributes: + label: Checklist + options: + - label: I searched existing issues and this is not a duplicate + required: true + - label: This is a design proposal, not a "please build this" request + required: true diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000000..9e84b051d96 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,40 @@ +## Summary + + +## Related Issue + + +## Changes + + +## Type of Change + +- [ ] Code change for a new feature, bug fix, or refactor. +- [ ] Code change with doc updates. +- [ ] Doc only. Prose changes without code sample modifications. +- [ ] Doc only. Includes code sample changes. + +## Testing + +- [ ] `make check` passes. +- [ ] `npm test` passes. +- [ ] `make docs` builds without warnings. (for doc-only changes) + +## Checklist + +### General +- [ ] I have read and followed the [contributing guide](CONTRIBUTING.md). +- [ ] I have read and followed the [style guide](docs/CONTRIBUTING.md). (for doc-only changes) + +### Code Changes + +- [ ] `make format` applied (TypeScript and Python). +- [ ] Tests added or updated for new or changed behavior. +- [ ] No secrets, API keys, or credentials committed. +- [ ] Doc pages updated for any user-facing behavior changes (new commands, changed defaults, new features, bug fixes that contradict existing docs). + +### Doc Changes + +- [ ] Follows the [style guide](docs/CONTRIBUTING.md). Try running the `update-docs` agent skill to draft changes while complying with the style guide. For example, prompt your agent with "`/update-docs` catch up the docs for the new changes I made in this PR." +- [ ] New pages include SPDX license header and frontmatter, if creating a new page. +- [ ] Cross-references and links verified. diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml new file mode 100644 index 00000000000..d59f31df4b4 --- /dev/null +++ b/.github/workflows/docs.yaml @@ -0,0 +1,43 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: docs + +on: + pull_request: + types: [opened, synchronize, reopened] + paths: + - "docs/**" + - "README.md" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/docs.yaml" + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Install uv + uses: astral-sh/setup-uv@v4 + + - name: Install doc dependencies + run: uv sync --group docs + + - name: Build docs (strict) + run: uv run --group docs sphinx-build -W -b html docs docs/_build/html diff --git a/.github/workflows/nightly-e2e.yaml b/.github/workflows/nightly-e2e.yaml new file mode 100644 index 00000000000..98f248bfe92 --- /dev/null +++ b/.github/workflows/nightly-e2e.yaml @@ -0,0 +1,49 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Nightly full E2E: install → onboard → live inference against NVIDIA Cloud API. +# Runs directly on the runner (not inside Docker) because OpenShell bootstraps +# a K3s cluster inside a privileged Docker container — nesting would break networking. +# +# Requires NVIDIA_API_KEY repository secret. +# Only runs on schedule and manual dispatch — never on PRs (secret protection). + +name: nightly-e2e + +on: + schedule: + - cron: "0 0 * * *" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: nightly-e2e + cancel-in-progress: true + +jobs: + full-e2e: + if: github.repository == 'NVIDIA/NemoClaw' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Run full E2E test + env: + NVIDIA_API_KEY: ${{ secrets.NVIDIA_API_KEY }} + NEMOCLAW_NON_INTERACTIVE: "1" + NEMOCLAW_SANDBOX_NAME: "e2e-nightly" + NEMOCLAW_RECREATE_SANDBOX: "1" + GITHUB_TOKEN: ${{ github.token }} + run: bash test/e2e/test-full-e2e.sh + + - name: Upload install log on failure + if: failure() + uses: actions/upload-artifact@v4 + with: + name: install-log + path: /tmp/nemoclaw-e2e-install.log + if-no-files-found: ignore diff --git a/.github/workflows/pr-limit.yaml b/.github/workflows/pr-limit.yaml new file mode 100644 index 00000000000..ebf95d34992 --- /dev/null +++ b/.github/workflows/pr-limit.yaml @@ -0,0 +1,40 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: pr-limit + +# pull_request_target runs in the base repo context, giving the token write +# access even for fork PRs. This is safe here because this workflow never +# checks out or executes code from the PR — it only counts open PRs and +# closes excess ones. Do NOT add a checkout step or run PR-sourced code +# in this workflow to prevent injection attacks. +on: + pull_request_target: + types: [opened, reopened] + +permissions: + pull-requests: write + +jobs: + check-pr-limit: + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Check open PR count for author + env: + GH_TOKEN: ${{ github.token }} + AUTHOR: ${{ github.event.pull_request.user.login }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + run: | + OPEN_COUNT=$(gh pr list --repo "$REPO" --author "$AUTHOR" --state open --json number --jq 'length') + + echo "Author $AUTHOR has $OPEN_COUNT open PR(s)" + + if [ "$OPEN_COUNT" -gt 10 ]; then + gh pr comment "$PR_NUMBER" --repo "$REPO" --body \ + "This repository limits contributors to 10 open pull requests. Please close or merge existing PRs before opening new ones." + gh pr close "$PR_NUMBER" --repo "$REPO" + echo "::error::PR closed — author $AUTHOR exceeds the 10 open PR limit" + exit 1 + fi diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml new file mode 100644 index 00000000000..936b6ef91f1 --- /dev/null +++ b/.github/workflows/pr.yaml @@ -0,0 +1,58 @@ +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +name: pr + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + test-unit: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + + - name: Install root dependencies + run: npm install + + - name: Install and build TypeScript plugin + working-directory: nemoclaw + run: | + npm install + npm run build + + - name: Run root unit tests + run: node --test test/*.test.js + + - name: Run TypeScript unit tests + working-directory: nemoclaw + run: npx vitest run + + test-e2e-sandbox: + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Build sandbox test image + run: docker build -f test/Dockerfile.sandbox -t nemoclaw-sandbox-test . + + - name: Run sandbox E2E tests + run: docker run --rm -v "${{ github.workspace }}/test:/opt/test" nemoclaw-sandbox-test /opt/test/e2e-test.sh diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 00000000000..efdeccacee2 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,84 @@ +# Contributor Covenant Code of Conduct + +## Overview + +Define the code of conduct followed and enforced for NemoClaw + +### Intended audience + +Community | Developers | Project Leads + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as +contributors and maintainers pledge to making participation in our project and +our community a harassment-free experience for everyone, regardless of age, body +size, disability, ethnicity, sex characteristics, gender identity and expression, +level of experience, education, socio-economic status, nationality, personal +appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment +include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or + advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic + address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable +behavior and are expected to take appropriate and fair corrective action in +response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or +reject comments, commits, code, wiki edits, issues, and other contributions +that are not aligned to this Code of Conduct, or to ban temporarily or +permanently any contributor for other behaviors that they deem inappropriate, +threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces +when an individual is representing the project or its community. Examples of +representing a project or community include using an official project e-mail +address, posting via an official social media account, or acting as an appointed +representative at an online or offline event. Representation of a project may be +further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported by contacting GitHub_Conduct@nvidia.com. All complaints will be reviewed and +investigated and will result in a response that is deemed necessary and appropriate +to the circumstances. The project team is obligated to maintain confidentiality with +regard to the reporter of an incident. Further details of specific enforcement policies +may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good +faith may face temporary or permanent repercussions as determined by other +members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, +available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see +https://www.contributor-covenant.org/faq \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 664c1bc4178..7751326fe50 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,63 +1,133 @@ -# Contributing +# Contributing to NVIDIA NemoClaw -## Signing Your Work +Thank you for your interest in contributing to NVIDIA NemoClaw. This guide covers how to set up your development environment, run tests, and submit changes. -* We require that all contributors "sign-off" on their commits. This certifies - that the contribution is your original work, or you have rights to submit it - under the same license, or a compatible license. +## Before You Open an Issue - * Any contribution which contains commits that are not Signed-Off will not be - accepted. +Open an issue when you encounter one of the following situations. -* To sign off on a commit you simply use the `--signoff` (or `-s`) option when - committing your changes: +- A real bug that you confirmed and could not fix. +- A feature proposal with a design — not a "please build this" request. +- Security vulnerabilities must follow [SECURITY.md](SECURITY.md) — **not** GitHub issues. - ```bash - git commit -s -m "Add cool feature." - ``` +## Prerequisites - This will append the following to your commit message: +Install the following before you begin. - ```text - Signed-off-by: Your Name - ``` +- Node.js 20+ and npm 10+ +- Python 3.11+ (for blueprint and documentation builds) +- Docker (running) +- [uv](https://docs.astral.sh/uv/) (for Python dependency management) -* Full text of the DCO: +## Getting Started - ```text - Developer Certificate of Origin - Version 1.1 +Install the root dependencies and build the TypeScript plugin: - Copyright (C) 2004, 2006 The Linux Foundation and its contributors. - 1 Letterman Drive - Suite D4700 - San Francisco, CA, 94129 +```bash +# Install root dependencies (OpenClaw + CLI entry point) +npm install - Everyone is permitted to copy and distribute verbatim copies of this - license document, but changing it is not allowed. - ``` +# Install and build the TypeScript plugin +cd nemoclaw && npm install && npm run build && cd .. - ```text - Developer's Certificate of Origin 1.1 +# Install Python deps for the blueprint +cd nemoclaw-blueprint && uv sync && cd .. +``` - By making a contribution to this project, I certify that: +## Building - (a) The contribution was created in whole or in part by me and I have the - right to submit it under the open source license indicated in the file; or +The TypeScript plugin lives in `nemoclaw/` and compiles with `tsc`: - (b) The contribution is based upon previous work that, to the best of my - knowledge, is covered under an appropriate open source license and I have - the right under that license to submit that work with modifications, - whether created in whole or in part by me, under the same open source - license (unless I am permitted to submit under a different license), as - indicated in the file; or +```bash +cd nemoclaw +npm run build # one-time compile +npm run dev # watch mode +``` - (c) The contribution was provided directly to me by some other person who - certified (a), (b) or (c) and I have not modified it. +## Main Tasks - (d) I understand and agree that this project and the contribution are - public and that a record of the contribution (including all personal - information I submit with it, including my sign-off) is maintained - indefinitely and may be redistributed consistent with this project or the - open source license(s) involved. - ``` +These are the primary `make` and `npm` targets for day-to-day development: + +| Task | Purpose | +|------|---------| +| `make check` | Run all linters (TypeScript + Python) | +| `make lint` | Same as `make check` | +| `make format` | Auto-format TypeScript and Python source | +| `npm test` | Run root-level tests (`test/*.test.js`) | +| `cd nemoclaw && npm test` | Run plugin unit tests (Vitest) | +| `make docs` | Build documentation (Sphinx/MyST) | +| `make docs-live` | Serve docs locally with auto-rebuild | + +## Project Structure + +The repository is organized as follows. + +| Path | Purpose | +|------|---------| +| `nemoclaw/` | TypeScript plugin (Commander CLI, OpenClaw extension) | +| `nemoclaw-blueprint/` | Python blueprint for sandbox orchestration | +| `bin/` | CLI entry point (`nemoclaw.js`) | +| `scripts/` | Install helpers and automation scripts | +| `test/` | Root-level integration tests | +| `docs/` | User-facing documentation (Sphinx/MyST) | + +## Documentation + +If your change affects user-facing behavior (new commands, changed defaults, new features, bug fixes that contradict existing docs), update the relevant pages under `docs/` in the same PR. + +If you use an AI coding agent (Cursor, Claude Code, Codex, etc.), the repo includes the `/update-docs` skill that drafts doc updates. Use them before writing from scratch and follow the style guide in [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). + +To build and preview docs locally: + +```bash +make docs # build the docs +make docs-live # serve locally with auto-rebuild +``` + +See [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md) for the full style guide and writing conventions. + +## Pull Requests + +We welcome contributions. Every PR requires maintainer review. To keep the review queue healthy, limit the number of open PRs you have at any time to fewer than 10. + +> [!WARNING] +> Accounts that repeatedly exceed this limit or submit automated bulk PRs may have their PRs closed or their access restricted. + +Follow these steps to submit a pull request. + +1. Create a feature branch from `main`. +2. Make your changes with tests. +3. Run `make check` and `npm test` to verify. +4. Open a PR. + +### Commit Messages + +This project uses [Conventional Commits](https://www.conventionalcommits.org/). All commit messages must follow the format: + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +**Types:** + +- `feat` - New feature +- `fix` - Bug fix +- `docs` - Documentation only +- `chore` - Maintenance tasks (dependencies, build config) +- `refactor` - Code change that neither fixes a bug nor adds a feature +- `test` - Adding or updating tests +- `ci` - CI/CD changes +- `perf` - Performance improvements + +**Examples:** + +``` +feat(cli): add --profile flag to nemoclaw onboard +fix(blueprint): handle missing API key gracefully +docs: update quickstart for new install wizard +chore(deps): bump commander to 13.2 +``` diff --git a/ISSUE_TEMPLATE/bug_report.yml b/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 00000000000..329242d6a71 --- /dev/null +++ b/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,18 @@ +name: Bug Report ++description: Report a bug to help us improve NemoClaw +labels: ["status: triage"] +body: + - type: textarea + id: repro + attributes: + label: Steps to Reproduce + placeholder: 1. Run 'nemo'... 2. See error... + validations: + required: true + - type: textarea + id: logs + attributes: + label: Error Logs / Traceback + render: shell + validations: + required: true diff --git a/Makefile b/Makefile index 50e5a96326e..7eaf3c64a56 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: check lint format lint-ts lint-py format-ts format-py docs docs-live docs-clean +.PHONY: check lint format lint-ts lint-py format-ts format-py docs docs-strict docs-live docs-clean check: lint-ts lint-py @echo "All checks passed." @@ -24,6 +24,9 @@ format-py: docs: uv run --group docs sphinx-build -b html docs docs/_build/html +docs-strict: + uv run --group docs sphinx-build -W -b html docs docs/_build/html + docs-live: uv run --group docs sphinx-autobuild docs docs/_build/html --open-browser diff --git a/README.md b/README.md index 2bf5d8dc9da..fb905acba08 100644 --- a/README.md +++ b/README.md @@ -6,48 +6,76 @@ [![Project Status](https://img.shields.io/badge/status-alpha-orange)](https://github.com/NVIDIA/NemoClaw/blob/main/docs/about/release-notes.md) -NVIDIA NemoClaw is an open source stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants safely. It installs the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of [NVIDIA Agent Toolkit](https://docs.nvidia.com/nemo/agent-toolkit/latest), a secure environment for running autonomous agents, with inference routed through [NVIDIA cloud](https://build.nvidia.com). +NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants safely. It installs the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of NVIDIA Agent Toolkit, a secure environment for running autonomous agents, and open source models such as [NVIDIA Nemotron](https://build.nvidia.com). > **Alpha software** > -> NemoClaw is early-stage. Expect rough edges. We are building toward production-ready sandbox orchestration, but the starting point is getting your own environment up and running. +> NemoClaw is available in early preview starting March 16, 2026. > Interfaces, APIs, and behavior may change without notice as we iterate on the design. -> The project is shared to gather feedback and enable early experimentation, but it -> should not yet be considered production-ready. +> The project is shared to gather feedback and enable early experimentation. > We welcome issues and discussion from the community while the project evolves. --- ## Quick Start - - Follow these steps to get started with NemoClaw and your first sandboxed OpenClaw agent. -:::{note} -NemoClaw currently requires a fresh installation of OpenClaw. -::: +> **ℹ️ Note** +> +> NemoClaw creates a fresh OpenClaw instance inside the sandbox during onboarding. + + ### Prerequisites Check the prerequisites before you start to ensure you have the necessary software and hardware to run NemoClaw. +#### Hardware + +| Resource | Minimum | Recommended | +|----------|----------------|------------------| +| CPU | 4 vCPU | 4+ vCPU | +| RAM | 8 GB | 16 GB | +| Disk | 20 GB free | 40 GB free | + +The sandbox image is approximately 2.4 GB compressed. During image push, the Docker daemon, k3s, and the OpenShell gateway run alongside the export pipeline, which buffers decompressed layers in memory. On machines with less than 8 GB of RAM, this combined usage can trigger the OOM killer. If you cannot add memory, configuring at least 8 GB of swap can work around the issue at the cost of slower performance. + #### Software -- Linux Ubuntu 22.04 LTS releases and later -- Node.js 20+ and npm 10+ (the installer recommends Node.js 22) -- Docker installed and running -- [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) installed +| Dependency | Version | +|------------|----------------------------------| +| Linux | Ubuntu 22.04 LTS or later | +| Node.js | 20 or later | +| npm | 10 or later | +| Container runtime | Supported runtime installed and running | +| [OpenShell](https://github.com/NVIDIA/OpenShell) | Installed | + +#### Container Runtime Support + +| Platform | Supported runtimes | Notes | +|----------|--------------------|-------| +| Linux | Docker | Primary supported path today | +| macOS (Apple Silicon) | Colima, Docker Desktop | Recommended runtimes for supported macOS setups | +| macOS | Podman | Not supported yet. NemoClaw currently depends on OpenShell support for Podman on macOS. | +| Windows WSL | Docker Desktop (WSL backend) | Supported target path | + +> **💡 Tip** +> +> For DGX Spark, follow the [DGX Spark setup guide](https://github.com/NVIDIA/NemoClaw/blob/main/spark-install.md). It covers Spark-specific prerequisites, such as cgroup v2 and Docker configuration, before running the standard installer. ### Install NemoClaw and Onboard OpenClaw Agent Download and run the installer script. The script installs Node.js if it is not already present, then runs the guided onboard wizard to create a sandbox, configure inference, and apply security policies. -```console -$ curl -fsSL https://nvidia.com/nemoclaw.sh | bash +```bash +curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash ``` +If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH. +If `nemoclaw` is not found after install, run `source ~/.bashrc` (or `source ~/.zshrc` for zsh) or open a new terminal. + When the install completes, a summary confirms the running environment: ``` @@ -67,26 +95,60 @@ Logs: nemoclaw my-assistant logs --follow Connect to the sandbox, then chat with the agent through the TUI or the CLI. -```console -$ nemoclaw my-assistant connect +#### Connect to the Sandbox + +Run the following command to connect to the sandbox: + +```bash +nemoclaw my-assistant connect ``` +This connects you to the sandbox shell `sandbox@my-assistant:~$` where you can run `openclaw` commands. + #### OpenClaw TUI -The OpenClaw TUI opens an interactive chat interface. Type a message and press Enter to send it to the agent: +In the sandbox shell, run the following command to open the OpenClaw TUI, which opens an interactive chat interface. -```console -sandbox@my-assistant:~$ openclaw tui +```bash +openclaw tui ``` Send a test message to the agent and verify you receive a response. +> **ℹ️ Note** +> +> The TUI is best for interactive back-and-forth. If you need the full text of a long response such as a large code generation output, use the CLI instead. + #### OpenClaw CLI -Use the OpenClaw CLI to send a single message and print the response: +In the sandbox shell, run the following command to send a single message and print the response: + +```bash +openclaw agent --agent main --local -m "hello" --session-id test +``` + +This prints the complete response directly in the terminal and avoids relying on the TUI view for long output. + +### Uninstall + +To remove NemoClaw and all resources created during setup, in the terminal outside the sandbox, run: -```console -sandbox@my-assistant:~$ openclaw agent --agent main --local -m "hello" --session-id test +```bash +curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash +``` + +The script removes sandboxes, the NemoClaw gateway and providers, related Docker images and containers, local state directories, and the global `nemoclaw` npm package. It does not remove shared system tooling such as Docker, Node.js, npm, or Ollama. + +| Flag | Effect | +|--------------------|-----------------------------------------------------| +| `--yes` | Skip the confirmation prompt. | +| `--keep-openshell` | Leave the `openshell` binary installed. | +| `--delete-models` | Also remove NemoClaw-pulled Ollama models. | + +For example, to skip the confirmation prompt: + +```bash +curl -fsSL https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh | bash -s -- --yes ``` @@ -120,6 +182,8 @@ Inference requests from the agent never leave the sandbox directly. OpenShell in Get an API key from [build.nvidia.com](https://build.nvidia.com). The `nemoclaw onboard` command prompts for this key during setup. +Local inference options such as Ollama and vLLM are still experimental. On macOS, they also depend on OpenShell host-routing support in addition to the local service itself being reachable on the host. + --- ## Protection Layers @@ -146,7 +210,6 @@ Run these on the host to set up, connect to, and manage sandboxes. | Command | Description | |--------------------------------------|--------------------------------------------------------| | `nemoclaw onboard` | Interactive setup wizard: gateway, providers, sandbox. | -| `nemoclaw deploy ` | Deploy to a remote GPU instance through Brev. | | `nemoclaw connect` | Open an interactive shell inside the sandbox. | | `openshell term` | Launch the OpenShell TUI for monitoring and approvals. | | `nemoclaw start` / `stop` / `status` | Manage auxiliary services (Telegram bridge, tunnel). | @@ -173,12 +236,14 @@ See the full [CLI reference](https://docs.nvidia.com/nemoclaw/latest/reference/c Refer to the documentation for more information on NemoClaw. -- [Overview](https://docs.nvidia.com/nemoclaw/latest/about/overview.html): what NemoClaw does and how it fits together -- [How It Works](https://docs.nvidia.com/nemoclaw/latest/about/how-it-works.html): plugin, blueprint, and sandbox lifecycle -- [Architecture](https://docs.nvidia.com/nemoclaw/latest/reference/architecture.html): plugin structure, blueprint lifecycle, and sandbox environment -- [Inference Profiles](https://docs.nvidia.com/nemoclaw/latest/reference/inference-profiles.html): NVIDIA cloud inference configuration -- [Network Policies](https://docs.nvidia.com/nemoclaw/latest/reference/network-policies.html): egress control and policy customization -- [CLI Commands](https://docs.nvidia.com/nemoclaw/latest/reference/commands.html): full command reference +- [Overview](https://docs.nvidia.com/nemoclaw/latest/about/overview.html): Learn what NemoClaw does and how it fits together. +- [How It Works](https://docs.nvidia.com/nemoclaw/latest/about/how-it-works.html): Learn about the plugin, blueprint, and sandbox lifecycle. +- [Architecture](https://docs.nvidia.com/nemoclaw/latest/reference/architecture.html): Learn about the plugin structure, blueprint lifecycle, and sandbox environment. +- [Inference Profiles](https://docs.nvidia.com/nemoclaw/latest/reference/inference-profiles.html): Learn about the NVIDIA cloud inference configuration. +- [Network Policies](https://docs.nvidia.com/nemoclaw/latest/reference/network-policies.html): Learn about egress control and policy customization. +- [CLI Commands](https://docs.nvidia.com/nemoclaw/latest/reference/commands.html): Learn about the full command reference. +- [Troubleshooting](https://docs.nvidia.com/nemoclaw/latest/reference/troubleshooting.html): Troubleshoot common issues and resolution steps. +- [Discord](https://discord.gg/XFpfPv9Uvx): Join the community for questions and discussion. ## License diff --git a/bin/lib/backup.js b/bin/lib/backup.js new file mode 100644 index 00000000000..3d42a862f23 --- /dev/null +++ b/bin/lib/backup.js @@ -0,0 +1,168 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Sandbox backup and restore functionality + +const fs = require("fs"); +const path = require("path"); +const os = require("os"); +const { execSync } = require("child_process"); +const registry = require("./registry"); + +const BACKUP_DIR = path.join(process.env.HOME || "/tmp", ".nemoclaw", "backups"); + +function ensureBackupDir() { + fs.mkdirSync(BACKUP_DIR, { recursive: true, mode: 0o700 }); +} + +function listBackups() { + ensureBackupDir(); + const files = fs.readdirSync(BACKUP_DIR).filter(f => f.endsWith(".json")); + const backups = []; + + for (const file of files) { + try { + const content = JSON.parse(fs.readFileSync(path.join(BACKUP_DIR, file), "utf-8")); + backups.push({ + name: content.metadata.name, + createdAt: content.metadata.createdAt, + path: path.join(BACKUP_DIR, file), + size: fs.statSync(path.join(BACKUP_DIR, file)).size, + }); + } catch {} + } + + return backups.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt)); +} + +function exportSandbox(sandboxName, outputPath) { + const sandbox = registry.getSandbox(sandboxName); + if (!sandbox) { + console.error(` Sandbox not found: ${sandboxName}`); + return null; + } + + ensureBackupDir(); + + const timestamp = new Date().toISOString().replace(/[:.]/g, "-"); + const defaultName = `${sandboxName}-${timestamp}.json`; + const backupPath = outputPath || path.join(BACKUP_DIR, defaultName); + + let policyContent = ""; + try { + policyContent = execSync(`openshell policy get ${sandboxName} 2>/dev/null`, { + encoding: "utf-8", + timeout: 10000 + }); + } catch { + policyContent = ""; + } + + const backup = { + version: "1.0", + metadata: { + name: sandboxName, + createdAt: new Date().toISOString(), + nemoclawVersion: require("../package.json").version, + }, + sandbox: { + name: sandbox.name, + model: sandbox.model, + provider: sandbox.provider, + gpuEnabled: sandbox.gpuEnabled, + policies: sandbox.policies || [], + }, + policy: policyContent, + }; + + const dir = path.dirname(backupPath); + if (dir !== BACKUP_DIR) { + fs.mkdirSync(dir, { recursive: true, mode: 0o700 }); + } + + fs.writeFileSync(backupPath, JSON.stringify(backup, null, 2), { mode: 0o600 }); + console.log(` Exported sandbox '${sandboxName}' to: ${backupPath}`); + + return backupPath; +} + +function importSandbox(backupPath, newName) { + if (!fs.existsSync(backupPath)) { + console.error(` Backup file not found: ${backupPath}`); + return false; + } + + let backup; + try { + backup = JSON.parse(fs.readFileSync(backupPath, "utf-8")); + } catch { + console.error(` Invalid backup file: ${backupPath}`); + return false; + } + + if (!backup.version || !backup.sandbox) { + console.error(" Invalid backup format"); + return false; + } + + const sandboxName = newName || backup.sandbox.name; + + console.log(` Creating sandbox '${sandboxName}' from backup...`); + + try { + execSync(`openshell sandbox exists ${sandboxName} 2>/dev/null`, { + encoding: "utf-8", + stdio: ["pipe", "pipe", "pipe"] + }); + console.error(` Sandbox '${sandboxName}' already exists. Use a different name or delete it first.`); + return false; + } catch {} + + console.log(` Note: This only imports the registry config.`); + console.log(` You need to manually recreate the sandbox and apply policies.`); + + registry.registerSandbox({ + name: sandboxName, + model: backup.sandbox.model, + provider: backup.sandbox.provider, + gpuEnabled: backup.sandbox.gpuEnabled, + policies: backup.sandbox.policies || [], + }); + + if (backup.policy) { + const policyPath = path.join(os.tmpdir(), `nemoclaw-restore-${Date.now()}.yaml`); + fs.writeFileSync(policyPath, backup.policy); + try { + execSync(`openshell policy set --policy "${policyPath}" --wait ${sandboxName}`, { + encoding: "utf-8", + timeout: 30000, + }); + console.log(` Restored policy for '${sandboxName}'`); + } catch (err) { + console.warn(` Warning: Could not restore policy: ${err.message}`); + } finally { + fs.unlinkSync(policyPath); + } + } + + console.log(` Imported sandbox '${sandboxName}' from backup`); + return true; +} + +function deleteBackup(backupPath) { + if (!fs.existsSync(backupPath)) { + console.error(` Backup not found: ${backupPath}`); + return false; + } + fs.unlinkSync(backupPath); + console.log(` Deleted backup: ${backupPath}`); + return true; +} + +module.exports = { + BACKUP_DIR, + listBackups, + exportSandbox, + importSandbox, + deleteBackup, +}; diff --git a/bin/lib/credentials.js b/bin/lib/credentials.js index 1ac405bed33..b48c73c4ad2 100644 --- a/bin/lib/credentials.js +++ b/bin/lib/credentials.js @@ -36,6 +36,14 @@ function prompt(question) { const rl = readline.createInterface({ input: process.stdin, output: process.stderr }); rl.question(question, (answer) => { rl.close(); + if (!process.stdin.isTTY) { + if (typeof process.stdin.pause === "function") { + process.stdin.pause(); + } + if (typeof process.stdin.unref === "function") { + process.stdin.unref(); + } + } resolve(answer.trim()); }); }); diff --git a/bin/lib/inference-config.js b/bin/lib/inference-config.js new file mode 100644 index 00000000000..61fa90ea393 --- /dev/null +++ b/bin/lib/inference-config.js @@ -0,0 +1,75 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const INFERENCE_ROUTE_URL = "https://inference.local/v1"; +const DEFAULT_CLOUD_MODEL = "nvidia/nemotron-3-super-120b-a12b"; +const CLOUD_MODEL_OPTIONS = [ + { id: "nvidia/nemotron-3-super-120b-a12b", label: "Nemotron 3 Super 120B" }, + { id: "moonshotai/kimi-k2.5", label: "Kimi K2.5" }, + { id: "z-ai/glm5", label: "GLM-5" }, + { id: "minimaxai/minimax-m2.5", label: "MiniMax M2.5" }, + { id: "qwen/qwen3.5-397b-a17b", label: "Qwen3.5 397B A17B" }, + { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" }, +]; +const DEFAULT_ROUTE_PROFILE = "inference-local"; +const DEFAULT_ROUTE_CREDENTIAL_ENV = "OPENAI_API_KEY"; +const MANAGED_PROVIDER_ID = "inference"; +const { DEFAULT_OLLAMA_MODEL } = require("./local-inference"); + +function getProviderSelectionConfig(provider, model) { + switch (provider) { + case "nvidia-nim": + return { + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: model || DEFAULT_CLOUD_MODEL, + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, + provider, + providerLabel: "NVIDIA Cloud API", + }; + case "vllm-local": + return { + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: model || "vllm-local", + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, + provider, + providerLabel: "Local vLLM", + }; + case "ollama-local": + return { + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: model || DEFAULT_OLLAMA_MODEL, + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, + provider, + providerLabel: "Local Ollama", + }; + default: + return null; + } +} + +function getOpenClawPrimaryModel(provider, model) { + const resolvedModel = + model || (provider === "ollama-local" ? DEFAULT_OLLAMA_MODEL : DEFAULT_CLOUD_MODEL); + return resolvedModel ? `${MANAGED_PROVIDER_ID}/${resolvedModel}` : null; +} + +module.exports = { + CLOUD_MODEL_OPTIONS, + DEFAULT_CLOUD_MODEL, + DEFAULT_OLLAMA_MODEL, + DEFAULT_ROUTE_CREDENTIAL_ENV, + DEFAULT_ROUTE_PROFILE, + INFERENCE_ROUTE_URL, + MANAGED_PROVIDER_ID, + getOpenClawPrimaryModel, + getProviderSelectionConfig, +}; diff --git a/bin/lib/local-inference.js b/bin/lib/local-inference.js new file mode 100644 index 00000000000..3892e969cea --- /dev/null +++ b/bin/lib/local-inference.js @@ -0,0 +1,179 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const HOST_GATEWAY_URL = "http://host.openshell.internal"; +const CONTAINER_REACHABILITY_IMAGE = "curlimages/curl:8.10.1"; +const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b"; + +function getLocalProviderBaseUrl(provider) { + switch (provider) { + case "vllm-local": + return `${HOST_GATEWAY_URL}:8000/v1`; + case "ollama-local": + return `${HOST_GATEWAY_URL}:11434/v1`; + default: + return null; + } +} + +function getLocalProviderHealthCheck(provider) { + switch (provider) { + case "vllm-local": + return "curl -sf http://localhost:8000/v1/models 2>/dev/null"; + case "ollama-local": + return "curl -sf http://localhost:11434/api/tags 2>/dev/null"; + default: + return null; + } +} + +function getLocalProviderContainerReachabilityCheck(provider) { + switch (provider) { + case "vllm-local": + return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:8000/v1/models 2>/dev/null`; + case "ollama-local": + return `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null`; + default: + return null; + } +} + +function validateLocalProvider(provider, runCapture) { + const command = getLocalProviderHealthCheck(provider); + if (!command) { + return { ok: true }; + } + + const output = runCapture(command, { ignoreError: true }); + if (!output) { + switch (provider) { + case "vllm-local": + return { + ok: false, + message: "Local vLLM was selected, but nothing is responding on http://localhost:8000.", + }; + case "ollama-local": + return { + ok: false, + message: "Local Ollama was selected, but nothing is responding on http://localhost:11434.", + }; + default: + return { ok: false, message: "The selected local inference provider is unavailable." }; + } + } + + const containerCommand = getLocalProviderContainerReachabilityCheck(provider); + if (!containerCommand) { + return { ok: true }; + } + + const containerOutput = runCapture(containerCommand, { ignoreError: true }); + if (containerOutput) { + return { ok: true }; + } + + switch (provider) { + case "vllm-local": + return { + ok: false, + message: + "Local vLLM is responding on localhost, but containers cannot reach http://host.openshell.internal:8000. Ensure the server is reachable from containers, not only from the host shell.", + }; + case "ollama-local": + return { + ok: false, + message: + "Local Ollama is responding on localhost, but containers cannot reach http://host.openshell.internal:11434. Ensure Ollama listens on 0.0.0.0:11434 instead of 127.0.0.1 so sandboxes can reach it.", + }; + default: + return { ok: false, message: "The selected local inference provider is unavailable from containers." }; + } +} + +function parseOllamaList(output) { + return String(output || "") + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !/^NAME\s+/i.test(line)) + .map((line) => line.split(/\s{2,}/)[0]) + .filter(Boolean); +} + +function getOllamaModelOptions(runCapture) { + const output = runCapture("ollama list 2>/dev/null", { ignoreError: true }); + const parsed = parseOllamaList(output); + if (parsed.length > 0) { + return parsed; + } + return [DEFAULT_OLLAMA_MODEL]; +} + +function getDefaultOllamaModel(runCapture) { + const models = getOllamaModelOptions(runCapture); + return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0]; +} + +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +function getOllamaWarmupCommand(model, keepAlive = "15m") { + const payload = JSON.stringify({ + model, + prompt: "hello", + stream: false, + keep_alive: keepAlive, + }); + return `nohup curl -s http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} >/dev/null 2>&1 &`; +} + +function getOllamaProbeCommand(model, timeoutSeconds = 120, keepAlive = "15m") { + const payload = JSON.stringify({ + model, + prompt: "hello", + stream: false, + keep_alive: keepAlive, + }); + return `curl -sS --max-time ${timeoutSeconds} http://localhost:11434/api/generate -H 'Content-Type: application/json' -d ${shellQuote(payload)} 2>/dev/null`; +} + +function validateOllamaModel(model, runCapture) { + const output = runCapture(getOllamaProbeCommand(model), { ignoreError: true }); + if (!output) { + return { + ok: false, + message: + `Selected Ollama model '${model}' did not answer the local probe in time. ` + + "It may still be loading, too large for the host, or otherwise unhealthy.", + }; + } + + try { + const parsed = JSON.parse(output); + if (parsed && typeof parsed.error === "string" && parsed.error.trim()) { + return { + ok: false, + message: `Selected Ollama model '${model}' failed the local probe: ${parsed.error.trim()}`, + }; + } + } catch {} + + return { ok: true }; +} + +module.exports = { + CONTAINER_REACHABILITY_IMAGE, + DEFAULT_OLLAMA_MODEL, + HOST_GATEWAY_URL, + getDefaultOllamaModel, + getLocalProviderBaseUrl, + getLocalProviderContainerReachabilityCheck, + getLocalProviderHealthCheck, + getOllamaModelOptions, + getOllamaProbeCommand, + getOllamaWarmupCommand, + parseOllamaList, + validateOllamaModel, + validateLocalProvider, +}; diff --git a/bin/lib/onboard.js b/bin/lib/onboard.js index cb3b804eaa6..060b5ba2120 100644 --- a/bin/lib/onboard.js +++ b/bin/lib/onboard.js @@ -2,26 +2,186 @@ // SPDX-License-Identifier: Apache-2.0 // // Interactive onboarding wizard — 7 steps from zero to running sandbox. +// Supports non-interactive mode via --non-interactive flag or +// NEMOCLAW_NON_INTERACTIVE=1 env var for CI/CD pipelines. const fs = require("fs"); const path = require("path"); const { ROOT, SCRIPTS, run, runCapture } = require("./runner"); +const { + getDefaultOllamaModel, + getLocalProviderBaseUrl, + getOllamaModelOptions, + getOllamaWarmupCommand, + validateOllamaModel, + validateLocalProvider, +} = require("./local-inference"); +const { + CLOUD_MODEL_OPTIONS, + DEFAULT_CLOUD_MODEL, + DEFAULT_OLLAMA_MODEL, + getOpenClawPrimaryModel, + getProviderSelectionConfig, +} = require("./inference-config"); +const { + inferContainerRuntime, + isUnsupportedMacosRuntime, + shouldPatchCoredns, +} = require("./platform"); const { prompt, ensureApiKey, getCredential } = require("./credentials"); const registry = require("./registry"); const nim = require("./nim"); const policies = require("./policies"); -const { checkCgroupConfig } = require("./preflight"); -const HOST_GATEWAY_URL = "http://host.openshell.internal"; +const { checkPortAvailable } = require("./preflight"); const EXPERIMENTAL = process.env.NEMOCLAW_EXPERIMENTAL === "1"; +// Non-interactive mode: set by --non-interactive flag or env var. +// When active, all prompts use env var overrides or sensible defaults. +let NON_INTERACTIVE = false; + +function isNonInteractive() { + return NON_INTERACTIVE; +} + +// Prompt wrapper: returns env var value or default in non-interactive mode, +// otherwise prompts the user interactively. +async function promptOrDefault(question, envVar, defaultValue) { + if (isNonInteractive()) { + const val = envVar ? process.env[envVar] : null; + const result = val || defaultValue; + console.log(` [non-interactive] ${question.trim()} → ${result}`); + return result; + } + return prompt(question); +} + // ── Helpers ────────────────────────────────────────────────────── +/** + * Check if a sandbox is in Ready state from `openshell sandbox list` output. + * Strips ANSI codes and exact-matches the sandbox name in the first column. + */ +function isSandboxReady(output, sandboxName) { + const clean = output.replace(/\x1b\[[0-9;]*m/g, ""); + return clean.split("\n").some((l) => { + const cols = l.trim().split(/\s+/); + return cols[0] === sandboxName && cols.includes("Ready") && !cols.includes("NotReady"); + }); +} + +/** + * Determine whether stale NemoClaw gateway output indicates a previous + * session that should be cleaned up before the port preflight check. + * @param {string} gwInfoOutput - Raw output from `openshell gateway info -g nemoclaw`. + * @returns {boolean} + */ +function hasStaleGateway(gwInfoOutput) { + return typeof gwInfoOutput === "string" && gwInfoOutput.length > 0 && gwInfoOutput.includes("nemoclaw"); +} + function step(n, total, msg) { console.log(""); console.log(` [${n}/${total}] ${msg}`); console.log(` ${"─".repeat(50)}`); } +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +function pythonLiteralJson(value) { + return JSON.stringify(JSON.stringify(value)); +} + +function buildSandboxConfigSyncScript(selectionConfig) { + const providerType = + selectionConfig.profile === "inference-local" + ? selectionConfig.model === DEFAULT_OLLAMA_MODEL + ? "ollama-local" + : "nvidia-nim" + : selectionConfig.endpointType === "vllm" + ? "vllm-local" + : "nvidia-nim"; + const primaryModel = getOpenClawPrimaryModel(providerType, selectionConfig.model); + const providerKey = "inference"; + const providerConfig = { + baseUrl: selectionConfig.endpointUrl, + apiKey: "unused", + api: "openai-completions", + models: [ + { + id: selectionConfig.model, + name: selectionConfig.model, + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 131072, + maxTokens: 4096, + }, + ], + }; + return ` +set -euo pipefail +mkdir -p ~/.nemoclaw ~/.openclaw +cat > ~/.nemoclaw/config.json <<'EOF_NEMOCLAW_CFG' +${JSON.stringify(selectionConfig, null, 2)} +EOF_NEMOCLAW_CFG +python3 - <<'PYCFG' +import json +import os + +cfg_path = os.path.expanduser('~/.openclaw/openclaw.json') +cfg = {} +if os.path.exists(cfg_path): + with open(cfg_path) as f: + cfg = json.load(f) + +cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = ${JSON.stringify(primaryModel)} +models_cfg = cfg.setdefault('models', {}) +models_cfg.setdefault('mode', 'merge') +providers_cfg = models_cfg.setdefault('providers', {}) +providers_cfg[${JSON.stringify(providerKey)}] = json.loads(${pythonLiteralJson(providerConfig)}) + +with open(cfg_path, 'w') as f: + json.dump(cfg, f, indent=2) + +os.chmod(cfg_path, 0o600) +PYCFG +openclaw models set ${shellQuote(primaryModel)} > /dev/null 2>&1 || true +exit +`.trim(); +} + +async function promptCloudModel() { + console.log(""); + console.log(" Cloud models:"); + CLOUD_MODEL_OPTIONS.forEach((option, index) => { + console.log(` ${index + 1}) ${option.label} (${option.id})`); + }); + console.log(""); + + const choice = await prompt(" Choose model [1]: "); + const index = parseInt(choice || "1", 10) - 1; + return (CLOUD_MODEL_OPTIONS[index] || CLOUD_MODEL_OPTIONS[0]).id; +} + +async function promptOllamaModel() { + const options = getOllamaModelOptions(runCapture); + const defaultModel = getDefaultOllamaModel(runCapture); + const defaultIndex = Math.max(0, options.indexOf(defaultModel)); + + console.log(""); + console.log(" Ollama models:"); + options.forEach((option, index) => { + console.log(` ${index + 1}) ${option}`); + }); + console.log(""); + + const choice = await prompt(` Choose model [${defaultIndex + 1}]: `); + const index = parseInt(choice || String(defaultIndex + 1), 10) - 1; + return options[index] || options[defaultIndex] || defaultModel; +} + function isDockerRunning() { try { runCapture("docker info", { ignoreError: false }); @@ -31,6 +191,11 @@ function isDockerRunning() { } } +function getContainerRuntime() { + const info = runCapture("docker info 2>/dev/null", { ignoreError: true }); + return inferContainerRuntime(info); +} + function isOpenshellInstalled() { try { runCapture("command -v openshell"); @@ -42,10 +207,63 @@ function isOpenshellInstalled() { function installOpenshell() { console.log(" Installing openshell CLI..."); - run(`bash "${path.join(SCRIPTS, "install.sh")}"`, { ignoreError: true }); + run(`bash "${path.join(SCRIPTS, "install-openshell.sh")}"`, { ignoreError: true }); + const localBin = process.env.XDG_BIN_HOME || path.join(process.env.HOME || "", ".local", "bin"); + if (fs.existsSync(path.join(localBin, "openshell")) && !process.env.PATH.split(path.delimiter).includes(localBin)) { + process.env.PATH = `${localBin}${path.delimiter}${process.env.PATH}`; + } return isOpenshellInstalled(); } +function sleep(seconds) { + require("child_process").spawnSync("sleep", [String(seconds)]); +} + +function waitForSandboxReady(sandboxName, attempts = 10, delaySeconds = 2) { + for (let i = 0; i < attempts; i += 1) { + const exists = runCapture(`openshell sandbox get "${sandboxName}" 2>/dev/null`, { ignoreError: true }); + if (exists) return true; + sleep(delaySeconds); + } + return false; +} + +function parsePolicyPresetEnv(value) { + return (value || "") + .split(",") + .map((s) => s.trim()) + .filter(Boolean); +} + +function isSafeModelId(value) { + return /^[A-Za-z0-9._:/-]+$/.test(value); +} + +function getNonInteractiveProvider() { + const providerKey = (process.env.NEMOCLAW_PROVIDER || "").trim().toLowerCase(); + if (!providerKey) return null; + + const validProviders = new Set(["cloud", "ollama", "vllm", "nim"]); + if (!validProviders.has(providerKey)) { + console.error(` Unsupported NEMOCLAW_PROVIDER: ${providerKey}`); + console.error(" Valid values: cloud, ollama, vllm, nim"); + process.exit(1); + } + + return providerKey; +} + +function getNonInteractiveModel(providerKey) { + const model = (process.env.NEMOCLAW_MODEL || "").trim(); + if (!model) return null; + if (!isSafeModelId(model)) { + console.error(` Invalid NEMOCLAW_MODEL for provider '${providerKey}': ${model}`); + console.error(" Model values may only contain letters, numbers, '.', '_', ':', '/', and '-'."); + process.exit(1); + } + return model; +} + // ── Step 1: Preflight ──────────────────────────────────────────── async function preflight() { @@ -58,6 +276,17 @@ async function preflight() { } console.log(" ✓ Docker is running"); + const runtime = getContainerRuntime(); + if (isUnsupportedMacosRuntime(runtime)) { + console.error(" Podman on macOS is not supported by NemoClaw at this time."); + console.error(" OpenShell currently depends on Docker host-gateway behavior that Podman on macOS does not provide."); + console.error(" Use Colima or Docker Desktop on macOS instead."); + process.exit(1); + } + if (runtime !== "unknown") { + console.log(` ✓ Container runtime: ${runtime}`); + } + // OpenShell CLI if (!isOpenshellInstalled()) { console.log(" openshell CLI not found. Attempting to install..."); @@ -69,26 +298,52 @@ async function preflight() { } console.log(` ✓ openshell CLI: ${runCapture("openshell --version 2>/dev/null || echo unknown", { ignoreError: true })}`); - // cgroup v2 + Docker cgroupns - const cgroup = checkCgroupConfig(); - if (!cgroup.ok) { - console.error(""); - console.error(" !! cgroup v2 detected but Docker is not configured for cgroupns=host."); - console.error(" OpenShell's gateway runs k3s inside Docker, which will fail with:"); - console.error(""); - console.error(" openat2 /sys/fs/cgroup/kubepods/pids.max: no such file or directory"); - console.error(""); - console.error(" To fix, run:"); - console.error(""); - console.error(" nemoclaw setup-spark"); - console.error(""); - console.error(" This adds \"default-cgroupns-mode\": \"host\" to /etc/docker/daemon.json"); - console.error(" (preserving any existing settings) and restarts Docker."); - console.error(""); - console.error(` Detail: ${cgroup.reason}`); - process.exit(1); + // Clean up stale NemoClaw session before checking ports. + // A previous onboard run may have left the gateway container and port + // forward running. If a NemoClaw-owned gateway is still present, tear + // it down so the port check below doesn't fail on our own leftovers. + const gwInfo = runCapture("openshell gateway info -g nemoclaw 2>/dev/null", { ignoreError: true }); + if (hasStaleGateway(gwInfo)) { + console.log(" Cleaning up previous NemoClaw session..."); + run("openshell forward stop 18789 2>/dev/null || true", { ignoreError: true }); + run("openshell gateway destroy -g nemoclaw 2>/dev/null || true", { ignoreError: true }); + console.log(" ✓ Previous session cleaned up"); + } + + // Required ports — gateway (8080) and dashboard (18789) + const requiredPorts = [ + { port: 8080, label: "OpenShell gateway" }, + { port: 18789, label: "NemoClaw dashboard" }, + ]; + for (const { port, label } of requiredPorts) { + const portCheck = await checkPortAvailable(port); + if (!portCheck.ok) { + console.error(""); + console.error(` !! Port ${port} is not available.`); + console.error(` ${label} needs this port.`); + console.error(""); + if (portCheck.process && portCheck.process !== "unknown") { + console.error(` Blocked by: ${portCheck.process}${portCheck.pid ? ` (PID ${portCheck.pid})` : ""}`); + console.error(""); + console.error(" To fix, stop the conflicting process:"); + console.error(""); + if (portCheck.pid) { + console.error(` sudo kill ${portCheck.pid}`); + } else { + console.error(` lsof -i :${port} -sTCP:LISTEN -P -n`); + } + console.error(" # or, if it's a systemd service:"); + console.error(" systemctl --user stop openclaw-gateway.service"); + } else { + console.error(` Could not identify the process using port ${port}.`); + console.error(` Run: lsof -i :${port} -sTCP:LISTEN`); + } + console.error(""); + console.error(` Detail: ${portCheck.reason}`); + process.exit(1); + } + console.log(` ✓ Port ${port} available (${label})`); } - console.log(" ✓ cgroup configuration OK"); // GPU const gpu = nim.detectGpu(); @@ -113,7 +368,11 @@ async function startGateway(gpu) { run("openshell gateway destroy -g nemoclaw 2>/dev/null || true", { ignoreError: true }); const gwArgs = ["--name", "nemoclaw"]; - if (gpu && gpu.nimCapable) gwArgs.push("--gpu"); + // Do NOT pass --gpu here. On DGX Spark (and most GPU hosts), inference is + // routed through a host-side provider (Ollama, vLLM, or cloud API) — the + // sandbox itself does not need direct GPU access. Passing --gpu causes + // FailedPrecondition errors when the gateway's k3s device plugin cannot + // allocate GPUs. See: https://build.nvidia.com/spark/nemoclaw/instructions run(`openshell gateway start ${gwArgs.join(" ")}`, { ignoreError: false }); @@ -128,21 +387,17 @@ async function startGateway(gpu) { console.error(" Gateway failed to start. Run: openshell gateway info"); process.exit(1); } - require("child_process").spawnSync("sleep", ["2"]); + sleep(2); } // CoreDNS fix — always run. k3s-inside-Docker has broken DNS on all platforms. - const home = process.env.HOME || "/tmp"; - const colimaSocket = [ - path.join(home, ".colima/default/docker.sock"), - path.join(home, ".config/colima/default/docker.sock"), - ].find((s) => fs.existsSync(s)); - if (colimaSocket) { + const runtime = getContainerRuntime(); + if (shouldPatchCoredns(runtime)) { console.log(" Patching CoreDNS for Colima..."); - run(`bash "${path.join(SCRIPTS, "fix-coredns.sh")}" 2>&1 || true`, { ignoreError: true }); + run(`bash "${path.join(SCRIPTS, "fix-coredns.sh")}" nemoclaw 2>&1 || true`, { ignoreError: true }); } // Give DNS a moment to propagate - require("child_process").spawnSync("sleep", ["5"]); + sleep(5); } // ── Step 3: Sandbox ────────────────────────────────────────────── @@ -150,19 +405,40 @@ async function startGateway(gpu) { async function createSandbox(gpu) { step(3, 7, "Creating sandbox"); - const nameAnswer = await prompt(" Sandbox name [my-assistant]: "); - const sandboxName = nameAnswer || "my-assistant"; + const nameAnswer = await promptOrDefault( + " Sandbox name (lowercase, numbers, hyphens) [my-assistant]: ", + "NEMOCLAW_SANDBOX_NAME", "my-assistant" + ); + const sandboxName = (nameAnswer || "my-assistant").trim().toLowerCase(); + + // Validate: RFC 1123 subdomain — lowercase alphanumeric and hyphens, + // must start and end with alphanumeric (required by Kubernetes/OpenShell) + if (!/^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName)) { + console.error(` Invalid sandbox name: '${sandboxName}'`); + console.error(" Names must be lowercase, contain only letters, numbers, and hyphens,"); + console.error(" and must start and end with a letter or number."); + process.exit(1); + } // Check if sandbox already exists in registry const existing = registry.getSandbox(sandboxName); if (existing) { - const recreate = await prompt(` Sandbox '${sandboxName}' already exists. Recreate? [y/N]: `); - if (recreate.toLowerCase() !== "y") { - console.log(" Keeping existing sandbox."); - return sandboxName; + if (isNonInteractive()) { + if (process.env.NEMOCLAW_RECREATE_SANDBOX !== "1") { + console.error(` Sandbox '${sandboxName}' already exists.`); + console.error(" Set NEMOCLAW_RECREATE_SANDBOX=1 to recreate it in non-interactive mode."); + process.exit(1); + } + console.log(` [non-interactive] Sandbox '${sandboxName}' exists — recreating`); + } else { + const recreate = await prompt(` Sandbox '${sandboxName}' already exists. Recreate? [y/N]: `); + if (recreate.toLowerCase() !== "y") { + console.log(" Keeping existing sandbox."); + return sandboxName; + } } // Destroy old sandbox - run(`openshell sandbox delete ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); + run(`openshell sandbox delete "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); registry.removeSandbox(sandboxName); } @@ -181,10 +457,10 @@ async function createSandbox(gpu) { const basePolicyPath = path.join(ROOT, "nemoclaw-blueprint", "policies", "openclaw-sandbox.yaml"); const createArgs = [ `--from "${buildCtx}/Dockerfile"`, - `--name ${sandboxName}`, + `--name "${sandboxName}"`, `--policy "${basePolicyPath}"`, ]; - if (gpu && gpu.nimCapable) createArgs.push("--gpu"); + // --gpu is intentionally omitted. See comment in startGateway(). console.log(` Creating sandbox '${sandboxName}' (this takes a few minutes on first run)...`); const chatUiUrl = process.env.CHAT_UI_URL || 'http://127.0.0.1:18789'; @@ -192,15 +468,66 @@ async function createSandbox(gpu) { if (process.env.NVIDIA_API_KEY) { envArgs.push(`NVIDIA_API_KEY=${process.env.NVIDIA_API_KEY}`); } - run(`openshell sandbox create ${createArgs.join(" ")} -- env ${envArgs.join(" ")} nemoclaw-start 2>&1 | awk '/Sandbox allocated/{if(!seen){print;seen=1}next}1'`); - // Forward dashboard port separately - run(`openshell forward start --background 18789 ${sandboxName}`, { ignoreError: true }); + // Run without piping through awk — the pipe masked non-zero exit codes + // from openshell because bash returns the status of the last pipeline + // command (awk, always 0) unless pipefail is set. Removing the pipe + // lets the real exit code flow through to run(). + const createResult = run( + `openshell sandbox create ${createArgs.join(" ")} -- env ${envArgs.join(" ")} nemoclaw-start 2>&1`, + { ignoreError: true } + ); - // Clean up build context + // Clean up build context regardless of outcome run(`rm -rf "${buildCtx}"`, { ignoreError: true }); - // Register in registry + if (createResult.status !== 0) { + console.error(""); + console.error(` Sandbox creation failed (exit ${createResult.status}).`); + console.error(" Try: openshell sandbox list # check gateway state"); + console.error(" Try: nemoclaw onboard # retry from scratch"); + process.exit(createResult.status || 1); + } + + // Wait for sandbox to reach Ready state in k3s before registering. + // On WSL2 + Docker Desktop the pod can take longer to initialize; + // without this gate, NemoClaw registers a phantom sandbox that + // causes "sandbox not found" on every subsequent connect/status call. + console.log(" Waiting for sandbox to become ready..."); + let ready = false; + for (let i = 0; i < 30; i++) { + const list = runCapture("openshell sandbox list 2>&1", { ignoreError: true }); + if (isSandboxReady(list, sandboxName)) { + ready = true; + break; + } + require("child_process").spawnSync("sleep", ["2"]); + } + + if (!ready) { + // Clean up the orphaned sandbox so the next onboard retry with the same + // name doesn't fail on "sandbox already exists". + const delResult = run(`openshell sandbox delete "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); + console.error(""); + console.error(` Sandbox '${sandboxName}' was created but did not become ready within 60s.`); + if (delResult.status === 0) { + console.error(" The orphaned sandbox has been removed — you can safely retry."); + } else { + console.error(` Could not remove the orphaned sandbox. Manual cleanup:`); + console.error(` openshell sandbox delete "${sandboxName}"`); + } + console.error(" Retry: nemoclaw onboard"); + process.exit(1); + } + + // Release any stale forward on port 18789 before claiming it for the new sandbox. + // A previous onboard run may have left the port forwarded to a different sandbox, + // which would silently prevent the new sandbox's dashboard from being reachable. + run(`openshell forward stop 18789 2>/dev/null || true`, { ignoreError: true }); + // Forward dashboard port to the new sandbox + run(`openshell forward start --background 18789 "${sandboxName}"`, { ignoreError: true }); + + // Register only after confirmed ready — prevents phantom entries registry.registerSandbox({ name: sandboxName, gpuEnabled: !!gpu, @@ -223,55 +550,72 @@ async function setupNim(sandboxName, gpu) { const hasOllama = !!runCapture("command -v ollama", { ignoreError: true }); const ollamaRunning = !!runCapture("curl -sf http://localhost:11434/api/tags 2>/dev/null", { ignoreError: true }); const vllmRunning = !!runCapture("curl -sf http://localhost:8000/v1/models 2>/dev/null", { ignoreError: true }); - - // Auto-select only with NEMOCLAW_EXPERIMENTAL=1 (prevents silent misconfiguration) - if (EXPERIMENTAL) { - if (vllmRunning) { - console.log(" ✓ vLLM detected on localhost:8000 — using it [experimental]"); - provider = "vllm-local"; - model = "vllm-local"; - registry.updateSandbox(sandboxName, { model, provider, nimContainer }); - return { model, provider }; - } - if (ollamaRunning) { - console.log(" ✓ Ollama detected on localhost:11434 — using it [experimental]"); - provider = "ollama-local"; - model = "nemotron-3-nano"; - registry.updateSandbox(sandboxName, { model, provider, nimContainer }); - return { model, provider }; - } - } - - // Build options list — always show local options but label as experimental + const requestedProvider = isNonInteractive() ? getNonInteractiveProvider() : null; + const requestedModel = isNonInteractive() ? getNonInteractiveModel(requestedProvider || "cloud") : null; + // Build options list — only show local options with NEMOCLAW_EXPERIMENTAL=1 const options = []; - if (gpu && gpu.nimCapable) { + if (EXPERIMENTAL && gpu && gpu.nimCapable) { options.push({ key: "nim", label: "Local NIM container (NVIDIA GPU) [experimental]" }); } - options.push({ key: "cloud", label: "NVIDIA Cloud API (build.nvidia.com)" }); + options.push({ + key: "cloud", + label: + "NVIDIA Cloud API (build.nvidia.com)" + + (!ollamaRunning && !(EXPERIMENTAL && vllmRunning) ? " (recommended)" : ""), + }); if (hasOllama || ollamaRunning) { - options.push({ key: "ollama", label: `Local Ollama (localhost:11434)${ollamaRunning ? " — running" : ""} [experimental]` }); + options.push({ + key: "ollama", + label: + `Local Ollama (localhost:11434)${ollamaRunning ? " — running" : ""}` + + (ollamaRunning ? " (suggested)" : ""), + }); } - if (vllmRunning) { - options.push({ key: "vllm", label: "Existing vLLM instance (localhost:8000) — running [experimental]" }); + if (EXPERIMENTAL && vllmRunning) { + options.push({ + key: "vllm", + label: "Existing vLLM instance (localhost:8000) — running [experimental] (suggested)", + }); } // On macOS without Ollama, offer to install it if (!hasOllama && process.platform === "darwin") { - options.push({ key: "install-ollama", label: "Install Ollama (macOS) [experimental]" }); + options.push({ key: "install-ollama", label: "Install Ollama (macOS)" }); } if (options.length > 1) { - console.log(""); - console.log(" Inference options:"); - options.forEach((o, i) => { - console.log(` ${i + 1}) ${o.label}`); - }); - console.log(""); + let selected; + + if (isNonInteractive()) { + const providerKey = requestedProvider || "cloud"; + selected = options.find((o) => o.key === providerKey); + if (!selected) { + console.error(` Requested provider '${providerKey}' is not available in this environment.`); + process.exit(1); + } + console.log(` [non-interactive] Provider: ${selected.key}`); + } else { + const suggestions = []; + if (vllmRunning) suggestions.push("vLLM"); + if (ollamaRunning) suggestions.push("Ollama"); + if (suggestions.length > 0) { + console.log(` Detected local inference option${suggestions.length > 1 ? "s" : ""}: ${suggestions.join(", ")}`); + console.log(" Select one explicitly to use it. Press Enter to keep the cloud default."); + console.log(""); + } - const defaultIdx = options.findIndex((o) => o.key === "cloud") + 1; - const choice = await prompt(` Choose [${defaultIdx}]: `); - const idx = parseInt(choice || String(defaultIdx), 10) - 1; - const selected = options[idx] || options[defaultIdx - 1]; + console.log(""); + console.log(" Inference options:"); + options.forEach((o, i) => { + console.log(` ${i + 1}) ${o.label}`); + }); + console.log(""); + + const defaultIdx = options.findIndex((o) => o.key === "cloud") + 1; + const choice = await prompt(` Choose [${defaultIdx}]: `); + const idx = parseInt(choice || String(defaultIdx), 10) - 1; + selected = options[idx] || options[defaultIdx - 1]; + } if (selected.key === "nim") { // List models that fit GPU VRAM @@ -279,16 +623,30 @@ async function setupNim(sandboxName, gpu) { if (models.length === 0) { console.log(" No NIM models fit your GPU VRAM. Falling back to cloud API."); } else { - console.log(""); - console.log(" Models that fit your GPU:"); - models.forEach((m, i) => { - console.log(` ${i + 1}) ${m.name} (min ${m.minGpuMemoryMB} MB)`); - }); - console.log(""); - - const modelChoice = await prompt(` Choose model [1]: `); - const midx = parseInt(modelChoice || "1", 10) - 1; - const sel = models[midx] || models[0]; + let sel; + if (isNonInteractive()) { + if (requestedModel) { + sel = models.find((m) => m.name === requestedModel); + if (!sel) { + console.error(` Unsupported NEMOCLAW_MODEL for NIM: ${requestedModel}`); + process.exit(1); + } + } else { + sel = models[0]; + } + console.log(` [non-interactive] NIM model: ${sel.name}`); + } else { + console.log(""); + console.log(" Models that fit your GPU:"); + models.forEach((m, i) => { + console.log(` ${i + 1}) ${m.name} (min ${m.minGpuMemoryMB} MB)`); + }); + console.log(""); + + const modelChoice = await prompt(` Choose model [1]: `); + const midx = parseInt(modelChoice || "1", 10) - 1; + sel = models[midx] || models[0]; + } model = sel.name; console.log(` Pulling NIM image for ${model}...`); @@ -310,20 +668,28 @@ async function setupNim(sandboxName, gpu) { if (!ollamaRunning) { console.log(" Starting Ollama..."); run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); - require("child_process").spawnSync("sleep", ["2"]); + sleep(2); } console.log(" ✓ Using Ollama on localhost:11434"); provider = "ollama-local"; - model = "nemotron-3-nano"; + if (isNonInteractive()) { + model = requestedModel || getDefaultOllamaModel(runCapture); + } else { + model = await promptOllamaModel(); + } } else if (selected.key === "install-ollama") { console.log(" Installing Ollama via Homebrew..."); run("brew install ollama", { ignoreError: true }); console.log(" Starting Ollama..."); run("OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 &", { ignoreError: true }); - require("child_process").spawnSync("sleep", ["2"]); + sleep(2); console.log(" ✓ Using Ollama on localhost:11434"); provider = "ollama-local"; - model = "nemotron-3-nano"; + if (isNonInteractive()) { + model = requestedModel || getDefaultOllamaModel(runCapture); + } else { + model = await promptOllamaModel(); + } } else if (selected.key === "vllm") { console.log(" ✓ Using existing vLLM on localhost:8000"); provider = "vllm-local"; @@ -333,8 +699,18 @@ async function setupNim(sandboxName, gpu) { } if (provider === "nvidia-nim") { - await ensureApiKey(); - model = model || "nvidia/nemotron-3-super-120b-a12b"; + if (isNonInteractive()) { + // In non-interactive mode, NVIDIA_API_KEY must be set via env var + if (!process.env.NVIDIA_API_KEY) { + console.error(" NVIDIA_API_KEY is required for cloud provider in non-interactive mode."); + console.error(" Set it via: NVIDIA_API_KEY=nvapi-... nemoclaw onboard --non-interactive"); + process.exit(1); + } + } else { + await ensureApiKey(); + model = model || (await promptCloudModel()) || DEFAULT_CLOUD_MODEL; + } + model = model || requestedModel || DEFAULT_CLOUD_MODEL; console.log(` Using NVIDIA Cloud API with model: ${model}`); } @@ -361,12 +737,18 @@ async function setupInference(sandboxName, model, provider) { { ignoreError: true } ); } else if (provider === "vllm-local") { + const validation = validateLocalProvider(provider, runCapture); + if (!validation.ok) { + console.error(` ${validation.message}`); + process.exit(1); + } + const baseUrl = getLocalProviderBaseUrl(provider); run( `openshell provider create --name vllm-local --type openai ` + `--credential "OPENAI_API_KEY=dummy" ` + - `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:8000/v1" 2>&1 || ` + + `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` + `openshell provider update vllm-local --credential "OPENAI_API_KEY=dummy" ` + - `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:8000/v1" 2>&1 || true`, + `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`, { ignoreError: true } ); run( @@ -374,18 +756,32 @@ async function setupInference(sandboxName, model, provider) { { ignoreError: true } ); } else if (provider === "ollama-local") { + const validation = validateLocalProvider(provider, runCapture); + if (!validation.ok) { + console.error(` ${validation.message}`); + console.error(" On macOS, local inference also depends on OpenShell host routing support."); + process.exit(1); + } + const baseUrl = getLocalProviderBaseUrl(provider); run( `openshell provider create --name ollama-local --type openai ` + `--credential "OPENAI_API_KEY=ollama" ` + - `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:11434/v1" 2>&1 || ` + + `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || ` + `openshell provider update ollama-local --credential "OPENAI_API_KEY=ollama" ` + - `--config "OPENAI_BASE_URL=${HOST_GATEWAY_URL}:11434/v1" 2>&1 || true`, + `--config "OPENAI_BASE_URL=${baseUrl}" 2>&1 || true`, { ignoreError: true } ); run( `openshell inference set --no-verify --provider ollama-local --model ${model} 2>/dev/null || true`, { ignoreError: true } ); + console.log(` Priming Ollama model: ${model}`); + run(getOllamaWarmupCommand(model), { ignoreError: true }); + const probe = validateOllamaModel(model, runCapture); + if (!probe.ok) { + console.error(` ${probe.message}`); + process.exit(1); + } } registry.updateSandbox(sandboxName, { model, provider }); @@ -394,14 +790,21 @@ async function setupInference(sandboxName, model, provider) { // ── Step 6: OpenClaw ───────────────────────────────────────────── -async function setupOpenclaw(sandboxName) { +async function setupOpenclaw(sandboxName, model, provider) { step(6, 7, "Setting up OpenClaw inside sandbox"); - // sandbox create with a command runs it inside the sandbox then exits. - // Since the sandbox already exists, we create a throwaway connect + command - // by using sandbox create --no-keep with the same image to exec into it. - // Simpler: just use sandbox connect which opens a shell — but it doesn't - // support passing commands. So we run the setup on next connect instead. + const selectionConfig = getProviderSelectionConfig(provider, model); + if (selectionConfig) { + const sandboxConfig = { + ...selectionConfig, + onboardedAt: new Date().toISOString(), + }; + const script = buildSandboxConfigSyncScript(sandboxConfig); + run(`cat <<'EOF_NEMOCLAW_SYNC' | openshell sandbox connect "${sandboxName}" +${script} +EOF_NEMOCLAW_SYNC`, { stdio: ["ignore", "ignore", "inherit"] }); + } + console.log(" ✓ OpenClaw gateway launched inside sandbox"); } @@ -438,24 +841,78 @@ async function setupPolicies(sandboxName) { }); console.log(""); - const answer = await prompt(` Apply suggested presets (${suggestions.join(", ")})? [Y/n/list]: `); + if (isNonInteractive()) { + const policyMode = (process.env.NEMOCLAW_POLICY_MODE || "suggested").trim().toLowerCase(); + let selectedPresets = suggestions; - if (answer.toLowerCase() === "n") { - console.log(" Skipping policy presets."); - return; - } + if (policyMode === "skip" || policyMode === "none" || policyMode === "no") { + console.log(" [non-interactive] Skipping policy presets."); + return; + } - if (answer.toLowerCase() === "list") { - // Let user pick - const picks = await prompt(" Enter preset names (comma-separated): "); - const selected = picks.split(",").map((s) => s.trim()).filter(Boolean); - for (const name of selected) { - policies.applyPreset(sandboxName, name); + if (policyMode === "custom" || policyMode === "list") { + selectedPresets = parsePolicyPresetEnv(process.env.NEMOCLAW_POLICY_PRESETS); + if (selectedPresets.length === 0) { + console.error(" NEMOCLAW_POLICY_PRESETS is required when NEMOCLAW_POLICY_MODE=custom."); + process.exit(1); + } + } else if (policyMode === "suggested" || policyMode === "default" || policyMode === "auto") { + const envPresets = parsePolicyPresetEnv(process.env.NEMOCLAW_POLICY_PRESETS); + if (envPresets.length > 0) { + selectedPresets = envPresets; + } + } else { + console.error(` Unsupported NEMOCLAW_POLICY_MODE: ${policyMode}`); + console.error(" Valid values: suggested, custom, skip"); + process.exit(1); + } + + const knownPresets = new Set(allPresets.map((p) => p.name)); + const invalidPresets = selectedPresets.filter((name) => !knownPresets.has(name)); + if (invalidPresets.length > 0) { + console.error(` Unknown policy preset(s): ${invalidPresets.join(", ")}`); + process.exit(1); + } + + if (!waitForSandboxReady(sandboxName)) { + console.error(` Sandbox '${sandboxName}' was not ready for policy application.`); + process.exit(1); + } + console.log(` [non-interactive] Applying policy presets: ${selectedPresets.join(", ")}`); + for (const name of selectedPresets) { + for (let attempt = 0; attempt < 3; attempt += 1) { + try { + policies.applyPreset(sandboxName, name); + break; + } catch (err) { + const message = err && err.message ? err.message : String(err); + if (!message.includes("sandbox not found") || attempt === 2) { + throw err; + } + sleep(2); + } + } } } else { - // Apply suggested - for (const name of suggestions) { - policies.applyPreset(sandboxName, name); + const answer = await prompt(` Apply suggested presets (${suggestions.join(", ")})? [Y/n/list]: `); + + if (answer.toLowerCase() === "n") { + console.log(" Skipping policy presets."); + return; + } + + if (answer.toLowerCase() === "list") { + // Let user pick + const picks = await prompt(" Enter preset names (comma-separated): "); + const selected = picks.split(",").map((s) => s.trim()).filter(Boolean); + for (const name of selected) { + policies.applyPreset(sandboxName, name); + } + } else { + // Apply suggested + for (const name of suggestions) { + policies.applyPreset(sandboxName, name); + } } } @@ -471,6 +928,7 @@ function printDashboard(sandboxName, model, provider) { let providerLabel = provider; if (provider === "nvidia-nim") providerLabel = "NVIDIA Cloud API"; else if (provider === "vllm-local") providerLabel = "Local vLLM"; + else if (provider === "ollama-local") providerLabel = "Local Ollama"; console.log(""); console.log(` ${"─".repeat(50)}`); @@ -488,9 +946,12 @@ function printDashboard(sandboxName, model, provider) { // ── Main ───────────────────────────────────────────────────────── -async function onboard() { +async function onboard(opts = {}) { + NON_INTERACTIVE = opts.nonInteractive || process.env.NEMOCLAW_NON_INTERACTIVE === "1"; + console.log(""); console.log(" NemoClaw Onboarding"); + if (isNonInteractive()) console.log(" (non-interactive mode)"); console.log(" ==================="); const gpu = await preflight(); @@ -498,9 +959,9 @@ async function onboard() { const sandboxName = await createSandbox(gpu); const { model, provider } = await setupNim(sandboxName, gpu); await setupInference(sandboxName, model, provider); - await setupOpenclaw(sandboxName); + await setupOpenclaw(sandboxName, model, provider); await setupPolicies(sandboxName); printDashboard(sandboxName, model, provider); } -module.exports = { onboard }; +module.exports = { buildSandboxConfigSyncScript, hasStaleGateway, isSandboxReady, onboard, setupNim }; diff --git a/bin/lib/platform.js b/bin/lib/platform.js new file mode 100644 index 00000000000..67c31a3f3e1 --- /dev/null +++ b/bin/lib/platform.js @@ -0,0 +1,102 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const os = require("os"); +const path = require("path"); + +function isWsl(opts = {}) { + const platform = opts.platform ?? process.platform; + if (platform !== "linux") return false; + + const env = opts.env ?? process.env; + const release = opts.release ?? os.release(); + const procVersion = opts.procVersion ?? ""; + + return ( + Boolean(env.WSL_DISTRO_NAME) || + Boolean(env.WSL_INTEROP) || + /microsoft/i.test(release) || + /microsoft/i.test(procVersion) + ); +} + +function inferContainerRuntime(info = "") { + const normalized = String(info).toLowerCase(); + if (!normalized.trim()) return "unknown"; + if (normalized.includes("podman")) return "podman"; + if (normalized.includes("colima")) return "colima"; + if (normalized.includes("docker desktop")) return "docker-desktop"; + if (normalized.includes("docker")) return "docker"; + return "unknown"; +} + +function isUnsupportedMacosRuntime(runtime, opts = {}) { + const platform = opts.platform ?? process.platform; + return platform === "darwin" && runtime === "podman"; +} + +function shouldPatchCoredns(runtime) { + return runtime === "colima"; +} + +function getColimaDockerSocketCandidates(opts = {}) { + const home = opts.home ?? process.env.HOME ?? "/tmp"; + return [ + path.join(home, ".colima/default/docker.sock"), + path.join(home, ".config/colima/default/docker.sock"), + ]; +} + +function findColimaDockerSocket(opts = {}) { + const existsSync = opts.existsSync ?? require("fs").existsSync; + return getColimaDockerSocketCandidates(opts).find((socketPath) => existsSync(socketPath)) ?? null; +} + +function getDockerSocketCandidates(opts = {}) { + const home = opts.home ?? process.env.HOME ?? "/tmp"; + const platform = opts.platform ?? process.platform; + + if (platform === "darwin") { + return [ + ...getColimaDockerSocketCandidates({ home }), + path.join(home, ".docker/run/docker.sock"), + ]; + } + + return []; +} + +function detectDockerHost(opts = {}) { + const env = opts.env ?? process.env; + if (env.DOCKER_HOST) { + return { + dockerHost: env.DOCKER_HOST, + source: "env", + socketPath: null, + }; + } + + const existsSync = opts.existsSync ?? require("fs").existsSync; + for (const socketPath of getDockerSocketCandidates(opts)) { + if (existsSync(socketPath)) { + return { + dockerHost: `unix://${socketPath}`, + source: "socket", + socketPath, + }; + } + } + + return null; +} + +module.exports = { + detectDockerHost, + findColimaDockerSocket, + getColimaDockerSocketCandidates, + getDockerSocketCandidates, + inferContainerRuntime, + isUnsupportedMacosRuntime, + isWsl, + shouldPatchCoredns, +}; diff --git a/bin/lib/policies.js b/bin/lib/policies.js index 575fcdee27d..80034ee164d 100644 --- a/bin/lib/policies.js +++ b/bin/lib/policies.js @@ -69,7 +69,31 @@ function parseCurrentPolicy(raw) { return raw.slice(sep + 3).trim(); } +/** + * Build the openshell policy set command with properly quoted arguments. + */ +function buildPolicySetCommand(policyFile, sandboxName) { + return `openshell policy set --policy "${policyFile}" --wait "${sandboxName}"`; +} + +/** + * Build the openshell policy get command with properly quoted arguments. + */ +function buildPolicyGetCommand(sandboxName) { + return `openshell policy get --full "${sandboxName}" 2>/dev/null`; +} + function applyPreset(sandboxName, presetName) { + // Guard against truncated sandbox names — WSL can truncate hyphenated + // names during argument parsing, e.g. "my-assistant" → "m" + const isRfc1123Label = /^[a-z0-9]([a-z0-9-]*[a-z0-9])?$/.test(sandboxName); + if (!sandboxName || sandboxName.length > 63 || !isRfc1123Label) { + throw new Error( + `Invalid or truncated sandbox name: '${sandboxName}'. ` + + `Names must be 1-63 chars, lowercase alphanumeric, with optional internal hyphens.` + ); + } + const presetContent = loadPreset(presetName); if (!presetContent) { console.error(` Cannot load preset: ${presetName}`); @@ -86,12 +110,12 @@ function applyPreset(sandboxName, presetName) { let rawPolicy = ""; try { rawPolicy = runCapture( - `openshell policy get --full ${sandboxName} 2>/dev/null`, + buildPolicyGetCommand(sandboxName), { ignoreError: true } ); } catch {} - const currentPolicy = parseCurrentPolicy(rawPolicy); + let currentPolicy = parseCurrentPolicy(rawPolicy); // Merge: inject preset entries under the existing network_policies key let merged; @@ -146,7 +170,8 @@ function applyPreset(sandboxName, presetName) { fs.writeFileSync(tmpFile, merged, "utf-8"); try { - run(`openshell policy set --policy "${tmpFile}" --wait ${sandboxName}`); + run(buildPolicySetCommand(tmpFile, sandboxName)); + console.log(` Applied preset: ${presetName}`); } finally { fs.unlinkSync(tmpFile); @@ -175,6 +200,10 @@ module.exports = { listPresets, loadPreset, getPresetEndpoints, + extractPresetEntries, + parseCurrentPolicy, + buildPolicySetCommand, + buildPolicyGetCommand, applyPreset, getAppliedPresets, }; diff --git a/bin/lib/preflight.js b/bin/lib/preflight.js index dee1400277d..7f191413d48 100644 --- a/bin/lib/preflight.js +++ b/bin/lib/preflight.js @@ -3,78 +3,89 @@ // // Preflight checks for NemoClaw onboarding. -const fs = require("fs"); +const net = require("net"); const { runCapture } = require("./runner"); -const DEFAULT_DAEMON_JSON = "/etc/docker/daemon.json"; - -/** - * Detect if the host uses cgroup v2. - * - * Returns true when /sys/fs/cgroup is mounted as cgroup2fs. - * stat -fc is GNU coreutils (Linux). On macOS/BSD this command does not - * exist, so the platform guard ensures we never rely on command failure - * as the non-Linux fallback path. - * - * NOTE: Docker Desktop for Linux runs its own VM and manages its own - * daemon.json. This check targets native Docker Engine installs, which - * is what ships on DGX Spark and WSL2. - */ -function isCgroupV2() { - if (process.platform !== "linux") return false; - const fstype = runCapture("stat -fc %T /sys/fs/cgroup 2>/dev/null", { ignoreError: true }); - return fstype === "cgroup2fs"; -} - /** - * Read and parse /etc/docker/daemon.json. + * Check whether a TCP port is available for listening. * - * Returns the parsed object, or null if the file doesn't exist or isn't - * valid JSON. - */ -function readDaemonJson(daemonPath) { - const p = daemonPath || DEFAULT_DAEMON_JSON; - try { - const raw = fs.readFileSync(p, "utf-8"); - return JSON.parse(raw); - } catch { - return null; - } -} - -/** - * Check whether Docker is configured for cgroupns=host. - * - * On cgroup v2 systems, OpenShell's gateway starts k3s inside a Docker - * container. k3s needs the host cgroup namespace to manage cgroup - * hierarchies. Without "default-cgroupns-mode": "host" in daemon.json, - * kubelet fails with: + * Detection chain: + * 1. lsof (primary) — identifies the blocking process name + PID + * 2. Node.js net probe (fallback) — cross-platform, detects EADDRINUSE * - * openat2 /sys/fs/cgroup/kubepods/pids.max: no such file or directory + * opts.lsofOutput — inject fake lsof output for testing (skips shell) + * opts.skipLsof — force the net-probe fallback path * - * Returns an object: - * { ok: true } -- no issue (cgroup v1, or already configured) - * { ok: false, reason: string } -- needs fix + * Returns: + * { ok: true } + * { ok: false, process: string, pid: number|null, reason: string } */ -function checkCgroupConfig(opts) { - const cgroupV2 = opts && typeof opts.cgroupV2 === "boolean" ? opts.cgroupV2 : isCgroupV2(); - if (!cgroupV2) { - return { ok: true }; - } +async function checkPortAvailable(port, opts) { + const p = port || 18789; + const o = opts || {}; - const daemonPath = (opts && opts.daemonPath) || DEFAULT_DAEMON_JSON; - const config = readDaemonJson(daemonPath); + // ── lsof path ────────────────────────────────────────────────── + if (!o.skipLsof) { + let lsofOut; + if (typeof o.lsofOutput === "string") { + lsofOut = o.lsofOutput; + } else { + const hasLsof = runCapture("command -v lsof", { ignoreError: true }); + if (hasLsof) { + lsofOut = runCapture( + `lsof -i :${p} -sTCP:LISTEN -P -n 2>/dev/null`, + { ignoreError: true } + ); + } + } - if (config && config["default-cgroupns-mode"] === "host") { - return { ok: true }; + if (typeof lsofOut === "string") { + const lines = lsofOut.split("\n").filter((l) => l.trim()); + // Skip the header line (starts with COMMAND) + const dataLines = lines.filter((l) => !l.startsWith("COMMAND")); + if (dataLines.length > 0) { + // Parse first data line: COMMAND PID USER ... + const parts = dataLines[0].split(/\s+/); + const proc = parts[0] || "unknown"; + const pid = parseInt(parts[1], 10) || null; + return { + ok: false, + process: proc, + pid, + reason: `lsof reports ${proc} (PID ${pid}) listening on port ${p}`, + }; + } + // Empty lsof output is not authoritative — non-root users cannot + // see listeners owned by root (e.g., docker-proxy, leftover gateway). + // Fall through to the net probe which uses bind() at the kernel level. + } } - return { - ok: false, - reason: config - ? `${daemonPath} exists but "default-cgroupns-mode" is not set to "host"` - : `${daemonPath} does not exist or is not valid JSON`, - }; + // ── net probe fallback ───────────────────────────────────────── + return new Promise((resolve) => { + const srv = net.createServer(); + srv.once("error", (err) => { + if (err.code === "EADDRINUSE") { + resolve({ + ok: false, + process: "unknown", + pid: null, + reason: `port ${p} is in use (EADDRINUSE)`, + }); + } else { + // Unexpected error — treat port as unavailable + resolve({ + ok: false, + process: "unknown", + pid: null, + reason: `port probe failed: ${err.message}`, + }); + } + }); + srv.listen(p, "127.0.0.1", () => { + srv.close(() => resolve({ ok: true })); + }); + }); } -module.exports = { isCgroupV2, readDaemonJson, checkCgroupConfig }; +module.exports = { checkPortAvailable }; diff --git a/bin/lib/resolve-openshell.js b/bin/lib/resolve-openshell.js new file mode 100644 index 00000000000..6e89ee1f134 --- /dev/null +++ b/bin/lib/resolve-openshell.js @@ -0,0 +1,49 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { execSync } = require("child_process"); +const fs = require("fs"); + +/** + * Resolve the openshell binary path. + * + * Checks `command -v` first (must return an absolute path to prevent alias + * injection), then falls back to common installation directories. + * + * @param {object} [opts] DI overrides for testing + * @param {string|null} [opts.commandVResult] Mock result (undefined = run real command) + * @param {function} [opts.checkExecutable] (path) => boolean + * @param {string} [opts.home] HOME override + * @returns {string|null} Absolute path to openshell, or null if not found + */ +function resolveOpenshell(opts = {}) { + const home = opts.home ?? process.env.HOME; + + // Step 1: command -v + if (opts.commandVResult === undefined) { + try { + const found = execSync("command -v openshell", { encoding: "utf-8" }).trim(); + if (found.startsWith("/")) return found; + } catch {} + } else if (opts.commandVResult && opts.commandVResult.startsWith("/")) { + return opts.commandVResult; + } + + // Step 2: fallback candidates + const checkExecutable = opts.checkExecutable || ((p) => { + try { fs.accessSync(p, fs.constants.X_OK); return true; } catch { return false; } + }); + + const candidates = [ + ...(home && home.startsWith("/") ? [`${home}/.local/bin/openshell`] : []), + "/usr/local/bin/openshell", + "/usr/bin/openshell", + ]; + for (const p of candidates) { + if (checkExecutable(p)) return p; + } + + return null; +} + +module.exports = { resolveOpenshell }; diff --git a/bin/lib/runner.js b/bin/lib/runner.js index 3614dc80da2..53ec88996aa 100644 --- a/bin/lib/runner.js +++ b/bin/lib/runner.js @@ -3,29 +3,35 @@ const { execSync, spawnSync } = require("child_process"); const path = require("path"); -const fs = require("fs"); +const { detectDockerHost } = require("./platform"); const ROOT = path.resolve(__dirname, "..", ".."); const SCRIPTS = path.join(ROOT, "scripts"); -// Auto-detect Colima Docker socket (legacy ~/.colima or XDG ~/.config/colima) -if (!process.env.DOCKER_HOST) { - const home = process.env.HOME || "/tmp"; - const candidates = [ - path.join(home, ".colima/default/docker.sock"), - path.join(home, ".config/colima/default/docker.sock"), - ]; - for (const sock of candidates) { - if (fs.existsSync(sock)) { - process.env.DOCKER_HOST = `unix://${sock}`; - break; - } - } +const dockerHost = detectDockerHost(); +if (dockerHost) { + process.env.DOCKER_HOST = dockerHost.dockerHost; } function run(cmd, opts = {}) { + const stdio = opts.stdio ?? ["ignore", "inherit", "inherit"]; + const result = spawnSync("bash", ["-c", cmd], { + stdio, + cwd: ROOT, + env: { ...process.env, ...opts.env }, + ...opts, + }); + if (result.status !== 0 && !opts.ignoreError) { + console.error(` Command failed (exit ${result.status}): ${cmd.slice(0, 80)}`); + process.exit(result.status || 1); + } + return result; +} + +function runInteractive(cmd, opts = {}) { + const stdio = opts.stdio ?? "inherit"; const result = spawnSync("bash", ["-c", cmd], { - stdio: "inherit", + stdio, cwd: ROOT, env: { ...process.env, ...opts.env }, ...opts, @@ -52,4 +58,4 @@ function runCapture(cmd, opts = {}) { } } -module.exports = { ROOT, SCRIPTS, run, runCapture }; +module.exports = { ROOT, SCRIPTS, run, runCapture, runInteractive }; diff --git a/bin/nemoclaw.js b/bin/nemoclaw.js index a8a31188a1b..a229cd111b1 100755 --- a/bin/nemoclaw.js +++ b/bin/nemoclaw.js @@ -7,7 +7,7 @@ const path = require("path"); const fs = require("fs"); const os = require("os"); -const { ROOT, SCRIPTS, run, runCapture } = require("./lib/runner"); +const { ROOT, SCRIPTS, run, runCapture, runInteractive } = require("./lib/runner"); const { ensureApiKey, ensureGithubToken, @@ -17,20 +17,74 @@ const { const registry = require("./lib/registry"); const nim = require("./lib/nim"); const policies = require("./lib/policies"); +const backup = require("./lib/backup"); // ── Global commands ────────────────────────────────────────────── const GLOBAL_COMMANDS = new Set([ "onboard", "list", "deploy", "setup", "setup-spark", "start", "stop", "status", + "uninstall", + "backups", + "completion", "help", "--help", "-h", ]); +const REMOTE_UNINSTALL_URL = "https://raw.githubusercontent.com/NVIDIA/NemoClaw/refs/heads/main/uninstall.sh"; + +function shellQuote(value) { + return `'${String(value).replace(/'/g, `'\\''`)}'`; +} + +function resolveUninstallScript() { + const candidates = [ + path.join(ROOT, "uninstall.sh"), + path.join(__dirname, "..", "uninstall.sh"), + ]; + + for (const candidate of candidates) { + if (fs.existsSync(candidate)) { + return candidate; + } + } + + return null; +} + +function exitWithSpawnResult(result) { + if (result.status !== null) { + process.exit(result.status); + } + + if (result.signal) { + const signalNumber = os.constants.signals[result.signal]; + process.exit(signalNumber ? 128 + signalNumber : 1); + } + + process.exit(1); +} + +======= +>>>>>>> fork/feat/sandbox-backup +const SANDBOX_ACTIONS = [ + "connect", "status", "logs", "policy-add", "policy-list", "destroy", "export" +]; + +const SHELL_TYPES = ["bash", "zsh", "fish"]; + // ── Commands ───────────────────────────────────────────────────── -async function onboard() { +async function onboard(args) { const { onboard: runOnboard } = require("./lib/onboard"); - await runOnboard(); + const allowedArgs = new Set(["--non-interactive"]); + const unknownArgs = args.filter((arg) => !allowedArgs.has(arg)); + if (unknownArgs.length > 0) { + console.error(` Unknown onboard option(s): ${unknownArgs.join(", ")}`); + console.error(" Usage: nemoclaw onboard [--non-interactive]"); + process.exit(1); + } + const nonInteractive = args.includes("--non-interactive"); + await runOnboard({ nonInteractive }); } async function setup() { @@ -39,7 +93,9 @@ async function setup() { console.log(" Running legacy setup.sh for backwards compatibility..."); console.log(""); await ensureApiKey(); - run(`bash "${SCRIPTS}/setup.sh"`); + const { defaultSandbox } = registry.listSandboxes(); + const safeName = defaultSandbox && /^[a-z0-9][a-z0-9-]*[a-z0-9]$/.test(defaultSandbox) ? defaultSandbox : ""; + run(`bash "${SCRIPTS}/setup.sh" ${safeName}`); } async function setupSpark() { @@ -119,7 +175,7 @@ async function deploy(instanceName) { fs.unlinkSync(envTmp); console.log(" Running setup..."); - run(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && bash scripts/brev-setup.sh'`); + runInteractive(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && bash scripts/brev-setup.sh'`); if (tgToken) { console.log(" Starting services..."); @@ -129,18 +185,46 @@ async function deploy(instanceName) { console.log(""); console.log(" Connecting to sandbox..."); console.log(""); - run(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && openshell sandbox connect nemoclaw'`); + runInteractive(`ssh -t -o StrictHostKeyChecking=no -o LogLevel=ERROR ${name} 'cd /home/ubuntu/nemoclaw && set -a && . .env && set +a && openshell sandbox connect nemoclaw'`); } async function start() { await ensureApiKey(); - run(`bash "${SCRIPTS}/start-services.sh"`); + const { defaultSandbox } = registry.listSandboxes(); + const safeName = defaultSandbox && /^[a-zA-Z0-9._-]+$/.test(defaultSandbox) ? defaultSandbox : null; + const sandboxEnv = safeName ? `SANDBOX_NAME="${safeName}"` : ""; + run(`${sandboxEnv} bash "${SCRIPTS}/start-services.sh"`); } function stop() { run(`bash "${SCRIPTS}/start-services.sh" --stop`); } +function uninstall(args) { + const localScript = resolveUninstallScript(); + if (localScript) { + console.log(` Running local uninstall script: ${localScript}`); + const result = spawnSync("bash", [localScript, ...args], { + stdio: "inherit", + cwd: ROOT, + env: process.env, + }); + exitWithSpawnResult(result); + } + + console.log(` Local uninstall script not found; falling back to ${REMOTE_UNINSTALL_URL}`); + const forwardedArgs = args.map(shellQuote).join(" "); + const command = forwardedArgs.length > 0 + ? `curl -fsSL ${shellQuote(REMOTE_UNINSTALL_URL)} | bash -s -- ${forwardedArgs}` + : `curl -fsSL ${shellQuote(REMOTE_UNINSTALL_URL)} | bash`; + const result = spawnSync("bash", ["-c", command], { + stdio: "inherit", + cwd: ROOT, + env: process.env, + }); + exitWithSpawnResult(result); +} + function showStatus() { // Show sandbox registry const { sandboxes, defaultSandbox } = registry.listSandboxes(); @@ -188,8 +272,8 @@ function listSandboxes() { function sandboxConnect(sandboxName) { // Ensure port forward is alive before connecting - run(`openshell forward start --background 18789 ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); - run(`openshell sandbox connect ${sandboxName}`); + run(`openshell forward start --background 18789 "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); + runInteractive(`openshell sandbox connect "${sandboxName}"`); } function sandboxStatus(sandboxName) { @@ -204,7 +288,7 @@ function sandboxStatus(sandboxName) { } // openshell info - run(`openshell sandbox get ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); + run(`openshell sandbox get "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); // NIM health const nimStat = nim.nimStatus(sandboxName); @@ -216,8 +300,8 @@ function sandboxStatus(sandboxName) { } function sandboxLogs(sandboxName, follow) { - const followFlag = follow ? " --follow" : ""; - run(`openshell sandbox logs ${sandboxName}${followFlag}`); + const followFlag = follow ? " --tail" : ""; + run(`openshell logs "${sandboxName}"${followFlag}`); } async function sandboxPolicyAdd(sandboxName) { @@ -260,12 +344,142 @@ function sandboxDestroy(sandboxName) { nim.stopNimContainer(sandboxName); console.log(` Deleting sandbox '${sandboxName}'...`); - run(`openshell sandbox delete ${sandboxName} 2>/dev/null || true`, { ignoreError: true }); + run(`openshell sandbox delete "${sandboxName}" 2>/dev/null || true`, { ignoreError: true }); registry.removeSandbox(sandboxName); console.log(` ✓ Sandbox '${sandboxName}' destroyed`); } +// ── Backup functions ───────────────────────────────────────────── + +function listBackups() { + const backups = backup.listBackups(); + if (backups.length === 0) { + console.log(""); + console.log(" No backups found."); + console.log(` Backups are stored in: ${backup.BACKUP_DIR}`); + console.log(""); + return; + } + console.log(""); + console.log(" Backups:"); + for (const b of backups) { + const size = (b.size / 1024).toFixed(1); + console.log(` ${b.name}`); + console.log(` Created: ${new Date(b.createdAt).toLocaleString()}`); + console.log(` Size: ${size} KB`); + console.log(` Path: ${b.path}`); + console.log(""); + } +} + +function sandboxExport(sandboxName, outputPath) { + const result = backup.exportSandbox(sandboxName, outputPath); + if (!result) { + process.exit(1); + } +} + +function importBackup(backupPath, newName) { + const result = backup.importSandbox(backupPath, newName); + if (!result) { + process.exit(1); + } +} + +// ── Shell Completion ───────────────────────────────────────────── + +function printCompletion(shell) { + if (!shell || !SHELL_TYPES.includes(shell)) { + console.log(" Usage: nemoclaw completion "); + console.log(""); + console.log(" Generate shell completion scripts."); + console.log(""); + console.log(" Shells supported: bash, zsh, fish"); + console.log(""); + console.log(" Example:"); + console.log(" # Bash:"); + console.log(" nemoclaw completion bash >> ~/.bashrc"); + console.log(""); + console.log(" # Zsh:"); + console.log(" nemoclaw completion zsh >> ~/.zshrc"); + console.log(""); + console.log(" # Fish:"); + console.log(" nemoclaw completion fish > ~/.config/fish/completions/nemoclaw.fish"); + return; + } + + const globalCmds = Array.from(GLOBAL_COMMANDS).filter(c => !c.startsWith("-")); + let sandboxNames = []; + try { + sandboxNames = registry.listSandboxes().sandboxes.map(s => s.name); + } catch { + sandboxNames = []; + } + + const sandboxNamesStr = sandboxNames.length > 0 ? sandboxNames.join(" ") : ""; + const sandboxNamesZsh = sandboxNames.length > 0 ? sandboxNames.map(s => `"${s}"`).join(" ") : ""; + const sandboxNamesFish = sandboxNames.length > 0 ? sandboxNames.map(s => `'${s}'`).join(" ") : ""; + + if (shell === "bash") { + console.log(`_nemoclaw_completions() { + local cur prev opts + COMPREPLY=() + cur="\${COMP_WORDS[COMP_CWORD]}" + prev="\${COMP_WORDS[COMP_CWORD-1]}" + + # Global commands + opts="${globalCmds.join(" ")}" + + # Sandbox names (if previous word is a known sandbox) + if [[ " ${sandboxNamesStr} " =~ " $prev " ]]; then + opts="${SANDBOX_ACTIONS.join(" ")}" + fi + + # Also add sandbox names as possible first argument + opts="$opts ${sandboxNamesStr}" + + COMPREPLY=(\$(compgen -W "\$opts" -- \$cur)) + return 0 +} + +complete -F _nemoclaw_completions nemoclaw`); + } else if (shell === "zsh") { + console.log(`# nemoclaw zsh completion + +local -a global_cmds +global_cmds=(${globalCmds.map(c => `"${c}"`).join(" ")}) + +local -a sandbox_actions +sandbox_actions=(${SANDBOX_ACTIONS.map(a => `"${a}"`).join(" ")}) + +local -a sandbox_names +sandbox_names=(${sandboxNamesZsh}) + +_nemoclaw() { + local -a cmd + cmd=(\${words[1,CURRENT-1]}) + + # Check if first word is a sandbox name + if [[ " \${sandbox_names[@]} " =~ " \${cmd[1]} " ]]; then + _describe 'sandbox actions' sandbox_actions + else + _describe 'commands' global_cmds + _describe 'sandboxes' sandbox_names + fi +} + +compdef _nemoclaw nemoclaw`); + } else if (shell === "fish") { + console.log(`# nemoclaw fish completion + +complete -c nemoclaw -f -a "${globalCmds.join(" ")} ${sandboxNamesStr}" -n "test (count (commandline -opc)) -eq 1" + +complete -c nemoclaw -f -a "${SANDBOX_ACTIONS.join(" ")}" -n "test (count (commandline -opc)) -ge 2; and contains (commandline -opc | head -1) ${sandboxNamesFish}" +`); + } +} + // ── Help ───────────────────────────────────────────────────────── function help() { @@ -283,6 +497,11 @@ function help() { nemoclaw status Show sandbox status and health nemoclaw logs [--follow] View sandbox logs nemoclaw destroy Stop NIM + delete sandbox + nemoclaw export [path] Export sandbox backup + + Backup & Restore: + nemoclaw backups List all backups + nemoclaw import [name] Import a sandbox from backup Policy Presets: nemoclaw policy-add Add a policy preset to a sandbox @@ -295,6 +514,18 @@ function help() { nemoclaw start Start services (Telegram, tunnel) nemoclaw stop Stop all services nemoclaw status Show sandbox list and service status + nemoclaw uninstall [flags] Run uninstall.sh (local first, curl fallback) + + Uninstall flags: + --yes Skip the confirmation prompt + --keep-openshell Leave the openshell binary installed + --delete-models Remove NemoClaw-pulled Ollama models + + Shell Completion: + nemoclaw completion Generate shell completion script + + Shell Completion: + nemoclaw completion Generate shell completion script Credentials are prompted on first use, then saved securely in ~/.nemoclaw/credentials.json (mode 600). @@ -315,19 +546,34 @@ const [cmd, ...args] = process.argv.slice(2); // Global commands if (GLOBAL_COMMANDS.has(cmd)) { switch (cmd) { - case "onboard": await onboard(); break; + case "onboard": await onboard(args); break; case "setup": await setup(); break; case "setup-spark": await setupSpark(); break; case "deploy": await deploy(args[0]); break; case "start": await start(); break; case "stop": stop(); break; case "status": showStatus(); break; + case "uninstall": uninstall(args); break; case "list": listSandboxes(); break; + case "backups": listBackups(); break; + case "completion": printCompletion(args[0]); break; default: help(); break; } return; } + // Import command (special case - not a sandbox name) + if (cmd === "import") { + const backupPath = args[0]; + const newName = args[1]; + if (!backupPath) { + console.error(" Usage: nemoclaw import [new-sandbox-name]"); + process.exit(1); + } + importBackup(backupPath, newName); + return; + } + // Sandbox-scoped commands: nemoclaw const sandbox = registry.getSandbox(cmd); if (sandbox) { @@ -341,9 +587,10 @@ const [cmd, ...args] = process.argv.slice(2); case "policy-add": await sandboxPolicyAdd(cmd); break; case "policy-list": sandboxPolicyList(cmd); break; case "destroy": sandboxDestroy(cmd); break; + case "export": sandboxExport(cmd, actionArgs[0]); break; default: console.error(` Unknown action: ${action}`); - console.error(` Valid actions: connect, status, logs, policy-add, policy-list, destroy`); + console.error(` Valid actions: connect, status, logs, policy-add, policy-list, destroy, export`); process.exit(1); } return; diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 168df85ac19..047e000c097 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -8,7 +8,7 @@ If you use an AI coding agent (Cursor, Claude Code, Codex, etc.), the repo inclu | Skill | What it does | When to use | |---|---|---| -| `update-docs-from-commits` | Scans recent commits for user-facing changes and drafts doc updates. | After landing features, before a release, or to find doc gaps. | +| `update-docs` | Scans recent commits for user-facing changes and drafts doc updates. | After landing features, before a release, or to find doc gaps. | The skills live in `.agents/skills/` and follow the style guide below automatically. To use one, ask your agent to run it. For example, ask it to "catch up the docs for everything merged since v0.2.0". diff --git a/docs/about/how-it-works.md b/docs/about/how-it-works.md index ea274fdb195..9a873a904c7 100644 --- a/docs/about/how-it-works.md +++ b/docs/about/how-it-works.md @@ -21,7 +21,7 @@ status: published # How NemoClaw Works NemoClaw combines a lightweight CLI plugin with a versioned blueprint to move OpenClaw into a controlled sandbox. -This page explains the key concepts at a high level. +This page explains the key concepts about NemoClaw at a high level. ## How It Fits Together diff --git a/docs/about/overview.md b/docs/about/overview.md index 03131b0225c..15002f2f219 100644 --- a/docs/about/overview.md +++ b/docs/about/overview.md @@ -2,8 +2,8 @@ title: page: "NemoClaw Overview — What It Does and How It Fits Together" nav: "Overview" -description: "NemoClaw sandboxes OpenClaw with NVIDIA inference and declarative policy." -keywords: ["nemoclaw overview", "openclaw openshell sandbox plugin"] +description: "NemoClaw is an open source reference stack that simplifies running OpenClaw always-on assistants safely." +keywords: ["nemoclaw overview", "openclaw always-on assistants", "nvidia openshell", "nvidia nemotron"] topics: ["generative_ai", "ai_agents"] tags: ["openclaw", "openshell", "sandboxing", "inference_routing", "blueprints"] content: @@ -20,8 +20,9 @@ status: published # Overview -NemoClaw is the [OpenClaw](https://openclaw.ai) plugin for [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell). -It moves OpenClaw into a sandboxed environment where every network request, file access, and inference call is governed by declarative policy. +NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants safely. It incorporates policy-based privacy and security guardrails, giving users control over their agents' behavior and data handling. This enables self-evolving claws to run more safely in clouds, on prem, RTX PCs, and DGX Spark. + +NemoClaw uses open source models, such as [NVIDIA Nemotron](https://build.nvidia.com), alongside the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of the NVIDIA Agent Toolkit, a secure environment designed for executing claws more safely. By combining open source models with built-in safety measures, NemoClaw simplifies and secures AI agent deployment. | Capability | Description | |-------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------| diff --git a/docs/about/release-notes.md b/docs/about/release-notes.md index e5a1fea8b74..bb98d56b02f 100644 --- a/docs/about/release-notes.md +++ b/docs/about/release-notes.md @@ -20,7 +20,7 @@ status: published # Release Notes -NVIDIA NemoClaw is in active development and follows a frequent release cadence. Use the following GitHub resources directly. +NVIDIA NemoClaw is available in early preview starting March 16, 2026. Use the following GitHub resources to track changes. | Resource | Description | |---|---| diff --git a/docs/conf.py b/docs/conf.py index d2b8198c804..210c216499a 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -97,11 +97,17 @@ # "public_docs_features": True, # TODO: Uncomment this when the docs are public "icon_links": [ { - "name": "GitHub", + "name": "NemoClaw GitHub", "url": "https://github.com/NVIDIA/NemoClaw", "icon": "fa-brands fa-github", "type": "fontawesome", }, + { + "name": "NemoClaw Discord", + "url": "https://discord.gg/XFpfPv9Uvx", + "icon": "fa-brands fa-discord", + "type": "fontawesome", + }, ], } diff --git a/docs/deployment/deploy-to-remote-gpu.md b/docs/deployment/deploy-to-remote-gpu.md index 737c9262f7d..6fcf603dce3 100644 --- a/docs/deployment/deploy-to-remote-gpu.md +++ b/docs/deployment/deploy-to-remote-gpu.md @@ -27,10 +27,14 @@ The deploy command provisions the VM, installs dependencies, and connects you to - The [Brev CLI](https://brev.nvidia.com) installed and authenticated. - An NVIDIA API key from [build.nvidia.com](https://build.nvidia.com). -- NemoClaw installed locally. Install with `npm install -g nemoclaw`. +- NemoClaw installed locally. Follow the [Quickstart](../get-started/quickstart.md) install steps. ## Deploy the Instance +:::{warning} +The `nemoclaw deploy` command is experimental and may not work as expected. +::: + Create a Brev instance and run the NemoClaw setup: ```console diff --git a/docs/get-started/quickstart.md b/docs/get-started/quickstart.md index 588bc66162f..123f9fdd2e1 100644 --- a/docs/get-started/quickstart.md +++ b/docs/get-started/quickstart.md @@ -20,6 +20,12 @@ status: published # Quickstart +Follow these steps to get started with NemoClaw and your first sandboxed OpenClaw agent. + +:::{note} +NemoClaw currently requires a fresh installation of OpenClaw. +::: + ```{include} ../../README.md :start-after: :end-before: @@ -32,3 +38,7 @@ status: published - [Customize the network policy](../network-policy/customize-network-policy.md) to pre-approve trusted domains. - [Deploy to a remote GPU instance](../deployment/deploy-to-remote-gpu.md) for always-on operation. - [Monitor sandbox activity](../monitoring/monitor-sandbox-activity.md) through the OpenShell TUI. + +### Troubleshooting + +If you run into issues during installation or onboarding, refer to the [Troubleshooting guide](../reference/troubleshooting.md) for common error messages and resolution steps. diff --git a/docs/index.md b/docs/index.md index b4dc9bf57d1..6fc4305ceac 100644 --- a/docs/index.md +++ b/docs/index.md @@ -2,8 +2,8 @@ title: page: "NVIDIA NemoClaw Developer Guide" nav: "NemoClaw" -description: "Sandbox OpenClaw with NVIDIA inference routing and strict network policies." -keywords: ["nemoclaw sandboxed ai agent", "openclaw openshell plugin"] +description: "NemoClaw is an open source reference stack that simplifies running OpenClaw always-on assistants more safely, with a single command." +keywords: ["nemoclaw open source reference stack", "openclaw always-on assistants", "nvidia openshell", "nvidia nemotron"] topics: ["generative_ai", "ai_agents"] tags: ["openclaw", "openshell", "sandboxing", "inference_routing", "nemoclaw"] content: @@ -25,9 +25,8 @@ status: published :end-before: ``` -NemoClaw is the OpenClaw plugin for [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell). -It runs OpenClaw inside a sandboxed environment with NVIDIA cloud inference, such as Nemotron 3 Super 120B through [build.nvidia.com](https://build.nvidia.com). -The sandbox enforces strict network policies and operator-controlled egress approval. +NVIDIA NemoClaw is an open source reference stack that simplifies running [OpenClaw](https://openclaw.ai) always-on assistants safely. +It installs the [NVIDIA OpenShell](https://github.com/NVIDIA/OpenShell) runtime, part of NVIDIA Agent Toolkit, a secure environment for running autonomous agents, and open source models like [NVIDIA Nemotron](https://build.nvidia.com). ## Get Started @@ -77,7 +76,7 @@ Install the CLI and launch a sandboxed OpenClaw instance in a few commands.
-
$ curl -fsSL https://nvidia.com/nemoclaw.sh | bash
+
$ curl -fsSL https://www.nvidia.com/nemoclaw.sh | bash
``` @@ -236,6 +235,7 @@ Architecture Commands Inference Profiles Network Policies +Troubleshooting ``` ```{toctree} @@ -243,4 +243,5 @@ Network Policies :hidden: resources/license +Discord ``` diff --git a/docs/monitoring/monitor-sandbox-activity.md b/docs/monitoring/monitor-sandbox-activity.md index 6b358d067e3..ab071e5f3ed 100644 --- a/docs/monitoring/monitor-sandbox-activity.md +++ b/docs/monitoring/monitor-sandbox-activity.md @@ -47,6 +47,8 @@ Key fields in the output include the following: - Blueprint run ID, which is the identifier for the most recent blueprint execution. - Inference provider, which shows the active provider, model, and endpoint. +If you run `openclaw nemoclaw status` from inside the sandbox, the command detects the sandbox context and reports it. Host-level sandbox and inference details are not available from within the sandbox. Run `openshell sandbox list` on the host to check the underlying sandbox state. + ## View Blueprint and Sandbox Logs Stream the most recent log output from the blueprint runner and sandbox: @@ -106,19 +108,9 @@ If the request fails, check the following: 2. Run `openclaw nemoclaw logs -f` to view error messages from the blueprint runner. 3. Verify that the inference endpoint is reachable from the host. -## Common Issues - -The following table lists common problems and their resolution steps: - -| Symptom | Resolution | -|---|---| -| Sandbox shows as stopped | Run `nemoclaw onboard` to recreate the sandbox. | -| Inference requests time out | Verify the provider endpoint is reachable. Check `openclaw nemoclaw status` for the active endpoint. | -| Agent cannot reach an external host | Open the TUI with `openshell term` and approve the blocked request, or add the endpoint to the policy. | -| Blueprint run failed | Run `openclaw nemoclaw logs --run-id ` to view the error output for the failed run. | - ## Related Topics +- [Troubleshooting](../reference/troubleshooting.md) for common issues and resolution steps. - [Commands](../reference/commands.md) for the full CLI reference. - [Approve or Deny Agent Network Requests](../network-policy/approve-network-requests.md) for the operator approval flow. - [Switch Inference Providers](../inference/switch-inference-providers.md) to change the active provider. diff --git a/docs/reference/commands.md b/docs/reference/commands.md index af4f130e81c..bb65f775b02 100644 --- a/docs/reference/commands.md +++ b/docs/reference/commands.md @@ -52,6 +52,14 @@ Use this after launch to connect and chat with the agent through the TUI or CLI. $ nemoclaw my-assistant connect ``` +If the TUI view is not a good fit for very long responses, use the CLI form instead: + +```console +$ openclaw agent --agent main --local -m "" --session-id +``` + +This is the recommended workaround when you need the full response printed directly in the terminal. + ### `openclaw nemoclaw status` Display sandbox health, blueprint run state, and inference configuration. @@ -63,6 +71,10 @@ $ openclaw nemoclaw status [--json] `--json` : Output as JSON for programmatic consumption. +When running inside an active OpenShell sandbox, the status command detects the sandbox context and reports "active (inside sandbox)" instead of false negatives. +Host-side sandbox state and inference configuration are not inspectable from inside the sandbox. +Run `openshell sandbox list` on the host to check the underlying sandbox state. + ### `openclaw nemoclaw logs` Stream blueprint execution and sandbox logs. @@ -104,6 +116,13 @@ $ nemoclaw onboard The first run prompts for your NVIDIA API key and saves it to `~/.nemoclaw/credentials.json`. +The wizard prompts for a sandbox name. +Names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character. +Uppercase letters are automatically lowercased. + +Before creating the gateway, the wizard runs preflight checks. +On systems with cgroup v2 (Ubuntu 24.04, DGX Spark, WSL2), it verifies that Docker is configured with `"default-cgroupns-mode": "host"` and provides fix instructions if the setting is missing. + ### `nemoclaw list` List all registered sandboxes with their model, provider, and policy presets. @@ -114,6 +133,10 @@ $ nemoclaw list ### `nemoclaw deploy` +:::{warning} +The `nemoclaw deploy` command is experimental and may not work as expected. +::: + Deploy NemoClaw to a remote GPU instance through [Brev](https://brev.nvidia.com). The deploy script installs Docker, NVIDIA Container Toolkit if a GPU is present, and OpenShell on the VM, then runs the nemoclaw setup and connects to the sandbox. @@ -214,3 +237,8 @@ $ nemoclaw status Set up NemoClaw on DGX Spark. This command applies cgroup v2 and Docker fixes required for Ubuntu 24.04. Run with `sudo` on the Spark host. +After the fixes complete, the script prompts you to run `nemoclaw onboard` to continue setup. + +```console +$ sudo nemoclaw setup-spark +``` diff --git a/docs/reference/troubleshooting.md b/docs/reference/troubleshooting.md new file mode 100644 index 00000000000..14ec2c27e18 --- /dev/null +++ b/docs/reference/troubleshooting.md @@ -0,0 +1,191 @@ +--- +title: + page: "NemoClaw Troubleshooting Guide" + nav: "Troubleshooting" +description: "Diagnose and resolve common NemoClaw installation, onboarding, and runtime issues." +keywords: ["nemoclaw troubleshooting", "nemoclaw debug sandbox issues"] +topics: ["generative_ai", "ai_agents"] +tags: ["openclaw", "openshell", "troubleshooting", "nemoclaw"] +content: + type: reference + difficulty: technical_beginner + audience: ["developer", "engineer"] +status: published +--- + + + + + +# Troubleshooting + +This page covers common issues you may encounter when installing, onboarding, or running NemoClaw, along with their resolution steps. + +:::{admonition} Get Help +:class: tip + +If your issue is not listed here, join the [NemoClaw Discord channel](https://discord.gg/XFpfPv9Uvx) to ask questions and get help from the community. You can also [file an issue on GitHub](https://github.com/NVIDIA/NemoClaw/issues/new). +::: + +## Installation + +### `nemoclaw` not found after install + +If you use nvm or fnm to manage Node.js, the installer may not update your current shell's PATH. +The `nemoclaw` binary is installed but the shell session does not know where to find it. + +Run `source ~/.bashrc` (or `source ~/.zshrc` for zsh), or open a new terminal window. + +### Installer fails on unsupported platform + +The installer checks for a supported OS and architecture before proceeding. +NemoClaw requires Linux Ubuntu 22.04 LTS or later. +If you see an unsupported platform error, verify that you are running on a supported Linux distribution. + +### Node.js version is too old + +NemoClaw requires Node.js 20 or later. +If the installer exits with a Node.js version error, check your current version: + +```console +$ node --version +``` + +If the version is below 20, install a supported release. +If you use nvm, run: + +```console +$ nvm install 20 +$ nvm use 20 +``` + +Then re-run the installer. + +### Docker is not running + +The installer and onboard wizard require Docker to be running. +If you see a Docker connection error, start the Docker daemon: + +```console +$ sudo systemctl start docker +``` + +On macOS with Docker Desktop, open the Docker Desktop application and wait for it to finish starting before retrying. + +### npm install fails with permission errors + +If `npm install` fails with an `EACCES` permission error, do not run npm with `sudo`. +Instead, configure npm to use a directory you own: + +```console +$ mkdir -p ~/.npm-global +$ npm config set prefix ~/.npm-global +$ export PATH=~/.npm-global/bin:$PATH +``` + +Add the `export` line to your `~/.bashrc` or `~/.zshrc` to make it permanent, then re-run the installer. + +### Port already in use + +The NemoClaw gateway uses port `18789` by default. +If another process is already bound to this port, onboarding fails. +Identify the conflicting process, verify it is safe to stop, and terminate it: + +```console +$ lsof -i :18789 +$ kill +``` + +If the process does not exit, use `kill -9 ` to force-terminate it. +Then retry onboarding. + +## Onboarding + +### Cgroup v2 errors during onboard + +On Ubuntu 24.04, DGX Spark, and WSL2, Docker may not be configured for cgroup v2 delegation. +The onboard preflight check detects this and fails with a clear error message. + +Run the Spark setup script to fix the Docker cgroup configuration, then retry onboarding: + +```console +$ sudo nemoclaw setup-spark +$ nemoclaw onboard +``` + +### Invalid sandbox name + +Sandbox names must follow RFC 1123 subdomain rules: lowercase alphanumeric characters and hyphens only, and must start and end with an alphanumeric character. +Uppercase letters are automatically lowercased. + +If the name does not match these rules, the wizard exits with an error. +Choose a name such as `my-assistant` or `dev1`. + +### Sandbox creation fails on DGX + +On DGX machines, sandbox creation can fail if the gateway's DNS has not finished propagating or if a stale port forward from a previous onboard run is still active. + +Run `nemoclaw onboard` to retry. +The wizard cleans up stale port forwards and waits for gateway readiness automatically. + +### Colima socket not detected (macOS) + +Newer Colima versions use the XDG base directory (`~/.config/colima/default/docker.sock`) instead of the legacy path (`~/.colima/default/docker.sock`). +NemoClaw checks both paths. +If neither is found, verify that Colima is running: + +```console +$ colima status +``` + +## Runtime + +### Sandbox shows as stopped + +The sandbox may have been stopped or deleted. +Run `nemoclaw onboard` to recreate the sandbox from the same blueprint and policy definitions. + +### Status shows "not running" inside the sandbox + +This is expected behavior. +When running `openclaw nemoclaw status` inside an active sandbox, host-side sandbox state and inference configuration are not inspectable. +The status command detects the sandbox context and reports "active (inside sandbox)" instead. + +Run `openshell sandbox list` on the host to check the underlying sandbox state. + +### Inference requests time out + +Verify that the inference provider endpoint is reachable from the host. +Check the active provider and endpoint: + +```console +$ openclaw nemoclaw status +``` + +If the endpoint is correct but requests still fail, check for network policy rules that may block the connection, and verify that your NVIDIA API key is valid. + +### Agent cannot reach an external host + +OpenShell blocks outbound connections to hosts not listed in the network policy. +Open the TUI to see blocked requests and approve them: + +```console +$ openshell term +``` + +To permanently allow an endpoint, add it to the network policy. +Refer to [Customize the Network Policy](../network-policy/customize-network-policy.md) for details. + +### Blueprint run failed + +View the error output for the failed blueprint run: + +```console +$ openclaw nemoclaw logs --run-id +``` + +If the run ID is unknown, omit `--run-id` to view logs from the most recent run. +Use `--follow` to stream logs in real time while debugging. diff --git a/install.sh b/install.sh index 652e229bb41..d72fd484307 100755 --- a/install.sh +++ b/install.sh @@ -19,6 +19,8 @@ MIN_NODE_MAJOR=20 MIN_NPM_MAJOR=10 RECOMMENDED_NODE_MAJOR=22 RUNTIME_REQUIREMENT_MSG="NemoClaw requires Node.js >=${MIN_NODE_MAJOR} and npm >=${MIN_NPM_MAJOR} (recommended Node.js ${RECOMMENDED_NODE_MAJOR})." +NEMOCLAW_SHIM_DIR="${HOME}/.local/bin" +ORIGINAL_PATH="${PATH:-}" # Compare two semver strings (major.minor.patch). Returns 0 if $1 >= $2. version_gte() { @@ -53,6 +55,30 @@ refresh_path() { if [[ -n "$npm_bin" && -d "$npm_bin" && ":$PATH:" != *":$npm_bin:"* ]]; then export PATH="$npm_bin:$PATH" fi + + if [[ -d "$NEMOCLAW_SHIM_DIR" && ":$PATH:" != *":$NEMOCLAW_SHIM_DIR:"* ]]; then + export PATH="$NEMOCLAW_SHIM_DIR:$PATH" + fi +} + +ensure_nemoclaw_shim() { + local npm_bin shim_path + npm_bin="$(npm config get prefix 2>/dev/null)/bin" || true + shim_path="${NEMOCLAW_SHIM_DIR}/nemoclaw" + + if [[ -z "$npm_bin" || ! -x "$npm_bin/nemoclaw" ]]; then + return 1 + fi + + if [[ ":$ORIGINAL_PATH:" == *":$npm_bin:"* ]] || [[ ":$ORIGINAL_PATH:" == *":$NEMOCLAW_SHIM_DIR:"* ]]; then + return 0 + fi + + mkdir -p "$NEMOCLAW_SHIM_DIR" + ln -sfn "$npm_bin/nemoclaw" "$shim_path" + refresh_path + info "Created user-local shim at $shim_path" + return 0 } version_major() { @@ -113,7 +139,7 @@ install_nodejs() { bash "$nvm_tmp" rm -f "$nvm_tmp" ensure_nvm_loaded - nvm install 24 + nvm install 22 info "Node.js installed: $(node --version)" } @@ -198,12 +224,13 @@ install_nemoclaw() { info "NemoClaw package.json found in current directory — installing from source…" npm install && npm link else - info "Installing NemoClaw from npm…" + info "Installing NemoClaw from GitHub…" # Revert once https://github.com/NVIDIA/NemoClaw/issues/71 is complete and the package is published - npm install -g git+ssh://git@github.com/nvidia/NemoClaw.git + npm install -g git+https://github.com/NVIDIA/NemoClaw.git fi refresh_path + ensure_nemoclaw_shim || true } # --------------------------------------------------------------------------- @@ -222,21 +249,26 @@ verify_nemoclaw() { npm_bin="$(npm config get prefix 2>/dev/null)/bin" || true if [[ -n "$npm_bin" && -x "$npm_bin/nemoclaw" ]]; then - warn "Found nemoclaw at $npm_bin/nemoclaw but that directory is not on PATH." + ensure_nemoclaw_shim || true + if command_exists nemoclaw; then + info "Verified: nemoclaw is available at $(command -v nemoclaw)" + return 0 + fi + + warn "Found nemoclaw at $npm_bin/nemoclaw but could not expose it on PATH." warn "" - warn "Add it to your shell profile:" - warn " echo 'export PATH=\"$npm_bin:\$PATH\"' >> ~/.bashrc" - warn " source ~/.bashrc" + warn "Add one of these directories to your shell profile:" + warn " $NEMOCLAW_SHIM_DIR" + warn " $npm_bin" warn "" - warn "Or for zsh:" - warn " echo 'export PATH=\"$npm_bin:\$PATH\"' >> ~/.zshrc" - warn " source ~/.zshrc" + warn "Continuing — nemoclaw is installed but requires a PATH update." + return 0 else warn "Could not locate the nemoclaw executable." - warn "Try running: npm install -g nemoclaw" + warn "Try running: npm install -g git+https://github.com/NVIDIA/NemoClaw.git" fi - error "Installation failed: nemoclaw --help could not be executed." + error "Installation failed: nemoclaw binary not found." } # --------------------------------------------------------------------------- @@ -244,13 +276,67 @@ verify_nemoclaw() { # --------------------------------------------------------------------------- run_onboard() { info "Running nemoclaw onboard…" - nemoclaw onboard + if [ "${NON_INTERACTIVE:-}" = "1" ]; then + nemoclaw onboard --non-interactive + elif [ -t 0 ]; then + nemoclaw onboard + elif exec 3", "API key for endpoints that require one (skips prompt)") - .option("--endpoint ", "Endpoint type: build, ncp, nim-local, vllm, ollama, custom (local options are experimental)") + .option("--endpoint ", "Endpoint type: build, ncp, ollama, nim-local, vllm, custom (nim-local and vllm are experimental)") .option("--ncp-partner ", "NCP partner name (when endpoint is ncp)") .option("--endpoint-url ", "Endpoint URL (for ncp, nim-local, ollama, or custom)") .option("--model ", "Model ID to use") diff --git a/nemoclaw/dist/cli.js.map b/nemoclaw/dist/cli.js.map index 8e3fe13a0a8..27a57c7617e 100644 --- a/nemoclaw/dist/cli.js.map +++ b/nemoclaw/dist/cli.js.map @@ -1 +1 @@ -{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"cli.js","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/onboard.d.ts.map b/nemoclaw/dist/commands/onboard.d.ts.map index bde36015fe7..9f7d7834261 100644 --- a/nemoclaw/dist/commands/onboard.d.ts.map +++ b/nemoclaw/dist/commands/onboard.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAUhE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,cAAc,CAAC;CAC9B;AA+JD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAwQpE"} \ No newline at end of file +{"version":3,"file":"onboard.d.ts","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAYhE,MAAM,WAAW,cAAc;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,cAAc,CAAC;CAC9B;AAyMD,wBAAsB,UAAU,CAAC,IAAI,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC,CAySpE"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/onboard.js b/nemoclaw/dist/commands/onboard.js index 62c04ae4333..c112c07b57c 100644 --- a/nemoclaw/dist/commands/onboard.js +++ b/nemoclaw/dist/commands/onboard.js @@ -8,7 +8,7 @@ const config_js_1 = require("../onboard/config.js"); const prompt_js_1 = require("../onboard/prompt.js"); const validate_js_1 = require("../onboard/validate.js"); const ENDPOINT_TYPES = ["build", "ncp", "nim-local", "vllm", "ollama", "custom"]; -const SUPPORTED_ENDPOINT_TYPES = ["build", "ncp"]; +const SUPPORTED_ENDPOINT_TYPES = ["build", "ncp", "ollama"]; function isExperimentalEnabled() { return process.env.NEMOCLAW_EXPERIMENTAL === "1"; } @@ -16,10 +16,13 @@ const BUILD_ENDPOINT_URL = "https://integrate.api.nvidia.com/v1"; const HOST_GATEWAY_URL = "http://host.openshell.internal"; const DEFAULT_MODELS = [ { id: "nvidia/nemotron-3-super-120b-a12b", label: "Nemotron 3 Super 120B" }, - { id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", label: "Nemotron Ultra 253B" }, - { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", label: "Nemotron Super 49B v1.5" }, - { id: "nvidia/nemotron-3-nano-30b-a3b", label: "Nemotron 3 Nano 30B" }, + { id: "moonshotai/kimi-k2.5", label: "Kimi K2.5" }, + { id: "z-ai/glm5", label: "GLM-5" }, + { id: "minimaxai/minimax-m2.5", label: "MiniMax M2.5" }, + { id: "qwen/qwen3.5-397b-a17b", label: "Qwen3.5 397B A17B" }, + { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" }, ]; +const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b"; function resolveProfile(endpointType) { switch (endpointType) { case "build": @@ -96,6 +99,30 @@ function detectOllama() { const running = testCommand("curl -sf http://localhost:11434/api/tags >/dev/null 2>&1"); return { installed, running }; } +function parseOllamaList(output) { + return output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !/^NAME\s+/i.test(line)) + .map((line) => line.split(/\s{2,}/)[0]) + .filter(Boolean); +} +function getOllamaModelOptions() { + try { + const output = (0, node_child_process_1.execSync)("ollama list", { encoding: "utf-8", shell: "/bin/bash" }); + const parsed = parseOllamaList(output); + if (parsed.length > 0) { + return parsed; + } + } + catch { } + return [DEFAULT_OLLAMA_MODEL]; +} +function getDefaultOllamaModel() { + const models = getOllamaModelOptions(); + return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0]; +} function testCommand(command) { try { (0, node_child_process_1.execSync)(command, { encoding: "utf-8", stdio: "ignore", shell: "/bin/bash" }); @@ -106,7 +133,8 @@ function testCommand(command) { } } function showConfig(config, logger) { - logger.info(` Endpoint: ${config.endpointType} (${config.endpointUrl})`); + logger.info(` Endpoint: ${(0, config_js_1.describeOnboardEndpoint)(config)}`); + logger.info(` Provider: ${(0, config_js_1.describeOnboardProvider)(config)}`); if (config.ncpPartner) { logger.info(` NCP Partner: ${config.ncpPartner}`); } @@ -116,7 +144,7 @@ function showConfig(config, logger) { logger.info(` Onboarded: ${config.onboardedAt}`); } async function promptEndpoint(ollama) { - return (await (0, prompt_js_1.promptSelect)("Select your inference endpoint:", [ + const options = [ { label: "NVIDIA Build (build.nvidia.com)", value: "build", @@ -127,22 +155,28 @@ async function promptEndpoint(ollama) { value: "ncp", hint: "dedicated capacity, SLA-backed", }, - { + ]; + options.push({ + label: "Local Ollama", + value: "ollama", + hint: ollama.running + ? "detected on localhost:11434" + : ollama.installed + ? "installed locally" + : "localhost:11434", + }); + if (isExperimentalEnabled()) { + options.push({ label: "Self-hosted NIM [experimental]", value: "nim-local", hint: "experimental — your own NIM container deployment", - }, - { + }, { label: "Local vLLM [experimental]", value: "vllm", hint: "experimental — local development", - }, - { - label: "Local Ollama [experimental]", - value: "ollama", - hint: `experimental — ${ollama.installed ? "installed locally" : "localhost:11434"}`, - }, - ])); + }); + } + return (await (0, prompt_js_1.promptSelect)("Select your inference endpoint:", options)); } function execOpenShell(args) { return (0, node_child_process_1.execFileSync)("openshell", args, { @@ -185,13 +219,11 @@ async function cliOnboard(opts) { } else { const ollama = detectOllama(); - if (ollama.running && isExperimentalEnabled()) { - logger.info("Detected Ollama on localhost:11434. Using it for onboarding."); - endpointType = "ollama"; - } - else { - endpointType = await promptEndpoint(ollama); + if (ollama.running) { + logger.info("Detected local inference option: Ollama."); + logger.info("Select it explicitly if you want to use it."); } + endpointType = await promptEndpoint(ollama); } // Step 2: Endpoint URL resolution let endpointUrl; @@ -279,20 +311,45 @@ async function cliOnboard(opts) { model = opts.model; } else { - // Build model options: prefer Nemotron models from the endpoint, fall back to defaults - const nemotronModels = validation.models.filter((m) => m.includes("nemotron")); - const modelOptions = nemotronModels.length > 0 - ? nemotronModels.map((id) => ({ label: id, value: id })) - : DEFAULT_MODELS.map((m) => ({ label: `${m.label} (${m.id})`, value: m.id })); - model = await (0, prompt_js_1.promptSelect)("Select your primary model:", modelOptions); + const discoveredModelOptions = endpointType === "ollama" + ? getOllamaModelOptions().map((id) => ({ label: id, value: id })) + : validation.models.map((id) => ({ label: id, value: id })); + const curatedCloudOptions = endpointType === "build" || endpointType === "ncp" + ? DEFAULT_MODELS.filter((option) => validation.models.includes(option.id)).map((option) => ({ + label: `${option.label} (${option.id})`, + value: option.id, + })) + : []; + const defaultIndex = endpointType === "ollama" + ? Math.max(0, discoveredModelOptions.findIndex((option) => option.value === getDefaultOllamaModel())) + : 0; + const modelOptions = curatedCloudOptions.length > 0 + ? curatedCloudOptions + : discoveredModelOptions.length > 0 + ? discoveredModelOptions + : DEFAULT_MODELS.map((m) => ({ label: `${m.label} (${m.id})`, value: m.id })); + model = await (0, prompt_js_1.promptSelect)("Select your primary model:", modelOptions, defaultIndex); } // Step 6: Resolve profile const profile = resolveProfile(endpointType); const providerName = resolveProviderName(endpointType); + const summaryConfig = { + endpointType, + endpointUrl, + ncpPartner, + model, + profile, + credentialEnv, + provider: providerName, + providerLabel: undefined, + onboardedAt: "", + }; + summaryConfig.providerLabel = (0, config_js_1.describeOnboardProvider)(summaryConfig); // Step 7: Confirmation logger.info(""); logger.info("Configuration summary:"); - logger.info(` Endpoint: ${endpointType} (${endpointUrl})`); + logger.info(` Endpoint: ${(0, config_js_1.describeOnboardEndpoint)(summaryConfig)}`); + logger.info(` Provider: ${summaryConfig.providerLabel}`); if (ncpPartner) { logger.info(` NCP Partner: ${ncpPartner}`); } @@ -374,13 +431,16 @@ async function cliOnboard(opts) { model, profile, credentialEnv, + provider: providerName, + providerLabel: summaryConfig.providerLabel, onboardedAt: new Date().toISOString(), }); // Step 9: Success logger.info(""); logger.info("Onboarding complete!"); logger.info(""); - logger.info(` Endpoint: ${endpointUrl}`); + logger.info(` Endpoint: ${(0, config_js_1.describeOnboardEndpoint)(summaryConfig)}`); + logger.info(` Provider: ${summaryConfig.providerLabel}`); logger.info(` Model: ${model}`); logger.info(` Credential: $${credentialEnv}`); logger.info(""); diff --git a/nemoclaw/dist/commands/onboard.js.map b/nemoclaw/dist/commands/onboard.js.map index 08c06da56ed..d75cad4c38d 100644 --- a/nemoclaw/dist/commands/onboard.js.map +++ b/nemoclaw/dist/commands/onboard.js.map @@ -1 +1 @@ -{"version":3,"file":"onboard.js","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"onboard.js","sourceRoot":"","sources":["../../src/commands/onboard.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/slash.d.ts.map b/nemoclaw/dist/commands/slash.d.ts.map index ff6015318b2..2b9c8844b13 100644 --- a/nemoclaw/dist/commands/slash.d.ts.map +++ b/nemoclaw/dist/commands/slash.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"slash.d.ts","sourceRoot":"","sources":["../../src/commands/slash.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAIhG,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,oBAAoB,EACzB,IAAI,EAAE,iBAAiB,GACtB,mBAAmB,CAarB"} \ No newline at end of file +{"version":3,"file":"slash.d.ts","sourceRoot":"","sources":["../../src/commands/slash.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,oBAAoB,EAAE,mBAAmB,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAQhG,wBAAgB,kBAAkB,CAChC,GAAG,EAAE,oBAAoB,EACzB,IAAI,EAAE,iBAAiB,GACtB,mBAAmB,CAarB"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/slash.js b/nemoclaw/dist/commands/slash.js index a7900d3096e..fe909e6e92b 100644 --- a/nemoclaw/dist/commands/slash.js +++ b/nemoclaw/dist/commands/slash.js @@ -67,7 +67,8 @@ function slashOnboard() { text: [ "**NemoClaw Onboard Status**", "", - `Endpoint: ${config.endpointType} (${config.endpointUrl})`, + `Endpoint: ${(0, config_js_1.describeOnboardEndpoint)(config)}`, + `Provider: ${(0, config_js_1.describeOnboardProvider)(config)}`, config.ncpPartner ? `NCP Partner: ${config.ncpPartner}` : null, `Model: ${config.model}`, `Credential: $${config.credentialEnv}`, diff --git a/nemoclaw/dist/commands/slash.js.map b/nemoclaw/dist/commands/slash.js.map index 13b84101ad2..cb97903a4a7 100644 --- a/nemoclaw/dist/commands/slash.js.map +++ b/nemoclaw/dist/commands/slash.js.map @@ -1 +1 @@ -{"version":3,"file":"slash.js","sourceRoot":"","sources":["../../src/commands/slash.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"slash.js","sourceRoot":"","sources":["../../src/commands/slash.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/status.d.ts.map b/nemoclaw/dist/commands/status.d.ts.map index 203b26f8fd9..7d79d03dea9 100644 --- a/nemoclaw/dist/commands/status.d.ts.map +++ b/nemoclaw/dist/commands/status.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAKA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAKhE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,cAAc,CAAC;CAC9B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoElE"} \ No newline at end of file +{"version":3,"file":"status.d.ts","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAehE,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,OAAO,CAAC;IACd,MAAM,EAAE,YAAY,CAAC;IACrB,YAAY,EAAE,cAAc,CAAC;CAC9B;AAED,wBAAsB,SAAS,CAAC,IAAI,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC,CAoFlE"} \ No newline at end of file diff --git a/nemoclaw/dist/commands/status.js b/nemoclaw/dist/commands/status.js index 1f93ffb6fc9..b7d805ef6d8 100644 --- a/nemoclaw/dist/commands/status.js +++ b/nemoclaw/dist/commands/status.js @@ -4,16 +4,27 @@ Object.defineProperty(exports, "__esModule", { value: true }); exports.cliStatus = cliStatus; const node_child_process_1 = require("node:child_process"); +const node_fs_1 = require("node:fs"); const node_util_1 = require("node:util"); const state_js_1 = require("../blueprint/state.js"); const execAsync = (0, node_util_1.promisify)(node_child_process_1.exec); +/** + * Detect whether the plugin is running inside an OpenShell sandbox. + * Inside sandboxes the root filesystem is mounted at /sandbox and openshell + * host commands are not available, so querying `openshell sandbox status` + * would always fail — producing false-negative "not running" reports. + */ +function isInsideSandbox() { + return (0, node_fs_1.existsSync)("/sandbox/.openclaw") || (0, node_fs_1.existsSync)("/sandbox/.nemoclaw"); +} async function cliStatus(opts) { const { json: jsonOutput, logger } = opts; const state = (0, state_js_1.loadState)(); const sandboxName = state.sandboxName ?? "openclaw"; + const insideSandbox = isInsideSandbox(); const [sandbox, inference] = await Promise.all([ - getSandboxStatus(sandboxName), - getInferenceStatus(), + getSandboxStatus(sandboxName, insideSandbox), + getInferenceStatus(insideSandbox), ]); const statusData = { nemoclaw: { @@ -26,6 +37,7 @@ async function cliStatus(opts) { }, sandbox, inference, + insideSandbox, }; if (jsonOutput) { logger.info(JSON.stringify(statusData, null, 2)); @@ -34,6 +46,12 @@ async function cliStatus(opts) { logger.info("NemoClaw Status"); logger.info("==============="); logger.info(""); + if (insideSandbox) { + logger.info("Context: running inside an active OpenShell sandbox"); + logger.info(" Host sandbox state is not inspectable from inside the sandbox."); + logger.info(" Run 'openshell sandbox status' on the host for full details."); + logger.info(""); + } logger.info("Plugin State:"); if (state.lastAction) { logger.info(` Last action: ${state.lastAction}`); @@ -51,6 +69,11 @@ async function cliStatus(opts) { logger.info(" Status: running"); logger.info(` Uptime: ${sandbox.uptime ?? "unknown"}`); } + else if (sandbox.insideSandbox) { + logger.info(` Name: ${sandbox.name}`); + logger.info(" Status: active (inside sandbox)"); + logger.info(" Note: Cannot query host sandbox state from within the sandbox."); + } else { logger.info(" Status: not running"); } @@ -61,6 +84,10 @@ async function cliStatus(opts) { logger.info(` Model: ${inference.model ?? "unknown"}`); logger.info(` Endpoint: ${inference.endpoint ?? "unknown"}`); } + else if (inference.insideSandbox) { + logger.info(" Status: unable to query from inside sandbox"); + logger.info(" Note: Run 'openshell inference get' on the host to check."); + } else { logger.info(" Not configured"); } @@ -71,7 +98,10 @@ async function cliStatus(opts) { logger.info(" Run 'openclaw nemoclaw eject' to restore host installation."); } } -async function getSandboxStatus(sandboxName) { +async function getSandboxStatus(sandboxName, insideSandbox) { + if (insideSandbox) { + return { name: sandboxName, running: false, uptime: null, insideSandbox: true }; + } try { const { stdout } = await execAsync(`openshell sandbox status ${sandboxName} --json`, { timeout: 5000, @@ -81,13 +111,17 @@ async function getSandboxStatus(sandboxName) { name: sandboxName, running: parsed.state === "running", uptime: parsed.uptime ?? null, + insideSandbox: false, }; } catch { - return { name: sandboxName, running: false, uptime: null }; + return { name: sandboxName, running: false, uptime: null, insideSandbox: false }; } } -async function getInferenceStatus() { +async function getInferenceStatus(insideSandbox) { + if (insideSandbox) { + return { configured: false, provider: null, model: null, endpoint: null, insideSandbox: true }; + } try { const { stdout } = await execAsync("openshell inference get --json", { timeout: 5000, @@ -98,10 +132,11 @@ async function getInferenceStatus() { provider: parsed.provider ?? null, model: parsed.model ?? null, endpoint: parsed.endpoint ?? null, + insideSandbox: false, }; } catch { - return { configured: false, provider: null, model: null, endpoint: null }; + return { configured: false, provider: null, model: null, endpoint: null, insideSandbox: false }; } } //# sourceMappingURL=status.js.map \ No newline at end of file diff --git a/nemoclaw/dist/commands/status.js.map b/nemoclaw/dist/commands/status.js.map index d7fd185f6ed..6099b1afb3f 100644 --- a/nemoclaw/dist/commands/status.js.map +++ b/nemoclaw/dist/commands/status.js.map @@ -1 +1 @@ -{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"status.js","sourceRoot":"","sources":["../../src/commands/status.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/index.d.ts.map b/nemoclaw/dist/index.d.ts.map index d775191d583..be261bffe08 100644 --- a/nemoclaw/dist/index.d.ts.map +++ b/nemoclaw/dist/index.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/index.js b/nemoclaw/dist/index.js index 9268d4b7c60..058f2303085 100644 --- a/nemoclaw/dist/index.js +++ b/nemoclaw/dist/index.js @@ -7,6 +7,57 @@ exports.default = register; const cli_js_1 = require("./cli.js"); const slash_js_1 = require("./commands/slash.js"); const config_js_1 = require("./onboard/config.js"); +function activeModelEntries(onboardCfg) { + if (!onboardCfg?.model) { + return [ + { + id: "nvidia/nemotron-3-super-120b-a12b", + label: "Nemotron 3 Super 120B (March 2026)", + contextWindow: 131072, + maxOutput: 8192, + }, + { + id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", + label: "Nemotron Ultra 253B", + contextWindow: 131072, + maxOutput: 4096, + }, + { + id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", + label: "Nemotron Super 49B v1.5", + contextWindow: 131072, + maxOutput: 4096, + }, + { + id: "nvidia/nemotron-3-nano-30b-a3b", + label: "Nemotron 3 Nano 30B", + contextWindow: 131072, + maxOutput: 4096, + }, + ]; + } + return [ + { + id: `inference/${onboardCfg.model}`, + label: onboardCfg.model, + contextWindow: 131072, + maxOutput: 8192, + }, + ]; +} +function registeredProviderForConfig(onboardCfg, providerCredentialEnv) { + const authLabel = providerCredentialEnv === "NVIDIA_API_KEY" + ? `NVIDIA API Key (${providerCredentialEnv})` + : `OpenAI API Key (${providerCredentialEnv})`; + return { + id: "inference", + label: "Managed Inference Route", + aliases: ["inference-local", "nemoclaw"], + envVars: [providerCredentialEnv], + models: { chat: activeModelEntries(onboardCfg) }, + auth: [{ type: "bearer", envVar: providerCredentialEnv, headerName: "Authorization", label: authLabel }], + }; +} const DEFAULT_PLUGIN_CONFIG = { blueprintVersion: "latest", blueprintRegistry: "ghcr.io/nvidia/nemoclaw-blueprint", @@ -48,59 +99,16 @@ function register(api) { // 3. Register nvidia-nim provider — use onboard config if available const onboardCfg = (0, config_js_1.loadOnboardConfig)(); const providerCredentialEnv = onboardCfg?.credentialEnv ?? "NVIDIA_API_KEY"; - const providerLabel = onboardCfg - ? `NVIDIA NIM (${onboardCfg.endpointType}${onboardCfg.ncpPartner ? ` - ${onboardCfg.ncpPartner}` : ""})` - : "NVIDIA NIM (build.nvidia.com)"; - api.registerProvider({ - id: "nvidia-nim", - label: providerLabel, - docsPath: "https://build.nvidia.com/docs", - aliases: ["nvidia", "nim"], - envVars: [providerCredentialEnv], - models: { - chat: [ - { - id: "nvidia/nemotron-3-super-120b-a12b", - label: "Nemotron 3 Super 120B (March 2026)", - contextWindow: 131072, - maxOutput: 8192, - }, - { - id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", - label: "Nemotron Ultra 253B", - contextWindow: 131072, - maxOutput: 4096, - }, - { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - label: "Nemotron Super 49B v1.5", - contextWindow: 131072, - maxOutput: 4096, - }, - { - id: "nvidia/nemotron-3-nano-30b-a3b", - label: "Nemotron 3 Nano 30B", - contextWindow: 131072, - maxOutput: 4096, - }, - ], - }, - auth: [ - { - type: "bearer", - envVar: providerCredentialEnv, - headerName: "Authorization", - label: `NVIDIA API Key (${providerCredentialEnv})`, - }, - ], - }); - const bannerEndpoint = onboardCfg?.endpointType ?? "build.nvidia.com"; + api.registerProvider(registeredProviderForConfig(onboardCfg, providerCredentialEnv)); + const bannerEndpoint = onboardCfg ? (0, config_js_1.describeOnboardEndpoint)(onboardCfg) : "build.nvidia.com"; + const bannerProvider = onboardCfg ? (0, config_js_1.describeOnboardProvider)(onboardCfg) : "NVIDIA Cloud API"; const bannerModel = onboardCfg?.model ?? "nvidia/nemotron-3-super-120b-a12b"; api.logger.info(""); api.logger.info(" ┌─────────────────────────────────────────────────────┐"); api.logger.info(" │ NemoClaw registered │"); api.logger.info(" │ │"); api.logger.info(` │ Endpoint: ${bannerEndpoint.padEnd(40)}│`); + api.logger.info(` │ Provider: ${bannerProvider.padEnd(40)}│`); api.logger.info(` │ Model: ${bannerModel.padEnd(40)}│`); api.logger.info(" │ Commands: openclaw nemoclaw │"); api.logger.info(" └─────────────────────────────────────────────────────┘"); diff --git a/nemoclaw/dist/index.js.map b/nemoclaw/dist/index.js.map index e45a1adab07..1c22a36ec3e 100644 --- a/nemoclaw/dist/index.js.map +++ b/nemoclaw/dist/index.js.map @@ -1 +1 @@ -{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file +{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"} \ No newline at end of file diff --git a/nemoclaw/dist/onboard/config.d.ts b/nemoclaw/dist/onboard/config.d.ts index 7171fc5412f..a2b6e4a8872 100644 --- a/nemoclaw/dist/onboard/config.d.ts +++ b/nemoclaw/dist/onboard/config.d.ts @@ -6,8 +6,12 @@ export interface NemoClawOnboardConfig { model: string; profile: string; credentialEnv: string; + provider?: string; + providerLabel?: string; onboardedAt: string; } +export declare function describeOnboardEndpoint(config: NemoClawOnboardConfig): string; +export declare function describeOnboardProvider(config: NemoClawOnboardConfig): string; export declare function loadOnboardConfig(): NemoClawOnboardConfig | null; export declare function saveOnboardConfig(config: NemoClawOnboardConfig): void; export declare function clearOnboardConfig(): void; diff --git a/nemoclaw/dist/onboard/config.d.ts.map b/nemoclaw/dist/onboard/config.d.ts.map index bf764c5ac9e..eef4c615c82 100644 --- a/nemoclaw/dist/onboard/config.d.ts.map +++ b/nemoclaw/dist/onboard/config.d.ts.map @@ -1 +1 @@ -{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/onboard/config.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,WAAW,EAAE,MAAM,CAAC;CACrB;AAgBD,wBAAgB,iBAAiB,IAAI,qBAAqB,GAAG,IAAI,CAOhE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAGrE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"} \ No newline at end of file +{"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../../src/onboard/config.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,KAAK,GAAG,WAAW,GAAG,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;AAExF,MAAM,WAAW,qBAAqB;IACpC,YAAY,EAAE,YAAY,CAAC;IAC3B,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;CACrB;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAM7E;AAED,wBAAgB,uBAAuB,CAAC,MAAM,EAAE,qBAAqB,GAAG,MAAM,CAqB7E;AAgBD,wBAAgB,iBAAiB,IAAI,qBAAqB,GAAG,IAAI,CAOhE;AAED,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,qBAAqB,GAAG,IAAI,CAGrE;AAED,wBAAgB,kBAAkB,IAAI,IAAI,CAKzC"} \ No newline at end of file diff --git a/nemoclaw/dist/onboard/config.js b/nemoclaw/dist/onboard/config.js index 8a313a45c30..68f380434b2 100644 --- a/nemoclaw/dist/onboard/config.js +++ b/nemoclaw/dist/onboard/config.js @@ -2,12 +2,41 @@ // SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-License-Identifier: Apache-2.0 Object.defineProperty(exports, "__esModule", { value: true }); +exports.describeOnboardEndpoint = describeOnboardEndpoint; +exports.describeOnboardProvider = describeOnboardProvider; exports.loadOnboardConfig = loadOnboardConfig; exports.saveOnboardConfig = saveOnboardConfig; exports.clearOnboardConfig = clearOnboardConfig; const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); const CONFIG_DIR = (0, node_path_1.join)(process.env.HOME ?? "/tmp", ".nemoclaw"); +function describeOnboardEndpoint(config) { + if (config.endpointUrl === "https://inference.local/v1") { + return "Managed Inference Route (inference.local)"; + } + return `${config.endpointType} (${config.endpointUrl})`; +} +function describeOnboardProvider(config) { + if (config.providerLabel) { + return config.providerLabel; + } + switch (config.endpointType) { + case "build": + return "NVIDIA Cloud API"; + case "ollama": + return "Local Ollama"; + case "vllm": + return "Local vLLM"; + case "nim-local": + return "Local NIM"; + case "ncp": + return "NVIDIA Cloud Partner"; + case "custom": + return "Managed Inference Route"; + default: + return "Unknown"; + } +} let configDirCreated = false; function ensureConfigDir() { if (configDirCreated) diff --git a/nemoclaw/dist/onboard/config.js.map b/nemoclaw/dist/onboard/config.js.map index 5328a59b890..5df39df24c4 100644 --- a/nemoclaw/dist/onboard/config.js.map +++ b/nemoclaw/dist/onboard/config.js.map @@ -1 +1 @@ -{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/onboard/config.ts"],"names":[],"mappings":";AAAA,mGAAmG;AACnG,sCAAsC;;AAiCtC,8CAOC;AAED,8CAGC;AAED,gDAKC;AAlDD,qCAAyF;AACzF,yCAAiC;AAEjC,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,WAAW,CAAC,CAAC;AAcjE,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAS,eAAe;IACtB,IAAI,gBAAgB;QAAE,OAAO;IAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,IAAA,mBAAS,EAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,gBAAgB,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAA,gBAAI,EAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,iBAAiB;IAC/B,eAAe,EAAE,CAAC;IAClB,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAA0B,CAAC;AAC1E,CAAC;AAED,SAAgB,iBAAiB,CAAC,MAA6B;IAC7D,eAAe,EAAE,CAAC;IAClB,IAAA,uBAAa,EAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAgB,kBAAkB;IAChC,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACrB,IAAA,oBAAU,EAAC,IAAI,CAAC,CAAC;IACnB,CAAC;AACH,CAAC"} \ No newline at end of file +{"version":3,"file":"config.js","sourceRoot":"","sources":["../../src/onboard/config.ts"],"names":[],"mappings":";AAAA,mGAAmG;AACnG,sCAAsC;;AAqBtC,0DAMC;AAED,0DAqBC;AAgBD,8CAOC;AAED,8CAGC;AAED,gDAKC;AAnFD,qCAAyF;AACzF,yCAAiC;AAEjC,MAAM,UAAU,GAAG,IAAA,gBAAI,EAAC,OAAO,CAAC,GAAG,CAAC,IAAI,IAAI,MAAM,EAAE,WAAW,CAAC,CAAC;AAgBjE,SAAgB,uBAAuB,CAAC,MAA6B;IACnE,IAAI,MAAM,CAAC,WAAW,KAAK,4BAA4B,EAAE,CAAC;QACxD,OAAO,2CAA2C,CAAC;IACrD,CAAC;IAED,OAAO,GAAG,MAAM,CAAC,YAAY,KAAK,MAAM,CAAC,WAAW,GAAG,CAAC;AAC1D,CAAC;AAED,SAAgB,uBAAuB,CAAC,MAA6B;IACnE,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACzB,OAAO,MAAM,CAAC,aAAa,CAAC;IAC9B,CAAC;IAED,QAAQ,MAAM,CAAC,YAAY,EAAE,CAAC;QAC5B,KAAK,OAAO;YACV,OAAO,kBAAkB,CAAC;QAC5B,KAAK,QAAQ;YACX,OAAO,cAAc,CAAC;QACxB,KAAK,MAAM;YACT,OAAO,YAAY,CAAC;QACtB,KAAK,WAAW;YACd,OAAO,WAAW,CAAC;QACrB,KAAK,KAAK;YACR,OAAO,sBAAsB,CAAC;QAChC,KAAK,QAAQ;YACX,OAAO,yBAAyB,CAAC;QACnC;YACE,OAAO,SAAS,CAAC;IACrB,CAAC;AACH,CAAC;AAED,IAAI,gBAAgB,GAAG,KAAK,CAAC;AAE7B,SAAS,eAAe;IACtB,IAAI,gBAAgB;QAAE,OAAO;IAC7B,IAAI,CAAC,IAAA,oBAAU,EAAC,UAAU,CAAC,EAAE,CAAC;QAC5B,IAAA,mBAAS,EAAC,UAAU,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAC7C,CAAC;IACD,gBAAgB,GAAG,IAAI,CAAC;AAC1B,CAAC;AAED,SAAS,UAAU;IACjB,OAAO,IAAA,gBAAI,EAAC,UAAU,EAAE,aAAa,CAAC,CAAC;AACzC,CAAC;AAED,SAAgB,iBAAiB;IAC/B,eAAe,EAAE,CAAC;IAClB,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,CAAC,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACtB,OAAO,IAAI,CAAC;IACd,CAAC;IACD,OAAO,IAAI,CAAC,KAAK,CAAC,IAAA,sBAAY,EAAC,IAAI,EAAE,OAAO,CAAC,CAA0B,CAAC;AAC1E,CAAC;AAED,SAAgB,iBAAiB,CAAC,MAA6B;IAC7D,eAAe,EAAE,CAAC;IAClB,IAAA,uBAAa,EAAC,UAAU,EAAE,EAAE,IAAI,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,CAAC;AAC/D,CAAC;AAED,SAAgB,kBAAkB;IAChC,MAAM,IAAI,GAAG,UAAU,EAAE,CAAC;IAC1B,IAAI,IAAA,oBAAU,EAAC,IAAI,CAAC,EAAE,CAAC;QACrB,IAAA,oBAAU,EAAC,IAAI,CAAC,CAAC;IACnB,CAAC;AACH,CAAC"} \ No newline at end of file diff --git a/nemoclaw/package-lock.json b/nemoclaw/package-lock.json index 2269dd89779..0d67f30b5bc 100644 --- a/nemoclaw/package-lock.json +++ b/nemoclaw/package-lock.json @@ -21,12 +21,47 @@ "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", - "typescript": "^5.4.0" + "typescript": "^5.4.0", + "vitest": "^4.1.0" }, "engines": { "node": ">=20.0.0" } }, + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.9.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", @@ -280,6 +315,348 @@ "node": ">=18.0.0" } }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/runtime": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/runtime/-/runtime-0.115.0.tgz", + "integrity": "sha512-Rg8Wlt5dCbXhQnsXPrkOjL1DTSvXLgb2R/KYfnf1/K+R0k6UMLEmbQXPM+kwrWqSmWA2t0B1EtHy2/3zikQpvQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.115.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.115.0.tgz", + "integrity": "sha512-4n91DKnebUS4yjUHl2g3/b2T+IUdCfmoZGhmwsovZCDaJSs+QkVAM+0AqqTxHSsHfeiMuueT75cZaZcT/m0pSw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-lcJL0bN5hpgJfSIz/8PIf02irmyL43P+j1pTCfbD1DbLkmGRuFIA4DD3B3ZOvGqG0XiVvRznbKtN0COQVaKUTg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-J7Zk3kLYFsLtuH6U+F4pS2sYVzac0qkjcO5QxHS7OS7yZu2LRs+IXo+uvJ/mvpyUljDJ3LROZPoQfgBIpCMhdQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-iwtmmghy8nhfRGeNAIltcNXzD0QMNaaA5U/NyZc1Ia4bxrzFByNMDoppoC+hl7cDiUq5/1CnFthpT9n+UtfFyg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.9.tgz", + "integrity": "sha512-DLFYI78SCiZr5VvdEplsVC2Vx53lnA4/Ga5C65iyldMVaErr86aiqCoNBLl92PXPfDtUYjUh+xFFor40ueNs4Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.9.tgz", + "integrity": "sha512-CsjTmTwd0Hri6iTw/DRMK7kOZ7FwAkrO4h8YWKoX/kcj833e4coqo2wzIFywtch/8Eb5enQ/lwLM7w6JX1W5RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-2x9O2JbSPxpxMDhP9Z74mahAStibTlrBMW0520+epJH5sac7/LwZW5Bmg/E6CXuEF53JJFW509uP+lSedaUNxg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-JA1QRW31ogheAIRhIg9tjMfsYbglXXYGNPLdPEYrwFxdbkQCAzvpSCSHCDWNl4hTtrol8WeboCSEpjdZK8qrCg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-aOKU9dJheda8Kj8Y3w9gnt9QFOO+qKPAl8SWd7JPHP+Cu0EuDAE5wokQubLzIDQWg2myXq2XhTpOVS07qqvT+w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-OalO94fqj7IWRn3VdXWty75jC5dk4C197AWEuMhIpvVv2lw9fiPhud0+bW2ctCxb3YoBZor71QHbY+9/WToadA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.9.tgz", + "integrity": "sha512-cVEl1vZtBsBZna3YMjGXNvnYYrOJ7RzuWvZU0ffvJUexWkukMaDuGhUXn0rjnV0ptzGVkvc+vW9Yqy6h8YX4pg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.9.tgz", + "integrity": "sha512-UzYnKCIIc4heAKgI4PZ3dfBGUZefGCJ1TPDuLHoCzgrMYPb5Rv6TLFuYtyM4rWyHM7hymNdsg5ik2C+UD9VDbA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.9.tgz", + "integrity": "sha512-+6zoiF+RRyf5cdlFQP7nm58mq7+/2PFaY2DNQeD4B87N36JzfF/l9mdBkkmTvSYcYPE8tMh/o3cRlsx1ldLfog==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.9.tgz", + "integrity": "sha512-rgFN6sA/dyebil3YTlL2evvi/M+ivhfnyxec7AccTpRPccno/rPoNlqybEZQBkcbZu8Hy+eqNJCqfBR8P7Pg8g==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-lHVNUG/8nlF1IQk1C0Ci574qKYyty2goMiPlRqkC5R+3LkXDkL5Dhx8ytbxq35m+pkHVIvIxviD+TWLdfeuadA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.9.tgz", + "integrity": "sha512-G0oA4+w1iY5AGi5HcDTxWsoxF509hrFIPB2rduV5aDqS9FtDg1CAfa7V34qImbjfhIcA8C+RekocJZA96EarwQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.9.tgz", + "integrity": "sha512-w6oiRWgEBl04QkFZgmW+jnU1EC9b57Oihi2ot3HNWIQRqgHp5PnYDia5iZ5FF7rpa4EQdiqMDXjlqKGXBhsoXw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -339,7 +716,6 @@ "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@typescript-eslint/scope-manager": "8.57.0", "@typescript-eslint/types": "8.57.0", @@ -538,13 +914,125 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, "node_modules/acorn": { "version": "8.16.0", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", "dev": true, "license": "MIT", - "peer": true, "bin": { "acorn": "bin/acorn" }, @@ -602,6 +1090,16 @@ "dev": true, "license": "Python-2.0" }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -642,6 +1140,16 @@ "node": ">=6" } }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -704,6 +1212,13 @@ "dev": true, "license": "MIT" }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -744,6 +1259,23 @@ "dev": true, "license": "MIT" }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -763,7 +1295,6 @@ "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", "dev": true, "license": "MIT", - "peer": true, "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", @@ -978,6 +1509,16 @@ "node": ">=4.0" } }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -988,6 +1529,16 @@ "node": ">=0.10.0" } }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -1078,6 +1629,21 @@ "dev": true, "license": "ISC" }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -1251,6 +1817,267 @@ "node": ">= 0.8.0" } }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -1274,6 +2101,16 @@ "dev": true, "license": "MIT" }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, "node_modules/minimatch": { "version": "10.2.4", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", @@ -1318,6 +2155,25 @@ "dev": true, "license": "MIT" }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, "node_modules/natural-compare": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", @@ -1325,6 +2181,17 @@ "dev": true, "license": "MIT" }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -1408,13 +2275,26 @@ "node": ">=8" } }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, "node_modules/picomatch": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, "license": "MIT", - "peer": true, "engines": { "node": ">=12" }, @@ -1422,6 +2302,35 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -1468,6 +2377,40 @@ "node": ">=4" } }, + "node_modules/rolldown": { + "version": "1.0.0-rc.9", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.9.tgz", + "integrity": "sha512-9EbgWge7ZH+yqb4d2EnELAntgPTWbfL8ajiTW+SyhJEC4qhBbkCKbqFV4Ge4zmu5ziQuVbWxb/XwLZ+RIO7E8Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.115.0", + "@rolldown/pluginutils": "1.0.0-rc.9" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.9", + "@rolldown/binding-darwin-x64": "1.0.0-rc.9", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.9", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.9", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.9", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.9", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.9", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.9", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.9", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.9" + } + }, "node_modules/semver": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", @@ -1504,6 +2447,37 @@ "node": ">=8" } }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -1546,6 +2520,23 @@ "node": ">=18" } }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.15", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", @@ -1563,6 +2554,16 @@ "url": "https://github.com/sponsors/SuperchupuDev" } }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, "node_modules/ts-api-utils": { "version": "2.4.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", @@ -1576,6 +2577,14 @@ "typescript": ">=4.8.4" } }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -1595,7 +2604,6 @@ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", "dev": true, "license": "Apache-2.0", - "peer": true, "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -1621,6 +2629,167 @@ "punycode": "^2.1.0" } }, + "node_modules/vite": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.0.tgz", + "integrity": "sha512-fPGaRNj9Zytaf8LEiBhY7Z6ijnFKdzU/+mL8EFBaKr7Vw1/FWcTBAMW0wLPJAGMPX38ZPVCVgLceWiEqeoqL2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/runtime": "0.115.0", + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.9", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.0.0-alpha.31", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -1637,6 +2806,23 @@ "node": ">= 8" } }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/word-wrap": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", diff --git a/nemoclaw/package.json b/nemoclaw/package.json index 56f66a911f2..b8b6489ae98 100644 --- a/nemoclaw/package.json +++ b/nemoclaw/package.json @@ -17,6 +17,8 @@ "lint:fix": "eslint 'src/**/*.ts' --fix", "format": "prettier --write 'src/**/*.ts'", "format:check": "prettier --check 'src/**/*.ts'", + "test": "vitest run", + "test:watch": "vitest", "check": "npm run lint && npm run format:check && tsc --noEmit", "clean": "rm -rf dist/" }, @@ -33,7 +35,8 @@ "eslint": "^9.39.4", "eslint-config-prettier": "^10.1.8", "prettier": "^3.8.1", - "typescript": "^5.4.0" + "typescript": "^5.4.0", + "vitest": "^4.1.0" }, "engines": { "node": ">=20.0.0" diff --git a/nemoclaw/src/cli.ts b/nemoclaw/src/cli.ts index 35694f31ab4..336dc2c3475 100644 --- a/nemoclaw/src/cli.ts +++ b/nemoclaw/src/cli.ts @@ -110,7 +110,7 @@ export function registerCliCommands(ctx: PluginCliContext, api: OpenClawPluginAp .command("onboard") .description("Interactive setup: configure inference endpoint, credential, and model") .option("--api-key ", "API key for endpoints that require one (skips prompt)") - .option("--endpoint ", "Endpoint type: build, ncp, nim-local, vllm, ollama, custom (local options are experimental)") + .option("--endpoint ", "Endpoint type: build, ncp, ollama, nim-local, vllm, custom (nim-local and vllm are experimental)") .option("--ncp-partner ", "NCP partner name (when endpoint is ncp)") .option("--endpoint-url ", "Endpoint URL (for ncp, nim-local, ollama, or custom)") .option("--model ", "Model ID to use") diff --git a/nemoclaw/src/commands/onboard.ts b/nemoclaw/src/commands/onboard.ts index b5b5f4731ea..72fb9fcdd4c 100644 --- a/nemoclaw/src/commands/onboard.ts +++ b/nemoclaw/src/commands/onboard.ts @@ -4,6 +4,8 @@ import { execFileSync, execSync } from "node:child_process"; import type { PluginLogger, NemoClawConfig } from "../index.js"; import { + describeOnboardEndpoint, + describeOnboardProvider, loadOnboardConfig, saveOnboardConfig, type EndpointType, @@ -23,7 +25,7 @@ export interface OnboardOptions { } const ENDPOINT_TYPES: EndpointType[] = ["build", "ncp", "nim-local", "vllm", "ollama", "custom"]; -const SUPPORTED_ENDPOINT_TYPES: EndpointType[] = ["build", "ncp"]; +const SUPPORTED_ENDPOINT_TYPES: EndpointType[] = ["build", "ncp", "ollama"]; function isExperimentalEnabled(): boolean { return process.env.NEMOCLAW_EXPERIMENTAL === "1"; @@ -34,10 +36,13 @@ const HOST_GATEWAY_URL = "http://host.openshell.internal"; const DEFAULT_MODELS = [ { id: "nvidia/nemotron-3-super-120b-a12b", label: "Nemotron 3 Super 120B" }, - { id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", label: "Nemotron Ultra 253B" }, - { id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", label: "Nemotron Super 49B v1.5" }, - { id: "nvidia/nemotron-3-nano-30b-a3b", label: "Nemotron 3 Nano 30B" }, + { id: "moonshotai/kimi-k2.5", label: "Kimi K2.5" }, + { id: "z-ai/glm5", label: "GLM-5" }, + { id: "minimaxai/minimax-m2.5", label: "MiniMax M2.5" }, + { id: "qwen/qwen3.5-397b-a17b", label: "Qwen3.5 397B A17B" }, + { id: "openai/gpt-oss-120b", label: "GPT-OSS 120B" }, ]; +const DEFAULT_OLLAMA_MODEL = "nemotron-3-nano:30b"; function resolveProfile(endpointType: EndpointType): string { switch (endpointType) { @@ -120,6 +125,32 @@ function detectOllama(): { installed: boolean; running: boolean } { return { installed, running }; } +function parseOllamaList(output: string): string[] { + return output + .split(/\r?\n/) + .map((line) => line.trim()) + .filter(Boolean) + .filter((line) => !/^NAME\s+/i.test(line)) + .map((line) => line.split(/\s{2,}/)[0]) + .filter(Boolean); +} + +function getOllamaModelOptions(): string[] { + try { + const output = execSync("ollama list", { encoding: "utf-8", shell: "/bin/bash" }); + const parsed = parseOllamaList(output); + if (parsed.length > 0) { + return parsed; + } + } catch {} + return [DEFAULT_OLLAMA_MODEL]; +} + +function getDefaultOllamaModel(): string { + const models = getOllamaModelOptions(); + return models.includes(DEFAULT_OLLAMA_MODEL) ? DEFAULT_OLLAMA_MODEL : models[0]; +} + function testCommand(command: string): boolean { try { execSync(command, { encoding: "utf-8", stdio: "ignore", shell: "/bin/bash" }); @@ -130,7 +161,8 @@ function testCommand(command: string): boolean { } function showConfig(config: NemoClawOnboardConfig, logger: PluginLogger): void { - logger.info(` Endpoint: ${config.endpointType} (${config.endpointUrl})`); + logger.info(` Endpoint: ${describeOnboardEndpoint(config)}`); + logger.info(` Provider: ${describeOnboardProvider(config)}`); if (config.ncpPartner) { logger.info(` NCP Partner: ${config.ncpPartner}`); } @@ -143,7 +175,7 @@ function showConfig(config: NemoClawOnboardConfig, logger: PluginLogger): void { async function promptEndpoint( ollama: { installed: boolean; running: boolean }, ): Promise { - return (await promptSelect("Select your inference endpoint:", [ + const options = [ { label: "NVIDIA Build (build.nvidia.com)", value: "build", @@ -154,22 +186,34 @@ async function promptEndpoint( value: "ncp", hint: "dedicated capacity, SLA-backed", }, - { - label: "Self-hosted NIM [experimental]", - value: "nim-local", - hint: "experimental — your own NIM container deployment", - }, - { - label: "Local vLLM [experimental]", - value: "vllm", - hint: "experimental — local development", - }, - { - label: "Local Ollama [experimental]", - value: "ollama", - hint: `experimental — ${ollama.installed ? "installed locally" : "localhost:11434"}`, - }, - ])) as EndpointType; + ]; + + options.push({ + label: "Local Ollama", + value: "ollama", + hint: ollama.running + ? "detected on localhost:11434" + : ollama.installed + ? "installed locally" + : "localhost:11434", + }); + + if (isExperimentalEnabled()) { + options.push( + { + label: "Self-hosted NIM [experimental]", + value: "nim-local", + hint: "experimental — your own NIM container deployment", + }, + { + label: "Local vLLM [experimental]", + value: "vllm", + hint: "experimental — local development", + }, + ); + } + + return (await promptSelect("Select your inference endpoint:", options)) as EndpointType; } function execOpenShell(args: string[]): string { @@ -221,12 +265,11 @@ export async function cliOnboard(opts: OnboardOptions): Promise { endpointType = ep; } else { const ollama = detectOllama(); - if (ollama.running && isExperimentalEnabled()) { - logger.info("Detected Ollama on localhost:11434. Using it for onboarding."); - endpointType = "ollama"; - } else { - endpointType = await promptEndpoint(ollama); + if (ollama.running) { + logger.info("Detected local inference option: Ollama."); + logger.info("Select it explicitly if you want to use it."); } + endpointType = await promptEndpoint(ollama); } // Step 2: Endpoint URL resolution @@ -324,24 +367,55 @@ export async function cliOnboard(opts: OnboardOptions): Promise { if (opts.model) { model = opts.model; } else { - // Build model options: prefer Nemotron models from the endpoint, fall back to defaults - const nemotronModels = validation.models.filter((m) => m.includes("nemotron")); + const discoveredModelOptions = + endpointType === "ollama" + ? getOllamaModelOptions().map((id) => ({ label: id, value: id })) + : validation.models.map((id) => ({ label: id, value: id })); + const curatedCloudOptions = + endpointType === "build" || endpointType === "ncp" + ? DEFAULT_MODELS.filter((option) => validation.models.includes(option.id)).map((option) => ({ + label: `${option.label} (${option.id})`, + value: option.id, + })) + : []; + const defaultIndex = + endpointType === "ollama" + ? Math.max( + 0, + discoveredModelOptions.findIndex((option) => option.value === getDefaultOllamaModel()), + ) + : 0; const modelOptions = - nemotronModels.length > 0 - ? nemotronModels.map((id) => ({ label: id, value: id })) - : DEFAULT_MODELS.map((m) => ({ label: `${m.label} (${m.id})`, value: m.id })); + curatedCloudOptions.length > 0 + ? curatedCloudOptions + : discoveredModelOptions.length > 0 + ? discoveredModelOptions + : DEFAULT_MODELS.map((m) => ({ label: `${m.label} (${m.id})`, value: m.id })); - model = await promptSelect("Select your primary model:", modelOptions); + model = await promptSelect("Select your primary model:", modelOptions, defaultIndex); } // Step 6: Resolve profile const profile = resolveProfile(endpointType); const providerName = resolveProviderName(endpointType); + const summaryConfig: NemoClawOnboardConfig = { + endpointType, + endpointUrl, + ncpPartner, + model, + profile, + credentialEnv, + provider: providerName, + providerLabel: undefined, + onboardedAt: "", + }; + summaryConfig.providerLabel = describeOnboardProvider(summaryConfig); // Step 7: Confirmation logger.info(""); logger.info("Configuration summary:"); - logger.info(` Endpoint: ${endpointType} (${endpointUrl})`); + logger.info(` Endpoint: ${describeOnboardEndpoint(summaryConfig)}`); + logger.info(` Provider: ${summaryConfig.providerLabel}`); if (ncpPartner) { logger.info(` NCP Partner: ${ncpPartner}`); } @@ -429,6 +503,8 @@ export async function cliOnboard(opts: OnboardOptions): Promise { model, profile, credentialEnv, + provider: providerName, + providerLabel: summaryConfig.providerLabel, onboardedAt: new Date().toISOString(), }); @@ -436,7 +512,8 @@ export async function cliOnboard(opts: OnboardOptions): Promise { logger.info(""); logger.info("Onboarding complete!"); logger.info(""); - logger.info(` Endpoint: ${endpointUrl}`); + logger.info(` Endpoint: ${describeOnboardEndpoint(summaryConfig)}`); + logger.info(` Provider: ${summaryConfig.providerLabel}`); logger.info(` Model: ${model}`); logger.info(` Credential: $${credentialEnv}`); logger.info(""); diff --git a/nemoclaw/src/commands/slash.ts b/nemoclaw/src/commands/slash.ts index f0292eabfb2..bd9afa295d5 100644 --- a/nemoclaw/src/commands/slash.ts +++ b/nemoclaw/src/commands/slash.ts @@ -12,7 +12,11 @@ import type { PluginCommandContext, PluginCommandResult, OpenClawPluginApi } from "../index.js"; import { loadState } from "../blueprint/state.js"; -import { loadOnboardConfig } from "../onboard/config.js"; +import { + describeOnboardEndpoint, + describeOnboardProvider, + loadOnboardConfig, +} from "../onboard/config.js"; export function handleSlashCommand( ctx: PluginCommandContext, @@ -87,7 +91,8 @@ function slashOnboard(): PluginCommandResult { text: [ "**NemoClaw Onboard Status**", "", - `Endpoint: ${config.endpointType} (${config.endpointUrl})`, + `Endpoint: ${describeOnboardEndpoint(config)}`, + `Provider: ${describeOnboardProvider(config)}`, config.ncpPartner ? `NCP Partner: ${config.ncpPartner}` : null, `Model: ${config.model}`, `Credential: $${config.credentialEnv}`, diff --git a/nemoclaw/src/commands/status.test.ts b/nemoclaw/src/commands/status.test.ts new file mode 100644 index 00000000000..78a55b47aa1 --- /dev/null +++ b/nemoclaw/src/commands/status.test.ts @@ -0,0 +1,456 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +import { describe, it, expect, vi, beforeEach } from "vitest"; +import type { NemoClawState } from "../blueprint/state.js"; +import type { PluginLogger, NemoClawConfig } from "../index.js"; + +// --------------------------------------------------------------------------- +// Mocks +// --------------------------------------------------------------------------- + +// Mock node:fs — controls isInsideSandbox() detection +vi.mock("node:fs", () => ({ + existsSync: vi.fn(() => false), +})); + +// Mock node:child_process — controls openshell command results +vi.mock("node:child_process", () => ({ + exec: vi.fn(), +})); + +// Mock state loader — controls plugin state +vi.mock("../blueprint/state.js", () => ({ + loadState: vi.fn(), +})); + +// Import after mocks are set up +const { existsSync } = await import("node:fs"); +const { exec } = await import("node:child_process"); +const { loadState } = await import("../blueprint/state.js"); +const { cliStatus } = await import("./status.js"); + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function blankState(): NemoClawState { + return { + lastRunId: null, + lastAction: null, + blueprintVersion: null, + sandboxName: null, + migrationSnapshot: null, + hostBackupPath: null, + createdAt: null, + updatedAt: new Date().toISOString(), + }; +} + +function populatedState(): NemoClawState { + return { + lastRunId: "run-a1b2c3d4", + lastAction: "migrate", + blueprintVersion: "0.1.0", + sandboxName: "openclaw", + migrationSnapshot: "/root/.nemoclaw/snapshots/pre-migrate.tar.gz", + hostBackupPath: "/root/.nemoclaw/backups/host-backup", + createdAt: "2026-03-15T10:30:00.000Z", + updatedAt: "2026-03-15T10:32:45.000Z", + }; +} + +const defaultConfig: NemoClawConfig = { + blueprintVersion: "latest", + blueprintRegistry: "ghcr.io/nvidia/nemoclaw-blueprint", + sandboxName: "openclaw", + inferenceProvider: "nvidia", +}; + +/** Create a logger that captures all info() calls into an array. */ +function captureLogger(): { lines: string[]; logger: PluginLogger } { + const lines: string[] = []; + return { + lines, + logger: { + info: (msg: string) => lines.push(msg), + warn: (msg: string) => lines.push(`WARN: ${msg}`), + error: (msg: string) => lines.push(`ERROR: ${msg}`), + debug: (_msg: string) => {}, + }, + }; +} + +/** + * Make the exec mock resolve with the given stdout, or reject if error is set. + * Routes by command substring so sandbox and inference calls can differ. + */ +function mockExec(responses: Record): void { + vi.mocked(exec).mockImplementation((( + cmd: string, + _opts: unknown, + callback?: (err: Error | null, result: { stdout: string; stderr: string }) => void, + ) => { + // promisify(exec)(cmd, opts) calls exec(cmd, opts, callback) + for (const [substring, response] of Object.entries(responses)) { + if (cmd.includes(substring)) { + if (response instanceof Error) { + callback?.(response, { stdout: "", stderr: response.message }); + } else { + callback?.(null, { stdout: response, stderr: "" }); + } + return; + } + } + // Default: command not found + callback?.(new Error(`command not found: ${cmd}`), { stdout: "", stderr: "" }); + }) as typeof exec); +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +beforeEach(() => { + vi.resetAllMocks(); + vi.mocked(existsSync).mockReturnValue(false); + vi.mocked(loadState).mockReturnValue(blankState()); + mockExec({}); +}); + +describe("cliStatus", () => { + // ========================================================================= + // Scenario 1: Host — no openshell, blank state + // ========================================================================= + describe("host — no openshell, blank state", () => { + it("shows 'not running' and 'Not configured' in text output", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Status: not running"); + expect(output).toContain("Not configured"); + expect(output).not.toContain("inside sandbox"); + expect(output).not.toContain("active (inside sandbox)"); + }); + + it("includes insideSandbox: false in JSON output", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(false); + expect(data.sandbox.insideSandbox).toBe(false); + expect(data.sandbox.running).toBe(false); + expect(data.inference.insideSandbox).toBe(false); + expect(data.inference.configured).toBe(false); + }); + + it("shows 'No operations have been performed yet'", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + expect(lines.join("\n")).toContain("No operations have been performed yet."); + }); + }); + + // ========================================================================= + // Scenario 2: Host — sandbox running, inference configured + // ========================================================================= + describe("host — sandbox running, inference configured", () => { + beforeEach(() => { + mockExec({ + "sandbox status": JSON.stringify({ state: "running", uptime: "2h 14m" }), + "inference get": JSON.stringify({ + provider: "nvidia", + model: "nemotron-3-super-120b", + endpoint: "https://integrate.api.nvidia.com", + }), + }); + }); + + it("shows running sandbox with uptime in text output", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Status: running"); + expect(output).toContain("Uptime: 2h 14m"); + expect(output).toContain("Name: openclaw"); + expect(output).not.toContain("inside sandbox"); + }); + + it("shows configured inference in text output", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Provider: nvidia"); + expect(output).toContain("Model: nemotron-3-super-120b"); + expect(output).toContain("Endpoint: https://integrate.api.nvidia.com"); + }); + + it("returns correct JSON structure", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(false); + expect(data.sandbox.running).toBe(true); + expect(data.sandbox.uptime).toBe("2h 14m"); + expect(data.sandbox.insideSandbox).toBe(false); + expect(data.inference.configured).toBe(true); + expect(data.inference.provider).toBe("nvidia"); + expect(data.inference.insideSandbox).toBe(false); + }); + }); + + // ========================================================================= + // Scenario 3: Host — sandbox running, no inference + // ========================================================================= + describe("host — sandbox running, no inference", () => { + beforeEach(() => { + mockExec({ + "sandbox status": JSON.stringify({ state: "running", uptime: "45m 12s" }), + "inference get": new Error("no inference configured"), + }); + }); + + it("shows running sandbox but 'Not configured' inference", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Status: running"); + expect(output).toContain("Not configured"); + expect(output).not.toContain("unable to query"); + }); + + it("JSON shows sandbox running, inference not configured, not inside sandbox", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.sandbox.running).toBe(true); + expect(data.inference.configured).toBe(false); + expect(data.inference.insideSandbox).toBe(false); + }); + }); + + // ========================================================================= + // Scenario 4: Inside sandbox — core bug fix + // ========================================================================= + describe("inside sandbox — core bug fix", () => { + beforeEach(() => { + vi.mocked(existsSync).mockImplementation((p: string | URL | Buffer) => { + const path = String(p); + return path === "/sandbox/.openclaw" || path === "/sandbox/.nemoclaw"; + }); + }); + + it("shows 'active (inside sandbox)' instead of 'not running'", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("active (inside sandbox)"); + expect(output).not.toContain("Status: not running"); + }); + + it("shows sandbox context banner", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Context: running inside an active OpenShell sandbox"); + expect(output).toContain("Host sandbox state is not inspectable from inside the sandbox."); + }); + + it("shows 'unable to query' instead of 'Not configured'", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("unable to query from inside sandbox"); + expect(output).not.toContain("Not configured"); + }); + + it("does not call openshell commands", async () => { + const { logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + expect(exec).not.toHaveBeenCalled(); + }); + + it("JSON output has insideSandbox: true everywhere", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(true); + expect(data.sandbox.insideSandbox).toBe(true); + expect(data.sandbox.running).toBe(false); + expect(data.inference.insideSandbox).toBe(true); + expect(data.inference.configured).toBe(false); + }); + }); + + // ========================================================================= + // Scenario 5: Inside sandbox with prior plugin state + // ========================================================================= + describe("inside sandbox — with prior plugin state", () => { + beforeEach(() => { + vi.mocked(existsSync).mockImplementation((p: string | URL | Buffer) => { + const path = String(p); + return path === "/sandbox/.openclaw" || path === "/sandbox/.nemoclaw"; + }); + vi.mocked(loadState).mockReturnValue(populatedState()); + }); + + it("shows plugin state from state file", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Last action: migrate"); + expect(output).toContain("Blueprint: 0.1.0"); + expect(output).toContain("Run ID: run-a1b2c3d4"); + }); + + it("shows rollback section when migrationSnapshot exists", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Rollback:"); + expect(output).toContain("Snapshot: /root/.nemoclaw/snapshots/pre-migrate.tar.gz"); + expect(output).toContain("openclaw nemoclaw eject"); + }); + + it("JSON includes full nemoclaw state alongside insideSandbox: true", async () => { + const { lines, logger } = captureLogger(); + + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(true); + expect(data.nemoclaw.lastAction).toBe("migrate"); + expect(data.nemoclaw.blueprintVersion).toBe("0.1.0"); + expect(data.nemoclaw.lastRunId).toBe("run-a1b2c3d4"); + expect(data.nemoclaw.migrationSnapshot).toBe( + "/root/.nemoclaw/snapshots/pre-migrate.tar.gz", + ); + }); + }); + + // ========================================================================= + // Edge cases + // ========================================================================= + describe("edge cases", () => { + it("uses state.sandboxName when available", async () => { + vi.mocked(loadState).mockReturnValue({ + ...blankState(), + sandboxName: "custom-sandbox", + }); + mockExec({ + "sandbox status": JSON.stringify({ state: "running", uptime: "1m" }), + "inference get": new Error("not configured"), + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Name: custom-sandbox"); + + // Verify the exec call used the custom sandbox name + expect(exec).toHaveBeenCalledWith( + expect.stringContaining("custom-sandbox"), + expect.anything(), + expect.anything(), + ); + }); + + it("defaults sandbox name to 'openclaw' when state has none", async () => { + mockExec({ + "sandbox status": new Error("not found"), + "inference get": new Error("not configured"), + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + // Verify exec was called with default name + expect(exec).toHaveBeenCalledWith( + expect.stringContaining("openclaw"), + expect.anything(), + expect.anything(), + ); + }); + + it("only detects sandbox via /sandbox/.openclaw", async () => { + vi.mocked(existsSync).mockImplementation((p: string | URL | Buffer) => { + return String(p) === "/sandbox/.openclaw"; + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(true); + }); + + it("only detects sandbox via /sandbox/.nemoclaw", async () => { + vi.mocked(existsSync).mockImplementation((p: string | URL | Buffer) => { + return String(p) === "/sandbox/.nemoclaw"; + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: true, logger, pluginConfig: defaultConfig }); + + const data = JSON.parse(lines.join("")); + expect(data.insideSandbox).toBe(true); + }); + + it("handles sandbox running but with missing uptime field", async () => { + mockExec({ + "sandbox status": JSON.stringify({ state: "running" }), + "inference get": new Error("not configured"), + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + const output = lines.join("\n"); + expect(output).toContain("Status: running"); + expect(output).toContain("Uptime: unknown"); + }); + + it("no rollback section when migrationSnapshot is null", async () => { + vi.mocked(loadState).mockReturnValue({ + ...populatedState(), + migrationSnapshot: null, + }); + + const { lines, logger } = captureLogger(); + await cliStatus({ json: false, logger, pluginConfig: defaultConfig }); + + expect(lines.join("\n")).not.toContain("Rollback:"); + }); + }); +}); diff --git a/nemoclaw/src/commands/status.ts b/nemoclaw/src/commands/status.ts index c298c22d855..cf594dc8d5e 100644 --- a/nemoclaw/src/commands/status.ts +++ b/nemoclaw/src/commands/status.ts @@ -2,12 +2,23 @@ // SPDX-License-Identifier: Apache-2.0 import { exec } from "node:child_process"; +import { existsSync } from "node:fs"; import { promisify } from "node:util"; import type { PluginLogger, NemoClawConfig } from "../index.js"; import { loadState } from "../blueprint/state.js"; const execAsync = promisify(exec); +/** + * Detect whether the plugin is running inside an OpenShell sandbox. + * Inside sandboxes the root filesystem is mounted at /sandbox and openshell + * host commands are not available, so querying `openshell sandbox status` + * would always fail — producing false-negative "not running" reports. + */ +function isInsideSandbox(): boolean { + return existsSync("/sandbox/.openclaw") || existsSync("/sandbox/.nemoclaw"); +} + export interface StatusOptions { json: boolean; logger: PluginLogger; @@ -18,10 +29,11 @@ export async function cliStatus(opts: StatusOptions): Promise { const { json: jsonOutput, logger } = opts; const state = loadState(); const sandboxName = state.sandboxName ?? "openclaw"; + const insideSandbox = isInsideSandbox(); const [sandbox, inference] = await Promise.all([ - getSandboxStatus(sandboxName), - getInferenceStatus(), + getSandboxStatus(sandboxName, insideSandbox), + getInferenceStatus(insideSandbox), ]); const statusData = { @@ -35,6 +47,7 @@ export async function cliStatus(opts: StatusOptions): Promise { }, sandbox, inference, + insideSandbox, }; if (jsonOutput) { @@ -46,6 +59,13 @@ export async function cliStatus(opts: StatusOptions): Promise { logger.info("==============="); logger.info(""); + if (insideSandbox) { + logger.info("Context: running inside an active OpenShell sandbox"); + logger.info(" Host sandbox state is not inspectable from inside the sandbox."); + logger.info(" Run 'openshell sandbox status' on the host for full details."); + logger.info(""); + } + logger.info("Plugin State:"); if (state.lastAction) { logger.info(` Last action: ${state.lastAction}`); @@ -62,6 +82,10 @@ export async function cliStatus(opts: StatusOptions): Promise { logger.info(` Name: ${sandbox.name}`); logger.info(" Status: running"); logger.info(` Uptime: ${sandbox.uptime ?? "unknown"}`); + } else if (sandbox.insideSandbox) { + logger.info(` Name: ${sandbox.name}`); + logger.info(" Status: active (inside sandbox)"); + logger.info(" Note: Cannot query host sandbox state from within the sandbox."); } else { logger.info(" Status: not running"); } @@ -72,6 +96,9 @@ export async function cliStatus(opts: StatusOptions): Promise { logger.info(` Provider: ${inference.provider ?? "unknown"}`); logger.info(` Model: ${inference.model ?? "unknown"}`); logger.info(` Endpoint: ${inference.endpoint ?? "unknown"}`); + } else if (inference.insideSandbox) { + logger.info(" Status: unable to query from inside sandbox"); + logger.info(" Note: Run 'openshell inference get' on the host to check."); } else { logger.info(" Not configured"); } @@ -88,6 +115,7 @@ interface SandboxStatus { name: string; running: boolean; uptime: string | null; + insideSandbox: boolean; } interface SandboxStatusResponse { @@ -95,7 +123,10 @@ interface SandboxStatusResponse { uptime?: string; } -async function getSandboxStatus(sandboxName: string): Promise { +async function getSandboxStatus(sandboxName: string, insideSandbox: boolean): Promise { + if (insideSandbox) { + return { name: sandboxName, running: false, uptime: null, insideSandbox: true }; + } try { const { stdout } = await execAsync(`openshell sandbox status ${sandboxName} --json`, { timeout: 5000, @@ -105,9 +136,10 @@ async function getSandboxStatus(sandboxName: string): Promise { name: sandboxName, running: parsed.state === "running", uptime: parsed.uptime ?? null, + insideSandbox: false, }; } catch { - return { name: sandboxName, running: false, uptime: null }; + return { name: sandboxName, running: false, uptime: null, insideSandbox: false }; } } @@ -116,6 +148,7 @@ interface InferenceStatus { provider: string | null; model: string | null; endpoint: string | null; + insideSandbox: boolean; } interface InferenceStatusResponse { @@ -124,7 +157,10 @@ interface InferenceStatusResponse { endpoint?: string; } -async function getInferenceStatus(): Promise { +async function getInferenceStatus(insideSandbox: boolean): Promise { + if (insideSandbox) { + return { configured: false, provider: null, model: null, endpoint: null, insideSandbox: true }; + } try { const { stdout } = await execAsync("openshell inference get --json", { timeout: 5000, @@ -135,8 +171,9 @@ async function getInferenceStatus(): Promise { provider: parsed.provider ?? null, model: parsed.model ?? null, endpoint: parsed.endpoint ?? null, + insideSandbox: false, }; } catch { - return { configured: false, provider: null, model: null, endpoint: null }; + return { configured: false, provider: null, model: null, endpoint: null, insideSandbox: false }; } } diff --git a/nemoclaw/src/index.ts b/nemoclaw/src/index.ts index 796564d8be2..f6defcd41b5 100644 --- a/nemoclaw/src/index.ts +++ b/nemoclaw/src/index.ts @@ -14,7 +14,11 @@ import type { Command } from "commander"; import { registerCliCommands } from "./cli.js"; import { handleSlashCommand } from "./commands/slash.js"; -import { loadOnboardConfig } from "./onboard/config.js"; +import { + describeOnboardEndpoint, + describeOnboardProvider, + loadOnboardConfig, +} from "./onboard/config.js"; // --------------------------------------------------------------------------- // OpenClaw Plugin SDK compatible types (mirrors openclaw/plugin-sdk) @@ -143,6 +147,65 @@ export interface NemoClawConfig { inferenceProvider: string; } +function activeModelEntries(onboardCfg: ReturnType): ModelProviderEntry[] { + if (!onboardCfg?.model) { + return [ + { + id: "nvidia/nemotron-3-super-120b-a12b", + label: "Nemotron 3 Super 120B (March 2026)", + contextWindow: 131072, + maxOutput: 8192, + }, + { + id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", + label: "Nemotron Ultra 253B", + contextWindow: 131072, + maxOutput: 4096, + }, + { + id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", + label: "Nemotron Super 49B v1.5", + contextWindow: 131072, + maxOutput: 4096, + }, + { + id: "nvidia/nemotron-3-nano-30b-a3b", + label: "Nemotron 3 Nano 30B", + contextWindow: 131072, + maxOutput: 4096, + }, + ]; + } + + return [ + { + id: `inference/${onboardCfg.model}`, + label: onboardCfg.model, + contextWindow: 131072, + maxOutput: 8192, + }, + ]; +} + +function registeredProviderForConfig( + onboardCfg: ReturnType, + providerCredentialEnv: string, +): ProviderPlugin { + const authLabel = + providerCredentialEnv === "NVIDIA_API_KEY" + ? `NVIDIA API Key (${providerCredentialEnv})` + : `OpenAI API Key (${providerCredentialEnv})`; + + return { + id: "inference", + label: "Managed Inference Route", + aliases: ["inference-local", "nemoclaw"], + envVars: [providerCredentialEnv], + models: { chat: activeModelEntries(onboardCfg) }, + auth: [{ type: "bearer", envVar: providerCredentialEnv, headerName: "Authorization", label: authLabel }], + }; +} + const DEFAULT_PLUGIN_CONFIG: NemoClawConfig = { blueprintVersion: "latest", blueprintRegistry: "ghcr.io/nvidia/nemoclaw-blueprint", @@ -196,55 +259,10 @@ export default function register(api: OpenClawPluginApi): void { // 3. Register nvidia-nim provider — use onboard config if available const onboardCfg = loadOnboardConfig(); const providerCredentialEnv = onboardCfg?.credentialEnv ?? "NVIDIA_API_KEY"; - const providerLabel = onboardCfg - ? `NVIDIA NIM (${onboardCfg.endpointType}${onboardCfg.ncpPartner ? ` - ${onboardCfg.ncpPartner}` : ""})` - : "NVIDIA NIM (build.nvidia.com)"; - - api.registerProvider({ - id: "nvidia-nim", - label: providerLabel, - docsPath: "https://build.nvidia.com/docs", - aliases: ["nvidia", "nim"], - envVars: [providerCredentialEnv], - models: { - chat: [ - { - id: "nvidia/nemotron-3-super-120b-a12b", - label: "Nemotron 3 Super 120B (March 2026)", - contextWindow: 131072, - maxOutput: 8192, - }, - { - id: "nvidia/llama-3.1-nemotron-ultra-253b-v1", - label: "Nemotron Ultra 253B", - contextWindow: 131072, - maxOutput: 4096, - }, - { - id: "nvidia/llama-3.3-nemotron-super-49b-v1.5", - label: "Nemotron Super 49B v1.5", - contextWindow: 131072, - maxOutput: 4096, - }, - { - id: "nvidia/nemotron-3-nano-30b-a3b", - label: "Nemotron 3 Nano 30B", - contextWindow: 131072, - maxOutput: 4096, - }, - ], - }, - auth: [ - { - type: "bearer", - envVar: providerCredentialEnv, - headerName: "Authorization", - label: `NVIDIA API Key (${providerCredentialEnv})`, - }, - ], - }); + api.registerProvider(registeredProviderForConfig(onboardCfg, providerCredentialEnv)); - const bannerEndpoint = onboardCfg?.endpointType ?? "build.nvidia.com"; + const bannerEndpoint = onboardCfg ? describeOnboardEndpoint(onboardCfg) : "build.nvidia.com"; + const bannerProvider = onboardCfg ? describeOnboardProvider(onboardCfg) : "NVIDIA Cloud API"; const bannerModel = onboardCfg?.model ?? "nvidia/nemotron-3-super-120b-a12b"; api.logger.info(""); @@ -252,6 +270,7 @@ export default function register(api: OpenClawPluginApi): void { api.logger.info(" │ NemoClaw registered │"); api.logger.info(" │ │"); api.logger.info(` │ Endpoint: ${bannerEndpoint.padEnd(40)}│`); + api.logger.info(` │ Provider: ${bannerProvider.padEnd(40)}│`); api.logger.info(` │ Model: ${bannerModel.padEnd(40)}│`); api.logger.info(" │ Commands: openclaw nemoclaw │"); api.logger.info(" └─────────────────────────────────────────────────────┘"); diff --git a/nemoclaw/src/onboard/config.ts b/nemoclaw/src/onboard/config.ts index 80a1d6efc8b..82082b9697a 100644 --- a/nemoclaw/src/onboard/config.ts +++ b/nemoclaw/src/onboard/config.ts @@ -15,9 +15,42 @@ export interface NemoClawOnboardConfig { model: string; profile: string; credentialEnv: string; + provider?: string; + providerLabel?: string; onboardedAt: string; } +export function describeOnboardEndpoint(config: NemoClawOnboardConfig): string { + if (config.endpointUrl === "https://inference.local/v1") { + return "Managed Inference Route (inference.local)"; + } + + return `${config.endpointType} (${config.endpointUrl})`; +} + +export function describeOnboardProvider(config: NemoClawOnboardConfig): string { + if (config.providerLabel) { + return config.providerLabel; + } + + switch (config.endpointType) { + case "build": + return "NVIDIA Cloud API"; + case "ollama": + return "Local Ollama"; + case "vllm": + return "Local vLLM"; + case "nim-local": + return "Local NIM"; + case "ncp": + return "NVIDIA Cloud Partner"; + case "custom": + return "Managed Inference Route"; + default: + return "Unknown"; + } +} + let configDirCreated = false; function ensureConfigDir(): void { diff --git a/nemoclaw/tsconfig.json b/nemoclaw/tsconfig.json index 05c87322fad..1a5057ed1d3 100644 --- a/nemoclaw/tsconfig.json +++ b/nemoclaw/tsconfig.json @@ -16,5 +16,5 @@ "resolveJsonModule": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] } diff --git a/nemoclaw/vitest.config.ts b/nemoclaw/vitest.config.ts new file mode 100644 index 00000000000..ae847ff6d9c --- /dev/null +++ b/nemoclaw/vitest.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["src/**/*.test.ts"], + }, +}); diff --git a/scripts/fix-coredns.sh b/scripts/fix-coredns.sh index 7afb101cff9..512ba851e66 100755 --- a/scripts/fix-coredns.sh +++ b/scripts/fix-coredns.sh @@ -19,16 +19,11 @@ set -euo pipefail GATEWAY_NAME="${1:-}" +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +# shellcheck source=./lib/runtime.sh +. "$SCRIPT_DIR/lib/runtime.sh" -# Find Colima socket (legacy or XDG path) -COLIMA_SOCKET="" -for _sock in "$HOME/.colima/default/docker.sock" "$HOME/.config/colima/default/docker.sock"; do - if [ -S "$_sock" ]; then - COLIMA_SOCKET="$_sock" - break - fi -done -unset _sock +COLIMA_SOCKET="$(find_colima_docker_socket || true)" if [ -z "${DOCKER_HOST:-}" ]; then if [ -n "$COLIMA_SOCKET" ]; then @@ -40,31 +35,29 @@ if [ -z "${DOCKER_HOST:-}" ]; then fi # Find the cluster container -CLUSTER=$(docker ps --filter "name=openshell-cluster" --format '{{.Names}}' | head -1) +CLUSTERS="$(docker ps --filter "name=openshell-cluster" --format '{{.Names}}')" +CLUSTER="$(select_openshell_cluster_container "$GATEWAY_NAME" "$CLUSTERS" || true)" if [ -z "$CLUSTER" ]; then - echo "ERROR: No openshell cluster container found." + if [ -n "$GATEWAY_NAME" ]; then + echo "ERROR: Could not uniquely determine the openshell cluster container for gateway '$GATEWAY_NAME'." + else + echo "ERROR: Could not uniquely determine the openshell cluster container." + fi exit 1 fi -# Get the container's upstream DNS from /etc/resolv.conf — this is the address -# the Docker/Colima VM uses for DNS and is reachable from k3s pods. -# The docker bridge gateway (172.17.0.1) does NOT serve DNS in Colima. -GATEWAY_IP=$(docker exec "$CLUSTER" grep nameserver /etc/resolv.conf | head -1 | awk '{print $2}') -if [ -z "$GATEWAY_IP" ]; then - echo "ERROR: Could not determine container gateway IP." - exit 1 -fi +CONTAINER_RESOLV_CONF="$(docker exec "$CLUSTER" cat /etc/resolv.conf 2>/dev/null || true)" +HOST_RESOLV_CONF="$(cat /etc/resolv.conf 2>/dev/null || true)" +UPSTREAM_DNS="$(resolve_coredns_upstream "$CONTAINER_RESOLV_CONF" "$HOST_RESOLV_CONF" "colima" || true)" -# Sanity check: don't use 127.x.x.x — it won't work from pods -if [[ "$GATEWAY_IP" == 127.* ]]; then - echo "ERROR: Gateway IP is $GATEWAY_IP (loopback). Cannot use from k3s pods." - echo "Falling back to public DNS (8.8.8.8)." - GATEWAY_IP="8.8.8.8" +if [ -z "$UPSTREAM_DNS" ]; then + echo "ERROR: Could not determine a non-loopback DNS upstream for Colima." + exit 1 fi -echo "Patching CoreDNS to forward to $GATEWAY_IP..." +echo "Patching CoreDNS to forward to $UPSTREAM_DNS..." -docker exec "$CLUSTER" kubectl patch configmap coredns -n kube-system --type merge -p "{\"data\":{\"Corefile\":\".:53 {\\n errors\\n health\\n ready\\n kubernetes cluster.local in-addr.arpa ip6.arpa {\\n pods insecure\\n fallthrough in-addr.arpa ip6.arpa\\n }\\n hosts /etc/coredns/NodeHosts {\\n ttl 60\\n reload 15s\\n fallthrough\\n }\\n prometheus :9153\\n cache 30\\n loop\\n reload\\n loadbalance\\n forward . $GATEWAY_IP\\n}\\n\"}}" > /dev/null +docker exec "$CLUSTER" kubectl patch configmap coredns -n kube-system --type merge -p "{\"data\":{\"Corefile\":\".:53 {\\n errors\\n health\\n ready\\n kubernetes cluster.local in-addr.arpa ip6.arpa {\\n pods insecure\\n fallthrough in-addr.arpa ip6.arpa\\n }\\n hosts /etc/coredns/NodeHosts {\\n ttl 60\\n reload 15s\\n fallthrough\\n }\\n prometheus :9153\\n cache 30\\n loop\\n reload\\n loadbalance\\n forward . $UPSTREAM_DNS\\n}\\n\"}}" > /dev/null docker exec "$CLUSTER" kubectl rollout restart deploy/coredns -n kube-system > /dev/null diff --git a/scripts/install-openshell.sh b/scripts/install-openshell.sh new file mode 100644 index 00000000000..444987eda24 --- /dev/null +++ b/scripts/install-openshell.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +set -euo pipefail + +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +NC='\033[0m' + +info() { echo -e "${GREEN}[install]${NC} $1"; } +warn() { echo -e "${YELLOW}[install]${NC} $1"; } +fail() { echo -e "${RED}[install]${NC} $1"; exit 1; } + +OS="$(uname -s)" +ARCH="$(uname -m)" + +case "$OS" in + Darwin) OS_LABEL="macOS" ;; + Linux) OS_LABEL="Linux" ;; + *) fail "Unsupported OS: $OS" ;; +esac + +case "$ARCH" in + x86_64|amd64) ARCH_LABEL="x86_64" ;; + aarch64|arm64) ARCH_LABEL="aarch64" ;; + *) fail "Unsupported architecture: $ARCH" ;; +esac + +info "Detected $OS_LABEL ($ARCH_LABEL)" + +if command -v openshell > /dev/null 2>&1; then + info "openshell already installed: $(openshell --version 2>&1 || echo 'unknown')" + exit 0 +fi + +info "Installing openshell CLI..." + +case "$OS" in + Darwin) + case "$ARCH_LABEL" in + x86_64) ASSET="openshell-x86_64-apple-darwin.tar.gz" ;; + aarch64) ASSET="openshell-aarch64-apple-darwin.tar.gz" ;; + esac + ;; + Linux) + case "$ARCH_LABEL" in + x86_64) ASSET="openshell-x86_64-unknown-linux-musl.tar.gz" ;; + aarch64) ASSET="openshell-aarch64-unknown-linux-musl.tar.gz" ;; + esac + ;; +esac + +tmpdir="$(mktemp -d)" +trap 'rm -rf "$tmpdir"' EXIT + +if command -v gh > /dev/null 2>&1; then + GH_TOKEN="${GITHUB_TOKEN:-}" gh release download --repo NVIDIA/OpenShell \ + --pattern "$ASSET" --dir "$tmpdir" +else + curl -fsSL "https://github.com/NVIDIA/OpenShell/releases/latest/download/$ASSET" \ + -o "$tmpdir/$ASSET" +fi + +tar xzf "$tmpdir/$ASSET" -C "$tmpdir" + +target_dir="/usr/local/bin" + +if [ -w "$target_dir" ]; then + install -m 755 "$tmpdir/openshell" "$target_dir/openshell" +elif [ "${NEMOCLAW_NON_INTERACTIVE:-}" = "1" ] || [ ! -t 0 ]; then + target_dir="${XDG_BIN_HOME:-$HOME/.local/bin}" + mkdir -p "$target_dir" + install -m 755 "$tmpdir/openshell" "$target_dir/openshell" + warn "Installed openshell to $target_dir/openshell (user-local path)" + warn "Ensure $target_dir is on PATH for future shells." +else + sudo install -m 755 "$tmpdir/openshell" "$target_dir/openshell" +fi + +info "$("$target_dir/openshell" --version 2>&1 || echo openshell) installed" diff --git a/scripts/install.sh b/scripts/install.sh index e2df98103d4..fb51bcfae6b 100644 --- a/scripts/install.sh +++ b/scripts/install.sh @@ -18,6 +18,109 @@ info() { echo -e "${GREEN}[install]${NC} $1"; } warn() { echo -e "${YELLOW}[install]${NC} $1"; } fail() { echo -e "${RED}[install]${NC} $1"; exit 1; } +define_runtime_helpers() { + socket_exists() { + local socket_path="$1" + + if [ -n "${NEMOCLAW_TEST_SOCKET_PATHS:-}" ]; then + case ":$NEMOCLAW_TEST_SOCKET_PATHS:" in + *":$socket_path:"*) return 0 ;; + esac + fi + + [ -S "$socket_path" ] + } + + find_colima_docker_socket() { + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path + + for socket_path in \ + "$home_dir/.colima/default/docker.sock" \ + "$home_dir/.config/colima/default/docker.sock" + do + if socket_exists "$socket_path"; then + printf '%s\n' "$socket_path" + return 0 + fi + done + + return 1 + } + + find_docker_desktop_socket() { + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path="$home_dir/.docker/run/docker.sock" + + if socket_exists "$socket_path"; then + printf '%s\n' "$socket_path" + return 0 + fi + + return 1 + } + + detect_docker_host() { + if [ -n "${DOCKER_HOST:-}" ]; then + printf '%s\n' "$DOCKER_HOST" + return 0 + fi + + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path + + if socket_path="$(find_colima_docker_socket "$home_dir")"; then + printf 'unix://%s\n' "$socket_path" + return 0 + fi + + if socket_path="$(find_docker_desktop_socket "$home_dir")"; then + printf 'unix://%s\n' "$socket_path" + return 0 + fi + + return 1 + } +} + +SCRIPT_PATH="${BASH_SOURCE[0]-}" +SCRIPT_DIR="" +if [ -n "$SCRIPT_PATH" ]; then + SCRIPT_DIR="$(cd "$(dirname "$SCRIPT_PATH")" && pwd)" +fi + +if [ -n "$SCRIPT_DIR" ] && [ -f "$SCRIPT_DIR/lib/runtime.sh" ]; then + # shellcheck source=/dev/null + . "$SCRIPT_DIR/lib/runtime.sh" +else + define_runtime_helpers +fi + +# Ensure nvm environment is loaded in the current shell. +ensure_nvm_loaded() { + if [ -z "${NVM_DIR:-}" ]; then + export NVM_DIR="$HOME/.nvm" + fi + if [ -s "$NVM_DIR/nvm.sh" ]; then + # shellcheck source=/dev/null + . "$NVM_DIR/nvm.sh" + fi +} + +# Refresh PATH so that npm global bin is discoverable. +refresh_path() { + ensure_nvm_loaded + + local npm_bin + npm_bin="$(npm config get prefix 2>/dev/null)/bin" || true + if [ -n "$npm_bin" ] && [ -d "$npm_bin" ]; then + case ":$PATH:" in + *":$npm_bin:"*) ;; # already on PATH + *) export PATH="$npm_bin:$PATH" ;; + esac + fi +} + MIN_NODE_MAJOR=20 MIN_NPM_MAJOR=10 RECOMMENDED_NODE_MAJOR=22 @@ -49,6 +152,9 @@ if command -v asdf > /dev/null 2>&1 && asdf plugin list 2>/dev/null | grep -q no NODE_MGR="asdf" elif [ -n "${NVM_DIR:-}" ] && [ -s "${NVM_DIR}/nvm.sh" ]; then NODE_MGR="nvm" +elif [ -s "$HOME/.nvm/nvm.sh" ]; then + export NVM_DIR="$HOME/.nvm" + NODE_MGR="nvm" elif command -v fnm > /dev/null 2>&1; then NODE_MGR="fnm" elif command -v brew > /dev/null 2>&1 && [ "$OS" = "Darwin" ]; then @@ -149,6 +255,23 @@ install_docker() { if command -v docker > /dev/null 2>&1; then # Docker installed but not running if [ "$OS" = "Darwin" ]; then + local colima_socket="" + local docker_desktop_socket="" + colima_socket="$(find_colima_docker_socket || true)" + docker_desktop_socket="$(find_docker_desktop_socket || true)" + + if [ -n "${DOCKER_HOST:-}" ]; then + fail "Docker is installed but the selected runtime is not running. Start the runtime behind DOCKER_HOST (${DOCKER_HOST}) and re-run." + fi + + if [ -n "$colima_socket" ] && [ -n "$docker_desktop_socket" ]; then + fail "Both Colima and Docker Desktop are available on this Mac. Start the runtime you want explicitly and re-run, or set DOCKER_HOST to select one." + fi + + if [ -n "$docker_desktop_socket" ]; then + fail "Docker Desktop appears to be installed but is not running. Start Docker Desktop and re-run." + fi + if command -v colima > /dev/null 2>&1; then info "Starting Colima..." colima start @@ -239,17 +362,41 @@ install_openshell # ── Install NemoClaw CLI ───────────────────────────────────────── info "Installing nemoclaw CLI..." -npm install -g nemoclaw +if [ "$NODE_MGR" = "nodesource" ]; then + sudo npm install -g git+https://github.com/NVIDIA/NemoClaw.git +else + npm install -g git+https://github.com/NVIDIA/NemoClaw.git +fi if [ "$NEED_RESHIM" = true ]; then info "Reshimming asdf..." asdf reshim nodejs fi +refresh_path + # ── Verify ─────────────────────────────────────────────────────── if ! command -v nemoclaw > /dev/null 2>&1; then - fail "nemoclaw not found in PATH after install. Check your Node.js bin directory." + # Try refreshing PATH one more time + refresh_path +fi + +if ! command -v nemoclaw > /dev/null 2>&1; then + npm_bin="$(npm config get prefix 2>/dev/null)/bin" || true + if [ -n "$npm_bin" ] && [ -x "$npm_bin/nemoclaw" ]; then + warn "nemoclaw installed at $npm_bin/nemoclaw but not on current PATH." + warn "" + warn "Add it to your shell profile:" + warn " echo 'export PATH=\"$npm_bin:\$PATH\"' >> ~/.bashrc" + warn " source ~/.bashrc" + warn "" + warn "Or for zsh:" + warn " echo 'export PATH=\"$npm_bin:\$PATH\"' >> ~/.zshrc" + warn " source ~/.zshrc" + else + fail "nemoclaw installation failed. Binary not found." + fi fi echo "" @@ -258,3 +405,24 @@ info "nemoclaw $(nemoclaw --version 2>/dev/null || echo 'v0.1.0') is ready." echo "" echo " Run \`nemoclaw onboard\` to get started" echo "" + +# ── Post-install: shell reload instructions ────────────────── + +if [ "$NODE_MGR" = "nvm" ] || [ "$NODE_MGR" = "fnm" ]; then + profile="$HOME/.bashrc" + if [ -n "${ZSH_VERSION:-}" ] || [ "$(basename "${SHELL:-}")" = "zsh" ]; then + profile="$HOME/.zshrc" + elif [ ! -f "$HOME/.bashrc" ] && [ -f "$HOME/.profile" ]; then + profile="$HOME/.profile" + fi + echo " ──────────────────────────────────────────────────" + warn "Your current shell may not have the updated PATH." + echo "" + echo " To use nemoclaw now, run:" + echo "" + echo " source $profile" + echo "" + echo " Or open a new terminal window." + echo " ──────────────────────────────────────────────────" + echo "" +fi diff --git a/scripts/lib/runtime.sh b/scripts/lib/runtime.sh new file mode 100644 index 00000000000..3bf546847e8 --- /dev/null +++ b/scripts/lib/runtime.sh @@ -0,0 +1,229 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +socket_exists() { + local socket_path="$1" + + if [ -n "${NEMOCLAW_TEST_SOCKET_PATHS:-}" ]; then + case ":$NEMOCLAW_TEST_SOCKET_PATHS:" in + *":$socket_path:"*) return 0 ;; + esac + fi + + [ -S "$socket_path" ] +} + +find_colima_docker_socket() { + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path + + for socket_path in \ + "$home_dir/.colima/default/docker.sock" \ + "$home_dir/.config/colima/default/docker.sock" + do + if socket_exists "$socket_path"; then + printf '%s\n' "$socket_path" + return 0 + fi + done + + return 1 +} + +find_docker_desktop_socket() { + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path="$home_dir/.docker/run/docker.sock" + + if socket_exists "$socket_path"; then + printf '%s\n' "$socket_path" + return 0 + fi + + return 1 +} + +detect_docker_host() { + if [ -n "${DOCKER_HOST:-}" ]; then + printf '%s\n' "$DOCKER_HOST" + return 0 + fi + + local home_dir="${1:-${HOME:-/tmp}}" + local socket_path + + if socket_path="$(find_colima_docker_socket "$home_dir")"; then + printf 'unix://%s\n' "$socket_path" + return 0 + fi + + if socket_path="$(find_docker_desktop_socket "$home_dir")"; then + printf 'unix://%s\n' "$socket_path" + return 0 + fi + + return 1 +} + +docker_host_runtime() { + local docker_host="${1:-${DOCKER_HOST:-}}" + + case "$docker_host" in + unix://*"/.colima/default/docker.sock"|unix://*"/.config/colima/default/docker.sock") + printf 'colima\n' + ;; + unix://*"/.docker/run/docker.sock") + printf 'docker-desktop\n' + ;; + "") + return 1 + ;; + *) + printf 'custom\n' + ;; + esac +} + +infer_container_runtime_from_info() { + local info="${1:-}" + local normalized + normalized="$(printf '%s' "$info" | tr '[:upper:]' '[:lower:]')" + + if [[ -z "${normalized// }" ]]; then + printf 'unknown\n' + elif [[ "$normalized" == *podman* ]]; then + printf 'podman\n' + elif [[ "$normalized" == *colima* ]]; then + printf 'colima\n' + elif [[ "$normalized" == *"docker desktop"* ]]; then + printf 'docker-desktop\n' + elif [[ "$normalized" == *docker* ]]; then + printf 'docker\n' + else + printf 'unknown\n' + fi +} + +is_unsupported_macos_runtime() { + local platform="${1:-$(uname -s)}" + local runtime="${2:-unknown}" + + [ "$platform" = "Darwin" ] && [ "$runtime" = "podman" ] +} + +is_loopback_ip() { + local ip="${1:-}" + [[ "$ip" == 127.* ]] +} + +first_non_loopback_nameserver() { + local resolv_conf="${1:-}" + + if [ -z "$resolv_conf" ]; then + return 1 + fi + + printf '%s\n' "$resolv_conf" \ + | awk '$1 == "nameserver" && $2 !~ /^127\./ { print $2; exit }' +} + +get_colima_vm_nameserver() { + if ! command -v colima > /dev/null 2>&1; then + return 1 + fi + + local profile="${COLIMA_PROFILE:-default}" + local resolv_conf + resolv_conf="$(colima ssh --profile "$profile" -- cat /etc/resolv.conf < /dev/null 2>/dev/null || true)" + first_non_loopback_nameserver "$resolv_conf" +} + +resolve_coredns_upstream() { + local container_resolv_conf="${1:-}" + local host_resolv_conf="${2:-}" + local runtime="${3:-unknown}" + local nameserver="" + + nameserver="$(first_non_loopback_nameserver "$container_resolv_conf" || true)" + if [ -n "$nameserver" ]; then + printf '%s\n' "$nameserver" + return 0 + fi + + if [ "$runtime" = "colima" ]; then + nameserver="$(get_colima_vm_nameserver || true)" + if [ -n "$nameserver" ]; then + printf '%s\n' "$nameserver" + return 0 + fi + fi + + nameserver="$(first_non_loopback_nameserver "$host_resolv_conf" || true)" + if [ -n "$nameserver" ]; then + printf '%s\n' "$nameserver" + return 0 + fi + + return 1 +} + +select_openshell_cluster_container() { + local gateway_name="${1:-}" + local containers="${2:-}" + local matches="" + local count=0 + local match_count=0 + + if [ -z "$containers" ]; then + return 1 + fi + + count="$(printf '%s\n' "$containers" | awk 'NF { count += 1 } END { print count + 0 }')" + + if [ -n "$gateway_name" ]; then + matches="$(printf '%s\n' "$containers" | grep -F -- "$gateway_name" || true)" + match_count="$(printf '%s\n' "$matches" | awk 'NF { count += 1 } END { print count + 0 }')" + + if [ "$match_count" -eq 1 ]; then + printf '%s\n' "$matches" + return 0 + fi + + if [ "$match_count" -gt 1 ]; then + return 1 + fi + fi + + if [ "$count" -eq 1 ]; then + printf '%s\n' "$containers" + return 0 + fi + + return 1 +} + +get_local_provider_base_url() { + local provider="${1:-}" + + case "$provider" in + vllm-local) printf 'http://host.openshell.internal:8000/v1\n' ;; + ollama-local) printf 'http://host.openshell.internal:11434/v1\n' ;; + *) return 1 ;; + esac +} + +check_local_provider_health() { + local provider="${1:-}" + + case "$provider" in + vllm-local) + curl -sf http://localhost:8000/v1/models > /dev/null 2>&1 + ;; + ollama-local) + curl -sf http://localhost:11434/api/tags > /dev/null 2>&1 + ;; + *) + return 1 + ;; + esac +} diff --git a/scripts/nemoclaw-start.sh b/scripts/nemoclaw-start.sh index 215973520ed..0fe41648c8d 100755 --- a/scripts/nemoclaw-start.sh +++ b/scripts/nemoclaw-start.sh @@ -30,7 +30,9 @@ if os.path.exists(config_path): with open(config_path) as f: cfg = json.load(f) -cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = 'nvidia/nemotron-3-super-120b-a12b' +default_model = os.environ.get('NEMOCLAW_MODEL') +if default_model: + cfg.setdefault('agents', {}).setdefault('defaults', {}).setdefault('model', {})['primary'] = default_model chat_ui_url = os.environ.get('CHAT_UI_URL', 'http://127.0.0.1:18789') parsed = urlparse(chat_ui_url) @@ -168,7 +170,6 @@ PYAUTOPAIR echo 'Setting up NemoClaw...' openclaw doctor --fix > /dev/null 2>&1 || true -openclaw models set nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 || true write_auth_profile export CHAT_UI_URL PUBLIC_PORT fix_openclaw_config diff --git a/scripts/setup-spark.sh b/scripts/setup-spark.sh index 27d21669db3..0cf5a20c1b3 100755 --- a/scripts/setup-spark.sh +++ b/scripts/setup-spark.sh @@ -7,18 +7,17 @@ # Spark ships Ubuntu 24.04 (cgroup v2) + Docker 28.x but no k3s. # OpenShell's gateway starts k3s inside a Docker container, which # needs cgroup host namespace access. This script configures Docker -# for that, then hands off to the normal setup.sh. +# for that. # # Usage: # sudo nemoclaw setup-spark # # or directly: # sudo bash scripts/setup-spark.sh # -# What it does (beyond setup.sh): +# What it does: # 1. Adds current user to docker group (avoids sudo for everything else) # 2. Configures Docker daemon for cgroupns=host (k3s-in-Docker on cgroup v2) # 3. Restarts Docker -# 4. Runs the normal setup.sh set -euo pipefail @@ -133,57 +132,10 @@ if [ "$NEEDS_RESTART" = true ]; then info "Docker restarted with cgroupns=host" fi -# ── 4. Install and start vLLM (local inference on Spark GPU) ────── +# ── 4. Run normal setup ────────────────────────────────────────── -if ! python3 -c "import vllm" 2>/dev/null; then - info "Installing vLLM..." - pip3 install --break-system-packages vllm 2>&1 | tail -1 - info "vLLM installed" -else - info "vLLM already installed" -fi - -# Start vLLM if not already running -VLLM_MODEL="nvidia/nemotron-3-nano-30b-a3b" -if curl -s http://localhost:8000/v1/models > /dev/null 2>&1; then - info "vLLM already running on :8000" -else - if python3 -c "import vllm" 2>/dev/null && command -v nvidia-smi > /dev/null 2>&1; then - info "Starting vLLM with $VLLM_MODEL..." - nohup python3 -m vllm.entrypoints.openai.api_server \ - --model "$VLLM_MODEL" \ - --port 8000 \ - --host 0.0.0.0 \ - > /tmp/vllm-server.log 2>&1 & - VLLM_PID=$! - # Wait for vLLM to be ready (model loading can take a while) - info "Waiting for vLLM to load model (this can take a few minutes)..." - for i in $(seq 1 120); do - if curl -s http://localhost:8000/v1/models > /dev/null 2>&1; then - info "vLLM ready (PID $VLLM_PID)" - break - fi - if ! kill -0 "$VLLM_PID" 2>/dev/null; then - warn "vLLM exited. Check /tmp/vllm-server.log" - break - fi - sleep 2 - done - fi -fi - -# ── 5. Run normal setup ────────────────────────────────────────── - -info "Running NemoClaw setup..." echo "" - -# Drop back to the real user for setup.sh (uses docker group, not root) -if [ -n "$REAL_USER" ]; then - # Pass through env vars that setup.sh needs - sudo -u "$REAL_USER" -E \ - NVIDIA_API_KEY="${NVIDIA_API_KEY:-}" \ - DOCKER_HOST="${DOCKER_HOST:-}" \ - bash "$SCRIPT_DIR/setup.sh" -else - bash "$SCRIPT_DIR/setup.sh" -fi +info "DGX Spark Docker configuration complete." +info "" +info "Next step: run 'nemoclaw onboard' to set up your sandbox." +info " nemoclaw onboard" diff --git a/scripts/setup.sh b/scripts/setup.sh index 77d24a77ab6..e0859a6f504 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -11,7 +11,7 @@ # # Usage: # export NVIDIA_API_KEY=nvapi-... -# ./scripts/setup.sh +# ./scripts/setup.sh [sandbox-name] # # What it does: # 1. Starts an OpenShell gateway (or reuses existing) @@ -29,6 +29,11 @@ GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=./lib/runtime.sh +. "$SCRIPT_DIR/lib/runtime.sh" + info() { echo -e "${GREEN}>>>${NC} $1"; } warn() { echo -e "${YELLOW}>>>${NC} $1"; } fail() { echo -e "${RED}>>>${NC} $1"; exit 1; } @@ -51,16 +56,25 @@ upsert_provider() { fi } -# Resolve DOCKER_HOST for Colima if needed (legacy ~/.colima or XDG ~/.config/colima) -if [ -z "${DOCKER_HOST:-}" ]; then - for _sock in "$HOME/.colima/default/docker.sock" "$HOME/.config/colima/default/docker.sock"; do - if [ -S "$_sock" ]; then - export DOCKER_HOST="unix://$_sock" - warn "Using Colima Docker socket: $_sock" - break - fi - done - unset _sock +# Resolve DOCKER_HOST for macOS user-scoped runtimes when needed. +ORIGINAL_DOCKER_HOST="${DOCKER_HOST:-}" +if docker_host="$(detect_docker_host)"; then + export DOCKER_HOST="$docker_host" + if [ -n "$ORIGINAL_DOCKER_HOST" ]; then + warn "Using DOCKER_HOST from environment: $docker_host" + else + case "$(docker_host_runtime "$docker_host" || true)" in + colima) + warn "Using Colima Docker socket: ${docker_host#unix://}" + ;; + docker-desktop) + warn "Using Docker Desktop socket: ${docker_host#unix://}" + ;; + custom) + warn "Using Docker host: $docker_host" + ;; + esac + fi fi # Check prerequisites @@ -68,8 +82,15 @@ command -v openshell > /dev/null || fail "openshell CLI not found. Install the b command -v docker > /dev/null || fail "docker not found" [ -n "${NVIDIA_API_KEY:-}" ] || fail "NVIDIA_API_KEY not set. Get one from build.nvidia.com" -SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" -REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +CONTAINER_RUNTIME="$(infer_container_runtime_from_info "$(docker info 2>/dev/null || true)")" +if is_unsupported_macos_runtime "$(uname -s)" "$CONTAINER_RUNTIME"; then + fail "Podman on macOS is not supported yet. NemoClaw currently depends on OpenShell support for Podman on macOS. Use Colima or Docker Desktop instead." +fi +if [ "$CONTAINER_RUNTIME" != "unknown" ]; then + info "Container runtime: $CONTAINER_RUNTIME" +fi +SANDBOX_NAME="${1:-nemoclaw}" +info "Using sandbox name: ${SANDBOX_NAME}" # 1. Gateway — always start fresh to avoid stale state info "Starting OpenShell gateway..." @@ -89,9 +110,9 @@ done info "Gateway is healthy" # 2. CoreDNS fix (Colima only) -if [ -S "$HOME/.colima/default/docker.sock" ]; then +if [ "$CONTAINER_RUNTIME" = "colima" ]; then info "Patching CoreDNS for Colima..." - bash "$SCRIPT_DIR/fix-coredns.sh" 2>&1 || warn "CoreDNS patch failed (may not be needed)" + bash "$SCRIPT_DIR/fix-coredns.sh" nemoclaw 2>&1 || warn "CoreDNS patch failed (may not be needed)" fi # 3. Providers @@ -105,12 +126,13 @@ upsert_provider \ "OPENAI_BASE_URL=https://integrate.api.nvidia.com/v1" # vllm-local (if vLLM is installed or running) -if curl -s http://localhost:8000/v1/models > /dev/null 2>&1 || python3 -c "import vllm" 2>/dev/null; then +if check_local_provider_health "vllm-local" || python3 -c "import vllm" 2>/dev/null; then + VLLM_LOCAL_BASE_URL="$(get_local_provider_base_url "vllm-local")" upsert_provider \ "vllm-local" \ "openai" \ "OPENAI_API_KEY=dummy" \ - "OPENAI_BASE_URL=http://host.openshell.internal:8000/v1" + "OPENAI_BASE_URL=$VLLM_LOCAL_BASE_URL" fi # 4a. Ollama (macOS local inference) @@ -121,16 +143,17 @@ if [ "$(uname -s)" = "Darwin" ]; then fi if command -v ollama > /dev/null 2>&1; then # Start Ollama service if not running - if ! curl -s http://localhost:11434/api/tags > /dev/null 2>&1; then + if ! check_local_provider_health "ollama-local"; then info "Starting Ollama service..." OLLAMA_HOST=0.0.0.0:11434 ollama serve > /dev/null 2>&1 & sleep 2 fi + OLLAMA_LOCAL_BASE_URL="$(get_local_provider_base_url "ollama-local")" upsert_provider \ "ollama-local" \ "openai" \ "OPENAI_API_KEY=ollama" \ - "OPENAI_BASE_URL=http://host.openshell.internal:11434/v1" + "OPENAI_BASE_URL=$OLLAMA_LOCAL_BASE_URL" fi fi @@ -139,8 +162,8 @@ info "Setting inference route to nvidia-nim / Nemotron 3 Super..." openshell inference set --no-verify --provider nvidia-nim --model nvidia/nemotron-3-super-120b-a12b > /dev/null 2>&1 # 5. Build and create sandbox -info "Deleting old nemoclaw sandbox (if any)..." -openshell sandbox delete nemoclaw > /dev/null 2>&1 || true +info "Deleting old ${SANDBOX_NAME} sandbox (if any)..." +openshell sandbox delete "$SANDBOX_NAME" > /dev/null 2>&1 || true info "Building and creating NemoClaw sandbox (this takes a few minutes on first run)..." @@ -162,7 +185,7 @@ fi # detect failures. The raw log is kept on failure for debugging. CREATE_LOG=$(mktemp /tmp/nemoclaw-create-XXXXXX.log) set +e -openshell sandbox create --from "$BUILD_CTX/Dockerfile" --name nemoclaw \ +openshell sandbox create --from "$BUILD_CTX/Dockerfile" --name "$SANDBOX_NAME" \ --provider nvidia-nim \ -- env NVIDIA_API_KEY="$NVIDIA_API_KEY" > "$CREATE_LOG" 2>&1 CREATE_RC=$? @@ -183,20 +206,20 @@ rm -f "$CREATE_LOG" # Verify sandbox is Ready (not just that a record exists) # Strip ANSI color codes before checking phase -SANDBOX_LINE=$(openshell sandbox list 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | grep "nemoclaw") +SANDBOX_LINE=$(openshell sandbox list 2>&1 | sed 's/\x1b\[[0-9;]*m//g' | awk -v name="$SANDBOX_NAME" '$1 == name { print; exit }') if ! echo "$SANDBOX_LINE" | grep -q "Ready"; then SANDBOX_PHASE=$(echo "$SANDBOX_LINE" | awk '{print $NF}') echo "" warn "Sandbox phase: ${SANDBOX_PHASE:-unknown}" # Check for common failure modes - SB_DETAIL=$(openshell sandbox get nemoclaw 2>&1 || true) + SB_DETAIL=$(openshell sandbox get "$SANDBOX_NAME" 2>&1 || true) if echo "$SB_DETAIL" | grep -qi "ImagePull\|ErrImagePull\|image.*not found"; then warn "Image pull failure detected. The sandbox image was built inside the" warn "gateway but k3s can't find it. This is a known openshell issue." warn "Workaround: run 'openshell gateway destroy && openshell gateway start'" warn "and re-run this script." fi - fail "Sandbox created but not Ready (phase: ${SANDBOX_PHASE:-unknown}). Check 'openshell sandbox get nemoclaw'." + fail "Sandbox created but not Ready (phase: ${SANDBOX_PHASE:-unknown}). Check 'openshell sandbox get ${SANDBOX_NAME}'." fi # 6. Done diff --git a/scripts/smoke-macos-install.sh b/scripts/smoke-macos-install.sh new file mode 100644 index 00000000000..443ef86a95d --- /dev/null +++ b/scripts/smoke-macos-install.sh @@ -0,0 +1,318 @@ +#!/usr/bin/env bash +# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 +# +# Run the primary NemoClaw install flow on a local machine, capture logs, +# then uninstall and verify cleanup. Intended for manual smoke validation. + +set -euo pipefail + +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +info() { echo -e "${GREEN}[smoke]${NC} $1"; } +warn() { echo -e "${YELLOW}[smoke]${NC} $1"; } +fail() { echo -e "${RED}[smoke]${NC} $1"; exit 1; } + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" +# shellcheck source=./lib/runtime.sh +. "$SCRIPT_DIR/lib/runtime.sh" + +SANDBOX_NAME="smoke-$(date +%Y%m%d%H%M%S)" +LOG_DIR="${TMPDIR:-/tmp}/nemoclaw-smoke" +RUNTIME="" +ALLOW_EXISTING_STATE=false +KEEP_LOGS=false +KEEP_OPEN_SHELL=true +DELETE_MODELS=false + +INSTALL_LOG="" +UNINSTALL_LOG="" +INSTALL_STATUS=1 +UNINSTALL_STATUS=1 +ANSWERS_PIPE="" +ANSWER_WRITER_PID="" +LOG_FOLLOW_PID="" + +stop_answer_writer() { + if [ -n "$ANSWER_WRITER_PID" ] && kill -0 "$ANSWER_WRITER_PID" 2>/dev/null; then + kill "$ANSWER_WRITER_PID" 2>/dev/null || true + wait "$ANSWER_WRITER_PID" 2>/dev/null || true + fi + ANSWER_WRITER_PID="" +} + +usage() { + cat <<'EOF' +Usage: ./scripts/smoke-macos-install.sh [options] + +Options: + --sandbox-name Sandbox name to feed into install.sh + --log-dir Directory for install/uninstall logs + --runtime Select runtime: colima or docker-desktop + --allow-existing-state Allow running even if NemoClaw/OpenShell state already exists + --keep-logs Preserve log files after success + --remove-openshell Allow uninstall.sh to remove openshell + --delete-models Allow uninstall.sh to delete Ollama models + -h, --help Show this help + +Environment: + NVIDIA_API_KEY Required for the cloud install path +EOF +} + +while [ $# -gt 0 ]; do + case "$1" in + --sandbox-name) + SANDBOX_NAME="${2:-}" + [ -n "$SANDBOX_NAME" ] || fail "--sandbox-name requires a value" + shift 2 + ;; + --log-dir) + LOG_DIR="${2:-}" + [ -n "$LOG_DIR" ] || fail "--log-dir requires a value" + shift 2 + ;; + --runtime) + RUNTIME="${2:-}" + [ -n "$RUNTIME" ] || fail "--runtime requires a value" + shift 2 + ;; + --allow-existing-state) + ALLOW_EXISTING_STATE=true + shift + ;; + --keep-logs) + KEEP_LOGS=true + shift + ;; + --remove-openshell) + KEEP_OPEN_SHELL=false + shift + ;; + --delete-models) + DELETE_MODELS=true + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + fail "Unknown argument: $1" + ;; + esac +done + +[ -n "${NVIDIA_API_KEY:-}" ] || fail "NVIDIA_API_KEY must be set for the smoke install flow." +[ -x "$REPO_DIR/install.sh" ] || fail "install.sh not found at repo root." +[ -x "$REPO_DIR/uninstall.sh" ] || fail "uninstall.sh not found at repo root." + +validate_sandbox_name() { + if ! [[ "$SANDBOX_NAME" =~ ^[a-z0-9]([a-z0-9-]*[a-z0-9])?$ ]]; then + fail "Invalid sandbox name '$SANDBOX_NAME'. Use lowercase letters, numbers, and hyphens." + fi +} + +select_runtime() { + case "$RUNTIME" in + "") + return 0 + ;; + colima) + local socket_path + socket_path="$(find_colima_docker_socket || true)" + [ -n "$socket_path" ] || fail "Requested runtime 'colima', but no Colima Docker socket was found." + export DOCKER_HOST="unix://$socket_path" + info "Using runtime 'colima' via $socket_path" + ;; + docker-desktop) + local socket_path + socket_path="$(find_docker_desktop_socket || true)" + [ -n "$socket_path" ] || fail "Requested runtime 'docker-desktop', but no Docker Desktop socket was found." + export DOCKER_HOST="unix://$socket_path" + info "Using runtime 'docker-desktop' via $socket_path" + ;; + *) + fail "Unsupported runtime '$RUNTIME'. Use 'colima' or 'docker-desktop'." + ;; + esac +} + +ensure_clean_start() { + if [ "$ALLOW_EXISTING_STATE" = true ]; then + return 0 + fi + + if [ -d "$HOME/.nemoclaw" ] || [ -d "$HOME/.config/nemoclaw" ] || [ -d "$HOME/.config/openshell" ]; then + fail "Existing NemoClaw/OpenShell state detected. Re-run with --allow-existing-state if you really want to test on this machine." + fi + + if command -v openshell > /dev/null 2>&1; then + if openshell sandbox list 2>/dev/null | grep -Eq '[[:alnum:]]'; then + fail "Existing OpenShell sandboxes detected. Re-run with --allow-existing-state only if you are prepared for uninstall.sh to remove them." + fi + fi +} + +feed_install_answers() { + local answers_pipe="$1" + local install_log="$2" + + ( + printf '%s\n' "$SANDBOX_NAME" + + while :; do + if [ -f "$install_log" ] && grep -q "OpenClaw gateway launched inside sandbox" "$install_log"; then + break + fi + sleep 1 + done + + printf 'n\n' + ) > "$answers_pipe" +} + +start_log_follow() { + local logfile="$1" + : > "$logfile" + tail -n +1 -f "$logfile" & + LOG_FOLLOW_PID=$! +} + +stop_log_follow() { + if [ -n "$LOG_FOLLOW_PID" ] && kill -0 "$LOG_FOLLOW_PID" 2>/dev/null; then + kill "$LOG_FOLLOW_PID" 2>/dev/null || true + wait "$LOG_FOLLOW_PID" 2>/dev/null || true + fi + LOG_FOLLOW_PID="" +} + +run_install() { + local answers_pipe="$1" + info "Running install.sh with sandbox '$SANDBOX_NAME'" + feed_install_answers "$answers_pipe" "$INSTALL_LOG" & + ANSWER_WRITER_PID=$! + start_log_follow "$INSTALL_LOG" + set +e + bash "$REPO_DIR/install.sh" < "$answers_pipe" >> "$INSTALL_LOG" 2>&1 + INSTALL_STATUS=$? + set -e + stop_log_follow + stop_answer_writer + return 0 +} + +run_uninstall() { + local -a args=(--yes) + if [ "$KEEP_OPEN_SHELL" = true ]; then + args+=(--keep-openshell) + fi + if [ "$DELETE_MODELS" = true ]; then + args+=(--delete-models) + fi + + info "Running uninstall.sh for cleanup" + start_log_follow "$UNINSTALL_LOG" + set +e + bash "$REPO_DIR/uninstall.sh" "${args[@]}" >> "$UNINSTALL_LOG" 2>&1 + UNINSTALL_STATUS=$? + set -e + stop_log_follow + return 0 +} + +verify_cleanup() { + local leftovers=0 + + if [ -d "$HOME/.nemoclaw" ] || [ -d "$HOME/.config/nemoclaw" ]; then + warn "NemoClaw state directories still exist under HOME." + leftovers=1 + fi + + if command -v openshell > /dev/null 2>&1; then + local sandbox_output + sandbox_output="$(openshell sandbox list 2>/dev/null || true)" + if printf '%s' "$sandbox_output" | grep -Eq '[[:alnum:]]'; then + warn "OpenShell still reports sandbox entries after uninstall." + leftovers=1 + fi + fi + + if command -v docker > /dev/null 2>&1 && docker info > /dev/null 2>&1; then + local related_containers + related_containers="$( + docker ps -a --format '{{.Image}} {{.Names}}' 2>/dev/null \ + | awk 'BEGIN { IGNORECASE=1 } /openshell-cluster|openshell|openclaw|nemoclaw/ { print }' + )" + if [ -n "$related_containers" ]; then + warn "Related Docker containers remain after uninstall:" + printf '%s\n' "$related_containers" + leftovers=1 + fi + fi + + return "$leftovers" +} + +cleanup() { + stop_log_follow + + stop_answer_writer + + if [ -n "$ANSWERS_PIPE" ] && [ -p "$ANSWERS_PIPE" ]; then + rm -f "$ANSWERS_PIPE" + fi + + if [ -n "$UNINSTALL_LOG" ]; then + run_uninstall + if [ "$UNINSTALL_STATUS" -ne 0 ]; then + warn "uninstall.sh exited with status $UNINSTALL_STATUS" + fi + if ! verify_cleanup; then + warn "Cleanup verification found leftover state." + else + info "Cleanup verification passed" + fi + fi + + if [ "$KEEP_LOGS" = false ] && [ "$INSTALL_STATUS" -eq 0 ] && [ "$UNINSTALL_STATUS" -eq 0 ]; then + rm -f "$INSTALL_LOG" "$UNINSTALL_LOG" + rmdir "$LOG_DIR" 2>/dev/null || true + else + info "Install log: $INSTALL_LOG" + info "Uninstall log: $UNINSTALL_LOG" + fi +} + +main() { + validate_sandbox_name + select_runtime + ensure_clean_start + + mkdir -p "$LOG_DIR" + local stamp + stamp="$(date +%Y%m%d-%H%M%S)" + INSTALL_LOG="$LOG_DIR/install-$stamp.log" + UNINSTALL_LOG="$LOG_DIR/uninstall-$stamp.log" + + ANSWERS_PIPE="$(mktemp -u "${TMPDIR:-/tmp}/nemoclaw-smoke-answers-XXXXXX")" + mkfifo "$ANSWERS_PIPE" + trap cleanup EXIT + + info "Logs will be written under $LOG_DIR" + run_install "$ANSWERS_PIPE" + + if [ "$INSTALL_STATUS" -ne 0 ]; then + fail "install.sh failed with status $INSTALL_STATUS. See $INSTALL_LOG" + fi + + info "install.sh completed successfully" +} + +if [ "${BASH_SOURCE[0]}" = "$0" ]; then + main "$@" +fi diff --git a/scripts/start-services.sh b/scripts/start-services.sh index 7c3bd30ecc8..cbce0f18359 100755 --- a/scripts/start-services.sh +++ b/scripts/start-services.sh @@ -19,7 +19,7 @@ REPO_DIR="$(cd "$SCRIPT_DIR/.." && pwd)" DASHBOARD_PORT="${DASHBOARD_PORT:-18789}" # ── Parse flags ────────────────────────────────────────────────── -SANDBOX_NAME="${NEMOCLAW_SANDBOX:-default}" +SANDBOX_NAME="${NEMOCLAW_SANDBOX:-${SANDBOX_NAME:-default}}" ACTION="start" while [ $# -gt 0 ]; do @@ -140,7 +140,7 @@ do_start() { # Telegram bridge (only if token provided) if [ -n "${TELEGRAM_BOT_TOKEN:-}" ]; then - start_service telegram-bridge \ + SANDBOX_NAME="$SANDBOX_NAME" start_service telegram-bridge \ node "$REPO_DIR/scripts/telegram-bridge.js" fi diff --git a/scripts/telegram-bridge.js b/scripts/telegram-bridge.js index 5d1af0be5ed..80a29069d8a 100755 --- a/scripts/telegram-bridge.js +++ b/scripts/telegram-bridge.js @@ -18,6 +18,13 @@ const https = require("https"); const { execSync, spawn } = require("child_process"); +const { resolveOpenshell } = require("../bin/lib/resolve-openshell"); + +const OPENSHELL = resolveOpenshell(); +if (!OPENSHELL) { + console.error("openshell not found on PATH or in common locations"); + process.exit(1); +} const TOKEN = process.env.TELEGRAM_BOT_TOKEN; const API_KEY = process.env.NVIDIA_API_KEY; @@ -85,7 +92,7 @@ async function sendTyping(chatId) { function runAgentInSandbox(message, sessionId) { return new Promise((resolve) => { - const sshConfig = execSync(`openshell sandbox ssh-config ${SANDBOX}`, { encoding: "utf-8" }); + const sshConfig = execSync(`"${OPENSHELL}" sandbox ssh-config "${SANDBOX}"`, { encoding: "utf-8" }); // Write temp ssh config const confPath = `/tmp/nemoclaw-tg-ssh-${sessionId}.conf`; diff --git a/test/cli.test.js b/test/cli.test.js index 3f67e4a875b..7a737c2ca4e 100644 --- a/test/cli.test.js +++ b/test/cli.test.js @@ -56,4 +56,10 @@ describe("CLI dispatch", () => { // With empty HOME, should say no sandboxes assert.ok(r.out.includes("No sandboxes")); }); + + it("unknown onboard option exits 1", () => { + const r = run("onboard --non-interactiv"); + assert.equal(r.code, 1); + assert.ok(r.out.includes("Unknown onboard option")); + }); }); diff --git a/test/credentials.test.js b/test/credentials.test.js new file mode 100644 index 00000000000..08ce5d7985b --- /dev/null +++ b/test/credentials.test.js @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +describe("credential prompts", () => { + it("exits cleanly when answers are staged through a pipe", () => { + const script = ` + set -euo pipefail + pipe="$(mktemp -u)" + mkfifo "$pipe" + trap 'rm -f "$pipe"' EXIT + { + printf 'sandbox-name\\n' + sleep 1 + printf 'n\\n' + } > "$pipe" & + node -e 'const { prompt } = require(${JSON.stringify(path.join(__dirname, "..", "bin", "lib", "credentials"))}); (async()=>{ await prompt("first: "); await prompt("second: "); })().catch(err=>{ console.error(err); process.exit(1); });' < "$pipe" + `; + + const result = spawnSync("bash", ["-lc", script], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + timeout: 5000, + }); + + assert.equal(result.status, 0); + }); +}); diff --git a/test/e2e/Dockerfile.full-e2e b/test/e2e/Dockerfile.full-e2e new file mode 100644 index 00000000000..e514c79afd3 --- /dev/null +++ b/test/e2e/Dockerfile.full-e2e @@ -0,0 +1,29 @@ +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive + +RUN apt-get update && apt-get install -y --no-install-recommends \ + curl git ca-certificates bash python3 sudo jq \ + && rm -rf /var/lib/apt/lists/* + +# Install Docker CLI only (NOT Docker daemon — will use host socket at runtime) +RUN curl -fsSL https://download.docker.com/linux/ubuntu/gpg | gpg --dearmor -o /etc/apt/keyrings/docker.gpg && \ + echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \ + https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo $VERSION_CODENAME) stable" \ + > /etc/apt/sources.list.d/docker.list && \ + apt-get update && apt-get install -y --no-install-recommends docker-ce-cli && \ + rm -rf /var/lib/apt/lists/* + +# Create non-root testuser with sudo and docker group +RUN groupadd -f docker && \ + useradd -m -s /bin/bash -G docker testuser && \ + echo "testuser ALL=(ALL) NOPASSWD:ALL" >> /etc/sudoers + +# Copy repo +COPY . /workspace +RUN chown -R testuser:testuser /workspace + +USER testuser +WORKDIR /workspace + +ENTRYPOINT ["bash", "test/e2e/test-full-e2e.sh"] diff --git a/test/e2e/test-double-onboard.sh b/test/e2e/test-double-onboard.sh new file mode 100755 index 00000000000..7ebdddcd5a7 --- /dev/null +++ b/test/e2e/test-double-onboard.sh @@ -0,0 +1,256 @@ +#!/bin/bash +# Double onboard: verify that consecutive `nemoclaw onboard` runs recover +# automatically from stale state (gateway, port forward, registry entries) +# left behind by a previous run. +# +# Regression test for issues #21, #22, #140, #152, #397. +# +# Key insight: running onboard without NVIDIA_API_KEY in non-interactive +# mode causes process.exit(1) at step 4, but steps 1-3 (preflight, +# gateway, sandbox) complete first — naturally simulating an unclean exit. +# +# Prerequisites: +# - Docker running +# - openshell CLI installed +# - nemoclaw CLI installed +# - NVIDIA_API_KEY must NOT be set +# +# Usage: +# unset NVIDIA_API_KEY +# bash test/e2e/test-double-onboard.sh + +set -uo pipefail + +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 + +pass() { ((PASS++)); ((TOTAL++)); printf '\033[32m PASS: %s\033[0m\n' "$1"; } +fail() { ((FAIL++)); ((TOTAL++)); printf '\033[31m FAIL: %s\033[0m\n' "$1"; } +skip() { ((SKIP++)); ((TOTAL++)); printf '\033[33m SKIP: %s\033[0m\n' "$1"; } +section() { echo ""; printf '\033[1;36m=== %s ===\033[0m\n' "$1"; } +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +SANDBOX_A="e2e-double-a" +SANDBOX_B="e2e-double-b" +REGISTRY="$HOME/.nemoclaw/sandboxes.json" + +# ══════════════════════════════════════════════════════════════════ +# Phase 0: Pre-cleanup +# ══════════════════════════════════════════════════════════════════ +section "Phase 0: Pre-cleanup" +info "Destroying any leftover test sandboxes/gateway from previous runs..." +# Use nemoclaw destroy (not just openshell sandbox delete) to also clean +# the nemoclaw registry at ~/.nemoclaw/sandboxes.json. Stale registry +# entries from a previous run would cause Phase 2 to exit with +# "Sandbox already exists" before the test even starts. +if command -v nemoclaw > /dev/null 2>&1; then + nemoclaw "$SANDBOX_A" destroy 2>/dev/null || true + nemoclaw "$SANDBOX_B" destroy 2>/dev/null || true +fi +openshell sandbox delete "$SANDBOX_A" 2>/dev/null || true +openshell sandbox delete "$SANDBOX_B" 2>/dev/null || true +openshell forward stop 18789 2>/dev/null || true +openshell gateway destroy -g nemoclaw 2>/dev/null || true +pass "Pre-cleanup complete" + +# ══════════════════════════════════════════════════════════════════ +# Phase 1: Prerequisites +# ══════════════════════════════════════════════════════════════════ +section "Phase 1: Prerequisites" + +if docker info > /dev/null 2>&1; then + pass "Docker is running" +else + fail "Docker is not running — cannot continue" + exit 1 +fi + +if command -v openshell > /dev/null 2>&1; then + pass "openshell CLI installed" +else + fail "openshell CLI not found — cannot continue" + exit 1 +fi + +if command -v nemoclaw > /dev/null 2>&1; then + pass "nemoclaw CLI installed" +else + fail "nemoclaw CLI not found — cannot continue" + exit 1 +fi + +if [ -n "${NVIDIA_API_KEY:-}" ]; then + fail "NVIDIA_API_KEY is set — this test requires it UNSET (unset NVIDIA_API_KEY)" + exit 1 +else + pass "NVIDIA_API_KEY is not set (required for controlled step-4 exit)" +fi + +# ══════════════════════════════════════════════════════════════════ +# Phase 2: First onboard (e2e-double-a) — leaves stale state +# ══════════════════════════════════════════════════════════════════ +section "Phase 2: First onboard ($SANDBOX_A)" +info "Running nemoclaw onboard — expect exit 1 (no API key)..." + +# Write to temp file to avoid openshell FD inheritance blocking $() +ONBOARD_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_SANDBOX_NAME="$SANDBOX_A" \ + NEMOCLAW_POLICY_MODE=skip \ + nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 +exit1=$? +output1="$(cat "$ONBOARD_LOG")" +rm -f "$ONBOARD_LOG" + +if [ $exit1 -eq 1 ]; then + pass "First onboard exited 1 (step 4 failed as expected)" +else + fail "First onboard exited $exit1 (expected 1)" +fi + +echo "$output1" | grep -q "Sandbox '${SANDBOX_A}' created" \ + && pass "Sandbox '$SANDBOX_A' created (step 3 completed)" \ + || fail "Sandbox creation not confirmed in output" + +# Verify stale state was left behind +openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw" \ + && pass "Gateway is still running (stale state)" \ + || fail "Gateway is not running after first onboard" + +openshell sandbox get "$SANDBOX_A" > /dev/null 2>&1 \ + && pass "Sandbox '$SANDBOX_A' exists in openshell" \ + || fail "Sandbox '$SANDBOX_A' not found in openshell" + +[ -f "$REGISTRY" ] && grep -q "$SANDBOX_A" "$REGISTRY" \ + && pass "Registry contains '$SANDBOX_A'" \ + || fail "Registry does not contain '$SANDBOX_A'" + +info "Stale state confirmed — NOT cleaning up before next onboard" + +# ══════════════════════════════════════════════════════════════════ +# Phase 3: Second onboard — SAME name (e2e-double-a) +# ══════════════════════════════════════════════════════════════════ +section "Phase 3: Second onboard ($SANDBOX_A — same name, stale state)" +info "Running nemoclaw onboard with NEMOCLAW_RECREATE_SANDBOX=1..." + +ONBOARD_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_SANDBOX_NAME="$SANDBOX_A" \ + NEMOCLAW_RECREATE_SANDBOX=1 \ + NEMOCLAW_POLICY_MODE=skip \ + nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 +exit2=$? +output2="$(cat "$ONBOARD_LOG")" +rm -f "$ONBOARD_LOG" + +# Step 4 still fails (no API key), but steps 1-3 should succeed +if [ $exit2 -eq 1 ]; then + pass "Second onboard exited 1 (step 4 failed as expected)" +else + fail "Second onboard exited $exit2 (expected 1)" +fi + +echo "$output2" | grep -q "Cleaning up previous NemoClaw session" \ + && pass "Stale session cleanup fired on second onboard" \ + || fail "Stale session cleanup did NOT fire (regression: #397)" + +echo "$output2" | grep -q "Port 8080 is not available" \ + && fail "Port 8080 conflict detected (regression: #21)" \ + || pass "No port 8080 conflict" + +echo "$output2" | grep -q "Port 18789 is not available" \ + && fail "Port 18789 conflict detected" \ + || pass "No port 18789 conflict" + +echo "$output2" | grep -q "Sandbox '${SANDBOX_A}' created" \ + && pass "Sandbox '$SANDBOX_A' recreated" \ + || fail "Sandbox '$SANDBOX_A' was not recreated" + +openshell gateway info -g nemoclaw 2>/dev/null | grep -q "nemoclaw" \ + && pass "Gateway running after second onboard" \ + || fail "Gateway not running after second onboard" + +# ══════════════════════════════════════════════════════════════════ +# Phase 4: Third onboard — DIFFERENT name (e2e-double-b) +# ══════════════════════════════════════════════════════════════════ +section "Phase 4: Third onboard ($SANDBOX_B — different name, stale state)" +info "Running nemoclaw onboard with new sandbox name..." + +ONBOARD_LOG="$(mktemp)" +NEMOCLAW_NON_INTERACTIVE=1 \ + NEMOCLAW_SANDBOX_NAME="$SANDBOX_B" \ + NEMOCLAW_POLICY_MODE=skip \ + nemoclaw onboard --non-interactive > "$ONBOARD_LOG" 2>&1 +exit3=$? +output3="$(cat "$ONBOARD_LOG")" +rm -f "$ONBOARD_LOG" + +if [ $exit3 -eq 1 ]; then + pass "Third onboard exited 1 (step 4 failed as expected)" +else + fail "Third onboard exited $exit3 (expected 1)" +fi + +echo "$output3" | grep -q "Cleaning up previous NemoClaw session" \ + && pass "Stale session cleanup fired on third onboard" \ + || fail "Stale session cleanup did NOT fire on third onboard" + +echo "$output3" | grep -q "Port 8080 is not available" \ + && fail "Port 8080 conflict on third onboard (regression)" \ + || pass "No port 8080 conflict on third onboard" + +echo "$output3" | grep -q "Port 18789 is not available" \ + && fail "Port 18789 conflict on third onboard" \ + || pass "No port 18789 conflict on third onboard" + +echo "$output3" | grep -q "Sandbox '${SANDBOX_B}' created" \ + && pass "Sandbox '$SANDBOX_B' created" \ + || fail "Sandbox '$SANDBOX_B' was not created" + +# ══════════════════════════════════════════════════════════════════ +# Phase 5: Final cleanup +# ══════════════════════════════════════════════════════════════════ +section "Phase 5: Final cleanup" + +nemoclaw "$SANDBOX_A" destroy 2>/dev/null || true +nemoclaw "$SANDBOX_B" destroy 2>/dev/null || true +openshell sandbox delete "$SANDBOX_A" 2>/dev/null || true +openshell sandbox delete "$SANDBOX_B" 2>/dev/null || true +openshell forward stop 18789 2>/dev/null || true +openshell gateway destroy -g nemoclaw 2>/dev/null || true + +openshell sandbox get "$SANDBOX_A" > /dev/null 2>&1 \ + && fail "Sandbox '$SANDBOX_A' still exists after cleanup" \ + || pass "Sandbox '$SANDBOX_A' cleaned up" + +openshell sandbox get "$SANDBOX_B" > /dev/null 2>&1 \ + && fail "Sandbox '$SANDBOX_B' still exists after cleanup" \ + || pass "Sandbox '$SANDBOX_B' cleaned up" + +[ -f "$REGISTRY" ] && grep -q "$SANDBOX_A\|$SANDBOX_B" "$REGISTRY" \ + && fail "Registry still contains test sandbox entries" \ + || pass "Registry cleaned up" + +pass "Final cleanup complete" + +# ══════════════════════════════════════════════════════════════════ +# Summary +# ══════════════════════════════════════════════════════════════════ +echo "" +echo "========================================" +echo " Double Onboard E2E Results:" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo " Skipped: $SKIP" +echo " Total: $TOTAL" +echo "========================================" + +if [ "$FAIL" -eq 0 ]; then + printf '\n\033[1;32m Double onboard PASSED — stale state recovery verified.\033[0m\n' + exit 0 +else + printf '\n\033[1;31m %d test(s) failed.\033[0m\n' "$FAIL" + exit 1 +fi diff --git a/test/e2e/test-full-e2e.sh b/test/e2e/test-full-e2e.sh new file mode 100644 index 00000000000..1e9ae22f651 --- /dev/null +++ b/test/e2e/test-full-e2e.sh @@ -0,0 +1,333 @@ +#!/bin/bash +# Full E2E: install → onboard → verify inference (REAL services, no mocks) +# +# Proves the COMPLETE user journey including real inference against +# the NVIDIA Cloud API. Runs install.sh --non-interactive which handles +# Node.js, openshell, NemoClaw, and onboard setup automatically. +# +# Prerequisites: +# - Docker running +# - NVIDIA_API_KEY set (real key, starts with nvapi-) +# - Network access to integrate.api.nvidia.com +# +# Environment variables: +# NEMOCLAW_NON_INTERACTIVE=1 — required (enables non-interactive install + onboard) +# NEMOCLAW_SANDBOX_NAME — sandbox name (default: e2e-nightly) +# NEMOCLAW_RECREATE_SANDBOX=1 — recreate sandbox if it exists from a previous run +# NVIDIA_API_KEY — required for NVIDIA Cloud API inference +# +# Usage: +# NEMOCLAW_NON_INTERACTIVE=1 NVIDIA_API_KEY=nvapi-... bash test/e2e/test-full-e2e.sh +# +# See: https://github.com/NVIDIA/NemoClaw/issues/71 + +set -uo pipefail + +PASS=0 +FAIL=0 +SKIP=0 +TOTAL=0 + +pass() { ((PASS++)); ((TOTAL++)); printf '\033[32m PASS: %s\033[0m\n' "$1"; } +fail() { ((FAIL++)); ((TOTAL++)); printf '\033[31m FAIL: %s\033[0m\n' "$1"; } +skip() { ((SKIP++)); ((TOTAL++)); printf '\033[33m SKIP: %s\033[0m\n' "$1"; } +section() { echo ""; printf '\033[1;36m=== %s ===\033[0m\n' "$1"; } +info() { printf '\033[1;34m [info]\033[0m %s\n' "$1"; } + +# Parse chat completion response — handles both content and reasoning_content +# (nemotron-3-super is a reasoning model that may put output in reasoning_content) +parse_chat_content() { + python3 -c " +import json, sys +try: + r = json.load(sys.stdin) + c = r['choices'][0]['message'] + content = c.get('content') or c.get('reasoning_content') or '' + print(content.strip()) +except Exception as e: + print(f'PARSE_ERROR: {e}', file=sys.stderr) + sys.exit(1) +" +} + +# Determine repo root +if [ -d /workspace ] && [ -f /workspace/install.sh ]; then + REPO="/workspace" +elif [ -f "$(cd "$(dirname "$0")/../.." && pwd)/install.sh" ]; then + REPO="$(cd "$(dirname "$0")/../.." && pwd)" +else + echo "ERROR: Cannot find repo root." + exit 1 +fi + +SANDBOX_NAME="${NEMOCLAW_SANDBOX_NAME:-e2e-nightly}" + +# ══════════════════════════════════════════════════════════════════ +# Phase 0: Pre-cleanup +# ══════════════════════════════════════════════════════════════════ +section "Phase 0: Pre-cleanup" +info "Destroying any leftover sandbox/gateway from previous runs..." +if command -v nemoclaw > /dev/null 2>&1; then + nemoclaw "$SANDBOX_NAME" destroy 2>/dev/null || true +fi +if command -v openshell > /dev/null 2>&1; then + openshell sandbox delete "$SANDBOX_NAME" 2>/dev/null || true + openshell gateway destroy -g nemoclaw 2>/dev/null || true +fi +pass "Pre-cleanup complete" + +# ══════════════════════════════════════════════════════════════════ +# Phase 1: Prerequisites +# ══════════════════════════════════════════════════════════════════ +section "Phase 1: Prerequisites" + +if docker info > /dev/null 2>&1; then + pass "Docker is running" +else + fail "Docker is not running — cannot continue" + exit 1 +fi + +if [ -n "${NVIDIA_API_KEY:-}" ] && [[ "${NVIDIA_API_KEY}" == nvapi-* ]]; then + pass "NVIDIA_API_KEY is set (starts with nvapi-)" +else + fail "NVIDIA_API_KEY not set or invalid — required for live inference" + exit 1 +fi + +if curl -sf --max-time 10 https://integrate.api.nvidia.com/v1/models > /dev/null 2>&1; then + pass "Network access to integrate.api.nvidia.com" +else + fail "Cannot reach integrate.api.nvidia.com" + exit 1 +fi + +if [ "${NEMOCLAW_NON_INTERACTIVE:-}" != "1" ]; then + fail "NEMOCLAW_NON_INTERACTIVE=1 is required" + exit 1 +fi + +# ══════════════════════════════════════════════════════════════════ +# Phase 2: Install nemoclaw (non-interactive mode) +# ══════════════════════════════════════════════════════════════════ +section "Phase 2: Install nemoclaw (non-interactive mode)" + +cd "$REPO" || { fail "Could not cd to repo root: $REPO"; exit 1; } + +info "Running install.sh --non-interactive..." +info "This installs Node.js, openshell, NemoClaw, and runs onboard." +info "Expected duration: 5-10 minutes on first run." + +INSTALL_LOG="/tmp/nemoclaw-e2e-install.log" +# Write to a file instead of piping through tee. openshell's background +# port-forward inherits pipe file descriptors, which prevents tee from exiting. +# Use tail -f in the background for real-time output in CI logs. +bash install.sh --non-interactive > "$INSTALL_LOG" 2>&1 & +install_pid=$! +tail -f "$INSTALL_LOG" --pid=$install_pid 2>/dev/null & +tail_pid=$! +wait $install_pid +install_exit=$? +kill $tail_pid 2>/dev/null || true +wait $tail_pid 2>/dev/null || true + +# Source shell profile to pick up nvm/PATH changes from install.sh +if [ -f "$HOME/.bashrc" ]; then + source "$HOME/.bashrc" 2>/dev/null || true +fi +# Ensure nvm is loaded in current shell +export NVM_DIR="${NVM_DIR:-$HOME/.nvm}" +[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" +# Ensure ~/.local/bin is on PATH (openshell may be installed there in non-interactive mode) +if [ -d "$HOME/.local/bin" ] && [[ ":$PATH:" != *":$HOME/.local/bin:"* ]]; then + export PATH="$HOME/.local/bin:$PATH" +fi + +if [ $install_exit -eq 0 ]; then + pass "install.sh completed (exit 0)" +else + fail "install.sh failed (exit $install_exit)" + exit 1 +fi + +# Verify nemoclaw is on PATH +if command -v nemoclaw > /dev/null 2>&1; then + pass "nemoclaw installed at $(command -v nemoclaw)" +else + fail "nemoclaw not found on PATH after install" + exit 1 +fi + +# Verify openshell was installed +if command -v openshell > /dev/null 2>&1; then + pass "openshell installed ($(openshell --version 2>&1 || echo unknown))" +else + fail "openshell not found on PATH after install" + exit 1 +fi + +nemoclaw --help > /dev/null 2>&1 \ + && pass "nemoclaw --help exits 0" \ + || fail "nemoclaw --help failed" + +# ══════════════════════════════════════════════════════════════════ +# Phase 3: Sandbox verification +# ══════════════════════════════════════════════════════════════════ +section "Phase 3: Sandbox verification" + +# 3a: nemoclaw list +if list_output=$(nemoclaw list 2>&1); then + echo "$list_output" | grep -Fq -- "$SANDBOX_NAME" \ + && pass "nemoclaw list contains '${SANDBOX_NAME}'" \ + || fail "nemoclaw list does not contain '${SANDBOX_NAME}'" +else + fail "nemoclaw list failed: ${list_output:0:200}" +fi + +# 3b: nemoclaw status +status_output=$(nemoclaw "$SANDBOX_NAME" status 2>&1) +if [ $? -eq 0 ]; then + pass "nemoclaw ${SANDBOX_NAME} status exits 0" +else + fail "nemoclaw ${SANDBOX_NAME} status failed: ${status_output:0:200}" +fi + +# 3c: Inference must be configured by onboard (no fallback — if onboard +# failed to configure it, that's a bug we want to catch) +if inf_check=$(openshell inference get 2>&1); then + echo "$inf_check" | grep -qi "nvidia-nim" \ + && pass "Inference configured via onboard" \ + || fail "Inference not configured — onboard did not set up nvidia-nim provider" +else + fail "openshell inference get failed: ${inf_check:0:200}" +fi + +# 3d: Policy presets applied +if policy_output=$(openshell policy get --full "$SANDBOX_NAME" 2>&1); then + echo "$policy_output" | grep -qi "network_policies" \ + && pass "Policy applied to sandbox" \ + || fail "No network policy found on sandbox" + + # Check that at least npm or pypi preset endpoints are present (onboard auto-suggests these) + echo "$policy_output" | grep -qi "registry.npmjs.org\|pypi.org" \ + && pass "Policy presets (npm/pypi) detected in sandbox policy" \ + || skip "Could not confirm npm/pypi presets in policy (may vary by environment)" +else + fail "openshell policy get failed: ${policy_output:0:200}" +fi + +# ══════════════════════════════════════════════════════════════════ +# Phase 4: Live inference — the real proof +# ══════════════════════════════════════════════════════════════════ +section "Phase 4: Live inference" + +# ── Test 4a: Direct NVIDIA Cloud API ── +info "[LIVE] Direct API test → integrate.api.nvidia.com..." +api_response=$(curl -s --max-time 30 \ + -X POST https://integrate.api.nvidia.com/v1/chat/completions \ + -H "Content-Type: application/json" \ + -H "Authorization: Bearer $NVIDIA_API_KEY" \ + -d '{ + "model": "nvidia/nemotron-3-super-120b-a12b", + "messages": [{"role": "user", "content": "Reply with exactly one word: PONG"}], + "max_tokens": 100 + }' 2>/dev/null) || true + +if [ -n "$api_response" ]; then + api_content=$(echo "$api_response" | parse_chat_content 2>/dev/null) || true + if echo "$api_content" | grep -qi "PONG"; then + pass "[LIVE] Direct API: model responded with PONG" + else + fail "[LIVE] Direct API: expected PONG, got: ${api_content:0:200}" + fi +else + fail "[LIVE] Direct API: empty response from curl" +fi + +# ── Test 4b: Inference through the sandbox (THE definitive test) ── +info "[LIVE] Sandbox inference test → user → sandbox → gateway → NVIDIA API..." +ssh_config="$(mktemp)" +sandbox_response="" + +if openshell sandbox ssh-config "$SANDBOX_NAME" > "$ssh_config" 2>/dev/null; then + # Use timeout if available (Linux, Homebrew), fall back to plain ssh + TIMEOUT_CMD="" + command -v timeout > /dev/null 2>&1 && TIMEOUT_CMD="timeout 90" + command -v gtimeout > /dev/null 2>&1 && TIMEOUT_CMD="gtimeout 90" + sandbox_response=$($TIMEOUT_CMD ssh -F "$ssh_config" \ + -o StrictHostKeyChecking=no \ + -o UserKnownHostsFile=/dev/null \ + -o ConnectTimeout=10 \ + -o LogLevel=ERROR \ + "openshell-${SANDBOX_NAME}" \ + "curl -s --max-time 60 https://inference.local/v1/chat/completions \ + -H 'Content-Type: application/json' \ + -d '{\"model\":\"nvidia/nemotron-3-super-120b-a12b\",\"messages\":[{\"role\":\"user\",\"content\":\"Reply with exactly one word: PONG\"}],\"max_tokens\":100}'" \ + 2>&1) || true +fi +rm -f "$ssh_config" + +if [ -n "$sandbox_response" ]; then + sandbox_content=$(echo "$sandbox_response" | parse_chat_content 2>/dev/null) || true + if echo "$sandbox_content" | grep -qi "PONG"; then + pass "[LIVE] Sandbox inference: model responded with PONG through sandbox" + info "Full path proven: user → sandbox → openshell gateway → NVIDIA Cloud API → response" + else + fail "[LIVE] Sandbox inference: expected PONG, got: ${sandbox_content:0:200}" + fi +else + fail "[LIVE] Sandbox inference: no response from inference.local inside sandbox" +fi + +# ══════════════════════════════════════════════════════════════════ +# Phase 5: NemoClaw CLI operations +# ══════════════════════════════════════════════════════════════════ +section "Phase 5: NemoClaw CLI operations" + +# Note: Policy enforcement (proxy blocking, L4/L7 rules, SSRF protection) +# and sandbox command execution are tested extensively in OpenShell's own +# E2E suite (e2e/python/test_sandbox_policy.py, test_sandbox_api.py). +# NemoClaw tests only that its onboard correctly *configured* the policies +# (Phase 3d above), not that OpenShell *enforces* them. + +# ── Test 5a: nemoclaw logs ── +info "Testing sandbox log retrieval..." +logs_output=$(nemoclaw "$SANDBOX_NAME" logs 2>&1) || true +if [ -n "$logs_output" ]; then + pass "nemoclaw logs: produced output ($(echo "$logs_output" | wc -l | tr -d ' ') lines)" +else + fail "nemoclaw logs: no output" +fi + +# ══════════════════════════════════════════════════════════════════ +# Phase 6: Cleanup +# ══════════════════════════════════════════════════════════════════ +section "Phase 6: Cleanup" + +nemoclaw "$SANDBOX_NAME" destroy 2>&1 | tail -3 || true +openshell gateway destroy -g nemoclaw 2>/dev/null || true + +list_after=$(nemoclaw list 2>&1) +echo "$list_after" | grep -Fq -- "$SANDBOX_NAME" \ + && fail "Sandbox ${SANDBOX_NAME} still in list after destroy" \ + || pass "Sandbox ${SANDBOX_NAME} removed" + +# ══════════════════════════════════════════════════════════════════ +# Summary +# ══════════════════════════════════════════════════════════════════ +echo "" +echo "========================================" +echo " Full E2E Results:" +echo " Passed: $PASS" +echo " Failed: $FAIL" +echo " Skipped: $SKIP" +echo " Total: $TOTAL" +echo "========================================" + +if [ "$FAIL" -eq 0 ]; then + printf '\n\033[1;32m Full E2E PASSED — real inference verified end-to-end.\033[0m\n' + exit 0 +else + printf '\n\033[1;31m %d test(s) failed.\033[0m\n' "$FAIL" + exit 1 +fi diff --git a/test/inference-config.test.js b/test/inference-config.test.js new file mode 100644 index 00000000000..6e6a9bd5b6f --- /dev/null +++ b/test/inference-config.test.js @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { + CLOUD_MODEL_OPTIONS, + DEFAULT_OLLAMA_MODEL, + DEFAULT_ROUTE_CREDENTIAL_ENV, + DEFAULT_ROUTE_PROFILE, + INFERENCE_ROUTE_URL, + MANAGED_PROVIDER_ID, + getOpenClawPrimaryModel, + getProviderSelectionConfig, +} = require("../bin/lib/inference-config"); + +describe("inference selection config", () => { + it("exposes the curated cloud model picker options", () => { + assert.deepEqual( + CLOUD_MODEL_OPTIONS.map((option) => option.id), + [ + "nvidia/nemotron-3-super-120b-a12b", + "moonshotai/kimi-k2.5", + "z-ai/glm5", + "minimaxai/minimax-m2.5", + "qwen/qwen3.5-397b-a17b", + "openai/gpt-oss-120b", + ], + ); + }); + + it("maps ollama-local to the sandbox inference route and default model", () => { + assert.deepEqual(getProviderSelectionConfig("ollama-local"), { + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: DEFAULT_OLLAMA_MODEL, + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, + provider: "ollama-local", + providerLabel: "Local Ollama", + }); + }); + + it("maps nvidia-nim to the sandbox inference route", () => { + assert.deepEqual(getProviderSelectionConfig("nvidia-nim", "nvidia/nemotron-3-super-120b-a12b"), { + endpointType: "custom", + endpointUrl: INFERENCE_ROUTE_URL, + ncpPartner: null, + model: "nvidia/nemotron-3-super-120b-a12b", + profile: DEFAULT_ROUTE_PROFILE, + credentialEnv: DEFAULT_ROUTE_CREDENTIAL_ENV, + provider: "nvidia-nim", + providerLabel: "NVIDIA Cloud API", + }); + }); + + it("builds a qualified OpenClaw primary model for ollama-local", () => { + assert.equal( + getOpenClawPrimaryModel("ollama-local", "nemotron-3-nano:30b"), + `${MANAGED_PROVIDER_ID}/nemotron-3-nano:30b`, + ); + }); +}); diff --git a/test/install-preflight.test.js b/test/install-preflight.test.js index 6f70e197e0e..fa32879657e 100644 --- a/test/install-preflight.test.js +++ b/test/install-preflight.test.js @@ -9,6 +9,9 @@ const path = require("node:path"); const { spawnSync } = require("node:child_process"); const INSTALLER = path.join(__dirname, "..", "install.sh"); +const CURL_PIPE_INSTALLER = path.join(__dirname, "..", "scripts", "install.sh"); +const GITHUB_INSTALL_URL = "git+https://github.com/NVIDIA/NemoClaw.git"; +const TEST_SYSTEM_PATH = "/usr/bin:/bin"; function writeExecutable(target, contents) { fs.writeFileSync(target, contents, { mode: 0o755 }); @@ -50,7 +53,7 @@ exit 98 env: { ...process.env, HOME: tmp, - PATH: `${fakeBin}:${process.env.PATH}`, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, }, }); @@ -61,4 +64,389 @@ exit 98 assert.match(output, /v18\.19\.1/); assert.match(output, /9\.8\.1/); }); + + it("uses the HTTPS GitHub fallback when not installing from a repo checkout", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-fallback-")); + const fakeBin = path.join(tmp, "bin"); + const prefix = path.join(tmp, "prefix"); + const npmLog = path.join(tmp, "npm.log"); + fs.mkdirSync(fakeBin); + fs.mkdirSync(path.join(prefix, "bin"), { recursive: true }); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "v22.14.0" + exit 0 +fi +echo "unexpected node invocation: $*" >&2 +exit 99 +`, + ); + + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +set -euo pipefail +printf '%s\\n' "$*" >> "$NPM_LOG_PATH" +if [ "$1" = "--version" ]; then + echo "10.9.2" + exit 0 +fi +if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then + echo "$NPM_PREFIX" + exit 0 +fi +if [ "$1" = "install" ] && [ "$2" = "-g" ] && [ "$3" = "${GITHUB_INSTALL_URL}" ]; then + cat > "$NPM_PREFIX/bin/nemoclaw" <<'EOS' +#!/usr/bin/env bash +if [ "$1" = "onboard" ]; then + exit 0 +fi +if [ "$1" = "--version" ]; then + echo "v0.1.0-test" + exit 0 +fi +exit 0 +EOS + chmod +x "$NPM_PREFIX/bin/nemoclaw" + exit 0 +fi +echo "unexpected npm invocation: $*" >&2 +exit 98 +`, + ); + + const result = spawnSync("bash", [INSTALLER], { + cwd: tmp, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NPM_PREFIX: prefix, + NPM_LOG_PATH: npmLog, + }, + }); + + assert.equal(result.status, 0); + assert.match(fs.readFileSync(npmLog, "utf-8"), new RegExp(`install -g ${GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}`)); + }); + + it("prints the HTTPS GitHub remediation when the binary is missing", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-remediation-")); + const fakeBin = path.join(tmp, "bin"); + const prefix = path.join(tmp, "prefix"); + fs.mkdirSync(fakeBin); + fs.mkdirSync(path.join(prefix, "bin"), { recursive: true }); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "v22.14.0" + exit 0 +fi +echo "unexpected node invocation: $*" >&2 +exit 99 +`, + ); + + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +set -euo pipefail +if [ "$1" = "--version" ]; then + echo "10.9.2" + exit 0 +fi +if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then + echo "$NPM_PREFIX" + exit 0 +fi +if [ "$1" = "install" ] && [ "$2" = "-g" ] && [ "$3" = "${GITHUB_INSTALL_URL}" ]; then + exit 0 +fi +echo "unexpected npm invocation: $*" >&2 +exit 98 +`, + ); + + const result = spawnSync("bash", [INSTALLER], { + cwd: tmp, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NPM_PREFIX: prefix, + }, + }); + + const output = `${result.stdout}${result.stderr}`; + assert.notEqual(result.status, 0); + assert.match(output, new RegExp(GITHUB_INSTALL_URL.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + assert.doesNotMatch(output, /npm install -g nemoclaw/); + }); + + it("does not silently prefer Colima when both macOS runtimes are available", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-macos-runtime-choice-")); + const fakeBin = path.join(tmp, "bin"); + const colimaSocket = path.join(tmp, ".colima/default/docker.sock"); + const dockerDesktopSocket = path.join(tmp, ".docker/run/docker.sock"); + fs.mkdirSync(fakeBin); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then + echo "v22.14.0" + exit 0 +fi +exit 99 +`, + ); + + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "10.9.2" + exit 0 +fi +echo "/tmp/npm-prefix" +exit 0 +`, + ); + + writeExecutable( + path.join(fakeBin, "docker"), + `#!/usr/bin/env bash +if [ "$1" = "info" ]; then + exit 1 +fi +exit 0 +`, + ); + + writeExecutable( + path.join(fakeBin, "colima"), + `#!/usr/bin/env bash +echo "colima should not be started" >&2 +exit 97 +`, + ); + + writeExecutable( + path.join(fakeBin, "uname"), + `#!/usr/bin/env bash +if [ "$1" = "-s" ]; then + echo "Darwin" + exit 0 +fi +if [ "$1" = "-m" ]; then + echo "arm64" + exit 0 +fi +echo "Darwin" +`, + ); + + const result = spawnSync("bash", [CURL_PIPE_INSTALLER], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_TEST_SOCKET_PATHS: `${colimaSocket}:${dockerDesktopSocket}`, + }, + }); + + const output = `${result.stdout}${result.stderr}`; + assert.notEqual(result.status, 0); + assert.match(output, /Both Colima and Docker Desktop are available/); + assert.doesNotMatch(output, /colima should not be started/); + }); + + it("can run via stdin without a sibling runtime.sh file", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-curl-pipe-installer-")); + const fakeBin = path.join(tmp, "bin"); + const prefix = path.join(tmp, "prefix"); + fs.mkdirSync(fakeBin); + fs.mkdirSync(path.join(prefix, "bin"), { recursive: true }); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then + echo "v22.14.0" + exit 0 +fi +exit 99 +`, + ); + + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +set -euo pipefail +if [ "$1" = "--version" ]; then + echo "10.9.2" + exit 0 +fi +if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then + echo "$NPM_PREFIX" + exit 0 +fi +if [ "$1" = "install" ] && [ "$2" = "-g" ]; then + cat > "$NPM_PREFIX/bin/nemoclaw" <<'EOS' +#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "v0.1.0-test" + exit 0 +fi +exit 0 +EOS + chmod +x "$NPM_PREFIX/bin/nemoclaw" + exit 0 +fi +echo "unexpected npm invocation: $*" >&2 +exit 98 +`, + ); + + writeExecutable( + path.join(fakeBin, "docker"), + `#!/usr/bin/env bash +if [ "$1" = "info" ]; then + exit 0 +fi +exit 0 +`, + ); + + writeExecutable( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "openshell 0.0.9" + exit 0 +fi +exit 0 +`, + ); + + const scriptContents = fs.readFileSync(CURL_PIPE_INSTALLER, "utf-8"); + const result = spawnSync("bash", [], { + cwd: tmp, + input: scriptContents, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NPM_PREFIX: prefix, + }, + }); + + const output = `${result.stdout}${result.stderr}`; + assert.equal(result.status, 0); + assert.match(output, /Installation complete!/); + assert.match(output, /nemoclaw v0\.1\.0-test is ready/); + }); + + it("creates a user-local shim when npm installs outside the current PATH", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-install-shim-")); + const fakeBin = path.join(tmp, "bin"); + const prefix = path.join(tmp, "prefix"); + fs.mkdirSync(fakeBin); + fs.mkdirSync(path.join(prefix, "bin"), { recursive: true }); + fs.mkdirSync(path.join(tmp, ".local"), { recursive: true }); + + writeExecutable( + path.join(fakeBin, "node"), + `#!/usr/bin/env bash +if [ "$1" = "-v" ] || [ "$1" = "--version" ]; then + echo "v22.14.0" + exit 0 +fi +exit 99 +`, + ); + + writeExecutable( + path.join(fakeBin, "npm"), + `#!/usr/bin/env bash +set -euo pipefail +if [ "$1" = "--version" ]; then + echo "10.9.2" + exit 0 +fi +if [ "$1" = "config" ] && [ "$2" = "get" ] && [ "$3" = "prefix" ]; then + echo "$NPM_PREFIX" + exit 0 +fi +if [ "$1" = "install" ] && [ "$2" = "-g" ] && [ "$3" = "${GITHUB_INSTALL_URL}" ]; then + cat > "$NPM_PREFIX/bin/nemoclaw" <<'EOS' +#!/usr/bin/env bash +if [ "$1" = "onboard" ]; then + exit 0 +fi +if [ "$1" = "--version" ]; then + echo "v0.1.0-test" + exit 0 +fi +exit 0 +EOS + chmod +x "$NPM_PREFIX/bin/nemoclaw" + exit 0 +fi +echo "unexpected npm invocation: $*" >&2 +exit 98 +`, + ); + + writeExecutable( + path.join(fakeBin, "docker"), + `#!/usr/bin/env bash +if [ "$1" = "info" ]; then + exit 0 +fi +exit 0 +`, + ); + + writeExecutable( + path.join(fakeBin, "openshell"), + `#!/usr/bin/env bash +if [ "$1" = "--version" ]; then + echo "openshell 0.0.9" + exit 0 +fi +exit 0 +`, + ); + + const result = spawnSync("bash", [INSTALLER], { + cwd: tmp, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmp, + PATH: `${fakeBin}:${TEST_SYSTEM_PATH}`, + NEMOCLAW_NON_INTERACTIVE: "1", + NPM_PREFIX: prefix, + }, + }); + + const shimPath = path.join(tmp, ".local", "bin", "nemoclaw"); + assert.equal(result.status, 0); + assert.equal(fs.readlinkSync(shimPath), path.join(prefix, "bin", "nemoclaw")); + assert.match(`${result.stdout}${result.stderr}`, /Created user-local shim/); + }); }); diff --git a/test/local-inference.test.js b/test/local-inference.test.js new file mode 100644 index 00000000000..5b77ee2f7ad --- /dev/null +++ b/test/local-inference.test.js @@ -0,0 +1,157 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { + CONTAINER_REACHABILITY_IMAGE, + DEFAULT_OLLAMA_MODEL, + getDefaultOllamaModel, + getLocalProviderBaseUrl, + getLocalProviderContainerReachabilityCheck, + getLocalProviderHealthCheck, + getOllamaModelOptions, + getOllamaProbeCommand, + getOllamaWarmupCommand, + parseOllamaList, + validateOllamaModel, + validateLocalProvider, +} = require("../bin/lib/local-inference"); + +describe("local inference helpers", () => { + it("returns the expected base URL for vllm-local", () => { + assert.equal( + getLocalProviderBaseUrl("vllm-local"), + "http://host.openshell.internal:8000/v1", + ); + }); + + it("returns the expected base URL for ollama-local", () => { + assert.equal( + getLocalProviderBaseUrl("ollama-local"), + "http://host.openshell.internal:11434/v1", + ); + }); + + it("returns the expected health check command for ollama-local", () => { + assert.equal( + getLocalProviderHealthCheck("ollama-local"), + "curl -sf http://localhost:11434/api/tags 2>/dev/null", + ); + }); + + it("returns the expected container reachability command for ollama-local", () => { + assert.equal( + getLocalProviderContainerReachabilityCheck("ollama-local"), + `docker run --rm --add-host host.openshell.internal:host-gateway ${CONTAINER_REACHABILITY_IMAGE} -sf http://host.openshell.internal:11434/api/tags 2>/dev/null`, + ); + }); + + it("validates a reachable local provider", () => { + let callCount = 0; + const result = validateLocalProvider("ollama-local", () => { + callCount += 1; + return '{"models":[]}'; + }); + assert.deepEqual(result, { ok: true }); + assert.equal(callCount, 2); + }); + + it("returns a clear error when ollama-local is unavailable", () => { + const result = validateLocalProvider("ollama-local", () => ""); + assert.equal(result.ok, false); + assert.match(result.message, /http:\/\/localhost:11434/); + }); + + it("returns a clear error when ollama-local is not reachable from containers", () => { + let callCount = 0; + const result = validateLocalProvider("ollama-local", () => { + callCount += 1; + return callCount === 1 ? '{"models":[]}' : ""; + }); + assert.equal(result.ok, false); + assert.match(result.message, /host\.openshell\.internal:11434/); + assert.match(result.message, /0\.0\.0\.0:11434/); + }); + + it("returns a clear error when vllm-local is unavailable", () => { + const result = validateLocalProvider("vllm-local", () => ""); + assert.equal(result.ok, false); + assert.match(result.message, /http:\/\/localhost:8000/); + }); + + it("parses model names from ollama list output", () => { + assert.deepEqual( + parseOllamaList( + [ + "NAME ID SIZE MODIFIED", + "nemotron-3-nano:30b abc123 24 GB 2 hours ago", + "qwen3:32b def456 20 GB 1 day ago", + ].join("\n"), + ), + ["nemotron-3-nano:30b", "qwen3:32b"], + ); + }); + + it("returns parsed ollama model options when available", () => { + assert.deepEqual( + getOllamaModelOptions(() => "nemotron-3-nano:30b abc 24 GB now\nqwen3:32b def 20 GB now"), + ["nemotron-3-nano:30b", "qwen3:32b"], + ); + }); + + it("falls back to the default ollama model when list output is empty", () => { + assert.deepEqual(getOllamaModelOptions(() => ""), [DEFAULT_OLLAMA_MODEL]); + }); + + it("prefers the default ollama model when present", () => { + assert.equal( + getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\nnemotron-3-nano:30b def 24 GB now"), + DEFAULT_OLLAMA_MODEL, + ); + }); + + it("falls back to the first listed ollama model when the default is absent", () => { + assert.equal( + getDefaultOllamaModel(() => "qwen3:32b abc 20 GB now\ngemma3:4b def 3 GB now"), + "qwen3:32b", + ); + }); + + it("builds a background warmup command for ollama models", () => { + const command = getOllamaWarmupCommand("nemotron-3-nano:30b"); + assert.match(command, /^nohup curl -s http:\/\/localhost:11434\/api\/generate /); + assert.match(command, /"model":"nemotron-3-nano:30b"/); + assert.match(command, /"keep_alive":"15m"/); + }); + + it("builds a foreground probe command for ollama models", () => { + const command = getOllamaProbeCommand("nemotron-3-nano:30b"); + assert.match(command, /^curl -sS --max-time 120 http:\/\/localhost:11434\/api\/generate /); + assert.match(command, /"model":"nemotron-3-nano:30b"/); + }); + + it("fails ollama model validation when the probe times out or returns nothing", () => { + const result = validateOllamaModel("nemotron-3-nano:30b", () => ""); + assert.equal(result.ok, false); + assert.match(result.message, /did not answer the local probe in time/); + }); + + it("fails ollama model validation when Ollama returns an error payload", () => { + const result = validateOllamaModel( + "gabegoodhart/minimax-m2.1:latest", + () => JSON.stringify({ error: "model requires more system memory" }), + ); + assert.equal(result.ok, false); + assert.match(result.message, /requires more system memory/); + }); + + it("passes ollama model validation when the probe returns a normal payload", () => { + const result = validateOllamaModel( + "nemotron-3-nano:30b", + () => JSON.stringify({ model: "nemotron-3-nano:30b", response: "hello", done: true }), + ); + assert.deepEqual(result, { ok: true }); + }); +}); diff --git a/test/onboard-readiness.test.js b/test/onboard-readiness.test.js new file mode 100644 index 00000000000..4eda74a5701 --- /dev/null +++ b/test/onboard-readiness.test.js @@ -0,0 +1,175 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { buildPolicySetCommand, buildPolicyGetCommand } = require("../bin/lib/policies"); +const { hasStaleGateway, isSandboxReady } = require("../bin/lib/onboard"); + +describe("sandbox readiness parsing", () => { + it("detects Ready sandbox", () => { + assert.ok(isSandboxReady("my-assistant Ready 2m ago", "my-assistant")); + }); + + it("rejects NotReady sandbox", () => { + assert.ok(!isSandboxReady("my-assistant NotReady init failed", "my-assistant")); + }); + + it("rejects empty output", () => { + assert.ok(!isSandboxReady("No sandboxes found.", "my-assistant")); + assert.ok(!isSandboxReady("", "my-assistant")); + }); + + it("strips ANSI escape codes before matching", () => { + assert.ok(isSandboxReady( + "\x1b[1mmy-assistant\x1b[0m \x1b[32mReady\x1b[0m 2m ago", + "my-assistant" + )); + }); + + it("rejects ANSI-wrapped NotReady", () => { + assert.ok(!isSandboxReady( + "\x1b[1mmy-assistant\x1b[0m \x1b[31mNotReady\x1b[0m crash", + "my-assistant" + )); + }); + + it("exact-matches sandbox name in first column", () => { + // "my" should NOT match "my-assistant" + assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my")); + }); + + it("does not match sandbox name in non-first column", () => { + assert.ok(!isSandboxReady("other-box Ready owned-by-my-assistant", "my-assistant")); + }); + + it("handles multiple sandboxes in output", () => { + const output = [ + "NAME STATUS AGE", + "dev-box NotReady 5m ago", + "my-assistant Ready 2m ago", + "staging Ready 10m ago", + ].join("\n"); + assert.ok(isSandboxReady(output, "my-assistant")); + assert.ok(!isSandboxReady(output, "dev-box")); // NotReady + assert.ok(isSandboxReady(output, "staging")); + assert.ok(!isSandboxReady(output, "prod")); // not present + }); + + it("handles Ready sandbox with extra status columns", () => { + assert.ok(isSandboxReady("my-assistant Ready Running 2m ago 1/1", "my-assistant")); + }); + + it("rejects when output only contains name in a URL or path", () => { + assert.ok(!isSandboxReady("Connecting to my-assistant.openshell.internal Ready", "my-assistant")); + // "my-assistant.openshell.internal" is cols[0], not "my-assistant" + }); + + it("handles tab-separated output", () => { + assert.ok(isSandboxReady("my-assistant\tReady\t2m ago", "my-assistant")); + }); +}); + +// Regression tests: WSL truncates hyphenated sandbox names during shell +// argument parsing (e.g. "my-assistant" → "m"). +describe("WSL sandbox name handling", () => { + it("buildPolicySetCommand preserves hyphenated sandbox name", () => { + const cmd = buildPolicySetCommand("/tmp/policy.yaml", "my-assistant"); + assert.ok(cmd.includes('"my-assistant"'), `Expected quoted name in: ${cmd}`); + assert.ok(!cmd.includes(' my-assistant '), "Name must be quoted, not bare"); + }); + + it("buildPolicyGetCommand preserves hyphenated sandbox name", () => { + const cmd = buildPolicyGetCommand("my-assistant"); + assert.ok(cmd.includes('"my-assistant"'), `Expected quoted name in: ${cmd}`); + }); + + it("buildPolicySetCommand preserves multi-hyphen names", () => { + const cmd = buildPolicySetCommand("/tmp/p.yaml", "my-dev-assistant-v2"); + assert.ok(cmd.includes('"my-dev-assistant-v2"')); + }); + + it("buildPolicySetCommand preserves single-char name", () => { + // If WSL truncates "my-assistant" to "m", the single-char name should + // still be quoted and passed through unchanged + const cmd = buildPolicySetCommand("/tmp/p.yaml", "m"); + assert.ok(cmd.includes('"m"')); + }); + + it("applyPreset rejects truncated/invalid sandbox name", () => { + const policies = require("../bin/lib/policies"); + // Empty name + assert.throws( + () => policies.applyPreset("", "npm"), + /Invalid or truncated sandbox name/ + ); + // Name with uppercase (not valid per RFC 1123) + assert.throws( + () => policies.applyPreset("My-Assistant", "npm"), + /Invalid or truncated sandbox name/ + ); + // Name starting with hyphen + assert.throws( + () => policies.applyPreset("-broken", "npm"), + /Invalid or truncated sandbox name/ + ); + }); + + it("readiness check uses exact match preventing truncated name false-positive", () => { + // If "my-assistant" was truncated to "m", the readiness check should + // NOT match a sandbox named "my-assistant" when searching for "m" + assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "m")); + assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my")); + assert.ok(!isSandboxReady("my-assistant Ready 2m ago", "my-")); + }); +}); + +// Regression tests for issue #397: stale gateway detection before port checks. +// A previous onboard session may leave the gateway container and port forward +// running, causing port-conflict failures on the next onboard invocation. +describe("stale gateway detection", () => { + it("detects active nemoclaw gateway from real output", () => { + // Actual output from `openshell gateway info -g nemoclaw` (ANSI stripped) + const output = [ + "Gateway Info", + "", + " Gateway: nemoclaw", + " Gateway endpoint: https://127.0.0.1:8080", + ].join("\n"); + assert.ok(hasStaleGateway(output)); + }); + + it("detects gateway from ANSI-colored output", () => { + const output = + "\x1b[1m\x1b[36mGateway Info\x1b[39m\x1b[0m\n\n" + + " \x1b[2mGateway:\x1b[0m nemoclaw\n" + + " \x1b[2mGateway endpoint:\x1b[0m https://127.0.0.1:8080"; + assert.ok(hasStaleGateway(output)); + }); + + it("returns false for empty string (no gateway running)", () => { + assert.ok(!hasStaleGateway("")); + }); + + it("returns false for null/undefined", () => { + assert.ok(!hasStaleGateway(null)); + assert.ok(!hasStaleGateway(undefined)); + }); + + it("returns false for error output without gateway name", () => { + assert.ok(!hasStaleGateway("Error: no gateway found")); + assert.ok(!hasStaleGateway("connection refused")); + }); + + it("returns false for a different gateway name", () => { + // If someone ran a non-nemoclaw gateway, we should not touch it + const output = [ + "Gateway Info", + "", + " Gateway: my-other-gateway", + " Gateway endpoint: https://127.0.0.1:8080", + ].join("\n"); + assert.ok(!hasStaleGateway(output)); + }); +}); diff --git a/test/onboard-selection.test.js b/test/onboard-selection.test.js new file mode 100644 index 00000000000..9000943ba2f --- /dev/null +++ b/test/onboard-selection.test.js @@ -0,0 +1,84 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +describe("onboard provider selection UX", () => { + it("prompts explicitly instead of silently auto-selecting detected Ollama", () => { + const repoRoot = path.join(__dirname, ".."); + const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-onboard-selection-")); + const scriptPath = path.join(tmpDir, "selection-check.js"); + const onboardPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "onboard.js")); + const credentialsPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "credentials.js")); + const runnerPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "runner.js")); + const registryPath = JSON.stringify(path.join(repoRoot, "bin", "lib", "registry.js")); + const script = String.raw` +const credentials = require(${credentialsPath}); +const runner = require(${runnerPath}); +const registry = require(${registryPath}); + +let promptCalls = 0; +const messages = []; +const updates = []; + +credentials.prompt = async (message) => { + promptCalls += 1; + messages.push(message); + return ""; +}; +credentials.ensureApiKey = async () => {}; +runner.runCapture = (command) => { + if (command.includes("command -v ollama")) return "/usr/bin/ollama"; + if (command.includes("localhost:11434/api/tags")) return JSON.stringify({ models: [{ name: "nemotron-3-nano:30b" }] }); + if (command.includes("ollama list")) return "nemotron-3-nano:30b abc 24 GB now\\nqwen3:32b def 20 GB now"; + if (command.includes("localhost:8000/v1/models")) return ""; + return ""; +}; +registry.updateSandbox = (_name, update) => updates.push(update); + +const { setupNim } = require(${onboardPath}); + +(async () => { + const originalLog = console.log; + const lines = []; + console.log = (...args) => lines.push(args.join(" ")); + try { + const result = await setupNim("selection-test", null); + originalLog(JSON.stringify({ result, promptCalls, messages, updates, lines })); + } finally { + console.log = originalLog; + } +})().catch((error) => { + console.error(error); + process.exit(1); +}); +`; + fs.writeFileSync(scriptPath, script); + + const result = spawnSync(process.execPath, [scriptPath], { + cwd: repoRoot, + encoding: "utf-8", + env: { + ...process.env, + HOME: tmpDir, + }, + }); + + assert.equal(result.status, 0, result.stderr); + assert.notEqual(result.stdout.trim(), "", result.stderr); + const payload = JSON.parse(result.stdout.trim()); + assert.equal(payload.result.provider, "nvidia-nim"); + assert.equal(payload.result.model, "nvidia/nemotron-3-super-120b-a12b"); + assert.equal(payload.promptCalls, 2); + assert.match(payload.messages[0], /Choose \[/); + assert.match(payload.messages[1], /Choose model \[1\]/); + assert.ok(payload.lines.some((line) => line.includes("Detected local inference option"))); + assert.ok(payload.lines.some((line) => line.includes("Press Enter to keep the cloud default"))); + assert.ok(payload.lines.some((line) => line.includes("Cloud models:"))); + }); +}); diff --git a/test/onboard.test.js b/test/onboard.test.js new file mode 100644 index 00000000000..3f4418ccca5 --- /dev/null +++ b/test/onboard.test.js @@ -0,0 +1,31 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); + +const { buildSandboxConfigSyncScript } = require("../bin/lib/onboard"); + +describe("onboard helpers", () => { + it("builds a sandbox sync script that writes config and updates the selected model", () => { + const script = buildSandboxConfigSyncScript({ + endpointType: "custom", + endpointUrl: "https://inference.local/v1", + ncpPartner: null, + model: "nemotron-3-nano:30b", + profile: "inference-local", + credentialEnv: "OPENAI_API_KEY", + onboardedAt: "2026-03-18T12:00:00.000Z", + }); + + assert.match(script, /cat > ~\/\.nemoclaw\/config\.json/); + assert.match(script, /"model": "nemotron-3-nano:30b"/); + assert.match(script, /"credentialEnv": "OPENAI_API_KEY"/); + assert.match(script, /openclaw models set 'inference\/nemotron-3-nano:30b'/); + assert.match(script, /cfg\.setdefault\('agents', \{\}\)\.setdefault\('defaults', \{\}\)\.setdefault\('model', \{\}\)\['primary'\]/); + assert.match(script, /providers_cfg\["inference"\]/); + assert.match(script, /json\.loads\("\{\\\"baseUrl\\\":\\\"https:\/\/inference\.local\/v1\\\",\\\"apiKey\\\":\\\"unused\\\"/); + assert.match(script, /inference\/nemotron-3-nano:30b/); + assert.match(script, /^exit$/m); + }); +}); diff --git a/test/platform.test.js b/test/platform.test.js new file mode 100644 index 00000000000..6a20e16ecae --- /dev/null +++ b/test/platform.test.js @@ -0,0 +1,165 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const path = require("node:path"); + +const { + detectDockerHost, + findColimaDockerSocket, + getDockerSocketCandidates, + inferContainerRuntime, + isUnsupportedMacosRuntime, + isWsl, + shouldPatchCoredns, +} = require("../bin/lib/platform"); + +describe("platform helpers", () => { + describe("isWsl", () => { + it("detects WSL from environment", () => { + assert.equal( + isWsl({ + platform: "linux", + env: { WSL_DISTRO_NAME: "Ubuntu" }, + release: "6.6.87.2-microsoft-standard-WSL2", + }), + true, + ); + }); + + it("does not treat macOS as WSL", () => { + assert.equal( + isWsl({ + platform: "darwin", + env: {}, + release: "24.6.0", + }), + false, + ); + }); + }); + + describe("getDockerSocketCandidates", () => { + it("returns macOS candidates in priority order", () => { + const home = "/tmp/test-home"; + assert.deepEqual(getDockerSocketCandidates({ platform: "darwin", home }), [ + path.join(home, ".colima/default/docker.sock"), + path.join(home, ".config/colima/default/docker.sock"), + path.join(home, ".docker/run/docker.sock"), + ]); + }); + + it("does not auto-detect sockets on Linux", () => { + assert.deepEqual(getDockerSocketCandidates({ platform: "linux", home: "/tmp/test-home" }), []); + }); + }); + + describe("findColimaDockerSocket", () => { + it("finds the first available Colima socket", () => { + const home = "/tmp/test-home"; + const sockets = new Set([path.join(home, ".config/colima/default/docker.sock")]); + const existsSync = (socketPath) => sockets.has(socketPath); + + assert.equal( + findColimaDockerSocket({ home, existsSync }), + path.join(home, ".config/colima/default/docker.sock"), + ); + }); + }); + + describe("detectDockerHost", () => { + it("respects an existing DOCKER_HOST", () => { + assert.deepEqual( + detectDockerHost({ + env: { DOCKER_HOST: "unix:///custom/docker.sock" }, + platform: "darwin", + home: "/tmp/test-home", + existsSync: () => false, + }), + { + dockerHost: "unix:///custom/docker.sock", + source: "env", + socketPath: null, + }, + ); + }); + + it("prefers Colima over Docker Desktop on macOS", () => { + const home = "/tmp/test-home"; + const sockets = new Set([ + path.join(home, ".colima/default/docker.sock"), + path.join(home, ".docker/run/docker.sock"), + ]); + const existsSync = (socketPath) => sockets.has(socketPath); + + assert.deepEqual( + detectDockerHost({ env: {}, platform: "darwin", home, existsSync }), + { + dockerHost: `unix://${path.join(home, ".colima/default/docker.sock")}`, + source: "socket", + socketPath: path.join(home, ".colima/default/docker.sock"), + }, + ); + }); + + it("detects Docker Desktop when Colima is absent", () => { + const home = "/tmp/test-home"; + const socketPath = path.join(home, ".docker/run/docker.sock"); + const existsSync = (candidate) => candidate === socketPath; + + assert.deepEqual( + detectDockerHost({ env: {}, platform: "darwin", home, existsSync }), + { + dockerHost: `unix://${socketPath}`, + source: "socket", + socketPath, + }, + ); + }); + + it("returns null when no auto-detected socket is available", () => { + assert.equal( + detectDockerHost({ + env: {}, + platform: "linux", + home: "/tmp/test-home", + existsSync: () => false, + }), + null, + ); + }); + }); + + describe("inferContainerRuntime", () => { + it("detects podman", () => { + assert.equal(inferContainerRuntime("podman version 5.4.1"), "podman"); + }); + + it("detects Docker Desktop", () => { + assert.equal(inferContainerRuntime("Docker Desktop 4.42.0 (190636)"), "docker-desktop"); + }); + + it("detects Colima", () => { + assert.equal(inferContainerRuntime("Server: Colima\n Docker Engine - Community"), "colima"); + }); + }); + + describe("isUnsupportedMacosRuntime", () => { + it("flags podman on macOS", () => { + assert.equal(isUnsupportedMacosRuntime("podman", { platform: "darwin" }), true); + }); + + it("does not flag podman on Linux", () => { + assert.equal(isUnsupportedMacosRuntime("podman", { platform: "linux" }), false); + }); + }); + + describe("shouldPatchCoredns", () => { + it("patches CoreDNS for Colima only", () => { + assert.equal(shouldPatchCoredns("colima"), true); + assert.equal(shouldPatchCoredns("docker-desktop"), false); + assert.equal(shouldPatchCoredns("docker"), false); + }); + }); +}); diff --git a/test/policies.test.js b/test/policies.test.js index 3d8f13c380c..ec1a021217e 100644 --- a/test/policies.test.js +++ b/test/policies.test.js @@ -65,6 +65,32 @@ describe("policies", () => { }); }); + describe("buildPolicySetCommand", () => { + it("quotes sandbox name to prevent argument splitting", () => { + const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "my-assistant"); + assert.equal(cmd, 'openshell policy set --policy "/tmp/policy.yaml" --wait "my-assistant"'); + }); + + it("handles sandbox names with spaces", () => { + const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "my sandbox"); + assert.ok(cmd.includes('"my sandbox"'), "sandbox name must be quoted"); + }); + + it("places --wait before the sandbox name", () => { + const cmd = policies.buildPolicySetCommand("/tmp/policy.yaml", "test-box"); + const waitIdx = cmd.indexOf("--wait"); + const nameIdx = cmd.indexOf('"test-box"'); + assert.ok(waitIdx < nameIdx, "--wait must come before sandbox name"); + }); + }); + + describe("buildPolicyGetCommand", () => { + it("quotes sandbox name", () => { + const cmd = policies.buildPolicyGetCommand("my-assistant"); + assert.equal(cmd, 'openshell policy get --full "my-assistant" 2>/dev/null'); + }); + }); + describe("preset YAML schema", () => { it("no preset has rules at NetworkPolicyRuleDef level", () => { // rules must be inside endpoints, not as sibling of endpoints/binaries diff --git a/test/preflight.test.js b/test/preflight.test.js index 1700c284f42..6471d798361 100644 --- a/test/preflight.test.js +++ b/test/preflight.test.js @@ -3,105 +3,123 @@ const { describe, it } = require("node:test"); const assert = require("node:assert/strict"); -const fs = require("fs"); -const os = require("os"); -const path = require("path"); -const { isCgroupV2, readDaemonJson, checkCgroupConfig } = require("../bin/lib/preflight"); - -// Helper: create a temp daemon.json with given content and return its path. -function writeTempDaemon(content) { - const dir = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-preflight-")); - const p = path.join(dir, "daemon.json"); - fs.writeFileSync(p, content, "utf-8"); - return p; -} - -describe("isCgroupV2", () => { - it("returns a boolean", () => { - assert.equal(typeof isCgroupV2(), "boolean"); +const net = require("net"); +const { checkPortAvailable } = require("../bin/lib/preflight"); + +describe("checkPortAvailable", () => { + it("falls through to net probe when lsof output is empty", async () => { + // Empty lsof output is not authoritative (non-root can't see root-owned + // listeners), so the function must fall through to the net probe. + // Use a guaranteed-free port so the net probe confirms availability. + const freePort = await new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, "127.0.0.1", () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + }); + const result = await checkPortAvailable(freePort, { lsofOutput: "" }); + assert.deepEqual(result, { ok: true }); }); - it("returns false on non-linux platforms", () => { - // On Linux this still returns a boolean (true or false depending on - // actual cgroup version). On macOS/other it always returns false. - // Either way the function must not throw. - const result = isCgroupV2(); - if (process.platform !== "linux") { - assert.equal(result, false); + it("net probe catches occupied port even when lsof returns empty", async () => { + // Simulates the non-root-can't-see-root-listener scenario: + // lsof returns empty, but net probe detects the port is taken. + const srv = net.createServer(); + const port = await new Promise((resolve) => { + srv.listen(0, "127.0.0.1", () => resolve(srv.address().port)); + }); + try { + const result = await checkPortAvailable(port, { lsofOutput: "" }); + assert.equal(result.ok, false); + assert.equal(result.process, "unknown"); + assert.ok(result.reason.includes("EADDRINUSE")); + } finally { + await new Promise((resolve) => srv.close(resolve)); } }); -}); - -describe("readDaemonJson", () => { - it("parses valid JSON", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "host" }'); - const result = readDaemonJson(p); - assert.deepEqual(result, { "default-cgroupns-mode": "host" }); - }); - it("returns null for invalid JSON", () => { - const p = writeTempDaemon("not json at all"); - assert.equal(readDaemonJson(p), null); - }); - - it("returns null for missing file", () => { - assert.equal(readDaemonJson("/tmp/nonexistent-daemon-" + Date.now() + ".json"), null); - }); -}); - -describe("checkCgroupConfig", () => { - it("runs without arguments (uses live detection)", () => { - const result = checkCgroupConfig(); - assert.equal(typeof result.ok, "boolean"); + it("parses process and PID from lsof output", async () => { + const lsofOutput = [ + "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME", + "openclaw 12345 root 7u IPv4 54321 0t0 TCP *:18789 (LISTEN)", + ].join("\n"); + const result = await checkPortAvailable(18789, { lsofOutput }); + assert.equal(result.ok, false); + assert.equal(result.process, "openclaw"); + assert.equal(result.pid, 12345); + assert.ok(result.reason.includes("openclaw")); }); - it("returns ok when cgroup v1 (skips daemon.json check)", () => { - const result = checkCgroupConfig({ cgroupV2: false }); - assert.deepEqual(result, { ok: true }); + it("picks first listener when lsof shows multiple", async () => { + const lsofOutput = [ + "COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME", + "gateway 111 root 7u IPv4 54321 0t0 TCP *:18789 (LISTEN)", + "node 222 root 8u IPv4 54322 0t0 TCP *:18789 (LISTEN)", + ].join("\n"); + const result = await checkPortAvailable(18789, { lsofOutput }); + assert.equal(result.ok, false); + assert.equal(result.process, "gateway"); + assert.equal(result.pid, 111); }); - it("returns ok when cgroup v2 and daemon.json has cgroupns=host", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "host" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); + it("net probe returns ok for a free port", async () => { + // Find a free port by binding then releasing + const freePort = await new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, "127.0.0.1", () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + }); + const result = await checkPortAvailable(freePort, { skipLsof: true }); assert.deepEqual(result, { ok: true }); }); - it("fails when cgroup v2 and daemon.json missing", () => { - const p = "/tmp/nonexistent-daemon-" + Date.now() + ".json"; - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("does not exist")); - }); - - it("fails when cgroup v2 and daemon.json has no cgroupns key", () => { - const p = writeTempDaemon('{ "storage-driver": "overlay2" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not set to")); + it("net probe detects occupied port", async () => { + const srv = net.createServer(); + const port = await new Promise((resolve) => { + srv.listen(0, "127.0.0.1", () => resolve(srv.address().port)); + }); + try { + const result = await checkPortAvailable(port, { skipLsof: true }); + assert.equal(result.ok, false); + assert.equal(result.process, "unknown"); + assert.ok(result.reason.includes("EADDRINUSE")); + } finally { + await new Promise((resolve) => srv.close(resolve)); + } }); - it("fails when cgroup v2 and cgroupns mode is wrong value", () => { - const p = writeTempDaemon('{ "default-cgroupns-mode": "private" }'); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not set to")); + it("smoke test with live detection on a dynamically selected free port", async () => { + const freePort = await new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, "127.0.0.1", () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + }); + const result = await checkPortAvailable(freePort); + assert.equal(result.ok, true); }); - it("fails when cgroup v2 and daemon.json is invalid JSON", () => { - const p = writeTempDaemon("oops"); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.equal(result.ok, false); - assert.ok(result.reason.includes("not valid JSON")); + it("defaults to port 18789 when no args given", async () => { + // Should not throw — just verify it returns a valid result object + const result = await checkPortAvailable(); + assert.equal(typeof result.ok, "boolean"); }); - it("passes with extra keys alongside cgroupns=host", () => { - const p = writeTempDaemon(JSON.stringify({ - "storage-driver": "overlay2", - "default-cgroupns-mode": "host", - "log-driver": "json-file", - })); - const result = checkCgroupConfig({ cgroupV2: true, daemonPath: p }); - assert.deepEqual(result, { ok: true }); + it("checks gateway port 8080", async () => { + const freePort = await new Promise((resolve) => { + const srv = net.createServer(); + srv.listen(0, "127.0.0.1", () => { + const port = srv.address().port; + srv.close(() => resolve(port)); + }); + }); + // Verify the function works with any port (including 8080-range) + const result = await checkPortAvailable(freePort); + assert.equal(result.ok, true); }); }); diff --git a/test/runner.test.js b/test/runner.test.js new file mode 100644 index 00000000000..03bd3a2b84d --- /dev/null +++ b/test/runner.test.js @@ -0,0 +1,55 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const path = require("node:path"); +const childProcess = require("node:child_process"); +const { spawnSync } = childProcess; + +const runnerPath = path.join(__dirname, "..", "bin", "lib", "runner"); + +describe("runner helpers", () => { + it("does not let child commands consume installer stdin", () => { + const script = ` + const { run } = require(${JSON.stringify(runnerPath)}); + process.stdin.setEncoding("utf8"); + run("cat >/dev/null || true"); + process.stdin.once("data", (chunk) => { + process.stdout.write(chunk); + }); + `; + + const result = spawnSync("node", ["-e", script], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + input: "preserved-answer\n", + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout, "preserved-answer\n"); + }); + + it("uses inherited stdio for interactive commands only", () => { + const calls = []; + const originalSpawnSync = childProcess.spawnSync; + childProcess.spawnSync = (...args) => { + calls.push(args); + return { status: 0 }; + }; + + try { + delete require.cache[require.resolve(runnerPath)]; + const { run, runInteractive } = require(runnerPath); + run("echo noninteractive"); + runInteractive("echo interactive"); + } finally { + childProcess.spawnSync = originalSpawnSync; + delete require.cache[require.resolve(runnerPath)]; + } + + assert.equal(calls.length, 2); + assert.deepEqual(calls[0][2].stdio, ["ignore", "inherit", "inherit"]); + assert.equal(calls[1][2].stdio, "inherit"); + }); +}); diff --git a/test/runtime-shell.test.js b/test/runtime-shell.test.js new file mode 100644 index 00000000000..979460b9888 --- /dev/null +++ b/test/runtime-shell.test.js @@ -0,0 +1,189 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { afterEach, describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const RUNTIME_SH = path.join(__dirname, "..", "scripts", "lib", "runtime.sh"); + +afterEach(() => {}); + +function runShell(script, env = {}) { + return spawnSync("bash", ["-lc", script], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { ...process.env, ...env }, + }); +} + +describe("shell runtime helpers", () => { + it("respects an existing DOCKER_HOST", () => { + const result = runShell(`source "${RUNTIME_SH}"; detect_docker_host`, { + DOCKER_HOST: "unix:///custom/docker.sock", + HOME: "/tmp/unused-home", + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "unix:///custom/docker.sock"); + }); + + it("prefers Colima over Docker Desktop", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-shell-")); + const colimaSocket = path.join(home, ".colima/default/docker.sock"); + const dockerDesktopSocket = path.join(home, ".docker/run/docker.sock"); + + const result = runShell(`source "${RUNTIME_SH}"; detect_docker_host`, { + HOME: home, + NEMOCLAW_TEST_SOCKET_PATHS: `${colimaSocket}:${dockerDesktopSocket}`, + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), `unix://${colimaSocket}`); + fs.rmSync(home, { recursive: true, force: true }); + }); + + it("detects Docker Desktop when Colima is absent", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-shell-")); + const dockerDesktopSocket = path.join(home, ".docker/run/docker.sock"); + + const result = runShell(`source "${RUNTIME_SH}"; detect_docker_host`, { + HOME: home, + NEMOCLAW_TEST_SOCKET_PATHS: dockerDesktopSocket, + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), `unix://${dockerDesktopSocket}`); + fs.rmSync(home, { recursive: true, force: true }); + }); + + it("classifies a Docker Desktop DOCKER_HOST correctly", () => { + const result = runShell(`source "${RUNTIME_SH}"; docker_host_runtime "unix:///Users/test/.docker/run/docker.sock"`); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "docker-desktop"); + }); + + it("selects the matching gateway cluster when a gateway name is present", () => { + const result = runShell( + `source "${RUNTIME_SH}"; + select_openshell_cluster_container "nemoclaw" $'openshell-cluster-alpha\\nopenshell-cluster-nemoclaw'`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "openshell-cluster-nemoclaw"); + }); + + it("fails on ambiguous cluster selection", () => { + const result = runShell( + `source "${RUNTIME_SH}"; + select_openshell_cluster_container "" $'openshell-cluster-a\\nopenshell-cluster-b'`, + ); + + assert.notEqual(result.status, 0); + }); + + it("finds the XDG Colima socket", () => { + const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-runtime-shell-")); + const xdgColimaSocket = path.join(home, ".config/colima/default/docker.sock"); + + const result = runShell(`source "${RUNTIME_SH}"; find_colima_docker_socket`, { + HOME: home, + NEMOCLAW_TEST_SOCKET_PATHS: xdgColimaSocket, + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), xdgColimaSocket); + fs.rmSync(home, { recursive: true, force: true }); + }); + + it("detects podman from docker info output", () => { + const result = runShell(`source "${RUNTIME_SH}"; infer_container_runtime_from_info "podman version 5.4.1"`); + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "podman"); + }); + + it("flags podman on macOS as unsupported", () => { + const result = runShell(`source "${RUNTIME_SH}"; is_unsupported_macos_runtime Darwin podman`); + assert.equal(result.status, 0); + }); + + it("does not flag podman on Linux", () => { + const result = runShell(`source "${RUNTIME_SH}"; is_unsupported_macos_runtime Linux podman`); + assert.notEqual(result.status, 0); + }); + + it("returns the vllm-local base URL", () => { + const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url vllm-local`); + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "http://host.openshell.internal:8000/v1"); + }); + + it("returns the ollama-local base URL", () => { + const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url ollama-local`); + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "http://host.openshell.internal:11434/v1"); + }); + + it("rejects unknown local providers", () => { + const result = runShell(`source "${RUNTIME_SH}"; get_local_provider_base_url bogus-provider`); + assert.notEqual(result.status, 0); + }); + + it("returns the first non-loopback nameserver", () => { + const result = runShell( + `source "${RUNTIME_SH}"; first_non_loopback_nameserver $'nameserver 127.0.0.11\\nnameserver 10.0.0.2'`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "10.0.0.2"); + }); + + it("prefers the container nameserver when it is not loopback", () => { + const result = runShell( + `source "${RUNTIME_SH}"; resolve_coredns_upstream $'nameserver 10.0.0.2' $'nameserver 1.1.1.1' colima`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "10.0.0.2"); + }); + + it("falls back to the Colima VM nameserver when the container resolver is loopback", () => { + const result = runShell( + `source "${RUNTIME_SH}"; + get_colima_vm_nameserver() { printf '192.168.5.1\\n'; } + resolve_coredns_upstream $'nameserver 127.0.0.11' $'nameserver 1.1.1.1' colima`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "192.168.5.1"); + }); + + it("falls back to the host nameserver when no Colima VM nameserver is available", () => { + const result = runShell( + `source "${RUNTIME_SH}"; + get_colima_vm_nameserver() { return 1; } + resolve_coredns_upstream $'nameserver 127.0.0.11' $'nameserver 9.9.9.9' colima`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "9.9.9.9"); + }); + + it("does not consume installer stdin when reading the Colima VM nameserver", () => { + const result = runShell( + `function colima() { cat > /dev/null || true; printf 'nameserver 100.100.100.100\\n'; } + source "${RUNTIME_SH}" + printf 'sandbox-answer\\n' | { + get_colima_vm_nameserver > /tmp/nemoclaw-colima-ns.out + cat + }`, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "sandbox-answer"); + }); +}); diff --git a/test/service-env.test.js b/test/service-env.test.js new file mode 100644 index 00000000000..cd822085dc6 --- /dev/null +++ b/test/service-env.test.js @@ -0,0 +1,119 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const { execSync } = require("child_process"); +const { resolveOpenshell } = require("../bin/lib/resolve-openshell"); + +describe("service environment", () => { + describe("resolveOpenshell logic", () => { + it("returns command -v result when absolute path", () => { + assert.equal( + resolveOpenshell({ commandVResult: "/usr/bin/openshell" }), + "/usr/bin/openshell" + ); + }); + + it("rejects non-absolute command -v result (alias)", () => { + assert.equal( + resolveOpenshell({ commandVResult: "openshell", checkExecutable: () => false }), + null + ); + }); + + it("rejects alias definition from command -v", () => { + assert.equal( + resolveOpenshell({ commandVResult: "alias openshell='echo pwned'", checkExecutable: () => false }), + null + ); + }); + + it("falls back to ~/.local/bin when command -v fails", () => { + assert.equal( + resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/fakehome/.local/bin/openshell", + home: "/fakehome", + }), + "/fakehome/.local/bin/openshell" + ); + }); + + it("falls back to /usr/local/bin", () => { + assert.equal( + resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/usr/local/bin/openshell", + }), + "/usr/local/bin/openshell" + ); + }); + + it("falls back to /usr/bin", () => { + assert.equal( + resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/usr/bin/openshell", + }), + "/usr/bin/openshell" + ); + }); + + it("prefers ~/.local/bin over /usr/local/bin", () => { + assert.equal( + resolveOpenshell({ + commandVResult: null, + checkExecutable: (p) => p === "/fakehome/.local/bin/openshell" || p === "/usr/local/bin/openshell", + home: "/fakehome", + }), + "/fakehome/.local/bin/openshell" + ); + }); + + it("returns null when openshell not found anywhere", () => { + assert.equal( + resolveOpenshell({ + commandVResult: null, + checkExecutable: () => false, + }), + null + ); + }); + }); + + describe("SANDBOX_NAME defaulting", () => { + it("start-services.sh preserves existing SANDBOX_NAME", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${NEMOCLAW_SANDBOX:-${SANDBOX_NAME:-default}}"; export SANDBOX_NAME; bash -c "echo \\$SANDBOX_NAME"\'', + { + encoding: "utf-8", + env: { ...process.env, NEMOCLAW_SANDBOX: "", SANDBOX_NAME: "my-box" }, + } + ).trim(); + assert.equal(result, "my-box"); + }); + + it("start-services.sh uses NEMOCLAW_SANDBOX over SANDBOX_NAME", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${NEMOCLAW_SANDBOX:-${SANDBOX_NAME:-default}}"; export SANDBOX_NAME; bash -c "echo \\$SANDBOX_NAME"\'', + { + encoding: "utf-8", + env: { ...process.env, NEMOCLAW_SANDBOX: "from-env", SANDBOX_NAME: "old" }, + } + ).trim(); + assert.equal(result, "from-env"); + }); + + it("start-services.sh falls back to default when both unset", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${NEMOCLAW_SANDBOX:-${SANDBOX_NAME:-default}}"; export SANDBOX_NAME; bash -c "echo \\$SANDBOX_NAME"\'', + { + encoding: "utf-8", + env: { ...process.env, NEMOCLAW_SANDBOX: "", SANDBOX_NAME: "" }, + } + ).trim(); + assert.equal(result, "default"); + }); + }); +}); diff --git a/test/setup-sandbox-name.test.js b/test/setup-sandbox-name.test.js new file mode 100644 index 00000000000..c454c012daa --- /dev/null +++ b/test/setup-sandbox-name.test.js @@ -0,0 +1,80 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 +// +// Verify that setup.sh uses a parameterized sandbox name instead of +// hardcoding "nemoclaw". Gateway name must stay hardcoded. +// +// See: https://github.com/NVIDIA/NemoClaw/issues/197 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const path = require("node:path"); +const { execSync } = require("node:child_process"); + +const ROOT = path.resolve(__dirname, ".."); + +describe("setup.sh sandbox name parameterization (#197)", () => { + const content = fs.readFileSync(path.join(ROOT, "scripts/setup.sh"), "utf-8"); + + it("accepts sandbox name as $1 with default", () => { + assert.ok( + content.includes('SANDBOX_NAME="${1:-nemoclaw}"'), + 'setup.sh must accept sandbox name as $1 with default "nemoclaw"' + ); + }); + + it("sandbox create uses $SANDBOX_NAME, not hardcoded", () => { + const createLine = content.match(/openshell sandbox create.*--name\s+(\S+)/); + assert.ok(createLine, "Could not find openshell sandbox create --name"); + assert.ok( + createLine[1].includes("$SANDBOX_NAME") || createLine[1].includes('"$SANDBOX_NAME"'), + `sandbox create --name must use $SANDBOX_NAME, found: ${createLine[1]}` + ); + }); + + it("sandbox delete uses $SANDBOX_NAME, not hardcoded", () => { + const deleteLine = content.match(/openshell sandbox delete\s+(\S+)/); + assert.ok(deleteLine, "Could not find openshell sandbox delete"); + assert.ok( + deleteLine[1].includes("$SANDBOX_NAME") || deleteLine[1].includes('"$SANDBOX_NAME"'), + `sandbox delete must use $SANDBOX_NAME, found: ${deleteLine[1]}` + ); + }); + + it("sandbox get uses $SANDBOX_NAME, not hardcoded", () => { + const getLine = content.match(/openshell sandbox get\s+(\S+)/); + assert.ok(getLine, "Could not find openshell sandbox get"); + assert.ok( + getLine[1].includes("$SANDBOX_NAME") || getLine[1].includes('"$SANDBOX_NAME"'), + `sandbox get must use $SANDBOX_NAME, found: ${getLine[1]}` + ); + }); + + it("gateway name stays hardcoded to nemoclaw", () => { + assert.ok( + content.includes("gateway destroy -g nemoclaw"), + "gateway destroy must use hardcoded nemoclaw (gateway != sandbox)" + ); + assert.ok( + content.includes("--name nemoclaw"), + "gateway start --name must use hardcoded nemoclaw" + ); + }); + + it("$1 arg actually sets SANDBOX_NAME in bash", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\' -- my-test-box', + { encoding: "utf-8" } + ).trim(); + assert.equal(result, "my-test-box"); + }); + + it("no arg defaults to nemoclaw in bash", () => { + const result = execSync( + 'bash -c \'SANDBOX_NAME="${1:-nemoclaw}"; echo "$SANDBOX_NAME"\'', + { encoding: "utf-8" } + ).trim(); + assert.equal(result, "nemoclaw"); + }); +}); diff --git a/test/smoke-macos-install.test.js b/test/smoke-macos-install.test.js new file mode 100644 index 00000000000..6d6e44c5095 --- /dev/null +++ b/test/smoke-macos-install.test.js @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const SMOKE_SCRIPT = path.join(__dirname, "..", "scripts", "smoke-macos-install.sh"); + +describe("macOS smoke install script guardrails", () => { + it("prints help", () => { + const result = spawnSync("bash", [SMOKE_SCRIPT, "--help"], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + }); + + assert.equal(result.status, 0); + assert.match(result.stdout, /Usage: \.\/scripts\/smoke-macos-install\.sh/); + }); + + it("requires NVIDIA_API_KEY", () => { + const result = spawnSync("bash", [SMOKE_SCRIPT], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { ...process.env, NVIDIA_API_KEY: "" }, + }); + + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}${result.stderr}`, /NVIDIA_API_KEY must be set/); + }); + + it("rejects invalid sandbox names", () => { + const result = spawnSync("bash", [SMOKE_SCRIPT, "--sandbox-name", "Bad Name"], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, + }); + + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}${result.stderr}`, /Invalid sandbox name/); + }); + + it("rejects unsupported runtimes", () => { + const result = spawnSync("bash", [SMOKE_SCRIPT, "--runtime", "podman"], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, + }); + + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}${result.stderr}`, /Unsupported runtime 'podman'/); + }); + + it("fails when a requested runtime socket is unavailable", () => { + const result = spawnSync("bash", [SMOKE_SCRIPT, "--runtime", "docker-desktop"], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { + ...process.env, + NVIDIA_API_KEY: "nvapi-test", + HOME: "/tmp/nemoclaw-smoke-no-runtime", + }, + }); + + assert.notEqual(result.status, 0); + assert.match(`${result.stdout}${result.stderr}`, /no Docker Desktop socket was found/); + }); + + it("stages the policy preset no answer after sandbox setup", () => { + const script = ` + set -euo pipefail + source "${SMOKE_SCRIPT}" + answers_pipe="$(mktemp -u)" + install_log="$(mktemp)" + mkfifo "$answers_pipe" + trap 'rm -f "$answers_pipe" "$install_log"' EXIT + SANDBOX_NAME="smoke-test" + feed_install_answers "$answers_pipe" "$install_log" & + feeder_pid="$!" + { + IFS= read -r first_line + printf '%s\\n' "$first_line" + printf ' ✓ OpenClaw gateway launched inside sandbox\\n' >> "$install_log" + IFS= read -r second_line + printf '%s\\n' "$second_line" + } < "$answers_pipe" + wait "$feeder_pid" + `; + + const result = spawnSync("bash", ["-lc", script], { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + env: { ...process.env, NVIDIA_API_KEY: "nvapi-test" }, + }); + + assert.equal(result.status, 0); + assert.equal(result.stdout, "smoke-test\nn\n"); + }); +}); diff --git a/test/uninstall.test.js b/test/uninstall.test.js new file mode 100644 index 00000000000..d17dcb36ed3 --- /dev/null +++ b/test/uninstall.test.js @@ -0,0 +1,47 @@ +// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +// SPDX-License-Identifier: Apache-2.0 + +const { describe, it } = require("node:test"); +const assert = require("node:assert/strict"); +const fs = require("node:fs"); +const os = require("node:os"); +const path = require("node:path"); +const { spawnSync } = require("node:child_process"); + +const UNINSTALL_SCRIPT = path.join(__dirname, "..", "uninstall.sh"); + +describe("uninstall helpers", () => { + it("returns the expected gateway volume candidate", () => { + const result = spawnSync( + "bash", + ["-lc", `source "${UNINSTALL_SCRIPT}"; gateway_volume_candidates nemoclaw`], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + }, + ); + + assert.equal(result.status, 0); + assert.equal(result.stdout.trim(), "openshell-cluster-nemoclaw"); + }); + + it("removes the user-local nemoclaw shim", () => { + const tmp = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-uninstall-shim-")); + const shimDir = path.join(tmp, ".local", "bin"); + const shimPath = path.join(shimDir, "nemoclaw"); + fs.mkdirSync(shimDir, { recursive: true }); + fs.writeFileSync(shimPath, "#!/usr/bin/env bash\n", { mode: 0o755 }); + + const result = spawnSync( + "bash", + ["-lc", `HOME="${tmp}" source "${UNINSTALL_SCRIPT}"; remove_nemoclaw_cli`], + { + cwd: path.join(__dirname, ".."), + encoding: "utf-8", + }, + ); + + assert.equal(result.status, 0); + assert.equal(fs.existsSync(shimPath), false); + }); +}); diff --git a/uninstall.sh b/uninstall.sh index 8a0244fe6e3..9b35cc48454 100755 --- a/uninstall.sh +++ b/uninstall.sh @@ -30,9 +30,10 @@ OPENSHELL_CONFIG_DIR="${HOME}/.config/openshell" NEMOCLAW_CONFIG_DIR="${HOME}/.config/nemoclaw" DEFAULT_GATEWAY="nemoclaw" PROVIDERS=("nvidia-nim" "vllm-local" "ollama-local" "nvidia-ncp" "nim-local") -OPEN_SHELL_INSTALL_PATHS=("/usr/local/bin/openshell") +OPEN_SHELL_INSTALL_PATHS=("/usr/local/bin/openshell" "${XDG_BIN_HOME:-$HOME/.local/bin}/openshell") OLLAMA_MODELS=("nemotron-3-super:120b" "nemotron-3-nano:30b") TMP_ROOT="${TMPDIR:-/tmp}" +NEMOCLAW_SHIM_DIR="${HOME}/.local/bin" ASSUME_YES=false KEEP_OPEN_SHELL=false @@ -88,7 +89,12 @@ confirm() { warn "Ollama models are preserved by default. Re-run with --delete-models to remove them." fi printf "Continue? [y/N] " - read -r reply + local reply="" + if [ -t 2 ] && read -r reply 0/dev/null; then + : + else + read -r reply || true + fi case "$reply" in y|Y|yes|YES) ;; *) info "Aborted."; exit 0 ;; @@ -131,6 +137,9 @@ remove_file_with_optional_sudo() { if [ -w "$path" ] || [ -w "$(dirname "$path")" ]; then rm -f "$path" + elif [ "${NEMOCLAW_NON_INTERACTIVE:-}" = "1" ] || [ ! -t 0 ]; then + warn "Skipping privileged removal of $path in non-interactive mode." + return 0 else sudo rm -f "$path" fi @@ -198,6 +207,10 @@ remove_nemoclaw_cli() { else warn "npm not found; skipping nemoclaw npm uninstall." fi + + if [ -L "${NEMOCLAW_SHIM_DIR}/nemoclaw" ] || [ -f "${NEMOCLAW_SHIM_DIR}/nemoclaw" ]; then + remove_path "${NEMOCLAW_SHIM_DIR}/nemoclaw" + fi } remove_nemoclaw_state() { @@ -308,6 +321,52 @@ remove_related_docker_images() { fi } +gateway_volume_candidates() { + local gateway_name="${1:-$DEFAULT_GATEWAY}" + + printf 'openshell-cluster-%s\n' "$gateway_name" +} + +remove_related_docker_volumes() { + if ! command -v docker > /dev/null 2>&1; then + warn "docker not found; skipping Docker volume cleanup." + return 0 + fi + + if ! docker info > /dev/null 2>&1; then + warn "docker is not running; skipping Docker volume cleanup." + return 0 + fi + + local -a volume_names=() + local volume_name + while IFS= read -r volume_name; do + [ -n "$volume_name" ] || continue + volume_names+=("$volume_name") + done < <(gateway_volume_candidates "$DEFAULT_GATEWAY") + + if [ "${#volume_names[@]}" -eq 0 ]; then + info "No NemoClaw/OpenShell Docker volumes found" + return 0 + fi + + local removed_any=false + for volume_name in "${volume_names[@]}"; do + if docker volume inspect "$volume_name" > /dev/null 2>&1; then + if docker volume rm -f "$volume_name" > /dev/null 2>&1; then + info "Removed Docker volume $volume_name" + removed_any=true + else + warn "Failed to remove Docker volume $volume_name" + fi + fi + done + + if [ "$removed_any" = false ]; then + info "No NemoClaw/OpenShell Docker volumes found" + fi +} + remove_optional_ollama_models() { if [ "$DELETE_MODELS" != true ]; then info "Keeping Ollama models as requested." @@ -375,15 +434,15 @@ main() { info "Removing global nemoclaw install..." remove_nemoclaw_cli - info "Removing NemoClaw state..." - remove_nemoclaw_state - info "Removing related Docker containers..." remove_related_docker_containers info "Removing related Docker images..." remove_related_docker_images + info "Removing related Docker volumes..." + remove_related_docker_volumes + info "Removing optional Ollama models..." remove_optional_ollama_models @@ -393,8 +452,13 @@ main() { info "Removing openshell binary..." remove_openshell_binary + info "Removing NemoClaw state..." + remove_nemoclaw_state + echo "" info "Uninstall complete." } -main "$@" +if [ "${BASH_SOURCE[0]-}" = "$0" ] || { [ -z "${BASH_SOURCE[0]-}" ] && { [ "$0" = "bash" ] || [ "$0" = "-bash" ]; }; }; then + main "$@" +fi