Skip to content

feat(local-runner): @deepnote/local-runner — run a .deepnote with edited inputs, locally or in Deepnote Cloud - #419

Merged
dinohamzic merged 77 commits into
mainfrom
feat/local-runner
Jul 24, 2026
Merged

feat(local-runner): @deepnote/local-runner — run a .deepnote with edited inputs, locally or in Deepnote Cloud#419
dinohamzic merged 77 commits into
mainfrom
feat/local-runner

Conversation

@jamesbhobbs

@jamesbhobbs jamesbhobbs commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Stacked on #418 (input-coercion) → #417 (deepnote run --cloud + @deepnote/cloud) → this PR. Review/merge in that order.

What

A new workspace package, @deepnote/local-runner, that extracts a reusable mechanism: run a .deepnote notebook with edited inputs and get outputs + a snapshot back — the building block a static page needs to drive execution. It runs a notebook two ways behind one small API: a local Python kernel, or Deepnote Cloud. Two example apps live under examples/local-runner/.

API

  • runWithInputs(input, inputs, options?) — run locally. input is a path, raw .deepnote YAML, or a DeepnoteFile. Reuses @deepnote/blocks (parse + coerceInputVariableValue), @deepnote/runtime-core (ExecutionEngine), and @deepnote/convert (snapshots) in-process — no shelling out. Returns per-block outputs in execution order, an ExecutionSummary, and a snapshot that is persisted next to the file by default (persistSnapshot: false to skip), like deepnote run.
  • runInCloud(input, inputs, options?) — run the same notebook in Deepnote Cloud via @deepnote/cloud. Resolves the notebook id from the file, falls back to finding it by project/notebook name, and if it isn't in the cloud yet creates it there — project, notebook, blocks — and runs it in the same call, reporting created: true. One click, no browser step: a token is required either way, so there is nothing a logged-in session could add. Pass createIfMissing: false to fail instead. On success it returns the parsed outputs plus a viewUrl deep-link to the notebook's runs sidebar. Inputs are coerced to the shape the API expects before sending.
  • serveStatic({ dir, notebookPath, cloudToken? }) — a deliberately small node:http helper exposing GET /api/info, POST /api/run (local), POST /api/run-cloud (cloud), and static files with a path-traversal guard. Binds to 127.0.0.1. No WebSocket/watch/theme/rendering.
  • listCloudRuns(input, options?) / getCloudRun(runId, options?) — a notebook's run history in Deepnote, and any past run's outputs read back from its snapshot without re-running it. They live in cloud-runs.ts; nothing there executes anything.
  • Plus openInCloud, applyInputOverrides, listInputBlocks, loadDeepnoteFile.

New @deepnote/cloud helpers

This PR adds to the package #417 introduced (it doesn't duplicate the runs client):

  • create-project.tscreateProject(baseUrl, token, spec) builds a project, its notebooks and their blocks over the authenticated API (POST /v2/projects, /v2/notebooks, /v2/blocks) and returns the ids Deepnote assigned. It takes a plain ProjectSpec rather than a DeepnoteFile, so the client stays thin and deepnote run --cloud --push can build on it rather than reinvent it.
  • cloud-runs.ts — also gains listNotebookRuns (GET /v2/notebooks/{id}/runs).
  • import.tsuploadNotebook(bytes, fileName, { domain }){ importId, launchUrl }: the unauthenticated POST /v1/import/init + presigned PUT. Untouched, and still the right flow for deepnote open, which has no token and genuinely wants a browser. It was only ever wrong for runInCloud, which always has one.
  • projects.tsfindNotebook (locate a notebook by project/notebook name via GET /v2/projects), getWorkspace (GET /v2/me), and notebookUrl (build the runs deep-link).

Design notes

  • Invariant: overrides are coerced to the schema shape for the persisted metadata and the cloud API (slider → string), while the raw native values are what get injected into the local kernel — the two are intentionally different.
  • Cloud converges, no re-create loop: run by id → find by name → create only if truly absent, so a second click runs the created notebook instead of creating another project. "Truly absent" is load-bearing: a lookup that fails is not evidence of absence, so it throws rather than creating a duplicate.
  • No snapshot drift: the in-memory and persisted snapshots are derived from the same (file, outputs, timing).
  • Failure semantics: a failing block is reported via summary.failedBlocks; only infra/config errors throw (no Python env, missing toolkit, invalid file, missing cloud token, or persistSnapshot without a path).
  • A browser renderer and a deepnote serve CLI command are intentionally out of scope (future work).

Demo

examples/local-runner/ — two small reference apps sharing a visual language. run-app builds a control per input and runs the notebook live (a local Python kernel, or Run in cloud via runInCloud), rendering the returned outputs, with a Cloud runs sidebar listing the notebook's run history in Deepnote — click any run to render its snapshot without re-running it. snapshot-viewer is a fully static page that renders an already-run snapshot — outputs, a chart, and a precomputed agent readout — with no server or kernel. Each is a one-command pnpm example:* away.

Tests

Unit tests mock ExecutionEngine and @deepnote/cloud (no Python, no network): input coercion/apply, the runWithInputs invariant + snapshot validity + failure passthrough, the full runInCloud flow (by-id, find-by-name, create-if-missing, block-id remapping, refusing to guess which notebook a cloud id means, lookup failures not being read as absence, viewUrl, token-required), createProject (ordering, placeholder cleanup, progress, auth, non-JSON bodies), and listCloudRuns / getCloudRun, and the serveStatic routes incl. /api/run-cloud, bad-body error JSON, content-type, and an encoded path-traversal rejection. A real end-to-end local test is gated behind DEEPNOTE_TOOLKIT_PYTHON; the cloud path was verified live against the API.

@deepnote/local-runner + @deepnote/cloud + @deepnote/cli + @deepnote/blocks suites green (1 skipped: the gated integration test); typecheck + biome clean.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features
    • Schema-aware input overrides with optional notebook scoping, atomic validation, and input-block discovery; added input listing utilities.
    • Local execution enhancements (streaming agent events) plus Deepnote Cloud execution (run triggering, polling, snapshot settling, optional project creation/import).
    • Browser-safe snapshot parsing/viewing and snapshot reading/sharing.
    • Added a local HTTP server for running and serving snapshots/files, with secured static routing.
    • Added gallery/run/snapshot-viewer example frontends and Cloud client APIs (projects/runs/create-project/import).
  • Documentation
    • Expanded local-runner and Cloud API guides; documented new examples.
  • Bug Fixes
    • Corrected Cloud run completion timestamp handling and improved v2 API error message extraction.
  • Tests
    • Added comprehensive unit/integration/e2e coverage for overrides, snapshots, server security, and Cloud APIs.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2f03560b-ebd6-4999-8cd6-d18b532264c4

📥 Commits

Reviewing files that changed from the base of the PR and between 4bebeab and 9a8d541.

📒 Files selected for processing (1)
  • examples/local-runner/run-app/index.html
🚧 Files skipped from review as they are similar to previous changes (1)
  • examples/local-runner/run-app/index.html

📝 Walkthrough

Walkthrough

Adds @deepnote/local-runner with schema-aware local and cloud execution, snapshot parsing, static serving, browser bundles, Cloud API helpers, package exports, tests, documentation, and demonstration applications.

Estimated code review effort: 5 (Critical) | ~120 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Browser
  participant serveStatic
  participant runWithInputs
  participant ExecutionEngine
  participant Snapshot
  Browser->>serveStatic: POST /api/run with inputs
  serveStatic->>runWithInputs: Run notebook
  runWithInputs->>ExecutionEngine: Start and execute project
  ExecutionEngine-->>runWithInputs: Outputs and block results
  runWithInputs->>Snapshot: Build snapshot YAML
  Snapshot-->>serveStatic: Snapshot and summary
  serveStatic-->>Browser: JSON execution result
Loading

Possibly related PRs

Suggested reviewers: dinohamzic

🚥 Pre-merge checks | ✅ 5 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 72.84% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately summarizes the new @deepnote/local-runner capability to run edited .deepnote notebooks locally or in Deepnote Cloud.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Updates Docs ✅ Passed Docs were updated in OSS: local-runner/cloud/example READMEs cover the new APIs, behaviors, and demos; I can't verify the private roadmap repo, so please update it separately.

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

@codecov

codecov Bot commented Jul 10, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.40659% with 48 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.36%. Comparing base (50a4af7) to head (9a8d541).

Files with missing lines Patch % Lines
packages/cloud/src/cloud-runs.ts 16.00% 21 Missing ⚠️
packages/local-runner/src/open-in-cloud.ts 0.00% 7 Missing ⚠️
packages/local-runner/src/run-in-cloud.ts 95.80% 6 Missing ⚠️
packages/local-runner/src/serve-static.ts 95.45% 6 Missing ⚠️
packages/local-runner/src/cloud-common.ts 93.84% 4 Missing ⚠️
packages/cloud/src/create-project.ts 96.15% 3 Missing ⚠️
packages/local-runner/src/load-file.ts 83.33% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #419      +/-   ##
==========================================
+ Coverage   86.85%   87.36%   +0.50%     
==========================================
  Files         166      181      +15     
  Lines        8766     9494     +728     
  Branches     2478     2624     +146     
==========================================
+ Hits         7614     8294     +680     
- Misses       1151     1199      +48     
  Partials        1        1              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (2)
packages/local-runner/src/serve-static.test.ts (2)

10-31: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Use ts-dedent for the multiline fixture.

This raw multiline template literal violates the repository TypeScript rule and makes indentation changes easier to introduce.

As per coding guidelines, TypeScript files should use ts-dedent for clean multiline template strings.

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

In `@packages/local-runner/src/serve-static.test.ts` around lines 10 - 31, Replace
the raw multiline NOTEBOOK template literal with the repository’s ts-dedent
utility, importing it as needed and wrapping the fixture content with dedent so
indentation remains clean and consistent.

Source: Coding guidelines


71-116: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Add regression coverage for rejected-request branches.

Add tests for invalid inputs shapes, oversized bodies, malformed percent-encoding, and symlink escapes so the server’s validation and traversal guarantees remain enforced.

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

In `@packages/local-runner/src/serve-static.test.ts` around lines 71 - 116, Add
regression tests to the serveStatic suite covering rejected requests: invalid
`inputs` shapes for POST /api/run, request bodies exceeding the configured size
limit, malformed percent-encoded paths, and static-file symlinks escaping the
serving root. Use the existing rawStatus/handle helpers and temporary fixture
setup as appropriate, asserting the expected 400, 413, 400, and 403 responses
respectively.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/local-runner/src/run-with-inputs.ts`:
- Around line 67-73: Validate the input’s persistSnapshot setting immediately
after loadDeepnoteFile and before constructing or starting ExecutionEngine;
reject persistSnapshot: true with the existing unsupported-configuration error.
Update the related test to assert validation occurs before engine startup and
that no execution or side effects occur.

In `@packages/local-runner/src/serve-static.ts`:
- Around line 77-86: Validate the parsed request’s inputs before calling runner
in the POST /api/run handler: accept only non-null, non-array objects, return a
400 response for null, arrays, strings, or other invalid values, and pass the
validated record to runner instead of relying on the erased type assertion.
- Around line 106-108: Handle malformed percent-encoding in serveFile by
catching URIError specifically around decodeURIComponent and responding with
HTTP 400, rather than allowing it to reach the outer handler and become a 500;
preserve existing behavior for other errors.
- Around line 115-120: Update the static-file serving logic to validate and read
the target before calling res.writeHead(200), ensuring directories are rejected
and readFile errors are handled by the existing error path without sending
headers first. Use the handler’s target validation and readFile flow around
sendJson and res.end to preserve the 404 response for invalid entries and only
send 200 after a successful file read.
- Around line 106-118: The path-traversal protection in serveFile is only
lexical and allows symlinks under rootDir to escape the serving directory.
Resolve rootDir and target to real paths, then verify the real target remains
within the real root before reading or serving it; return 403 for escapes and
preserve appropriate not-found handling for missing paths.
- Around line 128-137: Update readBody to track received byte length rather than
string length, and explicitly reject with a payload-too-large error when the
limit is exceeded before destroying the request. Handle req.aborted and
req.close events so the Promise cannot remain pending, while avoiding duplicate
settlement; ensure the /api/run caller can identify this limit error and respond
with HTTP 413.

---

Nitpick comments:
In `@packages/local-runner/src/serve-static.test.ts`:
- Around line 10-31: Replace the raw multiline NOTEBOOK template literal with
the repository’s ts-dedent utility, importing it as needed and wrapping the
fixture content with dedent so indentation remains clean and consistent.
- Around line 71-116: Add regression tests to the serveStatic suite covering
rejected requests: invalid `inputs` shapes for POST /api/run, request bodies
exceeding the configured size limit, malformed percent-encoded paths, and
static-file symlinks escaping the serving root. Use the existing
rawStatus/handle helpers and temporary fixture setup as appropriate, asserting
the expected 400, 413, 400, and 403 responses respectively.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f1cdcf9a-f056-41a6-8578-addfd4a80885

📥 Commits

Reviewing files that changed from the base of the PR and between feb76b0 and 7ebd653.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (12)
  • packages/local-runner/README.md
  • packages/local-runner/package.json
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/load-file.ts
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
  • packages/local-runner/tsdown.config.ts

Comment thread packages/local-runner/src/run-with-inputs.ts
Comment thread packages/local-runner/src/serve-static.ts Outdated
Comment thread packages/local-runner/src/serve-static.ts Outdated
Comment thread packages/local-runner/src/serve-static.ts
Comment thread packages/local-runner/src/serve-static.ts Outdated
Comment thread packages/local-runner/src/serve-static.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/local-runner/src/run-with-inputs.test.ts (1)

121-128: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert persisted snapshot contents, not only file existence.

Read result.snapshotPath and verify it contains the expected serialized output (or matches result.snapshotYaml). This catches write-path and serialization regressions that the current existence checks miss.

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

In `@packages/local-runner/src/run-with-inputs.test.ts` around lines 121 - 128,
Update the test “persists a snapshot next to a path input by default (like
deepnote run)” to read the file at result.snapshotPath and assert its contents
contain the expected serialized output, preferably matching result.snapshotYaml.
Keep the existing path and existence assertions while adding validation of the
persisted snapshot data.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@packages/local-runner/src/run-with-inputs.test.ts`:
- Around line 121-128: Update the test “persists a snapshot next to a path input
by default (like deepnote run)” to read the file at result.snapshotPath and
assert its contents contain the expected serialized output, preferably matching
result.snapshotYaml. Keep the existing path and existence assertions while
adding validation of the persisted snapshot data.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 3907b81a-4ec2-4f6b-8586-a20139bffac5

📥 Commits

Reviewing files that changed from the base of the PR and between 20f595c and 66d82ac.

📒 Files selected for processing (4)
  • packages/local-runner/README.md
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/local-runner/README.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/local-runner/src/serve-static.ts

@jamesbhobbs
jamesbhobbs force-pushed the fix/input-coercion-snapshot branch from feb76b0 to ee6e6a7 Compare July 11, 2026 10:32
@jamesbhobbs
jamesbhobbs force-pushed the feat/local-runner branch 2 times, most recently from 85f975b to 7965b78 Compare July 11, 2026 10:43

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/local-runner/src/run-in-cloud.ts (1)

105-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared input-coercion helper. coerceInputs repeats the same input-block lookup and coerceInputVariableValue call used by applyInputOverrides; extracting that shared logic would keep the cloud and local-run paths from drifting.

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

In `@packages/local-runner/src/run-in-cloud.ts` around lines 105 - 121, Extract
the input-block lookup and coerceInputVariableValue logic from coerceInputs and
applyInputOverrides into a shared helper, then have both callers reuse it while
preserving each function’s existing input/output behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/run-cloud.ts`:
- Around line 306-309: Update the defensive refetch in the post-poll handling
around getRun so failures are caught and represented as missing snapshot content
rather than escaping. Preserve the terminal runId and route the failed refetch
through the existing snapshot-error handling path, allowing the command to
render its structured result.
- Around line 214-218: Sanitize the remote runId before interpolating it into
snapshotPath in the fallback write flow. Ensure the resulting filename cannot
contain path separators or traversal segments and remains within the snapshots
directory, using an existing filename-safe identifier or generated name while
preserving the current snapshot extension and write behavior.

In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 79-91: Update runInCloud to defensively re-fetch the terminal run
with getRun when the successful polled result lacks an inline snapshot, then use
the refreshed run for fetchSnapshotContent and output extraction while
preserving existing failure handling. Add the corresponding getRun mock in
run-in-cloud.test.ts so this path is covered.

---

Nitpick comments:
In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 105-121: Extract the input-block lookup and
coerceInputVariableValue logic from coerceInputs and applyInputOverrides into a
shared helper, then have both callers reuse it while preserving each function’s
existing input/output behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdd426d1-356f-4d72-988c-4d2620dde6f7

📥 Commits

Reviewing files that changed from the base of the PR and between 66d82ac and 7965b78.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • packages/cli/package.json
  • packages/cli/src/commands/run-cloud.ts
  • packages/cloud/README.md
  • packages/cloud/package.json
  • packages/cloud/src/cloud-runs.test.ts
  • packages/cloud/src/cloud-runs.ts
  • packages/cloud/src/index.ts
  • packages/cloud/tsdown.config.ts
  • packages/local-runner/README.md
  • packages/local-runner/package.json
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/load-file.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
  • packages/local-runner/tsdown.config.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/cloud/README.md
  • packages/cloud/tsdown.config.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/tsdown.config.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/load-file.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Caution

Inline review comments failed to post. This is likely due to GitHub's internal server error or limits when posting large numbers of comments. If you are seeing this consistently it is likely a permissions issue. Please check "Moderation" -> "Code review limits" under your organization settings.

Actionable comments posted: 3

🧹 Nitpick comments (1)
packages/local-runner/src/run-in-cloud.ts (1)

105-121: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Reuse the shared input-coercion helper. coerceInputs repeats the same input-block lookup and coerceInputVariableValue call used by applyInputOverrides; extracting that shared logic would keep the cloud and local-run paths from drifting.

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

In `@packages/local-runner/src/run-in-cloud.ts` around lines 105 - 121, Extract
the input-block lookup and coerceInputVariableValue logic from coerceInputs and
applyInputOverrides into a shared helper, then have both callers reuse it while
preserving each function’s existing input/output behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cli/src/commands/run-cloud.ts`:
- Around line 306-309: Update the defensive refetch in the post-poll handling
around getRun so failures are caught and represented as missing snapshot content
rather than escaping. Preserve the terminal runId and route the failed refetch
through the existing snapshot-error handling path, allowing the command to
render its structured result.
- Around line 214-218: Sanitize the remote runId before interpolating it into
snapshotPath in the fallback write flow. Ensure the resulting filename cannot
contain path separators or traversal segments and remains within the snapshots
directory, using an existing filename-safe identifier or generated name while
preserving the current snapshot extension and write behavior.

In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 79-91: Update runInCloud to defensively re-fetch the terminal run
with getRun when the successful polled result lacks an inline snapshot, then use
the refreshed run for fetchSnapshotContent and output extraction while
preserving existing failure handling. Add the corresponding getRun mock in
run-in-cloud.test.ts so this path is covered.

---

Nitpick comments:
In `@packages/local-runner/src/run-in-cloud.ts`:
- Around line 105-121: Extract the input-block lookup and
coerceInputVariableValue logic from coerceInputs and applyInputOverrides into a
shared helper, then have both callers reuse it while preserving each function’s
existing input/output behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: bdd426d1-356f-4d72-988c-4d2620dde6f7

📥 Commits

Reviewing files that changed from the base of the PR and between 66d82ac and 7965b78.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (22)
  • packages/cli/package.json
  • packages/cli/src/commands/run-cloud.ts
  • packages/cloud/README.md
  • packages/cloud/package.json
  • packages/cloud/src/cloud-runs.test.ts
  • packages/cloud/src/cloud-runs.ts
  • packages/cloud/src/index.ts
  • packages/cloud/tsdown.config.ts
  • packages/local-runner/README.md
  • packages/local-runner/package.json
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/load-file.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
  • packages/local-runner/tsdown.config.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/cloud/README.md
  • packages/cloud/tsdown.config.ts
🚧 Files skipped from review as they are similar to previous changes (10)
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/tsdown.config.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/README.md
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/load-file.ts
🛑 Comments failed to post (3)
packages/cli/src/commands/run-cloud.ts (2)

214-218: 🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

rg -n 'runId|NormalizedRun|triggerNotebookRun|getRun' packages/cloud packages/cli

Repository: deepnote/deepnote

Length of output: 7747


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- outline: packages/cli/src/commands/run-cloud.ts ---'
ast-grep outline packages/cli/src/commands/run-cloud.ts --view expanded || true

echo
echo '--- lines 200-230 ---'
sed -n '200,230p' packages/cli/src/commands/run-cloud.ts

echo
echo '--- lines 288-320 ---'
sed -n '288,320p' packages/cli/src/commands/run-cloud.ts

echo
echo '--- relevant tests around snapshot path ---'
sed -n '250,340p' packages/cli/src/commands/run-cloud.test.ts

Repository: deepnote/deepnote

Length of output: 7873


Sanitize runId before using it in the fallback filename. runId comes from the remote response, so a path separator or .. segment can escape ./snapshots; use a filename-safe ID or a generated name here.

🧰 Tools
🪛 ast-grep (0.44.1)

[warning] 217-217: Filesystem path is not a string literal; a request-/variable-derived path can enable path traversal. Validate and normalize the path before use.
Context: fs.writeFile(snapshotPath, bytes, 'utf-8')
Note: [CWE-22] Improper Limitation of a Pathname to a Restricted Directory ('Path Traversal').

(detect-non-literal-fs-filename-typescript)

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

In `@packages/cli/src/commands/run-cloud.ts` around lines 214 - 218, Sanitize the
remote runId before interpolating it into snapshotPath in the fallback write
flow. Ensure the resulting filename cannot contain path separators or traversal
segments and remains within the snapshots directory, using an existing
filename-safe identifier or generated name while preserving the current snapshot
extension and write behavior.

Source: Linters/SAST tools


306-309: 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Do not let the defensive refetch bypass post-run handling.

If getRun fails after polling has already returned a terminal runId, the exception escapes before the command renders its structured result. Treat the refetch failure as missing snapshot content and let the existing snapshot-error path handle it.

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

In `@packages/cli/src/commands/run-cloud.ts` around lines 306 - 309, Update the
defensive refetch in the post-poll handling around getRun so failures are caught
and represented as missing snapshot content rather than escaping. Preserve the
terminal runId and route the failed refetch through the existing snapshot-error
handling path, allowing the command to render its structured result.
packages/local-runner/src/run-in-cloud.ts (1)

79-91: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Missing defensive re-fetch when the terminal run lacks an inline snapshot.

packages/cli/src/commands/run-cloud.ts hits this exact scenario and explicitly re-fetches via getRun when !finalRun.snapshot after polling, with the comment noting some deployments only attach the snapshot once terminal. runInCloud skips that guard — fetchSnapshotContent returns null immediately when run.snapshot is absent, so a genuinely successful run can silently come back with success: true, outputs: [], snapshotYaml: null.

🔧 Proposed fix: mirror the CLI's defensive re-fetch
 import {
   describeRunError,
   fetchSnapshotContent,
+  getRun,
   isSuccessStatus,
   type PollOptions,
   pollRunUntilComplete,
   triggerNotebookRun,
 } from '`@deepnote/cloud`'
@@
-  const run = await pollRunUntilComplete(baseUrl, token, started.runId, { snapshotDelivery: 'inline', ...options.poll })
+  let run = await pollRunUntilComplete(baseUrl, token, started.runId, { snapshotDelivery: 'inline', ...options.poll })
+  // Some deployments only attach the snapshot once the run is confirmed terminal.
+  if (!run.snapshot) {
+    run = await getRun(baseUrl, token, run.runId, { snapshotDelivery: 'inline' })
+  }
 
   const success = isSuccessStatus(run.status)

Note: run-in-cloud.test.ts's vi.mock('@deepnote/cloud', ...) will need to add a getRun mock for this path to be testable.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

  let run = await pollRunUntilComplete(baseUrl, token, started.runId, { snapshotDelivery: 'inline', ...options.poll })

  if (!run.snapshot) {
    run = await getRun(baseUrl, token, run.runId, { snapshotDelivery: 'inline' })
  }

  const success = isSuccessStatus(run.status)
  const snapshotYaml = success ? await fetchSnapshotContent(run, { baseUrl, token }) : null

  return {
    runId: run.runId,
    status: run.status,
    success,
    outputs: snapshotYaml ? extractOutputs(snapshotYaml) : [],
    snapshotYaml,
    error: success ? undefined : describeRunError(run),
  }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/local-runner/src/run-in-cloud.ts` around lines 79 - 91, Update
runInCloud to defensively re-fetch the terminal run with getRun when the
successful polled result lacks an inline snapshot, then use the refreshed run
for fetchSnapshotContent and output extraction while preserving existing failure
handling. Add the corresponding getRun mock in run-in-cloud.test.ts so this path
is covered.

Source: Linked repositories

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (2)
packages/local-runner/src/run-in-cloud.test.ts (1)

127-140: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

No test covers domain/baseUrl propagation into the upload fallback.

Given the upload-if-missing path silently drops any custom baseUrl/domain (see open-in-cloud.ts), a test asserting uploadNotebook receives the expected domain would catch that regression.

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

In `@packages/local-runner/src/run-in-cloud.test.ts` around lines 127 - 140,
Extend the upload-if-missing test around runInCloud to pass a custom
baseUrl/domain and assert cloudMock.uploadNotebook receives that value. Preserve
the existing missing-notebook result assertions while verifying domain
propagation through the fallback path.
packages/cloud/src/import.ts (1)

49-49: 🎯 Functional Correctness | 🔵 Trivial | 💤 Low value

No runtime validation of the init response shape.

init is blindly cast (as InitImportResponse) from JSON; a malformed/renamed field on the server side would surface as an opaque failure later (e.g. fetch(init.uploadUrl, ...) with undefined). Consider a minimal shape check before use.

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

In `@packages/cloud/src/import.ts` at line 49, Validate the parsed response in the
import initialization flow before using it as InitImportResponse. Add a minimal
runtime check that confirms the required initialization fields, especially
uploadUrl, exist and have the expected shape, and fail immediately with a clear
error when validation fails; keep subsequent fetch usage unchanged for valid
responses.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/cloud/src/import.ts`:
- Around line 51-55: Update the presigned upload request in the import flow to
pass an AbortSignal timeout using the existing timeout configuration, matching
the init POST behavior. Ensure the PUT fetch for fileBytes cannot block
indefinitely while preserving its current method, headers, and body.

In `@packages/local-runner/src/open-in-cloud.ts`:
- Around line 21-30: The runInCloud not-found fallback drops the custom domain
when delegating to openInCloud, causing uploads to use the default endpoint.
Update the fallback call in run-in-cloud.ts to pass the domain derived from
baseUrl alongside inputs, and add coverage in
packages/local-runner/src/run-in-cloud.test.ts for a custom baseUrl/domain
asserting uploadNotebook receives that domain; the openInCloud function already
forwards options.domain and requires no direct change.

---

Nitpick comments:
In `@packages/cloud/src/import.ts`:
- Line 49: Validate the parsed response in the import initialization flow before
using it as InitImportResponse. Add a minimal runtime check that confirms the
required initialization fields, especially uploadUrl, exist and have the
expected shape, and fail immediately with a clear error when validation fails;
keep subsequent fetch usage unchanged for valid responses.

In `@packages/local-runner/src/run-in-cloud.test.ts`:
- Around line 127-140: Extend the upload-if-missing test around runInCloud to
pass a custom baseUrl/domain and assert cloudMock.uploadNotebook receives that
value. Preserve the existing missing-notebook result assertions while verifying
domain propagation through the fallback path.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 19b9d0df-5044-49f6-a7ce-a8a099ebc629

📥 Commits

Reviewing files that changed from the base of the PR and between 7965b78 and 2f232dd.

📒 Files selected for processing (7)
  • packages/cloud/src/import.ts
  • packages/cloud/src/index.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/open-in-cloud.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/serve-static.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/serve-static.ts

Comment thread packages/cloud/src/import.ts
Comment thread packages/local-runner/src/open-in-cloud.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@packages/cloud/src/projects.ts`:
- Around line 58-66: Update findNotebookId so that when query.notebookName is
provided, it only returns a notebook with that exact name while iterating
through projects; continue searching older projects if the current project has
no match. Retain the fallback to the first notebook only when no notebookName
was requested.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 4d2cc5dc-e023-4402-b3fe-2fd19a2d504c

📥 Commits

Reviewing files that changed from the base of the PR and between 2f232dd and b5b2c08.

📒 Files selected for processing (5)
  • packages/cloud/src/cloud-runs.ts
  • packages/cloud/src/index.ts
  • packages/cloud/src/projects.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/cloud/src/index.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts

Comment thread packages/cloud/src/projects.ts

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/cloud/src/projects.ts (1)

63-76: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fallback to notebooks[0] still short-circuits the search for an exact notebookName match.

When query.notebookName is provided but absent from the current project, match ?? notebooks[0] falls back to that project's first notebook and returns immediately — it never checks older matching projects for the exact name. This is the same behavior flagged in a prior review (marked "Addressed"), but the current code still exhibits it.

🐛 Proposed fix
-  for (const project of projects) {
-    const notebooks = project.notebooks ?? []
-    const match = query.notebookName ? notebooks.find(notebook => notebook.name === query.notebookName) : undefined
-    const notebook = match ?? notebooks[0]
-    if (notebook) {
-      return { notebookId: notebook.id, projectId: project.id }
-    }
-  }
-  return undefined
+  if (query.notebookName) {
+    for (const project of projects) {
+      const match = (project.notebooks ?? []).find(notebook => notebook.name === query.notebookName)
+      if (match) {
+        return { notebookId: match.id, projectId: project.id }
+      }
+    }
+    return undefined
+  }
+  const first = projects[0]?.notebooks?.[0]
+  return first ? { notebookId: first.id, projectId: projects[0].id } : undefined
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cloud/src/projects.ts` around lines 63 - 76, Update the
project/notebook search loop so that when query.notebookName is provided,
projects without an exact notebook match are skipped rather than falling back to
notebooks[0]; return the matching notebook from any matching project, while
retaining the first-notebook fallback only when no notebook name is requested.
🧹 Nitpick comments (1)
examples/local-runner-demo/index.html (1)

163-168: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Avoid innerHTML for the view-URL link.

viewUrl is server-controlled today, but building via innerHTML template literal is an easy-to-copy anti-pattern. Building the anchor via DOM APIs sidesteps it entirely.

🔧 Proposed fix
-          if (data.viewUrl) {
-            const s = $('`#status`'); s.className = 'status ok'
-            s.innerHTML = `☁ cloud run ${data.status} — <a href="${data.viewUrl}" target="_blank" rel="noopener">view runs in Deepnote →</a>`
-          } else {
+          if (data.viewUrl) {
+            const s = $('`#status`'); s.className = 'status ok'
+            s.textContent = `☁ cloud run ${data.status} — `
+            const a = el('a'); a.href = data.viewUrl; a.target = '_blank'; a.rel = 'noopener'; a.textContent = 'view runs in Deepnote →'
+            s.appendChild(a)
+          } else {
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@examples/local-runner-demo/index.html` around lines 163 - 168, Replace the
innerHTML assignment in the data.viewUrl branch with DOM API construction: clear
the status element, create an anchor element, set its text, href, target, and
rel properties, then append the surrounding status text and anchor while
preserving the existing status class and displayed message.

Source: Linters/SAST tools

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

Inline comments:
In `@examples/local-runner-demo/index.html`:
- Around line 190-201: Update the text/html branch in renderOutput to sanitize
notebook HTML before assigning it to innerHTML, using an established sanitizer
if available and preserving rich HTML rendering. Ensure untrusted or shared
notebook output cannot execute scripts or otherwise inject unsafe content.

---

Duplicate comments:
In `@packages/cloud/src/projects.ts`:
- Around line 63-76: Update the project/notebook search loop so that when
query.notebookName is provided, projects without an exact notebook match are
skipped rather than falling back to notebooks[0]; return the matching notebook
from any matching project, while retaining the first-notebook fallback only when
no notebook name is requested.

---

Nitpick comments:
In `@examples/local-runner-demo/index.html`:
- Around line 163-168: Replace the innerHTML assignment in the data.viewUrl
branch with DOM API construction: clear the status element, create an anchor
element, set its text, href, target, and rel properties, then append the
surrounding status text and anchor while preserving the existing status class
and displayed message.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: d12ba3d8-86cc-491a-ac0a-04b7cc28e57c

📥 Commits

Reviewing files that changed from the base of the PR and between b5b2c08 and 0ab08e1.

📒 Files selected for processing (7)
  • examples/local-runner-demo/README.md
  • examples/local-runner-demo/index.html
  • examples/local-runner-demo/serve.mjs
  • packages/cloud/src/projects.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/serve-static.ts
✅ Files skipped from review due to trivial changes (1)
  • examples/local-runner-demo/README.md
🚧 Files skipped from review as they are similar to previous changes (3)
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/serve-static.ts

Comment thread examples/local-runner-demo/index.html Outdated
`deepnote run -i <slider>=N` silently dropped the execution snapshot: the
override value was written to `deepnote_variable_value` verbatim (a number),
but the block schema requires a string, so `serializeDeepnoteSnapshot` threw
`Expected string, received number` and the best-effort save swallowed it.

Add a type-aware `coerceInputVariableValue(block, value)` schema-normalization
helper to @deepnote/blocks (slider/text/textarea/date/file → string; checkbox
strict boolean; select shape-only respecting multi-value; date-range arity),
and apply it in the CLI's `applyInputOverrides`. The kernel-injection payload
passed to `runProject({ inputs })` intentionally keeps native user values.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jamesbhobbs
jamesbhobbs force-pushed the fix/input-coercion-snapshot branch from ee6e6a7 to ae895f2 Compare July 11, 2026 12:25
jamesbhobbs and others added 7 commits July 11, 2026 13:25
Adds a `--cloud` flag to the `run` command that triggers a cloud run of an
existing Deepnote notebook via the public API (`POST /v2/runs`), polls it to
completion (`GET /v2/runs/{runId}`), and downloads the resulting snapshot into
the local `snapshots/` convention — so `deepnote diff` and the MCP snapshot
tools work on it immediately.

The notebook to run is resolved from `--notebook-id`, else the local
`.deepnote` file's notebook (`--notebook <name>` or the single/main notebook).
The notebook must already exist in Deepnote; uploading local content
(`--push`) is a hidden, not-yet-implemented follow-up.

- New `utils/cloud-runs.ts`: Bearer-auth client mirroring `fetchIntegrations`
  with drift-tolerant polling (429/5xx backoff, per-request + total timeout,
  `RunTimeoutError` carrying the runId) and cross-origin-safe snapshot download
  (no bearer on presigned S3 URLs).
- New `commands/run-cloud.ts`: orchestration — notebook-id resolution, `.env`
  load, blank-token-as-missing, snapshot parse (snapshot-doc → full-file split
  → raw fallback), timestamped + latest writes, terminal-failure → exit 1 while
  preserving runId/status/snapshotPath, `-o json`/`-o toon` output.
- `utils/parse-inputs.ts`: extracted shared helper (avoids a run↔run-cloud cycle).
- `run.ts` / `cli.ts`: options, early dispatch, incompatible-flag guard, help.
- Docs: `skills/deepnote/references/cli-run.md` + `packages/cli/README.md`.

The runs API is in preview, so response schemas are intentionally permissive
and the exact snapshot field names / `detached` requirement should be confirmed
against a live token.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… origins

Cap per-request timeout, backoff, and interval sleeps to the remaining deadline so `timeoutMs` is a true total wait, and re-check the deadline before each request. Compare URL origins (not hosts) before attaching the bearer token to a snapshot download, so a same-host http:// URL is treated as cross-origin. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e saved

Downloading the snapshot is the command's contract, so a successful run whose snapshot is missing, unretrievable, or unwritable now exits 1 with success:false and an error (rather than best-effort success). A run that already failed keeps a missing snapshot non-fatal. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
These can hold secrets or PII and were emitted to stderr in debug mode; log only presence/count now, keeping token redaction. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Validate the whole string and require a safe positive integer so `1.5` and `10s` are rejected instead of silently truncated. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…re for cloud runs

Use the full `https://api.deepnote.com` default in the README, note that `-o llm` resolves to `toon`, and document that a successful run whose snapshot can't be saved exits 1. (CodeRabbit)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…/cloud package

Move the Deepnote Cloud runs client (trigger/poll/get/fetch-snapshot) out of
cli/utils/cloud-runs.ts into a new @deepnote/cloud package so it can be shared,
and fix snapshot fetching along the way: GET /v2/runs/{runId} returns the
snapshot on flat run.snapshotContent / run.snapshotDownloadUrl fields, not
nested under `snapshot`, so normalizeRun now reads them and `deepnote run
--cloud` actually retrieves outputs. run-cloud imports from @deepnote/cloud;
parseApiErrorMessage is inlined so the package has no CLI dependency.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
packages/cli/src/commands/run.ts (1)

583-599: 📐 Maintainability & Code Quality | 🟠 Major | ⚡ Quick win

Duplicate applyInputOverrides implementation.

This function is functionally identical to applyInputOverrides in packages/local-runner/src/apply-input-overrides.ts (same loop, same guard, same coerceInputVariableValue call, near-identical comments). Two independent copies of the same coercion loop will silently drift if either package extends this logic later (e.g. new guard conditions, error handling).

Consider exporting a single applyInputOverrides from @deepnote/blocks (which already hosts coerceInputVariableValue) and having both packages/cli/src/commands/run.ts and packages/local-runner/src/apply-input-overrides.ts import it, rather than maintaining two copies.

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

In `@packages/cli/src/commands/run.ts` around lines 583 - 599, Consolidate the
duplicate applyInputOverrides implementations by exporting a shared
applyInputOverrides from `@deepnote/blocks` alongside coerceInputVariableValue.
Replace the local loop in both run.ts and local-runner’s
apply-input-overrides.ts with imports of that shared function, preserving the
existing metadata override and coercion behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@examples/local-runner-demo/index.html`:
- Around line 163-168: Replace the template-literal assignment to s.innerHTML in
the data.viewUrl branch with DOM API construction: create the anchor element,
set its href and text content, apply target and rel attributes, and append it
alongside the status text. Preserve the existing status styling and displayed
cloud-run status without interpolating data.viewUrl or other dynamic values into
HTML.

---

Outside diff comments:
In `@packages/cli/src/commands/run.ts`:
- Around line 583-599: Consolidate the duplicate applyInputOverrides
implementations by exporting a shared applyInputOverrides from `@deepnote/blocks`
alongside coerceInputVariableValue. Replace the local loop in both run.ts and
local-runner’s apply-input-overrides.ts with imports of that shared function,
preserving the existing metadata override and coercion behavior.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: eadfe3fe-2486-4824-9300-be75e4e5905b

📥 Commits

Reviewing files that changed from the base of the PR and between 0ab08e1 and a86964c.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • examples/local-runner-demo/README.md
  • examples/local-runner-demo/index.html
  • examples/local-runner-demo/serve.mjs
  • packages/blocks/src/blocks/input-blocks.test.ts
  • packages/blocks/src/blocks/input-blocks.ts
  • packages/blocks/src/index.ts
  • packages/cli/package.json
  • packages/cli/src/commands/run-cloud.ts
  • packages/cli/src/commands/run.test.ts
  • packages/cli/src/commands/run.ts
  • packages/cloud/README.md
  • packages/cloud/package.json
  • packages/cloud/src/cloud-runs.test.ts
  • packages/cloud/src/cloud-runs.ts
  • packages/cloud/src/import.ts
  • packages/cloud/src/index.ts
  • packages/cloud/src/projects.ts
  • packages/cloud/tsdown.config.ts
  • packages/local-runner/README.md
  • packages/local-runner/package.json
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/local-runner/src/execution.integration.test.ts
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/load-file.ts
  • packages/local-runner/src/open-in-cloud.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/local-runner/src/serve-static.ts
  • packages/local-runner/tsdown.config.ts
✅ Files skipped from review due to trivial changes (7)
  • packages/blocks/src/index.ts
  • packages/cloud/package.json
  • packages/cloud/src/index.ts
  • packages/cloud/README.md
  • packages/cli/src/commands/run-cloud.ts
  • examples/local-runner-demo/README.md
  • packages/local-runner/README.md
🚧 Files skipped from review as they are similar to previous changes (21)
  • examples/local-runner-demo/serve.mjs
  • packages/local-runner/src/execution.integration.test.ts
  • packages/cloud/tsdown.config.ts
  • packages/cli/package.json
  • packages/local-runner/src/load-file.ts
  • packages/local-runner/package.json
  • packages/local-runner/src/index.ts
  • packages/local-runner/src/apply-input-overrides.test.ts
  • packages/local-runner/src/open-in-cloud.ts
  • packages/cloud/src/import.ts
  • packages/local-runner/tsdown.config.ts
  • packages/local-runner/src/run-with-inputs.test.ts
  • packages/local-runner/src/apply-input-overrides.ts
  • packages/cloud/src/cloud-runs.ts
  • packages/local-runner/src/serve-static.test.ts
  • packages/cloud/src/projects.ts
  • packages/local-runner/src/run-in-cloud.ts
  • packages/local-runner/src/run-with-inputs.ts
  • packages/local-runner/src/serve-static.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/cloud/src/cloud-runs.test.ts

Comment thread examples/local-runner-demo/index.html Outdated
@jamesbhobbs jamesbhobbs changed the title feat(local-runner): @deepnote/local-runner — run a .deepnote with edited inputs locally feat(local-runner): @deepnote/local-runner — run a .deepnote with edited inputs, locally or in Deepnote Cloud Jul 11, 2026
@jamesbhobbs
jamesbhobbs changed the base branch from fix/input-coercion-snapshot to feat/run-cloud July 11, 2026 12:35
Addresses CodeRabbit review on #417:
- A path-like `runId` from the runs API could escape ./snapshots when used
  in the fallback filename via resolve(); sanitize it (allow [A-Za-z0-9_-],
  cap length) and build the path with join(). Adds a regression test with a
  malicious `../` runId.
- Assert the cross-origin snapshot download omits the Authorization header
  (the presigned S3 URL is a different origin; the bearer must not leak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jamesbhobbs and others added 4 commits July 20, 2026 17:12
The /v1/import flow answers with `error`; the /v2 API answers with `message`.
Reading only the first meant every v2 failure reached the caller as the raw
JSON body — `{"message":"Notebook not found"}` where "Notebook not found" was
the whole point. Both keys are now read, `error` first so v1 is untouched.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… creating

A .deepnote SQL block keeps its connection in metadata.sql_integration_id.
Deepnote rejects that key outright — a 400, not a silent strip — and takes the
connection as a top-level integrationId, which it then writes into that very
key itself. We forwarded the metadata untouched and looked for a top-level
integrationId that the block schema has no field for, so the check was dead
code and every SQL block would have failed. Two of the shipped examples have
one. The value is now lifted out of the metadata; an id Deepnote cannot accept
(it must be a UUID, which the built-in dataframe connection is not) is dropped
with a warning, since an unbound block is the only shape the API will take.

The "not found" recovery matched /not found/i, which is three different
failures wearing one phrase. `Notebook not found` is the only one worth
recovering from; `Block not found in notebook` is a bad block id, and the
endpoint's only 404 is a bare `Not found` meaning the token's owner has left
the workspace. Both of those used to be answered by looking the notebook up and
creating a duplicate project — a typo, or an expired membership, and you get a
new project.

The rest is moving decisions ahead of the first request, since all of them are
facts about the local file: which notebook is the target, whether the requested
blocks are in it, and whether an input name is one the notebook defines.
Deepnote has no kernel injection, so an unmatched name is a 400 there — unlike
runWithInputs, where it is a variable and stays one.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Run history belongs to one notebook, but listCloudRuns quietly took the first
notebook of the file whenever there were several — answering with a real,
plausible history that simply belongs to something else. It now resolves the
notebook the same way runInCloud does, and refuses the ambiguity rather than
picking, so the two agree about which notebook a file means.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
lint-staged ran `sort -o .gitignore .gitignore` on every commit that touched
the file. Order is significant in a .gitignore — the last matching pattern
wins — so sorting hoisted `!packages/**/*.png` above `*.png` and quietly
un-did the exception. It also left every line duplicated.

Restoring the file alone would have lasted until the next commit that touched
it, so the rule goes too. There is no safe way to sort a file whose semantics
depend on order.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@jamesbhobbs

Copy link
Copy Markdown
Contributor Author

@dinohamzic thanks — this was a genuinely useful review. I verified each finding against deepnote-internal rather than taking them on faith, and all eleven were real. Several were worse or differently shaped than described, so the fixes differ from the suggestions in a few places. Seven commits, 6df77959f4..fd56699df1.

Where the verified behaviour changed the fix

Page through project lookupnextPageToken is nested under pagination, not top-level, and the query schema is a strictObject, so a limit param would 400 rather than be ignored. Now filters with nameContains (a case-insensitive substring match, so the exact-name filter still does the deciding) and reads every matching page. Malformed responses throw instead of reporting absence.

Reuse the seeded Notebook 1 — confirmed the seed is the literal string Notebook 1, the 409 is per-project and case-sensitive, and PATCH /v2/notebooks/{id} is a 501 stub, so adopting the placeholder really is the only option. Seeded notebooks come with no blocks, so adopting one is equivalent to having created it; unadopted ones are still deleted.

Translate SQL integration metadata — worse than described. It is a hard 400 from a superRefine (presence-based, so even sql_integration_id: null trips it), and our top-level block.integrationId check was dead code: the parsed block union has no such field, so the id was being dropped on every path. Two shipped examples carry one. One extra wrinkle: integrationId is constrained to z.uuid(), and the built-in deepnote-dataframe-sql sentinel is not a UUID — it is rejected as integrationId and rejected inside metadata, so it cannot be sent at all. Those blocks are now created unbound with an onWarning, since that is the only shape the API accepts.

Match only notebook-not-found — worse than described, in a way I'd flag. Notebook not found is a 400, not a 404, and the endpoint's only 404 is a bare Not found, which means the API key owner is no longer a workspace member. So the old statusCode === 404 branch answered an expired membership by creating a duplicate project. Two 500s (User not found, Public API coordinator user … was not found) matched the old regex too. Now matches /\bnotebook not found\b/i only, and the 404 branch is gone.

Preserve the completion timestampfinishedAt does not exist anywhere in the API surface; that name belongs to a field inside a snapshot document. Since our own list schema already read completedAt, the two endpoints disagreed about the same run. Renamed rather than aliased, as the field was never populated.

Two I implemented differently — flagging for your call

Reject undefined Cloud inputs — confirmed exactly (Input "x" is not defined for this notebook, 400, no generic injection path anywhere in v2). Applied to the cloud path only. Locally, runWithInputs deliberately forwards an unmatched name to the kernel for Python-literal injection — documented, and covered by run-with-inputs.test.ts:189 — so making both paths agree would have removed a real feature. TriggerRunBody.inputs is tightened to string | boolean | string[], which is what turned up the loose test fixture.

Remap notebook-function targets — real, but I did not build the remap, and I'd like your view. Deepnote's own project duplication doesn't remap function_notebook_id either (transformIds regenerates cell keys but _cloneDeeps the body), so a full remap would make us more correct than the product. A reference can only point inside the file in a multi-notebook file, which we're not supporting on the create path. So it now fails fast, before anything is created, with a message pointing at "create the project in Deepnote first". References pointing out of the file are left alone — those correctly name an existing cloud notebook. Happy to build the real remap if you'd rather.

Duplicate names — the local half of that finding

Cloud enforces per-project name uniqueness; a .deepnote file does not. So beyond adopting the seed, createProject now refuses duplicate or empty notebook names before the first request, rather than 409-ing partway through and stranding a half-built project. Same reasoning applied to the other pre-flight facts: target notebook, requested block ids (membership + uniqueness), and input names are all checked against the local file before any network call.

Two root causes the review reached the symptom of

.gitignore — the duplication and the hoisted !packages/**/*.png were both caused by "sort -o .gitignore .gitignore" in lint-staged (package.json:40). Order is significant in a gitignore — last match wins — so sorting silently un-does any negation. Restoring the file alone would have lasted until the next commit that touched it, so the rule is removed too.

Error messagesparseApiErrorMessage only read json.error, but v2 answers with message. Every v2 failure was reaching callers as a raw JSON blob ({"message":"Notebook not found"} where the message was the point). Now reads both, error first so the v1 import flow is untouched. This one is pre-existing on main, but it's what the not-found matcher above was regexing against, so it seemed worth doing here.

Full suite green (5127 passing), typecheck and spell-check clean. Ready for another look.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 1

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

Inline comments:
In `@packages/cloud/src/projects.ts`:
- Around line 74-88: Update the response parsing in the project-listing flow
around projectsSchema.safeParse so await response.json() is wrapped in error
handling that converts malformed or non-JSON bodies into ApiError with status
502. Preserve the existing schema-validation ApiError behavior for valid JSON
that does not match projectsSchema, and keep successful parsing unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

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

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 79696c28-cc87-4a19-9d5a-057e8a69a15f

📥 Commits

Reviewing files that changed from the base of the PR and between 4b150d2 and fd56699.

📒 Files selected for processing (16)
  • .gitignore
  • package.json
  • packages/cli/src/utils/deepnote-api.test.ts
  • packages/cli/src/utils/run-in-cloud.ts
  • packages/cloud/src/cloud-runs.test.ts
  • packages/cloud/src/cloud-runs.ts
  • packages/cloud/src/create-project.test.ts
  • packages/cloud/src/create-project.ts
  • packages/cloud/src/index.ts
  • packages/cloud/src/parse-api-error.ts
  • packages/cloud/src/projects.test.ts
  • packages/cloud/src/projects.ts
  • packages/local-runner/src/cloud-runs.test.ts
  • packages/local-runner/src/cloud-runs.ts
  • packages/local-runner/src/run-in-cloud.test.ts
  • packages/local-runner/src/run-in-cloud.ts
💤 Files with no reviewable changes (1)
  • package.json
🚧 Files skipped from review as they are similar to previous changes (5)
  • packages/cloud/src/create-project.ts
  • packages/cloud/src/projects.test.ts
  • packages/local-runner/src/cloud-runs.ts
  • packages/cloud/src/index.ts
  • packages/local-runner/src/cloud-runs.test.ts

Comment thread packages/cloud/src/projects.ts
@jamesbhobbs
jamesbhobbs requested a review from dinohamzic July 20, 2026 16:25
…Error

findNotebook now promises to throw rather than report absence when it cannot
read a response, but `await response.json()` was unguarded — so a non-JSON body
(an HTML error page from a proxy, say) escaped as a raw SyntaxError, which is
neither that contract nor the ApiError callers of this package catch.

`create-project.ts` already guards exactly this, so the guard is shared here and
applied to getWorkspace too rather than leaving one of the two unguarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 20, 2026

@dinohamzic dinohamzic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@jamesbhobbs thanks, second round, not much left:

  1. [P1] Block-targeted cloud runs always fail. Deepnote defaults runs to detached: true, but rejects blockIds in detached mode. Both direct and newly-created targeted runs send blockIds without detached: false, producing HTTP 400. Empty blockIds arrays should also be omitted because the API requires a non-empty array.

  2. [P2] Valid notebook-function projects cannot use create-if-missing. Rejecting internal function_notebook_id references prevents stale IDs, but it also rejects valid multi-notebook files despite the API promising to create missing projects. Create all notebooks first, build an old-to-new notebook ID map, then rewrite these references while creating blocks.

  3. [P2] Project lookup can still falsely report absence. The cloud contract always returns pagination, but the client makes it optional. It also silently stops after 20 pages even when another page token exists. Either case can return undefined before completing the lookup, causing createIfMissing to create a duplicate project. Require pagination and throw if the safety limit is reached with a remaining token.

jamesbhobbs and others added 4 commits July 23, 2026 14:00
…s them

`POST /v2/runs` defaults every run to `detached: true` and rejects `blockIds`
on one — `blockIds is not supported for detached runs`, a 400. So every
targeted run failed: the direct one, and the one started right after creating
the notebook. Deepnote only runs selected blocks in live mode, so a body
carrying blockIds now says `detached: false` for itself.

Done in `triggerNotebookRun` rather than at each call site, so the rule lives
once next to the endpoint that has it. An empty `blockIds` is dropped there
too — the API takes a non-empty list, and "no blocks in particular" is what
omitting it means.
…her than refuse it

A notebook-function block names the notebook it invokes in
`function_notebook_id`, and creating the file gives that notebook a new id the
reference would not follow — so `createFromFile` refused any file holding one.
That kept stale ids out, but it also turned down valid multi-notebook files
that create-if-missing promises to handle, and told the user to go and create
the project by hand.

The id is knowable, just not while notebooks and blocks are created in
lockstep: `createProject` now creates every notebook first, builds
`sourceId` → created id, and offers it to a `rewriteBlock` hook as each block
goes out. The client stays domain-agnostic — it echoes ids it never reads —
and `local-runner` uses that to re-point references into the file at the
notebooks Deepnote just made. A reference out of the file already names a real
Deepnote notebook, so it is left alone.
…e early

`findNotebook` returning `undefined` is what sends `createIfMissing` off to
create a project, so it has to mean "looked everywhere, not there". Two things
could make it mean less than that: `pagination` was optional in the client
though the endpoint always sends it, so a body without one read exactly like a
last page; and the page walk stopped at `MAX_PROJECT_PAGES` even with a token
still in hand, quietly answering from a lookup that had not finished.

Pagination is now required, and a walk that runs out of pages with more to read
throws. Both cases were a duplicate project waiting to happen.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 23, 2026
…keys

A deck that only moves on arrow keys is a deck nobody can present from a
trackpad, and clicking the slide is what every projector deck does. The
listener sits on `.stage` rather than the document, so the chrome needs no
exceptions: the gallery link and the dots are fixed siblings, not descendants,
and their clicks never reach it.

Forward only. Back stays on the arrow keys and the dots, which already reach
any slide — a left-half-goes-back zone is invention, not a fix. A click that
ends a text selection is skipped: the last slide is the agent readout as
selectable prose, and finishing a copy-drag should not jump off what you just
selected.
coderabbitai[bot]
coderabbitai Bot previously approved these changes Jul 23, 2026
@jamesbhobbs
jamesbhobbs requested a review from dinohamzic July 23, 2026 14:32

@dinohamzic dinohamzic left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🚀

@dinohamzic
dinohamzic merged commit 17a4c53 into main Jul 24, 2026
19 of 21 checks passed
@dinohamzic
dinohamzic deleted the feat/local-runner branch July 24, 2026 08:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants