Branch: feat/managed-identity-store
Depends on: current main
Priority: P2 — plaintext signing keys block the regulated ICP.
Prompt:
Agent signing identities are persisted by FileIdentityStore, which writes raw ed25519 secret keys hex-encoded in plaintext JSON under data/identities/<agent_id>.json. The module rustdoc is explicit that this is "appropriate for development and demo use only" and that production should use an OS keychain, HSM, or KMS — but no such implementation exists, and the Python attest() entry point silently auto-creates plaintext keys with no opt-out. The regulated mid-market buyers named as the primary ICP cannot accept plaintext signing keys on disk. This task ships at least one non-plaintext IdentityStore implementation behind the existing trait seam.
Context
crates/awp-core/src/identity.rs:148 — the IdentityStore trait (the intended production seam). identity.rs:176 — FileIdentityStore (the only impl), impl IdentityStore for FileIdentityStore at identity.rs:200. Storage format and dev-only warnings are documented at identity.rs:18-69; a commented-out KmsIdentityStore sketch sits at identity.rs:60.
AgentIdentity::load_or_create (identity.rs:104) takes a store and an agent id.
crates/awp-python/src/lib.rs:100-106 — the Python binding's load_or_create delegates to FileIdentityStore. python/awp-langgraph/awp/langgraph/wrapper.py calls this with no way to select a managed store.
- The
IdentityStore trait is the abstraction to implement against; do not change its shape.
Your Task
- Choose and implement one managed backend. Preferred first target is an OS-keychain store (portable, no cloud dependency) or an encrypted-at-rest file store using a KEK from env/KMS. Add
crates/awp-core/src/identity/<backend>.rs (or a submodule) implementing IdentityStore:
- Option A (recommended first):
EncryptedFileIdentityStore — same on-disk layout as FileIdentityStore but the secret key is sealed with an AEAD (e.g. chacha20poly1305) under a key-encryption-key read from AWP_IDENTITY_KEK (or fetched from a KMS). The secret is never written in cleartext.
- Option B:
KeyringIdentityStore — stores the secret in the OS keychain via the keyring crate.
Pick one; ask before adding the crypto/keyring dependency if it isn't already vendored, per the repo's dep policy.
- Expose it through the binding —
crates/awp-python/src/lib.rs: add a way for Python callers to select the managed store (e.g. an enum/string parameter on the identity constructor, or a distinct AgentIdentity.load_or_create_encrypted(...)).
- Give the SDK an opt-in —
python/awp-langgraph/awp/langgraph/wrapper.py: let attest(...) accept an identity-store selector so a caller can require the managed store instead of silently getting plaintext-on-disk. Keep FileIdentityStore as the default for local dev, but make the managed path a first-class option and document it.
- Document the security posture — update the rustdoc in
identity.rs so the recommended production path points to the new impl rather than only warning against FileIdentityStore.
Do Not Touch
- The
IdentityStore trait signature (identity.rs:148) — implement against it.
signing.rs — key generation and signing are correct; this task is about at-rest storage only.
FileIdentityStore — keep it as the documented dev/demo default; do not remove it.
Verification
make check
# → lint and tests pass
# Rust round-trip test (add to crates/awp-core/src/identity.rs tests):
cargo test -p awp-core identity::encrypted
# → generate → seal → reload yields the same agent_id/public_key; the
# on-disk bytes do NOT contain the raw secret key hex
# Prove the secret is not cleartext on disk:
cargo test -p awp-core identity::encrypted_at_rest_is_sealed
# → asserts the persisted file does not contain the known secret bytes
# Python selection path:
make check-python
# → a pytest asserts attest(..., identity_store="encrypted") signs and
# verifies without ever writing a plaintext key file
Branch:
feat/managed-identity-storeDepends on: current main
Priority: P2 — plaintext signing keys block the regulated ICP.
Prompt:
Agent signing identities are persisted by
FileIdentityStore, which writes raw ed25519 secret keys hex-encoded in plaintext JSON underdata/identities/<agent_id>.json. The module rustdoc is explicit that this is "appropriate for development and demo use only" and that production should use an OS keychain, HSM, or KMS — but no such implementation exists, and the Pythonattest()entry point silently auto-creates plaintext keys with no opt-out. The regulated mid-market buyers named as the primary ICP cannot accept plaintext signing keys on disk. This task ships at least one non-plaintextIdentityStoreimplementation behind the existing trait seam.Context
crates/awp-core/src/identity.rs:148— theIdentityStoretrait (the intended production seam).identity.rs:176—FileIdentityStore(the only impl),impl IdentityStore for FileIdentityStoreatidentity.rs:200. Storage format and dev-only warnings are documented atidentity.rs:18-69; a commented-outKmsIdentityStoresketch sits atidentity.rs:60.AgentIdentity::load_or_create(identity.rs:104) takes a store and an agent id.crates/awp-python/src/lib.rs:100-106— the Python binding'sload_or_createdelegates toFileIdentityStore.python/awp-langgraph/awp/langgraph/wrapper.pycalls this with no way to select a managed store.IdentityStoretrait is the abstraction to implement against; do not change its shape.Your Task
crates/awp-core/src/identity/<backend>.rs(or a submodule) implementingIdentityStore:EncryptedFileIdentityStore— same on-disk layout asFileIdentityStorebut the secret key is sealed with an AEAD (e.g.chacha20poly1305) under a key-encryption-key read fromAWP_IDENTITY_KEK(or fetched from a KMS). The secret is never written in cleartext.KeyringIdentityStore— stores the secret in the OS keychain via thekeyringcrate.Pick one; ask before adding the crypto/keyring dependency if it isn't already vendored, per the repo's dep policy.
crates/awp-python/src/lib.rs: add a way for Python callers to select the managed store (e.g. an enum/string parameter on the identity constructor, or a distinctAgentIdentity.load_or_create_encrypted(...)).python/awp-langgraph/awp/langgraph/wrapper.py: letattest(...)accept an identity-store selector so a caller can require the managed store instead of silently getting plaintext-on-disk. KeepFileIdentityStoreas the default for local dev, but make the managed path a first-class option and document it.identity.rsso the recommended production path points to the new impl rather than only warning againstFileIdentityStore.Do Not Touch
IdentityStoretrait signature (identity.rs:148) — implement against it.signing.rs— key generation and signing are correct; this task is about at-rest storage only.FileIdentityStore— keep it as the documented dev/demo default; do not remove it.Verification