Visitar URL original
Harden Audit Persistence and Schema Compatibility by PSchmiedmayer · Pull Request #106 · SchmiedmayerLab/heartwood · GitHub
Skip to content

Harden Audit Persistence and Schema Compatibility - #106

Merged
PSchmiedmayer merged 18 commits into
mainfrom
feature/audit-persistence-hardening
Aug 3, 2026
Merged

PSchmiedmayer merged 18 commits into
mainfrom
feature/audit-persistence-hardening

Conversation

@PSchmiedmayer

@PSchmiedmayer PSchmiedmayer commented Aug 2, 2026 •

Copy link
Copy Markdown
Member

♻️ Current situation & Problem

Closes #45.

Heartwood needed deterministic persistence recovery and an authoritative checkpoint path that does not expose deployment signing keys to projects or command arguments.

⚙️ Release Notes

  • Add versioned migrations, recoverable process-locked appends, and full audit verification.
  • Add provider-neutral checkpoint signing with deployment-approved profiles, pinned public keys, and a KMS/HSM-compatible HTTPS contract.
  • Add an authenticated loopback signer for explicit development and offline use.
  • Add CLI workflows to inspect or select signer profiles and create or verify authoritative checkpoints.

📚 Documentation

Documented signer ownership, registry precedence, production and local workflows, key rotation, retention responsibilities, platform integration, diagnostics, and verification.

✅ Testing

  • 1,526 Python tests with 92.13% branch coverage and 91.01% patch coverage.
  • 120 web unit tests, contract checks, linting, formatting, duplication checks, and a production build.
  • Strict typing, dead-code analysis, lock validation, and documentation build.
  • Signer protocol, authentication, identity pinning, P-256 and Ed25519 verification, interruption, concurrency, Carina handoff, and end-to-end process tests.

Code of Conduct & Contributing Guidelines

By creating and submitting this pull request, you agree to follow our Code of Conduct and Contributing Guidelines:

Copilot AI review requested due to automatic review settings August 2, 2026 23:00
@github-project-automation github-project-automation Bot moved this to Backlog in Heartwood Aug 2, 2026
@coderabbitai

coderabbitai Bot commented Aug 2, 2026 •

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

This change adds shared durable persistence, native locks, recoverable JSONL appends, and deterministic schema migrations. Audit logs now support full verification, canonical exports, sensitive-field scrubbing, and signed checkpoint bundles. Session, project, OpenHands, Skill, and configuration persistence use shared APIs. Gateway and CLI workflows create, export, verify, and retain audit artifacts. Documentation and tests define compatibility, durability, recovery, and checkpoint contracts.

Sequence Diagram(s)

sequenceDiagram
  participant Operator
  participant CLI
  participant SessionGateway
  participant FileSessionStore
  participant AuditLog
  participant CheckpointBundle
  Operator->>CLI: request audit checkpoint
  CLI->>SessionGateway: create checkpoint
  SessionGateway->>FileSessionStore: verify session and audit state
  FileSessionStore->>AuditLog: recover and verify JSONL history
  AuditLog-->>SessionGateway: canonical export and verification metadata
  SessionGateway->>CheckpointBundle: sign and publish bundle
  Operator->>CLI: verify checkpoint
  CLI->>SessionGateway: verify bundle
  SessionGateway->>CheckpointBundle: validate signature and export binding
Loading

Possibly related issues

  • Issue #48 — The issue covers audit append recovery, schema migration, and authoritative checkpointing, which are implemented in this change.

Possibly related PRs

🚥 Pre-merge checks | ✅ 3 | ❌ 2

❌ Failed checks (2 warnings)

Check name Status Explanation Resolution
Out of Scope Changes check ⚠️ Warning The tablet browser heading test change is unrelated to the linked audit persistence and schema compatibility objectives. Remove the unrelated tablet browser validation change or link it to an issue that explicitly requires this UI test update.
Docstring Coverage ⚠️ Warning Docstring coverage is 23.69% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (3 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The implementation addresses the linked issue objectives for migrations, recovery, verification, deployment-owned checkpoints, retention, and content minimization.
Title check ✅ Passed The title clearly summarizes the main changes to audit persistence and schema compatibility.
Description check ✅ Passed The description directly covers the persistence, checkpoint signing, CLI, documentation, and testing changes.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Copilot AI left a comment

Copy link
Copy Markdown

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 hardens Heartwood’s persisted-state and audit-evidence story by introducing a shared persistence/migration contract, durable file primitives (atomic writes + native process locks + recoverable JSONL appends), and adding authoritative Ed25519-signed audit checkpoints that can be retained outside the agent-writable project.

Changes:

  • Add a new heartwood-persistence package providing deterministic migration registry + owner-only durable file primitives (atomic replace, native locks, recoverable JSONL append).
  • Upgrade audit logging to use recoverable locked appends, provide full-verification utilities, and add signed “checkpoint bundles” (audit export + signed statement) with CLI/gateway entry points.
  • Wire the new primitives into gateway/session/skills/OpenHands persistence and add compatibility fixtures + tests + documentation for the migration and checkpoint workflows.

Reviewed changes

Copilot reviewed 69 out of 70 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
zensical.toml Adds audit checkpoint/retention page to docs nav.
uv.lock Adds new workspace member and dependencies (notably heartwood-persistence, cryptography).
pyproject.toml Registers packages/persistence in the uv workspace and mypy path.
packages/skills/tests/test_skill_verification.py Adds compatibility fixture loading and symlink/unsupported-metadata tests.
packages/skills/src/heartwood/skills/_verification.py Uses persistence readers + migration registry when loading skill metadata/frontmatter.
packages/skills/pyproject.toml Adds dependency on heartwood-persistence.
packages/schemas/tests/test_schema_records.py Adds schema inventory and validation tests for checkpoint/retention records.
packages/schemas/src/heartwood/schemas/_records.py Introduces AuditRetention, AuditCheckpointStatement, AuditCheckpoint schema records + validators.
packages/schemas/src/heartwood/schemas/init.py Exports new checkpoint/retention schema types.
packages/persistence/tests/test_migrations.py Adds determinism/compatibility tests for the shared migration registry.
packages/persistence/tests/test_files.py Adds atomic-write, interruption, recovery, lock, symlink rejection, and permission tests for durable file primitives.
packages/persistence/tests/fixtures/skill-metadata-v1.json Adds checked-in compatibility fixture for skill metadata.
packages/persistence/tests/fixtures/session-writer-v1.json Adds checked-in compatibility fixture for session writer metadata.
packages/persistence/tests/fixtures/session-metadata-v1.json Adds checked-in compatibility fixture for session metadata.
packages/persistence/tests/fixtures/session-event-v1.json Adds checked-in compatibility fixture for session events.
packages/persistence/tests/fixtures/session-commit-v1.json Adds checked-in compatibility fixture for pending commit recovery envelope.
packages/persistence/tests/fixtures/session-command-receipt-v1.json Adds checked-in compatibility fixture for command receipt envelopes.
packages/persistence/tests/fixtures/project-state-v2.json Adds checked-in compatibility fixture for project state marker v2.
packages/persistence/tests/fixtures/project-state-v1.json Adds checked-in compatibility fixture for legacy project state marker v1.
packages/persistence/tests/fixtures/project-config-v1.json Adds checked-in compatibility fixture for project config schema.
packages/persistence/tests/fixtures/openhands-state-v1.json Adds checked-in compatibility fixture for OpenHands marker v1.
packages/persistence/tests/fixtures/openhands-state-unversioned.json Adds checked-in compatibility fixture for unversioned OpenHands marker.
packages/persistence/tests/fixtures/audit-event-v1.json Adds checked-in compatibility fixture for audit events.
packages/persistence/src/heartwood/persistence/py.typed Marks heartwood.persistence as typed for type checkers.
packages/persistence/src/heartwood/persistence/_migrations.py Implements deterministic, non-mutating migration registry + kinds/versions.
packages/persistence/src/heartwood/persistence/_files.py Implements secure file IO primitives + native lock + recoverable JSONL store.
packages/persistence/src/heartwood/persistence/init.py Exposes persistence primitives and migration registry as package API.
packages/persistence/README.md Documents purpose/scope of persistence package.
packages/persistence/pyproject.toml Defines the heartwood-persistence package and dependencies.
packages/gateway/tests/test_session_catalog.py Adds session metadata fixture test and interrupted-write recovery coverage.
packages/gateway/tests/test_project.py Updates project state marker expectations + adds concurrent init + migration tests.
packages/gateway/tests/test_project_config.py Adds project-config compatibility fixture loading test.
packages/gateway/tests/test_openhands_persistence.py Adds OpenHands persistence compatibility/minimization and migration tests.
packages/gateway/tests/test_gateway_contract.py Updates REST error expectation for lock-unavailable case.
packages/gateway/tests/test_audit_checkpoint.py Adds gateway-level checkpoint creation/verification boundary tests.
packages/gateway/src/heartwood/gateway/_skill_settings.py Uses LockedJsonlStore and fsync for skill decision audit trail and install fsync.
packages/gateway/src/heartwood/gateway/_session_catalog.py Adds native-lock serialization and migration-aware metadata loading + lock-unavailable fallback.
packages/gateway/src/heartwood/gateway/_rest.py Maps NativeLockUnavailableError to a 409 REST error response.
packages/gateway/src/heartwood/gateway/_project.py Introduces project state marker v2, initialization lock, and migration during init.
packages/gateway/src/heartwood/gateway/_project_config.py Uses durable reads/writes + native lock; routes config schema via migration registry.
packages/gateway/src/heartwood/gateway/_openhands_persistence.py Adds versioned OpenHands marker, minimizes events, enforces SDK version compatibility, and uses atomic writes.
packages/gateway/src/heartwood/gateway/_gateway.py Adds audit verification + checkpoint/copy operations and enforces project boundary for deployment-owned artifacts.
packages/gateway/src/heartwood/gateway/init.py Re-exports audit checkpoint/verification types and lock-unavailable error.
packages/gateway/pyproject.toml Adds heartwood-audit and heartwood-persistence dependencies.
packages/core-adapter/tests/test_session_service.py Adds compatibility fixture replay and export re-verification regression test.
packages/core-adapter/src/heartwood/core_adapter/_state.py Uses persistence primitives + migration registry for durable session storage and verification-derived head state.
packages/core-adapter/src/heartwood/core_adapter/_service.py Uses verified head and verified export path; records export event count via sequence.
packages/core-adapter/pyproject.toml Adds dependency on heartwood-persistence.
packages/cli/tests/test_cli.py Adds CLI tests for audit verify/checkpoint/verify-checkpoint flows and updated lock-unavailable error text.
packages/cli/src/heartwood/cli/init.py Adds new audit subcommands and maps new error types to exit codes/messages.
packages/audit/tests/test_checkpoint.py Adds extensive checkpoint signing/verification, tamper detection, and interruption/concurrency tests.
packages/audit/tests/test_audit_log.py Adds concurrency, full verification, and lock-failure distinction tests for audit log.
packages/audit/src/heartwood/audit/_log.py Switches audit storage to LockedJsonlStore, adds full verification utilities, and canonical JSONL export.
packages/audit/src/heartwood/audit/_checkpoint.py Implements signed checkpoint bundle creation/verification with strict filesystem and key constraints.
packages/audit/src/heartwood/audit/init.py Exposes checkpoint and verification APIs.
packages/audit/README.md Updates audit package description to include recoverable appends and checkpoints.
packages/audit/pyproject.toml Adds cryptography and heartwood-persistence dependencies.
documentation/use/actions-audit.md Documents audit verify and points to checkpointing for authoritative retention.
documentation/reference/troubleshooting.md Adds troubleshooting guidance for audit verification and checkpoints.
documentation/reference/glossary.md Adds definitions for audit checkpoint, Ed25519, JSON Lines.
documentation/reference/configuration.md Documents state marker schema versions + deterministic migration + audit artifact boundaries.
documentation/reference/cli.md Documents new audit CLI commands and checkpoint workflow pointers.
documentation/operate/security.md Clarifies trust boundary for replaceable exports vs signed checkpoints.
documentation/operate/index.md Updates deployment evidence responsibilities to include signed checkpoints and retention controls.
documentation/operate/audit-checkpoints.md Adds full operator workflow doc for checkpoint creation/verification and retention responsibilities.
documentation/contribute/development.md Adds persistence package to repo map and codifies persistence testing/fixture requirements.
documentation/architecture/testing.md Adds persistence fault tests + checkpoint coverage to testing architecture.
documentation/architecture/sessions-audit.md Documents migration registry contract, OpenHands marker handling, and checkpoint limitations.
AGENTS.md Adds audit checkpoints doc to canonical documentation routing table.

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

Copilot AI review requested due to automatic review settings August 2, 2026 23:04

Copilot AI left a comment

Copy link
Copy Markdown

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 69 out of 70 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/audit/src/heartwood/audit/_log.py:112

  • AuditLog.append() only verifies the tail (_verify_tail) and only checks session_id on the last two events. Because the code already materializes all existing events here, a log with an earlier session-id change, sequence gap, or bad hash link can still pass these checks and be appended to (making the corruption harder to detect later). This should fail closed by verifying the full existing chain before deriving the next sequence/hash, and then separately ensuring the existing log's session matches the session_id being appended.
            events = tuple(_audit_event(record) for record in records)
            if events:
                _verify_tail(events)
                if any(event.session_id != session_id for event in events[-2:]):
                    raise AuditIntegrityError("audit append session does not match existing log")

packages/persistence/src/heartwood/persistence/_files.py:226

  • LockedJsonlStore.append_derived() reads and JSON-decodes the entire JSONL file (records = self._read_objects_locked()) on every append, even though many callers (e.g., audit appends) only need the current head (count + last record/hash). This makes appending O(n^2) over time and increases lock hold times for large logs, which can become a bottleneck for long-running sessions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 11

🧹 Nitpick comments (16)
packages/audit/tests/test_checkpoint.py (2)

273-276: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Verify the bundle with its own public key.

_checkpoint_bundle(tmp_path / "valid") returns the public key for the key pair it created, but the test discards it and reuses public_key from Line 260. The assertion still passes, because _bundle_paths rejects unexpected files before _load_public_key runs. The mismatch makes the intent unclear and hides a later regression if the ordering of checks changes.

♻️ Proposed change
-    bundle, _ = _checkpoint_bundle(tmp_path / "valid")
+    bundle, bundle_public_key = _checkpoint_bundle(tmp_path / "valid")
     (bundle / "unexpected.txt").write_text("not part of the checkpoint", encoding="utf-8")
     with pytest.raises(AuditCheckpointError, match="unexpected files"):
-        verify_audit_checkpoint(bundle=bundle, public_key=public_key)
+        verify_audit_checkpoint(bundle=bundle, public_key=bundle_public_key)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/tests/test_checkpoint.py` around lines 273 - 276, Update the
test around _checkpoint_bundle to retain the bundle’s generated public key and
pass that key to verify_audit_checkpoint instead of reusing the unrelated
public_key fixture. Keep the unexpected-files assertion unchanged so the test
explicitly verifies the bundle with its own key.

47-47: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use one deployment identifier across the checkpoint tests.

This test uses "carina-research" and Line 75 uses "terra-research", while every other test uses "generic-research". Use "generic-research" everywhere. This keeps the public test fixtures neutral and avoids organization-specific names in source control.

Based on learnings: "Attribute Heartwood only to the Schmiedmayer Lab at Stanford University; do not add legacy organizational attributions."

♻️ Proposed change
-        deployment_id="carina-research",
+        deployment_id="generic-research",
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/tests/test_checkpoint.py` at line 47, Update the deployment_id
in the checkpoint test at the visible deployment configuration to use
"generic-research", and ensure the corresponding deployment identifier at the
other referenced checkpoint test location also uses "generic-research" so all
checkpoint fixtures share the neutral identifier.

Source: Learnings

packages/gateway/tests/test_audit_checkpoint.py (1)

20-42: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Release the gateway even when an assertion fails.

gateway.stop() runs only after the assertions. If any assertion fails, stop() is skipped and the gateway keeps its session writer lock and background resources. Later tests in the same process can then fail for an unrelated reason. The same pattern appears at Line 74 and Line 105. Use a fixture or contextlib.closing so cleanup always runs.

♻️ Proposed fixture
`@pytest.fixture`
def gateway_factory() -> Iterator[Callable[[Path], SessionGateway]]:
    created: list[SessionGateway] = []

    def build(project_root: Path) -> SessionGateway:
        gateway = SessionGateway(
            project=ProjectContext(project_root),
            backend_id="deterministic",
        )
        created.append(gateway)
        return gateway

    yield build
    for gateway in created:
        gateway.stop()

Then replace each direct construction and drop the trailing gateway.stop() calls.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_audit_checkpoint.py` around lines 20 - 42, Ensure
all gateway instances in the affected tests, including the cases around
test_gateway_creates_and_verifies_checkpoint_outside_project and the tests at
the referenced later locations, are stopped even when assertions fail. Introduce
and use a pytest fixture or contextlib.closing-based cleanup around each
SessionGateway construction, then remove the trailing direct gateway.stop()
calls.
packages/core-adapter/src/heartwood/core_adapter/_state.py (1)

442-444: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win

Initialize the sequence and audit-hash caches on the empty-session path.

_verified_records returns early when session_dir does not exist. It does not set _next_sequence or _last_audit_hash. next_sequence() then calls replay_events(), finds _next_sequence still None, and raises SessionRecoveryError("session sequence could not be initialized") instead of returning 0. Callers that reach next_sequence() before acquire_writer() creates the directory get a recovery error for an empty session. Setting both caches also clears a stale _last_audit_hash if the directory is removed during the store's lifetime.

♻️ Proposed change
         if not self.session_dir.exists():
             verification = AuditLog(self.audit_path).verify(())
+            self._next_sequence = 0
+            self._last_audit_hash = None
             return (), (), verification
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/core-adapter/src/heartwood/core_adapter/_state.py` around lines 442
- 444, Update the empty-session early-return path in _verified_records to
initialize _next_sequence to 0 and _last_audit_hash to the empty-session audit
hash state before returning. Preserve the existing verification result and
return values, ensuring next_sequence() works before acquire_writer() and stale
audit-hash state is cleared.
packages/cli/src/heartwood/cli/__init__.py (1)

1917-1928: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Make the audit checkpoint CLI print the gateway-resolved bundle path.

SessionGateway.create_audit_checkpoint resolves the destination through _deployment_owned_path before calling create_audit_checkpoint(), but AuditCheckpointVerification only exposes metadata, not that resolved path. If the CLI needs to print where the bundle was written, get and return/attach the resolved destination from the gateway API rather than recomputing output.expanduser().resolve() in _handle_audit_checkpoint.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/heartwood/cli/__init__.py` around lines 1917 - 1928, The
audit checkpoint CLI currently recomputes the output path instead of reporting
the gateway-resolved destination. Update SessionGateway.create_audit_checkpoint
and the AuditCheckpointVerification result flow to return or attach the resolved
bundle path, then have _handle_audit_checkpoint print that value in the “Audit
checkpoint” line rather than calling output.expanduser().resolve().

Source: Coding guidelines

packages/persistence/src/heartwood/persistence/_migrations.py (1)

199-247: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider freezing the shared registry after construction.

PERSISTENCE_MIGRATIONS is a process-wide singleton, and register_kind and register stay public on it. Any imported module can change the persisted-schema contract for every consumer at import time. No current caller does this. A freeze flag keeps the deterministic contract enforced while MigrationRegistry stays mutable for the test-local registries in packages/persistence/tests/test_migrations.py.

♻️ Proposed freeze support
     def __init__(self) -> None:
         self._current: dict[str, str] = {}
         self._steps: dict[tuple[str, str], _MigrationStep] = {}
+        self._frozen = False
+
+    def freeze(self) -> None:
+        """Reject every further registration on this registry."""
+        self._frozen = True
+
+    def _require_mutable(self) -> None:
+        if self._frozen:
+            raise MigrationError("migration registry is frozen")

Call _require_mutable() at the start of register_kind and register, then freeze the singleton:

-    return registry
+    registry.freeze()
+    return registry
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/persistence/src/heartwood/persistence/_migrations.py` around lines
199 - 247, Make MigrationRegistry support freezing by adding a frozen-state
guard and calling _require_mutable() at the start of register_kind and register.
After _build_registry() completes, freeze PERSISTENCE_MIGRATIONS so the
process-wide registry cannot be modified, while newly constructed test-local
registries remain mutable.
packages/schemas/tests/test_schema_records.py (1)

164-169: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Extend the negative coverage to the other new validators.

The test covers only the terminal-hash and event-count rule. Two other new rules stay uncovered: the retention-ordering rule in AuditCheckpointStatement._validate_terminal_hash and the canonical Ed25519 signature rule in AuditCheckpoint._validate_signature. Both rules protect checkpoint integrity, so add explicit rejection cases.

💚 Proposed additional assertions
     invalid = statement.model_dump(mode="json")
     invalid["audit_event_count"] = 0
     with pytest.raises(ValidationError):
         AuditCheckpointStatement.model_validate(invalid)
+
+    early_retention = statement.model_dump(mode="json")
+    early_retention["retention"]["retain_until"] = "2026-08-01"
+    with pytest.raises(ValidationError):
+        AuditCheckpointStatement.model_validate(early_retention)
+
+    for bad_signature in ("not-base64!", base64.b64encode(bytes(32)).decode("ascii")):
+        with pytest.raises(ValidationError):
+            AuditCheckpoint(
+                statement=statement,
+                signing_key_id=f"sha256:{'c' * 64}",
+                signature=bad_signature,
+            )

As per coding guidelines: "Add or update tests when changing detector logic, policy decisions, adapter behavior, skill validation, audit records, attestation export, CLI output, notebook view models, or web-UI view models."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/schemas/tests/test_schema_records.py` around lines 164 - 169, Extend
the negative test around AuditCheckpointStatement.model_validate and
AuditCheckpoint validation to cover both missing rules: construct a statement
with invalid retention ordering and assert ValidationError, then construct a
checkpoint with a non-canonical Ed25519 signature and assert ValidationError.
Keep the existing terminal-hash and audit_event_count rejection coverage
unchanged.

Source: Coding guidelines

packages/persistence/tests/test_files.py (2)

162-195: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert a distinct message for each journal failure mode.

The parameterization covers four different rejection paths in _recover_locked. The assertion at Line 194 accepts any AppendRecoveryError. A regression that routes every case through one check still passes all four parameters. Add the expected message fragment to each parameter.

♻️ Proposed fix to pin each failure to its own message
-@pytest.mark.parametrize("failure", ["malformed", "invalid", "digest", "suffix"])
+@pytest.mark.parametrize(
+    ("failure", "message"),
+    [
+        ("malformed", "journal is malformed"),
+        ("invalid", "journal is invalid"),
+        ("digest", "journal digest does not match"),
+        ("suffix", "does not match its recovery journal"),
+    ],
+)
 def test_jsonl_recovery_rejects_invalid_journal_or_target(
     tmp_path: Path,
     monkeypatch: pytest.MonkeyPatch,
     failure: str,
+    message: str,
 ) -> None:
-    with pytest.raises(AppendRecoveryError):
+    with pytest.raises(AppendRecoveryError, match=message):
         store.recover()
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/persistence/tests/test_files.py` around lines 162 - 195, Update
test_jsonl_recovery_rejects_invalid_journal_or_target to parameterize each
failure mode with its expected AppendRecoveryError message fragment, then assert
that fragment via pytest.raises(match=...) when calling store.recover(). Keep
the existing setup and four failure cases unchanged.

269-276: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add coverage for the lock timeout branch.

This test covers the OSError branch of native_file_lock. The FileLockTimeout branch at _files.py Line 193 produces a different message and stays uncovered. native_file_lock exposes a timeout parameter, so callers can reach that branch.

♻️ Proposed additional test
def test_native_lock_timeout_is_explicit(tmp_path: Path) -> None:
    lock_path = tmp_path / "records.jsonl.lock"
    with _files.native_file_lock(lock_path):
        holder = FileLock(str(lock_path), timeout=0, is_singleton=False)
        del holder
        with pytest.raises(NativeLockUnavailableError, match="timed out"):
            _run_in_child_process_expecting_timeout(lock_path)

The lock is reentrant inside one process, so acquire the second lock from a forked child, as test_jsonl_store_serializes_process_writers does.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/persistence/tests/test_files.py` around lines 269 - 276, Add a test
alongside test_native_lock_failure_is_explicit that exercises native_file_lock’s
timeout path using a forked child process, since same-process locking is
reentrant. Hold the lock in the parent, attempt acquisition from the child with
timeout=0, and assert NativeLockUnavailableError with the “timed out” message
while preserving the existing OSError coverage.
packages/persistence/src/heartwood/persistence/_files.py (1)

116-130: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Sync the parent directory after append_private_bytes creates the file.

append_private_bytes passes O_CREAT and syncs the file data. It does not sync the parent directory. If the append creates the file, the new directory entry can be lost after a crash even though os.fsync(descriptor) succeeded. write_private_bytes_atomic and unlink_durable both call fsync_directory, so this path is the only durable operation that omits it.

LockedJsonlStore masks the gap because the journal write syncs the directory first. Direct callers of append_private_bytes do not get that protection.

♻️ Proposed fix to sync the directory on creation
 def append_private_bytes(path: Path, content: bytes) -> None:
     """Durably append bytes to an owner-only regular file."""
     _prepare_parent(path)
     _reject_non_regular_target(path)
+    created = not path.exists()
     descriptor = _open_regular(
         path,
         os.O_APPEND | os.O_CREAT | os.O_WRONLY,
         mode=0o600,
     )
     try:
         os.fchmod(descriptor, 0o600)
         _write_all(descriptor, content, operation="append")
         os.fsync(descriptor)
     finally:
         os.close(descriptor)
+    if created:
+        fsync_directory(path.parent)
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/persistence/src/heartwood/persistence/_files.py` around lines 116 -
130, Update append_private_bytes to sync the parent directory after the file
descriptor is durably written and closed, matching the durability behavior of
write_private_bytes_atomic and unlink_durable; ensure the directory sync occurs
for newly created entries without leaving the descriptor open.
packages/gateway/tests/test_openhands_persistence.py (2)

175-196: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Add a schema_version case to the marker rejection parameters.

_validate_marker rejects four conditions: an unexpected key set, an unsupported schema_version, an unsupported content_policy, and an unsupported adopted_from. This test covers three of them. An unsupported schema_version on an otherwise current marker reaches _migrated_marker first and must produce "schema is unsupported". Add that parameter to pin the behavior.

💚 Proposed change
         ({"unexpected": "field"}, "marker is malformed"),
+        ({"schema_version": "heartwood.openhands-state.v99"}, "schema is unsupported"),
         ({"content_policy": "unsupported"}, "content policy is unsupported"),
         ({"adopted_from": "unknown"}, "origin is unsupported"),

As per coding guidelines: "Add or update tests when changing detector logic, policy decisions, adapter behavior, skill validation, audit records, attestation export, CLI output, notebook view models, or web-UI view models."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_openhands_persistence.py` around lines 175 - 196,
Add an unsupported schema_version case to the marker_update/message parameters
in test_store_rejects_incompatible_current_markers, using an otherwise current
marker and expecting “schema is unsupported.” Preserve the existing cases and
parameterized test structure.

Source: Coding guidelines


133-154: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Assert the specific error message for each failure mode.

pytest.raises(OpenHandsPersistenceError) accepts any error from this module. _ensure_compatible_state raises the same exception type for symbolic links, malformed events, filename violations, and index gaps. A regression that moves one failure to a different check still passes this test. Add a match value per parameter, as the other tests in this file already do.

💚 Proposed change
-@pytest.mark.parametrize("failure", ["gap", "malformed", "symlink"])
-def test_markerless_store_rejects_incompatible_state(tmp_path: Path, failure: str) -> None:
+@pytest.mark.parametrize(
+    ("failure", "message"),
+    [
+        ("gap", "event sequence contains a gap"),
+        ("malformed", "event state is malformed"),
+        ("symlink", "symbolic link or special file"),
+    ],
+)
+def test_markerless_store_rejects_incompatible_state(
+    tmp_path: Path,
+    failure: str,
+    message: str,
+) -> None:
@@
-    with pytest.raises(OpenHandsPersistenceError):
+    with pytest.raises(OpenHandsPersistenceError, match=message):
         ContentMinimizedLocalFileStore(str(root))
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_openhands_persistence.py` around lines 133 - 154,
Update test_markerless_store_rejects_incompatible_state to supply a
failure-specific match pattern to pytest.raises for each parameterized case,
covering the symbolic-link, malformed-event, and index-gap error messages.
Follow the existing message-matching style used by other tests in this file
while preserving the current setup and exception type assertion.
packages/gateway/tests/test_project.py (2)

167-176: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Assert that the error message does not expose the persisted payload.

The fixture places "private":"value" in the unsupported marker. _migrate_state_marker formats the underlying error into f"unable to migrate .heartwood/state.json: {error}". The migration registry states that it does not expose payload content in errors, but this test does not verify that contract at the ProjectStateError boundary. test_store_rejects_malformed_event_without_exposing_content in packages/gateway/tests/test_openhands_persistence.py already uses this pattern.

💚 Proposed change
-    with pytest.raises(ProjectStateError):
+    with pytest.raises(ProjectStateError) as captured:
         project.initialize()

+    assert "value" not in str(captured.value)
     assert project.state_path.read_text(encoding="utf-8") == unsupported
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_project.py` around lines 167 - 176, Add an
assertion to test_project_context_preserves_state_when_migration_fails that the
raised ProjectStateError message does not contain the persisted payload value
"private" or "value", while retaining the existing state-preservation assertion.

68-73: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Document why the unsynchronized first_thread guard is safe.

pause_first_write reads and appends to first_thread without synchronization. The test is correct only because the second thread blocks on state_lock_path before it reaches the .gitignore write, so only one thread ever enters this branch. If the lock ordering in initialize changes, both threads can pause and the test deadlocks until the 5-second waits expire. Add a short comment that records this dependency.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_project.py` around lines 68 - 73, Add a short
comment immediately before the unsynchronized first_thread check in
pause_first_write, documenting that initialize’s state_lock_path ordering
ensures only one thread reaches the .gitignore branch and that changing this
ordering could cause both threads to wait and deadlock.
packages/gateway/src/heartwood/gateway/_openhands_persistence.py (1)

157-171: 🩺 Stability & Availability | 🔵 Trivial | 💤 Low value

Translate filesystem errors during the scan into OpenHandsPersistenceError.

path.lstat() and path.chmod() raise OSError directly. _ensure_compatible_state does not catch OSError for this call path, so a removed entry or a read-only mount surfaces a raw OSError from the constructor. Every other failure in this module surfaces as OpenHandsPersistenceError. Callers that handle only OpenHandsPersistenceError then fail unexpectedly.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/src/heartwood/gateway/_openhands_persistence.py` around
lines 157 - 171, Update the filesystem scan in _ensure_compatible_state to catch
OSError from path.lstat() and path.chmod() and re-raise it as
OpenHandsPersistenceError, preserving the original exception as context. Ensure
removed entries and read-only filesystem failures follow the module’s existing
OpenHandsPersistenceError contract.
packages/gateway/tests/test_project_config.py (1)

85-99: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Simplify the fixture path and consider sharing the resolver.

parents[2] is already the packages directory, so the extra parents[3] / "packages" hop is redundant and more sensitive to a directory move. packages/core-adapter/tests/test_session_service.py resolves the same fixture directory through its own _COMPATIBILITY_FIXTURES constant. Three packages now duplicate this traversal. A single shared constant or a pytest fixture would keep the compatibility fixture location in one place.

♻️ Proposed refactor
-    fixture = (
-        Path(__file__).resolve().parents[3]
-        / "packages"
-        / "persistence"
-        / "tests"
-        / "fixtures"
-        / "project-config-v1.json"
-    )
+    fixture = (
+        Path(__file__).resolve().parents[2]
+        / "persistence"
+        / "tests"
+        / "fixtures"
+        / "project-config-v1.json"
+    )
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_project_config.py` around lines 85 - 99, Update
test_checked_in_project_config_compatibility_fixture_loads to resolve the
fixture from Path(__file__).resolve().parents[2] / "persistence" / "tests" /
"fixtures" / "project-config-v1.json", removing the redundant packages hop.
Prefer reusing or defining a shared compatibility-fixture resolver, such as the
existing _COMPATIBILITY_FIXTURES convention, so the fixture location is
centralized across package tests.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@documentation/architecture/sessions-audit.md`:
- Around line 75-77: Update the schema-version paragraph in the sessions-audit
architecture documentation to state that current persisted envelopes are
versioned, while supported legacy inputs may be unversioned and are migrated
before typed validation. Keep the existing descriptions of the project state
marker and deterministic migration registry unchanged.

In `@documentation/operate/security.md`:
- Line 108: Update the limitation sentence in the security documentation to
state directly that verify_audit_checkpoint does not prove no events were
removed before checkpoint creation. Replace only the phrase “before an earlier
suffix was removed,” preserving the surrounding claims about authentication and
retention.

In `@documentation/reference/glossary.md`:
- Around line 18-20: Update the “Audit checkpoint” glossary definition to
replace the generic origin binding with the checkpoint schema terminology: state
that it is signed with a deployment-owned Ed25519 key and bound to its
deployment identifier, session, creation time, and retention declaration.

In `@packages/gateway/src/heartwood/gateway/_openhands_persistence.py`:
- Around line 85-119: Wrap the full migration and compatibility-check sequence
in ContentMinimizedLocalFileStore.__init__, including _ensure_compatible_state
and its event/marker rewrites, with the existing native session- or
conversation-scoped process lock used by ProjectContext.initialize(). Acquire
the lock before calling _ensure_compatible_state and release it after
initialization completes, preserving the current validation and marker behavior.
- Around line 78-83: Update the bytes branch in the write method guarded by
self._heartwood_write_lock to remove any existing self.cache entry for
str(full_path) after write_private_bytes_atomic completes, using the cache’s
dictionary-style removal; leave the text-write caching behavior unchanged.

In `@packages/gateway/src/heartwood/gateway/_project_config.py`:
- Around line 566-572: In the project configuration parsing flow, preserve the
original input dictionary and assign the migration payload to a separate
migrated variable. Update the subsequent unknown-field and value.get checks to
read from migrated, while keeping the existing MigrationError handling
unchanged.

In `@packages/gateway/src/heartwood/gateway/_project.py`:
- Around line 243-253: Update _migrate_state_marker to compare result.payload
with the current persisted payload and rewrite the state marker whenever they
differ, not only when result.applied_versions is non-empty. Preserve the
existing migration and error handling, ensuring initialize() can repair markers
whose formats map changed without a schema-version migration.

In `@packages/gateway/src/heartwood/gateway/_session_catalog.py`:
- Around line 199-223: Update _unavailable_summary to compute event_count from
the persisted events in session_dir, reusing the same event-reading/counting
mechanism as _catalog_events(store), while preserving the recovery-required
status and metadata handling; remove the hardcoded zero.

In `@packages/persistence/src/heartwood/persistence/_files.py`:
- Around line 227-244: Update the append flow around _sha256_prefix in the
relevant persistence method to avoid hashing the entire existing file: record
the current file size together with a digest of only its last line for journal
recovery validation. Preserve the journal’s ability to prove the file was
unchanged between journaling and appending, and leave the full-record parsing in
_read_objects_locked unchanged.
- Around line 173-203: Update native_file_lock so timeout is supplied to
lock.acquire rather than FileLock construction, keeping the constructor
configuration identical across calls for the same path while preserving the
existing timeout and NativeLockUnavailableError handling.

In `@packages/persistence/tests/test_migrations.py`:
- Around line 206-213: Update the migration test around
PERSISTENCE_MIGRATIONS.migrate to capture the raised MigrationError and
explicitly assert that its message does not contain "must-not-appear-in-errors",
mirroring test_registry_rejects_unsupported_version_without_exposing_payload
while retaining the existing "unsupported fields" match.

---

Nitpick comments:
In `@packages/audit/tests/test_checkpoint.py`:
- Around line 273-276: Update the test around _checkpoint_bundle to retain the
bundle’s generated public key and pass that key to verify_audit_checkpoint
instead of reusing the unrelated public_key fixture. Keep the unexpected-files
assertion unchanged so the test explicitly verifies the bundle with its own key.
- Line 47: Update the deployment_id in the checkpoint test at the visible
deployment configuration to use "generic-research", and ensure the corresponding
deployment identifier at the other referenced checkpoint test location also uses
"generic-research" so all checkpoint fixtures share the neutral identifier.

In `@packages/cli/src/heartwood/cli/__init__.py`:
- Around line 1917-1928: The audit checkpoint CLI currently recomputes the
output path instead of reporting the gateway-resolved destination. Update
SessionGateway.create_audit_checkpoint and the AuditCheckpointVerification
result flow to return or attach the resolved bundle path, then have
_handle_audit_checkpoint print that value in the “Audit checkpoint” line rather
than calling output.expanduser().resolve().

In `@packages/core-adapter/src/heartwood/core_adapter/_state.py`:
- Around line 442-444: Update the empty-session early-return path in
_verified_records to initialize _next_sequence to 0 and _last_audit_hash to the
empty-session audit hash state before returning. Preserve the existing
verification result and return values, ensuring next_sequence() works before
acquire_writer() and stale audit-hash state is cleared.

In `@packages/gateway/src/heartwood/gateway/_openhands_persistence.py`:
- Around line 157-171: Update the filesystem scan in _ensure_compatible_state to
catch OSError from path.lstat() and path.chmod() and re-raise it as
OpenHandsPersistenceError, preserving the original exception as context. Ensure
removed entries and read-only filesystem failures follow the module’s existing
OpenHandsPersistenceError contract.

In `@packages/gateway/tests/test_audit_checkpoint.py`:
- Around line 20-42: Ensure all gateway instances in the affected tests,
including the cases around
test_gateway_creates_and_verifies_checkpoint_outside_project and the tests at
the referenced later locations, are stopped even when assertions fail. Introduce
and use a pytest fixture or contextlib.closing-based cleanup around each
SessionGateway construction, then remove the trailing direct gateway.stop()
calls.

In `@packages/gateway/tests/test_openhands_persistence.py`:
- Around line 175-196: Add an unsupported schema_version case to the
marker_update/message parameters in
test_store_rejects_incompatible_current_markers, using an otherwise current
marker and expecting “schema is unsupported.” Preserve the existing cases and
parameterized test structure.
- Around line 133-154: Update test_markerless_store_rejects_incompatible_state
to supply a failure-specific match pattern to pytest.raises for each
parameterized case, covering the symbolic-link, malformed-event, and index-gap
error messages. Follow the existing message-matching style used by other tests
in this file while preserving the current setup and exception type assertion.

In `@packages/gateway/tests/test_project_config.py`:
- Around line 85-99: Update
test_checked_in_project_config_compatibility_fixture_loads to resolve the
fixture from Path(__file__).resolve().parents[2] / "persistence" / "tests" /
"fixtures" / "project-config-v1.json", removing the redundant packages hop.
Prefer reusing or defining a shared compatibility-fixture resolver, such as the
existing _COMPATIBILITY_FIXTURES convention, so the fixture location is
centralized across package tests.

In `@packages/gateway/tests/test_project.py`:
- Around line 167-176: Add an assertion to
test_project_context_preserves_state_when_migration_fails that the raised
ProjectStateError message does not contain the persisted payload value "private"
or "value", while retaining the existing state-preservation assertion.
- Around line 68-73: Add a short comment immediately before the unsynchronized
first_thread check in pause_first_write, documenting that initialize’s
state_lock_path ordering ensures only one thread reaches the .gitignore branch
and that changing this ordering could cause both threads to wait and deadlock.

In `@packages/persistence/src/heartwood/persistence/_files.py`:
- Around line 116-130: Update append_private_bytes to sync the parent directory
after the file descriptor is durably written and closed, matching the durability
behavior of write_private_bytes_atomic and unlink_durable; ensure the directory
sync occurs for newly created entries without leaving the descriptor open.

In `@packages/persistence/src/heartwood/persistence/_migrations.py`:
- Around line 199-247: Make MigrationRegistry support freezing by adding a
frozen-state guard and calling _require_mutable() at the start of register_kind
and register. After _build_registry() completes, freeze PERSISTENCE_MIGRATIONS
so the process-wide registry cannot be modified, while newly constructed
test-local registries remain mutable.

In `@packages/persistence/tests/test_files.py`:
- Around line 162-195: Update
test_jsonl_recovery_rejects_invalid_journal_or_target to parameterize each
failure mode with its expected AppendRecoveryError message fragment, then assert
that fragment via pytest.raises(match=...) when calling store.recover(). Keep
the existing setup and four failure cases unchanged.
- Around line 269-276: Add a test alongside test_native_lock_failure_is_explicit
that exercises native_file_lock’s timeout path using a forked child process,
since same-process locking is reentrant. Hold the lock in the parent, attempt
acquisition from the child with timeout=0, and assert NativeLockUnavailableError
with the “timed out” message while preserving the existing OSError coverage.

In `@packages/schemas/tests/test_schema_records.py`:
- Around line 164-169: Extend the negative test around
AuditCheckpointStatement.model_validate and AuditCheckpoint validation to cover
both missing rules: construct a statement with invalid retention ordering and
assert ValidationError, then construct a checkpoint with a non-canonical Ed25519
signature and assert ValidationError. Keep the existing terminal-hash and
audit_event_count rejection coverage unchanged.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: b5afe1c8-af73-40b0-926a-88ffeb0312bb

📥 Commits

Reviewing files that changed from the base of the PR and between 261961c and a0dfb9d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (68)
  • AGENTS.md
  • documentation/architecture/sessions-audit.md
  • documentation/architecture/testing.md
  • documentation/contribute/development.md
  • documentation/operate/audit-checkpoints.md
  • documentation/operate/index.md
  • documentation/operate/security.md
  • documentation/reference/cli.md
  • documentation/reference/configuration.md
  • documentation/reference/glossary.md
  • documentation/reference/troubleshooting.md
  • documentation/use/actions-audit.md
  • packages/audit/README.md
  • packages/audit/pyproject.toml
  • packages/audit/src/heartwood/audit/__init__.py
  • packages/audit/src/heartwood/audit/_checkpoint.py
  • packages/audit/src/heartwood/audit/_log.py
  • packages/audit/tests/test_audit_log.py
  • packages/audit/tests/test_checkpoint.py
  • packages/cli/src/heartwood/cli/__init__.py
  • packages/cli/tests/test_cli.py
  • packages/core-adapter/pyproject.toml
  • packages/core-adapter/src/heartwood/core_adapter/_service.py
  • packages/core-adapter/src/heartwood/core_adapter/_state.py
  • packages/core-adapter/tests/test_session_service.py
  • packages/gateway/pyproject.toml
  • packages/gateway/src/heartwood/gateway/__init__.py
  • packages/gateway/src/heartwood/gateway/_gateway.py
  • packages/gateway/src/heartwood/gateway/_openhands_persistence.py
  • packages/gateway/src/heartwood/gateway/_project.py
  • packages/gateway/src/heartwood/gateway/_project_config.py
  • packages/gateway/src/heartwood/gateway/_rest.py
  • packages/gateway/src/heartwood/gateway/_session_catalog.py
  • packages/gateway/src/heartwood/gateway/_skill_settings.py
  • packages/gateway/tests/test_audit_checkpoint.py
  • packages/gateway/tests/test_gateway_contract.py
  • packages/gateway/tests/test_openhands_persistence.py
  • packages/gateway/tests/test_project.py
  • packages/gateway/tests/test_project_config.py
  • packages/gateway/tests/test_session_catalog.py
  • packages/persistence/README.md
  • packages/persistence/pyproject.toml
  • packages/persistence/src/heartwood/persistence/__init__.py
  • packages/persistence/src/heartwood/persistence/_files.py
  • packages/persistence/src/heartwood/persistence/_migrations.py
  • packages/persistence/src/heartwood/persistence/py.typed
  • packages/persistence/tests/fixtures/audit-event-v1.json
  • packages/persistence/tests/fixtures/openhands-state-unversioned.json
  • packages/persistence/tests/fixtures/openhands-state-v1.json
  • packages/persistence/tests/fixtures/project-config-v1.json
  • packages/persistence/tests/fixtures/project-state-v1.json
  • packages/persistence/tests/fixtures/project-state-v2.json
  • packages/persistence/tests/fixtures/session-command-receipt-v1.json
  • packages/persistence/tests/fixtures/session-commit-v1.json
  • packages/persistence/tests/fixtures/session-event-v1.json
  • packages/persistence/tests/fixtures/session-metadata-v1.json
  • packages/persistence/tests/fixtures/session-writer-v1.json
  • packages/persistence/tests/fixtures/skill-metadata-v1.json
  • packages/persistence/tests/test_files.py
  • packages/persistence/tests/test_migrations.py
  • packages/schemas/src/heartwood/schemas/__init__.py
  • packages/schemas/src/heartwood/schemas/_records.py
  • packages/schemas/tests/test_schema_records.py
  • packages/skills/pyproject.toml
  • packages/skills/src/heartwood/skills/_verification.py
  • packages/skills/tests/test_skill_verification.py
  • pyproject.toml
  • zensical.toml

Comment thread documentation/operate/security.md Resolved
Comment thread documentation/reference/glossary.md Resolved
Copilot AI review requested due to automatic review settings August 2, 2026 23:13
@PSchmiedmayer PSchmiedmayer self-assigned this Aug 2, 2026
@PSchmiedmayer PSchmiedmayer added the enhancement New feature or request label Aug 2, 2026
@PSchmiedmayer PSchmiedmayer moved this from Backlog to In review in Heartwood Aug 2, 2026
@PSchmiedmayer PSchmiedmayer added this to the 0.3.0 milestone Aug 2, 2026

Copilot AI left a comment

Copy link
Copy Markdown

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 69 out of 70 changed files in this pull request and generated no new comments.

Suppressed comments (3)

packages/persistence/src/heartwood/persistence/_files.py:226

  • LockedJsonlStore.append_derived reads and JSON-parses the entire JSONL file into a tuple on every append. For long-lived audit/session logs this becomes O(n) memory per append (and tends toward O(n^2) total work across a session), which can become a practical bottleneck.
    packages/persistence/src/heartwood/persistence/_files.py:232
  • append_derived computes prefix_sha256 by re-reading the file from disk (_sha256_prefix) after the file has already been read/parsed in _read_objects_locked. That adds another full file scan per append; for large logs this doubles IO and CPU for every write.
    packages/audit/src/heartwood/audit/_log.py:111
  • AuditLog.append() builds events = tuple(_audit_event(record) for record in records) for the entire history on every append, even though only the current length and tail hashes are needed to derive the next event. This adds a full pydantic validation + migration pass per append and can significantly slow down large audit logs.
        def build(records: tuple[dict[str, object], ...]) -> dict[str, object]:
            events = tuple(_audit_event(record) for record in records)
            if events:
                _verify_tail(events)
                if any(event.session_id != session_id for event in events[-2:]):

Copilot AI review requested due to automatic review settings August 2, 2026 23:23

Copilot AI left a comment

Copy link
Copy Markdown

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 69 out of 70 changed files in this pull request and generated no new comments.

Suppressed comments (2)

packages/audit/src/heartwood/audit/_log.py:112

  • AuditLog.append only checks the last two existing records for session_id mismatches. If older records contain a different session_id (e.g., due to corruption or partial manual edits), append could succeed and further entrench an invalid log until a later full verification fails.

Since events is already fully materialized here, validate the session_id across the entire existing log before appending.

        def build(records: tuple[dict[str, object], ...]) -> dict[str, object]:
            events = tuple(_audit_event(record) for record in records)
            if events:
                _verify_tail(events)
                if any(event.session_id != session_id for event in events[-2:]):
                    raise AuditIntegrityError("audit append session does not match existing log")

packages/skills/src/heartwood/skills/_verification.py:147

  • load_skill_manifest maps filesystem safety failures (e.g., symlinks, non-regular files, invalid UTF-8) into the generic message "metadata.json is invalid JSON" because DurableFileError/OSError are caught alongside JSONDecodeError. That makes the failure mode misleading and can hinder remediation (the file may be valid JSON but an unsafe path).

Consider splitting the exception handling so file safety issues report a "must be a regular UTF-8 file"-style message, and reserve "invalid JSON" for JSONDecodeError.

Copilot AI review requested due to automatic review settings August 2, 2026 23:28

Copilot AI left a comment

Copy link
Copy Markdown

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 69 out of 70 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/audit/src/heartwood/audit/_log.py:121

  • AuditLog.append() currently re-validates (via _audit_event) every existing record on each append (events = tuple(...)). Since LockedJsonlStore.append_derived() already reads/parses the whole file, this adds an extra O(n) Pydantic validation pass per append, leading to O(n²) growth in CPU cost as the audit log grows. You only need the last one or two records to (a) ensure the tail is well-formed and (b) derive the next sequence and previous hash.

Consider validating just the tail records and deriving sequence from len(records), while keeping the same failure modes for an invalid tail or session mismatch.

        def build(records: tuple[dict[str, object], ...]) -> dict[str, object]:
            events = tuple(_audit_event(record) for record in records)
            if events:
                _verify_tail(events)
                if any(event.session_id != session_id for event in events[-2:]):

Copilot AI review requested due to automatic review settings August 3, 2026 04:19

Copilot AI left a comment

Copy link
Copy Markdown

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 82 out of 83 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/schemas/src/heartwood/schemas/_records.py:207

  • base64.b64decode(..., validate=True) can raise ValueError (e.g., non-ASCII input). Currently only binascii.Error is caught, which can surface a less helpful/less consistent validation message than the intended “signature must be canonical Base64”. Catch ValueError too so invalid inputs always fail with the same schema error message.

Copilot AI review requested due to automatic review settings August 3, 2026 04:25

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (9)
packages/audit/src/heartwood/audit/_signer.py (1)

181-188: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Use a dedicated bound for the token length.

validate_checkpoint_signer_token bounds the token with _MAXIMUM_SIGNER_RESPONSE_BYTES. That constant describes the HTTP response envelope, not a credential length. A separate constant makes the limit explicit and prevents an accidental coupling if the response bound changes.

♻️ Proposed refactor
 _MAXIMUM_SIGNER_RESPONSE_BYTES = 64 * 1024
+_MAXIMUM_SIGNER_TOKEN_CHARACTERS = 4096
     if (
-        len(value) > _MAXIMUM_SIGNER_RESPONSE_BYTES
+        len(value) > _MAXIMUM_SIGNER_TOKEN_CHARACTERS
         or re.fullmatch(r"[A-Za-z0-9\-._~+/]+=*", value) is None
     ):
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/src/heartwood/audit/_signer.py` around lines 181 - 188, Update
validate_checkpoint_signer_token to compare token length against a dedicated
signer-token maximum constant rather than _MAXIMUM_SIGNER_RESPONSE_BYTES. Define
the new bound alongside the existing limits with an explicit token-specific
name, while preserving the current character validation and error behavior.
packages/audit/tests/test_checkpoint.py (1)

215-263: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Split the combined failure-mode test.

test_checkpoint_rejects_invalid_input_identity_and_key_material asserts seven unrelated failure modes: session mismatch, missing key, EC private key, wrong trusted key, corrupted audit input, invalid PEM, and a writable public key. A failure in an early assertion hides the later ones. Split the signer key-material cases into a parametrized test and keep the checkpoint input cases separate.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/tests/test_checkpoint.py` around lines 215 - 263, Split
test_checkpoint_rejects_invalid_input_identity_and_key_material into focused
tests: keep session mismatch and corrupted audit input checkpoint cases
together, and move signer key-material failures into a parametrized test
covering missing, EC, invalid PEM, and writable-key scenarios. Keep the wrong
trusted public-key verification case separate, preserving each existing
exception type and match assertion.
packages/audit/src/heartwood/audit/_signer_registry.py (1)

300-306: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Make the endpoint and timeout validation explicit.

Lines 302-306 construct a RemoteCheckpointSigner and discard it. The construction is used only for its validation side effects, and it also allocates a default _UrlLibSignerTransport. Extract the endpoint and timeout checks into a public validation helper in _signer.py, then call that helper here. This also removes the redundant http:// check on line 300, because _validate_signer_endpoint already rejects cleartext endpoints when allow_insecure_loopback is False.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/src/heartwood/audit/_signer_registry.py` around lines 300 -
306, The signer registry should validate configuration without constructing and
discarding a RemoteCheckpointSigner. Add a public validation helper in
_signer.py that checks the endpoint and timeout using the same
allow_insecure_loopback rules, then call it from this registry path with the
profile values; remove the redundant production http:// check and the unused
RemoteCheckpointSigner construction.
packages/gateway/tests/test_project_config.py (1)

230-230: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick win

Assert that clearing removes the audit table from the file.

Line 230 checks the in-memory value after clearing. It does not check the persisted TOML. _config_mapping omits the audit table when signer_profile is None, and content minimization is a stated objective of this change. Add a file-level assertion to lock that behavior.

💚 Proposed addition
     assert store.select_checkpoint_signer(None).audit_settings == AuditCheckpointSettings()
+    cleared = tomllib.loads(project.config_path.read_text(encoding="utf-8"))
+    assert "audit" not in cleared
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/tests/test_project_config.py` at line 230, Extend the test
around store.select_checkpoint_signer(None) to read the persisted TOML
configuration and assert that the audit table is absent after clearing. Keep the
existing in-memory AuditCheckpointSettings assertion and verify the file-level
result reflects _config_mapping’s omission when signer_profile is None.
packages/cli/src/heartwood/cli/__init__.py (4)

2073-2084: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Pass the resolved root to the initializer.

_require_deployment_owned_cli_path validates the resolved path and returns it, but the return value is discarded. initialize_local_checkpoint_signer then receives the unresolved root. If root is relative or contains symlinked parents, the validated path and the created path can differ.

🛡️ Proposed fix
-    _require_deployment_owned_cli_path(project, root, label="local signer directory")
+    resolved_root = _require_deployment_owned_cli_path(
+        project, root, label="local signer directory"
+    )
     setup = initialize_local_checkpoint_signer(
-        directory=root,
+        directory=resolved_root,
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/heartwood/cli/__init__.py` around lines 2073 - 2084, Update
the local signer setup around _require_deployment_owned_cli_path so its
resolved-path return value is assigned back to root, then pass that resolved
root to initialize_local_checkpoint_signer. Preserve the existing directory
selection and validation behavior.

2186-2192: 🔒 Security & Privacy | 🔵 Trivial | ⚡ Quick win

Reuse the gateway boundary check instead of duplicating it.

SessionGateway._deployment_owned_path and SessionGateway._resolve_audit_path in packages/gateway/src/heartwood/gateway/_gateway.py implement the same rule. The gateway version also rejects a symbolic link before resolving. This copy does not, so a symlinked registry, key, or token path passes the CLI check. Extract one shared helper and use it in both places.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/heartwood/cli/__init__.py` around lines 2186 - 2192, Replace
the duplicated project-boundary logic in the CLI path-resolution helper with a
shared helper extracted from the gateway implementations, and use that helper
from both SessionGateway._deployment_owned_path and
SessionGateway._resolve_audit_path. Preserve the gateway behavior of rejecting
symlinks before resolution, while continuing to reject paths inside or equal to
project.root.

2119-2167: 📐 Maintainability & Code Quality | 🔵 Trivial | 🏗️ Heavy lift

Move the signer resolution policy into the audit package.

This block decides signer policy: it requires the development mode, matches the endpoint, resolves the default private-key path, validates the key fingerprint, and requires an authorization token. These are signer-domain rules, not presentation. initialize_local_checkpoint_signer already lives in packages/audit/src/heartwood/audit/_local_signer_setup.py.

Add a resolver in that package that returns the signer, the bind host, and the bind port. Keep the CLI limited to argument handling and printing.

As per coding guidelines: "Keep business rules, labels, setup choices, readiness, and persisted state in the gateway or their owning typed package; CLI, browser, and notebook code should only adapt shared projections to their interaction style."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/heartwood/cli/__init__.py` around lines 2119 - 2167, Move
the signer-policy logic from the CLI block into a resolver alongside
initialize_local_checkpoint_signer in _local_signer_setup.py. Have the resolver
enforce development mode, endpoint matching, private-key defaulting and
ownership, fingerprint validation, and authorization-token availability, then
return the signer, bind host, and bind port; leave
initialize_local_checkpoint_signer or add a focused companion API as
appropriate. Reduce the CLI flow to argument handling, invoking the resolver,
and printing.

Source: Coding guidelines


560-576: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Align the exit code for project-boundary rejections and document the CLI exit codes.

_require_deployment_owned_cli_path raises CheckpointSignerError for a signer path inside the project, while the gateway raises ProjectStateError for the same condition. The CLI catches CheckpointSignerError as 65 and ProjectStateError as 64, so serve-local --trusted-public-key can use a different exit code than checkpoint verify --trusted-public-key for the same boundary failure. Use one exit code, or document the difference. Also, documentation/reference/cli.md has no exit-code guidance, so add it for user-visible failure codes.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/cli/src/heartwood/cli/__init__.py` around lines 560 - 576, Align the
CLI exit-code handling for project-boundary failures raised by
_require_deployment_owned_cli_path and the gateway, so CheckpointSignerError and
ProjectStateError produce the same user-visible code for equivalent conditions.
Update the relevant exception mapping in the CLI handler while preserving
unrelated audit and session codes, then document the resulting exit-code
guidance in documentation/reference/cli.md.
packages/gateway/src/heartwood/gateway/_gateway.py (1)

763-782: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

The signer registry cache never expires.

The registry is validated once and cached for the lifetime of the gateway. A long-lived gateway process keeps a stale trust store after an operator rotates a key or removes a profile from the registry file. Consider revalidating on a file modification time change, or document that operators must restart the gateway after a registry change.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/gateway/src/heartwood/gateway/_gateway.py` around lines 763 - 782,
The _checkpoint_signer_registry method must not retain a stale registry
indefinitely. Add file-change-aware invalidation or revalidation for the
discovered registry source so updates to keys and profiles are picked up, while
preserving the existing override and deployment-ownership validation behavior;
alternatively, explicitly document the required gateway restart behavior if
expiration is intentionally unsupported.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/audit/tests/test_signer_registry.py`:
- Around line 197-226: Set the generated public key file’s permissions
explicitly after public_path.write_bytes, using the same restrictive mode
established by the analogous test in test_checkpoint.py, so
load_checkpoint_signer_registry is independent of the ambient umask.

In `@packages/cli/src/heartwood/cli/__init__.py`:
- Around line 2133-2145: Validate the resolved selected_host in the serve-local
endpoint setup before constructing expected_endpoint or starting the server,
allowing only 127.0.0.1 and ::1. Reject any configured or explicitly supplied
non-loopback host with CheckpointSignerError, while preserving the existing
host/port matching validation.
- Around line 1998-2005: Update the profile-list marker logic in
`_handle_audit_signer` to compare each profile against
`CheckpointSignerRegistry.default_profile` when adding the “deployment default”
marker, rather than using `active`; preserve the existing `selected is None`
condition and active-marker behavior.

---

Nitpick comments:
In `@packages/audit/src/heartwood/audit/_signer_registry.py`:
- Around line 300-306: The signer registry should validate configuration without
constructing and discarding a RemoteCheckpointSigner. Add a public validation
helper in _signer.py that checks the endpoint and timeout using the same
allow_insecure_loopback rules, then call it from this registry path with the
profile values; remove the redundant production http:// check and the unused
RemoteCheckpointSigner construction.

In `@packages/audit/src/heartwood/audit/_signer.py`:
- Around line 181-188: Update validate_checkpoint_signer_token to compare token
length against a dedicated signer-token maximum constant rather than
_MAXIMUM_SIGNER_RESPONSE_BYTES. Define the new bound alongside the existing
limits with an explicit token-specific name, while preserving the current
character validation and error behavior.

In `@packages/audit/tests/test_checkpoint.py`:
- Around line 215-263: Split
test_checkpoint_rejects_invalid_input_identity_and_key_material into focused
tests: keep session mismatch and corrupted audit input checkpoint cases
together, and move signer key-material failures into a parametrized test
covering missing, EC, invalid PEM, and writable-key scenarios. Keep the wrong
trusted public-key verification case separate, preserving each existing
exception type and match assertion.

In `@packages/cli/src/heartwood/cli/__init__.py`:
- Around line 2073-2084: Update the local signer setup around
_require_deployment_owned_cli_path so its resolved-path return value is assigned
back to root, then pass that resolved root to
initialize_local_checkpoint_signer. Preserve the existing directory selection
and validation behavior.
- Around line 2186-2192: Replace the duplicated project-boundary logic in the
CLI path-resolution helper with a shared helper extracted from the gateway
implementations, and use that helper from both
SessionGateway._deployment_owned_path and SessionGateway._resolve_audit_path.
Preserve the gateway behavior of rejecting symlinks before resolution, while
continuing to reject paths inside or equal to project.root.
- Around line 2119-2167: Move the signer-policy logic from the CLI block into a
resolver alongside initialize_local_checkpoint_signer in _local_signer_setup.py.
Have the resolver enforce development mode, endpoint matching, private-key
defaulting and ownership, fingerprint validation, and authorization-token
availability, then return the signer, bind host, and bind port; leave
initialize_local_checkpoint_signer or add a focused companion API as
appropriate. Reduce the CLI flow to argument handling, invoking the resolver,
and printing.
- Around line 560-576: Align the CLI exit-code handling for project-boundary
failures raised by _require_deployment_owned_cli_path and the gateway, so
CheckpointSignerError and ProjectStateError produce the same user-visible code
for equivalent conditions. Update the relevant exception mapping in the CLI
handler while preserving unrelated audit and session codes, then document the
resulting exit-code guidance in documentation/reference/cli.md.

In `@packages/gateway/src/heartwood/gateway/_gateway.py`:
- Around line 763-782: The _checkpoint_signer_registry method must not retain a
stale registry indefinitely. Add file-change-aware invalidation or revalidation
for the discovered registry source so updates to keys and profiles are picked
up, while preserving the existing override and deployment-ownership validation
behavior; alternatively, explicitly document the required gateway restart
behavior if expiration is intentionally unsupported.

In `@packages/gateway/tests/test_project_config.py`:
- Line 230: Extend the test around store.select_checkpoint_signer(None) to read
the persisted TOML configuration and assert that the audit table is absent after
clearing. Keep the existing in-memory AuditCheckpointSettings assertion and
verify the file-level result reflects _config_mapping’s omission when
signer_profile is None.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 89b792af-9100-4d74-b17a-29380e158016

📥 Commits

Reviewing files that changed from the base of the PR and between b895479 and 10ef0cb.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (38)
  • documentation/architecture/sessions-audit.md
  • documentation/architecture/testing.md
  • documentation/operate/audit-checkpoints.md
  • documentation/operate/index.md
  • documentation/operate/platform-integration.md
  • documentation/operate/security.md
  • documentation/reference/cli.md
  • documentation/reference/configuration.md
  • documentation/reference/glossary.md
  • documentation/reference/troubleshooting.md
  • images/generic/scripts/coding_agent_e2e.sh
  • packages/audit/README.md
  • packages/audit/pyproject.toml
  • packages/audit/src/heartwood/audit/__init__.py
  • packages/audit/src/heartwood/audit/_checkpoint.py
  • packages/audit/src/heartwood/audit/_local_signer_setup.py
  • packages/audit/src/heartwood/audit/_signer.py
  • packages/audit/src/heartwood/audit/_signer_registry.py
  • packages/audit/src/heartwood/audit/_signer_service.py
  • packages/audit/tests/test_checkpoint.py
  • packages/audit/tests/test_signer.py
  • packages/audit/tests/test_signer_registry.py
  • packages/audit/tests/test_signer_service.py
  • packages/cli/src/heartwood/cli/__init__.py
  • packages/cli/src/heartwood/cli/_launch.py
  • packages/cli/tests/test_carina_launch_integration.py
  • packages/cli/tests/test_cli.py
  • packages/cli/tests/test_launch.py
  • packages/compliance/tests/test_container_assets.py
  • packages/gateway/src/heartwood/gateway/__init__.py
  • packages/gateway/src/heartwood/gateway/_gateway.py
  • packages/gateway/src/heartwood/gateway/_project_config.py
  • packages/gateway/tests/test_audit_checkpoint.py
  • packages/gateway/tests/test_project.py
  • packages/gateway/tests/test_project_config.py
  • packages/schemas/src/heartwood/schemas/__init__.py
  • packages/schemas/src/heartwood/schemas/_records.py
  • packages/schemas/tests/test_schema_records.py
🚧 Files skipped from review as they are similar to previous changes (11)
  • packages/audit/pyproject.toml
  • documentation/reference/troubleshooting.md
  • documentation/architecture/sessions-audit.md
  • documentation/operate/index.md
  • packages/audit/README.md
  • packages/schemas/src/heartwood/schemas/init.py
  • packages/schemas/tests/test_schema_records.py
  • documentation/reference/cli.md
  • documentation/architecture/testing.md
  • documentation/reference/glossary.md
  • packages/gateway/tests/test_project.py

Copilot AI left a comment

Copy link
Copy Markdown

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 82 out of 83 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/audit/src/heartwood/audit/_log.py:112

  • AuditLog.append() only calls _verify_tail(events) before deriving the next record. This means corrupted earlier records (e.g., a sequence gap or broken previous_event_hash before the tail) won’t be detected at append time, and a new record can be appended onto an already-invalid chain. Since this method is the write boundary for the log, it should verify the full chain (sequence + link + hash) under the lock before appending.
            if events:
                _verify_tail(events)
                if any(event.session_id != session_id for event in events):
                    raise AuditIntegrityError("audit append session does not match existing log")

Copilot AI review requested due to automatic review settings August 3, 2026 04:31

Copilot AI left a comment

Copy link
Copy Markdown

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 83 out of 84 changed files in this pull request and generated 1 comment.

Copilot AI review requested due to automatic review settings August 3, 2026 05:20

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

♻️ Duplicate comments (1)
packages/audit/tests/test_signer_registry.py (1)

242-256: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick win

Set an explicit mode on the generated public key file.

public_path.write_bytes at Lines 249-254 uses the default mode (0o666 masked by the process umask). Every other file write in this test explicitly calls chmod(0o600) afterward (Lines 128, 133, 141, 216, 277, 292), but this one does not. If the ambient umask permits it, load_checkpoint_signer_registry can fail non-deterministically because _key_metadata in packages/audit/src/heartwood/audit/_signer.py rejects a public key writable by other users. This was flagged in a prior review round and the code is unchanged.

🐛 Proposed fix
     public_path.write_bytes(
         public_key.public_bytes(
             encoding=serialization.Encoding.PEM,
             format=serialization.PublicFormat.SubjectPublicKeyInfo,
         )
     )
+    public_path.chmod(0o644)
     registry_path = operator / "checkpoint-signers.toml"
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/audit/tests/test_signer_registry.py` around lines 242 - 256, Set the
generated public key file permissions explicitly after public_path.write_bytes
in
test_production_registry_supports_p256_without_project_or_private_key_material,
using the same restrictive mode as the other test fixture files so
load_checkpoint_signer_registry behaves deterministically regardless of the
process umask.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/audit/tests/test_signer_registry.py`:
- Around line 242-256: Set the generated public key file permissions explicitly
after public_path.write_bytes in
test_production_registry_supports_p256_without_project_or_private_key_material,
using the same restrictive mode as the other test fixture files so
load_checkpoint_signer_registry behaves deterministically regardless of the
process umask.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 7c4deba6-9d3f-40d2-9480-e88d11015c13

📥 Commits

Reviewing files that changed from the base of the PR and between 10ef0cb and 76e91b2.

📒 Files selected for processing (3)
  • packages/audit/tests/test_signer_registry.py
  • packages/schemas/src/heartwood/schemas/_records.py
  • packages/webui/src/e2e/app.spec.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/schemas/src/heartwood/schemas/_records.py

Copilot AI left a comment

Copy link
Copy Markdown

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 83 out of 84 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 3, 2026 05:28

Copilot AI left a comment

Copy link
Copy Markdown

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 83 out of 84 changed files in this pull request and generated no new comments.

Copilot AI review requested due to automatic review settings August 3, 2026 05:33

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@PSchmiedmayer
PSchmiedmayer merged commit c1ae61f into main Aug 3, 2026
25 checks passed
@PSchmiedmayer
PSchmiedmayer deleted the feature/audit-persistence-hardening branch August 3, 2026 14:17
@github-project-automation github-project-automation Bot moved this from In review to Done in Heartwood Aug 3, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Aug 7, 2026
1 task done
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Harden Audit Persistence And Schema Compatibility

3 participants