{"owner":"apache","repo":"zeppelin","hasSkills":true,"hasMcp":false,"mcpConfig":null,"found":["AGENTS.md"],"skills":{"AGENTS.md":"<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n\n# AGENTS.md\n\n> Guidance for AI coding agents working on the Apache Zeppelin codebase.\n> See [AGENTS.md specification](https://github.com/agentsmd/agents.md).\n\n## Project Overview\n\nApache Zeppelin is a web-based notebook for interactive data analytics. It provides a unified interface to multiple data processing backends (Spark, Flink, Python, JDBC, etc.) through a pluggable interpreter architecture. Each interpreter runs in its own JVM process and communicates with the server via Apache Thrift RPC.\n\n- **Language**: Java, Scala — versions in root `pom.xml` (`java.version`, `scala.binary.version`)\n- **Build**: Maven multi-module (wrapper: `./mvnw`)\n- **Frontend**: Angular + TypeScript in `zeppelin-web-angular/` — versions in its `package.json`\n- **Version**: see `<version>` in root `pom.xml`\n\n## Build & Test\n\n```bash\n# Full build (skip tests)\n./mvnw clean package -DskipTests\n\n# Build single module (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run module tests\n./mvnw test -pl zeppelin-interpreter --am\n\n# Run single test class/method\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Common profiles\n#   -Pspark-3.5 -Pspark-scala-2.12   Spark version\n#   -Pflink-1.20                       Flink version\n#   -Pbuild-distr                      Full distribution\n#   -Prat                              Apache RAT license check\n#   -Pweb-classic                      Additionally builds the classic UI web module when specified\n```\n\n## Build Gotchas\n\n### Shaded JAR Rebuild Chain\n\nThe most common build mistake: modifying `zeppelin-interpreter` without rebuilding `zeppelin-interpreter-shaded`. The shaded JAR is an uber JAR that all interpreter processes use. If it's stale, you get `ClassNotFoundException` or `NoSuchMethodError` at runtime.\n\n```bash\n# After changing zeppelin-interpreter, ALWAYS rebuild in order:\n./mvnw clean package -pl zeppelin-interpreter -DskipTests\n./mvnw clean package -pl zeppelin-interpreter-shaded -DskipTests\n# Then rebuild affected interpreter modules\n\n# Shorthand:\n./mvnw clean package -pl zeppelin-interpreter,zeppelin-interpreter-shaded -DskipTests\n```\n\nThe shaded JAR is also copied to `interpreter/` directory by maven-antrun-plugin after packaging. If this directory has a stale JAR, interpreter processes will load old code.\n\n### Module Build Order\n\nMaven modules are ordered in the root `pom.xml`. Key sequence:\n```\nzeppelin-interpreter → zeppelin-interpreter-shaded → zeppelin-server\n```\n\nAll interpreter modules build after `zeppelin-interpreter-shaded`. A second shading chain exists for Jupyter:\n```\nzeppelin-jupyter-interpreter → zeppelin-jupyter-interpreter-shaded → python\n```\n\n## Module Architecture\n\n### Dependency Flow\n\n```\nzeppelin-interpreter          Base API: Interpreter, InterpreterContext, Thrift services\n        ↓\nzeppelin-interpreter-shaded   Uber JAR (maven-shade-plugin, relocated packages)\n        ↓\nzeppelin-server               Core engine + Jetty 11, REST/WebSocket APIs, HK2 DI, entry point\n```\n\n### Core Modules\n\n#### `zeppelin-interpreter/`\nThe base framework that all interpreters depend on. Defines the interpreter API and the Thrift communication protocol. This module is shaded into an uber JAR (`zeppelin-interpreter-shaded`) and placed on each interpreter process's classpath.\n\nKey classes:\n- `Interpreter` (abstract) / `AbstractInterpreter` — base class every interpreter extends\n- `InterpreterContext` — carries notebook/paragraph/user info into `interpret()` calls\n- `InterpreterGroup` — manages a group of interpreter instances sharing one process\n- `InterpreterResult` / `InterpreterOutput` — execution result model\n- `RemoteInterpreterServer` — **entry point of each interpreter JVM process**; implements the Thrift `RemoteInterpreterService` server; receives RPC calls from zeppelin-server\n- `InterpreterLauncher` (abstract) — how an interpreter process is started (Standard, Docker, K8s, YARN)\n- `LifecycleManager` — manages interpreter process lifecycle (Null = keep alive, Timeout = idle shutdown)\n- `DependencyResolver` / `AbstractDependencyResolver` — Maven artifact resolution for `%dep` paragraphs\n\nThrift definitions (`src/main/thrift/`):\n- `RemoteInterpreterService.thrift` — server → interpreter RPCs\n- `RemoteInterpreterEventService.thrift` — interpreter → server event callbacks\n\n#### `zeppelin-server/`\nThe entry point and core of the Zeppelin application. Combines the web server / API layer with the core notebook engine, interpreter lifecycle management, scheduling, search, and plugin loading.\n\nWeb / API layer (`org.apache.zeppelin.server`, `rest`, `socket`):\n- `ZeppelinServer` — `main()`, embedded Jetty 11 server, HK2 DI setup\n- `NotebookRestApi`, `InterpreterRestApi`, `SecurityRestApi`, `ConfigurationsRestApi` — REST endpoints in `org.apache.zeppelin.rest`\n- `NotebookServer` — WebSocket endpoint (`/ws`) for real-time notebook operations and paragraph execution\n- `RemoteInterpreterEventServer` — Thrift server receiving callbacks from interpreter processes (output streaming, status updates)\n\nEngine / runtime (`org.apache.zeppelin.notebook`, `interpreter`, `scheduler`, `search`, `plugin`, `storage`, `conf`):\n- `Notebook` / `Note` / `Paragraph` — notebook data model and execution\n- `InterpreterFactory` — creates interpreter instances\n- `InterpreterSettingManager` — loads `interpreter-setting.json` from each interpreter directory, manages interpreter configurations\n- `InterpreterSetting` — one interpreter's config + runtime state; creates `InterpreterLauncher` and `RemoteInterpreterProcess`\n- `ManagedInterpreterGroup` — server-side `InterpreterGroup` implementation; owns the `RemoteInterpreterProcess`\n- `NoteManager` — notebook CRUD, folder tree\n- `SchedulerService` — Quartz-based cron scheduling\n- `SearchService` — Lucene-based notebook search\n- `PluginManager` — loads launcher and notebook-repo plugins (custom classloading, not Java SPI)\n- `ZeppelinConfiguration` — config management (env vars → system properties → `zeppelin-site.xml` → defaults)\n- `RecoveryStorage` — persists interpreter process info for server-restart recovery\n- `ConfigStorage` — persists interpreter settings to JSON\n\n#### `zeppelin-interpreter-shaded/`\nUses maven-shade-plugin to package `zeppelin-interpreter` + dependencies into an uber JAR with relocated packages (e.g., `org.apache.thrift` → `org.apache.zeppelin.shaded.org.apache.thrift`). This JAR is placed on each interpreter process's classpath.\n\n#### `zeppelin-client/`\nREST/WebSocket client library for programmatic access to Zeppelin.\n\n### Interpreter Modules\n\nEach interpreter is an independent Maven module inheriting from `zeppelin-interpreter-parent`:\n\n| Module | Description |\n|--------|-------------|\n| `spark/` | Apache Spark (Scala/Python/R/SQL) — most complex interpreter |\n| `python/` | IPython/Python |\n| `flink/` | Apache Flink (Scala/Python/SQL) |\n| `jdbc/` | JDBC (PostgreSQL, MySQL, Hive, etc.) |\n| `shell/` | Bash/Shell commands |\n| `markdown/` | Markdown rendering (Flexmark) |\n| `java/` | Java interpreter |\n| `groovy/` | Groovy |\n| `neo4j/` | Neo4j Cypher |\n| `mongodb/` | MongoDB |\n| `elasticsearch/` | Elasticsearch |\n| `bigquery/` | Google BigQuery |\n| `cassandra/` | Apache Cassandra CQL |\n| `hbase/` | Apache HBase |\n| `livy/` | Apache Livy (remote Spark) |\n| `sparql/` | SPARQL queries |\n| `influxdb/` | InfluxDB |\n| `file/` | HDFS/local file browser |\n\n### Plugin Modules (`zeppelin-plugins/`)\n\n**Launcher plugins** (`launcher/`) — how interpreter processes are started:\n- `StandardInterpreterLauncher` (builtin) — local JVM process via `bin/interpreter.sh`\n- `SparkInterpreterLauncher` (builtin) — Spark-specific launcher with `spark-submit`\n- `DockerInterpreterLauncher` — Docker container\n- `K8sStandardInterpreterLauncher` — Kubernetes pod\n- `YarnInterpreterLauncher` — YARN container\n- `FlinkInterpreterLauncher` — Flink-specific\n- `ClusterInterpreterLauncher` — Zeppelin cluster mode\n\n**NotebookRepo plugins** (`notebookrepo/`) — where notebooks are persisted:\n- `VFSNotebookRepo` (builtin) — local filesystem (Apache VFS)\n- `GitNotebookRepo` (builtin) — local git repo\n- `GitHubNotebookRepo` — GitHub\n- `S3NotebookRepo` — Amazon S3\n- `GCSNotebookRepo` — Google Cloud Storage\n- `AzureNotebookRepo` — Azure Blob Storage\n- `MongoNotebookRepo` — MongoDB\n- `OSSNotebookRepo` — Alibaba Cloud OSS\n\n### Frontend\n\n- `zeppelin-web-angular/` — active frontend (Angular; versions in `package.json`, Node build pin in `pom.xml` `node.version`)\n- `zeppelin-web/` — Legacy AngularJS (activated with `-Pweb-classic`)\n\n### Configuration Files\n\n| File | Purpose |\n|------|---------|\n| `conf/zeppelin-site.xml` | Main server config (port, SSL, notebook storage, interpreter settings). Copy from `.template` |\n| `conf/zeppelin-env.sh` | Shell environment (JAVA_OPTS, memory, Spark master). Copy from `.template` |\n| `conf/shiro.ini` | Authentication/authorization (users, roles, LDAP, Kerberos, PAM). Copy from `.template` |\n| `conf/interpreter.json` | Runtime interpreter settings — **auto-generated**, do not edit manually |\n| `conf/log4j2.properties` | Logging configuration |\n| `conf/interpreter-list` | Static list of available interpreters with Maven coordinates |\n| `{interpreter}/resources/interpreter-setting.json` | Interpreter defaults (build-time, bundled in JAR) |\n\n`conf/*.template` files are the source of truth. Actual config files (`zeppelin-site.xml`, `shiro.ini`, etc.) are `.gitignored`.\n\n### Module Boundaries\n\nWhere new code should go:\n\n| If the code... | Put it in |\n|----------------|-----------|\n| Is a base interface/class that all interpreters need | `zeppelin-interpreter` |\n| Handles notebook state, interpreter lifecycle, scheduling, search, REST/WebSocket, or authentication realm | `zeppelin-server` |\n| Is specific to one backend (Spark, Flink, JDBC, etc.) | That interpreter's module |\n| Is a new way to launch interpreter processes | `zeppelin-plugins/launcher/` |\n| Is a new notebook storage backend | `zeppelin-plugins/notebookrepo/` |\n\n**Important**: Code added to `zeppelin-interpreter` is exposed to **every interpreter process** via the shaded JAR. Only add code there if all interpreters genuinely need it.\n\n## Server–Interpreter Communication\n\nZeppelin's most important architectural concept: the server and each interpreter run in **separate JVM processes** communicating via **Apache Thrift RPC**. This provides isolation, fault tolerance, and the ability to run interpreters on remote hosts or containers.\n\n### Thrift Code Generation\n\nThe `.thrift` files are in `zeppelin-interpreter/src/main/thrift/`. Generated Java files are **checked into git** (not generated at build time) in `zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/`.\n\nTo regenerate after modifying `.thrift` files:\n```bash\ncd zeppelin-interpreter/src/main/thrift\n./genthrift.sh   # requires 'thrift' compiler (v0.13.0) installed locally\n```\n\nThe script runs the Thrift compiler, prepends ASF license headers, and moves files to the source tree. **Never edit the generated Java files directly** — changes will be lost on next regeneration.\n\n### Thrift IPC — Bidirectional\n\n**Server → Interpreter** (`RemoteInterpreterService`):\n```\ninit(properties)                    — initialize interpreter process with config\ncreateInterpreter(className, ...)   — instantiate an interpreter class\nopen(sessionId, className)          — open/initialize an interpreter\ninterpret(sessionId, className, code, context) — execute code (core method)\ncancel(sessionId, className, ...)   — cancel running execution\ngetProgress(sessionId, className)   — poll execution progress (0-100)\ncompletion(sessionId, className, buf, cursor) — code completion\nclose(sessionId, className)         — close an interpreter\nshutdown()                          — terminate the interpreter process\n```\n\n**Interpreter → Server** (`RemoteInterpreterEventService`):\n```\nregisterInterpreterProcess(info)    — register after process startup\nappendOutput(event)                 — stream execution output incrementally\nupdateOutput(event)                 — replace output content\nsendParagraphInfo(info)             — update paragraph metadata\nupdateAppStatus(event)              — Zeppelin Application status\nrunParagraphs(request)              — trigger paragraph execution from interpreter\ngetResource(resourceId)             — access ResourcePool shared state\ngetParagraphList(noteId)            — query notebook structure\n```\n\n### Paragraph Execution Chain\n\nWhen a user runs a paragraph, the full call chain is:\n\n```\nUser clicks \"Run\" in browser\n  → WebSocket message to NotebookServer\n    → NotebookServer.runParagraph()\n      → Notebook.run()\n        → Paragraph.execute()\n          → RemoteInterpreter.interpret(code, context)\n            → RemoteInterpreterProcess.callRemoteFunction()\n              → [Thrift RPC over TCP]\n                → RemoteInterpreterServer.interpret()\n                  → actual Interpreter.interpret()  (e.g. SparkInterpreter)\n                    → result returned via Thrift\n          → meanwhile: interpreter calls appendOutput() to stream partial results back\n```\n\n### Interpreter Launch Chain\n\nWhen an interpreter process needs to be started:\n\n```\nRemoteInterpreter.interpret()  [first call triggers launch]\n  → ManagedInterpreterGroup.getOrCreateInterpreterProcess()\n    → InterpreterSetting.createInterpreterProcess()\n      → InterpreterSetting.createLauncher(properties)\n        → PluginManager.loadInterpreterLauncher(launcherPlugin)\n          → [builtin: Class.forName() / external: URLClassLoader]\n            → InterpreterLauncher.launch(context)\n              → new ExecRemoteInterpreterProcess(...)\n      → ExecRemoteInterpreterProcess.start()\n        → ProcessBuilder → \"bin/interpreter.sh\"\n          → java -cp ... RemoteInterpreterServer  [new JVM]\n            → RemoteInterpreterServer.main()\n              → registerInterpreterProcess() callback to server\n```\n\n### Interpreter Process Lifecycle\n\n1. **Launch**: Server creates `RemoteInterpreterProcess` via launcher plugin\n2. **Start**: Process starts as separate JVM (`bin/interpreter.sh` → `RemoteInterpreterServer.main()`)\n3. **Register**: Process calls `registerInterpreterProcess()` back to server's `RemoteInterpreterEventServer`\n4. **Init**: Server calls `init(properties)` — passes all configuration as a flat `Map<String, String>`\n5. **Create**: Server calls `createInterpreter(className, properties)` — instantiates interpreter via reflection\n6. **Open**: First `interpret()` triggers `LazyOpenInterpreter.open()` — interpreter initializes resources\n7. **Execute**: `interpret(code, context)` — runs code; partial output streams via `appendOutput()` events\n8. **Shutdown**: `close()` → `shutdown()` → JVM exits\n9. **Recovery**: `RecoveryStorage` persists process info; on server restart, reconnects to surviving processes\n\n### InterpreterGroup Scoping\n\n`InterpreterOption` controls process isolation via `perNote` and `perUser` settings:\n\n| perNote | perUser | Behavior |\n|---------|---------|----------|\n| `shared` | `shared` | All users share one process (default) |\n| `scoped` | `shared` | Separate interpreter instance per note, same process |\n| `isolated` | `shared` | Separate process per note |\n| `shared` | `scoped` | Separate interpreter instance per user, same process |\n| `shared` | `isolated` | Separate process per user |\n| `scoped` | `scoped` | Separate instance per user+note |\n| `isolated` | `isolated` | Separate process per user+note (full isolation) |\n\n## Plugin System & Reflection Patterns\n\n### PluginManager — Custom Classloading\n\n`PluginManager` (`zeppelin-server/.../plugin/PluginManager.java`) loads plugins without Java SPI:\n\n```\nPlugin loading flow:\n1. Check builtin list (hardcoded class names):\n   - Launchers: StandardInterpreterLauncher, SparkInterpreterLauncher\n   - NotebookRepos: VFSNotebookRepo, GitNotebookRepo\n   → if builtin: Class.forName(className) — direct classloading\n\n2. If not builtin → external plugin:\n   → Scan pluginsDir/{Launcher|NotebookRepo}/{pluginName}/ for JARs\n   → Create URLClassLoader with those JARs\n   → classLoader.loadClass(className)\n   → Instantiate via reflection (constructor parameters)\n```\n\nExternal plugin directory structure:\n```\nplugins/\n  Launcher/\n    DockerInterpreterLauncher/\n      *.jar\n    K8sStandardInterpreterLauncher/\n      *.jar\n  NotebookRepo/\n    S3NotebookRepo/\n      *.jar\n    GCSNotebookRepo/\n      *.jar\n```\n\n### ReflectionUtils\n\n`ReflectionUtils` (`zeppelin-server/.../util/ReflectionUtils.java`) provides generic reflection-based instantiation:\n\n```java\n// No-arg constructor\nReflectionUtils.createClazzInstance(className)\n\n// Parameterized constructor\nReflectionUtils.createClazzInstance(className, parameterTypes, parameters)\n```\n\nUsed to instantiate:\n- `RecoveryStorage` — in `RemoteInterpreterServer` and `InterpreterSettingManager`\n- `ConfigStorage` — in `InterpreterSettingManager`\n- `LifecycleManager` — in `RemoteInterpreterServer`\n- `NotebookRepo` — in `PluginManager`\n- `InterpreterLauncher` — in `PluginManager`\n\n### Interpreter Discovery\n\n`InterpreterSettingManager` discovers interpreters at startup:\n\n```\n1. Scan interpreterDir (default: interpreter/) for subdirectories\n2. For each subdirectory, look for interpreter-setting.json\n3. Parse JSON → List<RegisteredInterpreter>\n4. Register each interpreter's className, properties, editor settings\n```\n\n`interpreter-setting.json` format (in each interpreter module's resources):\n```json\n[{\n  \"group\": \"spark\",\n  \"name\": \"spark\",\n  \"className\": \"org.apache.zeppelin.spark.SparkInterpreter\",\n  \"properties\": {\n    \"spark.master\": { \"defaultValue\": \"local[*]\", \"description\": \"Spark master\" }\n  },\n  \"editor\": { \"language\": \"scala\", \"editOnDblClick\": false }\n}]\n```\n\n### ZeppelinConfiguration Priority\n\nConfiguration values are resolved in order (first match wins):\n1. **Environment variables** (e.g., `ZEPPELIN_HOME`, `ZEPPELIN_PORT`)\n2. **System properties** (e.g., `-Dzeppelin.server.port=8080`)\n3. **zeppelin-site.xml** (`conf/zeppelin-site.xml`)\n4. **Hardcoded defaults** (`ConfVars` enum in `ZeppelinConfiguration`)\n\n### HK2 Dependency Injection (zeppelin-server)\n\n`ZeppelinServer.startZeppelin()` sets up HK2 DI via `ServiceLocatorUtilities.bind()`:\n\n```java\nnew AbstractBinder() {\n    protected void configure() {\n        bind(storage).to(ConfigStorage.class);\n        bindAsContract(PluginManager.class).in(Singleton.class);\n        bindAsContract(InterpreterFactory.class).in(Singleton.class);\n        bindAsContract(NotebookRepoSync.class).to(NotebookRepo.class).in(Singleton.class);\n        bindAsContract(Notebook.class).in(Singleton.class);\n        // ... InterpreterSettingManager, SearchService, etc.\n    }\n}\n```\n\nREST API classes use `@Inject` to receive these singletons.\n\n## Contributing Guide\n\n### Prerequisites\n\n| Tool | Version | Notes |\n|------|---------|-------|\n| JDK | pinned in `pom.xml` (`java.version`) | Required — use exactly that major, not a newer/older JDK |\n| Maven | provided by `./mvnw` (pinned in `.mvn/wrapper/maven-wrapper.properties`) | No separate install needed |\n| Node.js | see `zeppelin-web-angular/package.json` (`engines.node`) | Only for frontend (`zeppelin-web-angular/`) |\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/apache/zeppelin.git\ncd zeppelin\n\n# First build — skip tests to verify environment works\n./mvnw clean package -DskipTests\n# This takes ~10 minutes. If it succeeds, your environment is ready.\n\n# Frontend setup (only if working on UI)\ncd zeppelin-web-angular\nnpm install\ncd ..\n```\n\n### Development Workflow\n\nWhen starting a new change, use a **git worktree** instead of switching branches in your main checkout. This keeps your primary working directory clean and allows parallel work across multiple branches:\n\n```bash\n# Create a worktree for your feature branch\ngit worktree add ../zeppelin-ZEPPELIN-XXXX -b ZEPPELIN-XXXX-description\ncd ../zeppelin-ZEPPELIN-XXXX\n\n# When done, clean up\ngit worktree remove ../zeppelin-ZEPPELIN-XXXX\n```\n\n```bash\n# Build only the module you're changing (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run tests for your module\n./mvnw test -pl zeppelin-server --am\n\n# Run a specific test\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Start the dev frontend (proxies API to localhost:8080)\ncd zeppelin-web-angular && npm start\n```\n\nFor Spark or Flink work, add the version profile:\n```bash\n./mvnw clean package -pl spark -Pspark-3.5 -Pspark-scala-2.12 -DskipTests\n```\n\n### Before Submitting a PR\n\n1. **Write unit tests**. Every code change must include corresponding unit tests. Bug fixes should include a test that reproduces the bug. New features should have tests covering the main paths.\n\n2. **Run tests for affected modules**:\n   ```bash\n   ./mvnw test -pl <your-module>\n   ```\n\n3. **Check license headers** — all new files must have the Apache License 2.0 header:\n   ```bash\n   ./mvnw clean org.apache.rat:apache-rat-plugin:check -Prat\n   ```\n\n4. **Lint frontend changes** (if applicable):\n   ```bash\n   cd zeppelin-web-angular && npm run lint:fix\n   ```\n\n5. **Create a JIRA issue** at [issues.apache.org/jira/browse/ZEPPELIN](https://issues.apache.org/jira/browse/ZEPPELIN) and use the issue number in PR title: `[ZEPPELIN-XXXX] description`.\n\n### REST API Pattern\n\nAll REST endpoints follow this pattern:\n\n```java\n@Path(\"/notebook\")\n@Produces(\"application/json\")\n@Singleton\npublic class NotebookRestApi extends AbstractRestApi {\n    @Inject\n    public NotebookRestApi(Notebook notebook, ...) {\n        super(authenticationService);\n    }\n\n    @GET\n    @Path(\"/{noteId}\")\n    @ZeppelinApi\n    public Response getNote(@PathParam(\"noteId\") String noteId) {\n        // Authorization check\n        checkIfUserCanRead(noteId, \"Insufficient privileges\");\n        // Business logic via service layer\n        Note note = notebook.getNote(noteId);\n        // Return JsonResponse\n        return new JsonResponse<>(Status.OK, \"\", note).build();\n    }\n}\n```\n\nKey conventions:\n- Extend `AbstractRestApi` (provides `getServiceContext()` for auth)\n- Use `@Inject` constructor for HK2 DI\n- Annotate public methods with `@ZeppelinApi`\n- Return `JsonResponse<T>(status, message, body).build()`\n- Authorization via `checkIfUserCan{Read|Write|Run}()`\n\n### Code Style\n\n- **Java**: Google Java Style (2-space indent). Checkstyle enforced — no tabs, LF line endings, newline at EOF\n- **Frontend**: ESLint + Prettier, auto-enforced via pre-commit hook (Husky + lint-staged)\n- **Testing**: JUnit 5 (Jupiter) + Mockito (Java; a small number of legacy JUnit 4 tests still exist), Playwright (frontend E2E)\n- **Logging**: SLF4J + Log4j2\n- **License**: Apache License 2.0 — all new files need the ASF header\n\n## Security\n\nSecurity model: [SECURITY.md](./SECURITY.md), which links to the project's\nthreat model at [THREAT_MODEL.md](./THREAT_MODEL.md).\n\nAgents that scan this repository should consult `THREAT_MODEL.md` for the\nproject's in-scope / out-of-scope declarations, the security properties it\nprovides and disclaims, the configuration knobs whose defaults change the\nsecurity envelope, and the known non-findings (recurring false positives)\nbefore reporting issues. In particular, Apache Zeppelin executes user-supplied\nnotebook code through its interpreters by design — that is the product's\nfunction, not a vulnerability; see `THREAT_MODEL.md` §3, §9, and §11a.\n"},"files":{"AGENTS.md":"<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n\n# AGENTS.md\n\n> Guidance for AI coding agents working on the Apache Zeppelin codebase.\n> See [AGENTS.md specification](https://github.com/agentsmd/agents.md).\n\n## Project Overview\n\nApache Zeppelin is a web-based notebook for interactive data analytics. It provides a unified interface to multiple data processing backends (Spark, Flink, Python, JDBC, etc.) through a pluggable interpreter architecture. Each interpreter runs in its own JVM process and communicates with the server via Apache Thrift RPC.\n\n- **Language**: Java, Scala — versions in root `pom.xml` (`java.version`, `scala.binary.version`)\n- **Build**: Maven multi-module (wrapper: `./mvnw`)\n- **Frontend**: Angular + TypeScript in `zeppelin-web-angular/` — versions in its `package.json`\n- **Version**: see `<version>` in root `pom.xml`\n\n## Build & Test\n\n```bash\n# Full build (skip tests)\n./mvnw clean package -DskipTests\n\n# Build single module (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run module tests\n./mvnw test -pl zeppelin-interpreter --am\n\n# Run single test class/method\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Common profiles\n#   -Pspark-3.5 -Pspark-scala-2.12   Spark version\n#   -Pflink-1.20                       Flink version\n#   -Pbuild-distr                      Full distribution\n#   -Prat                              Apache RAT license check\n#   -Pweb-classic                      Additionally builds the classic UI web module when specified\n```\n\n## Build Gotchas\n\n### Shaded JAR Rebuild Chain\n\nThe most common build mistake: modifying `zeppelin-interpreter` without rebuilding `zeppelin-interpreter-shaded`. The shaded JAR is an uber JAR that all interpreter processes use. If it's stale, you get `ClassNotFoundException` or `NoSuchMethodError` at runtime.\n\n```bash\n# After changing zeppelin-interpreter, ALWAYS rebuild in order:\n./mvnw clean package -pl zeppelin-interpreter -DskipTests\n./mvnw clean package -pl zeppelin-interpreter-shaded -DskipTests\n# Then rebuild affected interpreter modules\n\n# Shorthand:\n./mvnw clean package -pl zeppelin-interpreter,zeppelin-interpreter-shaded -DskipTests\n```\n\nThe shaded JAR is also copied to `interpreter/` directory by maven-antrun-plugin after packaging. If this directory has a stale JAR, interpreter processes will load old code.\n\n### Module Build Order\n\nMaven modules are ordered in the root `pom.xml`. Key sequence:\n```\nzeppelin-interpreter → zeppelin-interpreter-shaded → zeppelin-server\n```\n\nAll interpreter modules build after `zeppelin-interpreter-shaded`. A second shading chain exists for Jupyter:\n```\nzeppelin-jupyter-interpreter → zeppelin-jupyter-interpreter-shaded → python\n```\n\n## Module Architecture\n\n### Dependency Flow\n\n```\nzeppelin-interpreter          Base API: Interpreter, InterpreterContext, Thrift services\n        ↓\nzeppelin-interpreter-shaded   Uber JAR (maven-shade-plugin, relocated packages)\n        ↓\nzeppelin-server               Core engine + Jetty 11, REST/WebSocket APIs, HK2 DI, entry point\n```\n\n### Core Modules\n\n#### `zeppelin-interpreter/`\nThe base framework that all interpreters depend on. Defines the interpreter API and the Thrift communication protocol. This module is shaded into an uber JAR (`zeppelin-interpreter-shaded`) and placed on each interpreter process's classpath.\n\nKey classes:\n- `Interpreter` (abstract) / `AbstractInterpreter` — base class every interpreter extends\n- `InterpreterContext` — carries notebook/paragraph/user info into `interpret()` calls\n- `InterpreterGroup` — manages a group of interpreter instances sharing one process\n- `InterpreterResult` / `InterpreterOutput` — execution result model\n- `RemoteInterpreterServer` — **entry point of each interpreter JVM process**; implements the Thrift `RemoteInterpreterService` server; receives RPC calls from zeppelin-server\n- `InterpreterLauncher` (abstract) — how an interpreter process is started (Standard, Docker, K8s, YARN)\n- `LifecycleManager` — manages interpreter process lifecycle (Null = keep alive, Timeout = idle shutdown)\n- `DependencyResolver` / `AbstractDependencyResolver` — Maven artifact resolution for `%dep` paragraphs\n\nThrift definitions (`src/main/thrift/`):\n- `RemoteInterpreterService.thrift` — server → interpreter RPCs\n- `RemoteInterpreterEventService.thrift` — interpreter → server event callbacks\n\n#### `zeppelin-server/`\nThe entry point and core of the Zeppelin application. Combines the web server / API layer with the core notebook engine, interpreter lifecycle management, scheduling, search, and plugin loading.\n\nWeb / API layer (`org.apache.zeppelin.server`, `rest`, `socket`):\n- `ZeppelinServer` — `main()`, embedded Jetty 11 server, HK2 DI setup\n- `NotebookRestApi`, `InterpreterRestApi`, `SecurityRestApi`, `ConfigurationsRestApi` — REST endpoints in `org.apache.zeppelin.rest`\n- `NotebookServer` — WebSocket endpoint (`/ws`) for real-time notebook operations and paragraph execution\n- `RemoteInterpreterEventServer` — Thrift server receiving callbacks from interpreter processes (output streaming, status updates)\n\nEngine / runtime (`org.apache.zeppelin.notebook`, `interpreter`, `scheduler`, `search`, `plugin`, `storage`, `conf`):\n- `Notebook` / `Note` / `Paragraph` — notebook data model and execution\n- `InterpreterFactory` — creates interpreter instances\n- `InterpreterSettingManager` — loads `interpreter-setting.json` from each interpreter directory, manages interpreter configurations\n- `InterpreterSetting` — one interpreter's config + runtime state; creates `InterpreterLauncher` and `RemoteInterpreterProcess`\n- `ManagedInterpreterGroup` — server-side `InterpreterGroup` implementation; owns the `RemoteInterpreterProcess`\n- `NoteManager` — notebook CRUD, folder tree\n- `SchedulerService` — Quartz-based cron scheduling\n- `SearchService` — Lucene-based notebook search\n- `PluginManager` — loads launcher and notebook-repo plugins (custom classloading, not Java SPI)\n- `ZeppelinConfiguration` — config management (env vars → system properties → `zeppelin-site.xml` → defaults)\n- `RecoveryStorage` — persists interpreter process info for server-restart recovery\n- `ConfigStorage` — persists interpreter settings to JSON\n\n#### `zeppelin-interpreter-shaded/`\nUses maven-shade-plugin to package `zeppelin-interpreter` + dependencies into an uber JAR with relocated packages (e.g., `org.apache.thrift` → `org.apache.zeppelin.shaded.org.apache.thrift`). This JAR is placed on each interpreter process's classpath.\n\n#### `zeppelin-client/`\nREST/WebSocket client library for programmatic access to Zeppelin.\n\n### Interpreter Modules\n\nEach interpreter is an independent Maven module inheriting from `zeppelin-interpreter-parent`:\n\n| Module | Description |\n|--------|-------------|\n| `spark/` | Apache Spark (Scala/Python/R/SQL) — most complex interpreter |\n| `python/` | IPython/Python |\n| `flink/` | Apache Flink (Scala/Python/SQL) |\n| `jdbc/` | JDBC (PostgreSQL, MySQL, Hive, etc.) |\n| `shell/` | Bash/Shell commands |\n| `markdown/` | Markdown rendering (Flexmark) |\n| `java/` | Java interpreter |\n| `groovy/` | Groovy |\n| `neo4j/` | Neo4j Cypher |\n| `mongodb/` | MongoDB |\n| `elasticsearch/` | Elasticsearch |\n| `bigquery/` | Google BigQuery |\n| `cassandra/` | Apache Cassandra CQL |\n| `hbase/` | Apache HBase |\n| `livy/` | Apache Livy (remote Spark) |\n| `sparql/` | SPARQL queries |\n| `influxdb/` | InfluxDB |\n| `file/` | HDFS/local file browser |\n\n### Plugin Modules (`zeppelin-plugins/`)\n\n**Launcher plugins** (`launcher/`) — how interpreter processes are started:\n- `StandardInterpreterLauncher` (builtin) — local JVM process via `bin/interpreter.sh`\n- `SparkInterpreterLauncher` (builtin) — Spark-specific launcher with `spark-submit`\n- `DockerInterpreterLauncher` — Docker container\n- `K8sStandardInterpreterLauncher` — Kubernetes pod\n- `YarnInterpreterLauncher` — YARN container\n- `FlinkInterpreterLauncher` — Flink-specific\n- `ClusterInterpreterLauncher` — Zeppelin cluster mode\n\n**NotebookRepo plugins** (`notebookrepo/`) — where notebooks are persisted:\n- `VFSNotebookRepo` (builtin) — local filesystem (Apache VFS)\n- `GitNotebookRepo` (builtin) — local git repo\n- `GitHubNotebookRepo` — GitHub\n- `S3NotebookRepo` — Amazon S3\n- `GCSNotebookRepo` — Google Cloud Storage\n- `AzureNotebookRepo` — Azure Blob Storage\n- `MongoNotebookRepo` — MongoDB\n- `OSSNotebookRepo` — Alibaba Cloud OSS\n\n### Frontend\n\n- `zeppelin-web-angular/` — active frontend (Angular; versions in `package.json`, Node build pin in `pom.xml` `node.version`)\n- `zeppelin-web/` — Legacy AngularJS (activated with `-Pweb-classic`)\n\n### Configuration Files\n\n| File | Purpose |\n|------|---------|\n| `conf/zeppelin-site.xml` | Main server config (port, SSL, notebook storage, interpreter settings). Copy from `.template` |\n| `conf/zeppelin-env.sh` | Shell environment (JAVA_OPTS, memory, Spark master). Copy from `.template` |\n| `conf/shiro.ini` | Authentication/authorization (users, roles, LDAP, Kerberos, PAM). Copy from `.template` |\n| `conf/interpreter.json` | Runtime interpreter settings — **auto-generated**, do not edit manually |\n| `conf/log4j2.properties` | Logging configuration |\n| `conf/interpreter-list` | Static list of available interpreters with Maven coordinates |\n| `{interpreter}/resources/interpreter-setting.json` | Interpreter defaults (build-time, bundled in JAR) |\n\n`conf/*.template` files are the source of truth. Actual config files (`zeppelin-site.xml`, `shiro.ini`, etc.) are `.gitignored`.\n\n### Module Boundaries\n\nWhere new code should go:\n\n| If the code... | Put it in |\n|----------------|-----------|\n| Is a base interface/class that all interpreters need | `zeppelin-interpreter` |\n| Handles notebook state, interpreter lifecycle, scheduling, search, REST/WebSocket, or authentication realm | `zeppelin-server` |\n| Is specific to one backend (Spark, Flink, JDBC, etc.) | That interpreter's module |\n| Is a new way to launch interpreter processes | `zeppelin-plugins/launcher/` |\n| Is a new notebook storage backend | `zeppelin-plugins/notebookrepo/` |\n\n**Important**: Code added to `zeppelin-interpreter` is exposed to **every interpreter process** via the shaded JAR. Only add code there if all interpreters genuinely need it.\n\n## Server–Interpreter Communication\n\nZeppelin's most important architectural concept: the server and each interpreter run in **separate JVM processes** communicating via **Apache Thrift RPC**. This provides isolation, fault tolerance, and the ability to run interpreters on remote hosts or containers.\n\n### Thrift Code Generation\n\nThe `.thrift` files are in `zeppelin-interpreter/src/main/thrift/`. Generated Java files are **checked into git** (not generated at build time) in `zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/`.\n\nTo regenerate after modifying `.thrift` files:\n```bash\ncd zeppelin-interpreter/src/main/thrift\n./genthrift.sh   # requires 'thrift' compiler (v0.13.0) installed locally\n```\n\nThe script runs the Thrift compiler, prepends ASF license headers, and moves files to the source tree. **Never edit the generated Java files directly** — changes will be lost on next regeneration.\n\n### Thrift IPC — Bidirectional\n\n**Server → Interpreter** (`RemoteInterpreterService`):\n```\ninit(properties)                    — initialize interpreter process with config\ncreateInterpreter(className, ...)   — instantiate an interpreter class\nopen(sessionId, className)          — open/initialize an interpreter\ninterpret(sessionId, className, code, context) — execute code (core method)\ncancel(sessionId, className, ...)   — cancel running execution\ngetProgress(sessionId, className)   — poll execution progress (0-100)\ncompletion(sessionId, className, buf, cursor) — code completion\nclose(sessionId, className)         — close an interpreter\nshutdown()                          — terminate the interpreter process\n```\n\n**Interpreter → Server** (`RemoteInterpreterEventService`):\n```\nregisterInterpreterProcess(info)    — register after process startup\nappendOutput(event)                 — stream execution output incrementally\nupdateOutput(event)                 — replace output content\nsendParagraphInfo(info)             — update paragraph metadata\nupdateAppStatus(event)              — Zeppelin Application status\nrunParagraphs(request)              — trigger paragraph execution from interpreter\ngetResource(resourceId)             — access ResourcePool shared state\ngetParagraphList(noteId)            — query notebook structure\n```\n\n### Paragraph Execution Chain\n\nWhen a user runs a paragraph, the full call chain is:\n\n```\nUser clicks \"Run\" in browser\n  → WebSocket message to NotebookServer\n    → NotebookServer.runParagraph()\n      → Notebook.run()\n        → Paragraph.execute()\n          → RemoteInterpreter.interpret(code, context)\n            → RemoteInterpreterProcess.callRemoteFunction()\n              → [Thrift RPC over TCP]\n                → RemoteInterpreterServer.interpret()\n                  → actual Interpreter.interpret()  (e.g. SparkInterpreter)\n                    → result returned via Thrift\n          → meanwhile: interpreter calls appendOutput() to stream partial results back\n```\n\n### Interpreter Launch Chain\n\nWhen an interpreter process needs to be started:\n\n```\nRemoteInterpreter.interpret()  [first call triggers launch]\n  → ManagedInterpreterGroup.getOrCreateInterpreterProcess()\n    → InterpreterSetting.createInterpreterProcess()\n      → InterpreterSetting.createLauncher(properties)\n        → PluginManager.loadInterpreterLauncher(launcherPlugin)\n          → [builtin: Class.forName() / external: URLClassLoader]\n            → InterpreterLauncher.launch(context)\n              → new ExecRemoteInterpreterProcess(...)\n      → ExecRemoteInterpreterProcess.start()\n        → ProcessBuilder → \"bin/interpreter.sh\"\n          → java -cp ... RemoteInterpreterServer  [new JVM]\n            → RemoteInterpreterServer.main()\n              → registerInterpreterProcess() callback to server\n```\n\n### Interpreter Process Lifecycle\n\n1. **Launch**: Server creates `RemoteInterpreterProcess` via launcher plugin\n2. **Start**: Process starts as separate JVM (`bin/interpreter.sh` → `RemoteInterpreterServer.main()`)\n3. **Register**: Process calls `registerInterpreterProcess()` back to server's `RemoteInterpreterEventServer`\n4. **Init**: Server calls `init(properties)` — passes all configuration as a flat `Map<String, String>`\n5. **Create**: Server calls `createInterpreter(className, properties)` — instantiates interpreter via reflection\n6. **Open**: First `interpret()` triggers `LazyOpenInterpreter.open()` — interpreter initializes resources\n7. **Execute**: `interpret(code, context)` — runs code; partial output streams via `appendOutput()` events\n8. **Shutdown**: `close()` → `shutdown()` → JVM exits\n9. **Recovery**: `RecoveryStorage` persists process info; on server restart, reconnects to surviving processes\n\n### InterpreterGroup Scoping\n\n`InterpreterOption` controls process isolation via `perNote` and `perUser` settings:\n\n| perNote | perUser | Behavior |\n|---------|---------|----------|\n| `shared` | `shared` | All users share one process (default) |\n| `scoped` | `shared` | Separate interpreter instance per note, same process |\n| `isolated` | `shared` | Separate process per note |\n| `shared` | `scoped` | Separate interpreter instance per user, same process |\n| `shared` | `isolated` | Separate process per user |\n| `scoped` | `scoped` | Separate instance per user+note |\n| `isolated` | `isolated` | Separate process per user+note (full isolation) |\n\n## Plugin System & Reflection Patterns\n\n### PluginManager — Custom Classloading\n\n`PluginManager` (`zeppelin-server/.../plugin/PluginManager.java`) loads plugins without Java SPI:\n\n```\nPlugin loading flow:\n1. Check builtin list (hardcoded class names):\n   - Launchers: StandardInterpreterLauncher, SparkInterpreterLauncher\n   - NotebookRepos: VFSNotebookRepo, GitNotebookRepo\n   → if builtin: Class.forName(className) — direct classloading\n\n2. If not builtin → external plugin:\n   → Scan pluginsDir/{Launcher|NotebookRepo}/{pluginName}/ for JARs\n   → Create URLClassLoader with those JARs\n   → classLoader.loadClass(className)\n   → Instantiate via reflection (constructor parameters)\n```\n\nExternal plugin directory structure:\n```\nplugins/\n  Launcher/\n    DockerInterpreterLauncher/\n      *.jar\n    K8sStandardInterpreterLauncher/\n      *.jar\n  NotebookRepo/\n    S3NotebookRepo/\n      *.jar\n    GCSNotebookRepo/\n      *.jar\n```\n\n### ReflectionUtils\n\n`ReflectionUtils` (`zeppelin-server/.../util/ReflectionUtils.java`) provides generic reflection-based instantiation:\n\n```java\n// No-arg constructor\nReflectionUtils.createClazzInstance(className)\n\n// Parameterized constructor\nReflectionUtils.createClazzInstance(className, parameterTypes, parameters)\n```\n\nUsed to instantiate:\n- `RecoveryStorage` — in `RemoteInterpreterServer` and `InterpreterSettingManager`\n- `ConfigStorage` — in `InterpreterSettingManager`\n- `LifecycleManager` — in `RemoteInterpreterServer`\n- `NotebookRepo` — in `PluginManager`\n- `InterpreterLauncher` — in `PluginManager`\n\n### Interpreter Discovery\n\n`InterpreterSettingManager` discovers interpreters at startup:\n\n```\n1. Scan interpreterDir (default: interpreter/) for subdirectories\n2. For each subdirectory, look for interpreter-setting.json\n3. Parse JSON → List<RegisteredInterpreter>\n4. Register each interpreter's className, properties, editor settings\n```\n\n`interpreter-setting.json` format (in each interpreter module's resources):\n```json\n[{\n  \"group\": \"spark\",\n  \"name\": \"spark\",\n  \"className\": \"org.apache.zeppelin.spark.SparkInterpreter\",\n  \"properties\": {\n    \"spark.master\": { \"defaultValue\": \"local[*]\", \"description\": \"Spark master\" }\n  },\n  \"editor\": { \"language\": \"scala\", \"editOnDblClick\": false }\n}]\n```\n\n### ZeppelinConfiguration Priority\n\nConfiguration values are resolved in order (first match wins):\n1. **Environment variables** (e.g., `ZEPPELIN_HOME`, `ZEPPELIN_PORT`)\n2. **System properties** (e.g., `-Dzeppelin.server.port=8080`)\n3. **zeppelin-site.xml** (`conf/zeppelin-site.xml`)\n4. **Hardcoded defaults** (`ConfVars` enum in `ZeppelinConfiguration`)\n\n### HK2 Dependency Injection (zeppelin-server)\n\n`ZeppelinServer.startZeppelin()` sets up HK2 DI via `ServiceLocatorUtilities.bind()`:\n\n```java\nnew AbstractBinder() {\n    protected void configure() {\n        bind(storage).to(ConfigStorage.class);\n        bindAsContract(PluginManager.class).in(Singleton.class);\n        bindAsContract(InterpreterFactory.class).in(Singleton.class);\n        bindAsContract(NotebookRepoSync.class).to(NotebookRepo.class).in(Singleton.class);\n        bindAsContract(Notebook.class).in(Singleton.class);\n        // ... InterpreterSettingManager, SearchService, etc.\n    }\n}\n```\n\nREST API classes use `@Inject` to receive these singletons.\n\n## Contributing Guide\n\n### Prerequisites\n\n| Tool | Version | Notes |\n|------|---------|-------|\n| JDK | pinned in `pom.xml` (`java.version`) | Required — use exactly that major, not a newer/older JDK |\n| Maven | provided by `./mvnw` (pinned in `.mvn/wrapper/maven-wrapper.properties`) | No separate install needed |\n| Node.js | see `zeppelin-web-angular/package.json` (`engines.node`) | Only for frontend (`zeppelin-web-angular/`) |\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/apache/zeppelin.git\ncd zeppelin\n\n# First build — skip tests to verify environment works\n./mvnw clean package -DskipTests\n# This takes ~10 minutes. If it succeeds, your environment is ready.\n\n# Frontend setup (only if working on UI)\ncd zeppelin-web-angular\nnpm install\ncd ..\n```\n\n### Development Workflow\n\nWhen starting a new change, use a **git worktree** instead of switching branches in your main checkout. This keeps your primary working directory clean and allows parallel work across multiple branches:\n\n```bash\n# Create a worktree for your feature branch\ngit worktree add ../zeppelin-ZEPPELIN-XXXX -b ZEPPELIN-XXXX-description\ncd ../zeppelin-ZEPPELIN-XXXX\n\n# When done, clean up\ngit worktree remove ../zeppelin-ZEPPELIN-XXXX\n```\n\n```bash\n# Build only the module you're changing (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run tests for your module\n./mvnw test -pl zeppelin-server --am\n\n# Run a specific test\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Start the dev frontend (proxies API to localhost:8080)\ncd zeppelin-web-angular && npm start\n```\n\nFor Spark or Flink work, add the version profile:\n```bash\n./mvnw clean package -pl spark -Pspark-3.5 -Pspark-scala-2.12 -DskipTests\n```\n\n### Before Submitting a PR\n\n1. **Write unit tests**. Every code change must include corresponding unit tests. Bug fixes should include a test that reproduces the bug. New features should have tests covering the main paths.\n\n2. **Run tests for affected modules**:\n   ```bash\n   ./mvnw test -pl <your-module>\n   ```\n\n3. **Check license headers** — all new files must have the Apache License 2.0 header:\n   ```bash\n   ./mvnw clean org.apache.rat:apache-rat-plugin:check -Prat\n   ```\n\n4. **Lint frontend changes** (if applicable):\n   ```bash\n   cd zeppelin-web-angular && npm run lint:fix\n   ```\n\n5. **Create a JIRA issue** at [issues.apache.org/jira/browse/ZEPPELIN](https://issues.apache.org/jira/browse/ZEPPELIN) and use the issue number in PR title: `[ZEPPELIN-XXXX] description`.\n\n### REST API Pattern\n\nAll REST endpoints follow this pattern:\n\n```java\n@Path(\"/notebook\")\n@Produces(\"application/json\")\n@Singleton\npublic class NotebookRestApi extends AbstractRestApi {\n    @Inject\n    public NotebookRestApi(Notebook notebook, ...) {\n        super(authenticationService);\n    }\n\n    @GET\n    @Path(\"/{noteId}\")\n    @ZeppelinApi\n    public Response getNote(@PathParam(\"noteId\") String noteId) {\n        // Authorization check\n        checkIfUserCanRead(noteId, \"Insufficient privileges\");\n        // Business logic via service layer\n        Note note = notebook.getNote(noteId);\n        // Return JsonResponse\n        return new JsonResponse<>(Status.OK, \"\", note).build();\n    }\n}\n```\n\nKey conventions:\n- Extend `AbstractRestApi` (provides `getServiceContext()` for auth)\n- Use `@Inject` constructor for HK2 DI\n- Annotate public methods with `@ZeppelinApi`\n- Return `JsonResponse<T>(status, message, body).build()`\n- Authorization via `checkIfUserCan{Read|Write|Run}()`\n\n### Code Style\n\n- **Java**: Google Java Style (2-space indent). Checkstyle enforced — no tabs, LF line endings, newline at EOF\n- **Frontend**: ESLint + Prettier, auto-enforced via pre-commit hook (Husky + lint-staged)\n- **Testing**: JUnit 5 (Jupiter) + Mockito (Java; a small number of legacy JUnit 4 tests still exist), Playwright (frontend E2E)\n- **Logging**: SLF4J + Log4j2\n- **License**: Apache License 2.0 — all new files need the ASF header\n\n## Security\n\nSecurity model: [SECURITY.md](./SECURITY.md), which links to the project's\nthreat model at [THREAT_MODEL.md](./THREAT_MODEL.md).\n\nAgents that scan this repository should consult `THREAT_MODEL.md` for the\nproject's in-scope / out-of-scope declarations, the security properties it\nprovides and disclaims, the configuration knobs whose defaults change the\nsecurity envelope, and the known non-findings (recurring false positives)\nbefore reporting issues. In particular, Apache Zeppelin executes user-supplied\nnotebook code through its interpreters by design — that is the product's\nfunction, not a vulnerability; see `THREAT_MODEL.md` §3, §9, and §11a.\n"},"items":[{"name":"AGENTS.md","path":"AGENTS.md","title":"AGENTS.md","content":"<!--\nLicensed to the Apache Software Foundation (ASF) under one or more\ncontributor license agreements.  See the NOTICE file distributed with\nthis work for additional information regarding copyright ownership.\nThe ASF licenses this file to You under the Apache License, Version 2.0\n(the \"License\"); you may not use this file except in compliance with\nthe License.  You may obtain a copy of the License at\n\n   http://www.apache.org/licenses/LICENSE-2.0\n\nUnless required by applicable law or agreed to in writing, software\ndistributed under the License is distributed on an \"AS IS\" BASIS,\nWITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\nSee the License for the specific language governing permissions and\nlimitations under the License.\n-->\n\n# AGENTS.md\n\n> Guidance for AI coding agents working on the Apache Zeppelin codebase.\n> See [AGENTS.md specification](https://github.com/agentsmd/agents.md).\n\n## Project Overview\n\nApache Zeppelin is a web-based notebook for interactive data analytics. It provides a unified interface to multiple data processing backends (Spark, Flink, Python, JDBC, etc.) through a pluggable interpreter architecture. Each interpreter runs in its own JVM process and communicates with the server via Apache Thrift RPC.\n\n- **Language**: Java, Scala — versions in root `pom.xml` (`java.version`, `scala.binary.version`)\n- **Build**: Maven multi-module (wrapper: `./mvnw`)\n- **Frontend**: Angular + TypeScript in `zeppelin-web-angular/` — versions in its `package.json`\n- **Version**: see `<version>` in root `pom.xml`\n\n## Build & Test\n\n```bash\n# Full build (skip tests)\n./mvnw clean package -DskipTests\n\n# Build single module (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run module tests\n./mvnw test -pl zeppelin-interpreter --am\n\n# Run single test class/method\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Common profiles\n#   -Pspark-3.5 -Pspark-scala-2.12   Spark version\n#   -Pflink-1.20                       Flink version\n#   -Pbuild-distr                      Full distribution\n#   -Prat                              Apache RAT license check\n#   -Pweb-classic                      Additionally builds the classic UI web module when specified\n```\n\n## Build Gotchas\n\n### Shaded JAR Rebuild Chain\n\nThe most common build mistake: modifying `zeppelin-interpreter` without rebuilding `zeppelin-interpreter-shaded`. The shaded JAR is an uber JAR that all interpreter processes use. If it's stale, you get `ClassNotFoundException` or `NoSuchMethodError` at runtime.\n\n```bash\n# After changing zeppelin-interpreter, ALWAYS rebuild in order:\n./mvnw clean package -pl zeppelin-interpreter -DskipTests\n./mvnw clean package -pl zeppelin-interpreter-shaded -DskipTests\n# Then rebuild affected interpreter modules\n\n# Shorthand:\n./mvnw clean package -pl zeppelin-interpreter,zeppelin-interpreter-shaded -DskipTests\n```\n\nThe shaded JAR is also copied to `interpreter/` directory by maven-antrun-plugin after packaging. If this directory has a stale JAR, interpreter processes will load old code.\n\n### Module Build Order\n\nMaven modules are ordered in the root `pom.xml`. Key sequence:\n```\nzeppelin-interpreter → zeppelin-interpreter-shaded → zeppelin-server\n```\n\nAll interpreter modules build after `zeppelin-interpreter-shaded`. A second shading chain exists for Jupyter:\n```\nzeppelin-jupyter-interpreter → zeppelin-jupyter-interpreter-shaded → python\n```\n\n## Module Architecture\n\n### Dependency Flow\n\n```\nzeppelin-interpreter          Base API: Interpreter, InterpreterContext, Thrift services\n        ↓\nzeppelin-interpreter-shaded   Uber JAR (maven-shade-plugin, relocated packages)\n        ↓\nzeppelin-server               Core engine + Jetty 11, REST/WebSocket APIs, HK2 DI, entry point\n```\n\n### Core Modules\n\n#### `zeppelin-interpreter/`\nThe base framework that all interpreters depend on. Defines the interpreter API and the Thrift communication protocol. This module is shaded into an uber JAR (`zeppelin-interpreter-shaded`) and placed on each interpreter process's classpath.\n\nKey classes:\n- `Interpreter` (abstract) / `AbstractInterpreter` — base class every interpreter extends\n- `InterpreterContext` — carries notebook/paragraph/user info into `interpret()` calls\n- `InterpreterGroup` — manages a group of interpreter instances sharing one process\n- `InterpreterResult` / `InterpreterOutput` — execution result model\n- `RemoteInterpreterServer` — **entry point of each interpreter JVM process**; implements the Thrift `RemoteInterpreterService` server; receives RPC calls from zeppelin-server\n- `InterpreterLauncher` (abstract) — how an interpreter process is started (Standard, Docker, K8s, YARN)\n- `LifecycleManager` — manages interpreter process lifecycle (Null = keep alive, Timeout = idle shutdown)\n- `DependencyResolver` / `AbstractDependencyResolver` — Maven artifact resolution for `%dep` paragraphs\n\nThrift definitions (`src/main/thrift/`):\n- `RemoteInterpreterService.thrift` — server → interpreter RPCs\n- `RemoteInterpreterEventService.thrift` — interpreter → server event callbacks\n\n#### `zeppelin-server/`\nThe entry point and core of the Zeppelin application. Combines the web server / API layer with the core notebook engine, interpreter lifecycle management, scheduling, search, and plugin loading.\n\nWeb / API layer (`org.apache.zeppelin.server`, `rest`, `socket`):\n- `ZeppelinServer` — `main()`, embedded Jetty 11 server, HK2 DI setup\n- `NotebookRestApi`, `InterpreterRestApi`, `SecurityRestApi`, `ConfigurationsRestApi` — REST endpoints in `org.apache.zeppelin.rest`\n- `NotebookServer` — WebSocket endpoint (`/ws`) for real-time notebook operations and paragraph execution\n- `RemoteInterpreterEventServer` — Thrift server receiving callbacks from interpreter processes (output streaming, status updates)\n\nEngine / runtime (`org.apache.zeppelin.notebook`, `interpreter`, `scheduler`, `search`, `plugin`, `storage`, `conf`):\n- `Notebook` / `Note` / `Paragraph` — notebook data model and execution\n- `InterpreterFactory` — creates interpreter instances\n- `InterpreterSettingManager` — loads `interpreter-setting.json` from each interpreter directory, manages interpreter configurations\n- `InterpreterSetting` — one interpreter's config + runtime state; creates `InterpreterLauncher` and `RemoteInterpreterProcess`\n- `ManagedInterpreterGroup` — server-side `InterpreterGroup` implementation; owns the `RemoteInterpreterProcess`\n- `NoteManager` — notebook CRUD, folder tree\n- `SchedulerService` — Quartz-based cron scheduling\n- `SearchService` — Lucene-based notebook search\n- `PluginManager` — loads launcher and notebook-repo plugins (custom classloading, not Java SPI)\n- `ZeppelinConfiguration` — config management (env vars → system properties → `zeppelin-site.xml` → defaults)\n- `RecoveryStorage` — persists interpreter process info for server-restart recovery\n- `ConfigStorage` — persists interpreter settings to JSON\n\n#### `zeppelin-interpreter-shaded/`\nUses maven-shade-plugin to package `zeppelin-interpreter` + dependencies into an uber JAR with relocated packages (e.g., `org.apache.thrift` → `org.apache.zeppelin.shaded.org.apache.thrift`). This JAR is placed on each interpreter process's classpath.\n\n#### `zeppelin-client/`\nREST/WebSocket client library for programmatic access to Zeppelin.\n\n### Interpreter Modules\n\nEach interpreter is an independent Maven module inheriting from `zeppelin-interpreter-parent`:\n\n| Module | Description |\n|--------|-------------|\n| `spark/` | Apache Spark (Scala/Python/R/SQL) — most complex interpreter |\n| `python/` | IPython/Python |\n| `flink/` | Apache Flink (Scala/Python/SQL) |\n| `jdbc/` | JDBC (PostgreSQL, MySQL, Hive, etc.) |\n| `shell/` | Bash/Shell commands |\n| `markdown/` | Markdown rendering (Flexmark) |\n| `java/` | Java interpreter |\n| `groovy/` | Groovy |\n| `neo4j/` | Neo4j Cypher |\n| `mongodb/` | MongoDB |\n| `elasticsearch/` | Elasticsearch |\n| `bigquery/` | Google BigQuery |\n| `cassandra/` | Apache Cassandra CQL |\n| `hbase/` | Apache HBase |\n| `livy/` | Apache Livy (remote Spark) |\n| `sparql/` | SPARQL queries |\n| `influxdb/` | InfluxDB |\n| `file/` | HDFS/local file browser |\n\n### Plugin Modules (`zeppelin-plugins/`)\n\n**Launcher plugins** (`launcher/`) — how interpreter processes are started:\n- `StandardInterpreterLauncher` (builtin) — local JVM process via `bin/interpreter.sh`\n- `SparkInterpreterLauncher` (builtin) — Spark-specific launcher with `spark-submit`\n- `DockerInterpreterLauncher` — Docker container\n- `K8sStandardInterpreterLauncher` — Kubernetes pod\n- `YarnInterpreterLauncher` — YARN container\n- `FlinkInterpreterLauncher` — Flink-specific\n- `ClusterInterpreterLauncher` — Zeppelin cluster mode\n\n**NotebookRepo plugins** (`notebookrepo/`) — where notebooks are persisted:\n- `VFSNotebookRepo` (builtin) — local filesystem (Apache VFS)\n- `GitNotebookRepo` (builtin) — local git repo\n- `GitHubNotebookRepo` — GitHub\n- `S3NotebookRepo` — Amazon S3\n- `GCSNotebookRepo` — Google Cloud Storage\n- `AzureNotebookRepo` — Azure Blob Storage\n- `MongoNotebookRepo` — MongoDB\n- `OSSNotebookRepo` — Alibaba Cloud OSS\n\n### Frontend\n\n- `zeppelin-web-angular/` — active frontend (Angular; versions in `package.json`, Node build pin in `pom.xml` `node.version`)\n- `zeppelin-web/` — Legacy AngularJS (activated with `-Pweb-classic`)\n\n### Configuration Files\n\n| File | Purpose |\n|------|---------|\n| `conf/zeppelin-site.xml` | Main server config (port, SSL, notebook storage, interpreter settings). Copy from `.template` |\n| `conf/zeppelin-env.sh` | Shell environment (JAVA_OPTS, memory, Spark master). Copy from `.template` |\n| `conf/shiro.ini` | Authentication/authorization (users, roles, LDAP, Kerberos, PAM). Copy from `.template` |\n| `conf/interpreter.json` | Runtime interpreter settings — **auto-generated**, do not edit manually |\n| `conf/log4j2.properties` | Logging configuration |\n| `conf/interpreter-list` | Static list of available interpreters with Maven coordinates |\n| `{interpreter}/resources/interpreter-setting.json` | Interpreter defaults (build-time, bundled in JAR) |\n\n`conf/*.template` files are the source of truth. Actual config files (`zeppelin-site.xml`, `shiro.ini`, etc.) are `.gitignored`.\n\n### Module Boundaries\n\nWhere new code should go:\n\n| If the code... | Put it in |\n|----------------|-----------|\n| Is a base interface/class that all interpreters need | `zeppelin-interpreter` |\n| Handles notebook state, interpreter lifecycle, scheduling, search, REST/WebSocket, or authentication realm | `zeppelin-server` |\n| Is specific to one backend (Spark, Flink, JDBC, etc.) | That interpreter's module |\n| Is a new way to launch interpreter processes | `zeppelin-plugins/launcher/` |\n| Is a new notebook storage backend | `zeppelin-plugins/notebookrepo/` |\n\n**Important**: Code added to `zeppelin-interpreter` is exposed to **every interpreter process** via the shaded JAR. Only add code there if all interpreters genuinely need it.\n\n## Server–Interpreter Communication\n\nZeppelin's most important architectural concept: the server and each interpreter run in **separate JVM processes** communicating via **Apache Thrift RPC**. This provides isolation, fault tolerance, and the ability to run interpreters on remote hosts or containers.\n\n### Thrift Code Generation\n\nThe `.thrift` files are in `zeppelin-interpreter/src/main/thrift/`. Generated Java files are **checked into git** (not generated at build time) in `zeppelin-interpreter/src/main/java/org/apache/zeppelin/interpreter/thrift/`.\n\nTo regenerate after modifying `.thrift` files:\n```bash\ncd zeppelin-interpreter/src/main/thrift\n./genthrift.sh   # requires 'thrift' compiler (v0.13.0) installed locally\n```\n\nThe script runs the Thrift compiler, prepends ASF license headers, and moves files to the source tree. **Never edit the generated Java files directly** — changes will be lost on next regeneration.\n\n### Thrift IPC — Bidirectional\n\n**Server → Interpreter** (`RemoteInterpreterService`):\n```\ninit(properties)                    — initialize interpreter process with config\ncreateInterpreter(className, ...)   — instantiate an interpreter class\nopen(sessionId, className)          — open/initialize an interpreter\ninterpret(sessionId, className, code, context) — execute code (core method)\ncancel(sessionId, className, ...)   — cancel running execution\ngetProgress(sessionId, className)   — poll execution progress (0-100)\ncompletion(sessionId, className, buf, cursor) — code completion\nclose(sessionId, className)         — close an interpreter\nshutdown()                          — terminate the interpreter process\n```\n\n**Interpreter → Server** (`RemoteInterpreterEventService`):\n```\nregisterInterpreterProcess(info)    — register after process startup\nappendOutput(event)                 — stream execution output incrementally\nupdateOutput(event)                 — replace output content\nsendParagraphInfo(info)             — update paragraph metadata\nupdateAppStatus(event)              — Zeppelin Application status\nrunParagraphs(request)              — trigger paragraph execution from interpreter\ngetResource(resourceId)             — access ResourcePool shared state\ngetParagraphList(noteId)            — query notebook structure\n```\n\n### Paragraph Execution Chain\n\nWhen a user runs a paragraph, the full call chain is:\n\n```\nUser clicks \"Run\" in browser\n  → WebSocket message to NotebookServer\n    → NotebookServer.runParagraph()\n      → Notebook.run()\n        → Paragraph.execute()\n          → RemoteInterpreter.interpret(code, context)\n            → RemoteInterpreterProcess.callRemoteFunction()\n              → [Thrift RPC over TCP]\n                → RemoteInterpreterServer.interpret()\n                  → actual Interpreter.interpret()  (e.g. SparkInterpreter)\n                    → result returned via Thrift\n          → meanwhile: interpreter calls appendOutput() to stream partial results back\n```\n\n### Interpreter Launch Chain\n\nWhen an interpreter process needs to be started:\n\n```\nRemoteInterpreter.interpret()  [first call triggers launch]\n  → ManagedInterpreterGroup.getOrCreateInterpreterProcess()\n    → InterpreterSetting.createInterpreterProcess()\n      → InterpreterSetting.createLauncher(properties)\n        → PluginManager.loadInterpreterLauncher(launcherPlugin)\n          → [builtin: Class.forName() / external: URLClassLoader]\n            → InterpreterLauncher.launch(context)\n              → new ExecRemoteInterpreterProcess(...)\n      → ExecRemoteInterpreterProcess.start()\n        → ProcessBuilder → \"bin/interpreter.sh\"\n          → java -cp ... RemoteInterpreterServer  [new JVM]\n            → RemoteInterpreterServer.main()\n              → registerInterpreterProcess() callback to server\n```\n\n### Interpreter Process Lifecycle\n\n1. **Launch**: Server creates `RemoteInterpreterProcess` via launcher plugin\n2. **Start**: Process starts as separate JVM (`bin/interpreter.sh` → `RemoteInterpreterServer.main()`)\n3. **Register**: Process calls `registerInterpreterProcess()` back to server's `RemoteInterpreterEventServer`\n4. **Init**: Server calls `init(properties)` — passes all configuration as a flat `Map<String, String>`\n5. **Create**: Server calls `createInterpreter(className, properties)` — instantiates interpreter via reflection\n6. **Open**: First `interpret()` triggers `LazyOpenInterpreter.open()` — interpreter initializes resources\n7. **Execute**: `interpret(code, context)` — runs code; partial output streams via `appendOutput()` events\n8. **Shutdown**: `close()` → `shutdown()` → JVM exits\n9. **Recovery**: `RecoveryStorage` persists process info; on server restart, reconnects to surviving processes\n\n### InterpreterGroup Scoping\n\n`InterpreterOption` controls process isolation via `perNote` and `perUser` settings:\n\n| perNote | perUser | Behavior |\n|---------|---------|----------|\n| `shared` | `shared` | All users share one process (default) |\n| `scoped` | `shared` | Separate interpreter instance per note, same process |\n| `isolated` | `shared` | Separate process per note |\n| `shared` | `scoped` | Separate interpreter instance per user, same process |\n| `shared` | `isolated` | Separate process per user |\n| `scoped` | `scoped` | Separate instance per user+note |\n| `isolated` | `isolated` | Separate process per user+note (full isolation) |\n\n## Plugin System & Reflection Patterns\n\n### PluginManager — Custom Classloading\n\n`PluginManager` (`zeppelin-server/.../plugin/PluginManager.java`) loads plugins without Java SPI:\n\n```\nPlugin loading flow:\n1. Check builtin list (hardcoded class names):\n   - Launchers: StandardInterpreterLauncher, SparkInterpreterLauncher\n   - NotebookRepos: VFSNotebookRepo, GitNotebookRepo\n   → if builtin: Class.forName(className) — direct classloading\n\n2. If not builtin → external plugin:\n   → Scan pluginsDir/{Launcher|NotebookRepo}/{pluginName}/ for JARs\n   → Create URLClassLoader with those JARs\n   → classLoader.loadClass(className)\n   → Instantiate via reflection (constructor parameters)\n```\n\nExternal plugin directory structure:\n```\nplugins/\n  Launcher/\n    DockerInterpreterLauncher/\n      *.jar\n    K8sStandardInterpreterLauncher/\n      *.jar\n  NotebookRepo/\n    S3NotebookRepo/\n      *.jar\n    GCSNotebookRepo/\n      *.jar\n```\n\n### ReflectionUtils\n\n`ReflectionUtils` (`zeppelin-server/.../util/ReflectionUtils.java`) provides generic reflection-based instantiation:\n\n```java\n// No-arg constructor\nReflectionUtils.createClazzInstance(className)\n\n// Parameterized constructor\nReflectionUtils.createClazzInstance(className, parameterTypes, parameters)\n```\n\nUsed to instantiate:\n- `RecoveryStorage` — in `RemoteInterpreterServer` and `InterpreterSettingManager`\n- `ConfigStorage` — in `InterpreterSettingManager`\n- `LifecycleManager` — in `RemoteInterpreterServer`\n- `NotebookRepo` — in `PluginManager`\n- `InterpreterLauncher` — in `PluginManager`\n\n### Interpreter Discovery\n\n`InterpreterSettingManager` discovers interpreters at startup:\n\n```\n1. Scan interpreterDir (default: interpreter/) for subdirectories\n2. For each subdirectory, look for interpreter-setting.json\n3. Parse JSON → List<RegisteredInterpreter>\n4. Register each interpreter's className, properties, editor settings\n```\n\n`interpreter-setting.json` format (in each interpreter module's resources):\n```json\n[{\n  \"group\": \"spark\",\n  \"name\": \"spark\",\n  \"className\": \"org.apache.zeppelin.spark.SparkInterpreter\",\n  \"properties\": {\n    \"spark.master\": { \"defaultValue\": \"local[*]\", \"description\": \"Spark master\" }\n  },\n  \"editor\": { \"language\": \"scala\", \"editOnDblClick\": false }\n}]\n```\n\n### ZeppelinConfiguration Priority\n\nConfiguration values are resolved in order (first match wins):\n1. **Environment variables** (e.g., `ZEPPELIN_HOME`, `ZEPPELIN_PORT`)\n2. **System properties** (e.g., `-Dzeppelin.server.port=8080`)\n3. **zeppelin-site.xml** (`conf/zeppelin-site.xml`)\n4. **Hardcoded defaults** (`ConfVars` enum in `ZeppelinConfiguration`)\n\n### HK2 Dependency Injection (zeppelin-server)\n\n`ZeppelinServer.startZeppelin()` sets up HK2 DI via `ServiceLocatorUtilities.bind()`:\n\n```java\nnew AbstractBinder() {\n    protected void configure() {\n        bind(storage).to(ConfigStorage.class);\n        bindAsContract(PluginManager.class).in(Singleton.class);\n        bindAsContract(InterpreterFactory.class).in(Singleton.class);\n        bindAsContract(NotebookRepoSync.class).to(NotebookRepo.class).in(Singleton.class);\n        bindAsContract(Notebook.class).in(Singleton.class);\n        // ... InterpreterSettingManager, SearchService, etc.\n    }\n}\n```\n\nREST API classes use `@Inject` to receive these singletons.\n\n## Contributing Guide\n\n### Prerequisites\n\n| Tool | Version | Notes |\n|------|---------|-------|\n| JDK | pinned in `pom.xml` (`java.version`) | Required — use exactly that major, not a newer/older JDK |\n| Maven | provided by `./mvnw` (pinned in `.mvn/wrapper/maven-wrapper.properties`) | No separate install needed |\n| Node.js | see `zeppelin-web-angular/package.json` (`engines.node`) | Only for frontend (`zeppelin-web-angular/`) |\n\n### Initial Setup\n\n```bash\n# Clone the repository\ngit clone https://github.com/apache/zeppelin.git\ncd zeppelin\n\n# First build — skip tests to verify environment works\n./mvnw clean package -DskipTests\n# This takes ~10 minutes. If it succeeds, your environment is ready.\n\n# Frontend setup (only if working on UI)\ncd zeppelin-web-angular\nnpm install\ncd ..\n```\n\n### Development Workflow\n\nWhen starting a new change, use a **git worktree** instead of switching branches in your main checkout. This keeps your primary working directory clean and allows parallel work across multiple branches:\n\n```bash\n# Create a worktree for your feature branch\ngit worktree add ../zeppelin-ZEPPELIN-XXXX -b ZEPPELIN-XXXX-description\ncd ../zeppelin-ZEPPELIN-XXXX\n\n# When done, clean up\ngit worktree remove ../zeppelin-ZEPPELIN-XXXX\n```\n\n```bash\n# Build only the module you're changing (--am builds required upstream modules)\n./mvnw clean package -pl zeppelin-server --am -DskipTests\n\n# Run tests for your module\n./mvnw test -pl zeppelin-server --am\n\n# Run a specific test\n./mvnw test -pl zeppelin-server --am -Dtest=NotebookServerTest#testMethod\n\n# Start the dev frontend (proxies API to localhost:8080)\ncd zeppelin-web-angular && npm start\n```\n\nFor Spark or Flink work, add the version profile:\n```bash\n./mvnw clean package -pl spark -Pspark-3.5 -Pspark-scala-2.12 -DskipTests\n```\n\n### Before Submitting a PR\n\n1. **Write unit tests**. Every code change must include corresponding unit tests. Bug fixes should include a test that reproduces the bug. New features should have tests covering the main paths.\n\n2. **Run tests for affected modules**:\n   ```bash\n   ./mvnw test -pl <your-module>\n   ```\n\n3. **Check license headers** — all new files must have the Apache License 2.0 header:\n   ```bash\n   ./mvnw clean org.apache.rat:apache-rat-plugin:check -Prat\n   ```\n\n4. **Lint frontend changes** (if applicable):\n   ```bash\n   cd zeppelin-web-angular && npm run lint:fix\n   ```\n\n5. **Create a JIRA issue** at [issues.apache.org/jira/browse/ZEPPELIN](https://issues.apache.org/jira/browse/ZEPPELIN) and use the issue number in PR title: `[ZEPPELIN-XXXX] description`.\n\n### REST API Pattern\n\nAll REST endpoints follow this pattern:\n\n```java\n@Path(\"/notebook\")\n@Produces(\"application/json\")\n@Singleton\npublic class NotebookRestApi extends AbstractRestApi {\n    @Inject\n    public NotebookRestApi(Notebook notebook, ...) {\n        super(authenticationService);\n    }\n\n    @GET\n    @Path(\"/{noteId}\")\n    @ZeppelinApi\n    public Response getNote(@PathParam(\"noteId\") String noteId) {\n        // Authorization check\n        checkIfUserCanRead(noteId, \"Insufficient privileges\");\n        // Business logic via service layer\n        Note note = notebook.getNote(noteId);\n        // Return JsonResponse\n        return new JsonResponse<>(Status.OK, \"\", note).build();\n    }\n}\n```\n\nKey conventions:\n- Extend `AbstractRestApi` (provides `getServiceContext()` for auth)\n- Use `@Inject` constructor for HK2 DI\n- Annotate public methods with `@ZeppelinApi`\n- Return `JsonResponse<T>(status, message, body).build()`\n- Authorization via `checkIfUserCan{Read|Write|Run}()`\n\n### Code Style\n\n- **Java**: Google Java Style (2-space indent). Checkstyle enforced — no tabs, LF line endings, newline at EOF\n- **Frontend**: ESLint + Prettier, auto-enforced via pre-commit hook (Husky + lint-staged)\n- **Testing**: JUnit 5 (Jupiter) + Mockito (Java; a small number of legacy JUnit 4 tests still exist), Playwright (frontend E2E)\n- **Logging**: SLF4J + Log4j2\n- **License**: Apache License 2.0 — all new files need the ASF header\n\n## Security\n\nSecurity model: [SECURITY.md](./SECURITY.md), which links to the project's\nthreat model at [THREAT_MODEL.md](./THREAT_MODEL.md).\n\nAgents that scan this repository should consult `THREAT_MODEL.md` for the\nproject's in-scope / out-of-scope declarations, the security properties it\nprovides and disclaims, the configuration knobs whose defaults change the\nsecurity envelope, and the known non-findings (recurring false positives)\nbefore reporting issues. In particular, Apache Zeppelin executes user-supplied\nnotebook code through its interpreters by design — that is the product's\nfunction, not a vulnerability; see `THREAT_MODEL.md` §3, §9, and §11a.\n","category":"root","tokens":6078}]}