Repository navigation
feat(core): add checkpoint namespaces - #135
kaihao-zhao wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
2 issues found.
About Unblocked
Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.
📖 Documentation — Learn more in our docs.
💬 Ask questions — Mention @unblocked-local-kaihao to request a review or summary, or ask follow-up questions.
👍 Give feedback — React to comments with 👍 or 👎 to help us improve.
⚙️ Customize — Adjust settings in your preferences.
Builder copy constructor does not copy checkpointNamespace
The Builder(RunnableConfig config) copy constructor copies threadId, checkPointId, nextNode, and streamMode but omits checkpointNamespace. This means every with* method (e.g. withCheckPointId, withStreamMode, withCheckpointNamespace, updateMetadata) that internally calls RunnableConfig.builder(this) will silently drop the checkpoint namespace from the resulting config.
Add the missing line:
this.checkpointNamespace = config.checkpointNamespace;| * @return a configuration using the requested namespace | ||
| */ | ||
| public RunnableConfig withCheckpointNamespace(String checkpointNamespace) { | ||
| if (Objects.equals(this.threadId, checkpointNamespace)) { |
There was a problem hiding this comment.
The short-circuit check compares this.threadId with checkpointNamespace instead of this.checkpointNamespace with checkpointNamespace. This means the method will incorrectly return this (skipping the update) when the thread ID happens to equal the requested namespace, and will always create a new object even when the namespace hasn't changed.
Compare with the sibling method withCheckPointId which correctly uses Objects.equals(this.checkPointId, checkPointId).
| if (Objects.equals(this.threadId, checkpointNamespace)) { | |
| if (Objects.equals(this.checkpointNamespace, checkpointNamespace)) { |
There was a problem hiding this comment.
1 issue found.
About Unblocked
Unblocked has been set up to automatically review your team's pull requests to identify genuine bugs and issues.
📖 Documentation — Learn more in our docs.
💬 Ask questions — Mention @unblocked-local-kaihao to request a review or summary, or ask follow-up questions.
👍 Give feedback — React to comments with 👍 or 👎 to help us improve.
⚙️ Customize — Adjust settings in your preferences.
| private Path getNamespaceFolder(RunnableConfig config) { | ||
| return config.checkpointNamespace() | ||
| .map(targetFolder::resolve) | ||
| .orElse(targetFolder); | ||
| } |
There was a problem hiding this comment.
getNamespaceFolder passes the user-supplied checkpointNamespace directly to targetFolder::resolve with no validation. A malicious or accidental namespace value such as ../../etc or an absolute path like /tmp will resolve outside the intended targetFolder, allowing reads/writes to arbitrary locations on the filesystem.
The documentation (core-library.md) states "Namespace values are trimmed and must contain only letters, digits, hyphens, or underscores", but no such validation is actually enforced anywhere in the code.
Add validation either in BaseCheckpointSaver.checkpointNamespace(), in Builder.checkpointNamespace(), or at minimum in getNamespaceFolder itself. For example:
private static final Pattern SAFE_NAMESPACE = Pattern.compile("[A-Za-z0-9_-]+");
private Path getNamespaceFolder(RunnableConfig config) {
return config.checkpointNamespace()
.map(ns -> {
if (!SAFE_NAMESPACE.matcher(ns).matches()) {
throw new IllegalArgumentException(
"checkpointNamespace must contain only letters, digits, hyphens, or underscores: " + ns);
}
return targetFolder.resolve(ns);
})
.orElse(targetFolder);
}
Adds checkpoint namespace support to RunnableConfig and applies namespace isolation to the built-in memory and filesystem checkpoint savers. Includes API documentation, backward-compatible default behavior, and focused tests for tenant isolation. Test command: ./mvnw -pl langgraph4j-core test.