Skip to content

Python: [Bug]: SecretString cannot be pickled, so credential-bearing workflow checkpoints cannot be saved or restored #9514

Open

Description

@Rishidar-lab

Observed Behavior

Since #8127 made SecretString an immutable, slots-based, non-str wrapper, it can no longer be
pickled — and the workflow checkpoint codec pickles every non-JSON-native value. So a workflow whose
executor/session state holds a SecretString cannot be durably checkpointed:

Mechanism: SecretString has __slots__ = ("_value",) and a mutation-rejecting __setattr__
(raise AttributeError("SecretString is immutable.")) but no __getstate__/__setstate__/
__reduce__. Pickle protocols 0–1 fail at dumps; protocols ≥2 succeed at dumps but fail at
loads, because the BUILD step calls setattr(obj, "_value", …) and hits the immutability guard.

The obvious workaround — unwrap the credential to a plain str so the save succeeds — writes the raw
secret into the checkpoint in cleartext (file mode 0o666 & ~umask).

Expected Behavior

A supported, documented outcome for a credential in durable workflow state — one of:
(a) SecretString round-trips through checkpoints (if persisting secrets is intended, accepting that
the secret is written to storage); (b) the checkpoint path rejects it early with a precise error,
as the session-store path already does (_validate_durable_state_value raises a clear TypeError
for a SecretString in durable session state); or (c) secrets are excluded from persisted state and
re-injected from configuration on restore. The current behavior (encode succeeds, then an opaque
pickle BUILD failure — or, on Cosmos, failure only at restore) is none of these.

Steps to Reproduce

  1. Check out current main (ebcf4db7f9462ef6c377761e0b3159c4c5a73027).
  2. Put a SecretString in checkpointed executor/session state and call FileCheckpointStorage.save().
  3. The save is refused. (Replacing it with a plain str lets the save succeed and writes the secret
    to the checkpoint file in cleartext.)

Minimal Reproduction

# (1) Core defect — bare pickle, no framework:
import pickle
from agent_framework import SecretString
pickle.loads(pickle.dumps(SecretString("sk-example-123")))
# -> AttributeError: SecretString is immutable.

# (2) Framework impact — FileCheckpointStorage.save() is refused:
import asyncio, tempfile
from agent_framework import FileCheckpointStorage, WorkflowCheckpoint, SecretString
from agent_framework.exceptions import WorkflowCheckpointException

async def main():
    with tempfile.TemporaryDirectory() as d:
        storage = FileCheckpointStorage(d)
        cp = WorkflowCheckpoint(
            workflow_name="wf", graph_signature_hash="h",
            state={"_executor_state": {"a": {"api_key": SecretString("sk-example-123")}}},
        )
        try:
            await storage.save(cp); print("saved (unexpected)")
        except WorkflowCheckpointException as e:
            print(f"save refused: {e}")

asyncio.run(main())
# -> save refused: Failed to decode pickled checkpoint data: SecretString is immutable.

End-to-end (a real WorkflowBuilder graph): an Executor returning {"api_key": SecretString(...)}
from on_checkpoint_save() logs Failed to create checkpoint at iteration N: Failed to decode pickled checkpoint data: SecretString is immutable. at every superstep and persists zero
checkpoints; the run itself does not abort (the failure is logged, not raised — _runner.py).

Error Messages and Stack Traces

agent_framework.exceptions.WorkflowCheckpointException: Failed to decode pickled checkpoint data: SecretString is immutable.

Raised from _workflows/_checkpoint_encoding.py _base64_to_unpickle (underlying
AttributeError: SecretString is immutable. from SecretString.__setattr__ during pickle BUILD).
On FileCheckpointStorage it surfaces from save() during the #8214 save-time restore check, before
any file is written.

Package Versions

agent-framework-core 1.22.0 at commit ebcf4db7f9462ef6c377761e0b3159c4c5a73027 (current main).

Python Version

Reproduced on CPython 3.10.22, 3.12.3, and 3.13.13 (identical; only the protocol-0/1
dumps error-string wording varies by version). Not tested on 3.11/3.14/3.15; the mechanism is
Python-version-independent.

Operating System

Linux (x86_64).

Regression

Yes — introduced by #8127 (merged 2026-09-08), which changed class SecretString(str) to a
slots-based immutable wrapper. As a str subclass the prior SecretString pickled as a string, so
durable checkpointing of state containing it regressed. (As a str subclass it was also JSON-native
and previously written into checkpoints verbatim — a separate cleartext concern that predates this.)

Additional Context

Acknowledgements

  • I searched existing issues and did not find a duplicate.
  • I personally verified this behavior and the reproduction details are authentic.
  • I will wait for explicit maintainer agreement before starting implementation of a non-trivial change.

Activity

  1. added
    pythonUsage: [Issues, PRs], Target: Python
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  2. added
    reproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow
    on Oct 18, 2026
  3. github-actions commented on Oct 18, 2026

    @github-actions
    Contributor

    🤖 Automated triage reproduction notes (agent-authored — trust but verify)

    Agent analysis

    The defect is in python/packages/core/agent_framework/_settings.py::SecretString, whose mutation-rejecting __setattr__ at lines 90-92 prevents pickle from restoring the _value slot. It is triggered when FileCheckpointStorage.save() validates a checkpoint containing a SecretString anywhere in state. Minimal repro: construct a WorkflowCheckpoint with state={"api_key": SecretString("secret")} and await FileCheckpointStorage.save().

    • Failing test: python/packages/core/tests/workflow/test_checkpoint.py::test_file_checkpoint_storage_save_and_load_secret_string
    • Files examined: python/packages/core/agent_framework/_settings.py, python/packages/core/agent_framework/_workflows/_checkpoint_encoding.py, python/packages/core/agent_framework/_workflows/_checkpoint.py, python/packages/azure-cosmos/agent_framework_azure_cosmos/_checkpoint_storage.py, python/packages/core/tests/workflow/test_checkpoint.py, python/packages/core/pyproject.toml
    • Tests run: test_file_checkpoint_storage_save_and_load, test_file_checkpoint_storage_save_and_load_secret_string
    • Reported version: 1.22.0
    • Current version: 1.22.0
  4. added
    agentsUsage: [Issues, PRs], Target: Single agent
    and removed
    triageUsage: [Issues], Target: All issues that still need to be triaged
    on Oct 18, 2026
  5. eavanvalkenburg commented on Oct 26, 2026

    @eavanvalkenburg
    Member

    /plan Design-only investigation; maintainers are still considering the desired behavior and have not agreed to implementation or secret persistence. Compare explicit rejection with an actionable error, exclusion plus configuration-based rehydration, and deliberate round-trip support with its recoverable-secret-at-rest implications. Establish existing checkpoint/session contracts and the broad user need, core-versus-application responsibilities, compatibility costs, and recommended policy before selecting an implementation. Inspect nested dictionaries/lists/custom objects and whole-object pickle fallback, including the unresolved bypass in closed PR #9515; a top-level SecretString guard is insufficient. Coordinate File, Cosmos and Foundry-hosting save/load behavior with #8874/#8875, preserving immutability and avoiding secret values in errors or logs. Identify focused tests, migration behavior, and documentation/sample guidance. Keep current 1.x handling separate from #8522 v2 design; do not assume that proposal settles credential policy. Do not start implementation or reopen #9515 without subsequent maintainer agreement.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Labels

agentsUsage: [Issues, PRs], Target: Single agentpythonUsage: [Issues, PRs], Target: PythonreproducedUsage: [Issues], Target: all issues that can be reproduced by the triage workflow

Type

Projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions