# Defense-in-Depth Score: Notte

**Repo:** https://github.com/nottelabs/notte · **Commit:** `9c898d9cc8876e6ebcd0ba82751dbe046078cb6e` (1.9.5.dev0) · **Reviewed:** 2026-10-04
**What it is:** Python framework and SDK for web-browsing AI agents, with a local Playwright-based agent and a hosted browser/agent API.
**Category:** AI Assistants
**Scored configuration:** Local open-source mode as in the README quickstart: notte.Session() with default config.toml and notte.Agent(session=..., max_steps default), no vault, persona or storage.
**Agent surface (default):** code execution yes · filesystem write opt-in · network egress yes · external credentials opt-in · persistent memory no · untrusted input yes · third party extensions opt-in · sub agents no · external communication yes

## Score: 4.0 / 10.0 (Minimal)

| # | Criterion | S | C | D | B | Raw | Cap | Score | Confidence |
|---|---|---|---|---|---|---|---|---|---|
| C1 | Identity & least privilege | L1 | L1 | L2 | L2 | 0.35 | — | **0.35** | High |
| C2 | Approval gates | L0 | L0 | L0 | L1 | 0.05 | — | **0.05** | High |
| C3 | Tool & action scoping | L1 | L1 | L0 | L1 | 0.20 | — | **0.20** | High |
| C4 | Code-execution isolation | L2 | L3 | L2 | L0 | 0.47 | — | **0.47** | High |
| C5 | Untrusted input blast radius | L1 | L1 | L2 | L0 | 0.25 | C5-WORSTCASE | **0.25** | High |
| C6 | Memory, context & configuration integrity | SA | SA | SA | SA | 1.00 | — | **1.00** (SA) | High |
| C7 | Third-party extensions | L1 | L1 | L2 | L0 | 0.25 | — | **0.25** | High |
| C8 | Secrets & sensitive-data protection | L2 | L2 | L2 | L1 | 0.45 | G1 | **0.45** (alt) | High |
| C9 | Audit & traceability | L2 | L2 | L2 | L1 | 0.45 | — | **0.45** | High |
| C10 | Limits & kill switch | L2 | L2 | L3 | L2 | 0.55 | — | **0.55** | High |

Controls where a risk surface exists: 3.03 / 9.0 (34%); 1 criterion scored SA (surface absent).

Notte's local agent acts on whatever web pages it reads with no approval step: every navigation, form submission and model-written JavaScript runs immediately, so a malicious page can steer it into exfiltrating data or submitting forms, including with vault logins and the stored card if a vault is attached. Browser launch hardening is incomplete. Its vault keeps secrets out of model prompts well, and step and timeout limits are sensible, but nothing contains a hijacked run.

## Critical gaps
- Prompt injection from any visited page can drive unattended exfiltration (arbitrary goto/JavaScript) and irreversible web actions, including vault logins and the card, with no approval step. (ASI01, T6, LLM01; C5) — [packages/notte-agent/src/notte_agent/agent.py:349](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L349); [packages/notte-agent/src/notte_agent/agent.py:249-251](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L249-L251)
- Locally run SDK functions (including forked shared ones) are downloaded unverified and exec()'d in-process, inheriting all the process's credentials. (ASI04, T17; C7) — [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:845-852](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L845-L852); [packages/notte-core/src/notte_core/ast.py:826-853](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/ast.py#L826-L853)

## Criterion details

### C1 Identity & least privilege — 0.35 (high)

In local mode the agent drives a freshly created, non-persistent browser context, so it starts with no cookies or logged-in accounts of the user's own browser. It only receives site credentials if the developer attaches a cloud vault, and the vault releases a login only for the URL of the current page and checks the target field type. There is no authorization layer on actions themselves, the browser runs on the operator's machine and network (so it can reach localhost and intranet hosts), and the vault does not fully protect payment-card data.

- **S L1:** Authority is narrowed only by starting from an empty, non-persistent browser context; credentials come from an optional vault that binds logins to the current page URL. — [packages/notte-browser/src/notte_browser/playwright.py:134-147](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/playwright.py#L134-L147); [packages/notte-core/src/notte_core/credentials/base.py:668-672](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/credentials/base.py#L668-L672) (verified)
  - *To reach the next level:* No per-capability scoping: one browser context and one vault serve every action.
- **C L1:** The URL-bound credential lookup covers vault logins, but every other action (goto, evaluate_js, clicks, form submits) runs with no authorization check. — [packages/notte-agent/src/notte_agent/agent.py:249-251](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L249-L251); [packages/notte-core/src/notte_core/credentials/base.py:668-672](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/credentials/base.py#L668-L672) (verified)
  - *To reach the next level:* No authorization layer that every action passes; page-level actions bypass any check.
- **D L2:** By default no vault, persona or storage is attached and the browser context is fresh, so the default identity is anonymous; widening it is an explicit developer argument. — [packages/notte-browser/src/notte_browser/playwright.py:134-147](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/playwright.py#L134-L147); [packages/notte-browser/src/notte_browser/session.py:136-142](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L136-L142) (verified)
  - *To reach the next level:* Network position is not narrowed by default (localhost and intranet reachable) and nothing stops a developer attaching a cloud CDP URL to their own browser without warning.
- **B L2:** A hijacked default agent holds no account credentials but can post to any public site and reach services on the operator's network; with the documented vault it holds the user's site logins plus a payment card. — [packages/notte-browser/src/notte_browser/controller.py:219-220](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L219-L220); searched `rg -n -S -i 'allowed_domains|allowed_urls|blocked_domains|domain_allowlist|url_allowlist|169\.254|is_private|ipaddress' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 0 hits (no host allowlist or private-address block anywhere in the agent, browser, or core packages) (verified)
  - *To reach the next level:* Not limited to one project or mostly-read access: any web host, including internal ones, is writable through the browser.
- **Cap:** none

### C2 Approval gates — 0.05 (high)

There is no human approval step anywhere in the local agent loop. Every action the model chooses, including navigating to any URL, submitting forms, running JavaScript on the page and filling vault credentials or a payment card, executes immediately. The only human-in-the-loop hook, a 'help' action, simply ends the run as a failure. Consequential web actions such as submitting forms or purchases are generally irreversible.

- **S L0:** No approval mechanism exists; the model's chosen action is executed directly, and a request for human help terminates the run. — [packages/notte-agent/src/notte_agent/agent.py:249-251](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L249-L251); [packages/notte-agent/src/notte_agent/agent.py:164-176](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L164-L176); searched `rg -n -S -i 'approv|human_in_the_loop|confirm\(|input\(' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 6 hits (5 hits are is_file_input DOM helpers in dom/locate.py and 1 is the email-verification action description; none is an approval gate) (verified)
  - *To reach the next level:* No per-call human approval for consequential actions.
- **C L0:** The most powerful paths (evaluate_js, goto, form fill with vault values) are ungated because no gate exists. — [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786); [packages/notte-browser/src/notte_browser/controller.py:219-220](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L219-L220) (verified)
  - *To reach the next level:* No action path crosses an approval gate.
- **D L0:** Nothing to enable; no approval setting exists in the shipped configuration. — [packages/notte-core/src/notte_core/config.toml:1-20](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/config.toml#L1-L20); searched `rg -n -S -i 'approv|human_in_the_loop|confirm\(|input\(' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 6 hits (5 hits are is_file_input DOM helpers in dom/locate.py and 1 is the email-verification action description; none is an approval gate) (verified)
  - *To reach the next level:* No approval on by default.
- **B L1:** Wrongly chosen actions are real web submissions (forms, posts, purchases with the vault card) with no undo; only navigation-only actions are harmless. — [packages/notte-browser/src/notte_browser/controller.py:219-220](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L219-L220) (verified)
  - *To reach the next level:* No preview, dry-run or rollback for external actions.
- **Cap:** none

### C3 Tool & action scoping — 0.20 (high)

Actions are typed models in a fixed registry and navigation only accepts http and https URLs, and downloaded file names are reduced to a base name. Beyond that, the agent's main tools are general-purpose: 'goto' accepts any URL with no host allowlist or block on internal addresses, and 'evaluate_js' runs arbitrary model-written JavaScript in the page. Every registered action, including JavaScript evaluation, is offered to the model by default, and there is no per-task tool selection.

- **S L1:** Typed pydantic action schemas and an http/https scheme filter, but goto takes any URL and evaluate_js any JavaScript. — [packages/notte-core/src/notte_core/utils/url.py:50-53](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/utils/url.py#L50-L53); [packages/notte-browser/src/notte_browser/window.py:647-657](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/window.py#L647-L657); [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786); searched `rg -n -S -i 'allowed_domains|allowed_urls|blocked_domains|domain_allowlist|url_allowlist|169\.254|is_private|ipaddress' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 0 hits (no host allowlist or private-address block anywhere in the agent, browser, or core packages) (verified)
  - *To reach the next level:* No host allowlist, no internal-address blocking, and no narrow replacements for the general goto and evaluate_js tools.
- **C L1:** Validation exists on a few actions (goto scheme check, download filename basename, upload through storage) but not on evaluate_js or form values. — [packages/notte-core/src/notte_core/utils/url.py:50-53](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/utils/url.py#L50-L53); [packages/notte-browser/src/notte_browser/controller.py:550-553](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L550-L553); [packages/notte-browser/src/notte_browser/controller.py:388-390](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L388-L390) (verified)
  - *To reach the next level:* Most built-in actions do not validate their arguments against allowlists or bounds.
- **D L0:** The prompt renders every action in the global registry, including evaluate_js, for every agent; only one internal action is excluded. — [packages/notte-core/src/notte_core/actions/actions.py:79-90](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/actions/actions.py#L79-L90); [packages/notte-agent/src/notte_agent/falco/prompt.py:28-34](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/falco/prompt.py#L28-L34) (verified)
  - *To reach the next level:* No way to give the agent a reduced tool set; exec-like and network tools are always on.
- **B L1:** A misused tool reaches any web host, including the operator's localhost and intranet, but has no shell or general filesystem access (file upload/download needs an explicit storage object). — [packages/notte-browser/src/notte_browser/controller.py:219-220](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L219-L220); searched `rg -n -S -i 'allowed_domains|allowed_urls|blocked_domains|domain_allowlist|url_allowlist|169\.254|is_private|ipaddress' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 0 hits (no host allowlist or private-address block anywhere in the agent, browser, or core packages); [packages/notte-browser/src/notte_browser/controller.py:388-390](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L388-L390) (verified)
  - *To reach the next level:* Not scoped to a site or workspace; network reach is unbounded.
- **Cap:** none

### C4 Code-execution isolation — 0.47 (high)

The only model-reachable code execution is JavaScript the model writes, which runs inside pages of a locally launched Chromium. That keeps it within the web platform (no direct file or shell access), but the default browser launch configuration is not fully hardened, and the agent deliberately visits pages chosen by untrusted content. There is no containment beneath the browser: it runs as the operator's user, alongside the agent's environment and API keys. Remote browser providers exist as an opt-in alternative but their isolation is outside this repository.

- **S L2:** Model JavaScript runs inside the browser's web-platform runtime (no filesystem or process APIs), but the default browser launch configuration is not fully hardened. — [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786); [packages/notte-browser/src/notte_browser/session.py:784-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L784-L786) (verified)
  - *To reach the next level:* No hardened OS sandbox around the browser.
- **C L3:** Every model-reachable execution path (evaluate_js and page scripts on visited pages) runs inside the same browser runtime; there is no separate shell or Python tool in the agent. — [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786); [packages/notte-core/src/notte_core/actions/actions.py:79-90](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/actions/actions.py#L79-L90) (verified)
  - *To reach the next level:* No fail-closed sandbox requirement.
- **D L2:** The browser boundary is always present, but web_security and the Chrome argument list are plain config values the operator can change silently (web_security=false adds --disable-web-security). — [packages/notte-core/src/notte_core/config.toml:31](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/config.toml#L31); [packages/notte-browser/src/notte_browser/window.py:137-145](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/window.py#L137-L145) (verified)
  - *To reach the next level:* Disabling protections is a silent config change.
- **B L0:** Nothing contains the browser beneath it: it runs as the operator's OS user alongside the agent process's environment (LLM and Notte API keys) and home directory, and the default launch configuration is not fully hardened; page JS has unrestricted network egress including localhost. — [packages/notte-core/src/notte_core/common/config.py:128-130](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/config.py#L128-L130); searched `rg -n -S -i 'allowed_domains|allowed_urls|blocked_domains|domain_allowlist|url_allowlist|169\.254|is_private|ipaddress' --type py` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src` → 0 hits (no host allowlist or private-address block anywhere in the agent, browser, or core packages) (verified)
  - *To reach the next level:* No containment beneath the browser: no separate user, container, or network restriction.
- **Cap:** none

### C5 Untrusted input blast radius — 0.25 (high)

Every page the agent visits is untrusted input, and the only defenses are delimiter tags around the page observation and a system-prompt instruction to treat page text as data. Results of previous actions (scraped data, JavaScript output, emails and SMS read by a persona) re-enter the conversation without those tags. Nothing in code restricts what the agent may do after reading a page, so a hijacked agent can navigate to an attacker URL carrying data, run JavaScript, submit forms, and use vault logins or the payment card, all unattended.

- **S L1:** Spotlighting only: WEBSITE_CONTENT_BEGIN/END delimiters and a prompt instruction; no capability is restricted after untrusted content is read. — [packages/notte-agent/src/notte_agent/falco/perception.py:48](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/falco/perception.py#L48); [packages/notte-agent/src/notte_agent/agent.py:349](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L349); [packages/notte-agent/src/notte_agent/falco/system.md:27](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/falco/system.md#L27) (verified)
  - *To reach the next level:* No code-level restriction on egress or state change once untrusted content is in context.
- **C L1:** Only the current page observation is wrapped; prior action results, scraped data and tool outputs are added as plain user messages. — [packages/notte-agent/src/notte_agent/agent.py:349](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L349); [packages/notte-agent/src/notte_agent/agent.py:329-333](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L329-L333) (verified)
  - *To reach the next level:* Tool results and scraped data are not distinguished from user instructions.
- **D L2:** The delimiters are hard-coded and on by default; the system prompt file can be replaced by the developer. — [packages/notte-agent/src/notte_agent/falco/perception.py:48](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/falco/perception.py#L48); [packages/notte-agent/src/notte_agent/falco/prompt.py:84-91](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/falco/prompt.py#L84-L91) (verified)
  - *To reach the next level:* No guarantee the developer cannot silently replace the prompt file carrying the instruction; nothing beyond detection exists to keep on.
- **B L0:** A hijacked agent can exfiltrate page or task data via goto/evaluate_js to any host and take irreversible actions (form submits, logins and card payments with the documented vault), with no human involved. — [packages/notte-browser/src/notte_browser/controller.py:219-220](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/controller.py#L219-L220); [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786); [packages/notte-agent/src/notte_agent/agent.py:249-251](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L249-L251) (verified)
  - *To reach the next level:* No step requires human approval for egress or irreversible actions.
- **Cap:** C5-WORSTCASE — Worst case (B L0): a hijacked agent can leak data and take irreversible actions unattended.

### C6 Memory, context & configuration integrity — 1.00 (high)

The local agent keeps no memory between runs: the trajectory lives in memory for a single run (an agent can only run once), the browser context is not persistent, and no instruction files, dotenv files or vector stores are loaded from the working directory. Configuration comes from the package's own file or an explicit NOTTE_CONFIG_PATH. Cloud features such as browser profiles are outside the scored local configuration.

- **Structural absence:** searched `rg -n -S 'load_dotenv|AGENTS\.md|CLAUDE\.md|chromadb|faiss|vector_store|save_memory|launch_persistent_context|user_data_dir'` in `packages/notte-agent/src packages/notte-browser/src packages/notte-core/src packages/notte-llm/src src` → 0 hits (no memory store, no dotenv loading, no auto-loaded instruction files, no persistent browser profile in the local runtime)

### C7 Third-party extensions — 0.25 (high)

The agent loop loads no plugins, MCP servers or model files. The SDK can, however, run a stored or shared 'function' locally: it downloads the code from the Notte service, by default the latest version, and executes it inside the developer's Python process, under RestrictedPython by default (which allows the requests library and the Notte client). This is an explicit opt-in call, but nothing verifies the code's integrity and it runs with all of the process's credentials.

- **S L1:** Code comes from a source the user chooses (their own or a forked shared function), latest version unless one is given, with no hash or signature check. — [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:845-852](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L845-L852); [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:626-632](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L626-L632) (verified)
  - *To reach the next level:* No pinning by default and no integrity verification of downloaded function code.
- **C L1:** The only extension type is SDK functions; there is no verification on it. — [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:845-852](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L845-L852) (verified)
  - *To reach the next level:* No verification for the one extension type.
- **D L2:** Nothing runs unless the developer calls run(local=True); the call does not display the code that will execute. — [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:783-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L783-L786); [packages/notte-sdk/src/notte_sdk/endpoints/workflows.py:845-852](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/endpoints/workflows.py#L845-L852) (verified)
  - *To reach the next level:* Running locally does not show the exact code or permissions before executing it.
- **B L0:** Downloaded code runs via exec() in the same process; RestrictedPython is an in-process filter, it permits requests, and the full process environment and Notte client are reachable. — [packages/notte-core/src/notte_core/ast.py:826-853](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/ast.py#L826-L853); [packages/notte-core/src/notte_core/ast.py:257-262](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/ast.py#L257-L262) (verified)
  - *To reach the next level:* No separate process or scrubbed environment for downloaded function code.
- **Cap:** none

### C8 Secrets & sensitive-data protection — 0.45 (high)

In the default local flow the only secrets are LLM provider keys read from environment variables, and no code logs them; usage telemetry to PostHog and Scarf is on by default but deliberately carries only event names, status and system info. The stronger mechanism is the optional cloud vault: the model sees placeholders, real values are substituted at execution time, values the vault returned are scrubbed from model inputs and masked in screenshots, and filled values are SecretStr types. Gaps in the vault: payment-card data is not fully protected, and page JavaScript run by the agent can read filled fields and send them anywhere.

- **default configuration** (default; raw 0.30 → 0.30)
  - **S L1:** Default: secrets from environment variables, and metadata-only telemetry. — [packages/notte-core/src/notte_core/common/config.py:128-130](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/config.py#L128-L130); [packages/notte-core/src/notte_core/common/telemetry.py:232-238](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/telemetry.py#L232-L238) (verified)
    - *To reach the next level:* No type-level masking or log redaction on the default path.
  - **C L1:** Only the telemetry path is deliberately content-free; there is no redaction for model-bound messages or logs without a vault. — [packages/notte-core/src/notte_core/common/telemetry.py:232-238](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/telemetry.py#L232-L238) (verified)
    - *To reach the next level:* Logs and model-bound messages are not protected by default.
  - **D L1:** PostHog and Scarf telemetry are on unless DISABLE_TELEMETRY=true, content-free by design. — [packages/notte-core/src/notte_core/common/telemetry.py:39-45](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/telemetry.py#L39-L45); [packages/notte-core/src/notte_core/common/telemetry.py:232-238](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/telemetry.py#L232-L238) (verified)
    - *To reach the next level:* Telemetry is opt-out rather than opt-in.
  - **B L2:** Leakable default keys are LLM-provider and Notte API keys: service-scoped but long-lived; the model has no shell or env access to read them directly. — [packages/notte-core/src/notte_core/common/config.py:128-130](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/config.py#L128-L130) (verified)
    - *To reach the next level:* Keys are long-lived and not rotated or issued per task.
- **opt-in cloud vault with placeholder substitution** (alt; raw 0.45, cap G1 → 0.45) ← counted
  - **S L2:** Placeholders in model context, values substituted at execution, vault-returned values scrubbed from LLM inputs and screenshots, SecretStr display types. — [packages/notte-agent/src/notte_agent/agent.py:80-88](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L80-L88); [packages/notte-core/src/notte_core/credentials/base.py:601-611](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/credentials/base.py#L601-L611); [packages/notte-core/src/notte_core/credentials/types.py:15-37](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/credentials/types.py#L15-L37) (verified)
    - *To reach the next level:* Encryption at rest happens in the hosted vault and cannot be verified here, and redaction coverage is incomplete, so the secret-manager-plus-redaction-on-all-paths level is not shown.
  - **C L2:** Covers model-bound text, screenshots and logged action values, but coverage does not extend to every path, and page JS can read filled values. — [packages/notte-browser/src/notte_browser/session.py:769-786](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L769-L786) (verified)
    - *To reach the next level:* Some vault values and JavaScript-readable fields are outside the protection.
  - **D L2:** Vault requires an explicit vault argument and a Notte API account. — [packages/notte-agent/src/notte_agent/agent.py:80-88](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L80-L88) (verified)
    - *To reach the next level:* Not on by default.
  - **B L1:** Vault holds long-lived site passwords, TOTP seeds and a payment card. — [packages/notte-core/src/notte_core/credentials/base.py:505-510](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/credentials/base.py#L505-L510) (verified)
    - *To reach the next level:* Credentials are long-lived and not narrowly scoped.
- **Cap:** G1 — Opt-in mechanism: off in the scored default configuration.

### C9 Audit & traceability — 0.45 (high)

Each run builds a structured trajectory of every model decision and executed action with its arguments, result and start and end times, and step summaries are printed through the logger as they happen. The trajectory lives only in memory and is handed back to the developer at the end of the run; nothing is written to disk or shipped elsewhere by default, there is no actor or approver attribution, and a crash loses whatever was not printed.

- **S L2:** Structured in-memory trajectory of completions and execution results with timestamps; per-step log lines. — [packages/notte-agent/src/notte_agent/agent.py:137-139](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L137-L139); [packages/notte-agent/src/notte_agent/agent.py:111-121](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L111-L121); [packages/notte-agent/src/notte_agent/agent.py:157-160](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L157-L160) (verified)
  - *To reach the next level:* No actor/principal attribution or correlation IDs.
- **C L2:** All built-in actions go through the same session execute path and are appended to the trajectory. — [packages/notte-agent/src/notte_agent/agent.py:249-251](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L249-L251); [packages/notte-agent/src/notte_agent/agent.py:137-139](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L137-L139) (verified)
  - *To reach the next level:* No record of configuration, credential use (vault fills) beyond the placeholder, or extension (function) runs in the same record.
- **D L2:** Recorded by default in the agent's own process, outside anything the browser actions can touch. — [packages/notte-agent/src/notte_agent/agent.py:137-139](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L137-L139) (verified)
  - *To reach the next level:* Not written by a component outside the agent's process.
- **B L1:** Records are in memory and returned at the end; only console log lines are emitted per step, best-effort. — [packages/notte-agent/src/notte_agent/agent.py:111-121](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L111-L121); [packages/notte-agent/src/notte_agent/agent.py:157-160](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L157-L160) (verified)
  - *To reach the next level:* Records are not durably flushed per action.
- **Cap:** none

### C10 Limits & kill switch — 0.55 (high)

The agent loop is capped at 20 steps by default (the request schema refuses more than 150), it stops after 3 consecutive failed actions, and individual actions, JavaScript evaluation and LLM calls have timeouts. There is no wall-clock limit for the whole run, no token or cost budget, and no stop control beyond cancelling the Python task; in-flight browser actions run until their timeout.

- **S L2:** Step cap plus per-action, evaluate_js and LLM-call timeouts enforced in code. — [packages/notte-agent/src/notte_agent/agent.py:400-414](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L400-L414); [packages/notte-browser/src/notte_browser/session.py:855-859](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L855-L859); [packages/notte-llm/src/notte_llm/engine.py:583-589](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-llm/src/notte_llm/engine.py#L583-L589); [packages/notte-agent/src/notte_agent/agent.py:254-260](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L254-L260) (verified)
  - *To reach the next level:* No run wall-clock limit, token/cost budget, or rate limit on side-effecting actions.
- **C L2:** The top-level loop and every browser action/LLM call are bounded; there are no sub-agents or background tasks to escape the budget. — [packages/notte-agent/src/notte_agent/agent.py:400-414](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-agent/src/notte_agent/agent.py#L400-L414); [packages/notte-browser/src/notte_browser/session.py:855-859](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-browser/src/notte_browser/session.py#L855-L859) (verified)
  - *To reach the next level:* Validator LLM calls and retries are not counted against a shared budget.
- **D L3:** Defaults are 20 steps and 3 consecutive failures; the model cannot change config, and request-level max_steps is capped at 150. — [packages/notte-core/src/notte_core/config.toml:11](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/config.toml#L11); [packages/notte-sdk/src/notte_sdk/types.py:2145](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-sdk/src/notte_sdk/types.py#L2145); [packages/notte-core/src/notte_core/common/config.py:440-441](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/common/config.py#L440-L441) (verified)
  - *To reach the next level:* The config file path (NOTTE_CONFIG_PATH) can set max_steps without any hard ceiling.
- **B L2:** Worst case is about 20 steps of up to tens of seconds each; stopping ends the loop but an in-flight action runs to its timeout. — [packages/notte-core/src/notte_core/config.toml:11](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/config.toml#L11); [packages/notte-core/src/notte_core/config.toml:73-76](https://github.com/nottelabs/notte/blob/9c898d9cc8876e6ebcd0ba82751dbe046078cb6e/packages/notte-core/src/notte_core/config.toml#L73-L76) (verified)
  - *To reach the next level:* No cost ceiling and no cancellation of in-flight browser actions.
- **Cap:** none

## Rule-of-Two check
[A] untrusted input: Every visited web page enters model context (packages/notte-agent/src/notte_agent/falco/perception.py:48) · [B] sensitive data/systems: Optional vault logins and payment card filled into pages (packages/notte-core/src/notte_core/credentials/base.py) · [C] state change / egress: Arbitrary goto and evaluate_js (packages/notte-browser/src/notte_browser/controller.py:220, packages/notte-browser/src/notte_browser/session.py:769) · Same default session? Yes

## Highest-impact improvements
1. Harden the default browser launch configuration. — C4 B L0→L2, +0.100 before caps (Playbook 3 (sandboxing))
2. Add an optional per-call approval callback for consequential actions (form submit, vault/card fills, evaluate_js, navigation off the start domain), on by default for vault-enabled agents. — C2 S L0→L3, +0.225 before caps (Playbook 5)
3. Let developers pass an allowed-actions list and a domain allowlist (blocking private/loopback addresses) that the session enforces in code. — C3 S L1→L3, +0.150 before caps (Playbook 3 step 1)
4. Harden vault payment-card handling. — C1 C L1→L2, +0.075 before caps (Playbook 4)
5. Run locally executed functions in a subprocess with a scrubbed environment and verify a pinned version hash before exec. — C7 B L0→L2, +0.100 before caps (Playbook 3)

## Re-audit log
- No changes.

## Limitations
- Static source review of the pinned commit only; nothing was executed, installed, or probed.
- The hosted Notte API (cloud agents, remote browser sessions, server-side vault URL matching, profiles) is not in this repository and was not scored; the SDK cloud mode the README also recommends runs the agent loop server-side.
- Git submodules (notte-cli, notte-skills, browserarena, templates, claude-managed-agents) were empty in the clone and were not reviewed.
- The Node SDK and docs were only skimmed; scoring covers the Python packages under packages/ and src/.
- No text aimed at AI reviewers was found; docs/src/.claude/CLAUDE.md is ordinary documentation-writing guidance.
