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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions makefile
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,17 @@ test-cicd:

.PHONY: test-sdk
test-sdk:
uv run pytest -n logical tests/sdk
uv run pytest -n logical tests/integration/sdk
uv run pytest -n 4 tests/sdk
uv run pytest -n 4 tests/integration/sdk

.PHONY: test-docs
test-docs:
uv run pytest -n logical tests/docs
uv run pytest -n 4 tests/docs

.PHONY: test-agent
test-agent:
uv run pytest -n logical tests/agent
uv run pytest -n logical tests/integration/sdk/test_vault.py
uv run pytest -n 4 tests/agent
uv run pytest -n 4 tests/integration/sdk/test_vault.py

.PHONY: test-sdk-staging
test-sdk-staging:
Expand Down
2 changes: 2 additions & 0 deletions packages/notte-core/src/notte_core/ast.py
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ class ScriptValidator(RestrictingNodeTransformer):
"notte_sdk",
"notte_agent",
"notte_core",
"notte_llm",
Copy link
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that notte_llm aligns with the security model.

The security comments (lines 62-66) specify that allowed modules must not provide file system access, process control, network access beyond basic parsing, system introspection, or code execution capabilities. Please confirm that notte_llm adheres to these constraints.


🏁 Script executed:

#!/bin/bash
# Search for potentially unsafe operations in notte_llm module
# Looking for file operations, subprocess calls, network requests, etc.

fd -e py . -x rg -l "notte_llm" | head -5

Length of output: 173



---

🏁 Script executed:

```shell
#!/bin/bash
# Find notte_llm module source directory
fd -type d -name "notte*llm*" 2>/dev/null

# Also search for where notte_llm is defined/imported from
rg -n "^from notte_llm import|^import notte_llm" tests/llms/test_engine.py -C2

Length of output: 65



---

🏁 Script executed:

```shell
#!/bin/bash
# Map repository structure
ls -la | head -20

# Search for notte_llm package or module definition
rg -n "notte_llm" --type=py -g "!test*" --max-count=5

Length of output: 4153



---

🏁 Script executed:

```shell
#!/bin/bash
# Examine notte_llm package structure and main exports
cat packages/notte-llm/src/notte_llm/__init__.py 2>/dev/null || echo "No __init__.py"

# Check main engine.py for unsafe operations
rg -n "subprocess|import os|open\(|requests\.|eval\(|exec\(|__import__|compile\(" packages/notte-llm/src/notte_llm/ --type=py

Length of output: 422



---

🏁 Script executed:

```shell
#!/bin/bash
# Get full __init__.py contents
cat packages/notte-llm/src/notte_llm/__init__.py

# Check what's exported and the public API surface
rg -n "^class |^def |^from .* import|__all__" packages/notte-llm/src/notte_llm/engine.py | head -20

# Check service.py public API
rg -n "^class |^def " packages/notte-llm/src/notte_llm/service.py | head -20

Length of output: 1092



---

🏁 Script executed:

```shell
#!/bin/bash
# Find where ALLOWED_IMPORTS is used in code
rg -n "ALLOWED_IMPORTS" packages/notte-core/src/notte_core/ -A3 -B3

# Look at the execution context to understand how RestrictedPython uses it
grep -r "safe_globals\|exec_result\|compile_restricted" packages/notte-core/src/notte_core/ast.py -A5 -B5

Length of output: 3964



---

🏁 Script executed:

```shell
#!/bin/bash
# Get complete __init__.py to see all exports
cat packages/notte-llm/src/notte_llm/__init__.py

# Check if tracer classes are exported
rg -n "from.*tracer import|LlmTracer|LlmUsageFileTracer" packages/notte-llm/src/notte_llm/__init__.py

# See how LLMEngine uses tracers
rg -n "tracer|Tracer" packages/notte-llm/src/notte_llm/engine.py | head -10

Length of output: 386


Critical: notte_llm exposes file system access, violating the stated security model.

Adding notte_llm to ALLOWED_IMPORTS allows user scripts to import and instantiate LlmUsageFileTracer, which internally uses open() for file I/O. This bypasses RestrictedPython's restrictions—while RestrictedPython blocks direct open() calls in user code, it cannot prevent allowed modules from using unrestricted builtins. User scripts can therefore write arbitrary files:

from notte_llm.tracer import LlmUsageFileTracer
tracer = LlmUsageFileTracer(file_path="/tmp/exfil.txt")

This violates lines 62-64 which explicitly forbid "File system access". Either remove notte_llm from ALLOWED_IMPORTS or create a restricted wrapper that excludes tracer functionality.

🤖 Prompt for AI Agents
packages/notte-core/src/notte_core/ast.py around line 74: ALLOWED_IMPORTS
currently includes "notte_llm", which exposes file-system access via
LlmUsageFileTracer and thus violates the stated "File system access"
restriction; either remove "notte_llm" from ALLOWED_IMPORTS, or replace it with
a restricted wrapper module (or granular allowed submodules) that exports only
safe, non-I/O APIs (explicitly excluding tracer classes/functions that call
open()); update ALLOWED_IMPORTS to reference the safe wrapper or the vetted
submodule list, add or update unit tests to assert that tracer-related names are
not importable from the sandbox, and add a brief comment explaining the change
for future reviews.

# Safe third-party
"pydantic", # Data validation library
"loguru", # Logging library
Expand All @@ -80,6 +81,7 @@ class ScriptValidator(RestrictingNodeTransformer):
"gspread",
"google",
"litellm",
"tqdm",
# Safe standard library modules - data processing and utilities
"types",
"json", # JSON parsing
Expand Down
2 changes: 1 addition & 1 deletion packages/notte-core/src/notte_core/browser/observation.py
Original file line number Diff line number Diff line change
Expand Up @@ -248,4 +248,4 @@ def validate_exception(cls, v: Any) -> NotteBaseError | Exception | None:
def model_post_init(self, context: Any, /) -> None:
if self.success:
if self.exception is not None:
raise ValueError("Exception should be None if success is True")
raise ValueError(f"Exception should be None if success is True: {self.exception}")
5 changes: 5 additions & 0 deletions packages/notte-sdk/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,13 +15,18 @@ dependencies = [
"halo>=0.0.28",
"notte-core==1.4.4.dev",
"websockets>=13.1",
"typer>=0.9.0",
"tqdm>=4.66.0",
]

[project.optional-dependencies]
playwright = [
"playwright~=1.55",
]

[project.scripts]
notte = "notte_sdk.cli:main"

[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"
4 changes: 4 additions & 0 deletions packages/notte-sdk/src/notte_sdk/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,9 @@
UploadFile,
Wait,
)
from notte_sdk.cli.workflow_cli import workflow_cli
from notte_sdk.client import NotteClient
from notte_sdk.decorators import workflow
from notte_sdk.endpoints.agents import RemoteAgent
from notte_sdk.endpoints.sessions import RemoteSession
from notte_sdk.errors import retry
Expand All @@ -42,6 +44,8 @@
"RemoteAgent",
"retry",
"generate_cookies",
"workflow",
"workflow_cli",
"FormFill",
"Goto",
"GotoNewTab",
Expand Down
68 changes: 68 additions & 0 deletions packages/notte-sdk/src/notte_sdk/cli/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
# Notte Workflow CLI

Manage Notte workflow lifecycle from the command line.

## Usage

```bash
notte workflow [--workflow-path FILE] COMMAND [OPTIONS]
```

## Commands

### Create
```bash
notte workflow --workflow-path my_workflow.py create
```

### Update
```bash
notte workflow --workflow-path my_workflow.py update
```

### Run
```bash
# Run locally
notte workflow --workflow-path my_workflow.py run --local

# Run on cloud
notte workflow --workflow-path my_workflow.py run --variables vars.json
```

### Benchmark
```bash
# Run 10 iterations locally
notte workflow --workflow-path my_workflow.py benchmark --local --iterations 10

# Run on cloud with parallelism
notte workflow --workflow-path my_workflow.py benchmark --iterations 50 --parallelism 4
```

## Auto-Detection

When running from a workflow file, `--workflow-path` is optional:

```python
# my_workflow.py
from notte_sdk import NotteClient, workflow_cli

def run(url: str) -> str:
# ... workflow code ...
return result

if __name__ == "__main__":
workflow_cli() # Enables CLI commands
```

```bash
# These work without --workflow-path
python my_workflow.py create
python my_workflow.py run --local
python my_workflow.py benchmark --iterations 10
```

## Environment Variables

- `NOTTE_API_KEY` - API key (required for cloud operations)
- `NOTTE_API_URL` - API server URL (optional)

53 changes: 53 additions & 0 deletions packages/notte-sdk/src/notte_sdk/cli/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""
Main CLI module for Notte.

This module aggregates all CLI subcommands (workflow, session, agent, etc.)
into a single unified CLI interface.

Usage:
notte workflow create <file>
notte workflow run <file>
notte workflow benchmark <file>
"""

from __future__ import annotations

from pathlib import Path

import typer

from notte_sdk.cli import workflow

# Main CLI app
app = typer.Typer(
name="notte",
help="Notte CLI - Manage workflows, sessions, agents, and more",
add_completion=False,
no_args_is_help=True,
)

# Add workflow subcommand
app.add_typer(workflow.workflow_app, name="workflow")

# Future subcommands can be added here:
# from notte_sdk.cli import session
# app.add_typer(session.session_app, name="session")
#
# from notte_sdk.cli import agent
# app.add_typer(agent.agent_app, name="agent")


def main(_file_path: Path | None = None) -> None:
"""
Main CLI entry point.

Args:
_file_path: Optional path to workflow file. If None, will be auto-detected from sys.argv.
Currently unused, kept for compatibility with workflow_cli().
"""
# Run typer app directly - typer handles help, argument parsing, etc.
app()


if __name__ == "__main__":
main()
Loading
Loading