Skip to content

feat(trust): adding digital signature verification on import#237

Closed
Akshat-Raj wants to merge 3 commits into
qbtrix:mainfrom
Akshat-Raj:cli
Closed

feat(trust): adding digital signature verification on import#237
Akshat-Raj wants to merge 3 commits into
qbtrix:mainfrom
Akshat-Raj:cli

Conversation

@Akshat-Raj
Copy link
Copy Markdown

@Akshat-Raj Akshat-Raj commented May 10, 2026

What does this PR do?

This PR shifts the trust chain verification from a "fail-open" to a "fail-closed" security posture. It wires trust chain verification directly into the Soul.awaken() import path. If a .soul file has a tampered, missing, or broken trust chain, it will now hard-fail and refuse to load by raising a SoulTrustError. This strict verification can be bypassed using the newly introduced --allow-unverified flag, which will emit a WARNING log instead.

Fixes #227

How to test

# Run the specific trust verification tests
uv run pytest tests/test_trust_chain/test_import_verification.py -v

Checklist

  • Tests pass locally (uv run pytest tests/)
  • Lint passes (uv run ruff check . && uv run ruff format --check .)
  • No secrets or credentials in the diff
  • PR title follows Conventional Commits (feat:, fix:, docs:, etc.)

@Akshat-Raj Akshat-Raj marked this pull request as ready for review May 10, 2026 19:37
Copilot AI review requested due to automatic review settings May 10, 2026 19:37
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR aims to shift soul import verification to a fail-closed posture by running trust-chain verification during Soul.awaken(), raising a SoulTrustError on verification failure unless explicitly bypassed.

Changes:

  • Introduces VerificationState / VerificationResult and updates trust-chain verification APIs to return structured results.
  • Wires trust-chain verification into Soul.awaken() with an allow_unverified bypass path (logs a warning instead of raising).
  • Adds import-path tests for valid chains and several tampering scenarios; updates CLI verify/audit to awaken in allow-unverified mode so they can inspect invalid souls.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
tests/test_trust_chain/test_import_verification.py Adds tests asserting awaken passes for valid chains and fails (or warns) for tampered/missing-signature/key-mismatch chains.
src/soul_protocol/runtime/trust/manager.py Adds VerificationState/VerificationResult and changes TrustChainManager.verify() to return structured verification info.
src/soul_protocol/runtime/soul.py Adds allow_unverified to Soul.awaken() and enforces trust-chain verification on load, raising SoulTrustError on failures.
src/soul_protocol/runtime/exceptions.py Introduces SoulTrustError for trust verification failures.
src/soul_protocol/cli/main.py Updates verify and audit commands to awaken with allow_unverified=True so they can operate on invalid souls.
Comments suppressed due to low confidence (1)

src/soul_protocol/runtime/soul.py:890

  • allow_unverified is added to the method signature, but the docstring’s Args section doesn’t mention it. Please document what it does (bypass fail-closed import and emit a warning) so API callers discover it without reading the implementation.
    async def awaken(
        cls,
        source: str | Path | bytes,
        engine: CognitiveEngine | Callable | str | None = None,
        search_strategy: SearchStrategy | None = None,
        password: str | None = None,
        eternal: EternalStorageManager | None = None,
        allow_unverified: bool = False,
    ) -> Soul:
        """Awaken a Soul from a .soul file, directory, soul.json, soul.yaml, or soul.md.

        Args:
            source: Path to soul file/directory, or raw bytes of a .soul archive.
                    Directories must contain a ``soul.json`` file.
            engine: Optional CognitiveEngine for LLM-enhanced cognition.
            search_strategy: Optional SearchStrategy for pluggable retrieval (v0.2.2).
            password: Optional password for decrypting encrypted .soul archives.
        """

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/soul_protocol/runtime/trust/manager.py
Comment thread src/soul_protocol/runtime/trust/manager.py Outdated
Comment thread src/soul_protocol/runtime/trust/manager.py Outdated
Comment thread src/soul_protocol/runtime/soul.py Outdated
Comment thread src/soul_protocol/runtime/soul.py
@Akshat-Raj Akshat-Raj changed the title this commit adds a fail close verification step to soul.awaken feat(trust): adding digital signature verification on import May 11, 2026
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 5 comments.

Comment thread src/soul_protocol/runtime/soul.py Outdated
Comment on lines +982 to +996
if verification_result.status == VerificationState.FAILED:
if allow_unverified:
logger.warning(
"Awakening unverified soul. Signer: %s, Reason: %s",
verification_result.signer,
verification_result.reason,
)
else:
logger.error("Soul import refused due to trust chain failure.")
raise SoulTrustError(
f"Refusing to awaken soul: {verification_result.reason}. "
"Pass allow_unverified=True to override."
)
elif verification_result.status == VerificationState.WARNED:
logger.warning(f"Trust chain warning: {verification_result.reason}")
Comment thread src/soul_protocol/runtime/soul.py Outdated
return VerificationResult(
status=VerificationState.FAILED,
reason=f"public key mismatch at seq {entry.seq}",
signer=entry.public_key,
Comment thread src/soul_protocol/runtime/soul.py Outdated
logger.error("Soul import refused due to trust chain failure.")
raise SoulTrustError(
f"Refusing to awaken soul: {verification_result.reason}. "
"Pass allow_unverified=True to override."
Comment on lines 3146 to 3150
from soul_protocol.runtime.soul import Soul
from soul_protocol.spec.trust import chain_integrity_check

soul = await Soul.awaken(path)
soul = await Soul.awaken(path, allow_unverified=True)
summary = chain_integrity_check(soul.trust_chain)
Comment on lines 3221 to 3225
async def _audit():
from soul_protocol.runtime.soul import Soul

soul = await Soul.awaken(path)
soul = await Soul.awaken(path, allow_unverified=True)
log = soul.audit_log(action_prefix=action_prefix, limit=limit)
Copy link
Copy Markdown
Contributor

@prakashUXtech prakashUXtech left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The architecture matches the spec (structured VerificationResult, three-state enum, --allow-unverified bypass with WARNING log), and the test matrix exercises all five acceptance scenarios from #227. Couple of blockers and a handful of polish items before it can land.

Blockers

1. Retarget to dev, not main.

Same workflow miss as your #232. All v0.5.0 work goes through dev per tracker #218. Rebase + update the PR base.

2. Legacy souls (empty chain) shouldn't be FAILED.

TrustChainManager.verify() now returns VerificationState.FAILED when chain.entries is empty (trust/manager.py). The test test_legacy_archive_without_chain_still_loads was updated to expect FAILED and pass allow_unverified=True to make it load.

The issue says "refuse to import on hard failure", but a pre-trust-chain legacy soul isn't tampered, it's just from before the trust chain existed. Conflating "no chain" with "broken chain" means anyone with a v0.3.x soul archive has to use --allow-unverified forever to load it. That defeats the security posture: users train themselves to always pass the flag, then miss real tampering.

Suggested fix: map empty chain to WARNED instead of FAILED. That logs a heads-up about a legacy file but still loads. Keep FAILED for actually-broken chains (tampered signatures, key mismatches).

High-priority cleanup

3. The VerificationResult tuple shim deserves a deprecation path.

__iter__ + __eq__ against tuples let passed, reason = result and result == (True, None) keep working for old callers. Two issues with the shim as it stands:

  • No removal path. It'll be in the API forever unless someone files a follow-up issue.
  • The __eq__ against tuples is asymmetric. result == (True, None) works (calls VerificationResult.__eq__ first), but (True, None) == result calls Python's tuple __eq__ first, gets NotImplemented, and falls back. Equality should be symmetric.

Either grep for the few tuple-unpacking callers (should be under 10 sites) and migrate them now, or leave the shim with a TODO: remove in 0.6.0 (see #XXX) comment pointing at a follow-up issue.

Polish

  • Branch name cli is too generic. Future you (or whoever rebases on top) will thank you for feat/issue-227-trust-import-verification instead.
  • Thinking-out-loud comments in test_import_verification.py:81-83. The "Need to update payload hash..." → "Wait..." → "So it should..." three-line train-of-thought is great while writing the test, but should be cleaned up to a clear one-line docstring before posting. Worth a self-check pass before the next push.
  • tests/test_cli/test_org_init.py has an unrelated whitespace change (removed blank line between two tests, line 95). Revert to keep the diff focused on this issue.
  • SoulTrustError.__str__ drops the signer. The exception captures signer as an attribute, but the message format is f"Trust chain validation failed: {reason}". Operators triaging from logs would benefit from including signer in the message too.
  • CHANGELOG entry missing. This is both an addition (the verification + SoulTrustError) and a behavior change (default refuses tampered souls). Add lines under both ## Added and ## Changed.
  • --allow-unverified is only on verify and audit commands. Other CLI entry points that load souls (inspect, status, recall, etc.) will hard-refuse on a broken soul with no escape hatch. Intentional? If so, doc it (verify/audit are the diagnostic paths, others enforce strict). If not, add the flag.

What's working

  • The structured VerificationResult dataclass + three-state enum matches the issue spec exactly
  • Test matrix is the right one (clean / tampered / missing / key-rotation / bypass-warns)
  • Adding lifecycle.birth chain entry in birth() and lifecycle.reincarnate in reincarnate() is a thoughtful touch: removes the edge case of "freshly-birthed has no chain"
  • Honest test updates (test_encryption.py, test_back_compat_load.py, test_persistence.py passing allow_unverified=True explicitly). The dependency is visible rather than hidden.
  • New SoulTrustError with reason + signer fields makes downstream error handling cleaner

Once the base retarget + the legacy-souls semantics + the tuple-shim path are sorted, this is mergeable.

@Akshat-Raj
Copy link
Copy Markdown
Author

@prakashUXtech Will fix the issues ASAP!

@Akshat-Raj Akshat-Raj closed this May 17, 2026
@Akshat-Raj Akshat-Raj deleted the cli branch May 17, 2026 00:40
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(trust): digital signature verification on import

3 participants