Skip to content

fix(cron): isolate fresh cron session state#71340

Merged
vincentkoc merged 6 commits into
mainfrom
fix/cron-isolated-state-runtime
Apr 25, 2026
Merged

fix(cron): isolate fresh cron session state#71340
vincentkoc merged 6 commits into
mainfrom
fix/cron-isolated-state-runtime

Conversation

@vincentkoc

Copy link
Copy Markdown
Member

Summary

  • clear stale runtime/session artifacts when an isolated cron run creates a fresh session id
  • keep user-selected model/auth overrides, but drop auto/runtime leftovers from prior turns
  • persist per-run :run:<sessionId> rows as snapshots instead of aliasing the mutable base cron session object
  • add regression coverage for stale field cleanup and per-run snapshot isolation

Why

Fresh isolated cron runs were seeded from the previous session row, so old lifecycle fields, runtime model facts, auth/model switch state, CLI session ids, exec overrides, heartbeat cache, delivery metadata, usage totals, and fallback notices could leak into the next run. The base cron key and per-run key also pointed at the same object, so later base-session mutations could contaminate run rows.

Fixes #63211.
Fixes #68405.
Fixes #67598.
Fixes #58485.
Fixes #58285.
Fixes #57968.
Fixes #58575.
Fixes #61879.
Fixes #59257.
Fixes #47478.
Fixes #57947.
Fixes #58533.
Fixes #58511.
Fixes #61294.
Fixes #57112.
Fixes #58459.
Fixes #58517.
Fixes #57501.
Fixes #63617.
Fixes #62580.

Validation

  • pnpm test src/cron/isolated-agent/session.test.ts src/cron/isolated-agent/run-session-state.test.ts src/cron/isolated-agent/run.cron-model-override.test.ts src/cron/isolated-agent/run.cron-model-override-forwarding.test.ts src/cron/isolated-agent/run.message-tool-policy.test.ts
  • pnpm format:check src/cron/isolated-agent/session.ts src/cron/isolated-agent/session.test.ts src/cron/isolated-agent/run-session-state.ts src/cron/isolated-agent/run-session-state.test.ts CHANGELOG.md
  • pnpm check:changed

@vincentkoc vincentkoc self-assigned this Apr 25, 2026
@openclaw-barnacle openclaw-barnacle Bot added size: M maintainer Maintainer-authored PR labels Apr 25, 2026
@vincentkoc vincentkoc marked this pull request as ready for review April 25, 2026 01:40
@greptile-apps

greptile-apps Bot commented Apr 25, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes stale session state leaking into fresh isolated cron runs by introducing clearFreshCronSessionState (which strips all runtime/lifecycle/auth fields on rollover) and by snapshotting the per-run session entry as a distinct object instead of aliasing the mutable base session. User-selected model and auth overrides are correctly preserved; auto/runtime-sourced overrides are cleared.

Confidence Score: 4/5

Safe to merge; changes are well-tested and the core logic is correct with only minor style concerns.

Only P2 findings: shallow clone in cloneSessionEntry leaves nested objects shared, and the opt-out delete list in clearFreshCronSessionState will silently miss new SessionEntry fields. Neither is a current defect given how session entries are mutated in practice.

session.ts opt-out delete list needs manual updates whenever new runtime fields are added to SessionEntry.

Prompt To Fix All With AI
This is a comment left during a code review.
Path: src/cron/isolated-agent/run-session-state.ts
Line: 22-24

Comment:
**Shallow clone only protects top-level properties**

`cloneSessionEntry` uses a one-level spread, so nested objects (`skillsSnapshot`, any remaining `cliSessionIds`/`cliSessionBindings` if they survive to persist time, etc.) are shared references between the base session and the run snapshot. A direct mutation like `cronSession.sessionEntry.skillsSnapshot.someField = …` would silently contaminate the snapshot. The current test only exercises a primitive top-level field (`status`). A structuredClone (or deep-copy helper) would give complete isolation at modest cost.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: src/cron/isolated-agent/session.ts
Line: 12-68

Comment:
**Opt-out field list is fragile as `SessionEntry` grows**

`clearFreshCronSessionState` enumerates every runtime field to delete individually. Any new field added to `SessionEntry` that should be cleared on rollover must also be added here manually; there's no compiler or lint guard preventing a silent omission. A more robust pattern would be to define an explicit `PRESERVED_SESSION_FIELDS` allowlist (the few user-preference fields to keep) and reconstruct the entry from only those fields, so new runtime fields are cleared by default rather than by opt-in.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "fix(cron): isolate fresh cron session st..." | Re-trigger Greptile

Comment thread src/cron/isolated-agent/run-session-state.ts
Comment thread src/cron/isolated-agent/session.ts Outdated
@aisle-research-bot

aisle-research-bot Bot commented Apr 25, 2026

Copy link
Copy Markdown

🔒 Aisle Security Analysis

We found 2 potential security issue(s) in this PR:

# Severity Title
1 🟠 High SSRF and prompt exfiltration via unvalidated Anthropic Vertex baseUrl passed to SDK client
2 🟡 Medium Potential DoS: structuredClone of SessionEntry can throw due to non-cloneable Skill objects in skillsSnapshot.resolvedSkills
1. 🟠 SSRF and prompt exfiltration via unvalidated Anthropic Vertex `baseUrl` passed to SDK client
Property Value
Severity High
CWE CWE-918
Location extensions/anthropic-vertex/stream-runtime.ts:180-215

Description

createAnthropicVertexStreamFnForModel derives a baseURL from model.baseUrl and passes it directly into the Anthropic Vertex SDK constructor.

  • Input: model.baseUrl (comes from model configuration; ModelDefinitionConfig supports baseUrl?: string)
  • Transformation: resolveAnthropicVertexSdkBaseUrl() accepts arbitrary strings; on parse failure it returns the raw trimmed value
  • Sink: new deps.AnthropicVertex({ ..., baseURL }) causes the SDK to send the full request payload (prompts/messages/metadata) to that URL

If an attacker can influence model.baseUrl (e.g., per-tenant/provider config, plugin-supplied models, or any user-editable model registry), they can:

  • Perform SSRF to internal services by pointing baseUrl at internal hosts/IPs
  • Exfiltrate sensitive prompts/attachments/metadata to an attacker-controlled endpoint
  • Potentially leverage any credentials/headers the SDK attaches for Vertex auth in outbound requests

Vulnerable code:

const client = new deps.AnthropicVertex({
  region,
  ...(baseURL ? { baseURL } : {}),
  ...(projectId ? { projectId } : {}),
});
...
resolveAnthropicVertexSdkBaseUrl(model.baseUrl)

Recommendation

Treat model.baseUrl as untrusted and enforce an SSRF-safe policy before passing it to the SDK.

Minimum hardening:

  • Require https: (and optionally http: only for localhost/dev builds)
  • Block localhost/private IP ranges and special hostnames (e.g., metadata.google.internal)
  • Prefer an allowlist of expected Vertex hosts (e.g., *.aiplatform.googleapis.com) and explicitly allow only known proxy domains

Example (conceptual):

import { isBlockedHostnameOrIp } from "../../src/infra/net/ssrf.js";

function validateVertexBaseUrl(raw?: string): string | undefined {
  if (!raw?.trim()) return undefined;
  const url = new URL(raw);
  if (url.protocol !== "https:") throw new Error("Vertex baseUrl must be https");
  if (isBlockedHostnameOrIp(url.hostname)) throw new Error("Blocked baseUrl host");// optional allowlist
  if (!url.hostname.endsWith("aiplatform.googleapis.com") && url.hostname !== "proxy.example.com") {
    throw new Error("Unapproved baseUrl host");
  }
  return url.toString().replace(/\/+$/, "");
}

const baseURL = validateVertexBaseUrl(model.baseUrl);
const client = new AnthropicVertex({ region, ...(baseURL ? { baseURL } : {}) });

If possible, route SDK HTTP through the project’s existing fetchWithSsrFGuard/pinned-dispatcher infrastructure so all outbound requests share the same SSRF protections.

2. 🟡 Potential DoS: structuredClone of SessionEntry can throw due to non-cloneable Skill objects in skillsSnapshot.resolvedSkills
Property Value
Severity Medium
CWE CWE-400
Location src/cron/isolated-agent/run-session-state.ts:22-38

Description

createPersistCronSessionEntry now clones the entire in-memory SessionEntry via globalThis.structuredClone() to create an isolated per-run snapshot. This can crash the cron process with a DataCloneError if the session entry contains non-structured-cloneable values.

Why this is plausible in this codebase:

  • SessionEntry.skillsSnapshot is typed to include resolvedSkills?: Skill[].
  • Skill objects (and related runtime contexts) commonly include functions/methods and other non-cloneable properties.
  • persistCronSkillsSnapshotIfChanged writes skillsSnapshot into cronSession.sessionEntry, so the persisted entry can include resolvedSkills.
  • When createPersistCronSessionEntry runs, it clones the whole entry before persisting; any non-cloneable value will throw and can prevent the session store from being updated, potentially causing a persistent crash loop for cron runs.

Vulnerable code:

function cloneSessionEntry(entry: MutableCronSessionEntry): MutableCronSessionEntry {
  return globalThis.structuredClone(entry);
}
...
const runSessionEntry = cloneSessionEntry(params.cronSession.sessionEntry);

Recommendation

Avoid cloning the full in-memory SessionEntry with structuredClone when it may contain runtime-only, non-serializable/non-cloneable fields (notably skillsSnapshot.resolvedSkills). Instead, persist a serialization-safe snapshot.

Options:

  1. Strip runtime-only fields before cloning/persisting:
function toPersistableSessionEntry(entry: SessionEntry): SessionEntry {
  const next = { ...entry };
  if (next.skillsSnapshot?.resolvedSkills) {
    next.skillsSnapshot = { ...next.skillsSnapshot, resolvedSkills: undefined };
  }
  return next;
}

const runSessionEntry = toPersistableSessionEntry(params.cronSession.sessionEntry);
  1. Or, if the store is JSON-based, clone via JSON round-trip (and handle errors):
const runSessionEntry = JSON.parse(JSON.stringify(params.cronSession.sessionEntry)) as SessionEntry;

Additionally, consider wrapping cloning in try/catch and falling back to a safer snapshot to prevent cron crash loops.


Analyzed PR: #71340 at commit c6e5cd9

Last updated on: 2026-04-25T05:06:52Z

@steipete steipete force-pushed the fix/cron-isolated-state-runtime branch from 406eba8 to 7c3f239 Compare April 25, 2026 04:49
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment