Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,28 @@ Versioning follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

---

## [1.5.1] - 2026-03-17

Storage column width fix. Prevents PostgreSQL `StorageError` on every capsule write.

### Fixed

- **`signed_at` column overflow on PostgreSQL** — `String(30)` was too narrow for `datetime.isoformat()` output from timezone-aware datetimes (32 characters for UTC `+00:00` suffix). Widened to `String(40)` in both `CapsuleModel` (SQLite) and `CapsuleModelPG` (PostgreSQL). SQLite was unaffected (it doesn't enforce `VARCHAR` length), but every `seal_and_store()` against PostgreSQL raised `StorageError: value too long for type character varying(30)`.
- **`signed_by` column zero headroom** — `String(16)` exactly matched the legacy 16-character hex fingerprint with no margin. Widened to `String(32)` in both models. The keyring `qp_key_XXXX` format (11 chars) was safe, but any future fingerprint format change would have caused the same overflow.

### Migration

- **New installations**: columns are created with the correct width by `create_all()`.
- **Existing PostgreSQL databases**:
```sql
ALTER TABLE quantumpipes_capsules
ALTER COLUMN signed_at TYPE VARCHAR(40),
ALTER COLUMN signed_by TYPE VARCHAR(32);
```
- **Existing SQLite databases**: no action required (SQLite does not enforce `VARCHAR` length).

---

## [1.5.0] - 2026-03-15

Hash chain concurrency protection. Prevents race conditions where concurrent writes could fork the chain.
Expand Down Expand Up @@ -178,6 +200,8 @@ Initial public release of the Capsule Protocol Specification (CPS) v1.0 referenc

---

[1.5.1]: https://github.com/quantumpipes/capsule/releases/tag/v1.5.1
[1.5.0]: https://github.com/quantumpipes/capsule/releases/tag/v1.5.0
[1.4.0]: https://github.com/quantumpipes/capsule/releases/tag/v1.4.0
[1.3.0]: https://github.com/quantumpipes/capsule/releases/tag/v1.3.0
[1.2.0]: https://github.com/quantumpipes/capsule/releases/tag/v1.2.0
Expand Down
46 changes: 43 additions & 3 deletions reference/python/docs/api.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
---
title: "API Reference"
description: "Complete API reference for Capsule: every class, method, parameter, and type."
date_modified: "2026-03-09"
date_modified: "2026-03-17"
ai_context: |
Complete Python API reference for the qp-capsule package v1.3.0. Covers Capsule model
Complete Python API reference for the qp-capsule package v1.5.0. Covers Capsule model
(6 sections, 8 CapsuleTypes), Seal (seal, verify, verify_with_key, compute_hash,
keyring integration), Keyring (epoch-based key rotation, NIST SP 800-57),
CapsuleChain (add, verify, seal_and_store), CapsuleStorageProtocol (7 methods),
CapsuleStorage (SQLite), PostgresCapsuleStorage (multi-tenant), exception hierarchy,
CapsuleStorage (SQLite), PostgresCapsuleStorage (multi-tenant), storage schema with
column constraints (signed_at String(40), signed_by String(32)), exception hierarchy,
CLI (verify, inspect, keys, hash), and the high-level API: Capsules class, @audit()
decorator, current() context variable, and mount_capsules() FastAPI integration.
---
Expand Down Expand Up @@ -507,6 +508,45 @@ The `list()` and `count()` methods accept extra parameters beyond the Protocol:

---

## Storage Schema

Both storage backends persist seal metadata in dedicated columns. The column widths are sized to accommodate all values produced by `Seal.seal()`.

<!-- VERIFIED: reference/python/src/qp_capsule/storage.py:60-71 -->
<!-- VERIFIED: reference/python/src/qp_capsule/storage_pg.py:56-68 -->

| Column | Type | Description |
|---|---|---|
| `id` | `String(36)` | UUIDv4 primary key |
| `type` | `String(20)` | `CapsuleType` value |
| `sequence` | `Integer` | Chain position (indexed, unique per tenant) |
| `previous_hash` | `String(64)` | SHA3-256 hash of previous Capsule (nullable) |
| `data` | `Text` | Full Capsule as JSON |
| `hash` | `String(64)` | SHA3-256 content hash (indexed) |
| `signature` | `Text` | Ed25519 signature (hex) |
| `signature_pq` | `Text` | ML-DSA-65 signature (hex, empty if PQ disabled) |
| `signed_at` | `String(40)` | `datetime.isoformat()` — UTC-aware produces 32 chars (e.g., `2026-03-17T05:24:42.485699+00:00`); 40 provides headroom |
| `signed_by` | `String(32)` | Key fingerprint — legacy hex prefix is 16 chars, keyring `qp_key_XXXX` is 11 chars; 32 provides headroom |
| `session_id` | `String(36)` | Conversation session UUID (nullable, indexed) |
| `domain` | `String(50)` | Capsule domain (PostgreSQL only, indexed) |
| `tenant_id` | `String(36)` | Tenant isolation (PostgreSQL only, nullable, indexed) |

### Migration from pre-1.5.1

Versions prior to 1.5.1 used `String(30)` for `signed_at` and `String(16)` for `signed_by`. The `signed_at` column was 2 characters too narrow for UTC-aware `datetime.isoformat()` output, causing `StorageError` on every PostgreSQL write.

For existing PostgreSQL deployments:

```sql
ALTER TABLE quantumpipes_capsules
ALTER COLUMN signed_at TYPE VARCHAR(40),
ALTER COLUMN signed_by TYPE VARCHAR(32);
```

SQLite does not enforce `VARCHAR` length, so no migration is needed for SQLite databases.

---

## Exceptions

<!-- VERIFIED: reference/python/src/qp_capsule/exceptions.py:12-29 -->
Expand Down
2 changes: 1 addition & 1 deletion reference/python/pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ build-backend = "hatchling.build"

[project]
name = "qp-capsule"
version = "1.5.0"
version = "1.5.1"
description = "Capsule Protocol Specification (CPS) — tamper-evident audit records for AI operations. Create, seal, verify, and chain Capsules in Python."
readme = "README.md"
license = "Apache-2.0"
Expand Down
2 changes: 1 addition & 1 deletion reference/python/src/qp_capsule/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@
Spec: https://github.com/quantumpipes/capsule
"""

__version__ = "1.5.0"
__version__ = "1.5.1"
__author__ = "Quantum Pipes Technologies, LLC"
__license__ = "Apache-2.0"

Expand Down
4 changes: 2 additions & 2 deletions reference/python/src/qp_capsule/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ class CapsuleModel(Base):
hash: Mapped[str] = mapped_column(String(64), index=True)
signature: Mapped[str] = mapped_column(Text) # Ed25519 (classical)
signature_pq: Mapped[str] = mapped_column(Text, default="") # ML-DSA-65 (post-quantum)
signed_at: Mapped[str | None] = mapped_column(String(30), nullable=True)
signed_by: Mapped[str | None] = mapped_column(String(16), nullable=True)
signed_at: Mapped[str | None] = mapped_column(String(40), nullable=True)
signed_by: Mapped[str | None] = mapped_column(String(32), nullable=True)
# v1.0.0: Session tracking for conversation queries
session_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)

Expand Down
4 changes: 2 additions & 2 deletions reference/python/src/qp_capsule/storage_pg.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,8 @@ class CapsuleModelPG(PGBase):
hash: Mapped[str] = mapped_column(String(64), index=True)
signature: Mapped[str] = mapped_column(Text) # Ed25519 (classical)
signature_pq: Mapped[str] = mapped_column(Text, default="") # ML-DSA-65 (post-quantum)
signed_at: Mapped[str | None] = mapped_column(String(30), nullable=True)
signed_by: Mapped[str | None] = mapped_column(String(16), nullable=True)
signed_at: Mapped[str | None] = mapped_column(String(40), nullable=True)
signed_by: Mapped[str | None] = mapped_column(String(32), nullable=True)
session_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
domain: Mapped[str] = mapped_column(String(50), default="agents", index=True)
tenant_id: Mapped[str | None] = mapped_column(String(36), nullable=True, index=True)
Expand Down
2 changes: 1 addition & 1 deletion reference/python/tests/test_chain_concurrency.py
Original file line number Diff line number Diff line change
Expand Up @@ -484,7 +484,7 @@ def test_returns_version_string(self):
from qp_capsule.cli import _get_version

version = _get_version()
assert version == "1.5.0"
assert version == "1.5.1"

def test_matches_package_version(self):
import qp_capsule
Expand Down
2 changes: 1 addition & 1 deletion reference/python/tests/test_cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -632,7 +632,7 @@ def test_version_flag(self, capsys, monkeypatch):
main(["--version"])
out = capsys.readouterr().out
assert "capsule" in out
assert "1.5.0" in out
assert "1.5.1" in out

def test_verify_via_main(self, seal, temp_dir, monkeypatch):
monkeypatch.setenv("NO_COLOR", "1")
Expand Down
Loading